Compare commits
No commits in common. "b2d518e736149fab80d6e82762c120b0c194ec0a" and "3a305b4a7e6c79202cda932cb1e2de01ec3e8dd4" have entirely different histories.
b2d518e736
...
3a305b4a7e
26
CLAUDE.md
26
CLAUDE.md
|
|
@ -1,26 +0,0 @@
|
|||
# mat3 项目指引
|
||||
|
||||
## 后端运行环境
|
||||
|
||||
后端有独立虚拟环境,路径为 `backend/.venv/`。执行 Django 管理命令、运行 Python 脚本时**必须使用**该虚拟环境的 Python,否则会因系统 Python 缺少 `decouple` 等依赖而失败。
|
||||
|
||||
**正确用法**(从任意工作目录):
|
||||
|
||||
```bash
|
||||
D:/projects/mat3/backend/.venv/Scripts/python.exe D:/projects/mat3/backend/manage.py <command>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 运行 migration
|
||||
D:/projects/mat3/backend/.venv/Scripts/python.exe D:/projects/mat3/backend/manage.py migrate
|
||||
|
||||
# 生成 migration
|
||||
D:/projects/mat3/backend/.venv/Scripts/python.exe D:/projects/mat3/backend/manage.py makemigrations
|
||||
|
||||
# 启动开发服务器
|
||||
D:/projects/mat3/backend/.venv/Scripts/python.exe D:/projects/mat3/backend/manage.py runserver
|
||||
```
|
||||
|
||||
**不要**直接 `python manage.py ...`——系统 Python 没有项目依赖。
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Generated migration for factory app
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('factory', '0004_rename_brand_to_short_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='factory',
|
||||
name='unified_social_credit_code',
|
||||
field=models.CharField(blank=True, max_length=18, null=True, verbose_name='统一社会信用代码'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='factory',
|
||||
name='cooperation_mode',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[('direct', '厂家直供'), ('authorized', '授权代理商')],
|
||||
max_length=20,
|
||||
null=True,
|
||||
verbose_name='合作模式',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='factory',
|
||||
name='interaction_capability',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='交互能力'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,12 +1,5 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
COOPERATION_MODE_CHOICES = [
|
||||
('direct', '厂家直供'),
|
||||
('authorized', '授权代理商'),
|
||||
]
|
||||
|
||||
|
||||
class Factory(models.Model):
|
||||
"""
|
||||
工厂模型
|
||||
|
|
@ -15,9 +8,6 @@ class Factory(models.Model):
|
|||
product_category = models.CharField(max_length=255, blank=True, null=True, verbose_name='产品分类')
|
||||
factory_name = models.CharField(max_length=255, verbose_name='生产工厂全称')
|
||||
short_name = models.CharField(max_length=100, unique=True, verbose_name='供应商简称')
|
||||
unified_social_credit_code = models.CharField(max_length=18, blank=True, null=True, verbose_name='统一社会信用代码')
|
||||
cooperation_mode = models.CharField(max_length=20, choices=COOPERATION_MODE_CHOICES, blank=True, null=True, verbose_name='合作模式')
|
||||
interaction_capability = models.TextField(blank=True, null=True, verbose_name='交互能力')
|
||||
province = models.CharField(max_length=50, verbose_name='省')
|
||||
city = models.CharField(max_length=50, verbose_name='市')
|
||||
district = models.CharField(max_length=50, blank=True, null=True, verbose_name='区')
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import re
|
||||
|
||||
from rest_framework import serializers
|
||||
from .models import Factory, COOPERATION_MODE_CHOICES
|
||||
|
||||
|
||||
USCC_PATTERN = re.compile(r'^[0-9A-Z]{18}$')
|
||||
from .models import Factory
|
||||
|
||||
|
||||
class FactorySerializer(serializers.ModelSerializer):
|
||||
|
|
@ -13,25 +8,14 @@ class FactorySerializer(serializers.ModelSerializer):
|
|||
"""
|
||||
material_count = serializers.SerializerMethodField()
|
||||
usernames = serializers.SerializerMethodField()
|
||||
cooperation_mode_display = serializers.CharField(source='get_cooperation_mode_display', read_only=True)
|
||||
unified_social_credit_code = serializers.CharField(
|
||||
required=True, allow_null=False, allow_blank=False, max_length=18
|
||||
)
|
||||
cooperation_mode = serializers.ChoiceField(
|
||||
choices=COOPERATION_MODE_CHOICES, required=True, allow_null=False, allow_blank=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
||||
'short_name', 'unified_social_credit_code',
|
||||
'cooperation_mode', 'cooperation_mode_display',
|
||||
'interaction_capability',
|
||||
'province', 'city', 'district',
|
||||
'short_name', 'province', 'city', 'district',
|
||||
'address', 'website', 'created_at', 'updated_at',
|
||||
'material_count', 'usernames']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count',
|
||||
'usernames', 'cooperation_mode_display']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count', 'usernames']
|
||||
|
||||
def get_material_count(self, obj):
|
||||
return obj.materials.count()
|
||||
|
|
@ -39,24 +23,16 @@ class FactorySerializer(serializers.ModelSerializer):
|
|||
def get_usernames(self, obj):
|
||||
return list(obj.users.values_list('username', flat=True))
|
||||
|
||||
def validate_unified_social_credit_code(self, value):
|
||||
if not USCC_PATTERN.match(value or ''):
|
||||
raise serializers.ValidationError('统一社会信用代码必须为 18 位数字或大写字母')
|
||||
return value
|
||||
|
||||
|
||||
class FactoryListSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
工厂列表序列化器(简化版)
|
||||
"""
|
||||
usernames = serializers.SerializerMethodField()
|
||||
cooperation_mode_display = serializers.CharField(source='get_cooperation_mode_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'factory_name', 'short_name', 'province', 'city',
|
||||
'dealer_name', 'usernames',
|
||||
'cooperation_mode', 'cooperation_mode_display']
|
||||
fields = ['id', 'factory_name', 'short_name', 'province', 'city', 'dealer_name', 'usernames']
|
||||
|
||||
def get_usernames(self, obj):
|
||||
return list(obj.users.values_list('username', flat=True))
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
# 供应商新增工商与合作信息字段 — 设计文档
|
||||
|
||||
**日期**:2026-04-24
|
||||
**范围**:在 `Factory`(供应商)模型上新增 3 个业务字段,并在前端列表、表单、详情页同步展示/编辑。
|
||||
|
||||
## 目标
|
||||
|
||||
满足供应商资料登记的新业务要求:
|
||||
1. 工商注册信息 — 本期仅记录**统一社会信用代码**(其余工商字段本期不做)
|
||||
2. **交互能力** — 包含最大月供量、常规库存量、生产周期、运输方式、应急供货响应时间等信息(本期先用单个大文本框承载)
|
||||
3. **合作模式** — 厂家直供 / 授权代理商 二选一
|
||||
|
||||
## 新增字段
|
||||
|
||||
| 字段 | DB 类型 | DB 约束 | 表单必填 | 校验规则 |
|
||||
|---|---|---|---|---|
|
||||
| `unified_social_credit_code` 统一社会信用代码 | `CharField(max_length=18)` | `null=True, blank=True` | ✅ 必填 | 正则 `^[0-9A-Z]{18}$` — 长度 18,仅数字 + 大写字母 |
|
||||
| `interaction_capability` 交互能力 | `TextField` | `null=True, blank=True` | 选填 | 无;前端 textarea 的 placeholder 提示:"最大月供量 / 常规库存量 / 生产周期 / 运输方式 / 应急供货响应时间" |
|
||||
| `cooperation_mode` 合作模式 | `CharField(max_length=20, choices=COOPERATION_MODE_CHOICES)` | `null=True, blank=True` | ✅ 必填 | choices: `direct='厂家直供'`, `authorized='授权代理商'` |
|
||||
|
||||
### 存量数据策略
|
||||
|
||||
三个字段在数据库层均允许为空(`null=True, blank=True`),用以兼容已有的供应商记录。前端表单和后端 serializer 在新增/编辑时强制校验"必填"字段。
|
||||
|
||||
对用户行为的影响:
|
||||
- 历史供应商记录在列表/详情页展示为 `-`
|
||||
- 用户第一次编辑历史供应商时,两个"必填"字段必须补齐才能保存,否则前/后端都会拦截
|
||||
|
||||
### 校验位置
|
||||
|
||||
| 校验点 | 统一社会信用代码 | 合作模式 | 交互能力 |
|
||||
|---|---|---|---|
|
||||
| 前端 `el-form rules` | 必填 + 正则 | 必填(在 choices 内) | 无 |
|
||||
| 后端 `serializer.validate_*` | 必填 + 正则 | 必填(在 choices 内) | 无 |
|
||||
|
||||
后端 `null=True, blank=True` 的字段在序列化器层面仍然通过 `required=True` 覆盖为必填。
|
||||
|
||||
## 改动清单
|
||||
|
||||
### 后端 `backend/apps/factory/`
|
||||
|
||||
- **`models.py`**:
|
||||
- 在 `Factory` 模型上新增 3 个字段
|
||||
- 新增常量 `COOPERATION_MODE_CHOICES = [('direct', '厂家直供'), ('authorized', '授权代理商')]`
|
||||
- **`migrations/0005_add_business_info.py`**:新增 3 字段的 Django migration
|
||||
- **`serializers.py`**:
|
||||
- `FactorySerializer.Meta.fields` 追加 3 个字段
|
||||
- `FactorySerializer` 新增 `cooperation_mode_display` 只读字段(`source='get_cooperation_mode_display'`)
|
||||
- `FactorySerializer` 新增 `validate_unified_social_credit_code` 方法做正则校验 + 必填
|
||||
- `FactorySerializer` 对 `unified_social_credit_code` 和 `cooperation_mode` 显式 `required=True, allow_null=False, allow_blank=False` 覆盖模型的 null/blank
|
||||
- `FactoryListSerializer.Meta.fields` 追加 `cooperation_mode` 和 `cooperation_mode_display`
|
||||
|
||||
### 前端 `frontend/src/views/`
|
||||
|
||||
- **`FactoryManage.vue`**:
|
||||
- 列表在"经销商"列之后新增「合作模式」列(`min-width: 120`,显示 `cooperation_mode_display`)
|
||||
- dialog 表单新增 3 个表单项:
|
||||
- 统一社会信用代码 — `<el-input>`,含 form rules(必填 + 正则)
|
||||
- 合作模式 — `<el-select>`(厂家直供 / 授权代理商),含 form rules(必填)
|
||||
- 交互能力 — `<el-input type="textarea">`,提示语 placeholder,3~6 行高度
|
||||
- `form` reactive 对象新增 3 个字段;`resetForm()` 同步新增
|
||||
- `onSubmit` 前走 `formRef.validate()`;新增 form rules 定义
|
||||
- **`FactoryDetail.vue`**:
|
||||
- `<el-descriptions>` 新增 3 行:统一社会信用代码、合作模式(使用 `cooperation_mode_display`)、交互能力
|
||||
- 交互能力内容可能较长,需保持换行(`white-space: pre-wrap`)
|
||||
|
||||
## 非目标(YAGNI)
|
||||
|
||||
- 工商注册信息中的注册资本/注册地址/法人代表/营业期限/实缴资本/认缴资本等字段本期不做
|
||||
- 资质证书(生产许可证、安全生产许可证、绿色建材认证、ISO 三体系等)本期不做
|
||||
- 交互能力本期不拆分为结构化子字段(如独立的月供量/库存量/生产周期字段)。后续如有按字段检索/对比的需求再演进
|
||||
- 统一社会信用代码本期不做末位校验码算法验证
|
||||
- 文件上传、附件管理本期不涉及
|
||||
|
||||
## 验收
|
||||
|
||||
- [ ] 新建供应商:填完所有必填字段(含新增的 2 个必填)能成功保存;任一必填字段留空或格式错时,前/后端都拦截
|
||||
- [ ] 编辑历史供应商(历史字段为空):必须补齐两个必填字段后才能保存
|
||||
- [ ] 列表页展示「合作模式」列,值为中文标签("厂家直供"/"授权代理商"),历史数据显示 `-`
|
||||
- [ ] 详情页展示三个新字段;历史数据显示 `-`
|
||||
- [ ] 统一社会信用代码校验:长度非 18 或含小写字母/非字母数字字符被拦截
|
||||
|
|
@ -5,7 +5,28 @@
|
|||
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
<div class="card detail-card" v-if="factory">
|
||||
<FactoryForm :model-value="factory" mode="view" @update:model-value="() => {}" />
|
||||
<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.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="官网">
|
||||
<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>
|
||||
</template>
|
||||
|
|
@ -13,8 +34,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()
|
||||
|
|
@ -29,6 +50,12 @@ 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>
|
||||
|
|
@ -37,4 +64,9 @@ const goBack = () => {
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.website-link {
|
||||
color: var(--brand-500);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,7 @@
|
|||
<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 label="合作模式" min-width="120" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.cooperation_mode_display || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dealer_name" label="经销商" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="用户账号" min-width="160" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ (scope.row.usernames || []).join('、') || '-' }}
|
||||
|
|
@ -47,19 +43,41 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
size="50%"
|
||||
:close-on-click-modal="false"
|
||||
class="factory-drawer"
|
||||
>
|
||||
<FactoryForm ref="factoryFormRef" v-model="form" mode="edit" />
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-scroll">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="经销商">
|
||||
<el-input v-model="form.dealer_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品分类">
|
||||
<el-input v-model="form.product_category" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商全称" required>
|
||||
<el-input v-model="form.factory_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商简称" required>
|
||||
<el-input v-model="form.short_name" />
|
||||
</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="地址">
|
||||
<el-input v-model="form.address" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="官网">
|
||||
<el-input v-model="form.website" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -67,10 +85,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 } from '@/utils/region'
|
||||
import { formatRegion, regionLabel } 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()
|
||||
|
|
@ -85,14 +103,15 @@ const dialogVisible = ref(false)
|
|||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentId = ref(null)
|
||||
const factoryFormRef = ref(null)
|
||||
|
||||
const emptyForm = () => ({
|
||||
const regionOptions = regionData
|
||||
const regionValue = ref([])
|
||||
|
||||
const form = reactive({
|
||||
dealer_name: '',
|
||||
product_category: '',
|
||||
factory_name: '',
|
||||
short_name: '',
|
||||
unified_social_credit_code: '',
|
||||
cooperation_mode: '',
|
||||
interaction_capability: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
|
|
@ -100,8 +119,6 @@ const emptyForm = () => ({
|
|||
website: ''
|
||||
})
|
||||
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const loadFactories = async () => {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
|
|
@ -113,45 +130,55 @@ const loadFactories = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.dealer_name = ''
|
||||
form.product_category = ''
|
||||
form.factory_name = ''
|
||||
form.short_name = ''
|
||||
form.province = ''
|
||||
form.city = ''
|
||||
form.district = ''
|
||||
form.address = ''
|
||||
form.website = ''
|
||||
regionValue.value = []
|
||||
}
|
||||
|
||||
const onRegionChange = (val) => {
|
||||
form.province = val?.[0] || ''
|
||||
form.city = val?.[1] || ''
|
||||
form.district = val?.[2] || ''
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
form.value = emptyForm()
|
||||
factoryFormRef.value?.clearValidate()
|
||||
resetForm()
|
||||
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)
|
||||
form.value = { ...emptyForm(), ...detail }
|
||||
factoryFormRef.value?.clearValidate()
|
||||
Object.assign(form, detail)
|
||||
regionValue.value = [detail.province, detail.city, detail.district].filter(Boolean).map(regionLabel)
|
||||
dialogTitle.value = '编辑供应商'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await factoryFormRef.value?.validate()
|
||||
} catch (_) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateFactory(currentId.value, { ...form.value })
|
||||
await updateFactory(currentId.value, { ...form })
|
||||
} else {
|
||||
await createFactory({ ...form.value })
|
||||
await createFactory({ ...form })
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
loadFactories()
|
||||
} catch (error) {
|
||||
const data = error.response?.data
|
||||
const msg = data?.detail
|
||||
|| (data && typeof data === 'object' ? Object.values(data).flat().join(';') : null)
|
||||
|| '保存失败'
|
||||
ElMessage.error(msg)
|
||||
ElMessage.error(error.response?.data?.detail || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,12 +213,10 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.factory-drawer :deep(.el-drawer__body) {
|
||||
padding: 20px 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.factory-drawer :deep(.el-drawer__footer) {
|
||||
padding: 12px 24px;
|
||||
.dialog-scroll :deep(.el-dialog__body) {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,43 @@
|
|||
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
<div class="card detail-card" v-if="material">
|
||||
<MaterialForm :model-value="material" mode="view" @update:model-value="() => {}" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -14,7 +50,6 @@
|
|||
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()
|
||||
|
|
@ -29,6 +64,14 @@ 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>
|
||||
|
|
@ -37,4 +80,20 @@ const goBack = () => {
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -80,16 +80,206 @@
|
|||
:title="dialogTitle"
|
||||
size="60%"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
:destroy-on-close="false"
|
||||
class="material-drawer"
|
||||
>
|
||||
<MaterialForm ref="materialFormRef" v-model="form" mode="edit" />
|
||||
<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>
|
||||
<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 v-model="importDialogVisible" title="导入材料" width="420px">
|
||||
<div class="import-dialog">
|
||||
<div class="import-dialog__text">请先下载模板,按模板填写后上传 `.xlsx` 文件。</div>
|
||||
|
|
@ -113,27 +303,13 @@
|
|||
|
||||
<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,
|
||||
importMaterialsExcel,
|
||||
exportMaterialsExcel
|
||||
} from '@/api/material'
|
||||
import { fetchSubcategories } from '@/api/category'
|
||||
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 { fetchBrands } from '@/api/brand'
|
||||
import MaterialForm from '@/views/material/MaterialForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { isAdmin } = useAuth()
|
||||
const materials = ref([])
|
||||
const tableLoading = ref(false)
|
||||
|
|
@ -142,16 +318,20 @@ 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 materialFormRef = ref(null)
|
||||
// const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL).replace(/\/api$/, "");
|
||||
// const templateDownloadUrl = `${apiBaseUrl}/media/material_import_template.xlsx`
|
||||
const templateDownloadUrl = `http://101.42.1.64:2260/media/material_import_template.xlsx`
|
||||
|
||||
const filters = reactive({
|
||||
name: '',
|
||||
status: '',
|
||||
|
|
@ -160,11 +340,11 @@ const filters = reactive({
|
|||
})
|
||||
|
||||
const brandFilterOptions = ref([])
|
||||
const brandFormOptions = ref([])
|
||||
const brandSearchLoading = ref(false)
|
||||
const statusOptions = ref([])
|
||||
const filterSubcategoryOptions = ref([])
|
||||
const brandFormSearchLoading = ref(false)
|
||||
|
||||
const emptyForm = () => ({
|
||||
const form = reactive({
|
||||
name: '',
|
||||
major_category: '',
|
||||
material_category: '',
|
||||
|
|
@ -200,7 +380,18 @@ const emptyForm = () => ({
|
|||
brand: null
|
||||
})
|
||||
|
||||
const form = ref(emptyForm())
|
||||
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 loadMaterials = async () => {
|
||||
tableLoading.value = true
|
||||
|
|
@ -217,20 +408,82 @@ const loadMaterials = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const loadStatusOptions = async () => {
|
||||
const loadChoices = 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 loadFilterSubcategories = async () => {
|
||||
const data = await fetchSubcategories({})
|
||||
filterSubcategoryOptions.value = (data.results || data).map((item) => ({
|
||||
const loadCategories = async () => {
|
||||
const data = await fetchCategories()
|
||||
categoryOptions.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
|
||||
|
|
@ -246,36 +499,85 @@ const searchBrands = async (query) => {
|
|||
}
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
form.value = emptyForm()
|
||||
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()
|
||||
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)
|
||||
form.value = {
|
||||
...emptyForm(),
|
||||
...item,
|
||||
application_scene: item.application_scene || [],
|
||||
advantage: item.advantage || [],
|
||||
brochure_url: item.brochure_url || '',
|
||||
brand: item.brand || null
|
||||
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)
|
||||
}
|
||||
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.value.brand) {
|
||||
if (!form.brand) {
|
||||
ElMessage.warning('请选择品牌')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload = { ...form.value }
|
||||
const payload = { ...form }
|
||||
delete payload.brochure_url
|
||||
delete payload.brand_name
|
||||
if (!isAdmin.value) {
|
||||
|
|
@ -387,10 +689,20 @@ 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 = (row) => {
|
||||
router.push(`/materials/${row.id}`)
|
||||
const goDetail = async (row) => {
|
||||
try {
|
||||
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) => {
|
||||
pagination.page = page
|
||||
loadMaterials()
|
||||
|
|
@ -403,8 +715,10 @@ const onPageSizeChange = (size) => {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatusOptions()
|
||||
loadFilterSubcategories()
|
||||
loadChoices()
|
||||
loadCategories()
|
||||
loadSubcategories()
|
||||
loadFactories()
|
||||
loadBrandFilterOptions()
|
||||
loadMaterials()
|
||||
})
|
||||
|
|
@ -427,6 +741,12 @@ onMounted(() => {
|
|||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-scroll :deep(.el-dialog__body) {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.material-drawer :deep(.el-drawer__body) {
|
||||
padding: 20px 24px;
|
||||
overflow-y: auto;
|
||||
|
|
@ -435,4 +755,29 @@ onMounted(() => {
|
|||
.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>
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
<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