Files
inkreach-official-website/apps/admin/src/views/product-family/FamilyDetailDrawer.vue
T

327 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>