feat(admin): manage merged origin goods in good edit modal

This commit is contained in:
yeuimu
2026-08-27 18:43:30 +08:00
parent 491e74b905
commit ac3a39f40d
+103 -2
View File
@@ -4,10 +4,11 @@ import { useVirtualList } from '@vueuse/core'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { import {
Plus, Edit, Delete, Search, Top, Refresh, Plus, Edit, Delete, Search, Top, Refresh,
FolderAdd, Aim, ArrowDown, FolderAdd, Aim, ArrowDown, QuestionFilled,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import type { import type {
CategoryTree, Country, Tag, TagGroup, Good, GoodDetail, CategoryTree, Country, Tag, TagGroup, Good, GoodDetail,
MergedOriginGoodSummary,
OriginGoodsTreeResponse, OriginGoodsTreeResponse,
} from '@/types' } from '@/types'
import { goodsApi } from '@/api/goods' import { goodsApi } from '@/api/goods'
@@ -513,6 +514,9 @@ const editLoading = ref(false)
const editDetailLoading = ref(false) const editDetailLoading = ref(false)
const detailSyncing = ref(false) const detailSyncing = ref(false)
const editGood = ref<Good | GoodDetail | null>(null) const editGood = ref<Good | GoodDetail | null>(null)
const editMerged = ref<MergedOriginGoodSummary[]>([])
const editMergeSearch = ref('')
const originalOriginGoodId = ref<string>('')
const editForm = ref({ const editForm = ref({
id: '', goodName: '', goodImage: '', countryId: '', cascaderCategory: [] as string[], id: '', goodName: '', goodImage: '', countryId: '', cascaderCategory: [] as string[],
categoryId: '', tagIds: [] as string[], positionId: '', categoryId: '', tagIds: [] as string[], positionId: '',
@@ -533,6 +537,53 @@ const editSizeRows = computed(() => {
}) })
const editPackageRows = computed(() => editOriginDetail.value?.packageSpecs?.rows ?? []) const editPackageRows = computed(() => editOriginDetail.value?.packageSpecs?.rows ?? [])
const editIsCustom = computed(() => editGood.value?.originGood?.isCustom === true) 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({ const customContentForm = ref({
goodPrice: '', productCode: '', englishName: '', productionCycleHours: undefined as number | undefined, goodPrice: '', productCode: '', englishName: '', productionCycleHours: undefined as number | undefined,
minWeightG: '', productionProcess: '', materialDescription: '', minWeightG: '', productionProcess: '', materialDescription: '',
@@ -604,6 +655,8 @@ function addCustomVariant() {
async function openEdit(g: Good) { async function openEdit(g: Good) {
editGood.value = g editGood.value = g
editMerged.value = (g.mergedOriginGoods as MergedOriginGoodSummary[]) ?? []
originalOriginGoodId.value = g.originGoodId
editForm.value = { editForm.value = {
id: g.id, goodName: g.goodName, id: g.id, goodName: g.goodName,
goodImage: g.goodImage || g.originGood?.goodImage || '', goodImage: g.goodImage || g.originGood?.goodImage || '',
@@ -618,6 +671,7 @@ async function openEdit(g: Good) {
try { try {
const detail = await goodsApi.getGoodById(g.id) const detail = await goodsApi.getGoodById(g.id)
editGood.value = detail editGood.value = detail
editMerged.value = (detail.mergedOriginGoods as MergedOriginGoodSummary[]) ?? []
if (detail.originGood?.isCustom) fillCustomContent(detail) if (detail.originGood?.isCustom) fillCustomContent(detail)
} catch { } catch {
ElMessage.warning('商品详情加载失败,当前显示列表数据') ElMessage.warning('商品详情加载失败,当前显示列表数据')
@@ -649,7 +703,9 @@ async function handleSyncOneDetail() {
detailSyncing.value = true detailSyncing.value = true
try { try {
const result = await syncApi.syncOneProductDetail(goodId) 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`) ElMessage.success(`详情同步完成,共 ${result.variants} 个 SKU`)
await Promise.all([refreshLeftTree(), refreshRightTree()]) await Promise.all([refreshLeftTree(), refreshRightTree()])
} catch (error: any) { } catch (error: any) {
@@ -732,6 +788,10 @@ async function handleEditSubmit() {
await goodsApi.updateGood(editForm.value.id, { await goodsApi.updateGood(editForm.value.id, {
goodName: editForm.value.goodName, goodName: editForm.value.goodName,
goodImage: editForm.value.goodImage || null, 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), countryId: Number(editForm.value.countryId),
categoryId: Number(editForm.value.categoryId), categoryId: Number(editForm.value.categoryId),
tagIds: editForm.value.tagIds.map(Number), tagIds: editForm.value.tagIds.map(Number),
@@ -1876,6 +1936,31 @@ onMounted(() => loadAll())
@click="handleSyncOneDetail" @click="handleSyncOneDetail"
>同步详情</el-button> >同步详情</el-button>
</div> </div>
<div v-if="!editIsCustom" class="edit-merged-box">
<div class="edit-merged-title">
关联原产品主源 + 副源
<el-tooltip content="主源决定价格、详情与上下架;副源变体合并展示。切换主源后旧主源自动转为副源。">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</div>
<div class="edit-merged-list">
<div class="edit-merged-item primary">
<span class="edit-merged-tag"></span>
<span>{{ editGood?.originGood?.goodName }}</span>
</div>
<div v-for="m in editMerged" :key="m.id" class="edit-merged-item">
<span class="edit-merged-tag sub"></span>
<span>{{ m.goodName }}</span>
<el-button size="small" link type="primary" @click="promoteMerged(m)">设为主源</el-button>
<el-button size="small" link type="danger" @click="editMerged = editMerged.filter(x => x.id !== m.id)">移除</el-button>
</div>
</div>
<el-select v-model="editMergeSearch" filterable remote :remote-method="(q: string) => editMergeSearch = q"
placeholder="搜索原产品名称以添加副源(用于合并同名商品)" clearable style="width:100%">
<el-option v-for="c in editMergeCandidates" :key="c.rawId" :label="c.goodName" :value="String(c.rawId)"
@click="addEditMerge(c)" />
</el-select>
</div>
<el-form v-loading="editDetailLoading" label-width="80px" style="margin-top: 16px"> <el-form v-loading="editDetailLoading" label-width="80px" style="margin-top: 16px">
<el-form-item label="名称"><el-input v-model="editForm.goodName" /></el-form-item> <el-form-item label="名称"><el-input v-model="editForm.goodName" /></el-form-item>
<el-form-item label="图片"> <el-form-item label="图片">
@@ -2319,6 +2404,22 @@ onMounted(() => loadAll())
.edit-og-sub { color: #909399; font-size: 12px; margin-top: 2px; } .edit-og-sub { color: #909399; font-size: 12px; margin-top: 2px; }
.edit-og-meta { flex: 1; min-width: 0; } .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-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; } .detail-tabs { margin-top: 12px; padding-top: 4px; border-top: 1px solid #ebeef5; }
/* Config modal */ /* Config modal */