From 4208f8ca893c829d2f361e0f82a43c80e647ff42 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 29 Sep 2026 10:49:49 +0800 Subject: [PATCH] =?UTF-8?q?[CMS-REWORK]=20#1=20=E9=A6=96=E9=A1=B5=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E7=BC=96=E8=BE=91=E5=99=A8=E6=95=B0=E6=8D=AE=E6=A0=8F?= =?UTF-8?q?=E7=9B=AE=E4=B8=BA=E7=A9=BA:=20=E6=A0=8F=E7=9B=AE=E6=A0=91?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=B8=8E=E9=A6=96=E7=94=A8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=BC=96=E6=8E=92=E8=A7=A3=E8=80=A6(=E6=8C=82=E8=BD=BD+siteId?= =?UTF-8?q?=20watch=E5=8F=8C=E9=80=9A=E9=81=93=E5=B9=82=E7=AD=89=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/homeLayoutEditor.spec.ts | 55 ++++++++++++++++++- src/views/cms/homeLayout/HomeLayoutEditor.vue | 30 ++++++++-- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/__tests__/homeLayoutEditor.spec.ts b/src/__tests__/homeLayoutEditor.spec.ts index 9cf37e6..4e89389 100644 --- a/src/__tests__/homeLayoutEditor.spec.ts +++ b/src/__tests__/homeLayoutEditor.spec.ts @@ -49,7 +49,10 @@ const TREE: CategoryTreeNode[] = [ { id: 12, name: "媒体聚焦", type: 1, children: [] }, ] 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> }, +) { const el = document.createElement("div"); document.body.appendChild(el); const model = ref(configJson); @@ -58,7 +61,7 @@ async function mountEditor(configJson: string, opts?: { siteId?: number; portalU render: () => h(HomeLayoutEditor, { modelValue: model.value, - siteId: opts?.siteId, + siteId: opts?.siteIdRef ? opts.siteIdRef.value : opts?.siteId, portalUrl: opts?.portalUrl, "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(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(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)", () => { it("stats_band:统计项三列编辑器(名称/数值/单位)渲染并可增行", async () => { const config = JSON.stringify({ diff --git a/src/views/cms/homeLayout/HomeLayoutEditor.vue b/src/views/cms/homeLayout/HomeLayoutEditor.vue index 75a9168..11f6708 100644 --- a/src/views/cms/homeLayout/HomeLayoutEditor.vue +++ b/src/views/cms/homeLayout/HomeLayoutEditor.vue @@ -92,24 +92,36 @@ watch( { immediate: true }, ); +/** 已完成树加载的站点 id(幂等标记;null=未加载或上次失败可重试) */ +let loadedTreeSiteId: number | null = null; + async function loadTree() { if (!props.siteId) return; + // REWORK-DECISION: 幂等保护(返工清单 #1-3)——同一 siteId 已加载过不重复请求,避免 onMounted + watch 双触发; + // 请求失败则清标记允许后续重试 + if (loadedTreeSiteId === props.siteId) return; + loadedTreeSiteId = props.siteId; treeLoading.value = true; try { tree.value = (await categoryApi.tree(props.siteId).send()) || []; } catch { tree.value = []; + loadedTreeSiteId = null; } finally { treeLoading.value = false; } } -/** F2 首用自动编排:进入编辑器时无 home_layout 键(回落态)且站点有列表型栏目 → 静默生成初始序列 */ -async function autoGenOnFirstUse() { - if (parseHomeLayout(props.modelValue) || !props.siteId) { - return; - } +/** + * 站点上下文就绪后的数据初始化(docs/20 返工 #1): + * ① 只要 siteId 有值就无条件加载栏目树(「数据栏目/人物分组」数据源),与「是否首用自动编排」解耦; + * ② 首用自动编排(D-4)复用已加载的树,不再独立承担加载职责。 + * siteId 由父级异步种入(晚于本组件挂载)时由 watch 补跑。 + */ +async function initSiteData() { + if (!props.siteId) return; await loadTree(); + if (parseHomeLayout(props.modelValue)) return; if (!blocks.value.length && flat.value.length) { blocks.value = genFromCategories(tree.value, props.modelValue).blocks; selectedId.value = null; @@ -117,7 +129,13 @@ async function autoGenOnFirstUse() { } } -onMounted(autoGenOnFirstUse); +onMounted(initSiteData); +watch( + () => props.siteId, + id => { + if (id) initSiteData(); + }, +); // ===== 中栏:实时预览(iframe + 视口切换 + postMessage 近似模拟 + 手动刷新兜底)=====