revert(admin): restore original layout, replace primary/secondary source UI with family members
This commit is contained in:
@@ -4,7 +4,7 @@ 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,
|
||||
@@ -257,13 +257,14 @@ const leftTreeData = computed(() => {
|
||||
})
|
||||
|
||||
function buildRightTree(tree: OriginGoodsTreeResponse) {
|
||||
function mapOgs(ogs: any[], bucketId: string): any[] {
|
||||
return ogs.map((og: any) => ({
|
||||
function mapCat(node: any): any {
|
||||
const children = (node.children || []).map(mapCat)
|
||||
const goods = (node.originGoods || []).map((og: any) => ({
|
||||
id: 'og-' + og.id,
|
||||
label: og.goodName,
|
||||
isOG: true,
|
||||
rawId: og.id,
|
||||
parentId: bucketId,
|
||||
parentId: 'rc-' + node.categoryId,
|
||||
goodName: og.goodName,
|
||||
goodImage: og.goodImage,
|
||||
goodPrice: og.goodPrice,
|
||||
@@ -280,56 +281,12 @@ function buildRightTree(tree: OriginGoodsTreeResponse) {
|
||||
familyName: og.familyName ?? null,
|
||||
familyStale: og.familyStale ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
/** 分类内按族分桶:族节点为父、链接为子;无族链接进「未入族」桶 */
|
||||
function groupByFamily(ogs: any[], bucketPrefix: string): any[] {
|
||||
const byFamily = new Map<string, { key: string; code: string | null; name: string | null; stale: boolean | null; ogs: any[] }>()
|
||||
const ungrouped: any[] = []
|
||||
for (const og of ogs) {
|
||||
if (og.familyId) {
|
||||
const entry = byFamily.get(og.familyId) ?? {
|
||||
key: og.familyId, code: og.familyCode ?? null, name: og.familyName ?? null,
|
||||
stale: og.familyStale ?? null, ogs: [] as any[],
|
||||
}
|
||||
entry.ogs.push(og)
|
||||
byFamily.set(og.familyId, entry)
|
||||
} else {
|
||||
ungrouped.push(og)
|
||||
}
|
||||
}
|
||||
const nodes: any[] = [...byFamily.values()].map((f) => ({
|
||||
id: `fam-${f.key}`,
|
||||
label: `${f.code ?? f.name ?? f.key}(${f.ogs.length})`,
|
||||
isFamily: true,
|
||||
familyId: f.key,
|
||||
familyCode: f.code,
|
||||
familyStale: f.stale,
|
||||
children: mapOgs(f.ogs, `fam-${f.key}`),
|
||||
}))
|
||||
if (ungrouped.length) {
|
||||
nodes.push({
|
||||
id: `fam-none-${bucketPrefix}`,
|
||||
label: `未入族(${ungrouped.length})`,
|
||||
isFamily: true,
|
||||
familyId: null,
|
||||
familyCode: null,
|
||||
familyStale: null,
|
||||
children: mapOgs(ungrouped, `fam-none-${bucketPrefix}`),
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
function mapCat(node: any): any {
|
||||
const children = (node.children || []).map(mapCat)
|
||||
const familyNodes = groupByFamily(node.originGoods || [], node.categoryId)
|
||||
return {
|
||||
id: 'rc-' + node.categoryId,
|
||||
label: node.categoryName,
|
||||
configuredCount: node.configuredCount ?? 0,
|
||||
totalCount: node.totalCount ?? 0,
|
||||
children: [...children, ...familyNodes],
|
||||
children: [...children, ...goods],
|
||||
}
|
||||
}
|
||||
rightTreeData.value = tree.tree.map(mapCat)
|
||||
@@ -484,7 +441,8 @@ function onConfigCascaderChange(val: any) {
|
||||
configForm.value.categoryId = val.length ? val[val.length - 1] : ''
|
||||
}
|
||||
|
||||
/** 配置时把勾选的兄弟链接与主链接归入同一族(无族则自动建族);失败不阻断原配置流程 */
|
||||
/** 多对一合并的族支撑:勾选的兄弟链接与主链接静默归入同一族(无族则自动建族),
|
||||
* 前端交互保持原「合并同名」流程不变;失败不阻断原配置流程 */
|
||||
async function ensureFamilyMembership() {
|
||||
const primaryId = String(configPrimaryId.value || configOG.value.rawId)
|
||||
const checked = configChecked.value.filter((id) => id !== primaryId)
|
||||
@@ -675,34 +633,91 @@ function addCustomVariant() {
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Edit family (产品族) ───
|
||||
// ─── Edit family(编辑弹窗的族成员管理,替代旧主源/副源) ───
|
||||
const editFamilyId = ref<string>('')
|
||||
const editFamilyTouched = ref(false)
|
||||
const editFamilyOptions = ref<Array<{ id: string; familyCode: string | null; familyName: string }>>([])
|
||||
const editFamilyLoading = ref(false)
|
||||
const editFamilyCode = ref<string | null>(null)
|
||||
const editFamilyMembers = ref<Array<{ id: string; goodName: string; goodImage: string | null }>>([])
|
||||
const familyMemberCount = ref<number | null>(null)
|
||||
const editFamilyLoaded = ref(false)
|
||||
const editFamilyAddKw = ref('')
|
||||
const editFamilyCandidates = ref<Array<{ id: string; goodName: string }>>([])
|
||||
|
||||
function initEditFamily(family: { familyId?: string; familyCode?: string | null; familyName?: string } | null) {
|
||||
function initEditFamily(family: { familyId?: string; familyCode?: string | null } | null) {
|
||||
editFamilyId.value = family?.familyId ?? ''
|
||||
editFamilyTouched.value = false
|
||||
editFamilyOptions.value = family?.familyId
|
||||
? [{ id: family.familyId, familyCode: family.familyCode ?? null, familyName: family.familyName ?? '' }]
|
||||
: []
|
||||
editFamilyCode.value = family?.familyCode ?? null
|
||||
editFamilyMembers.value = []
|
||||
familyMemberCount.value = null
|
||||
editFamilyLoaded.value = false
|
||||
editFamilyAddKw.value = ''
|
||||
editFamilyCandidates.value = []
|
||||
if (editFamilyId.value) loadEditFamily()
|
||||
}
|
||||
|
||||
async function searchEditFamilies(q: string) {
|
||||
if (!q) return
|
||||
editFamilyLoading.value = true
|
||||
async function loadEditFamily() {
|
||||
if (!editFamilyId.value) return
|
||||
try {
|
||||
const res = await productFamiliesApi.list({ keyword: q, page: 1, pageSize: 30 })
|
||||
editFamilyOptions.value = res.items.map((f) => ({ id: f.id, familyCode: f.familyCode, familyName: f.familyName }))
|
||||
const f = await productFamiliesApi.detail(editFamilyId.value)
|
||||
editFamilyCode.value = f.familyCode
|
||||
familyMemberCount.value = f._count.originGoods
|
||||
const primaryId = f.primaryOriginGoodId ?? editGood.value?.originGoodId
|
||||
editFamilyMembers.value = f.originGoods
|
||||
.filter((m) => String(m.id) !== String(primaryId))
|
||||
.map((m) => ({ id: String(m.id), goodName: m.goodName, goodImage: m.goodImage }))
|
||||
} finally {
|
||||
editFamilyLoading.value = false
|
||||
editFamilyLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onEditFamilyChange(v: string | null | undefined) {
|
||||
editFamilyTouched.value = true
|
||||
editFamilyId.value = v ?? ''
|
||||
async function refreshAfterFamilyChange() {
|
||||
if (!editGood.value) return
|
||||
const detail = await goodsApi.getGoodById(editGood.value.id)
|
||||
editGood.value = detail
|
||||
initEditFamily((detail.originGood as any)?.family ?? null)
|
||||
refreshLeftTree()
|
||||
refreshRightTree()
|
||||
}
|
||||
|
||||
async function searchFamilyCandidates(q: string) {
|
||||
if (!q) { editFamilyCandidates.value = []; return }
|
||||
try {
|
||||
const res = await originGoodsApi.getOriginGoodsList({ keyword: q, page: 1, pageSize: 20 })
|
||||
editFamilyCandidates.value = res.items.map((o: any) => ({ id: String(o.id), goodName: o.goodName ?? o.sdsGoodId }))
|
||||
} catch { editFamilyCandidates.value = [] }
|
||||
}
|
||||
|
||||
async function addFamilyMember(c: { id: string; goodName: string }) {
|
||||
if (!editFamilyId.value) { ElMessage.warning('该商品尚未成族'); return }
|
||||
try {
|
||||
await productFamiliesApi.updateMembers(editFamilyId.value, { addOriginGoodIds: [c.id] })
|
||||
ElMessage.success('已加入族')
|
||||
editFamilyAddKw.value = ''
|
||||
await refreshAfterFamilyChange()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '加入失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFamilyMember(m: { id: string }) {
|
||||
if (!editFamilyId.value) return
|
||||
try {
|
||||
await productFamiliesApi.updateMembers(editFamilyId.value, { removeOriginGoodIds: [m.id] })
|
||||
ElMessage.success('已移除出族')
|
||||
await refreshAfterFamilyChange()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function promoteFamilyPrimary(m: { id: string }) {
|
||||
if (!editFamilyId.value || !editGood.value) return
|
||||
try {
|
||||
await productFamiliesApi.patch(editFamilyId.value, { primaryOriginGoodId: m.id })
|
||||
await goodsApi.updateGood(editGood.value.id, { originGoodId: Number(m.id) } as any)
|
||||
ElMessage.success('已切换主链接')
|
||||
await refreshAfterFamilyChange()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '切换失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function openEdit(g: Good) {
|
||||
@@ -846,10 +861,6 @@ async function handleEditSubmit() {
|
||||
categoryId: Number(editForm.value.categoryId),
|
||||
tagIds: editForm.value.tagIds.map(Number),
|
||||
positionId: editForm.value.positionId ? Number(editForm.value.positionId) : null,
|
||||
// 族变更:仅在用户改动过选择时提交(undefined = 不动,null = 脱离族)
|
||||
...(editFamilyTouched.value
|
||||
? { familyId: editFamilyId.value ? Number(editFamilyId.value) : null }
|
||||
: {}),
|
||||
} as any)
|
||||
if (customPayload) await goodsApi.updateCustomGoodContent(editForm.value.id, customPayload)
|
||||
ElMessage.success('更新成功')
|
||||
@@ -1741,18 +1752,6 @@ onMounted(() => loadAll())
|
||||
<el-image v-if="data.goodImage" :src="data.goodImage" fit="cover" class="og-thumb" />
|
||||
<div v-else class="og-thumb og-thumb-placeholder" />
|
||||
<span class="og-name">{{ data.label }}</span>
|
||||
<el-tag
|
||||
v-if="data.familyCode"
|
||||
size="small"
|
||||
type="info"
|
||||
class="og-family-tag"
|
||||
:title="data.familyStale ? '族待处理:上游已变化' : '所属产品族'"
|
||||
>{{ data.familyCode }}</el-tag>
|
||||
<span
|
||||
v-if="data.familyStale"
|
||||
class="og-badge og-badge--warn"
|
||||
title="该链接所属族已锁定且上游数据变化,待人工处理"
|
||||
>族待处理</span>
|
||||
<span
|
||||
v-if="data.configuredCount > 0"
|
||||
class="og-badge og-badge--ok"
|
||||
@@ -1789,12 +1788,8 @@ onMounted(() => loadAll())
|
||||
@click.stop="openConfigFromRightTree(data)"
|
||||
>配置</el-button>
|
||||
</div>
|
||||
<div v-else class="og-cat-node" :class="{ 'og-family-node': data.isFamily }">
|
||||
<span class="og-cat-label">{{ data.isFamily ? '族' : '' }} {{ data.label }}</span>
|
||||
<el-tag
|
||||
v-if="data.isFamily && data.familyStale"
|
||||
size="small" type="warning" class="og-family-stale"
|
||||
>待处理</el-tag>
|
||||
<div v-else class="og-cat-node">
|
||||
<span>{{ data.label }}</span>
|
||||
<span v-if="data.totalCount" class="og-cat-count">
|
||||
<template v-if="data.configuredCount < data.totalCount">
|
||||
{{ data.configuredCount }}/{{ data.totalCount }}
|
||||
@@ -1905,12 +1900,12 @@ onMounted(() => loadAll())
|
||||
<el-cascader v-if="mode === 'country' || !configDropTarget" v-model="configForm.cascaderCategory" :options="categoryCascader as any" :props="{ checkStrictly: true }" placeholder="请选择分类" @change="onConfigCascaderChange" style="width:100%" />
|
||||
<el-tag v-else>{{ configDropTarget?.label }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="configSiblings.length" label="同族链接">
|
||||
<el-form-item v-if="configSiblings.length" label="合并同名">
|
||||
<div class="config-merge-box">
|
||||
<div class="config-merge-tip">勾选的同分类链接将与主链接一起归入同一产品族(并集尺码/包装 + 五维价格矩阵);主链接决定详情与跳转。</div>
|
||||
<div class="config-merge-tip">勾选同分类下同名(不同工厂/仓库)原产品,合并为一个商品;主源决定价格与详情。</div>
|
||||
<div class="config-merge-primary">
|
||||
<el-radio-group v-model="configPrimaryId">
|
||||
<el-radio :value="String(configOG.rawId)">主链接:{{ configOG.goodName }}</el-radio>
|
||||
<el-radio :value="String(configOG.rawId)">主源:{{ configOG.goodName }}</el-radio>
|
||||
<el-radio v-for="s in checkedSiblingNodes" :key="s.rawId" :value="String(s.rawId)">{{ s.goodName }}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
@@ -2008,34 +2003,35 @@ onMounted(() => loadAll())
|
||||
>同步详情</el-button>
|
||||
</div>
|
||||
<div v-if="!editIsCustom" class="edit-merged-box">
|
||||
<div class="edit-merged-title">产品族合并(新机制)</div>
|
||||
<div class="config-merge-tip" style="padding: 4px 0 8px">
|
||||
多链接合并已由「产品族」承载:同族链接自动合并尺码/包装并集与五维价格矩阵(下方"所属族"可切换)。
|
||||
旧的副源关联已废弃,仅历史数据只读保留。
|
||||
<div class="edit-merged-title">
|
||||
关联原产品(族成员)
|
||||
<el-tooltip content="同族链接合并为一个商品:尺码/包装并集 + 价格矩阵;主链接决定详情与跳转。">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
<span v-if="editFamilyCode" class="edit-family-code">族 {{ editFamilyCode }} · {{ familyMemberCount ?? editFamilyMembers.length + 1 }} 条链接</span>
|
||||
</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 editFamilyMembers" :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="promoteFamilyPrimary(m)">设为主链接</el-button>
|
||||
<el-button size="small" link type="danger" @click="removeFamilyMember(m)">移除出族</el-button>
|
||||
</div>
|
||||
<div v-if="!editFamilyLoaded && editFamilyId" class="edit-family-loading">族成员加载中…</div>
|
||||
<div v-else-if="!editFamilyId" class="edit-family-loading">暂未成族:配置合并时自动成族</div>
|
||||
</div>
|
||||
<el-select v-model="editFamilyAddKw" filterable remote :remote-method="searchFamilyCandidates"
|
||||
placeholder="搜索原产品名称添加到族" clearable style="width:100%">
|
||||
<el-option v-for="c in editFamilyCandidates" :key="c.id" :label="c.goodName" :value="String(c.id)"
|
||||
@click="addFamilyMember(c)" />
|
||||
</el-select>
|
||||
</div>
|
||||
<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-select
|
||||
:model-value="editFamilyId"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
placeholder="搜索族名称/编码切换,清空则脱离族"
|
||||
:remote-method="searchEditFamilies"
|
||||
:loading="editFamilyLoading"
|
||||
style="width: 100%"
|
||||
@change="onEditFamilyChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="f in editFamilyOptions"
|
||||
:key="f.id"
|
||||
:value="f.id"
|
||||
:label="`${f.familyCode ? f.familyCode + ' · ' : ''}${f.familyName}`"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<ImageUpload v-model="editForm.goodImage" label="上传图片" />
|
||||
</el-form-item>
|
||||
@@ -2447,9 +2443,6 @@ onMounted(() => loadAll())
|
||||
background: linear-gradient(135deg, #f5f7fa, #e9ecef);
|
||||
}
|
||||
.og-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
||||
.og-family-tag { flex-shrink: 0; }
|
||||
.og-family-node .og-cat-label { color: var(--el-color-primary); font-weight: 600; }
|
||||
.og-family-stale { margin-left: 4px; }
|
||||
.og-price { color: #909399; font-size: 12px; flex-shrink: 0; }
|
||||
.og-badge {
|
||||
font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 8px;
|
||||
@@ -2496,6 +2489,8 @@ onMounted(() => loadAll())
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 12px; color: #909399; margin-bottom: 8px;
|
||||
}
|
||||
.edit-family-code { margin-left: auto; color: var(--el-color-primary); }
|
||||
.edit-family-loading { color: var(--el-text-color-placeholder); font-size: 12px; padding: 4px 0; }
|
||||
.edit-merged-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
|
||||
.edit-merged-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user