- Country 加 sort_order(默认 0 保持 id 序);PATCH /countries/sort 批量保存 顺序(对齐 /tags/sort 模式);findAll 与公开 /public/countries 均按 sortOrder 排序 - CountriesView 表格改为可拖拽行列表:拖动松开即全量保存新顺序,失败回滚 - 商品编辑弹窗成员展开面板:同步详情按钮常驻(已同步显示 重新同步详情), 不再只在未同步态出现 - goods.service.spec 的 FamilyRecomputeService mock 补齐 syncFamilyTags 等 方法(全量并行时其他套件的扫名归族会把本套件夹具收进族,create/update 会调用到,mock 缺方法导致偶发 TypeError) - api 164/164、admin typecheck+22/22+构建全绿
928 lines
46 KiB
Vue
928 lines
46 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Refresh, ArrowDown, QuestionFilled } from '@element-plus/icons-vue'
|
||
import type { CategoryTree, Country, Good, GoodDetail, Tag } from '@/types'
|
||
import { goodsApi } from '@/api/goods'
|
||
import { originGoodsApi } from '@/api/origin-goods'
|
||
import { productFamiliesApi } from '@/api/product-families'
|
||
import { countriesApi } from '@/api/countries'
|
||
import { syncApi } from '@/api/sync'
|
||
import { cleanLinkName, deriveLinkTagNames, linkDims } from '@/utils/origin-name'
|
||
import { findCategoryPath, buildCascader } from '@/utils/category-tree'
|
||
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
goodId: string
|
||
countries: Country[]
|
||
categories: CategoryTree[]
|
||
/** 分组后的标签选项 {id,label,tags} */
|
||
tagOptionGroups: Array<{ id: string; label: string; tags: Tag[] }>
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:visible', v: boolean): void
|
||
/** 商品数据变化(保存/同步/族变更),父级刷新左右树 */
|
||
(e: 'changed'): void
|
||
(e: 'deleted'): void
|
||
/** 快捷新建国家后由父级刷新字典 */
|
||
(e: 'dictionaries-changed'): void
|
||
}>()
|
||
|
||
const editLoading = ref(false)
|
||
const editDetailLoading = ref(false)
|
||
/** 正在同步详情的成员行 id */
|
||
const detailSyncing = ref<string>('')
|
||
const editGood = ref<Good | GoodDetail | null>(null)
|
||
const editForm = ref({
|
||
id: '', goodName: '', goodImage: '', countryId: '', cascaderCategory: [] as string[],
|
||
categoryId: '', positionId: '',
|
||
})
|
||
|
||
const editIsCustom = computed(() => editGood.value?.originGood?.isCustom === true)
|
||
const categoryCascader = computed(() => buildCascader(props.categories))
|
||
|
||
// ─── 自定义商品内容编辑 ───
|
||
const customContentForm = ref({
|
||
goodPrice: '', productCode: '', englishName: '', productionCycleHours: undefined as number | undefined,
|
||
minWeightG: '', productionProcess: '', materialDescription: '',
|
||
blankDesignUrl: '', detailsPageVideoUrl: '', textureName: '', reminder: '',
|
||
productPerformance: '', applicableScenarios: '', washingInstructions: '', specialDescription: '',
|
||
designExplanation: '', designArea: '', pictureRequest: '',
|
||
sizeChartJson: '{\n "columns": [],\n "rows": []\n}',
|
||
packageSpecsJson: '{\n "rows": []\n}',
|
||
optionsJson: '{}',
|
||
mediaJson: '{}',
|
||
variants: [] as Array<{
|
||
sku: string; sizeId: string; sizeName: string; colorId: string; colorName: string; colorHex: string; imageUrl: string;
|
||
price: string; originalPrice: string; weightG: string; boxLengthCm: string;
|
||
boxWidthCm: string; boxHeightCm: string; designDataJson: string; enabled: boolean
|
||
}>,
|
||
})
|
||
|
||
function fillCustomContent(g: GoodDetail) {
|
||
const detail = g.originDetail ?? {}
|
||
customContentForm.value = {
|
||
goodPrice: g.originGood?.goodPrice ?? '',
|
||
productCode: String(detail.productCode ?? ''),
|
||
englishName: String(detail.englishName ?? ''),
|
||
productionCycleHours: detail.productionCycleHours == null ? undefined : Number(detail.productionCycleHours),
|
||
minWeightG: String(detail.minWeightG ?? ''),
|
||
productionProcess: String(detail.productionProcess ?? ''),
|
||
materialDescription: String(detail.materialDescription ?? ''),
|
||
blankDesignUrl: String(detail.blankDesignUrl ?? ''),
|
||
detailsPageVideoUrl: String(detail.detailsPageVideoUrl ?? ''),
|
||
textureName: String(detail.textureName ?? ''),
|
||
reminder: String(detail.reminder ?? ''),
|
||
productPerformance: String(detail.productPerformance ?? ''),
|
||
applicableScenarios: String(detail.applicableScenarios ?? ''),
|
||
washingInstructions: String(detail.washingInstructions ?? ''),
|
||
specialDescription: String(detail.specialDescription ?? ''),
|
||
designExplanation: String(detail.designExplanation ?? ''),
|
||
designArea: String(detail.designArea ?? ''),
|
||
pictureRequest: String(detail.pictureRequest ?? ''),
|
||
sizeChartJson: JSON.stringify(detail.sizeChart ?? { columns: [], rows: [] }, null, 2),
|
||
packageSpecsJson: JSON.stringify(detail.packageSpecs ?? { rows: [] }, null, 2),
|
||
optionsJson: JSON.stringify(detail.options ?? {}, null, 2),
|
||
mediaJson: JSON.stringify(detail.media ?? {}, null, 2),
|
||
variants: g.variants.map((variant) => ({
|
||
sku: variant.sku,
|
||
sizeId: String(variant.sizeId ?? ''),
|
||
sizeName: String(variant.sizeName ?? ''),
|
||
colorId: String(variant.colorId ?? ''),
|
||
colorName: String(variant.colorName ?? ''),
|
||
colorHex: String(variant.colorHex ?? ''),
|
||
imageUrl: String(variant.imageUrl ?? ''),
|
||
price: String(variant.price ?? ''),
|
||
originalPrice: String(variant.originalPrice ?? ''),
|
||
weightG: String(variant.weightG ?? ''),
|
||
boxLengthCm: String(variant.boxLengthCm ?? ''),
|
||
boxWidthCm: String(variant.boxWidthCm ?? ''),
|
||
boxHeightCm: String(variant.boxHeightCm ?? ''),
|
||
designDataJson: JSON.stringify(variant.designData ?? {}, null, 2),
|
||
enabled: variant.enabled,
|
||
})),
|
||
}
|
||
}
|
||
|
||
function addCustomVariant() {
|
||
customContentForm.value.variants.push({
|
||
sku: '', sizeId: '', sizeName: '', colorId: '', colorName: '', colorHex: '', imageUrl: '', price: '',
|
||
originalPrice: '', weightG: '', boxLengthCm: '', boxWidthCm: '', boxHeightCm: '', designDataJson: '{}', enabled: true,
|
||
})
|
||
}
|
||
|
||
// ─── 关联原产品(族成员,可展开查看详情 / 配标签 / 改价格) ───
|
||
interface FamilyMemberRow {
|
||
id: string
|
||
goodName: string
|
||
goodImage: string | null
|
||
source: string
|
||
delisted: boolean
|
||
sdsGoodId: string
|
||
goodPrice: string | null
|
||
variantCount: number
|
||
logisticsLabel: string | null
|
||
craftLabel: string | null
|
||
warehouseLabel: string | null
|
||
tagsManual: boolean
|
||
tags: Array<{ id: string; tagName: string; tagColor: string | null; manual: boolean }>
|
||
}
|
||
interface MemberPriceCell {
|
||
sizeId: string
|
||
colorId: string
|
||
sizeName: string | null
|
||
colorName: string | null
|
||
printCount: string
|
||
craft: string
|
||
logistics: string
|
||
price: string
|
||
manual: boolean
|
||
editPrice: string
|
||
}
|
||
const editFamilyId = ref<string>('')
|
||
const editFamilyCode = ref<string | null>(null)
|
||
const editFamilyMembers = ref<FamilyMemberRow[]>([])
|
||
const familyMemberCount = ref<number | null>(null)
|
||
const editFamilyLoaded = ref(false)
|
||
const editFamilyAddKw = ref('')
|
||
const editFamilyCandidates = ref<Array<{ id: string; goodName: string }>>([])
|
||
const expandedMemberKeys = ref<Set<string>>(new Set())
|
||
const memberTagEdits = ref<Record<string, string[]>>({})
|
||
const memberTagSaving = ref<string>('')
|
||
/** 成员展开面板的 尺码×颜色→价格 编辑态(key = og id) */
|
||
const memberPrices = ref<Record<string, MemberPriceCell[]>>({})
|
||
const memberPriceSaving = ref<string>('')
|
||
/** 族原始详情(含 priceMatrix) */
|
||
const familyDetailRaw = ref<any>(null)
|
||
|
||
// ─── 族级并集视图:尺码表/包装规格(族物化并集)+ SKU(价格矩阵 = 各链接动态合并) ───
|
||
// 尺码行把 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 familyPackageRows = computed(() => familyDetailRaw.value?.packageSpecs?.rows ?? [])
|
||
const familySkuRows = computed(() => {
|
||
const rows = (familyDetailRaw.value?.priceMatrix?.rows ?? []) as any[]
|
||
return rows.map((r) => ({ ...r, sourceCount: Array.isArray(r.sources) ? r.sources.length : 0 }))
|
||
})
|
||
|
||
function toggleMemberExpand(key: string) {
|
||
const next = new Set(expandedMemberKeys.value)
|
||
if (next.has(key)) next.delete(key)
|
||
else {
|
||
next.add(key)
|
||
loadMemberPrices(key)
|
||
ensureMemberDetail(key)
|
||
}
|
||
expandedMemberKeys.value = next
|
||
}
|
||
|
||
// ─── 成员详情(懒加载:GET /origin-goods/:id) ───
|
||
const memberDetailCache = ref<Record<string, any>>({})
|
||
|
||
function memberDetail(id: string) {
|
||
return memberDetailCache.value[id]
|
||
}
|
||
|
||
async function ensureMemberDetail(ogId: string) {
|
||
const row = editFamilyMembers.value.find((m) => m.id === ogId)
|
||
if (!row || row.source !== 'SDS' || memberDetailCache.value[ogId] !== undefined) return
|
||
memberDetailCache.value[ogId] = { loading: true }
|
||
try {
|
||
memberDetailCache.value[ogId] = await originGoodsApi.getOriginGood(ogId)
|
||
} catch {
|
||
memberDetailCache.value[ogId] = null
|
||
}
|
||
}
|
||
|
||
function memberSizeColumns(id: string) {
|
||
return memberDetail(id)?.detail?.sizeChart?.columns ?? []
|
||
}
|
||
|
||
function memberSizeRows(id: string) {
|
||
const rows = memberDetail(id)?.detail?.sizeChart?.rows ?? []
|
||
return rows.map((row: any) => ({
|
||
...row,
|
||
...(row.measurements ?? []).reduce((out: Record<string, string>, item: any) => {
|
||
out[item.key] = item.cm ?? '-'
|
||
return out
|
||
}, {}),
|
||
}))
|
||
}
|
||
|
||
function memberPackageRows(id: string) {
|
||
return memberDetail(id)?.detail?.packageSpecs?.rows ?? []
|
||
}
|
||
|
||
function memberVariants(id: string) {
|
||
return memberDetail(id)?.variants ?? []
|
||
}
|
||
|
||
// 矩阵三个归因维度的合法取值(与 api 端 auto-tag-rules 一致)
|
||
const PRINT_COUNT_VALUES = ['单面印花', '双面印花']
|
||
const CRAFT_VALUES = ['烫画', '直喷', '不打印']
|
||
const LOGISTICS_VALUES = ['包邮', '不包邮']
|
||
|
||
/** 成员的矩阵归因维度:优先取有效标签(人工接管后仍准确),回退按名称解析 */
|
||
function memberDims(id: string) {
|
||
const row = editFamilyMembers.value.find((m) => m.id === id)
|
||
const fallback = linkDims(row?.goodName ?? null)
|
||
const tagNames = ((row as any)?.originGoodTags ?? [])
|
||
.map((r: any) => r.tag?.tagName)
|
||
.filter(Boolean) as string[]
|
||
const crafts = CRAFT_VALUES.filter((v) => tagNames.includes(v))
|
||
return {
|
||
logistics: LOGISTICS_VALUES.find((v) => tagNames.includes(v)) ?? fallback.logistics,
|
||
crafts: crafts.length ? crafts : fallback.crafts,
|
||
printCount: PRINT_COUNT_VALUES.find((v) => tagNames.includes(v)) ?? fallback.printCount,
|
||
}
|
||
}
|
||
|
||
async function handleSyncMemberDetail(row: { id: string; sdsGoodId: string }) {
|
||
if (!row.sdsGoodId || detailSyncing.value) return
|
||
detailSyncing.value = row.id
|
||
try {
|
||
const result = await syncApi.syncOneProductDetail(row.sdsGoodId)
|
||
memberDetailCache.value[row.id] = await originGoodsApi.getOriginGood(row.id)
|
||
ElMessage.success(`详情同步完成,共 ${result.variants} 个 SKU`)
|
||
emit('changed')
|
||
} catch (error: any) {
|
||
ElMessage.error(error?.response?.data?.message || '商品详情同步失败')
|
||
} finally { detailSyncing.value = '' }
|
||
}
|
||
|
||
/** 该链接在族价格矩阵中的格子(印花数量/工艺/物流 三维即该链接的归因标签) */
|
||
function loadMemberPrices(ogId: string) {
|
||
const dims = memberDims(ogId)
|
||
if (!dims.printCount || !dims.crafts.length || !dims.logistics) {
|
||
memberPrices.value[ogId] = []
|
||
return
|
||
}
|
||
const matrixRows = (familyDetailRaw.value?.priceMatrix?.rows ?? []) as any[]
|
||
memberPrices.value[ogId] = matrixRows
|
||
.filter((r: any) =>
|
||
r.printCount === dims.printCount &&
|
||
dims.crafts.includes(r.craft) &&
|
||
r.logistics === dims.logistics)
|
||
.map((r: any) => ({
|
||
sizeId: String(r.sizeId),
|
||
colorId: String(r.colorId),
|
||
sizeName: r.sizeName ?? null,
|
||
colorName: r.colorName ?? null,
|
||
printCount: r.printCount,
|
||
craft: r.craft,
|
||
logistics: r.logistics,
|
||
price: String(r.price ?? ''),
|
||
manual: Boolean(r.manual),
|
||
editPrice: String(r.price ?? ''),
|
||
}))
|
||
}
|
||
|
||
async function saveMemberPrices(row: { id: string }) {
|
||
const cells = (memberPrices.value[row.id] ?? []).filter((c) => c.editPrice !== '' && c.editPrice !== c.price)
|
||
if (!cells.length) { ElMessage.info('价格没有修改'); return }
|
||
memberPriceSaving.value = row.id
|
||
try {
|
||
await productFamiliesApi.putOverrides(editFamilyId.value, cells.map((c) => ({
|
||
sizeId: c.sizeId, colorId: c.colorId, printCount: c.printCount, craft: c.craft, logistics: c.logistics, price: c.editPrice,
|
||
})))
|
||
ElMessage.success('价格已保存')
|
||
await loadEditFamily()
|
||
emit('changed')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || '保存失败')
|
||
} finally { memberPriceSaving.value = '' }
|
||
}
|
||
|
||
function initEditFamily(family: { familyId?: string; familyCode?: string | null } | null) {
|
||
editFamilyId.value = family?.familyId ?? ''
|
||
editFamilyCode.value = family?.familyCode ?? null
|
||
editFamilyMembers.value = []
|
||
familyMemberCount.value = null
|
||
editFamilyLoaded.value = false
|
||
editFamilyAddKw.value = ''
|
||
editFamilyCandidates.value = []
|
||
expandedMemberKeys.value = new Set()
|
||
memberTagEdits.value = {}
|
||
memberPrices.value = {}
|
||
familyDetailRaw.value = null
|
||
if (editFamilyId.value) loadEditFamily()
|
||
}
|
||
|
||
async function loadEditFamily() {
|
||
if (!editFamilyId.value) return
|
||
try {
|
||
const f = await productFamiliesApi.detail(editFamilyId.value) as any
|
||
familyDetailRaw.value = f
|
||
editFamilyCode.value = f.familyCode
|
||
familyMemberCount.value = f._count.originGoods
|
||
const primaryOgId = String(editGood.value?.originGoodId ?? '')
|
||
const members: FamilyMemberRow[] = (f.originGoods ?? []).map((m: any) => ({
|
||
id: String(m.id),
|
||
goodName: m.goodName ?? '',
|
||
goodImage: m.goodImage ?? null,
|
||
source: m.source,
|
||
delisted: Boolean(m.delisted),
|
||
sdsGoodId: m.sdsGoodId ?? '',
|
||
goodPrice: m.goodPrice ?? null,
|
||
variantCount: m._count?.variants ?? 0,
|
||
logisticsLabel: m.logisticsLabel ?? null,
|
||
craftLabel: m.craftLabel ?? null,
|
||
warehouseLabel: m.warehouseLabel ?? null,
|
||
tagsManual: Boolean(m.tagsManual),
|
||
tags: (m.originGoodTags ?? []).map((t: any) => ({
|
||
id: String(t.tag.id),
|
||
tagName: t.tag.tagName,
|
||
tagColor: t.tag.tagColor ?? null,
|
||
manual: Boolean(t.manual),
|
||
})),
|
||
}))
|
||
// 当前商品的链接排最前,其余按 id 稳定排序(界面上不区分主次)
|
||
members.sort((a, b) => {
|
||
const aP = a.id === primaryOgId ? 0 : 1
|
||
const bP = b.id === primaryOgId ? 0 : 1
|
||
return aP - bP || a.id.localeCompare(b.id)
|
||
})
|
||
editFamilyMembers.value = members
|
||
const edits: Record<string, string[]> = {}
|
||
for (const m of members) edits[m.id] = m.tags.map((t) => t.id)
|
||
memberTagEdits.value = edits
|
||
for (const key of expandedMemberKeys.value) loadMemberPrices(key)
|
||
} finally {
|
||
editFamilyLoaded.value = true
|
||
}
|
||
}
|
||
|
||
async function saveMemberTags(row: { id: string }) {
|
||
memberTagSaving.value = row.id
|
||
try {
|
||
await originGoodsApi.updateTags(row.id, memberTagEdits.value[row.id] ?? [])
|
||
ElMessage.success('标签已保存')
|
||
await loadEditFamily()
|
||
emit('changed')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || '保存失败')
|
||
} finally { memberTagSaving.value = '' }
|
||
}
|
||
|
||
async function resetMemberTags(row: { id: string }) {
|
||
memberTagSaving.value = row.id
|
||
try {
|
||
await originGoodsApi.resetTags(row.id)
|
||
ElMessage.success('已恢复按链接名称自动解析')
|
||
await loadEditFamily()
|
||
emit('changed')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || '恢复失败')
|
||
} finally { memberTagSaving.value = '' }
|
||
}
|
||
|
||
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 loadEditFamily()
|
||
emit('changed')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || '加入失败')
|
||
}
|
||
}
|
||
|
||
async function removeFamilyMember(m: { id: string }) {
|
||
if (!editFamilyId.value) return
|
||
try {
|
||
await ElMessageBox.confirm('确定把该链接移出族吗?', '确认', {
|
||
type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消',
|
||
})
|
||
} catch { return }
|
||
try {
|
||
await productFamiliesApi.updateMembers(editFamilyId.value, { removeOriginGoodIds: [m.id] })
|
||
ElMessage.success('已移除出族')
|
||
await loadEditFamily()
|
||
emit('changed')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || '移除失败')
|
||
}
|
||
}
|
||
|
||
// ─── 打开 / 保存 / 删除 / 同步 ───
|
||
watch(() => props.visible, (v) => {
|
||
if (v && props.goodId) initLoad()
|
||
})
|
||
|
||
async function initLoad() {
|
||
editGood.value = null
|
||
editDetailLoading.value = true
|
||
try {
|
||
const detail = await goodsApi.getGoodById(props.goodId)
|
||
applyGood(detail)
|
||
} catch {
|
||
ElMessage.warning('商品详情加载失败')
|
||
} finally {
|
||
editDetailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function applyGood(g: GoodDetail) {
|
||
editGood.value = g
|
||
// 名称展示解析为「品名 型号」,保存后即采用解析名
|
||
editForm.value = {
|
||
id: g.id,
|
||
goodName: cleanLinkName(g.goodName) || g.goodName,
|
||
goodImage: g.goodImage || g.originGood?.goodImage || '',
|
||
countryId: g.countryId,
|
||
cascaderCategory: findCategoryPath(props.categories, g.categoryId),
|
||
categoryId: g.categoryId,
|
||
positionId: g.positionId || '',
|
||
}
|
||
initEditFamily((g as any).originGood?.family ?? null)
|
||
if (g.originGood?.isCustom) fillCustomContent(g)
|
||
}
|
||
|
||
async function quickCreateCountry() {
|
||
try {
|
||
const { value } = await ElMessageBox.prompt('请输入国家名称', '新增国家', {
|
||
confirmButtonText: '新增', cancelButtonText: '取消', inputPlaceholder: '国家名称',
|
||
})
|
||
if (!value.trim()) return
|
||
const created = await countriesApi.createCountry({ countryName: value.trim() } as any)
|
||
emit('dictionaries-changed')
|
||
if (created?.id) editForm.value.countryId = created.id
|
||
ElMessage.success('已创建并选中')
|
||
} catch {}
|
||
}
|
||
|
||
function onEditCascaderChange(val: any) {
|
||
const path = Array.isArray(val) ? val : []
|
||
editForm.value.categoryId = path.length ? String(path[path.length - 1]) : ''
|
||
}
|
||
|
||
async function handleEditSubmit() {
|
||
let customPayload: any = null
|
||
if (editIsCustom.value) {
|
||
let sizeChart: Record<string, unknown>
|
||
let packageSpecs: Record<string, unknown>
|
||
let options: Record<string, unknown>
|
||
let media: Record<string, unknown>
|
||
try {
|
||
sizeChart = JSON.parse(customContentForm.value.sizeChartJson)
|
||
packageSpecs = JSON.parse(customContentForm.value.packageSpecsJson)
|
||
options = JSON.parse(customContentForm.value.optionsJson)
|
||
media = JSON.parse(customContentForm.value.mediaJson)
|
||
for (const variant of customContentForm.value.variants) JSON.parse(variant.designDataJson)
|
||
} catch {
|
||
ElMessage.error('尺码表、包装规格、选项、媒体或 SKU 设计数据不是有效 JSON')
|
||
return
|
||
}
|
||
if (customContentForm.value.variants.some((variant) => !variant.sku.trim())) {
|
||
ElMessage.error('SKU 不能为空')
|
||
return
|
||
}
|
||
customPayload = {
|
||
goodName: editForm.value.goodName,
|
||
goodImage: editForm.value.goodImage || null,
|
||
goodPrice: customContentForm.value.goodPrice || null,
|
||
detail: {
|
||
productCode: customContentForm.value.productCode || null,
|
||
englishName: customContentForm.value.englishName || null,
|
||
productionCycleHours: customContentForm.value.productionCycleHours ?? null,
|
||
minWeightG: customContentForm.value.minWeightG || null,
|
||
productionProcess: customContentForm.value.productionProcess || null,
|
||
materialDescription: customContentForm.value.materialDescription || null,
|
||
blankDesignUrl: customContentForm.value.blankDesignUrl || null,
|
||
detailsPageVideoUrl: customContentForm.value.detailsPageVideoUrl || null,
|
||
textureName: customContentForm.value.textureName || null,
|
||
reminder: customContentForm.value.reminder || null,
|
||
productPerformance: customContentForm.value.productPerformance || null,
|
||
applicableScenarios: customContentForm.value.applicableScenarios || null,
|
||
washingInstructions: customContentForm.value.washingInstructions || null,
|
||
specialDescription: customContentForm.value.specialDescription || null,
|
||
designExplanation: customContentForm.value.designExplanation || null,
|
||
designArea: customContentForm.value.designArea || null,
|
||
pictureRequest: customContentForm.value.pictureRequest || null,
|
||
sizeChart,
|
||
packageSpecs,
|
||
options,
|
||
media,
|
||
},
|
||
variants: customContentForm.value.variants.map((variant: any, index: number) => ({
|
||
sku: variant.sku.trim(),
|
||
sizeId: variant.sizeId || null,
|
||
sizeName: variant.sizeName || null,
|
||
colorId: variant.colorId || null,
|
||
colorName: variant.colorName || null,
|
||
colorHex: variant.colorHex || null,
|
||
imageUrl: variant.imageUrl || null,
|
||
price: variant.price || null,
|
||
originalPrice: variant.originalPrice || null,
|
||
weightG: variant.weightG || null,
|
||
boxLengthCm: variant.boxLengthCm || null,
|
||
boxWidthCm: variant.boxWidthCm || null,
|
||
boxHeightCm: variant.boxHeightCm || null,
|
||
designData: JSON.parse(variant.designDataJson),
|
||
enabled: variant.enabled,
|
||
sortOrder: index,
|
||
})),
|
||
}
|
||
}
|
||
editLoading.value = true
|
||
try {
|
||
await goodsApi.updateGood(editForm.value.id, {
|
||
goodName: editForm.value.goodName,
|
||
goodImage: editForm.value.goodImage || null,
|
||
countryId: Number(editForm.value.countryId),
|
||
categoryId: Number(editForm.value.categoryId),
|
||
positionId: editForm.value.positionId ? Number(editForm.value.positionId) : null,
|
||
} as any)
|
||
if (customPayload) await goodsApi.updateCustomGoodContent(editForm.value.id, customPayload)
|
||
ElMessage.success('更新成功')
|
||
emit('update:visible', false)
|
||
emit('changed')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || '更新失败')
|
||
} finally { editLoading.value = false }
|
||
}
|
||
|
||
async function handleDeleteGood() {
|
||
const g = editGood.value
|
||
if (!g) return
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除「${g.goodName}」吗?`, '确认', {
|
||
type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消',
|
||
})
|
||
} catch { return }
|
||
await goodsApi.deleteGood(g.id)
|
||
ElMessage.success('删除成功')
|
||
emit('update:visible', false)
|
||
emit('deleted')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<el-dialog :model-value="visible" :title="cleanLinkName(editGood?.goodName) || '编辑商品'" width="900px" destroy-on-close @update:model-value="emit('update:visible', $event)">
|
||
<div v-if="!editIsCustom" class="edit-merged-box">
|
||
<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 }} 条链接</span>
|
||
</div>
|
||
<div class="edit-merged-list">
|
||
<template v-for="row in editFamilyMembers" :key="row.id">
|
||
<div class="edit-merged-item">
|
||
<el-icon
|
||
class="member-expand-toggle"
|
||
:class="{ 'is-expanded': expandedMemberKeys.has(row.id) }"
|
||
@click="toggleMemberExpand(row.id)"
|
||
><ArrowDown /></el-icon>
|
||
<span class="edit-merged-name" :title="row.goodName">{{ row.goodName }}</span>
|
||
<span v-if="row.tags.length || deriveLinkTagNames(row.goodName).length" class="member-chips">
|
||
<template v-if="row.tags.length">
|
||
<span v-for="c in row.tags" :key="c.id" class="member-chip" :class="{ 'is-manual': c.manual }">{{ c.tagName }}</span>
|
||
</template>
|
||
<template v-else>
|
||
<span v-for="c in deriveLinkTagNames(row.goodName)" :key="c" class="member-chip is-preview">{{ c }}</span>
|
||
</template>
|
||
</span>
|
||
<span v-if="row.tagsManual" class="member-manual-flag" title="人工接管:自动同步不再覆盖该链接的标签">人工</span>
|
||
<div class="edit-merged-actions">
|
||
<el-button size="small" link type="danger" @click="removeFamilyMember(row)">移除出族</el-button>
|
||
</div>
|
||
</div>
|
||
<div v-if="expandedMemberKeys.has(row.id)" class="member-detail-panel">
|
||
<div class="member-detail-grid">
|
||
<span class="md-label">SDS ID</span><span class="md-val">{{ row.sdsGoodId || '-' }}</span>
|
||
<span class="md-label">链接价格</span><span class="md-val">{{ row.goodPrice ? `¥${row.goodPrice}` : '-' }}</span>
|
||
<span class="md-label">SKU 数</span><span class="md-val">{{ row.id === editGood?.originGoodId ? (editGood?.originGood?.variantCount ?? row.variantCount) : row.variantCount }}</span>
|
||
<span class="md-label">物流备注</span><span class="md-val">{{ row.logisticsLabel || '-' }}</span>
|
||
<span class="md-label">工艺位置</span><span class="md-val">{{ row.craftLabel || '-' }}</span>
|
||
<span class="md-label">仓库</span><span class="md-val">{{ row.warehouseLabel || '-' }}</span>
|
||
</div>
|
||
<div class="member-tags-edit">
|
||
<span class="md-label">标签</span>
|
||
<el-select
|
||
v-model="memberTagEdits[row.id]"
|
||
multiple filterable size="small"
|
||
placeholder="选择标签"
|
||
style="flex:1"
|
||
>
|
||
<el-option-group v-for="g in tagOptionGroups" :key="g.id" :label="g.label">
|
||
<el-option v-for="t in g.tags" :key="t.id" :label="t.tagName" :value="t.id" />
|
||
</el-option-group>
|
||
</el-select>
|
||
<el-button size="small" type="primary" :loading="memberTagSaving === row.id" @click="saveMemberTags(row)">保存标签</el-button>
|
||
<el-button v-if="row.tagsManual" size="small" :loading="memberTagSaving === row.id" @click="resetMemberTags(row)">恢复自动</el-button>
|
||
</div>
|
||
<div v-if="row.craftLabel && row.logisticsLabel" class="member-price-block">
|
||
<div class="member-price-head">
|
||
<el-button size="small" type="primary" :loading="memberPriceSaving === row.id" @click="saveMemberPrices(row)">保存价格</el-button>
|
||
</div>
|
||
<el-table :data="memberPrices[row.id] ?? []" border size="small" max-height="240">
|
||
<el-table-column prop="sizeName" label="尺码" width="100" />
|
||
<el-table-column prop="colorName" label="颜色" min-width="120" />
|
||
<el-table-column label="价格" width="130">
|
||
<template #default="{ row: cell }">
|
||
<el-input v-model="cell.editPrice" size="small" @input="cell.editPrice = String(cell.editPrice)" />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div v-if="row.source === 'SDS'" class="member-extra">
|
||
<div class="member-detail-head">
|
||
<span v-if="!memberDetail(row.id) || memberDetail(row.id).loading" class="md-empty">详情加载中…</span>
|
||
<span v-else-if="!memberDetail(row.id).hasDetail" class="md-empty">尚未同步详情</span>
|
||
<el-button
|
||
size="small"
|
||
:icon="Refresh"
|
||
:loading="detailSyncing === row.id"
|
||
@click="handleSyncMemberDetail(row)"
|
||
>{{ memberDetail(row.id)?.hasDetail ? '重新同步详情' : '同步详情' }}</el-button>
|
||
</div>
|
||
<el-tabs v-if="memberDetail(row.id) && memberDetail(row.id).hasDetail" class="member-detail-tabs">
|
||
<el-tab-pane label="商品详情">
|
||
<el-descriptions :column="2" border size="small">
|
||
<el-descriptions-item label="商品编码">{{ memberDetail(row.id).detail.productCode || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="英文名称">{{ memberDetail(row.id).detail.englishName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="生产周期">{{ memberDetail(row.id).detail.productionCycleHours ?? '-' }} 小时</el-descriptions-item>
|
||
<el-descriptions-item label="净重">{{ memberDetail(row.id).detail.minWeightG ?? '-' }} g</el-descriptions-item>
|
||
<el-descriptions-item label="生产工艺">{{ memberDetail(row.id).detail.productionProcess || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="材质">{{ memberDetail(row.id).detail.materialDescription || '-' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="`尺码表 (${memberSizeRows(row.id).length})`">
|
||
<el-table :data="memberSizeRows(row.id)" border size="small" max-height="240">
|
||
<el-table-column prop="sizeName" label="尺码" width="100" fixed />
|
||
<el-table-column
|
||
v-for="column in memberSizeColumns(row.id)"
|
||
:key="column.key"
|
||
:prop="column.key"
|
||
:label="`${column.name} (cm)`"
|
||
min-width="110"
|
||
/>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="`包装规格 (${memberPackageRows(row.id).length})`">
|
||
<el-table :data="memberPackageRows(row.id)" border size="small" max-height="240">
|
||
<el-table-column prop="sizeName" label="尺码" width="90" fixed />
|
||
<el-table-column label="包装尺寸 (cm)" min-width="150">
|
||
<template #default="{ row: pkg }">
|
||
{{ pkg.dimensionsCm ? `${pkg.dimensionsCm.length}×${pkg.dimensionsCm.width}×${pkg.dimensionsCm.height}` : '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="volumeCm3" label="体积 (cm³)" width="110" />
|
||
<el-table-column prop="grossWeightG" label="含包装重量 (g)" width="140" />
|
||
</el-table>
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="`SKU (${memberVariants(row.id).length})`">
|
||
<el-table :data="memberVariants(row.id)" border size="small" max-height="240">
|
||
<el-table-column prop="sku" label="SKU" min-width="160" fixed />
|
||
<el-table-column label="物流" width="100">
|
||
<template #default>{{ memberDims(row.id).logistics || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="工艺" width="90">
|
||
<template #default>{{ memberDims(row.id).crafts.join(' / ') || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="印花数量" width="95">
|
||
<template #default>{{ memberDims(row.id).printCount || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sizeName" label="尺码" width="85" />
|
||
<el-table-column prop="colorName" label="颜色" width="100" />
|
||
<el-table-column prop="price" label="价格" width="90" />
|
||
<el-table-column label="状态" width="80">
|
||
<template #default="{ row: variant }">
|
||
<el-tag :type="variant.enabled ? 'success' : 'info'" size="small">{{ variant.enabled ? '可用' : '停用' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<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="图片">
|
||
<ImageUpload v-model="editForm.goodImage" label="上传图片" />
|
||
</el-form-item>
|
||
<el-form-item label="国家">
|
||
<div class="select-inline">
|
||
<el-select v-model="editForm.countryId" filterable placeholder="请选择国家">
|
||
<el-option v-for="c in countries" :key="c.id" :label="c.countryName" :value="c.id" />
|
||
</el-select>
|
||
<el-button text :icon="Plus" @click="quickCreateCountry" />
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="分类">
|
||
<el-cascader v-model="editForm.cascaderCategory" :options="categoryCascader as any" :props="{ checkStrictly: true }" placeholder="请选择分类" @change="onEditCascaderChange" />
|
||
</el-form-item>
|
||
<el-form-item v-if="editIsCustom" label="基础价格">
|
||
<el-input v-model="customContentForm.goodPrice" placeholder="例如 28.00" />
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 自定义商品内容编辑(SDS 商品的详情在「关联原产品」成员面板内查看) -->
|
||
<el-tabs v-if="editIsCustom" class="detail-tabs">
|
||
<el-tab-pane label="商品详情">
|
||
<el-form label-width="100px" class="custom-detail-form">
|
||
<el-form-item label="商品编码"><el-input v-model="customContentForm.productCode" /></el-form-item>
|
||
<el-form-item label="英文名称"><el-input v-model="customContentForm.englishName" /></el-form-item>
|
||
<el-form-item label="生产周期"><el-input-number v-model="customContentForm.productionCycleHours" :min="0" /><span style="margin-left:8px">小时</span></el-form-item>
|
||
<el-form-item label="净重"><el-input v-model="customContentForm.minWeightG"><template #append>g</template></el-input></el-form-item>
|
||
<el-form-item label="生产工艺"><el-input v-model="customContentForm.productionProcess" type="textarea" /></el-form-item>
|
||
<el-form-item label="材质"><el-input v-model="customContentForm.materialDescription" type="textarea" /></el-form-item>
|
||
<el-form-item label="空白设计图"><el-input v-model="customContentForm.blankDesignUrl" /></el-form-item>
|
||
<el-form-item label="详情视频"><el-input v-model="customContentForm.detailsPageVideoUrl" /></el-form-item>
|
||
<el-form-item label="面料名称"><el-input v-model="customContentForm.textureName" /></el-form-item>
|
||
<el-form-item label="温馨提示"><el-input v-model="customContentForm.reminder" type="textarea" /></el-form-item>
|
||
<el-form-item label="产品性能"><el-input v-model="customContentForm.productPerformance" type="textarea" /></el-form-item>
|
||
<el-form-item label="适用场景"><el-input v-model="customContentForm.applicableScenarios" type="textarea" /></el-form-item>
|
||
<el-form-item label="洗涤说明"><el-input v-model="customContentForm.washingInstructions" type="textarea" /></el-form-item>
|
||
<el-form-item label="特殊说明"><el-input v-model="customContentForm.specialDescription" type="textarea" /></el-form-item>
|
||
<el-form-item label="设计说明"><el-input v-model="customContentForm.designExplanation" type="textarea" /></el-form-item>
|
||
<el-form-item label="设计区域"><el-input v-model="customContentForm.designArea" /></el-form-item>
|
||
<el-form-item label="图片要求"><el-input v-model="customContentForm.pictureRequest" type="textarea" /></el-form-item>
|
||
<el-form-item label="商品选项 JSON"><el-input v-model="customContentForm.optionsJson" type="textarea" :rows="6" /></el-form-item>
|
||
<el-form-item label="媒体数据 JSON"><el-input v-model="customContentForm.mediaJson" type="textarea" :rows="6" /></el-form-item>
|
||
</el-form>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="尺码表">
|
||
<el-input v-model="customContentForm.sizeChartJson" type="textarea" :rows="14" placeholder="尺码表 JSON" />
|
||
</el-tab-pane>
|
||
<el-tab-pane label="包装规格">
|
||
<el-input v-model="customContentForm.packageSpecsJson" type="textarea" :rows="14" placeholder="包装规格 JSON" />
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="`SKU (${customContentForm.variants.length})`">
|
||
<div style="display:flex;justify-content:flex-end;margin-bottom:8px"><el-button size="small" :icon="Plus" @click="addCustomVariant">添加 SKU</el-button></div>
|
||
<el-table :data="customContentForm.variants" border max-height="320">
|
||
<el-table-column label="SKU" min-width="160"><template #default="{ row }"><el-input v-model="row.sku" /></template></el-table-column>
|
||
<el-table-column label="尺码 ID" width="120"><template #default="{ row }"><el-input v-model="row.sizeId" /></template></el-table-column>
|
||
<el-table-column label="尺码" width="120"><template #default="{ row }"><el-input v-model="row.sizeName" /></template></el-table-column>
|
||
<el-table-column label="颜色 ID" width="120"><template #default="{ row }"><el-input v-model="row.colorId" /></template></el-table-column>
|
||
<el-table-column label="颜色" width="120"><template #default="{ row }"><el-input v-model="row.colorName" /></template></el-table-column>
|
||
<el-table-column label="色值" width="120"><template #default="{ row }"><el-input v-model="row.colorHex" placeholder="#FFFFFF" /></template></el-table-column>
|
||
<el-table-column label="图片" min-width="180"><template #default="{ row }"><el-input v-model="row.imageUrl" /></template></el-table-column>
|
||
<el-table-column label="价格" width="120"><template #default="{ row }"><el-input v-model="row.price" /></template></el-table-column>
|
||
<el-table-column label="原价" width="120"><template #default="{ row }"><el-input v-model="row.originalPrice" /></template></el-table-column>
|
||
<el-table-column label="重量(g)" width="120"><template #default="{ row }"><el-input v-model="row.weightG" /></template></el-table-column>
|
||
<el-table-column label="包装长(cm)" width="130"><template #default="{ row }"><el-input v-model="row.boxLengthCm" /></template></el-table-column>
|
||
<el-table-column label="包装宽(cm)" width="130"><template #default="{ row }"><el-input v-model="row.boxWidthCm" /></template></el-table-column>
|
||
<el-table-column label="包装高(cm)" width="130"><template #default="{ row }"><el-input v-model="row.boxHeightCm" /></template></el-table-column>
|
||
<el-table-column label="设计数据 JSON" min-width="220"><template #default="{ row }"><el-input v-model="row.designDataJson" type="textarea" :rows="2" /></template></el-table-column>
|
||
<el-table-column label="启用" width="80"><template #default="{ row }"><el-switch v-model="row.enabled" /></template></el-table-column>
|
||
<el-table-column label="操作" width="70"><template #default="{ $index }"><el-button link type="danger" @click="customContentForm.variants.splice($index, 1)">删除</el-button></template></el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<!-- 族级并集视图(弹窗最底部):尺码表/包装规格为族并集,SKU 为价格矩阵动态合并 -->
|
||
<el-tabs v-if="!editIsCustom && editFamilyId && editFamilyLoaded" class="detail-tabs">
|
||
<el-tab-pane :label="`尺码表 (${familySizeRows.length})`">
|
||
<el-table :data="familySizeRows" border size="small" max-height="260">
|
||
<el-table-column prop="sizeName" label="尺码" width="100" fixed />
|
||
<el-table-column
|
||
v-for="column in familySizeColumns"
|
||
:key="column.key"
|
||
:prop="column.key"
|
||
:label="`${column.name} (cm)`"
|
||
min-width="110"
|
||
/>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="`包装规格 (${familyPackageRows.length})`">
|
||
<el-table :data="familyPackageRows" border size="small" max-height="260">
|
||
<el-table-column prop="sizeName" label="尺码" width="90" fixed />
|
||
<el-table-column label="包装尺寸 (cm)" min-width="150">
|
||
<template #default="{ row }">
|
||
{{ row.dimensionsCm ? `${row.dimensionsCm.length}×${row.dimensionsCm.width}×${row.dimensionsCm.height}` : '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="volumeCm3" label="体积 (cm³)" width="110" />
|
||
<el-table-column prop="grossWeightG" label="含包装重量 (g)" width="140" />
|
||
</el-table>
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="`SKU (${familySkuRows.length})`">
|
||
<el-table :data="familySkuRows" border size="small" max-height="260">
|
||
<el-table-column label="物流" width="110">
|
||
<template #default="{ row }">{{ row.logistics || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="工艺" width="100">
|
||
<template #default="{ row }">{{ row.craft || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="印花数量" width="100">
|
||
<template #default="{ row }">{{ row.printCount || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sizeName" label="尺码" width="90" />
|
||
<el-table-column prop="colorName" label="颜色" min-width="110" />
|
||
<el-table-column label="价格" width="100">
|
||
<template #default="{ row }">{{ row.price }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="链接数" width="80">
|
||
<template #default="{ row }">{{ row.sourceCount || 1 }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.manual ? 'warning' : 'success'" size="small">{{ row.manual ? '人工价' : '推导价' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<template #footer>
|
||
<el-button type="danger" @click="handleDeleteGood">删除</el-button>
|
||
<el-button @click="emit('update:visible', false)">取消</el-button>
|
||
<el-button type="primary" :loading="editLoading" @click="handleEditSubmit">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.edit-merged-box { margin-bottom: 4px; }
|
||
.edit-merged-title {
|
||
display: flex; align-items: center; gap: 4px;
|
||
font-weight: 600; font-size: 13px; margin-bottom: 8px;
|
||
}
|
||
.edit-family-code { margin-left: auto; font-weight: 400; font-size: 12px; color: var(--el-color-primary); }
|
||
.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-name {
|
||
flex: 1; min-width: 0;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
.edit-merged-actions { flex-shrink: 0; display: flex; align-items: center; }
|
||
.edit-merged-actions .el-button + .el-button { margin-left: 4px; }
|
||
.member-chips { display: flex; align-items: center; gap: 4px; flex-shrink: 0; flex-wrap: nowrap; overflow: hidden; }
|
||
.member-chip {
|
||
font-size: 11px; line-height: 16px; padding: 1px 7px; border-radius: 8px;
|
||
background: #ecf5ff; color: var(--el-color-primary); white-space: nowrap;
|
||
}
|
||
.member-chip.is-manual { background: #f0f9eb; color: var(--el-color-success); }
|
||
.member-chip.is-preview { background: #f4f4f5; color: #909399; border: 1px dashed #dcdfe6; }
|
||
.member-manual-flag {
|
||
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 4px;
|
||
background: #f0f9eb; color: var(--el-color-success); flex-shrink: 0;
|
||
}
|
||
.member-expand-toggle { cursor: pointer; flex-shrink: 0; color: #909399; transition: transform 0.18s; }
|
||
.member-expand-toggle.is-expanded { transform: rotate(180deg); }
|
||
.member-detail-panel {
|
||
background: #fbfcfe; border: 1px solid #ebeef5; border-radius: 6px;
|
||
padding: 10px 12px; margin: -2px 0 2px 30px;
|
||
}
|
||
.member-detail-grid {
|
||
display: grid; grid-template-columns: 64px minmax(0, 1fr) 64px minmax(0, 1fr);
|
||
gap: 4px 10px; font-size: 12px; margin-bottom: 8px;
|
||
}
|
||
.md-label { color: #909399; font-size: 12px; }
|
||
.md-val { color: #606266; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.member-tags-edit { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||
.member-tags-edit .md-label { flex-shrink: 0; }
|
||
.member-price-block { border-top: 1px dashed #e4e7ed; padding-top: 8px; }
|
||
.member-price-head { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 6px; }
|
||
.member-extra { border-top: 1px dashed #e4e7ed; padding-top: 8px; }
|
||
.md-empty {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||
font-size: 12px; color: var(--el-text-color-placeholder); padding: 4px 0;
|
||
}
|
||
.member-detail-head {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.member-detail-head .md-empty { padding: 0; }
|
||
.member-detail-tabs :deep(.el-tabs__header) { margin-bottom: 8px; }
|
||
.edit-family-loading { color: var(--el-text-color-placeholder); font-size: 12px; padding: 4px 0; }
|
||
.family-detail-tabs { margin-top: 10px; }
|
||
.family-detail-tabs :deep(.el-tabs__header) { margin-bottom: 8px; }
|
||
.detail-tabs { margin-top: 12px; padding-top: 4px; border-top: 1px solid #ebeef5; }
|
||
.select-inline { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||
</style>
|