refactor: 抽取 FactoryForm / MaterialForm 共享组件

供应商与材料的编辑表单和详情展示复用同一组件(mode: edit|view 切换),
减少字段级重复。材料详情由抽屉改为独立页,与供应商保持一致的交互模式。

- 新增 views/factory/FactoryForm.vue:9 个字段、USCC 正则校验、区域级联、edit/view 双模式
- 新增 views/material/MaterialForm.vue:30+ 字段、自载 choices/categories/brands/factories、宣传页上传、edit/view 双模式
- FactoryManage/FactoryDetail 瘦身到壳层
- MaterialManage 编辑抽屉改 el-dialog,删除内嵌详情抽屉
- MaterialManage "详情" 跳转到 /materials/:id
- 顺带从前端移除供应商"经销商"/"产品分类"字段展示(后端字段保留)
This commit is contained in:
caoqianming 2026-04-24 11:13:53 +08:00
parent 19cd3710dd
commit e8e122ca61
6 changed files with 653 additions and 621 deletions

View File

@ -5,33 +5,7 @@
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
</div>
<div class="card detail-card" v-if="factory">
<el-descriptions :column="1" border class="detail-descriptions">
<el-descriptions-item label="供应商全称">{{ displayText(factory.factory_name) }}</el-descriptions-item>
<el-descriptions-item label="供应商简称">{{ displayText(factory.short_name) }}</el-descriptions-item>
<el-descriptions-item label="统一社会信用代码">{{ displayText(factory.unified_social_credit_code) }}</el-descriptions-item>
<el-descriptions-item label="合作模式">{{ displayText(factory.cooperation_mode_display) }}</el-descriptions-item>
<el-descriptions-item label="经销商">{{ displayText(factory.dealer_name) }}</el-descriptions-item>
<el-descriptions-item label="产品分类">{{ displayText(factory.product_category) }}</el-descriptions-item>
<el-descriptions-item label="地区">{{ displayRegion(factory) }}</el-descriptions-item>
<el-descriptions-item label="地址">{{ displayText(factory.address) }}</el-descriptions-item>
<el-descriptions-item label="交互能力">
<span class="multiline">{{ displayText(factory.interaction_capability) }}</span>
</el-descriptions-item>
<el-descriptions-item label="官网">
<a
v-if="factory.website"
:href="factory.website"
target="_blank"
rel="noopener noreferrer"
class="website-link"
>
{{ factory.website }}
</a>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="用户账号">{{ displayList(factory.usernames) }}</el-descriptions-item>
<el-descriptions-item label="材料数量">{{ displayText(factory.material_count) }}</el-descriptions-item>
</el-descriptions>
<FactoryForm :model-value="factory" mode="view" @update:model-value="() => {}" />
</div>
</div>
</template>
@ -39,8 +13,8 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { formatRegion } from '@/utils/region'
import { fetchFactoryDetail } from '@/api/factory'
import FactoryForm from '@/views/factory/FactoryForm.vue'
const route = useRoute()
const router = useRouter()
@ -55,12 +29,6 @@ onMounted(loadDetail)
const goBack = () => {
router.back()
}
const displayText = (value) => value || '-'
const displayList = (value) => (value?.length ? value.join('、') : '-')
const displayRegion = (item) => formatRegion(item.province, item.city, item.district) || '-'
</script>
<style scoped>
@ -69,13 +37,4 @@ const displayRegion = (item) => formatRegion(item.province, item.city, item.dist
align-items: center;
justify-content: space-between;
}
.website-link {
color: var(--brand-500);
word-break: break-all;
}
.multiline {
white-space: pre-wrap;
}
</style>

View File

@ -8,7 +8,6 @@
<el-table v-loading="tableLoading" :data="factories" border height="100%">
<el-table-column prop="factory_name" label="供应商全称" min-width="220" show-overflow-tooltip />
<el-table-column prop="short_name" label="供应商简称" min-width="160" show-overflow-tooltip />
<el-table-column prop="dealer_name" label="经销商" min-width="160" show-overflow-tooltip />
<el-table-column label="合作模式" min-width="120" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.cooperation_mode_display || '-' }}
@ -49,60 +48,7 @@
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-scroll">
<el-form ref="formRef" :model="form" :rules="formRules" label-width="140px">
<el-form-item label="经销商" prop="dealer_name">
<el-input v-model="form.dealer_name" />
</el-form-item>
<el-form-item label="产品分类" prop="product_category">
<el-input v-model="form.product_category" />
</el-form-item>
<el-form-item label="供应商全称" prop="factory_name" required>
<el-input v-model="form.factory_name" />
</el-form-item>
<el-form-item label="供应商简称" prop="short_name" required>
<el-input v-model="form.short_name" />
</el-form-item>
<el-form-item label="统一社会信用代码" prop="unified_social_credit_code" required>
<el-input
v-model="form.unified_social_credit_code"
maxlength="18"
placeholder="18 位数字或大写字母"
/>
</el-form-item>
<el-form-item label="合作模式" prop="cooperation_mode" required>
<el-select v-model="form.cooperation_mode" placeholder="请选择" style="width: 100%">
<el-option
v-for="item in cooperationModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="省市区" required>
<el-cascader
v-model="regionValue"
:options="regionOptions"
:props="{ value: 'label' }"
clearable
@change="onRegionChange"
/>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" type="textarea" />
</el-form-item>
<el-form-item label="官网" prop="website">
<el-input v-model="form.website" />
</el-form-item>
<el-form-item label="交互能力" prop="interaction_capability">
<el-input
v-model="form.interaction_capability"
type="textarea"
:rows="4"
placeholder="最大月供量 / 常规库存量 / 生产周期 / 运输方式 / 应急供货响应时间"
/>
</el-form-item>
</el-form>
<FactoryForm ref="factoryFormRef" v-model="form" mode="edit" />
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">保存</el-button>
@ -115,10 +61,10 @@
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { regionData } from 'element-china-area-data'
import { useAuth } from '@/store/auth'
import { formatRegion, regionLabel } from '@/utils/region'
import { formatRegion } from '@/utils/region'
import { fetchFactories, fetchFactoryDetail, createFactory, updateFactory, deleteFactory } from '@/api/factory'
import FactoryForm from '@/views/factory/FactoryForm.vue'
const router = useRouter()
const { isAdmin } = useAuth()
@ -133,40 +79,9 @@ const dialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const currentId = ref(null)
const formRef = ref(null)
const factoryFormRef = ref(null)
const regionOptions = regionData
const regionValue = ref([])
const cooperationModeOptions = [
{ value: 'direct', label: '厂家直供' },
{ value: 'authorized', label: '授权代理商' }
]
const USCC_REGEX = /^[0-9A-Z]{18}$/
const formRules = {
factory_name: [{ required: true, message: '请输入供应商全称', trigger: 'blur' }],
short_name: [{ required: true, message: '请输入供应商简称', trigger: 'blur' }],
unified_social_credit_code: [
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
{
validator: (_rule, value, cb) => {
if (!value) return cb()
if (!USCC_REGEX.test(value)) {
return cb(new Error('必须为 18 位数字或大写字母'))
}
cb()
},
trigger: 'blur'
}
],
cooperation_mode: [{ required: true, message: '请选择合作模式', trigger: 'change' }]
}
const form = reactive({
dealer_name: '',
product_category: '',
const emptyForm = () => ({
factory_name: '',
short_name: '',
unified_social_credit_code: '',
@ -179,6 +94,8 @@ const form = reactive({
website: ''
})
const form = ref(emptyForm())
const loadFactories = async () => {
tableLoading.value = true
try {
@ -190,58 +107,35 @@ const loadFactories = async () => {
}
}
const resetForm = () => {
form.dealer_name = ''
form.product_category = ''
form.factory_name = ''
form.short_name = ''
form.unified_social_credit_code = ''
form.cooperation_mode = ''
form.interaction_capability = ''
form.province = ''
form.city = ''
form.district = ''
form.address = ''
form.website = ''
regionValue.value = []
formRef.value?.clearValidate()
}
const onRegionChange = (val) => {
form.province = val?.[0] || ''
form.city = val?.[1] || ''
form.district = val?.[2] || ''
}
const openCreate = () => {
resetForm()
form.value = emptyForm()
factoryFormRef.value?.clearValidate()
isEdit.value = false
dialogTitle.value = '新增供应商'
dialogVisible.value = true
}
const openEdit = async (row) => {
resetForm()
isEdit.value = true
currentId.value = row.id
const detail = await fetchFactoryDetail(row.id)
Object.assign(form, detail)
regionValue.value = [detail.province, detail.city, detail.district].filter(Boolean).map(regionLabel)
form.value = { ...emptyForm(), ...detail }
factoryFormRef.value?.clearValidate()
dialogTitle.value = '编辑供应商'
dialogVisible.value = true
}
const onSubmit = async () => {
try {
await formRef.value?.validate()
await factoryFormRef.value?.validate()
} catch (_) {
return
}
try {
if (isEdit.value) {
await updateFactory(currentId.value, { ...form })
await updateFactory(currentId.value, { ...form.value })
} else {
await createFactory({ ...form })
await createFactory({ ...form.value })
}
ElMessage.success('保存成功')
dialogVisible.value = false
@ -292,4 +186,3 @@ onMounted(() => {
padding-right: 8px;
}
</style>

View File

@ -5,43 +5,7 @@
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
</div>
<div class="card detail-card" v-if="material">
<el-descriptions :column="1" border class="detail-descriptions">
<el-descriptions-item label="材料名称">{{ displayText(material.name) }}</el-descriptions-item>
<el-descriptions-item label="专业类别">{{ displayText(material.major_category_display) }}</el-descriptions-item>
<el-descriptions-item label="材料分类">{{ displayText(material.material_category) }}</el-descriptions-item>
<el-descriptions-item label="材料子类">{{ displayText(material.material_subcategory) }}</el-descriptions-item>
<el-descriptions-item label="阶段">{{ displayText(material.stage_display) }}</el-descriptions-item>
<el-descriptions-item label="重要等级">{{ displayText(material.importance_level_display) }}</el-descriptions-item>
<el-descriptions-item label="落地项目">{{ displayText(material.landing_project) }}</el-descriptions-item>
<el-descriptions-item label="对接人">{{ displayText(material.contact_person) }}</el-descriptions-item>
<el-descriptions-item label="对接人联系方式">{{ displayText(material.contact_phone) }}</el-descriptions-item>
<el-descriptions-item label="经办人">{{ displayText(material.handler) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ displayText(material.remark) }}</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ displayText(material.spec) }}</el-descriptions-item>
<el-descriptions-item label="符合标准">{{ displayText(material.standard) }}</el-descriptions-item>
<el-descriptions-item label="应用场景">{{ displayList(material.application_scene_display) }}</el-descriptions-item>
<el-descriptions-item label="应用说明">{{ displayText(material.application_desc) }}</el-descriptions-item>
<el-descriptions-item label="替代材料类型">{{ displayText(material.replace_type_display) }}</el-descriptions-item>
<el-descriptions-item label="竞争优势">{{ displayList(material.advantage_display) }}</el-descriptions-item>
<el-descriptions-item label="优势说明">{{ displayText(material.advantage_desc) }}</el-descriptions-item>
<el-descriptions-item label="成本对比">{{ formatPercent(material.cost_compare) }}</el-descriptions-item>
<el-descriptions-item label="成本说明">{{ displayText(material.cost_desc) }}</el-descriptions-item>
<el-descriptions-item label="案例">{{ displayText(material.cases) }}</el-descriptions-item>
<el-descriptions-item label="所属供应商">{{ displayText(material.factory_name) }}</el-descriptions-item>
<el-descriptions-item label="品牌">{{ displayText(material.brand_name) }}</el-descriptions-item>
<el-descriptions-item label="质量等级">{{ formatStarLevel(material.quality_level) }}</el-descriptions-item>
<el-descriptions-item label="耐久等级">{{ formatStarLevel(material.durability_level) }}</el-descriptions-item>
<el-descriptions-item label="环保等级">{{ formatStarLevel(material.eco_level) }}</el-descriptions-item>
<el-descriptions-item label="低碳等级">{{ formatStarLevel(material.carbon_level) }}</el-descriptions-item>
<el-descriptions-item label="总评分">{{ formatStarLevel(material.score_level) }}</el-descriptions-item>
<el-descriptions-item label="连接方式">{{ displayText(material.connection_method) }}</el-descriptions-item>
<el-descriptions-item label="施工工艺">{{ displayText(material.construction_method) }}</el-descriptions-item>
<el-descriptions-item label="限制条件">{{ displayText(material.limit_condition) }}</el-descriptions-item>
</el-descriptions>
<div v-if="material.brochure_url" class="brochure">
<div class="brochure-title">宣传页</div>
<img :src="material.brochure_url" alt="宣传页" />
</div>
<MaterialForm :model-value="material" mode="view" @update:model-value="() => {}" />
</div>
</div>
</template>
@ -50,6 +14,7 @@
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchMaterialDetail } from '@/api/material'
import MaterialForm from '@/views/material/MaterialForm.vue'
const route = useRoute()
const router = useRouter()
@ -64,14 +29,6 @@ onMounted(loadDetail)
const goBack = () => {
router.back()
}
const displayText = (value) => value || '-'
const displayList = (value) => (value?.length ? value.join('、') : '-')
const formatPercent = (value) => (value === null || value === undefined || value === '' ? '-' : `${value}%`)
const formatStarLevel = (value) => (value ? `${value}` : '-')
</script>
<style scoped>
@ -80,20 +37,4 @@ const formatStarLevel = (value) => (value ? `${value}星` : '-')
align-items: center;
justify-content: space-between;
}
.brochure {
margin-top: 20px;
}
.brochure-title {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
}
.brochure img {
max-width: 100%;
border-radius: 8px;
border: 1px solid #eee;
}
</style>

View File

@ -75,210 +75,20 @@
/>
</div>
<el-drawer
<el-dialog
v-if="dialogVisible"
v-model="dialogVisible"
:title="dialogTitle"
size="60%"
width="720px"
class="dialog-scroll"
:close-on-click-modal="false"
:destroy-on-close="false"
class="material-drawer"
>
<el-form :model="form" label-width="110px">
<el-form-item label="材料名称" required>
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="材料大类" required>
<el-select v-model="form.major_category">
<el-option v-for="item in majorOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="细分种类" required>
<el-select v-model="form.material_category" filterable @change="onCategoryChange">
<el-option v-for="item in categoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="材料子类">
<el-select v-model="form.material_subcategory" filterable clearable>
<el-option v-for="item in subcategoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="阶段">
<el-select v-model="form.stage" clearable>
<el-option v-for="item in stageOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="重要等级">
<el-select v-model="form.importance_level" clearable>
<el-option v-for="item in importanceLevelOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="落地项目">
<el-input v-model="form.landing_project" />
</el-form-item>
<el-form-item label="对接人">
<el-input v-model="form.contact_person" />
</el-form-item>
<el-form-item label="对接人联系方式">
<el-input v-model="form.contact_phone" />
</el-form-item>
<el-form-item label="经办人">
<el-input v-model="form.handler" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" />
</el-form-item>
<el-form-item label="规格型号">
<el-input v-model="form.spec" />
</el-form-item>
<el-form-item label="符合标准">
<el-input v-model="form.standard" />
</el-form-item>
<el-form-item label="应用场景">
<el-select v-model="form.application_scene" multiple>
<el-option v-for="item in sceneOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="应用说明">
<el-input v-model="form.application_desc" type="textarea" />
</el-form-item>
<el-form-item label="替代材料类型">
<el-select v-model="form.replace_type" clearable>
<el-option v-for="item in replaceOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="竞争优势">
<el-select v-model="form.advantage" multiple>
<el-option v-for="item in advantageOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="优势说明">
<el-input v-model="form.advantage_desc" type="textarea" />
</el-form-item>
<el-form-item label="成本对比(%)">
<el-input-number v-model="form.cost_compare" :min="-100" :max="100" />
</el-form-item>
<el-form-item label="成本说明">
<el-input v-model="form.cost_desc" type="textarea" />
</el-form-item>
<el-form-item label="案例">
<el-input v-model="form.cases" type="textarea" />
</el-form-item>
<el-form-item label="宣传页">
<el-upload
class="upload"
:auto-upload="true"
:show-file-list="false"
:http-request="handleUpload"
accept="image/*"
>
<el-button :loading="uploading">{{ uploading ? '上传中...' : '选择图片' }}</el-button>
</el-upload>
<div v-if="form.brochure_url" class="preview">
<img :src="form.brochure_url" alt="预览" />
</div>
</el-form-item>
<el-form-item label="质量等级">
<el-select v-model="form.quality_level" clearable>
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="耐久等级">
<el-select v-model="form.durability_level" clearable>
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="环保等级">
<el-select v-model="form.eco_level" clearable>
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="低碳等级">
<el-select v-model="form.carbon_level" clearable>
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="总评分">
<el-select v-model="form.score_level" clearable>
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="连接方式">
<el-input v-model="form.connection_method" />
</el-form-item>
<el-form-item label="施工工艺">
<el-input v-model="form.construction_method" />
</el-form-item>
<el-form-item label="限制条件">
<el-input v-model="form.limit_condition" type="textarea" />
</el-form-item>
<el-form-item label="供应商" v-if="isAdmin">
<el-select v-model="form.factory">
<el-option v-for="item in factories" :key="item.id" :label="item.short_name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="品牌" required>
<el-select
v-model="form.brand"
filterable
remote
:remote-method="searchBrandsForForm"
:loading="brandFormSearchLoading"
placeholder="请选择品牌"
>
<el-option v-for="item in brandFormOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<MaterialForm ref="materialFormRef" v-model="form" mode="edit" />
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</template>
</el-drawer>
<el-drawer
v-model="detailVisible"
title="材料详情"
size="60%"
class="material-drawer"
>
<el-descriptions v-if="detailData" :column="1" border>
<el-descriptions-item label="材料名称">{{ displayText(detailData.name) }}</el-descriptions-item>
<el-descriptions-item label="材料大类">{{ displayText(detailData.major_category_display) }}</el-descriptions-item>
<el-descriptions-item label="细分种类">{{ displayText(detailData.material_category) }}</el-descriptions-item>
<el-descriptions-item label="材料子类">{{ displayText(detailData.material_subcategory) }}</el-descriptions-item>
<el-descriptions-item label="阶段">{{ displayText(detailData.stage_display) }}</el-descriptions-item>
<el-descriptions-item label="重要等级">{{ displayText(detailData.importance_level_display) }}</el-descriptions-item>
<el-descriptions-item label="落地项目">{{ displayText(detailData.landing_project) }}</el-descriptions-item>
<el-descriptions-item label="对接人">{{ displayText(detailData.contact_person) }}</el-descriptions-item>
<el-descriptions-item label="对接人联系方式">{{ displayText(detailData.contact_phone) }}</el-descriptions-item>
<el-descriptions-item label="经办人">{{ displayText(detailData.handler) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ displayText(detailData.remark) }}</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ displayText(detailData.spec) }}</el-descriptions-item>
<el-descriptions-item label="符合标准">{{ displayText(detailData.standard) }}</el-descriptions-item>
<el-descriptions-item label="应用场景">{{ displayList(detailData.application_scene_display) }}</el-descriptions-item>
<el-descriptions-item label="应用说明">{{ displayText(detailData.application_desc) }}</el-descriptions-item>
<el-descriptions-item label="替代材料类型">{{ displayText(detailData.replace_type_display) }}</el-descriptions-item>
<el-descriptions-item label="竞争优势">{{ displayList(detailData.advantage_display) }}</el-descriptions-item>
<el-descriptions-item label="优势说明">{{ displayText(detailData.advantage_desc) }}</el-descriptions-item>
<el-descriptions-item label="成本对比">{{ formatPercent(detailData.cost_compare) }}</el-descriptions-item>
<el-descriptions-item label="成本说明">{{ displayText(detailData.cost_desc) }}</el-descriptions-item>
<el-descriptions-item label="案例">{{ displayText(detailData.cases) }}</el-descriptions-item>
<el-descriptions-item label="供应商">{{ displayText(detailData.factory_name) }}</el-descriptions-item>
<el-descriptions-item label="品牌">{{ displayText(detailData.brand_name) }}</el-descriptions-item>
<el-descriptions-item label="质量等级">{{ formatStarLevel(detailData.quality_level) }}</el-descriptions-item>
<el-descriptions-item label="耐久等级">{{ formatStarLevel(detailData.durability_level) }}</el-descriptions-item>
<el-descriptions-item label="环保等级">{{ formatStarLevel(detailData.eco_level) }}</el-descriptions-item>
<el-descriptions-item label="低碳等级">{{ formatStarLevel(detailData.carbon_level) }}</el-descriptions-item>
<el-descriptions-item label="总评分">{{ formatStarLevel(detailData.score_level) }}</el-descriptions-item>
<el-descriptions-item label="连接方式">{{ displayText(detailData.connection_method) }}</el-descriptions-item>
<el-descriptions-item label="施工工艺">{{ displayText(detailData.construction_method) }}</el-descriptions-item>
<el-descriptions-item label="限制条件">{{ displayText(detailData.limit_condition) }}</el-descriptions-item>
</el-descriptions>
<div v-if="detailData?.brochure_url" class="brochure">
<div class="brochure-title">宣传页</div>
<img :src="detailData.brochure_url" alt="宣传页" />
</div>
</el-drawer>
</el-dialog>
<el-dialog v-model="importDialogVisible" title="导入材料" width="420px">
<div class="import-dialog">
@ -303,13 +113,27 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAuth } from '@/store/auth'
import { fetchMaterials, fetchMaterialDetail, createMaterial, updateMaterial, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial, fetchMaterialChoices, uploadImage, importMaterialsExcel, exportMaterialsExcel } from '@/api/material'
import { fetchCategories, fetchSubcategories } from '@/api/category'
import { fetchFactorySimple } from '@/api/factory'
import {
fetchMaterials,
fetchMaterialDetail,
createMaterial,
updateMaterial,
deleteMaterial,
submitMaterial,
approveMaterial,
rejectMaterial,
fetchMaterialChoices,
importMaterialsExcel,
exportMaterialsExcel
} from '@/api/material'
import { fetchSubcategories } from '@/api/category'
import { fetchBrands } from '@/api/brand'
import MaterialForm from '@/views/material/MaterialForm.vue'
const router = useRouter()
const { isAdmin } = useAuth()
const materials = ref([])
const tableLoading = ref(false)
@ -318,20 +142,16 @@ const pagination = reactive({
pageSize: 20,
total: 0
})
const factories = ref([])
const dialogVisible = ref(false)
const detailVisible = ref(false)
const detailData = ref(null)
const importDialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const currentId = ref(null)
const uploading = ref(false)
const importing = ref(false)
const exporting = ref(false)
// const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL).replace(/\/api$/, "");
// const templateDownloadUrl = `${apiBaseUrl}/media/material_import_template.xlsx`
const materialFormRef = ref(null)
const templateDownloadUrl = `http://101.42.1.64:2260/media/material_import_template.xlsx`
const filters = reactive({
name: '',
status: '',
@ -340,11 +160,11 @@ const filters = reactive({
})
const brandFilterOptions = ref([])
const brandFormOptions = ref([])
const brandSearchLoading = ref(false)
const brandFormSearchLoading = ref(false)
const statusOptions = ref([])
const filterSubcategoryOptions = ref([])
const form = reactive({
const emptyForm = () => ({
name: '',
major_category: '',
material_category: '',
@ -380,18 +200,7 @@ const form = reactive({
brand: null
})
const majorOptions = ref([])
const stageOptions = ref([])
const importanceLevelOptions = ref([])
const replaceOptions = ref([])
const advantageOptions = ref([])
const sceneOptions = ref([])
const starOptions = ref([])
const statusOptions = ref([])
const categoryOptions = ref([])
const subcategoryOptions = ref([])
const filterSubcategoryOptions = ref([])
const allSubcategories = ref([])
const form = ref(emptyForm())
const loadMaterials = async () => {
tableLoading.value = true
@ -408,82 +217,20 @@ const loadMaterials = async () => {
}
}
const loadChoices = async () => {
const loadStatusOptions = async () => {
const data = await fetchMaterialChoices()
majorOptions.value = data.major_category
stageOptions.value = data.stage
importanceLevelOptions.value = data.importance_level
replaceOptions.value = data.replace_type
advantageOptions.value = data.advantage
sceneOptions.value = data.application_scene
starOptions.value = data.star_level
statusOptions.value = data.status
}
const loadCategories = async () => {
const data = await fetchCategories()
categoryOptions.value = (data.results || data).map((item) => ({
const loadFilterSubcategories = async () => {
const data = await fetchSubcategories({})
filterSubcategoryOptions.value = (data.results || data).map((item) => ({
id: item.id,
name: item.name,
value: item.value
}))
}
const loadSubcategories = async (categoryId = '') => {
const data = await fetchSubcategories(categoryId ? { category_id: categoryId } : {})
allSubcategories.value = data.results || data
const mapped = allSubcategories.value.map((item) => ({
id: item.id,
name: item.name,
value: item.value,
category: item.category
}))
filterSubcategoryOptions.value = mapped
subcategoryOptions.value = mapped
}
const loadFactories = async () => {
factories.value = await fetchFactorySimple()
}
const resetForm = () => {
Object.assign(form, {
name: '',
major_category: '',
material_category: '',
material_subcategory: '',
stage: '',
importance_level: '',
landing_project: '',
contact_person: '',
contact_phone: '',
handler: '',
remark: '',
spec: '',
standard: '',
application_scene: [],
application_desc: '',
replace_type: '',
advantage: [],
advantage_desc: '',
cost_compare: null,
cost_desc: '',
cases: '',
brochure: null,
brochure_url: '',
quality_level: null,
durability_level: null,
eco_level: null,
carbon_level: null,
score_level: null,
connection_method: '',
construction_method: '',
limit_condition: '',
factory: null,
brand: null
})
}
const loadBrandFilterOptions = async () => {
const data = await fetchBrands({ page_size: 100 })
brandFilterOptions.value = data.results || data
@ -499,85 +246,36 @@ const searchBrands = async (query) => {
}
}
const searchBrandsForForm = async (query) => {
brandFormSearchLoading.value = true
try {
const data = await fetchBrands({ page_size: 50, search: query || '' })
brandFormOptions.value = data.results || data
} finally {
brandFormSearchLoading.value = false
}
}
const ensureBrandInFormOptions = (brandId, brandName) => {
if (!brandId) return
if (!brandFormOptions.value.some((item) => item.id === brandId)) {
brandFormOptions.value = [{ id: brandId, name: brandName || '' }, ...brandFormOptions.value]
}
}
const onCategoryChange = async (val, resetSub = true) => {
if (!val) {
await loadSubcategories()
return
}
const category = categoryOptions.value.find((item) => item.value === val)
if (category) {
await loadSubcategories(category.id)
}
if (resetSub) {
form.material_subcategory = ''
}
}
const openCreate = async () => {
resetForm()
const openCreate = () => {
form.value = emptyForm()
isEdit.value = false
dialogTitle.value = '新增材料'
await searchBrandsForForm('')
dialogVisible.value = true
}
const openEdit = async (row) => {
resetForm()
isEdit.value = true
currentId.value = row.id
const item = await fetchMaterialDetail(row.id)
Object.assign(form, item)
form.application_scene = item.application_scene || []
form.advantage = item.advantage || []
form.brochure_url = item.brochure_url || ''
form.brand = item.brand || null
if (form.material_category) {
await onCategoryChange(form.material_category, false)
form.value = {
...emptyForm(),
...item,
application_scene: item.application_scene || [],
advantage: item.advantage || [],
brochure_url: item.brochure_url || '',
brand: item.brand || null
}
await searchBrandsForForm('')
ensureBrandInFormOptions(item.brand, item.brand_name)
dialogTitle.value = '编辑材料'
dialogVisible.value = true
}
const handleUpload = async (options) => {
uploading.value = true
try {
const result = await uploadImage(options.file)
form.brochure = result.path
form.brochure_url = result.url
ElMessage.success('图片上传成功')
} catch {
ElMessage.error('图片上传失败')
} finally {
uploading.value = false
}
}
const onSave = async () => {
if (!form.brand) {
if (!form.value.brand) {
ElMessage.warning('请选择品牌')
return
}
try {
const payload = { ...form }
const payload = { ...form.value }
delete payload.brochure_url
delete payload.brand_name
if (!isAdmin.value) {
@ -689,20 +387,10 @@ const canDelete = (row) => isAdmin.value || row.status === 'draft'
const canSubmit = (row) => !isAdmin.value && row.status === 'draft'
const canApprove = (row) => isAdmin.value && row.status === 'pending'
const goDetail = async (row) => {
try {
detailData.value = await fetchMaterialDetail(row.id)
detailVisible.value = true
} catch (error) {
ElMessage.error(error.response?.data?.detail || '加载详情失败')
}
const goDetail = (row) => {
router.push(`/materials/${row.id}`)
}
const displayText = (value) => value || '-'
const displayList = (value) => (value?.length ? value.join('、') : '-')
const formatPercent = (value) => (value === null || value === undefined || value === '' ? '-' : `${value}%`)
const formatStarLevel = (value) => (value ? `${value}` : '-')
const onPageChange = (page) => {
pagination.page = page
loadMaterials()
@ -715,10 +403,8 @@ const onPageSizeChange = (size) => {
}
onMounted(() => {
loadChoices()
loadCategories()
loadSubcategories()
loadFactories()
loadStatusOptions()
loadFilterSubcategories()
loadBrandFilterOptions()
loadMaterials()
})
@ -746,38 +432,4 @@ onMounted(() => {
overflow: auto;
padding-right: 8px;
}
.material-drawer :deep(.el-drawer__body) {
padding: 20px 24px;
overflow-y: auto;
}
.material-drawer :deep(.el-drawer__footer) {
padding: 12px 24px;
}
.brochure {
margin-top: 20px;
}
.brochure-title {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
}
.brochure img {
max-width: 100%;
border-radius: 8px;
border: 1px solid #eee;
}
</style>
<style scoped>
.preview img {
width: 120px;
margin-top: 8px;
border-radius: 6px;
border: 1px solid #eee;
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<el-form
v-if="mode === 'edit'"
ref="formRef"
:model="modelValue"
:rules="formRules"
label-width="140px"
>
<el-form-item label="供应商全称" prop="factory_name" required>
<el-input :model-value="modelValue.factory_name" @update:model-value="updateField('factory_name', $event)" />
</el-form-item>
<el-form-item label="供应商简称" prop="short_name" required>
<el-input :model-value="modelValue.short_name" @update:model-value="updateField('short_name', $event)" />
</el-form-item>
<el-form-item label="统一社会信用代码" prop="unified_social_credit_code" required>
<el-input
:model-value="modelValue.unified_social_credit_code"
maxlength="18"
placeholder="18 位数字或大写字母"
@update:model-value="updateField('unified_social_credit_code', $event)"
/>
</el-form-item>
<el-form-item label="合作模式" prop="cooperation_mode" required>
<el-select
:model-value="modelValue.cooperation_mode"
placeholder="请选择"
style="width: 100%"
@update:model-value="updateField('cooperation_mode', $event)"
>
<el-option
v-for="item in cooperationModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="省市区" required>
<el-cascader
v-model="regionValue"
:options="regionOptions"
:props="{ value: 'label' }"
clearable
@change="onRegionChange"
/>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input
:model-value="modelValue.address"
type="textarea"
@update:model-value="updateField('address', $event)"
/>
</el-form-item>
<el-form-item label="官网" prop="website">
<el-input :model-value="modelValue.website" @update:model-value="updateField('website', $event)" />
</el-form-item>
<el-form-item label="交互能力" prop="interaction_capability">
<el-input
:model-value="modelValue.interaction_capability"
type="textarea"
:rows="4"
placeholder="最大月供量 / 常规库存量 / 生产周期 / 运输方式 / 应急供货响应时间"
@update:model-value="updateField('interaction_capability', $event)"
/>
</el-form-item>
</el-form>
<el-descriptions v-else :column="1" border class="detail-descriptions">
<el-descriptions-item label="供应商全称">{{ displayText(modelValue.factory_name) }}</el-descriptions-item>
<el-descriptions-item label="供应商简称">{{ displayText(modelValue.short_name) }}</el-descriptions-item>
<el-descriptions-item label="统一社会信用代码">{{ displayText(modelValue.unified_social_credit_code) }}</el-descriptions-item>
<el-descriptions-item label="合作模式">{{ displayText(modelValue.cooperation_mode_display) }}</el-descriptions-item>
<el-descriptions-item label="地区">{{ displayRegion(modelValue) }}</el-descriptions-item>
<el-descriptions-item label="地址">{{ displayText(modelValue.address) }}</el-descriptions-item>
<el-descriptions-item label="交互能力">
<span class="multiline">{{ displayText(modelValue.interaction_capability) }}</span>
</el-descriptions-item>
<el-descriptions-item label="官网">
<a
v-if="modelValue.website"
:href="modelValue.website"
target="_blank"
rel="noopener noreferrer"
class="website-link"
>
{{ modelValue.website }}
</a>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item v-if="showReadonlyExtras" label="用户账号">
{{ displayList(modelValue.usernames) }}
</el-descriptions-item>
<el-descriptions-item v-if="showReadonlyExtras" label="材料数量">
{{ displayText(modelValue.material_count) }}
</el-descriptions-item>
</el-descriptions>
</template>
<script setup>
import { ref, watch } from 'vue'
import { regionData } from 'element-china-area-data'
import { formatRegion, regionLabel } from '@/utils/region'
const props = defineProps({
modelValue: { type: Object, required: true },
mode: { type: String, default: 'edit' },
showReadonlyExtras: { type: Boolean, default: true }
})
const emit = defineEmits(['update:modelValue'])
const formRef = ref(null)
const regionOptions = regionData
const regionValue = ref([])
const cooperationModeOptions = [
{ value: 'direct', label: '厂家直供' },
{ value: 'authorized', label: '授权代理商' }
]
const USCC_REGEX = /^[0-9A-Z]{18}$/
const formRules = {
factory_name: [{ required: true, message: '请输入供应商全称', trigger: 'blur' }],
short_name: [{ required: true, message: '请输入供应商简称', trigger: 'blur' }],
unified_social_credit_code: [
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
{
validator: (_rule, value, cb) => {
if (!value) return cb()
if (!USCC_REGEX.test(value)) {
return cb(new Error('必须为 18 位数字或大写字母'))
}
cb()
},
trigger: 'blur'
}
],
cooperation_mode: [{ required: true, message: '请选择合作模式', trigger: 'change' }]
}
const updateField = (key, value) => {
emit('update:modelValue', { ...props.modelValue, [key]: value })
}
const onRegionChange = (val) => {
emit('update:modelValue', {
...props.modelValue,
province: val?.[0] || '',
city: val?.[1] || '',
district: val?.[2] || ''
})
}
watch(
() => [props.modelValue.province, props.modelValue.city, props.modelValue.district],
([p, c, d]) => {
const next = [p, c, d].filter(Boolean).map(regionLabel)
if (next.join('|') !== regionValue.value.join('|')) {
regionValue.value = next
}
},
{ immediate: true }
)
const displayText = (value) =>
value === null || value === undefined || value === '' ? '-' : value
const displayList = (value) => (value?.length ? value.join('、') : '-')
const displayRegion = (item) => formatRegion(item.province, item.city, item.district) || '-'
const validate = () => formRef.value?.validate()
const clearValidate = () => formRef.value?.clearValidate()
defineExpose({ validate, clearValidate })
</script>
<style scoped>
.multiline {
white-space: pre-wrap;
}
.website-link {
color: var(--brand-500);
word-break: break-all;
}
</style>

View File

@ -0,0 +1,400 @@
<template>
<el-form
v-if="mode === 'edit'"
ref="formRef"
:model="modelValue"
label-width="120px"
>
<el-form-item label="材料名称" required>
<el-input :model-value="modelValue.name" @update:model-value="set('name', $event)" />
</el-form-item>
<el-form-item label="材料大类" required>
<el-select :model-value="modelValue.major_category" @update:model-value="set('major_category', $event)">
<el-option v-for="item in majorOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="细分种类" required>
<el-select
:model-value="modelValue.material_category"
filterable
@update:model-value="onCategoryChange"
>
<el-option v-for="item in categoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="材料子类">
<el-select
:model-value="modelValue.material_subcategory"
filterable
clearable
@update:model-value="set('material_subcategory', $event)"
>
<el-option v-for="item in subcategoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="阶段">
<el-select :model-value="modelValue.stage" clearable @update:model-value="set('stage', $event)">
<el-option v-for="item in stageOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="重要等级">
<el-select :model-value="modelValue.importance_level" clearable @update:model-value="set('importance_level', $event)">
<el-option v-for="item in importanceLevelOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="落地项目">
<el-input :model-value="modelValue.landing_project" @update:model-value="set('landing_project', $event)" />
</el-form-item>
<el-form-item label="对接人">
<el-input :model-value="modelValue.contact_person" @update:model-value="set('contact_person', $event)" />
</el-form-item>
<el-form-item label="对接人联系方式">
<el-input :model-value="modelValue.contact_phone" @update:model-value="set('contact_phone', $event)" />
</el-form-item>
<el-form-item label="经办人">
<el-input :model-value="modelValue.handler" @update:model-value="set('handler', $event)" />
</el-form-item>
<el-form-item label="备注">
<el-input :model-value="modelValue.remark" @update:model-value="set('remark', $event)" />
</el-form-item>
<el-form-item label="规格型号">
<el-input :model-value="modelValue.spec" @update:model-value="set('spec', $event)" />
</el-form-item>
<el-form-item label="符合标准">
<el-input :model-value="modelValue.standard" @update:model-value="set('standard', $event)" />
</el-form-item>
<el-form-item label="应用场景">
<el-select :model-value="modelValue.application_scene" multiple @update:model-value="set('application_scene', $event)">
<el-option v-for="item in sceneOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="应用说明">
<el-input :model-value="modelValue.application_desc" type="textarea" @update:model-value="set('application_desc', $event)" />
</el-form-item>
<el-form-item label="替代材料类型">
<el-select :model-value="modelValue.replace_type" clearable @update:model-value="set('replace_type', $event)">
<el-option v-for="item in replaceOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="竞争优势">
<el-select :model-value="modelValue.advantage" multiple @update:model-value="set('advantage', $event)">
<el-option v-for="item in advantageOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="优势说明">
<el-input :model-value="modelValue.advantage_desc" type="textarea" @update:model-value="set('advantage_desc', $event)" />
</el-form-item>
<el-form-item label="成本对比(%)">
<el-input-number
:model-value="modelValue.cost_compare"
:min="-100"
:max="100"
@update:model-value="set('cost_compare', $event)"
/>
</el-form-item>
<el-form-item label="成本说明">
<el-input :model-value="modelValue.cost_desc" type="textarea" @update:model-value="set('cost_desc', $event)" />
</el-form-item>
<el-form-item label="案例">
<el-input :model-value="modelValue.cases" type="textarea" @update:model-value="set('cases', $event)" />
</el-form-item>
<el-form-item label="宣传页">
<el-upload
class="upload"
:auto-upload="true"
:show-file-list="false"
:http-request="handleUpload"
accept="image/*"
>
<el-button :loading="uploading">{{ uploading ? '上传中...' : '选择图片' }}</el-button>
</el-upload>
<div v-if="modelValue.brochure_url" class="preview">
<img :src="modelValue.brochure_url" alt="预览" />
</div>
</el-form-item>
<el-form-item label="质量等级">
<el-select :model-value="modelValue.quality_level" clearable @update:model-value="set('quality_level', $event)">
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="耐久等级">
<el-select :model-value="modelValue.durability_level" clearable @update:model-value="set('durability_level', $event)">
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="环保等级">
<el-select :model-value="modelValue.eco_level" clearable @update:model-value="set('eco_level', $event)">
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="低碳等级">
<el-select :model-value="modelValue.carbon_level" clearable @update:model-value="set('carbon_level', $event)">
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="总评分">
<el-select :model-value="modelValue.score_level" clearable @update:model-value="set('score_level', $event)">
<el-option v-for="item in starOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select>
</el-form-item>
<el-form-item label="连接方式">
<el-input :model-value="modelValue.connection_method" @update:model-value="set('connection_method', $event)" />
</el-form-item>
<el-form-item label="施工工艺">
<el-input :model-value="modelValue.construction_method" @update:model-value="set('construction_method', $event)" />
</el-form-item>
<el-form-item label="限制条件">
<el-input :model-value="modelValue.limit_condition" type="textarea" @update:model-value="set('limit_condition', $event)" />
</el-form-item>
<el-form-item v-if="isAdmin" label="供应商">
<el-select :model-value="modelValue.factory" @update:model-value="set('factory', $event)">
<el-option v-for="item in factories" :key="item.id" :label="item.short_name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="品牌" required>
<el-select
:model-value="modelValue.brand"
filterable
remote
:remote-method="searchBrandsForForm"
:loading="brandFormSearchLoading"
placeholder="请选择品牌"
@update:model-value="set('brand', $event)"
>
<el-option v-for="item in brandFormOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template v-else>
<el-descriptions :column="1" border>
<el-descriptions-item label="材料名称">{{ displayText(modelValue.name) }}</el-descriptions-item>
<el-descriptions-item label="材料大类">{{ displayText(modelValue.major_category_display) }}</el-descriptions-item>
<el-descriptions-item label="细分种类">{{ displayText(modelValue.material_category) }}</el-descriptions-item>
<el-descriptions-item label="材料子类">{{ displayText(modelValue.material_subcategory) }}</el-descriptions-item>
<el-descriptions-item label="阶段">{{ displayText(modelValue.stage_display) }}</el-descriptions-item>
<el-descriptions-item label="重要等级">{{ displayText(modelValue.importance_level_display) }}</el-descriptions-item>
<el-descriptions-item label="落地项目">{{ displayText(modelValue.landing_project) }}</el-descriptions-item>
<el-descriptions-item label="对接人">{{ displayText(modelValue.contact_person) }}</el-descriptions-item>
<el-descriptions-item label="对接人联系方式">{{ displayText(modelValue.contact_phone) }}</el-descriptions-item>
<el-descriptions-item label="经办人">{{ displayText(modelValue.handler) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ displayText(modelValue.remark) }}</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ displayText(modelValue.spec) }}</el-descriptions-item>
<el-descriptions-item label="符合标准">{{ displayText(modelValue.standard) }}</el-descriptions-item>
<el-descriptions-item label="应用场景">{{ displayList(modelValue.application_scene_display) }}</el-descriptions-item>
<el-descriptions-item label="应用说明">{{ displayText(modelValue.application_desc) }}</el-descriptions-item>
<el-descriptions-item label="替代材料类型">{{ displayText(modelValue.replace_type_display) }}</el-descriptions-item>
<el-descriptions-item label="竞争优势">{{ displayList(modelValue.advantage_display) }}</el-descriptions-item>
<el-descriptions-item label="优势说明">{{ displayText(modelValue.advantage_desc) }}</el-descriptions-item>
<el-descriptions-item label="成本对比">{{ formatPercent(modelValue.cost_compare) }}</el-descriptions-item>
<el-descriptions-item label="成本说明">{{ displayText(modelValue.cost_desc) }}</el-descriptions-item>
<el-descriptions-item label="案例">{{ displayText(modelValue.cases) }}</el-descriptions-item>
<el-descriptions-item label="供应商">{{ displayText(modelValue.factory_name) }}</el-descriptions-item>
<el-descriptions-item label="品牌">{{ displayText(modelValue.brand_name) }}</el-descriptions-item>
<el-descriptions-item label="质量等级">{{ formatStarLevel(modelValue.quality_level) }}</el-descriptions-item>
<el-descriptions-item label="耐久等级">{{ formatStarLevel(modelValue.durability_level) }}</el-descriptions-item>
<el-descriptions-item label="环保等级">{{ formatStarLevel(modelValue.eco_level) }}</el-descriptions-item>
<el-descriptions-item label="低碳等级">{{ formatStarLevel(modelValue.carbon_level) }}</el-descriptions-item>
<el-descriptions-item label="总评分">{{ formatStarLevel(modelValue.score_level) }}</el-descriptions-item>
<el-descriptions-item label="连接方式">{{ displayText(modelValue.connection_method) }}</el-descriptions-item>
<el-descriptions-item label="施工工艺">{{ displayText(modelValue.construction_method) }}</el-descriptions-item>
<el-descriptions-item label="限制条件">{{ displayText(modelValue.limit_condition) }}</el-descriptions-item>
</el-descriptions>
<div v-if="modelValue.brochure_url" class="brochure">
<div class="brochure-title">宣传页</div>
<img :src="modelValue.brochure_url" alt="宣传页" />
</div>
</template>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useAuth } from '@/store/auth'
import { fetchMaterialChoices, uploadImage } from '@/api/material'
import { fetchCategories, fetchSubcategories } from '@/api/category'
import { fetchFactorySimple } from '@/api/factory'
import { fetchBrands } from '@/api/brand'
const props = defineProps({
modelValue: { type: Object, required: true },
mode: { type: String, default: 'edit' }
})
const emit = defineEmits(['update:modelValue'])
const { isAdmin } = useAuth()
const formRef = ref(null)
const uploading = ref(false)
const majorOptions = ref([])
const stageOptions = ref([])
const importanceLevelOptions = ref([])
const replaceOptions = ref([])
const advantageOptions = ref([])
const sceneOptions = ref([])
const starOptions = ref([])
const categoryOptions = ref([])
const subcategoryOptions = ref([])
const factories = ref([])
const brandFormOptions = ref([])
const brandFormSearchLoading = ref(false)
const set = (key, value) => {
emit('update:modelValue', { ...props.modelValue, [key]: value })
}
const onCategoryChange = async (val) => {
emit('update:modelValue', {
...props.modelValue,
material_category: val,
material_subcategory: ''
})
if (!val) {
const data = await fetchSubcategories({})
subcategoryOptions.value = (data.results || data).map((item) => ({
id: item.id,
name: item.name,
value: item.value,
category: item.category
}))
return
}
const category = categoryOptions.value.find((item) => item.value === val)
if (category) {
const data = await fetchSubcategories({ category_id: category.id })
subcategoryOptions.value = (data.results || data).map((item) => ({
id: item.id,
name: item.name,
value: item.value,
category: item.category
}))
}
}
const searchBrandsForForm = async (query) => {
brandFormSearchLoading.value = true
try {
const data = await fetchBrands({ page_size: 50, search: query || '' })
brandFormOptions.value = data.results || data
} finally {
brandFormSearchLoading.value = false
}
}
const ensureBrandInFormOptions = (brandId, brandName) => {
if (!brandId) return
if (!brandFormOptions.value.some((item) => item.id === brandId)) {
brandFormOptions.value = [{ id: brandId, name: brandName || '' }, ...brandFormOptions.value]
}
}
const handleUpload = async (options) => {
uploading.value = true
try {
const result = await uploadImage(options.file)
emit('update:modelValue', {
...props.modelValue,
brochure: result.path,
brochure_url: result.url
})
ElMessage.success('图片上传成功')
} catch {
ElMessage.error('图片上传失败')
} finally {
uploading.value = false
}
}
const displayText = (value) => (value === null || value === undefined || value === '' ? '-' : value)
const displayList = (value) => (value?.length ? value.join('、') : '-')
const formatPercent = (value) => (value === null || value === undefined || value === '' ? '-' : `${value}%`)
const formatStarLevel = (value) => (value ? `${value}` : '-')
onMounted(async () => {
if (props.mode !== 'edit') return
const [choices, categories, subs] = await Promise.all([
fetchMaterialChoices(),
fetchCategories(),
fetchSubcategories({})
])
majorOptions.value = choices.major_category
stageOptions.value = choices.stage
importanceLevelOptions.value = choices.importance_level
replaceOptions.value = choices.replace_type
advantageOptions.value = choices.advantage
sceneOptions.value = choices.application_scene
starOptions.value = choices.star_level
categoryOptions.value = (categories.results || categories).map((item) => ({
id: item.id,
name: item.name,
value: item.value
}))
subcategoryOptions.value = (subs.results || subs).map((item) => ({
id: item.id,
name: item.name,
value: item.value,
category: item.category
}))
factories.value = await fetchFactorySimple()
await searchBrandsForForm('')
ensureBrandInFormOptions(props.modelValue.brand, props.modelValue.brand_name)
if (props.modelValue.material_category) {
const category = categoryOptions.value.find((item) => item.value === props.modelValue.material_category)
if (category) {
const data = await fetchSubcategories({ category_id: category.id })
subcategoryOptions.value = (data.results || data).map((item) => ({
id: item.id,
name: item.name,
value: item.value,
category: item.category
}))
}
}
})
watch(
() => props.modelValue?.brand,
(val) => {
if (val) {
ensureBrandInFormOptions(val, props.modelValue?.brand_name)
}
}
)
const validate = () => formRef.value?.validate()
const clearValidate = () => formRef.value?.clearValidate()
defineExpose({ validate, clearValidate })
</script>
<style scoped>
.upload {
display: inline-block;
}
.preview {
margin-top: 12px;
}
.preview img {
max-width: 300px;
border-radius: 6px;
border: 1px solid #eee;
}
.brochure {
margin-top: 20px;
}
.brochure-title {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
}
.brochure img {
max-width: 100%;
border-radius: 8px;
border: 1px solid #eee;
}
</style>