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:
parent
19cd3710dd
commit
e8e122ca61
|
|
@ -5,33 +5,7 @@
|
||||||
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card detail-card" v-if="factory">
|
<div class="card detail-card" v-if="factory">
|
||||||
<el-descriptions :column="1" border class="detail-descriptions">
|
<FactoryForm :model-value="factory" mode="view" @update:model-value="() => {}" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -39,8 +13,8 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { formatRegion } from '@/utils/region'
|
|
||||||
import { fetchFactoryDetail } from '@/api/factory'
|
import { fetchFactoryDetail } from '@/api/factory'
|
||||||
|
import FactoryForm from '@/views/factory/FactoryForm.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -55,12 +29,6 @@ onMounted(loadDetail)
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayText = (value) => value || '-'
|
|
||||||
|
|
||||||
const displayList = (value) => (value?.length ? value.join('、') : '-')
|
|
||||||
|
|
||||||
const displayRegion = (item) => formatRegion(item.province, item.city, item.district) || '-'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -69,13 +37,4 @@ const displayRegion = (item) => formatRegion(item.province, item.city, item.dist
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.website-link {
|
|
||||||
color: var(--brand-500);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiline {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
<el-table v-loading="tableLoading" :data="factories" border height="100%">
|
<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="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="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>
|
<el-table-column label="合作模式" min-width="120" show-overflow-tooltip>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.cooperation_mode_display || '-' }}
|
{{ scope.row.cooperation_mode_display || '-' }}
|
||||||
|
|
@ -49,60 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-scroll">
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-scroll">
|
||||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="140px">
|
<FactoryForm ref="factoryFormRef" v-model="form" mode="edit" />
|
||||||
<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>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="onSubmit">保存</el-button>
|
<el-button type="primary" @click="onSubmit">保存</el-button>
|
||||||
|
|
@ -115,10 +61,10 @@
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { regionData } from 'element-china-area-data'
|
|
||||||
import { useAuth } from '@/store/auth'
|
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 { fetchFactories, fetchFactoryDetail, createFactory, updateFactory, deleteFactory } from '@/api/factory'
|
||||||
|
import FactoryForm from '@/views/factory/FactoryForm.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isAdmin } = useAuth()
|
const { isAdmin } = useAuth()
|
||||||
|
|
@ -133,40 +79,9 @@ const dialogVisible = ref(false)
|
||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const currentId = ref(null)
|
const currentId = ref(null)
|
||||||
const formRef = ref(null)
|
const factoryFormRef = ref(null)
|
||||||
|
|
||||||
const regionOptions = regionData
|
const emptyForm = () => ({
|
||||||
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: '',
|
|
||||||
factory_name: '',
|
factory_name: '',
|
||||||
short_name: '',
|
short_name: '',
|
||||||
unified_social_credit_code: '',
|
unified_social_credit_code: '',
|
||||||
|
|
@ -179,6 +94,8 @@ const form = reactive({
|
||||||
website: ''
|
website: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const form = ref(emptyForm())
|
||||||
|
|
||||||
const loadFactories = async () => {
|
const loadFactories = async () => {
|
||||||
tableLoading.value = true
|
tableLoading.value = true
|
||||||
try {
|
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 = () => {
|
const openCreate = () => {
|
||||||
resetForm()
|
form.value = emptyForm()
|
||||||
|
factoryFormRef.value?.clearValidate()
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
dialogTitle.value = '新增供应商'
|
dialogTitle.value = '新增供应商'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = async (row) => {
|
const openEdit = async (row) => {
|
||||||
resetForm()
|
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentId.value = row.id
|
currentId.value = row.id
|
||||||
const detail = await fetchFactoryDetail(row.id)
|
const detail = await fetchFactoryDetail(row.id)
|
||||||
Object.assign(form, detail)
|
form.value = { ...emptyForm(), ...detail }
|
||||||
regionValue.value = [detail.province, detail.city, detail.district].filter(Boolean).map(regionLabel)
|
factoryFormRef.value?.clearValidate()
|
||||||
dialogTitle.value = '编辑供应商'
|
dialogTitle.value = '编辑供应商'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate()
|
await factoryFormRef.value?.validate()
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateFactory(currentId.value, { ...form })
|
await updateFactory(currentId.value, { ...form.value })
|
||||||
} else {
|
} else {
|
||||||
await createFactory({ ...form })
|
await createFactory({ ...form.value })
|
||||||
}
|
}
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
|
|
@ -292,4 +186,3 @@ onMounted(() => {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,43 +5,7 @@
|
||||||
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card detail-card" v-if="material">
|
<div class="card detail-card" v-if="material">
|
||||||
<el-descriptions :column="1" border class="detail-descriptions">
|
<MaterialForm :model-value="material" mode="view" @update:model-value="() => {}" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -50,6 +14,7 @@
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { fetchMaterialDetail } from '@/api/material'
|
import { fetchMaterialDetail } from '@/api/material'
|
||||||
|
import MaterialForm from '@/views/material/MaterialForm.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -64,14 +29,6 @@ onMounted(loadDetail)
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -80,20 +37,4 @@ const formatStarLevel = (value) => (value ? `${value}星` : '-')
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -75,210 +75,20 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-drawer
|
<el-dialog
|
||||||
|
v-if="dialogVisible"
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
size="60%"
|
width="720px"
|
||||||
|
class="dialog-scroll"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:destroy-on-close="false"
|
|
||||||
class="material-drawer"
|
|
||||||
>
|
>
|
||||||
<el-form :model="form" label-width="110px">
|
<MaterialForm ref="materialFormRef" v-model="form" mode="edit" />
|
||||||
<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>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="onSave">保存</el-button>
|
<el-button type="primary" @click="onSave">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-dialog>
|
||||||
|
|
||||||
<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 v-model="importDialogVisible" title="导入材料" width="420px">
|
<el-dialog v-model="importDialogVisible" title="导入材料" width="420px">
|
||||||
<div class="import-dialog">
|
<div class="import-dialog">
|
||||||
|
|
@ -303,13 +113,27 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useAuth } from '@/store/auth'
|
import { useAuth } from '@/store/auth'
|
||||||
import { fetchMaterials, fetchMaterialDetail, createMaterial, updateMaterial, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial, fetchMaterialChoices, uploadImage, importMaterialsExcel, exportMaterialsExcel } from '@/api/material'
|
import {
|
||||||
import { fetchCategories, fetchSubcategories } from '@/api/category'
|
fetchMaterials,
|
||||||
import { fetchFactorySimple } from '@/api/factory'
|
fetchMaterialDetail,
|
||||||
|
createMaterial,
|
||||||
|
updateMaterial,
|
||||||
|
deleteMaterial,
|
||||||
|
submitMaterial,
|
||||||
|
approveMaterial,
|
||||||
|
rejectMaterial,
|
||||||
|
fetchMaterialChoices,
|
||||||
|
importMaterialsExcel,
|
||||||
|
exportMaterialsExcel
|
||||||
|
} from '@/api/material'
|
||||||
|
import { fetchSubcategories } from '@/api/category'
|
||||||
import { fetchBrands } from '@/api/brand'
|
import { fetchBrands } from '@/api/brand'
|
||||||
|
import MaterialForm from '@/views/material/MaterialForm.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const { isAdmin } = useAuth()
|
const { isAdmin } = useAuth()
|
||||||
const materials = ref([])
|
const materials = ref([])
|
||||||
const tableLoading = ref(false)
|
const tableLoading = ref(false)
|
||||||
|
|
@ -318,20 +142,16 @@ const pagination = reactive({
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
const factories = ref([])
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const detailVisible = ref(false)
|
|
||||||
const detailData = ref(null)
|
|
||||||
const importDialogVisible = ref(false)
|
const importDialogVisible = ref(false)
|
||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const currentId = ref(null)
|
const currentId = ref(null)
|
||||||
const uploading = ref(false)
|
|
||||||
const importing = ref(false)
|
const importing = ref(false)
|
||||||
const exporting = ref(false)
|
const exporting = ref(false)
|
||||||
// const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL).replace(/\/api$/, "");
|
const materialFormRef = ref(null)
|
||||||
// const templateDownloadUrl = `${apiBaseUrl}/media/material_import_template.xlsx`
|
|
||||||
const templateDownloadUrl = `http://101.42.1.64:2260/media/material_import_template.xlsx`
|
const templateDownloadUrl = `http://101.42.1.64:2260/media/material_import_template.xlsx`
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
|
@ -340,11 +160,11 @@ const filters = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const brandFilterOptions = ref([])
|
const brandFilterOptions = ref([])
|
||||||
const brandFormOptions = ref([])
|
|
||||||
const brandSearchLoading = ref(false)
|
const brandSearchLoading = ref(false)
|
||||||
const brandFormSearchLoading = ref(false)
|
const statusOptions = ref([])
|
||||||
|
const filterSubcategoryOptions = ref([])
|
||||||
|
|
||||||
const form = reactive({
|
const emptyForm = () => ({
|
||||||
name: '',
|
name: '',
|
||||||
major_category: '',
|
major_category: '',
|
||||||
material_category: '',
|
material_category: '',
|
||||||
|
|
@ -380,18 +200,7 @@ const form = reactive({
|
||||||
brand: null
|
brand: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const majorOptions = ref([])
|
const form = ref(emptyForm())
|
||||||
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 loadMaterials = async () => {
|
const loadMaterials = async () => {
|
||||||
tableLoading.value = true
|
tableLoading.value = true
|
||||||
|
|
@ -408,82 +217,20 @@ const loadMaterials = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadChoices = async () => {
|
const loadStatusOptions = async () => {
|
||||||
const data = await fetchMaterialChoices()
|
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
|
statusOptions.value = data.status
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadCategories = async () => {
|
const loadFilterSubcategories = async () => {
|
||||||
const data = await fetchCategories()
|
const data = await fetchSubcategories({})
|
||||||
categoryOptions.value = (data.results || data).map((item) => ({
|
filterSubcategoryOptions.value = (data.results || data).map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
value: item.value
|
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 loadBrandFilterOptions = async () => {
|
||||||
const data = await fetchBrands({ page_size: 100 })
|
const data = await fetchBrands({ page_size: 100 })
|
||||||
brandFilterOptions.value = data.results || data
|
brandFilterOptions.value = data.results || data
|
||||||
|
|
@ -499,85 +246,36 @@ const searchBrands = async (query) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchBrandsForForm = async (query) => {
|
const openCreate = () => {
|
||||||
brandFormSearchLoading.value = true
|
form.value = emptyForm()
|
||||||
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()
|
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
dialogTitle.value = '新增材料'
|
dialogTitle.value = '新增材料'
|
||||||
await searchBrandsForForm('')
|
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = async (row) => {
|
const openEdit = async (row) => {
|
||||||
resetForm()
|
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentId.value = row.id
|
currentId.value = row.id
|
||||||
const item = await fetchMaterialDetail(row.id)
|
const item = await fetchMaterialDetail(row.id)
|
||||||
Object.assign(form, item)
|
form.value = {
|
||||||
form.application_scene = item.application_scene || []
|
...emptyForm(),
|
||||||
form.advantage = item.advantage || []
|
...item,
|
||||||
form.brochure_url = item.brochure_url || ''
|
application_scene: item.application_scene || [],
|
||||||
form.brand = item.brand || null
|
advantage: item.advantage || [],
|
||||||
if (form.material_category) {
|
brochure_url: item.brochure_url || '',
|
||||||
await onCategoryChange(form.material_category, false)
|
brand: item.brand || null
|
||||||
}
|
}
|
||||||
await searchBrandsForForm('')
|
|
||||||
ensureBrandInFormOptions(item.brand, item.brand_name)
|
|
||||||
dialogTitle.value = '编辑材料'
|
dialogTitle.value = '编辑材料'
|
||||||
dialogVisible.value = true
|
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 () => {
|
const onSave = async () => {
|
||||||
if (!form.brand) {
|
if (!form.value.brand) {
|
||||||
ElMessage.warning('请选择品牌')
|
ElMessage.warning('请选择品牌')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const payload = { ...form }
|
const payload = { ...form.value }
|
||||||
delete payload.brochure_url
|
delete payload.brochure_url
|
||||||
delete payload.brand_name
|
delete payload.brand_name
|
||||||
if (!isAdmin.value) {
|
if (!isAdmin.value) {
|
||||||
|
|
@ -689,20 +387,10 @@ const canDelete = (row) => isAdmin.value || row.status === 'draft'
|
||||||
const canSubmit = (row) => !isAdmin.value && row.status === 'draft'
|
const canSubmit = (row) => !isAdmin.value && row.status === 'draft'
|
||||||
const canApprove = (row) => isAdmin.value && row.status === 'pending'
|
const canApprove = (row) => isAdmin.value && row.status === 'pending'
|
||||||
|
|
||||||
const goDetail = async (row) => {
|
const goDetail = (row) => {
|
||||||
try {
|
router.push(`/materials/${row.id}`)
|
||||||
detailData.value = await fetchMaterialDetail(row.id)
|
|
||||||
detailVisible.value = true
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error.response?.data?.detail || '加载详情失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
const onPageChange = (page) => {
|
||||||
pagination.page = page
|
pagination.page = page
|
||||||
loadMaterials()
|
loadMaterials()
|
||||||
|
|
@ -715,10 +403,8 @@ const onPageSizeChange = (size) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadChoices()
|
loadStatusOptions()
|
||||||
loadCategories()
|
loadFilterSubcategories()
|
||||||
loadSubcategories()
|
|
||||||
loadFactories()
|
|
||||||
loadBrandFilterOptions()
|
loadBrandFilterOptions()
|
||||||
loadMaterials()
|
loadMaterials()
|
||||||
})
|
})
|
||||||
|
|
@ -746,38 +432,4 @@ onMounted(() => {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 8px;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue