327 lines
12 KiB
Vue
327 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Refresh, Delete, Plus } from '@element-plus/icons-vue'
|
||
import type { ProductFamily } from '@/types'
|
||
import { productFamiliesApi } from '@/api/product-families'
|
||
import ImageUpload from '@/components/ImageUpload.vue'
|
||
import PriceOverridePanel from './PriceOverridePanel.vue'
|
||
import CustomMemberDialog from './CustomMemberDialog.vue'
|
||
|
||
const props = defineProps<{ modelValue: boolean; familyId: string | null }>()
|
||
const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'reload'): void }>()
|
||
|
||
const visible = computed({
|
||
get: () => props.modelValue,
|
||
set: (v) => emit('update:modelValue', v),
|
||
})
|
||
|
||
const loading = ref(false)
|
||
const family = ref<ProductFamily | null>(null)
|
||
const customMemberVisible = ref(false)
|
||
|
||
const editForm = ref({ familyName: '', familyCode: '', familyImage: '', primaryOriginGoodId: '' })
|
||
const saving = ref(false)
|
||
const busy = ref(false)
|
||
|
||
const matrixSummary = computed(() => {
|
||
const m = family.value?.priceMatrix
|
||
if (!m) return null
|
||
const prices = m.rows.map((r) => Number(r.price)).filter((n) => Number.isFinite(n))
|
||
return {
|
||
sizes: m.sizes.length,
|
||
colors: m.colors.length,
|
||
crafts: m.crafts.length,
|
||
logistics: m.logistics.length,
|
||
rows: m.rows.length,
|
||
minPrice: prices.length ? Math.min(...prices) : null,
|
||
}
|
||
})
|
||
|
||
async function load() {
|
||
if (!props.familyId) return
|
||
loading.value = true
|
||
try {
|
||
family.value = await productFamiliesApi.detail(props.familyId)
|
||
editForm.value = {
|
||
familyName: family.value.familyName,
|
||
familyCode: family.value.familyCode ?? '',
|
||
familyImage: family.value.familyImage ?? '',
|
||
primaryOriginGoodId: family.value.primaryOriginGoodId ?? '',
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message ?? '加载族详情失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
watch(() => [props.modelValue, props.familyId], ([open]) => {
|
||
if (open) load()
|
||
})
|
||
|
||
async function saveCanonical() {
|
||
if (!family.value) return
|
||
saving.value = true
|
||
try {
|
||
await productFamiliesApi.patch(family.value.id, {
|
||
familyName: editForm.value.familyName.trim(),
|
||
familyCode: editForm.value.familyCode.trim(),
|
||
familyImage: editForm.value.familyImage || null,
|
||
primaryOriginGoodId: editForm.value.primaryOriginGoodId || null,
|
||
})
|
||
ElMessage.success('已保存')
|
||
await load()
|
||
emit('reload')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message ?? '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function toggleAutoManaged(v: boolean | string | number) {
|
||
if (!family.value) return
|
||
busy.value = true
|
||
try {
|
||
await productFamiliesApi.patch(family.value.id, { autoManaged: Boolean(v) })
|
||
ElMessage.success(v ? '已切回自动托管' : '已锁定(重算只标记待处理)')
|
||
await load()
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message ?? '切换失败')
|
||
} finally {
|
||
busy.value = false
|
||
}
|
||
}
|
||
|
||
async function recompute() {
|
||
if (!family.value) return
|
||
busy.value = true
|
||
try {
|
||
family.value = await productFamiliesApi.recompute(family.value.id)
|
||
ElMessage.success('重算完成')
|
||
emit('reload')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message ?? '重算失败')
|
||
} finally {
|
||
busy.value = false
|
||
}
|
||
}
|
||
|
||
async function removeMember(id: string) {
|
||
if (!family.value) return
|
||
try {
|
||
await ElMessageBox.confirm('将该链接移出本族?并集与价格矩阵会重算。', '移除成员', { type: 'warning' })
|
||
} catch {
|
||
return
|
||
}
|
||
busy.value = true
|
||
try {
|
||
family.value = await productFamiliesApi.updateMembers(family.value.id, { removeOriginGoodIds: [id] })
|
||
ElMessage.success('已移除')
|
||
emit('reload')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message ?? '移除失败')
|
||
} finally {
|
||
busy.value = false
|
||
}
|
||
}
|
||
|
||
// 添加成员:远程搜索无族链接
|
||
const memberSearchKeyword = ref('')
|
||
const memberOptions = ref<Array<{ id: string; goodName: string }>>([])
|
||
const memberSearchLoading = ref(false)
|
||
|
||
async function searchMembers(q: string) {
|
||
if (!q) {
|
||
memberOptions.value = []
|
||
return
|
||
}
|
||
memberSearchLoading.value = true
|
||
try {
|
||
const res = await (await import('@/api/origin-goods')).originGoodsApi.getOriginGoodsList({
|
||
keyword: q,
|
||
page: 1,
|
||
pageSize: 20,
|
||
})
|
||
memberOptions.value = res.items.map((o: any) => ({ id: String(o.id), goodName: o.goodName ?? o.sdsGoodId }))
|
||
} finally {
|
||
memberSearchLoading.value = false
|
||
}
|
||
}
|
||
|
||
const memberAddIds = ref<string[]>([])
|
||
|
||
async function addMembers() {
|
||
if (!family.value || !memberAddIds.value.length) return
|
||
busy.value = true
|
||
try {
|
||
family.value = await productFamiliesApi.updateMembers(family.value.id, {
|
||
addOriginGoodIds: memberAddIds.value,
|
||
})
|
||
ElMessage.success('已添加')
|
||
memberAddIds.value = []
|
||
memberSearchKeyword.value = ''
|
||
emit('reload')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message ?? '添加失败')
|
||
} finally {
|
||
busy.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<el-drawer v-model="visible" title="产品族详情" size="72%" destroy-on-close>
|
||
<div v-loading="loading || busy" class="fam-detail">
|
||
<template v-if="family">
|
||
<div class="fam-header">
|
||
<div class="fam-title">
|
||
<el-tag v-if="family.familyCode" type="info" size="small">{{ family.familyCode }}</el-tag>
|
||
<span class="fam-name">{{ family.familyName }}</span>
|
||
<el-tag v-if="family.stale" type="danger" size="small">待处理:上游已变化</el-tag>
|
||
</div>
|
||
<div class="fam-actions">
|
||
<el-switch
|
||
:model-value="family.autoManaged"
|
||
active-text="自动托管"
|
||
inactive-text="锁定"
|
||
@change="toggleAutoManaged"
|
||
/>
|
||
<el-button type="primary" :icon="Refresh" @click="recompute">重算</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-collapse class="fam-collapse">
|
||
<el-collapse-item title="基本信息(canonical)" name="base">
|
||
<el-form label-width="90px" class="fam-form">
|
||
<el-form-item label="族名称">
|
||
<el-input v-model="editForm.familyName" />
|
||
</el-form-item>
|
||
<el-form-item label="族编码">
|
||
<el-input v-model="editForm.familyCode" placeholder="如 DG001" />
|
||
</el-form-item>
|
||
<el-form-item label="主图">
|
||
<ImageUpload v-model="editForm.familyImage" label="上传图片" />
|
||
</el-form-item>
|
||
<el-form-item label="主链接">
|
||
<el-select v-model="editForm.primaryOriginGoodId" clearable filterable placeholder="canonical 详情与跳转兜底">
|
||
<el-option
|
||
v-for="m in family.originGoods"
|
||
:key="m.id"
|
||
:value="m.id"
|
||
:label="m.goodName"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="saving" @click="saveCanonical">保存</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item :title="`成员链接(${family.originGoods.length})`" name="members">
|
||
<div class="fam-member-add">
|
||
<el-select
|
||
v-model="memberAddIds"
|
||
multiple
|
||
filterable
|
||
remote
|
||
clearable
|
||
placeholder="搜索无族链接(名称/ID)"
|
||
:remote-method="searchMembers"
|
||
:loading="memberSearchLoading"
|
||
style="width: 420px"
|
||
>
|
||
<el-option v-for="o in memberOptions" :key="o.id" :value="o.id" :label="o.goodName" />
|
||
</el-select>
|
||
<el-button type="primary" plain :icon="Plus" :disabled="!memberAddIds.length" @click="addMembers">
|
||
添加成员
|
||
</el-button>
|
||
<el-button type="success" plain :icon="Plus" @click="customMemberVisible = true">
|
||
新建自定义成员
|
||
</el-button>
|
||
</div>
|
||
<el-table :data="family.originGoods" size="small" border>
|
||
<el-table-column prop="goodName" label="链接名" min-width="260" show-overflow-tooltip />
|
||
<el-table-column prop="logisticsLabel" label="物流" width="110">
|
||
<template #default="{ row }">{{ row.logisticsLabel ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="craftLabel" label="工艺" width="100">
|
||
<template #default="{ row }">{{ row.craftLabel ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="warehouseLabel" label="仓库" width="140">
|
||
<template #default="{ row }">{{ row.warehouseLabel ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="来源" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.source === 'CUSTOM' ? 'warning' : 'info'" size="small">
|
||
{{ row.source === 'CUSTOM' ? '自建' : 'SDS' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="主链接" width="70" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.id === family.primaryOriginGoodId" type="success" size="small">主</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="70" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="danger" :icon="Delete" @click="removeMember(row.id)">移除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item title="价格矩阵与人工改价" name="price">
|
||
<div v-if="matrixSummary" class="fam-matrix-summary">
|
||
<el-tag size="small">尺码 {{ matrixSummary.sizes }}</el-tag>
|
||
<el-tag size="small" type="success">颜色 {{ matrixSummary.colors }}</el-tag>
|
||
<el-tag size="small" type="warning">工艺 {{ matrixSummary.crafts }}</el-tag>
|
||
<el-tag size="small" type="info">物流 {{ matrixSummary.logistics }}</el-tag>
|
||
<el-tag size="small">共 {{ matrixSummary.rows }} 格</el-tag>
|
||
<el-tag v-if="matrixSummary.minPrice !== null" size="small" type="danger">
|
||
起价 ¥{{ matrixSummary.minPrice }}
|
||
</el-tag>
|
||
</div>
|
||
<PriceOverridePanel v-if="family.priceMatrix" :family="family" @changed="load" />
|
||
<el-empty v-else description="暂无价格矩阵(请先重算)" :image-size="60" />
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item title="并集尺码表" name="size">
|
||
<el-table v-if="family.sizeChart?.rows?.length" :data="family.sizeChart.rows" size="small" border>
|
||
<el-table-column prop="sizeName" label="尺码" width="100">
|
||
<template #default="{ row }">{{ row.sizeName ?? row.sizeId }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-for="key in Object.keys(family.sizeChart.rows[0] ?? {}).filter((k) => k !== 'sizeId' && k !== 'sizeName')"
|
||
:key="key"
|
||
:prop="key"
|
||
:label="key"
|
||
/>
|
||
</el-table>
|
||
<el-empty v-else description="暂无尺码表" :image-size="60" />
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</template>
|
||
</div>
|
||
<CustomMemberDialog
|
||
v-model="customMemberVisible"
|
||
:family-id="family?.id ?? null"
|
||
@created="load(); emit('reload')"
|
||
/>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.fam-detail { min-height: 200px; }
|
||
.fam-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||
.fam-title { display: flex; align-items: center; gap: 8px; }
|
||
.fam-name { font-size: 16px; font-weight: 600; }
|
||
.fam-actions { display: flex; align-items: center; gap: 12px; }
|
||
.fam-collapse { border-top: none; }
|
||
.fam-form { max-width: 560px; }
|
||
.fam-member-add { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
|
||
.fam-matrix-summary { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
|
||
</style>
|