[CMS-REWORK] #1 首页布局编辑器数据栏目为空: 栏目树加载与首用自动编排解耦(挂载+siteId watch双通道幂等加载)
This commit is contained in:
@@ -49,7 +49,10 @@ const TREE: CategoryTreeNode[] = [
|
|||||||
{ id: 12, name: "媒体聚焦", type: 1, children: [] },
|
{ id: 12, name: "媒体聚焦", type: 1, children: [] },
|
||||||
] as unknown as CategoryTreeNode[];
|
] as unknown as CategoryTreeNode[];
|
||||||
|
|
||||||
async function mountEditor(configJson: string, opts?: { siteId?: number; portalUrl?: string }) {
|
async function mountEditor(
|
||||||
|
configJson: string,
|
||||||
|
opts?: { siteId?: number; portalUrl?: string; siteIdRef?: ReturnType<typeof ref<number | undefined>> },
|
||||||
|
) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
const model = ref(configJson);
|
const model = ref(configJson);
|
||||||
@@ -58,7 +61,7 @@ async function mountEditor(configJson: string, opts?: { siteId?: number; portalU
|
|||||||
render: () =>
|
render: () =>
|
||||||
h(HomeLayoutEditor, {
|
h(HomeLayoutEditor, {
|
||||||
modelValue: model.value,
|
modelValue: model.value,
|
||||||
siteId: opts?.siteId,
|
siteId: opts?.siteIdRef ? opts.siteIdRef.value : opts?.siteId,
|
||||||
portalUrl: opts?.portalUrl,
|
portalUrl: opts?.portalUrl,
|
||||||
"onUpdate:modelValue": (v: string) => (model.value = v),
|
"onUpdate:modelValue": (v: string) => (model.value = v),
|
||||||
}),
|
}),
|
||||||
@@ -237,6 +240,54 @@ describe("HomeLayoutEditor - F2 三栏 + 预览 + 缩略图 + 自动编排(doc
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("HomeLayoutEditor - 栏目树加载与首用编排解耦(docs/20 返工 #1)", () => {
|
||||||
|
it("已有布局 + 有列表栏目 + siteId:栏目树仍无条件加载,数据栏目可勾选", async () => {
|
||||||
|
treeMock.send.mockResolvedValue(TREE);
|
||||||
|
const config = JSON.stringify({
|
||||||
|
home_layout: { version: 1, blocks: [{ id: "a1", type: "article_list", categoryIds: [11], style: "card", size: 8 }] },
|
||||||
|
});
|
||||||
|
const { el, model } = await mountEditor(config, { siteId: 1 });
|
||||||
|
// 树加载发生(幂等标记下 treeMock 恰好 1 次调用)
|
||||||
|
expect(treeMock.send).toHaveBeenCalledTimes(1);
|
||||||
|
(el.querySelector(".hle-row") as HTMLElement).click();
|
||||||
|
await settle();
|
||||||
|
const cats = fieldOf(el as HTMLElement, "数据栏目")!;
|
||||||
|
expect(cats).not.toBeNull();
|
||||||
|
expect(cats.querySelectorAll(".hle-cat").length).toBe(2);
|
||||||
|
expect(cats.textContent).toContain("校园时讯");
|
||||||
|
expect(cats.textContent).toContain("媒体聚焦");
|
||||||
|
// 已有布局不被自动编排改写
|
||||||
|
expect(JSON.parse(model.value).home_layout.blocks[0].id).toBe("a1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("siteId 晚于挂载到位(父级异步种入):watch 补跑加载,数据栏目可用", async () => {
|
||||||
|
treeMock.send.mockResolvedValue(TREE);
|
||||||
|
const siteIdRef = ref<number | undefined>(undefined);
|
||||||
|
const config = JSON.stringify({
|
||||||
|
home_layout: { version: 1, blocks: [{ id: "a1", type: "article_list", categoryIds: [], style: "card", size: 8 }] },
|
||||||
|
});
|
||||||
|
const { el } = await mountEditor(config, { siteIdRef });
|
||||||
|
await settle();
|
||||||
|
// siteId 未到位时不请求
|
||||||
|
expect(treeMock.send).not.toHaveBeenCalled();
|
||||||
|
siteIdRef.value = 1;
|
||||||
|
await settle(6);
|
||||||
|
expect(treeMock.send).toHaveBeenCalledTimes(1);
|
||||||
|
(el.querySelector(".hle-row") as HTMLElement).click();
|
||||||
|
await settle();
|
||||||
|
const cats = fieldOf(el as HTMLElement, "数据栏目")!;
|
||||||
|
expect(cats.querySelectorAll(".hle-cat").length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("watch + onMounted 双触发下幂等:同 siteId 只请求一次", async () => {
|
||||||
|
treeMock.send.mockResolvedValue(TREE);
|
||||||
|
const siteIdRef = ref<number | undefined>(1);
|
||||||
|
await mountEditor(JSON.stringify({ logo: "/a.png" }), { siteIdRef });
|
||||||
|
await settle(6);
|
||||||
|
expect(treeMock.send).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("HomeLayoutEditor - D 组件属性面板(docs/20 D-1/D-8)", () => {
|
describe("HomeLayoutEditor - D 组件属性面板(docs/20 D-1/D-8)", () => {
|
||||||
it("stats_band:统计项三列编辑器(名称/数值/单位)渲染并可增行", async () => {
|
it("stats_band:统计项三列编辑器(名称/数值/单位)渲染并可增行", async () => {
|
||||||
const config = JSON.stringify({
|
const config = JSON.stringify({
|
||||||
|
|||||||
@@ -92,24 +92,36 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** 已完成树加载的站点 id(幂等标记;null=未加载或上次失败可重试) */
|
||||||
|
let loadedTreeSiteId: number | null = null;
|
||||||
|
|
||||||
async function loadTree() {
|
async function loadTree() {
|
||||||
if (!props.siteId) return;
|
if (!props.siteId) return;
|
||||||
|
// REWORK-DECISION: 幂等保护(返工清单 #1-3)——同一 siteId 已加载过不重复请求,避免 onMounted + watch 双触发;
|
||||||
|
// 请求失败则清标记允许后续重试
|
||||||
|
if (loadedTreeSiteId === props.siteId) return;
|
||||||
|
loadedTreeSiteId = props.siteId;
|
||||||
treeLoading.value = true;
|
treeLoading.value = true;
|
||||||
try {
|
try {
|
||||||
tree.value = (await categoryApi.tree(props.siteId).send()) || [];
|
tree.value = (await categoryApi.tree(props.siteId).send()) || [];
|
||||||
} catch {
|
} catch {
|
||||||
tree.value = [];
|
tree.value = [];
|
||||||
|
loadedTreeSiteId = null;
|
||||||
} finally {
|
} finally {
|
||||||
treeLoading.value = false;
|
treeLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** F2 首用自动编排:进入编辑器时无 home_layout 键(回落态)且站点有列表型栏目 → 静默生成初始序列 */
|
/**
|
||||||
async function autoGenOnFirstUse() {
|
* 站点上下文就绪后的数据初始化(docs/20 返工 #1):
|
||||||
if (parseHomeLayout(props.modelValue) || !props.siteId) {
|
* ① 只要 siteId 有值就无条件加载栏目树(「数据栏目/人物分组」数据源),与「是否首用自动编排」解耦;
|
||||||
return;
|
* ② 首用自动编排(D-4)复用已加载的树,不再独立承担加载职责。
|
||||||
}
|
* siteId 由父级异步种入(晚于本组件挂载)时由 watch 补跑。
|
||||||
|
*/
|
||||||
|
async function initSiteData() {
|
||||||
|
if (!props.siteId) return;
|
||||||
await loadTree();
|
await loadTree();
|
||||||
|
if (parseHomeLayout(props.modelValue)) return;
|
||||||
if (!blocks.value.length && flat.value.length) {
|
if (!blocks.value.length && flat.value.length) {
|
||||||
blocks.value = genFromCategories(tree.value, props.modelValue).blocks;
|
blocks.value = genFromCategories(tree.value, props.modelValue).blocks;
|
||||||
selectedId.value = null;
|
selectedId.value = null;
|
||||||
@@ -117,7 +129,13 @@ async function autoGenOnFirstUse() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(autoGenOnFirstUse);
|
onMounted(initSiteData);
|
||||||
|
watch(
|
||||||
|
() => props.siteId,
|
||||||
|
id => {
|
||||||
|
if (id) initSiteData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ===== 中栏:实时预览(iframe + 视口切换 + postMessage 近似模拟 + 手动刷新兜底)=====
|
// ===== 中栏:实时预览(iframe + 视口切换 + postMessage 近似模拟 + 手动刷新兜底)=====
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user