From ac3a39f40dcd4723315d932ac4d19264f82aeef7 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Thu, 27 Aug 2026 18:43:30 +0800 Subject: [PATCH] feat(admin): manage merged origin goods in good edit modal --- apps/admin/src/views/goods/GoodsView.vue | 105 ++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/apps/admin/src/views/goods/GoodsView.vue b/apps/admin/src/views/goods/GoodsView.vue index bf1a781..84f8ffa 100644 --- a/apps/admin/src/views/goods/GoodsView.vue +++ b/apps/admin/src/views/goods/GoodsView.vue @@ -4,10 +4,11 @@ import { useVirtualList } from '@vueuse/core' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Edit, Delete, Search, Top, Refresh, - FolderAdd, Aim, ArrowDown, + FolderAdd, Aim, ArrowDown, QuestionFilled, } from '@element-plus/icons-vue' import type { CategoryTree, Country, Tag, TagGroup, Good, GoodDetail, + MergedOriginGoodSummary, OriginGoodsTreeResponse, } from '@/types' import { goodsApi } from '@/api/goods' @@ -513,6 +514,9 @@ const editLoading = ref(false) const editDetailLoading = ref(false) const detailSyncing = ref(false) const editGood = ref(null) +const editMerged = ref([]) +const editMergeSearch = ref('') +const originalOriginGoodId = ref('') const editForm = ref({ id: '', goodName: '', goodImage: '', countryId: '', cascaderCategory: [] as string[], categoryId: '', tagIds: [] as string[], positionId: '', @@ -533,6 +537,53 @@ const editSizeRows = computed(() => { }) const editPackageRows = computed(() => editOriginDetail.value?.packageSpecs?.rows ?? []) const editIsCustom = computed(() => editGood.value?.originGood?.isCustom === true) + +// 候选 = 右栏树全部 og,按搜索词过滤 +const editMergeCandidates = computed(() => { + if (!editMergeSearch.value) return [] + const kw = editMergeSearch.value + const result: any[] = [] + function traverse(nodes: any[]) { + for (const n of nodes) { + if (n.isOG && (n.goodName ?? '').includes(kw)) result.push(n) + if (n.children?.length) traverse(n.children) + } + } + traverse(rightTreeData.value) + return result.filter( + (n) => String(n.rawId) !== String(editGood.value?.originGoodId) + && !editMerged.value.some((m) => m.id === String(n.rawId)), + ) +}) + +function addEditMerge(node: any) { + editMerged.value.push({ + id: String(node.rawId), sdsGoodId: node.sdsGoodId, + goodName: node.goodName, goodImage: node.goodImage, + goodPrice: node.goodPrice, hasDetail: Boolean(node.hasDetail), + variantCount: node.variantCount ?? 0, + }) + editMergeSearch.value = '' +} + +function promoteMerged(m: MergedOriginGoodSummary) { + if (!editGood.value) return + const oldPrimaryId = editGood.value.originGoodId + const oldPrimary = editGood.value.originGood + editGood.value = { + ...editGood.value, + originGoodId: m.id, + originGood: oldPrimary + ? { ...oldPrimary, id: m.id, sdsGoodId: m.sdsGoodId, goodName: m.goodName } + : { id: m.id, sdsGoodId: m.sdsGoodId, goodName: m.goodName } as any, + } + editMerged.value = [ + { id: oldPrimaryId, sdsGoodId: oldPrimary?.sdsGoodId ?? '', goodName: oldPrimary?.goodName ?? null, + goodImage: oldPrimary?.goodImage ?? null, goodPrice: oldPrimary?.goodPrice ?? null, + hasDetail: oldPrimary?.hasDetail ?? false, variantCount: (oldPrimary as any)?.variantCount ?? 0 }, + ...editMerged.value.filter((x) => x.id !== m.id), + ] +} const customContentForm = ref({ goodPrice: '', productCode: '', englishName: '', productionCycleHours: undefined as number | undefined, minWeightG: '', productionProcess: '', materialDescription: '', @@ -604,6 +655,8 @@ function addCustomVariant() { async function openEdit(g: Good) { editGood.value = g + editMerged.value = (g.mergedOriginGoods as MergedOriginGoodSummary[]) ?? [] + originalOriginGoodId.value = g.originGoodId editForm.value = { id: g.id, goodName: g.goodName, goodImage: g.goodImage || g.originGood?.goodImage || '', @@ -618,6 +671,7 @@ async function openEdit(g: Good) { try { const detail = await goodsApi.getGoodById(g.id) editGood.value = detail + editMerged.value = (detail.mergedOriginGoods as MergedOriginGoodSummary[]) ?? [] if (detail.originGood?.isCustom) fillCustomContent(detail) } catch { ElMessage.warning('商品详情加载失败,当前显示列表数据') @@ -649,7 +703,9 @@ async function handleSyncOneDetail() { detailSyncing.value = true try { const result = await syncApi.syncOneProductDetail(goodId) - editGood.value = await goodsApi.getGoodById(editGood.value!.id) + const detail = await goodsApi.getGoodById(editGood.value!.id) + editGood.value = detail + editMerged.value = (detail.mergedOriginGoods as MergedOriginGoodSummary[]) ?? [] ElMessage.success(`详情同步完成,共 ${result.variants} 个 SKU`) await Promise.all([refreshLeftTree(), refreshRightTree()]) } catch (error: any) { @@ -732,6 +788,10 @@ async function handleEditSubmit() { await goodsApi.updateGood(editForm.value.id, { goodName: editForm.value.goodName, goodImage: editForm.value.goodImage || null, + originGoodId: editGood.value?.originGoodId !== originalOriginGoodId.value && !editIsCustom.value + ? Number(editGood.value!.originGoodId) + : undefined, + mergedOriginGoodIds: editIsCustom.value ? undefined : editMerged.value.map((m) => Number(m.id)), countryId: Number(editForm.value.countryId), categoryId: Number(editForm.value.categoryId), tagIds: editForm.value.tagIds.map(Number), @@ -1876,6 +1936,31 @@ onMounted(() => loadAll()) @click="handleSyncOneDetail" >同步详情 +
+
+ 关联原产品(主源 + 副源) + + + +
+
+
+ + {{ editGood?.originGood?.goodName }} +
+
+ + {{ m.goodName }} + 设为主源 + 移除 +
+
+ + + +
@@ -2319,6 +2404,22 @@ onMounted(() => loadAll()) .edit-og-sub { color: #909399; font-size: 12px; margin-top: 2px; } .edit-og-meta { flex: 1; min-width: 0; } .edit-og-status { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; margin-top: 6px; color: #909399; font-size: 12px; } +.edit-merged-box { margin-top: 12px; } +.edit-merged-title { + display: flex; align-items: center; gap: 4px; + font-size: 12px; color: #909399; margin-bottom: 8px; +} +.edit-merged-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; } +.edit-merged-item { + display: flex; align-items: center; gap: 8px; + padding: 6px 10px; background: #f5f7fa; border-radius: 6px; font-size: 13px; +} +.edit-merged-item .el-button { margin-left: auto; } +.edit-merged-tag { + padding: 0 5px; border-radius: 4px; font-size: 11px; line-height: 18px; + background: var(--el-color-primary); color: #fff; flex-shrink: 0; +} +.edit-merged-tag.sub { background: var(--el-color-info); } .detail-tabs { margin-top: 12px; padding-top: 4px; border-top: 1px solid #ebeef5; } /* Config modal */