From f28f51155fbd39426233553190ce480d8ff102e9 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 28 Aug 2026 16:55:07 +0800 Subject: [PATCH] feat(goods): per-member detail tabs in edit dialog; revert right-tree to raw names; split configured/family badges; add GET /origin-goods/:id --- apps/admin/src/api/origin-goods.ts | 5 + apps/admin/src/views/goods/GoodsView.vue | 33 +- .../goods/components/GoodsEditDialog.vue | 305 ++++++++++-------- .../origin-goods/origin-goods.controller.ts | 6 + .../origin-goods/origin-goods.service.spec.ts | 14 + .../src/origin-goods/origin-goods.service.ts | 48 +++ docs/references/product-center.md | 21 +- 7 files changed, 281 insertions(+), 151 deletions(-) diff --git a/apps/admin/src/api/origin-goods.ts b/apps/admin/src/api/origin-goods.ts index dd1be73..fdb019d 100644 --- a/apps/admin/src/api/origin-goods.ts +++ b/apps/admin/src/api/origin-goods.ts @@ -10,6 +10,11 @@ export const originGoodsApi = { return request.get>('/origin-goods', { params }) }, + // 单链接详情(detail + variants,成员展开面板用) + getOriginGood: (id: string) => { + return request.get>(`/origin-goods/${id}`) + }, + getTags: (id: string) => { return request.get(`/origin-goods/${id}/tags`) }, diff --git a/apps/admin/src/views/goods/GoodsView.vue b/apps/admin/src/views/goods/GoodsView.vue index c5d387b..85a1274 100644 --- a/apps/admin/src/views/goods/GoodsView.vue +++ b/apps/admin/src/views/goods/GoodsView.vue @@ -286,12 +286,12 @@ function buildRightTree(tree: OriginGoodsTreeResponse) { } } const roots = tree.tree.map(mapCat) - // 已配置数按族计入:链接成族(或已配置商品)即视为已配置,后序汇总到各级分类 + // 已配置数 = 已配置为官网商品的链接(成族但未配置不算) function fillConfiguredEff(nodes: any[]): number { let count = 0 for (const n of nodes) { if (n.isOG) { - n.configuredEff = n.configuredCount > 0 || n.familyId ? 1 : 0 + n.configuredEff = n.configuredCount > 0 ? 1 : 0 } else { n.configuredEff = fillConfiguredEff(n.children ?? []) } @@ -305,12 +305,8 @@ function buildRightTree(tree: OriginGoodsTreeResponse) { function rightFilterNode(_value: string, data: any) { if (data.isOG) { - // 「仅未配置」同样把族内链接视为已配置 - if (showUnconfiguredOnly.value && (data.configuredCount > 0 || data.familyId)) return false - if (searchKeyword.value) { - const kw = searchKeyword.value - if (!data.label.includes(kw) && !cleanLinkName(data.label).includes(kw)) return false - } + if (showUnconfiguredOnly.value && data.configuredCount > 0) return false + if (searchKeyword.value && !data.label.includes(searchKeyword.value)) return false return true } return true @@ -519,13 +515,14 @@ function locateInRightTree(originGoodId: string) { const locateFlashGoodId = ref('') function locateInLeftTree(originGoodId: string) { - // 链接 → 商品:优先精确匹配该链接配置的商品,其次同族商品(族内任一链接都可定位) + // 链接 → 商品:主链接精确匹配 → 同族商品 → 旧副源合并商品 const ogNode = findOgNodeById(originGoodId) const familyId = ogNode?.familyId ?? null const good = allGoods.value.find(g => String(g.originGoodId) === String(originGoodId)) ?? (familyId ? allGoods.value.find(g => g.originGood?.family?.familyId === familyId) : null) + ?? allGoods.value.find(g => (g.mergedOriginGoods ?? []).some(m => String(m.id) === String(originGoodId))) if (!good) { ElMessage.warning('该原产品尚未配置到官网') return @@ -1049,12 +1046,17 @@ onMounted(() => loadAll())
- {{ cleanLinkName(data.label) }} + {{ data.label }} 已配置{{ data.configuredCount > 1 ? ' ' + data.configuredCount : '' }} + 成族 loadAll()) @click.stop="handleSyncOriginDetail(data)" /> 配置 @@ -1453,6 +1455,9 @@ onMounted(() => loadAll()) .og-badge--ok { color: #67c23a; background: #f0f9eb; } +.og-badge--family { + color: #337ecc; background: #ecf5ff; +} .og-badge--warn { color: #ff6800; background: #fff2e8; } diff --git a/apps/admin/src/views/goods/components/GoodsEditDialog.vue b/apps/admin/src/views/goods/components/GoodsEditDialog.vue index 1d31b16..0303ee2 100644 --- a/apps/admin/src/views/goods/components/GoodsEditDialog.vue +++ b/apps/admin/src/views/goods/components/GoodsEditDialog.vue @@ -31,30 +31,15 @@ const emit = defineEmits<{ const editLoading = ref(false) const editDetailLoading = ref(false) -const detailSyncing = ref(false) +/** 正在同步详情的成员行 id */ +const detailSyncing = ref('') const editGood = ref(null) const editForm = ref({ id: '', goodName: '', goodImage: '', countryId: '', cascaderCategory: [] as string[], categoryId: '', positionId: '', }) -const editOriginDetail = computed(() => (editGood.value as GoodDetail | null)?.originDetail ?? null) -const editVariants = computed(() => (editGood.value as GoodDetail | null)?.variants ?? []) -const editSizeColumns = computed(() => editOriginDetail.value?.sizeChart?.columns ?? []) -const editSizeRows = computed(() => { - const rows = editOriginDetail.value?.sizeChart?.rows ?? [] - return rows.map((row: any) => ({ - ...row, - ...(row.measurements ?? []).reduce((out: Record, item: any) => { - out[item.key] = item.cm ?? '-' - return out - }, {}), - })) -}) -const editPackageRows = computed(() => editOriginDetail.value?.packageSpecs?.rows ?? []) const editIsCustom = computed(() => editGood.value?.originGood?.isCustom === true) -/** 商品自身链接的定价维度(SKU 表列) */ -const editLinkDims = computed(() => linkDims(editGood.value?.originGood?.goodName ?? null)) const categoryCascader = computed(() => buildCascader(props.categories)) // ─── 自定义商品内容编辑 ─── @@ -176,10 +161,70 @@ function toggleMemberExpand(key: string) { else { next.add(key) loadMemberPrices(key) + ensureMemberDetail(key) } expandedMemberKeys.value = next } +// ─── 成员详情(懒加载:GET /origin-goods/:id) ─── +const memberDetailCache = ref>({}) + +function memberDetail(id: string) { + return memberDetailCache.value[id] +} + +async function ensureMemberDetail(ogId: string) { + const row = editFamilyMembers.value.find((m) => m.id === ogId) + if (!row || row.source !== 'SDS' || memberDetailCache.value[ogId] !== undefined) return + memberDetailCache.value[ogId] = { loading: true } + try { + memberDetailCache.value[ogId] = await originGoodsApi.getOriginGood(ogId) + } catch { + memberDetailCache.value[ogId] = null + } +} + +function memberSizeColumns(id: string) { + return memberDetail(id)?.detail?.sizeChart?.columns ?? [] +} + +function memberSizeRows(id: string) { + const rows = memberDetail(id)?.detail?.sizeChart?.rows ?? [] + return rows.map((row: any) => ({ + ...row, + ...(row.measurements ?? []).reduce((out: Record, item: any) => { + out[item.key] = item.cm ?? '-' + return out + }, {}), + })) +} + +function memberPackageRows(id: string) { + return memberDetail(id)?.detail?.packageSpecs?.rows ?? [] +} + +function memberVariants(id: string) { + return memberDetail(id)?.variants ?? [] +} + +function memberDims(id: string) { + const row = editFamilyMembers.value.find((m) => m.id === id) + return linkDims(row?.goodName ?? null) +} + +async function handleSyncMemberDetail(row: { id: string; sdsGoodId: string }) { + if (!row.sdsGoodId || detailSyncing.value) return + detailSyncing.value = row.id + try { + const result = await syncApi.syncOneProductDetail(row.sdsGoodId) + memberDetailCache.value[row.id] = await originGoodsApi.getOriginGood(row.id) + ElMessage.success(`详情同步完成,共 ${result.variants} 个 SKU`) + emit('changed') + } catch (error: any) { + ElMessage.error(error?.response?.data?.message || '商品详情同步失败') + } finally { detailSyncing.value = '' } +} + /** 该链接在族价格矩阵中的格子(craft/logistics 即该链接的标签维度) */ function loadMemberPrices(ogId: string) { const row = editFamilyMembers.value.find((m) => m.id === ogId) @@ -406,22 +451,6 @@ function onEditCascaderChange(val: any) { editForm.value.categoryId = path.length ? String(path[path.length - 1]) : '' } -async function handleSyncOneDetail() { - if (editIsCustom.value) return - const sdsGoodId = editGood.value?.originGood?.sdsGoodId - if (!sdsGoodId) return - detailSyncing.value = true - try { - const result = await syncApi.syncOneProductDetail(sdsGoodId) - const detail = await goodsApi.getGoodById(editGood.value!.id) - applyGood(detail) - ElMessage.success(`详情同步完成,共 ${result.variants} 个 SKU`) - emit('changed') - } catch (error: any) { - ElMessage.error(error?.response?.data?.message || '商品详情同步失败') - } finally { detailSyncing.value = false } -} - async function handleEditSubmit() { let customPayload: any = null if (editIsCustom.value) { @@ -564,28 +593,21 @@ async function handleDeleteGood() { 工艺位置{{ row.craftLabel || '-' }} 仓库{{ row.warehouseLabel || '-' }}
-
- 标签 - - - - - - 保存标签 - 恢复自动 - 同步详情 -
+
+ 标签 + + + + + + 保存标签 + 恢复自动 +
尺码 × 颜色 → 价格 @@ -612,6 +634,71 @@ async function handleDeleteGood() {
+
+
详情加载中…
+
+ 尚未同步详情 + 同步详情 +
+ + + + {{ memberDetail(row.id).detail.productCode || '-' }} + {{ memberDetail(row.id).detail.englishName || '-' }} + {{ memberDetail(row.id).detail.productionCycleHours ?? '-' }} 小时 + {{ memberDetail(row.id).detail.minWeightG ?? '-' }} g + {{ memberDetail(row.id).detail.productionProcess || '-' }} + {{ memberDetail(row.id).detail.materialDescription || '-' }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
族成员加载中…
@@ -644,9 +731,10 @@ async function handleDeleteGood() { - + + - + 小时 @@ -667,86 +755,35 @@ async function handleDeleteGood() { - - {{ editOriginDetail.productCode || '-' }} - {{ editOriginDetail.englishName || '-' }} - {{ editOriginDetail.productionCycleHours ?? '-' }} 小时 - {{ editOriginDetail.minWeightG ?? '-' }} g - {{ editOriginDetail.productionProcess || '-' }} - {{ editOriginDetail.materialDescription || '-' }} - - - - - - - + + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - + +
添加 SKU
+ + + + + + + + + + + + + + + + +
-