refactor(admin): split GoodsView into dialog components; fix(GoodsView): raw member names, no primary badges, member price grid; feat(tag): 光板 maps to 不打印

This commit is contained in:
yeuimu
2026-08-28 16:40:48 +08:00
parent 34d84a7a7c
commit 10197c47a8
14 changed files with 1719 additions and 1441 deletions
@@ -0,0 +1,155 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { CategoryTree, Country } from '@/types'
import { goodsApi } from '@/api/goods'
import { productFamiliesApi } from '@/api/product-families'
import { findCategoryPath, buildCascader } from '@/utils/category-tree'
const props = defineProps<{
visible: boolean
/** 拖拽/配置的链接节点(含 rawId/goodName/goodImage/goodPrice/sdsGoodId/familyId */
og: any | null
/** 同分类下的可合并兄弟链接 */
siblings: any[]
/** 默认勾选的兄弟 rawId */
defaultCheckedIds: string[]
/** 左树拖放目标(国家/分类节点),为空时表单手动选 */
dropTarget: any | null
mode: 'category' | 'country' | 'global'
countries: Country[]
categories: CategoryTree[]
}>()
const emit = defineEmits<{
(e: 'update:visible', v: boolean): void
(e: 'configured'): void
}>()
const loading = ref(false)
const form = ref({
countryId: '', cascaderCategory: [] as string[], categoryId: '', goodImage: '',
})
watch(() => props.visible, (v) => {
if (!v) return
const og = props.og
form.value = { countryId: '', cascaderCategory: [], categoryId: '', goodImage: og?.goodImage || '' }
if (props.dropTarget) {
if (props.mode === 'category') {
form.value.categoryId = props.dropTarget.id
form.value.cascaderCategory = findCategoryPath(props.categories, props.dropTarget.id)
} else {
form.value.countryId = props.dropTarget.id
}
}
})
const categoryCascader = computed(() => buildCascader(props.categories))
const checkedIds = ref<string[]>([])
watch(() => props.visible, (v) => {
if (v) checkedIds.value = [...props.defaultCheckedIds]
})
function onCascaderChange(val: any) {
form.value.categoryId = val.length ? val[val.length - 1] : ''
}
/** 勾选的兄弟链接与当前链接静默归入同一族(无族则自动建族);失败不阻断原配置流程 */
async function ensureFamilyMembership() {
const og = props.og
const primaryId = String(og.rawId)
const checked = checkedIds.value.filter((id) => id !== primaryId)
if (!checked.length) return
try {
if (og.familyId) {
await productFamiliesApi.updateMembers(og.familyId, {
addOriginGoodIds: [...new Set([primaryId, ...checked])],
})
} else {
await productFamiliesApi.create({
familyName: og.goodName ?? '',
originGoodIds: [primaryId, ...checked],
primaryOriginGoodId: primaryId,
})
}
} catch (e: any) {
ElMessage.warning(e?.response?.data?.message || '挂族失败,商品仍按原方式配置')
}
}
async function handleSubmit() {
const og = props.og
if (!form.value.countryId) { ElMessage.warning('请选择国家'); return }
if (!form.value.categoryId) { ElMessage.warning('请选择分类'); return }
loading.value = true
try {
await ensureFamilyMembership()
await goodsApi.createGood({
goodName: og.goodName,
goodImage: form.value.goodImage || undefined,
originGoodId: Number(og.rawId),
countryId: Number(form.value.countryId),
categoryId: Number(form.value.categoryId),
positionId: undefined,
} as any)
ElMessage.success('配置成功')
emit('update:visible', false)
emit('configured')
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || '配置失败')
} finally { loading.value = false }
}
</script>
<template>
<el-dialog :model-value="visible" title="配置原产品" width="520px" destroy-on-close @update:model-value="emit('update:visible', $event)">
<div v-if="og" class="config-og-info">
<div>
<div class="config-og-name">{{ og.goodName }}</div>
<div class="config-og-meta">SDS ID: {{ og.sdsGoodId }}<template v-if="og.goodPrice"> · 价格: ¥{{ og.goodPrice }}</template></div>
</div>
</div>
<el-form label-width="80px" style="margin-top: 16px">
<el-form-item label="国家">
<div v-if="mode === 'category' || !dropTarget" class="select-inline">
<el-select v-model="form.countryId" placeholder="请选择国家" filterable>
<el-option v-for="c in countries" :key="c.id" :label="c.countryName" :value="c.id" />
</el-select>
</div>
<el-tag v-else>{{ countries.find(c => c.id === form.countryId)?.countryName }}</el-tag>
</el-form-item>
<el-form-item label="分类">
<el-cascader v-if="mode === 'country' || !dropTarget" v-model="form.cascaderCategory" :options="categoryCascader as any" :props="{ checkStrictly: true }" placeholder="请选择分类" @change="onCascaderChange" style="width:100%" />
<el-tag v-else>{{ dropTarget?.label }}</el-tag>
</el-form-item>
<el-form-item v-if="siblings.length" label="合并同名">
<div class="config-merge-box">
<div class="config-merge-tip">勾选同分类下同名不同工厂/仓库原产品与当前链接归入同一族尺码/包装并集 + 价格矩阵</div>
<el-checkbox-group v-model="checkedIds">
<el-checkbox v-for="s in siblings" :key="s.rawId" :value="String(s.rawId)">
{{ s.goodName }}<template v-if="s.goodPrice"> · ¥{{ s.goodPrice }}</template>
</el-checkbox>
</el-checkbox-group>
</div>
</el-form-item>
<el-form-item label="图片">
<ImageUpload v-model="form.goodImage" label="上传图片" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="emit('update:visible', false)">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">确认配置</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.config-og-info { padding: 12px; background: #f5f7fa; border-radius: 8px; }
.config-og-name { font-weight: 600; font-size: 14px; }
.config-og-meta { color: #909399; font-size: 12px; margin-top: 2px; }
.config-merge-box { width: 100%; }
.config-merge-tip { color: #909399; font-size: 12px; margin-bottom: 8px; line-height: 1.5; }
.config-merge-box .el-checkbox-group { display: flex; flex-direction: column; align-items: flex-start; max-height: 160px; overflow-y: auto; }
</style>