merge: fix/tree-badge-family-aware-dev into develop (local only, not pushed)

This commit is contained in:
yeuimu
2026-08-28 19:16:22 +08:00
3 changed files with 54 additions and 19 deletions
+36 -12
View File
@@ -286,26 +286,40 @@ function buildRightTree(tree: OriginGoodsTreeResponse) {
} }
} }
const roots = tree.tree.map(mapCat) const roots = tree.tree.map(mapCat)
// 已配置数 = 已配置为官网商品的链接(成族但未配置不算) // 族级配置聚合:公开契约一族一款 —— 族内任一链接已配置,全族视为已配置
function fillConfiguredEff(nodes: any[]): number { const ogNodes: any[] = []
;(function collect(nodes: any[]) {
for (const n of nodes) {
if (n.isOG) ogNodes.push(n)
else collect(n.children ?? [])
}
})(roots)
const familyConfigured = new Map<string, number>()
for (const og of ogNodes) {
if (og.familyId) {
familyConfigured.set(og.familyId, (familyConfigured.get(og.familyId) ?? 0) + og.configuredCount)
}
}
for (const og of ogNodes) {
og.familyConfiguredCount = og.familyId ? (familyConfigured.get(og.familyId) ?? 0) : 0
og.configuredEff = og.configuredCount > 0 || og.familyConfiguredCount > 0 ? 1 : 0
}
// 分组头部已配置数 = 有效已配置的链接数(含族配置带动)
;(function fillCategory(nodes: any[]): number {
let count = 0 let count = 0
for (const n of nodes) { for (const n of nodes) {
if (n.isOG) { n.configuredEff = n.isOG ? n.configuredEff : fillCategory(n.children ?? [])
n.configuredEff = n.configuredCount > 0 ? 1 : 0
} else {
n.configuredEff = fillConfiguredEff(n.children ?? [])
}
count += n.configuredEff count += n.configuredEff
} }
return count return count
} })(roots)
fillConfiguredEff(roots)
rightTreeData.value = roots rightTreeData.value = roots
} }
function rightFilterNode(_value: string, data: any) { function rightFilterNode(_value: string, data: any) {
if (data.isOG) { if (data.isOG) {
if (showUnconfiguredOnly.value && data.configuredCount > 0) return false const configured = data.configuredCount > 0 || data.familyConfiguredCount > 0
if (showUnconfiguredOnly.value && configured) return false
if (searchKeyword.value && !data.label.includes(searchKeyword.value)) return false if (searchKeyword.value && !data.label.includes(searchKeyword.value)) return false
return true return true
} }
@@ -398,6 +412,11 @@ function collectSiblings(ogNode: any): any[] {
function openConfigModal(og: any, dropTarget: any) { function openConfigModal(og: any, dropTarget: any) {
const ogNode = findOgNodeById(og.rawId) const ogNode = findOgNodeById(og.rawId)
// 公开契约一族一款:族已配置后再配置成员只会产出官网不可见的重复商品
if ((ogNode?.familyConfiguredCount ?? 0) > 0 || (ogNode?.configuredCount ?? 0) > 0) {
ElMessage.info('该链接所属族已配置为官网商品,无需重复配置')
return
}
configOG.value = { ...og, familyId: og.familyId ?? ogNode?.familyId ?? null } configOG.value = { ...og, familyId: og.familyId ?? ogNode?.familyId ?? null }
configDropTarget.value = dropTarget configDropTarget.value = dropTarget
configSiblings.value = ogNode ? collectSiblings(ogNode) : [] configSiblings.value = ogNode ? collectSiblings(ogNode) : []
@@ -1053,6 +1072,11 @@ onMounted(() => loadAll())
class="og-badge og-badge--ok" class="og-badge og-badge--ok"
:title="`已配置 ${data.configuredCount} 国`" :title="`已配置 ${data.configuredCount} 国`"
>已配置{{ data.configuredCount > 1 ? ' ' + data.configuredCount : '' }}</span> >已配置{{ data.configuredCount > 1 ? ' ' + data.configuredCount : '' }}</span>
<span
v-else-if="data.familyConfiguredCount > 0"
class="og-badge og-badge--ok"
:title="`族 ${data.familyCode || ''} 已配置为官网商品,本链接并入该款`"
>已配置</span>
<span <span
v-else-if="data.familyId" v-else-if="data.familyId"
class="og-badge og-badge--family" class="og-badge og-badge--family"
@@ -1079,12 +1103,12 @@ onMounted(() => loadAll())
@click.stop="handleSyncOriginDetail(data)" @click.stop="handleSyncOriginDetail(data)"
/> />
<el-button <el-button
v-if="data.configuredCount > 0" v-if="data.configuredCount > 0 || data.familyConfiguredCount > 0"
size="small" link :icon="Aim" title="定位到官网商品" size="small" link :icon="Aim" title="定位到官网商品"
@click.stop="locateInLeftTree(data.rawId)" @click.stop="locateInLeftTree(data.rawId)"
/> />
<el-button <el-button
v-if="!data.configuredCount" v-if="!data.configuredCount && !data.familyConfiguredCount"
size="small" type="primary" link class="og-config-btn" size="small" type="primary" link class="og-config-btn"
@click.stop="openConfigFromRightTree(data)" @click.stop="openConfigFromRightTree(data)"
>配置</el-button> >配置</el-button>
@@ -157,7 +157,16 @@ const memberPriceSaving = ref<string>('')
const familyDetailRaw = ref<any>(null) const familyDetailRaw = ref<any>(null)
// ─── 族级并集视图:尺码表/包装规格(族物化并集)+ SKU(价格矩阵 = 各链接动态合并) ─── // ─── 族级并集视图:尺码表/包装规格(族物化并集)+ SKU(价格矩阵 = 各链接动态合并) ───
const familySizeRows = computed(() => familyDetailRaw.value?.sizeChart?.rows ?? []) // 尺码行把 measurements 数组摊平成 { 列key: cm }(与成员级渲染一致,否则格子全空)
const familySizeRows = computed(() =>
((familyDetailRaw.value?.sizeChart?.rows ?? []) as any[]).map((row) => ({
...row,
...(row.measurements ?? []).reduce((out: Record<string, string>, item: any) => {
out[item.key] = item.cm ?? '-'
return out
}, {}),
})),
)
const familySizeColumns = computed(() => familyDetailRaw.value?.sizeChart?.columns ?? []) const familySizeColumns = computed(() => familyDetailRaw.value?.sizeChart?.columns ?? [])
const familyPackageRows = computed(() => familyDetailRaw.value?.packageSpecs?.rows ?? []) const familyPackageRows = computed(() => familyDetailRaw.value?.packageSpecs?.rows ?? [])
const familySkuRows = computed(() => { const familySkuRows = computed(() => {
+8 -6
View File
@@ -155,16 +155,18 @@ pnpm --filter @inkreach/api backfill:product-families
- 编辑弹窗商品「名称」预填解析后的「品名 型号」,保存即采用解析名;右侧原产品库与配置弹窗 - 编辑弹窗商品「名称」预填解析后的「品名 型号」,保存即采用解析名;右侧原产品库与配置弹窗
始终显示**原始链接名** 始终显示**原始链接名**
- 配置弹窗「合并同名」支持**搜索过滤**同分类链接(含已配置链接)。 - 配置弹窗「合并同名」支持**搜索过滤**同分类链接(含已配置链接)。
- 右树徽标语义:**已配置 N**(绿,已配置为官网商品/ **成族**(蓝,已归族但尚未配置)/ - 右树徽标语义**族感知**,公开契约一族一款)**已配置 N**(绿,本链接已配置为官网商品
**配置**橙);「定位到官网商品」仅在已配置的链接上出现(按 主链接 → 同族 → 旧副源 N=国数)/ **配置**绿,本链接未单独建商品但所属族已配置)/ **成族**(蓝,已归族且全族
顺序定位并高亮),「配置」在未配置的链接上出现; 未配置)/ **未配置**(橙);「定位到官网商品」在自身或同族已配置时出现(按 主链接 → 同族 →
旧副源顺序定位并高亮),「配置」仅在全族未配置时出现;把已配置族的链接拖拽/点击配置会被
拦截提示(避免产出官网不可见的重复商品);
- 族分组头部计数与「仅未配置」筛选同样按族语义(族已配置 → 计入已配置)。
- 人工改价/自动成族等族管理 API`/product-families/*`)保留,供脚本或后续界面使用; - 人工改价/自动成族等族管理 API`/product-families/*`)保留,供脚本或后续界面使用;
- 页面组件化:`GoodsView.vue` 拆出 `components/` 下的 GoodsEditDialog(编辑弹窗)、 - 页面组件化:`GoodsView.vue` 拆出 `components/` 下的 GoodsEditDialog(编辑弹窗)、
GoodsConfigDialog(配置弹窗)、CustomGoodDialog(自定义商品)、TagFilterPopover(标签筛选弹层)。 GoodsConfigDialog(配置弹窗)、CustomGoodDialog(自定义商品)、TagFilterPopover(标签筛选弹层)。
> 注意:同一族的多条链接(如 单面印花/双面印花)如果各自都点了一次「配置」会产生多个 > 注意:历史上每族多点「配置」会产生多个同名的官网商品(重复项可手动删除)。现在族已配置后
> 解析后同名的官网商品(名称相同但关联链接不同)——族合并的预期用法是每族配置一个商品, > 「配置」入口会被拦截;存量重复商品(如早期 DG120/AUTM003 测试期间产生的)仍建议清理。
> 其余链接保持为族成员即可;重复的可手动删除。
**链接级标签(自动派生 + 人工修正,2026-08 规则改版)** **链接级标签(自动派生 + 人工修正,2026-08 规则改版)**