feat(admin): manage merged origin goods in good edit modal
This commit is contained in:
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user