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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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