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
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
COOPERATION_MODE_CHOICES = [
|
|
||||||
('direct', '厂家直供'),
|
|
||||||
('authorized', '授权代理商'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Factory(models.Model):
|
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='产品分类')
|
product_category = models.CharField(max_length=255, blank=True, null=True, verbose_name='产品分类')
|
||||||
factory_name = models.CharField(max_length=255, verbose_name='生产工厂全称')
|
factory_name = models.CharField(max_length=255, verbose_name='生产工厂全称')
|
||||||
short_name = models.CharField(max_length=100, unique=True, 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='省')
|
province = models.CharField(max_length=50, verbose_name='省')
|
||||||
city = 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='区')
|
district = models.CharField(max_length=50, blank=True, null=True, verbose_name='区')
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Factory, COOPERATION_MODE_CHOICES
|
from .models import Factory
|
||||||
|
|
||||||
|
|
||||||
USCC_PATTERN = re.compile(r'^[0-9A-Z]{18}$')
|
|
||||||
|
|
||||||
|
|
||||||
class FactorySerializer(serializers.ModelSerializer):
|
class FactorySerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -13,25 +8,14 @@ class FactorySerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
material_count = serializers.SerializerMethodField()
|
material_count = serializers.SerializerMethodField()
|
||||||
usernames = 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:
|
class Meta:
|
||||||
model = Factory
|
model = Factory
|
||||||
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
||||||
'short_name', 'unified_social_credit_code',
|
'short_name', 'province', 'city', 'district',
|
||||||
'cooperation_mode', 'cooperation_mode_display',
|
|
||||||
'interaction_capability',
|
|
||||||
'province', 'city', 'district',
|
|
||||||
'address', 'website', 'created_at', 'updated_at',
|
'address', 'website', 'created_at', 'updated_at',
|
||||||
'material_count', 'usernames']
|
'material_count', 'usernames']
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count',
|
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count', 'usernames']
|
||||||
'usernames', 'cooperation_mode_display']
|
|
||||||
|
|
||||||
def get_material_count(self, obj):
|
def get_material_count(self, obj):
|
||||||
return obj.materials.count()
|
return obj.materials.count()
|
||||||
|
|
@ -39,24 +23,16 @@ class FactorySerializer(serializers.ModelSerializer):
|
||||||
def get_usernames(self, obj):
|
def get_usernames(self, obj):
|
||||||
return list(obj.users.values_list('username', flat=True))
|
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):
|
class FactoryListSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
工厂列表序列化器(简化版)
|
工厂列表序列化器(简化版)
|
||||||
"""
|
"""
|
||||||
usernames = serializers.SerializerMethodField()
|
usernames = serializers.SerializerMethodField()
|
||||||
cooperation_mode_display = serializers.CharField(source='get_cooperation_mode_display', read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Factory
|
model = Factory
|
||||||
fields = ['id', 'factory_name', 'short_name', 'province', 'city',
|
fields = ['id', 'factory_name', 'short_name', 'province', 'city', 'dealer_name', 'usernames']
|
||||||
'dealer_name', 'usernames',
|
|
||||||
'cooperation_mode', 'cooperation_mode_display']
|
|
||||||
|
|
||||||
def get_usernames(self, obj):
|
def get_usernames(self, obj):
|
||||||
return list(obj.users.values_list('username', flat=True))
|
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>
|
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card detail-card" v-if="factory">
|
<div class="card detail-card" v-if="factory">
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -13,8 +34,8 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { formatRegion } from '@/utils/region'
|
||||||
import { fetchFactoryDetail } from '@/api/factory'
|
import { fetchFactoryDetail } from '@/api/factory'
|
||||||
import FactoryForm from '@/views/factory/FactoryForm.vue'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -29,6 +50,12 @@ onMounted(loadDetail)
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayText = (value) => value || '-'
|
||||||
|
|
||||||
|
const displayList = (value) => (value?.length ? value.join('、') : '-')
|
||||||
|
|
||||||
|
const displayRegion = (item) => formatRegion(item.province, item.city, item.district) || '-'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -37,4 +64,9 @@ const goBack = () => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.website-link {
|
||||||
|
color: var(--brand-500);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,7 @@
|
||||||
<el-table v-loading="tableLoading" :data="factories" border height="100%">
|
<el-table v-loading="tableLoading" :data="factories" border height="100%">
|
||||||
<el-table-column prop="factory_name" label="供应商全称" min-width="220" show-overflow-tooltip />
|
<el-table-column prop="factory_name" label="供应商全称" min-width="220" show-overflow-tooltip />
|
||||||
<el-table-column prop="short_name" label="供应商简称" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="short_name" label="供应商简称" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column label="合作模式" min-width="120" show-overflow-tooltip>
|
<el-table-column prop="dealer_name" label="经销商" min-width="160" show-overflow-tooltip />
|
||||||
<template #default="scope">
|
|
||||||
{{ scope.row.cooperation_mode_display || '-' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="用户账号" min-width="160" show-overflow-tooltip>
|
<el-table-column label="用户账号" min-width="160" show-overflow-tooltip>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ (scope.row.usernames || []).join('、') || '-' }}
|
{{ (scope.row.usernames || []).join('、') || '-' }}
|
||||||
|
|
@ -47,19 +43,41 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-drawer
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-scroll">
|
||||||
v-model="dialogVisible"
|
<el-form :model="form" label-width="100px">
|
||||||
:title="dialogTitle"
|
<el-form-item label="经销商">
|
||||||
size="50%"
|
<el-input v-model="form.dealer_name" />
|
||||||
:close-on-click-modal="false"
|
</el-form-item>
|
||||||
class="factory-drawer"
|
<el-form-item label="产品分类">
|
||||||
>
|
<el-input v-model="form.product_category" />
|
||||||
<FactoryForm ref="factoryFormRef" v-model="form" mode="edit" />
|
</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>
|
<template #footer>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="onSubmit">保存</el-button>
|
<el-button type="primary" @click="onSubmit">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -67,10 +85,10 @@
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { regionData } from 'element-china-area-data'
|
||||||
import { useAuth } from '@/store/auth'
|
import { useAuth } from '@/store/auth'
|
||||||
import { formatRegion } from '@/utils/region'
|
import { formatRegion, regionLabel } from '@/utils/region'
|
||||||
import { fetchFactories, fetchFactoryDetail, createFactory, updateFactory, deleteFactory } from '@/api/factory'
|
import { fetchFactories, fetchFactoryDetail, createFactory, updateFactory, deleteFactory } from '@/api/factory'
|
||||||
import FactoryForm from '@/views/factory/FactoryForm.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isAdmin } = useAuth()
|
const { isAdmin } = useAuth()
|
||||||
|
|
@ -85,14 +103,15 @@ const dialogVisible = ref(false)
|
||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const currentId = ref(null)
|
const currentId = ref(null)
|
||||||
const factoryFormRef = ref(null)
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
const regionOptions = regionData
|
||||||
|
const regionValue = ref([])
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
dealer_name: '',
|
||||||
|
product_category: '',
|
||||||
factory_name: '',
|
factory_name: '',
|
||||||
short_name: '',
|
short_name: '',
|
||||||
unified_social_credit_code: '',
|
|
||||||
cooperation_mode: '',
|
|
||||||
interaction_capability: '',
|
|
||||||
province: '',
|
province: '',
|
||||||
city: '',
|
city: '',
|
||||||
district: '',
|
district: '',
|
||||||
|
|
@ -100,8 +119,6 @@ const emptyForm = () => ({
|
||||||
website: ''
|
website: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const form = ref(emptyForm())
|
|
||||||
|
|
||||||
const loadFactories = async () => {
|
const loadFactories = async () => {
|
||||||
tableLoading.value = true
|
tableLoading.value = true
|
||||||
try {
|
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 = () => {
|
const openCreate = () => {
|
||||||
form.value = emptyForm()
|
resetForm()
|
||||||
factoryFormRef.value?.clearValidate()
|
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
dialogTitle.value = '新增供应商'
|
dialogTitle.value = '新增供应商'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = async (row) => {
|
const openEdit = async (row) => {
|
||||||
|
resetForm()
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentId.value = row.id
|
currentId.value = row.id
|
||||||
const detail = await fetchFactoryDetail(row.id)
|
const detail = await fetchFactoryDetail(row.id)
|
||||||
form.value = { ...emptyForm(), ...detail }
|
Object.assign(form, detail)
|
||||||
factoryFormRef.value?.clearValidate()
|
regionValue.value = [detail.province, detail.city, detail.district].filter(Boolean).map(regionLabel)
|
||||||
dialogTitle.value = '编辑供应商'
|
dialogTitle.value = '编辑供应商'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
|
||||||
await factoryFormRef.value?.validate()
|
|
||||||
} catch (_) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateFactory(currentId.value, { ...form.value })
|
await updateFactory(currentId.value, { ...form })
|
||||||
} else {
|
} else {
|
||||||
await createFactory({ ...form.value })
|
await createFactory({ ...form })
|
||||||
}
|
}
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
loadFactories()
|
loadFactories()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const data = error.response?.data
|
ElMessage.error(error.response?.data?.detail || '保存失败')
|
||||||
const msg = data?.detail
|
|
||||||
|| (data && typeof data === 'object' ? Object.values(data).flat().join(';') : null)
|
|
||||||
|| '保存失败'
|
|
||||||
ElMessage.error(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,12 +213,10 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.factory-drawer :deep(.el-drawer__body) {
|
.dialog-scroll :deep(.el-dialog__body) {
|
||||||
padding: 20px 24px;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
}
|
padding-right: 8px;
|
||||||
|
|
||||||
.factory-drawer :deep(.el-drawer__footer) {
|
|
||||||
padding: 12px 24px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,43 @@
|
||||||
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
<el-button class="back-btn" plain size="small" @click="goBack">返回</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card detail-card" v-if="material">
|
<div class="card detail-card" v-if="material">
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -14,7 +50,6 @@
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { fetchMaterialDetail } from '@/api/material'
|
import { fetchMaterialDetail } from '@/api/material'
|
||||||
import MaterialForm from '@/views/material/MaterialForm.vue'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -29,6 +64,14 @@ onMounted(loadDetail)
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayText = (value) => value || '-'
|
||||||
|
|
||||||
|
const displayList = (value) => (value?.length ? value.join('、') : '-')
|
||||||
|
|
||||||
|
const formatPercent = (value) => (value === null || value === undefined || value === '' ? '-' : `${value}%`)
|
||||||
|
|
||||||
|
const formatStarLevel = (value) => (value ? `${value}星` : '-')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -37,4 +80,20 @@ const goBack = () => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brochure {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,206 @@
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
size="60%"
|
size="60%"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:destroy-on-close="true"
|
:destroy-on-close="false"
|
||||||
class="material-drawer"
|
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>
|
<template #footer>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="onSave">保存</el-button>
|
<el-button type="primary" @click="onSave">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-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">
|
<el-dialog v-model="importDialogVisible" title="导入材料" width="420px">
|
||||||
<div class="import-dialog">
|
<div class="import-dialog">
|
||||||
<div class="import-dialog__text">请先下载模板,按模板填写后上传 `.xlsx` 文件。</div>
|
<div class="import-dialog__text">请先下载模板,按模板填写后上传 `.xlsx` 文件。</div>
|
||||||
|
|
@ -113,27 +303,13 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useAuth } from '@/store/auth'
|
import { useAuth } from '@/store/auth'
|
||||||
import {
|
import { fetchMaterials, fetchMaterialDetail, createMaterial, updateMaterial, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial, fetchMaterialChoices, uploadImage, importMaterialsExcel, exportMaterialsExcel } from '@/api/material'
|
||||||
fetchMaterials,
|
import { fetchCategories, fetchSubcategories } from '@/api/category'
|
||||||
fetchMaterialDetail,
|
import { fetchFactorySimple } from '@/api/factory'
|
||||||
createMaterial,
|
|
||||||
updateMaterial,
|
|
||||||
deleteMaterial,
|
|
||||||
submitMaterial,
|
|
||||||
approveMaterial,
|
|
||||||
rejectMaterial,
|
|
||||||
fetchMaterialChoices,
|
|
||||||
importMaterialsExcel,
|
|
||||||
exportMaterialsExcel
|
|
||||||
} from '@/api/material'
|
|
||||||
import { fetchSubcategories } from '@/api/category'
|
|
||||||
import { fetchBrands } from '@/api/brand'
|
import { fetchBrands } from '@/api/brand'
|
||||||
import MaterialForm from '@/views/material/MaterialForm.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { isAdmin } = useAuth()
|
const { isAdmin } = useAuth()
|
||||||
const materials = ref([])
|
const materials = ref([])
|
||||||
const tableLoading = ref(false)
|
const tableLoading = ref(false)
|
||||||
|
|
@ -142,16 +318,20 @@ const pagination = reactive({
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
const factories = ref([])
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const detailData = ref(null)
|
||||||
const importDialogVisible = ref(false)
|
const importDialogVisible = ref(false)
|
||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const currentId = ref(null)
|
const currentId = ref(null)
|
||||||
|
const uploading = ref(false)
|
||||||
const importing = ref(false)
|
const importing = ref(false)
|
||||||
const exporting = ref(false)
|
const exporting = ref(false)
|
||||||
const 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 templateDownloadUrl = `http://101.42.1.64:2260/media/material_import_template.xlsx`
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
|
@ -160,11 +340,11 @@ const filters = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const brandFilterOptions = ref([])
|
const brandFilterOptions = ref([])
|
||||||
|
const brandFormOptions = ref([])
|
||||||
const brandSearchLoading = ref(false)
|
const brandSearchLoading = ref(false)
|
||||||
const statusOptions = ref([])
|
const brandFormSearchLoading = ref(false)
|
||||||
const filterSubcategoryOptions = ref([])
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
major_category: '',
|
major_category: '',
|
||||||
material_category: '',
|
material_category: '',
|
||||||
|
|
@ -200,7 +380,18 @@ const emptyForm = () => ({
|
||||||
brand: null
|
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 () => {
|
const loadMaterials = async () => {
|
||||||
tableLoading.value = true
|
tableLoading.value = true
|
||||||
|
|
@ -217,20 +408,82 @@ const loadMaterials = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadStatusOptions = async () => {
|
const loadChoices = async () => {
|
||||||
const data = await fetchMaterialChoices()
|
const data = await fetchMaterialChoices()
|
||||||
|
majorOptions.value = data.major_category
|
||||||
|
stageOptions.value = data.stage
|
||||||
|
importanceLevelOptions.value = data.importance_level
|
||||||
|
replaceOptions.value = data.replace_type
|
||||||
|
advantageOptions.value = data.advantage
|
||||||
|
sceneOptions.value = data.application_scene
|
||||||
|
starOptions.value = data.star_level
|
||||||
statusOptions.value = data.status
|
statusOptions.value = data.status
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFilterSubcategories = async () => {
|
const loadCategories = async () => {
|
||||||
const data = await fetchSubcategories({})
|
const data = await fetchCategories()
|
||||||
filterSubcategoryOptions.value = (data.results || data).map((item) => ({
|
categoryOptions.value = (data.results || data).map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
value: item.value
|
value: item.value
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadSubcategories = async (categoryId = '') => {
|
||||||
|
const data = await fetchSubcategories(categoryId ? { category_id: categoryId } : {})
|
||||||
|
allSubcategories.value = data.results || data
|
||||||
|
const mapped = allSubcategories.value.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
category: item.category
|
||||||
|
}))
|
||||||
|
filterSubcategoryOptions.value = mapped
|
||||||
|
subcategoryOptions.value = mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadFactories = async () => {
|
||||||
|
factories.value = await fetchFactorySimple()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(form, {
|
||||||
|
name: '',
|
||||||
|
major_category: '',
|
||||||
|
material_category: '',
|
||||||
|
material_subcategory: '',
|
||||||
|
stage: '',
|
||||||
|
importance_level: '',
|
||||||
|
landing_project: '',
|
||||||
|
contact_person: '',
|
||||||
|
contact_phone: '',
|
||||||
|
handler: '',
|
||||||
|
remark: '',
|
||||||
|
spec: '',
|
||||||
|
standard: '',
|
||||||
|
application_scene: [],
|
||||||
|
application_desc: '',
|
||||||
|
replace_type: '',
|
||||||
|
advantage: [],
|
||||||
|
advantage_desc: '',
|
||||||
|
cost_compare: null,
|
||||||
|
cost_desc: '',
|
||||||
|
cases: '',
|
||||||
|
brochure: null,
|
||||||
|
brochure_url: '',
|
||||||
|
quality_level: null,
|
||||||
|
durability_level: null,
|
||||||
|
eco_level: null,
|
||||||
|
carbon_level: null,
|
||||||
|
score_level: null,
|
||||||
|
connection_method: '',
|
||||||
|
construction_method: '',
|
||||||
|
limit_condition: '',
|
||||||
|
factory: null,
|
||||||
|
brand: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const loadBrandFilterOptions = async () => {
|
const loadBrandFilterOptions = async () => {
|
||||||
const data = await fetchBrands({ page_size: 100 })
|
const data = await fetchBrands({ page_size: 100 })
|
||||||
brandFilterOptions.value = data.results || data
|
brandFilterOptions.value = data.results || data
|
||||||
|
|
@ -246,36 +499,85 @@ const searchBrands = async (query) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCreate = () => {
|
const searchBrandsForForm = async (query) => {
|
||||||
form.value = emptyForm()
|
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
|
isEdit.value = false
|
||||||
dialogTitle.value = '新增材料'
|
dialogTitle.value = '新增材料'
|
||||||
|
await searchBrandsForForm('')
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = async (row) => {
|
const openEdit = async (row) => {
|
||||||
|
resetForm()
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentId.value = row.id
|
currentId.value = row.id
|
||||||
const item = await fetchMaterialDetail(row.id)
|
const item = await fetchMaterialDetail(row.id)
|
||||||
form.value = {
|
Object.assign(form, item)
|
||||||
...emptyForm(),
|
form.application_scene = item.application_scene || []
|
||||||
...item,
|
form.advantage = item.advantage || []
|
||||||
application_scene: item.application_scene || [],
|
form.brochure_url = item.brochure_url || ''
|
||||||
advantage: item.advantage || [],
|
form.brand = item.brand || null
|
||||||
brochure_url: item.brochure_url || '',
|
if (form.material_category) {
|
||||||
brand: item.brand || null
|
await onCategoryChange(form.material_category, false)
|
||||||
}
|
}
|
||||||
|
await searchBrandsForForm('')
|
||||||
|
ensureBrandInFormOptions(item.brand, item.brand_name)
|
||||||
dialogTitle.value = '编辑材料'
|
dialogTitle.value = '编辑材料'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpload = async (options) => {
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const result = await uploadImage(options.file)
|
||||||
|
form.brochure = result.path
|
||||||
|
form.brochure_url = result.url
|
||||||
|
ElMessage.success('图片上传成功')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('图片上传失败')
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
if (!form.value.brand) {
|
if (!form.brand) {
|
||||||
ElMessage.warning('请选择品牌')
|
ElMessage.warning('请选择品牌')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const payload = { ...form.value }
|
const payload = { ...form }
|
||||||
delete payload.brochure_url
|
delete payload.brochure_url
|
||||||
delete payload.brand_name
|
delete payload.brand_name
|
||||||
if (!isAdmin.value) {
|
if (!isAdmin.value) {
|
||||||
|
|
@ -387,10 +689,20 @@ const canDelete = (row) => isAdmin.value || row.status === 'draft'
|
||||||
const canSubmit = (row) => !isAdmin.value && row.status === 'draft'
|
const canSubmit = (row) => !isAdmin.value && row.status === 'draft'
|
||||||
const canApprove = (row) => isAdmin.value && row.status === 'pending'
|
const canApprove = (row) => isAdmin.value && row.status === 'pending'
|
||||||
|
|
||||||
const goDetail = (row) => {
|
const goDetail = async (row) => {
|
||||||
router.push(`/materials/${row.id}`)
|
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) => {
|
const onPageChange = (page) => {
|
||||||
pagination.page = page
|
pagination.page = page
|
||||||
loadMaterials()
|
loadMaterials()
|
||||||
|
|
@ -403,8 +715,10 @@ const onPageSizeChange = (size) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadStatusOptions()
|
loadChoices()
|
||||||
loadFilterSubcategories()
|
loadCategories()
|
||||||
|
loadSubcategories()
|
||||||
|
loadFactories()
|
||||||
loadBrandFilterOptions()
|
loadBrandFilterOptions()
|
||||||
loadMaterials()
|
loadMaterials()
|
||||||
})
|
})
|
||||||
|
|
@ -427,6 +741,12 @@ onMounted(() => {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-scroll :deep(.el-dialog__body) {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.material-drawer :deep(.el-drawer__body) {
|
.material-drawer :deep(.el-drawer__body) {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -435,4 +755,29 @@ onMounted(() => {
|
||||||
.material-drawer :deep(.el-drawer__footer) {
|
.material-drawer :deep(.el-drawer__footer) {
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brochure {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview img {
|
||||||
|
width: 120px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -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