From 5e8183c220d01ce5b164b973f3968a9f8feaaf53 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 28 Aug 2026 23:22:04 +0800 Subject: [PATCH 1/3] feat(admin): family-folded left tree, toolbar regroup, sync log humanize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 视觉走查驱动的后台优化(feature/admin-ui-polish-dev,确认后再合并): - 左树(国家/品类模式)同款多配置折叠为一行族组节点:代表名 + "N 个配置" 徽标,展开看各行;locateInLeftTree 沿父链展开保证定位有效;国家模式 拖拽排序适配族嵌套(同容器内排序,按折叠后展示顺序保序重分配优先级) - 工具栏分区重排:模式切换(品类/国家/全局)移至最左,筛选组居中, 新增自定义商品靠右,消除 1680 宽下的折行错乱 - 商品行属性小字/国家徽标颜色加深提升对比度 - 右树原产品行加拖拽提示 title - 数据同步日志人话化:inserted/updated/total/delisted 等报文渲染为 "新增 0 · 更新 536 · 共计 536",原始报文进 tooltip - 计划文档 plans/feature/admin-ui-polish-feature.md(含一项误报修正: 右树 29/3 计数为截图裁切误读,实际 29/33 无 bug) 验证:admin typecheck + 22/22 + 构建绿;浏览器实机复拍国家模式折叠、 族组展开、同步页文案均生效 --- apps/admin/src/views/goods/GoodsView.vue | 127 +++++++++++++++++++---- apps/admin/src/views/sync/SyncView.vue | 119 ++++++++++++--------- plans/feature/admin-ui-polish-feature.md | 28 +++++ 3 files changed, 203 insertions(+), 71 deletions(-) create mode 100644 plans/feature/admin-ui-polish-feature.md diff --git a/apps/admin/src/views/goods/GoodsView.vue b/apps/admin/src/views/goods/GoodsView.vue index de1f200..0f607de 100644 --- a/apps/admin/src/views/goods/GoodsView.vue +++ b/apps/admin/src/views/goods/GoodsView.vue @@ -214,11 +214,40 @@ const filteredGoods = computed(() => { return sorted }) +/** 左树同族折叠:同款(同 familyId)多条官网商品折叠为一个"族组"节点,children 为各行明细 */ +function foldByFamily(nodes: any[]): any[] { + const out: any[] = [] + const byFamily = new Map() + for (const n of nodes) { + const fid = n.raw?.originGood?.family?.familyId + ? String(n.raw.originGood.family.familyId) + : null + if (!fid) { out.push(n); continue } + if (!byFamily.has(fid)) { + const bucket: any[] = [] + byFamily.set(fid, bucket) + out.push(bucket) + } + ;(byFamily.get(fid) as any[]).push(n) + } + return out.map(n => + Array.isArray(n) + ? { + id: 'fam-' + String(n[0].raw.originGood.family.familyId), + label: n[0].goodName, + isFamilyGroup: true, + goodCount: n.length, + children: n, + } + : n, + ) +} + function mapCatNodes(nodes: CategoryTree[], goods: Good[]): any[] { return nodes.map(n => { const childNodes = n.children?.length ? mapCatNodes(n.children, goods) : [] const myGoods = goods.filter(g => g.categoryId === n.id).map(goodToNode) - const children = [...childNodes, ...myGoods] + const children = [...childNodes, ...foldByFamily(myGoods)] const goodCount = children.reduce((sum: number, c: any) => sum + (c.goodCount ?? (c.isGood ? 1 : 0)), 0) return { id: n.id, @@ -247,7 +276,7 @@ const leftTreeData = computed(() => { isCat: true, raw: c, goodCount: cGoods.length, - children: cGoods.map(goodToNode), + children: foldByFamily(cGoods.map(goodToNode)), } }) } @@ -575,6 +604,12 @@ function locateInLeftTree(originGoodId: string) { const node = tree.getNode(keyId) if (node) node.expanded = true } + // 族折叠后商品嵌套在族组节点下:沿父链全部展开(两种模式通用) + let anc = tree.getNode(targetKey)?.parent as any + while (anc?.data) { + anc.expanded = true + anc = anc.parent + } setTimeout(() => { tree.setCurrentKey(targetKey) // 闪烁高亮定位行(2.4s 后自动消失) @@ -703,6 +738,28 @@ function onTreeGoodDragLeave(_event: DragEvent, data: any) { if (treeDragOver.value?.id === data.goodId) treeDragOver.value = null } +/** 国家分组内:找商品所在容器数组(国家根 or 族组 children) */ +function findContainingArray(group: any, goodId: string): any[] | null { + for (const c of group.children ?? []) { + if (c.isFamilyGroup) { + if ((c.children ?? []).some((g: any) => g.goodId === goodId)) return c.children + } else if (c.isGood && c.goodId === goodId) { + return group.children + } + } + return null +} + +/** 国家分组内商品的折叠后展示顺序(族组按位置展开计入) */ +function flattenCountryGoods(group: any): any[] { + const out: any[] = [] + for (const c of group.children ?? []) { + if (c.isFamilyGroup) out.push(...(c.children ?? [])) + else if (c.isGood) out.push(c) + } + return out +} + async function onTreeGoodDrop(event: DragEvent, data: any) { event.preventDefault() event.stopPropagation() @@ -713,25 +770,30 @@ async function onTreeGoodDrop(event: DragEvent, data: any) { if (mode.value !== 'country' || !draggedId || !target) return if (target.id !== data.goodId || draggedId === data.goodId) return - // 目标所在国家分组的商品(新顺序) - const group = leftTreeData.value.find((c: any) => - (c.children ?? []).some((g: any) => g.goodId === target.id), - ) + // 目标所在国家分组 + const group = leftTreeData.value.find((c: any) => findContainingArray(c, target.id)) if (!group) return - const nodes = [...group.children] + const fromArr = findContainingArray(group, draggedId) + const toArr = findContainingArray(group, target.id) + if (!fromArr || fromArr !== toArr) { + ElMessage.info('暂只支持同一分组(或同一族)内拖拽排序') + return + } + const nodes = [...fromArr] const fromIdx = nodes.findIndex(n => n.goodId === draggedId) - if (fromIdx === -1) return // 跨国拖动不支持(商品归属固定国家) const [moved] = nodes.splice(fromIdx, 1) let insertIdx = nodes.findIndex(n => n.goodId === target.id) if (target.pos === 'after') insertIdx++ nodes.splice(insertIdx, 0, moved) + fromArr.splice(0, fromArr.length, ...nodes) - // 保序重排:该国家现有优先级集合不变,按新顺序重新分配(尽量少扰动全局/品类排序) - const priorities = nodes.map(n => Number(n.priority || 0)).sort((a, b) => b - a) + // 保序重排:该国家现有优先级集合不变,按折叠后展示顺序重新分配(尽量少扰动全局/品类排序) + const flat = flattenCountryGoods(group) + const priorities = flat.map(n => Number(n.priority || 0)).sort((a, b) => b - a) moveTopLoading.value = 'drag' try { await goodsApi.batchUpdatePriority({ - items: nodes.map((n, i) => ({ id: Number(n.goodId), priority: priorities[i] ?? 0 })), + items: flat.map((n, i) => ({ id: Number(n.goodId), priority: priorities[i] ?? 0 })), }) ElMessage.success('排序已保存') await refreshLeftTree() @@ -972,6 +1034,11 @@ onMounted(() => loadAll())
+ + 品类 + 国家 + 全局 + @@ -996,14 +1063,8 @@ onMounted(() => loadAll()) 搜索 - 新增自定义商品 -
- - 品类 - 国家 - 全局 - + 新增自定义商品
@@ -1046,6 +1107,11 @@ onMounted(() => loadAll())
+ +
+ {{ cleanLinkName(data.label) }} + {{ data.goodCount }} 个配置 +
loadAll()) :expand-on-click-node="true" >