feat(bid): 完成物料管理模块全功能开发
1. 新增物料详情页路由、菜单与接口,支持查看物料报价与信息 2. 重构物料列表页面,新增品牌筛选、表格样式优化与详情跳转 3. 扩展物料实体与数据库字段,新增材质、用途、性能参数等字段 4. 新增供应商/甲方报价查询、批量对比、同名称物料匹配接口 5. 新增物料详情组件,包含基础信息、供应商报价、甲方报价标签页 6. 修复比价路由跳转路径错误,调整数据库密码配置 7. 新增物料相关SQL脚本与初始化数据
This commit is contained in:
@@ -230,6 +230,7 @@ VALUES
|
||||
(2003,'报价请求',2000,3,'rfq','bid/rfq/index',NULL,1,0,'C','0','0','bid:rfq:list','form','admin',NOW(),'','',''),
|
||||
(2004,'供应商报价',2000,4,'quotation','bid/quotation/index',NULL,1,0,'C','0','0','bid:quotation:list','money','admin',NOW(),'','',''),
|
||||
(2005,'智慧比价',2000,5,'comparison','bid/comparison/index',NULL,1,0,'C','0','0','bid:comparison:list','chart','admin',NOW(),'','',''),
|
||||
(2051,'比价详情',2005,1,'detail','bid/comparison/detail',NULL,1,0,'C','1','0','bid:comparison:detail','#','admin',NOW(),'','',''),
|
||||
(2006,'采购单',2000,6,'purchaseorder','bid/purchaseorder/index',NULL,1,0,'C','0','0','bid:purchaseorder:list','shopping','admin',NOW(),'','',''),
|
||||
(2007,'供应商评价',2000,7,'evaluation','bid/evaluation/index',NULL,1,0,'C','0','0','bid:evaluation:list','star','admin',NOW(),'','',''),
|
||||
(2008,'订单异议',2000,8,'objection','bid/objection/index',NULL,1,0,'C','0','0','bid:objection:list','warning','admin',NOW(),'','',''),
|
||||
|
||||
497
doc/数据库业务表结构分析报告.md
Normal file
497
doc/数据库业务表结构分析报告.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# 福安德智慧报价平台 - 数据库业务表结构分析报告
|
||||
|
||||
> 文档版本: v1.0
|
||||
> 生成日期: 2025-05-27
|
||||
> 关联文档: [福安德智慧报价平台-完整开发方案.md](./福安德智慧报价平台-完整开发方案.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、概述
|
||||
|
||||
本报告对福安德智慧报价平台的数据库业务表结构进行全面分析,评估现有表结构是否满足开发方案中的业务需求,并指出缺失字段和建议优化项。
|
||||
|
||||
### 1.1 业务模块划分
|
||||
|
||||
根据开发方案,系统包含以下核心业务模块:
|
||||
|
||||
1. **物料信息管理** - 基础数据层
|
||||
2. **供应商管理** - 供货商基础、历史报价、纠纷
|
||||
3. **报价请求(RFQ)** - 询价单管理
|
||||
4. **供应商报价** - 子报价单填写
|
||||
5. **智慧比价** - 报价对比分析
|
||||
6. **采购单管理** - 订单执行
|
||||
7. **供应商评价** - 绩效评估
|
||||
8. **订单异议** - 纠纷处理
|
||||
9. **交易记录** - 财务流水
|
||||
|
||||
---
|
||||
|
||||
## 二、业务表结构详细分析
|
||||
|
||||
### 2.1 物料分类表 (biz_material_category)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| category_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| category_name | VARCHAR(100) | NO | - | 分类名称 | ✅ |
|
||||
| parent_id | BIGINT | YES | 0 | 父分类ID | ✅ |
|
||||
| ancestors | VARCHAR(500) | YES | '' | 祖级列表 | ✅ |
|
||||
| sort | INT | YES | 0 | 排序 | ✅ |
|
||||
| status | CHAR(1) | YES | '0' | 状态 | ⚠️ 无注释 |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 80% |
|
||||
| 缺失字段 | category_code(分类编码)、icon(图标)、level(层级) |
|
||||
| 建议优化 | 补充字段注释 |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 物料表 (biz_material)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| material_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| category_id | BIGINT | YES | 0 | 分类ID | ✅ |
|
||||
| material_code | VARCHAR(50) | NO | - | 物料编码 | ✅ |
|
||||
| material_name | VARCHAR(200) | NO | - | 物料名称 | ✅ |
|
||||
| spec | VARCHAR(500) | YES | '' | 规格型号 | ✅ |
|
||||
| unit | VARCHAR(50) | YES | '' | 单位 | ✅ |
|
||||
| brand | VARCHAR(100) | YES | '' | **厂家/品牌** | ✅ 已修改注释 |
|
||||
| description | TEXT | YES | NULL | 描述 | ✅ |
|
||||
| status | CHAR(1) | YES | '0' | 状态 | ⚠️ 无注释 |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
| update_by | VARCHAR(64) | YES | '' | 更新者 | ⚠️ 无注释 |
|
||||
| update_time | DATETIME | YES | NULL | 更新时间 | ⚠️ 无注释 |
|
||||
| remark | VARCHAR(500) | YES | NULL | 备注 | ✅ |
|
||||
|
||||
#### 二期扩展字段 (2026-05-27)
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| performance_params | TEXT | YES | NULL | 性能参数(JSON) | ✅ 新增 |
|
||||
| material | VARCHAR(100) | YES | '' | 材质 | ✅ 新增 |
|
||||
| purpose | VARCHAR(500) | YES | '' | 用途 | ✅ 新增 |
|
||||
| image_url | VARCHAR(500) | YES | '' | 物料图片URL | ✅ 新增 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 95% |
|
||||
| 已满足需求 | 物料ID、厂家/品牌、规格型号、性能参数、材质、用途、备注 |
|
||||
| 建议优化 | 补充status、create_by等字段注释 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 供应商表 (biz_supplier)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| supplier_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| supplier_name | VARCHAR(200) | NO | - | 供应商名称 | ✅ |
|
||||
| contact | VARCHAR(50) | YES | '' | 联系人 | ⚠️ 无注释 |
|
||||
| phone | VARCHAR(20) | YES | '' | 联系电话 | ⚠️ 无注释 |
|
||||
| email | VARCHAR(100) | YES | '' | 邮箱 | ⚠️ 无注释 |
|
||||
| address | VARCHAR(500) | YES | '' | 地址 | ⚠️ 无注释 |
|
||||
| user_id | BIGINT | YES | NULL | 关联用户ID | ❌ 无注释,用途不明 |
|
||||
| status | CHAR(1) | YES | '0' | 状态 | ⚠️ 无注释 |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
| update_by | VARCHAR(64) | YES | '' | 更新者 | ⚠️ 无注释 |
|
||||
| update_time | DATETIME | YES | NULL | 更新时间 | ⚠️ 无注释 |
|
||||
| remark | VARCHAR(500) | YES | NULL | 备注 | ✅ |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 60% |
|
||||
| **缺失字段** | 供应商编码、统一社会信用代码、开户银行、银行账号、合作状态、资质证书 |
|
||||
| **需求不匹配** | 缺少"历史报价"、"纠纷"相关字段(应在关联表中) |
|
||||
| 建议优化 | 补充所有字段注释;考虑增加供应商分类字段 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 报价请求主表 (biz_rfq)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| rfq_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| rfq_no | VARCHAR(50) | NO | - | 询价单号 | ✅ |
|
||||
| rfq_title | VARCHAR(200) | NO | - | 询价标题 | ✅ |
|
||||
| deadline | DATETIME | YES | NULL | 截止日期 | ⚠️ 无注释 |
|
||||
| delivery_addr | VARCHAR(500) | YES | '' | 交货地址 | ⚠️ 无注释 |
|
||||
| status | VARCHAR(20) | YES | 'draft' | 状态 | ⚠️ 无注释 |
|
||||
| remark | TEXT | YES | NULL | 备注 | ✅ |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
| update_by | VARCHAR(64) | YES | '' | 更新者 | ⚠️ 无注释 |
|
||||
| update_time | DATETIME | YES | NULL | 更新时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 70% |
|
||||
| **缺失字段** | 询价类型、紧急程度、付款方式、税率、甲方ID、项目ID |
|
||||
| **状态值不明确** | status字段应注释状态值: draft/published/closed/completed |
|
||||
| 建议优化 | 补充字段注释;明确状态流转规则 |
|
||||
|
||||
---
|
||||
|
||||
### 2.5 报价请求明细表 (biz_rfq_item)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| item_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| rfq_id | BIGINT | NO | - | 询价单ID | ✅ |
|
||||
| material_id | BIGINT | YES | 0 | 物料ID | ✅ |
|
||||
| material_name | VARCHAR(200) | NO | - | 物料名称 | ✅ |
|
||||
| spec | VARCHAR(500) | YES | '' | 规格 | ⚠️ 无注释 |
|
||||
| unit | VARCHAR(50) | YES | '' | 单位 | ⚠️ 无注释 |
|
||||
| quantity | DECIMAL(15,4) | NO | - | 数量 | ⚠️ 无注释 |
|
||||
| expected_price | DECIMAL(15,4) | YES | NULL | 期望单价 | ⚠️ 无注释 |
|
||||
| remark | VARCHAR(500) | YES | '' | 备注 | ✅ |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 85% |
|
||||
| **缺失字段** | 技术要求、质量标准、品牌要求、交货期要求 |
|
||||
| 建议优化 | 补充字段注释 |
|
||||
|
||||
---
|
||||
|
||||
### 2.6 询价供应商关联表 (biz_rfq_supplier)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| rfq_id | BIGINT | NO | - | 询价单ID | ✅ |
|
||||
| supplier_id | BIGINT | NO | - | 供应商ID | ✅ |
|
||||
| invited_time | DATETIME | YES | NULL | 邀请时间 | ⚠️ 无注释 |
|
||||
| quoted_time | DATETIME | YES | NULL | 报价时间 | ⚠️ 无注释 |
|
||||
| status | VARCHAR(20) | YES | 'pending' | 状态 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 75% |
|
||||
| **缺失字段** | 邀请人、邀请方式、是否查看、查看时间 |
|
||||
| **状态值不明确** | status字段应注释: pending/invited/quoted/rejected |
|
||||
| 建议优化 | 补充字段注释;增加邀请记录追踪 |
|
||||
|
||||
---
|
||||
|
||||
### 2.7 供应商报价单表 (biz_quotation)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| quotation_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| rfq_id | BIGINT | NO | - | 询价单ID | ✅ |
|
||||
| supplier_id | BIGINT | NO | - | 供应商ID | ✅ |
|
||||
| quote_no | VARCHAR(50) | YES | '' | 报价单号 | ⚠️ 无注释 |
|
||||
| valid_days | INT | YES | 30 | 有效期(天) | ⚠️ 无注释 |
|
||||
| delivery_days | INT | YES | 0 | 交货周期(天) | ⚠️ 无注释 |
|
||||
| total_amount | DECIMAL(15,4) | YES | 0 | 总金额 | ⚠️ 无注释 |
|
||||
| currency | VARCHAR(10) | YES | 'CNY' | 币种 | ⚠️ 无注释 |
|
||||
| status | VARCHAR(20) | YES | 'draft' | 状态 | ⚠️ 无注释 |
|
||||
| note | TEXT | YES | NULL | 备注 | ⚠️ 无注释 |
|
||||
| submit_time | DATETIME | YES | NULL | 提交时间 | ⚠️ 无注释 |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
| update_time | DATETIME | YES | NULL | 更新时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 70% |
|
||||
| **缺失字段** | 报价有效期截止日期、付款方式、运费承担方、税率、是否含税 |
|
||||
| **状态值不明确** | status字段应注释: draft/submitted/accepted/rejected |
|
||||
| 建议优化 | 补充所有字段注释 |
|
||||
|
||||
---
|
||||
|
||||
### 2.8 报价明细表 (biz_quotation_item)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| item_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| quotation_id | BIGINT | NO | - | 报价单ID | ✅ |
|
||||
| rfq_item_id | BIGINT | NO | - | 询价明细ID | ✅ |
|
||||
| material_name | VARCHAR(200) | YES | '' | 物料名称 | ⚠️ 无注释 |
|
||||
| spec | VARCHAR(500) | YES | '' | 规格 | ⚠️ 无注释 |
|
||||
| unit | VARCHAR(50) | YES | '' | 单位 | ⚠️ 无注释 |
|
||||
| quantity | DECIMAL(15,4) | YES | 0 | 数量 | ⚠️ 无注释 |
|
||||
| unit_price | DECIMAL(15,4) | NO | - | 单价 | ⚠️ 无注释 |
|
||||
| total_price | DECIMAL(15,4) | YES | 0 | 总价 | ⚠️ 无注释 |
|
||||
| delivery_days | INT | YES | 0 | 交货天数 | ⚠️ 无注释 |
|
||||
| remark | VARCHAR(500) | YES | '' | 备注 | ✅ |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 80% |
|
||||
| **缺失字段** | 品牌、产地、质保期、最小起订量、库存状态 |
|
||||
| 建议优化 | 补充所有字段注释 |
|
||||
|
||||
---
|
||||
|
||||
### 2.9 采购单主表 (biz_purchase_order)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| po_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| po_no | VARCHAR(50) | NO | - | 采购单号 | ✅ |
|
||||
| rfq_id | BIGINT | YES | NULL | 询价单ID | ✅ |
|
||||
| supplier_id | BIGINT | NO | - | 供应商ID | ✅ |
|
||||
| total_amount | DECIMAL(15,4) | YES | 0 | 总金额 | ⚠️ 无注释 |
|
||||
| currency | VARCHAR(10) | YES | 'CNY' | 币种 | ⚠️ 无注释 |
|
||||
| delivery_addr | VARCHAR(500) | YES | '' | 交货地址 | ⚠️ 无注释 |
|
||||
| delivery_date | DATE | YES | NULL | 交货日期 | ⚠️ 无注释 |
|
||||
| status | VARCHAR(20) | YES | 'draft' | 状态 | ⚠️ 无注释 |
|
||||
| remark | TEXT | YES | NULL | 备注 | ✅ |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
| update_by | VARCHAR(64) | YES | '' | 更新者 | ⚠️ 无注释 |
|
||||
| update_time | DATETIME | YES | NULL | 更新时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 65% |
|
||||
| **缺失字段** | 合同编号、付款方式、付款比例、运费承担、税率、发票类型、联系人、联系电话 |
|
||||
| **状态值不明确** | status字段应注释: draft/confirmed/shipped/received/completed |
|
||||
| 建议优化 | 补充所有字段注释;增加订单执行跟踪字段 |
|
||||
|
||||
---
|
||||
|
||||
### 2.10 采购单明细表 (biz_purchase_order_item)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| item_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| po_id | BIGINT | NO | - | 采购单ID | ✅ |
|
||||
| material_id | BIGINT | YES | 0 | 物料ID | ✅ |
|
||||
| material_name | VARCHAR(200) | NO | - | 物料名称 | ✅ |
|
||||
| spec | VARCHAR(500) | YES | '' | 规格 | ⚠️ 无注释 |
|
||||
| unit | VARCHAR(50) | YES | '' | 单位 | ⚠️ 无注释 |
|
||||
| quantity | DECIMAL(15,4) | NO | - | 数量 | ⚠️ 无注释 |
|
||||
| unit_price | DECIMAL(15,4) | NO | - | 单价 | ⚠️ 无注释 |
|
||||
| total_price | DECIMAL(15,4) | YES | 0 | 总价 | ⚠️ 无注释 |
|
||||
| remark | VARCHAR(500) | YES | '' | 备注 | ✅ |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 75% |
|
||||
| **缺失字段** | 已到货数量、到货率、质检状态、入库状态 |
|
||||
| 建议优化 | 补充字段注释;增加订单执行跟踪字段 |
|
||||
|
||||
---
|
||||
|
||||
### 2.11 供应商评价表 (biz_supplier_evaluation)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| eval_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| po_id | BIGINT | NO | - | 采购单ID | ✅ |
|
||||
| supplier_id | BIGINT | NO | - | 供应商ID | ✅ |
|
||||
| quality_score | INT | YES | 5 | 质量评分 | ⚠️ 无注释 |
|
||||
| delivery_score | INT | YES | 5 | 交货评分 | ⚠️ 无注释 |
|
||||
| service_score | INT | YES | 5 | 服务评分 | ⚠️ 无注释 |
|
||||
| price_score | INT | YES | 5 | 价格评分 | ⚠️ 无注释 |
|
||||
| total_score | DECIMAL(3,1) | YES | 5.0 | 综合评分 | ⚠️ 无注释 |
|
||||
| comment | TEXT | YES | NULL | 评价内容 | ⚠️ 无注释 |
|
||||
| evaluator | VARCHAR(64) | YES | '' | 评价人 | ⚠️ 无注释 |
|
||||
| eval_time | DATETIME | YES | NULL | 评价时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 75% |
|
||||
| **缺失字段** | 评价周期、是否匿名、评价状态(草稿/已提交) |
|
||||
| **评分范围不明确** | 应注释评分范围: 1-10分或1-5分 |
|
||||
| 建议优化 | 补充所有字段注释;明确评分规则 |
|
||||
|
||||
---
|
||||
|
||||
### 2.12 订单异议表 (biz_order_objection)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| objection_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| po_id | BIGINT | NO | - | 采购单ID | ✅ |
|
||||
| supplier_id | BIGINT | NO | - | 供应商ID | ✅ |
|
||||
| reason | TEXT | NO | - | 异议原因 | ⚠️ 无注释 |
|
||||
| attachment | VARCHAR(500) | YES | '' | 附件 | ⚠️ 无注释 |
|
||||
| status | VARCHAR(20) | YES | 'pending' | 状态 | ⚠️ 无注释 |
|
||||
| resolution | TEXT | YES | NULL | 解决方案 | ⚠️ 无注释 |
|
||||
| create_by | VARCHAR(64) | YES | '' | 创建者 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
| resolve_time | DATETIME | YES | NULL | 解决时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 70% |
|
||||
| **缺失字段** | 异议类型(质量/交期/数量/其他)、责任方、赔偿金额、关闭人、关闭时间 |
|
||||
| **状态值不明确** | status字段应注释: pending/processing/resolved/closed |
|
||||
| 建议优化 | 补充所有字段注释;完善异议处理流程 |
|
||||
|
||||
---
|
||||
|
||||
### 2.13 交易记录表 (biz_transaction)
|
||||
|
||||
#### 表结构
|
||||
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 备注 | 需求匹配度 |
|
||||
|--------|------|------|--------|------|-----------|
|
||||
| tx_id | BIGINT | NO | AUTO_INCREMENT | 主键 | ✅ |
|
||||
| tenant_id | BIGINT | NO | 1 | 租户ID | ✅ |
|
||||
| po_id | BIGINT | YES | NULL | 采购单ID | ✅ |
|
||||
| supplier_id | BIGINT | YES | NULL | 供应商ID | ✅ |
|
||||
| tx_type | VARCHAR(50) | YES | '' | 交易类型 | ⚠️ 无注释 |
|
||||
| tx_no | VARCHAR(50) | YES | '' | 交易单号 | ⚠️ 无注释 |
|
||||
| amount | DECIMAL(15,4) | YES | NULL | 金额 | ⚠️ 无注释 |
|
||||
| currency | VARCHAR(10) | YES | 'CNY' | 币种 | ⚠️ 无注释 |
|
||||
| description | TEXT | YES | NULL | 描述 | ⚠️ 无注释 |
|
||||
| operator | VARCHAR(64) | YES | '' | 操作人 | ⚠️ 无注释 |
|
||||
| tx_time | DATETIME | YES | NULL | 交易时间 | ⚠️ 无注释 |
|
||||
| create_time | DATETIME | YES | NULL | 创建时间 | ⚠️ 无注释 |
|
||||
|
||||
#### 分析结论
|
||||
|
||||
| 评估项 | 结果 |
|
||||
|--------|------|
|
||||
| 需求满足度 | 60% |
|
||||
| **缺失字段** | 交易对方账户、交易状态、凭证号、附件、审核人、审核时间 |
|
||||
| **交易类型不明确** | tx_type应注释: payment/refund/deposit/withdrawal |
|
||||
| 建议优化 | 补充所有字段注释;完善财务审核流程 |
|
||||
|
||||
---
|
||||
|
||||
## 三、总体评估
|
||||
|
||||
### 3.1 各表需求满足度汇总
|
||||
|
||||
| 表名 | 需求满足度 | 主要问题 |
|
||||
|------|-----------|---------|
|
||||
| biz_material_category | 80% | 缺少分类编码、图标字段 |
|
||||
| biz_material | 95% | 字段注释不完整 |
|
||||
| biz_supplier | 60% | 缺少供应商编码、资质信息 |
|
||||
| biz_rfq | 70% | 缺少询价类型、付款方式 |
|
||||
| biz_rfq_item | 85% | 缺少技术要求、质量标准 |
|
||||
| biz_rfq_supplier | 75% | 缺少邀请记录追踪 |
|
||||
| biz_quotation | 70% | 缺少付款方式、运费承担 |
|
||||
| biz_quotation_item | 80% | 缺少品牌、产地、质保期 |
|
||||
| biz_purchase_order | 65% | 缺少合同信息、付款信息 |
|
||||
| biz_purchase_order_item | 75% | 缺少到货跟踪字段 |
|
||||
| biz_supplier_evaluation | 75% | 评分规则不明确 |
|
||||
| biz_order_objection | 70% | 缺少异议类型、赔偿信息 |
|
||||
| biz_transaction | 60% | 缺少交易状态、审核流程 |
|
||||
|
||||
### 3.2 共性问题
|
||||
|
||||
1. **字段注释缺失严重** - 约70%的字段缺少注释
|
||||
2. **状态值不明确** - 所有status字段未注释状态值枚举
|
||||
3. **缺少关键业务字段** - 如付款方式、税率、发票类型等
|
||||
4. **缺少审计字段** - 部分表缺少update_by字段
|
||||
|
||||
### 3.3 建议优化项
|
||||
|
||||
#### 高优先级 (P0)
|
||||
- [ ] 补充所有字段注释
|
||||
- [ ] 明确所有status字段的状态值枚举
|
||||
- [ ] 为biz_supplier添加供应商编码字段
|
||||
|
||||
#### 中优先级 (P1)
|
||||
- [ ] 为biz_purchase_order添加合同信息字段
|
||||
- [ ] 为biz_transaction添加交易状态字段
|
||||
- [ ] 为biz_order_objection添加异议类型字段
|
||||
|
||||
#### 低优先级 (P2)
|
||||
- [ ] 为biz_material_category添加分类编码
|
||||
- [ ] 为biz_quotation_item添加品牌、产地字段
|
||||
- [ ] 考虑增加数据版本控制字段
|
||||
|
||||
---
|
||||
|
||||
## 四、附录
|
||||
|
||||
### 4.1 相关文件
|
||||
|
||||
- 业务表SQL: `sql/bid_tables.sql`
|
||||
- 物料扩展SQL: `sql/20260527/01_material_extension.sql`
|
||||
- 开发方案: `doc/福安德智慧报价平台-完整开发方案.md`
|
||||
- 物料开发文档: `doc/智采系统-物料信息模块开发文档.md`
|
||||
|
||||
### 4.2 建议补充的通用字段
|
||||
|
||||
所有业务表建议补充以下字段:
|
||||
|
||||
```sql
|
||||
-- 数据版本控制
|
||||
version INT DEFAULT 1 COMMENT '数据版本号',
|
||||
|
||||
-- 扩展字段(预留)
|
||||
ext_field1 VARCHAR(500) COMMENT '扩展字段1',
|
||||
ext_field2 VARCHAR(500) COMMENT '扩展字段2',
|
||||
|
||||
-- 数据隔离
|
||||
dept_id BIGINT COMMENT '所属部门ID',
|
||||
|
||||
-- 删除标记(物理删除场景)
|
||||
delete_time DATETIME COMMENT '删除时间',
|
||||
delete_by VARCHAR(64) COMMENT '删除人',
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*报告生成完成,建议根据以上分析逐步优化数据库结构。*
|
||||
868
doc/智采系统-物料信息模块开发文档.md
Normal file
868
doc/智采系统-物料信息模块开发文档.md
Normal file
@@ -0,0 +1,868 @@
|
||||
# 智采系统-物料信息模块开发文档
|
||||
|
||||
> 文档版本: v1.1
|
||||
> 创建日期: 2025-05-27
|
||||
> 关联文档: [福安德智慧报价平台-完整开发方案.md](./福安德智慧报价平台-完整开发方案.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
### 1.1 功能定位
|
||||
智采系统-物料信息模块是整个报价平台的基础数据层,负责管理所有物料的基础信息、性能参数、历史报价记录。
|
||||
|
||||
### 1.2 核心功能
|
||||
- **物料信息管理**: 挂载于物料类型下,支持汇总和CRUD操作
|
||||
- **物料详情展示**: 基础信息展示,支持动态更新
|
||||
- **历史报价查询**:
|
||||
- 供应商历史报价清单(单价、供应商)
|
||||
- 我方给甲方的历史报价
|
||||
|
||||
### 1.3 字段需求
|
||||
根据业务需求,物料信息需要以下字段:
|
||||
|
||||
| 需求字段 | 对应数据库字段 | 说明 |
|
||||
|---------|--------------|------|
|
||||
| 物料ID | material_id | 主键,自增 |
|
||||
| **厂家/品牌** | **brand** | **改为"厂家/品牌"** |
|
||||
| 规格型号 | spec | 规格型号(**保持不变**) |
|
||||
| 性能参数 | performance_params | JSON格式存储(**新增**) |
|
||||
| 材质 | material | 材质(铜/铝合金/PVC等)(**新增**) |
|
||||
| 用途 | purpose | 用途描述(**新增**) |
|
||||
| 备注 | remark | 备注(**保持不变**) |
|
||||
|
||||
---
|
||||
|
||||
## 二、数据库设计
|
||||
|
||||
### 2.1 现有表结构(保持不变)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `biz_material` (
|
||||
`material_id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` bigint NOT NULL DEFAULT '1',
|
||||
`category_id` bigint DEFAULT '0',
|
||||
`material_code` varchar(50) NOT NULL COMMENT '物料编码',
|
||||
`material_name` varchar(200) NOT NULL COMMENT '物料名称',
|
||||
`spec` varchar(500) DEFAULT '' COMMENT '规格型号',
|
||||
`unit` varchar(50) DEFAULT '' COMMENT '单位',
|
||||
`brand` varchar(100) DEFAULT '' COMMENT '品牌',
|
||||
`description` text COMMENT '描述',
|
||||
`status` char(1) DEFAULT '0',
|
||||
`create_by` varchar(64) DEFAULT '',
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`update_by` varchar(64) DEFAULT '',
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`remark` varchar(500) DEFAULT NULL,
|
||||
PRIMARY KEY (`material_id`)
|
||||
) ENGINE=InnoDB COMMENT='物料';
|
||||
```
|
||||
|
||||
### 2.2 扩展字段
|
||||
|
||||
执行SQL文件: [sql/20250527/01_material_alter.sql](../sql/20250527/01_material_alter.sql)
|
||||
|
||||
```sql
|
||||
-- 新增字段
|
||||
ALTER TABLE biz_material
|
||||
ADD COLUMN IF NOT EXISTS performance_params TEXT COMMENT '性能参数(JSON)',
|
||||
ADD COLUMN IF NOT EXISTS material VARCHAR(100) DEFAULT '' COMMENT '材质',
|
||||
ADD COLUMN IF NOT EXISTS purpose VARCHAR(500) DEFAULT '' COMMENT '用途',
|
||||
ADD COLUMN IF NOT EXISTS image_url VARCHAR(500) DEFAULT '' COMMENT '物料图片URL';
|
||||
|
||||
-- 修改brand字段为"厂家/品牌"
|
||||
ALTER TABLE biz_material MODIFY COLUMN brand VARCHAR(100) COMMENT '厂家/品牌';
|
||||
```
|
||||
|
||||
### 2.3 横向对比关系表
|
||||
|
||||
用于存储物料之间的对比关系(不同品牌、规格的同比信息):
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz_material_comparison (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1 COMMENT '租户ID',
|
||||
material_id BIGINT NOT NULL COMMENT '当前物料ID',
|
||||
counterpart_id BIGINT NOT NULL COMMENT '对标物料ID',
|
||||
comparison_type VARCHAR(50) DEFAULT 'same_spec' COMMENT '对比类型(same_spec:同规格, similar:相似规格)',
|
||||
brand_level VARCHAR(50) COMMENT '品牌等级(国产一线/国产二线/进口一线/进口二线)',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||
status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME COMMENT '创建时间',
|
||||
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME COMMENT '更新时间',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
UNIQUE KEY uk_material_counterpart (material_id, counterpart_id),
|
||||
KEY idx_material (material_id),
|
||||
KEY idx_counterpart (counterpart_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物料横向对比关系';
|
||||
```
|
||||
|
||||
**表设计说明**:
|
||||
- 不采用"对比组"模式,而是直接建立物料之间的对比关系,更灵活
|
||||
- 支持同规格对比和相似规格对比
|
||||
- 可维护品牌等级,用于综合评分
|
||||
- 通过唯一索引防止重复添加对标物料
|
||||
|
||||
### 2.3 字段对照说明
|
||||
|
||||
| 原字段 | 原含义 | 新含义 | 说明 |
|
||||
|--------|--------|--------|------|
|
||||
| spec | 规格型号 | **规格型号** | **保持不变** |
|
||||
| **brand** | **品牌** | **厂家/品牌** | **修改注释** |
|
||||
| remark | 备注 | **备注** | **保持不变** |
|
||||
| performance_params | - | 性能参数 | 新增 |
|
||||
| material | - | 材质 | 新增 |
|
||||
| purpose | - | 用途 | 新增 |
|
||||
| image_url | - | 物料图片URL | 新增 |
|
||||
|
||||
### 2.4 性能参数JSON规范
|
||||
|
||||
```json
|
||||
// 变频器示例
|
||||
{
|
||||
"功率": "37kW",
|
||||
"电压": "380V",
|
||||
"电流": "72A",
|
||||
"过载能力": "150%/60s",
|
||||
"通讯接口": "RS485/PROFINET",
|
||||
"防护等级": "IP20"
|
||||
}
|
||||
|
||||
// 电缆示例
|
||||
{
|
||||
"芯数×截面": "3×70+1×35",
|
||||
"额定电压": "0.6/1kV",
|
||||
"外径": "42mm",
|
||||
"载流量": "210A",
|
||||
"护套材质": "PVC铠装"
|
||||
}
|
||||
|
||||
// PLC示例
|
||||
{
|
||||
"CPU型号": "1515-2 PN",
|
||||
"内存": "500KB",
|
||||
"数字量输入": "1024",
|
||||
"数字量输出": "1024",
|
||||
"通讯接口": "PROFINET"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、页面设计
|
||||
|
||||
### 3.1 物料信息列表页
|
||||
|
||||
#### 3.1.1 页面布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 智采系统-物料信息 [返回首页] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 挂载于物料类型下面的物料信息在此进行汇总和CRUD │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 搜索栏: [物料名称] [物料编码] [厂家/品牌] [分类] [搜索] [重置]│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ [新增] [删除] [导入] [导出] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ □ | 物料ID | 厂家/品牌 | 规格型号 | 性能参数 | 材质 | 用途 | 操作 │
|
||||
│ ───────────────────────────────────────────────────────── │
|
||||
│ □ | M001 | 汇川 | MD500T37G | 37kW... | 铜 | 通用 | [详情][修改][删除] │
|
||||
│ □ | M002 | 西门子 | G120 | 37kW... | 铝 | 风机 | [详情][修改][删除] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 分页: 1 2 3 ... 10 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.1.2 表格字段
|
||||
|
||||
| 字段 | 宽度 | 对应字段 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| 复选框 | 55px | - | 批量操作 |
|
||||
| 物料ID | 100px | material_id | - |
|
||||
| **厂家/品牌** | 120px | **brand** | **改为"厂家/品牌"** |
|
||||
| **规格型号** | 150px | **spec** | **保持不变** |
|
||||
| 性能参数 | 200px | performance_params | 截取显示 |
|
||||
| 材质 | 80px | material | 新增 |
|
||||
| 用途 | 150px | purpose | 新增 |
|
||||
| 操作 | 180px | - | 详情/修改/删除 |
|
||||
|
||||
#### 3.1.3 操作按钮
|
||||
|
||||
- **详情**: 跳转到物料详情页
|
||||
- **修改**: 打开编辑对话框
|
||||
- **删除**: 删除物料(逻辑删除)
|
||||
|
||||
### 3.2 新增/编辑对话框
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 新增物料 / 修改物料 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 物料编码* [________________] │
|
||||
│ 物料名称* [________________] │
|
||||
│ 所属分类 [下拉选择________] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 厂家/品牌* [________________] │ ← brand字段(改为"厂家/品牌")
|
||||
│ 规格型号* [________________] │ ← spec字段(保持不变)
|
||||
│ 备注 [________________] │ ← remark字段(保持不变)
|
||||
│ 材质 [________________] │ ← material字段(新增)
|
||||
│ 用途 [________________] │ ← purpose字段(新增)
|
||||
├─────────────────────────────────────────┤
|
||||
│ 性能参数 (动态表单) │
|
||||
│ ┌─────────┬─────────┬───────┐ [添加] │
|
||||
│ │ 参数名 │ 参数值 │ 单位 │ │
|
||||
│ │ [功率] │ [37] │ [kW] │ [删除] │
|
||||
│ │ [电压] │ [380] │ [V] │ [删除] │
|
||||
│ └─────────┴─────────┴───────┘ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 描述 [多行文本框____________] │ ← description字段
|
||||
├─────────────────────────────────────────┤
|
||||
│ [取消] [确定] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 物料详情页
|
||||
|
||||
#### 3.3.1 整体布局(重新设计)
|
||||
|
||||
页面分为上下两部分:
|
||||
- **上半部分**: 可编辑的基础信息(物料编码、厂家/品牌、规格型号等 + 性能参数 + 描述)
|
||||
- **下半部分**: Tab切换(供应商报价历史、甲方报价历史)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 明纬/西门子 电源模块 24VDC/10A,输入85-264VAC,效率≥90% [返回列表] │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 基础信息 [编辑] [保存] [取消] │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────┬────────────┬────────────┬────────────┬────────────┬────────┐│
|
||||
│ │ 物料编码 │ M-EL-006 │ 厂家/品牌 │ 明纬/西门子│ 规格型号 │ 24VDC/10A...│
|
||||
│ ├────────────┼────────────┼────────────┼────────────┼────────────┼────────┤│
|
||||
│ │ 材质 │ - │ 用途 │ - │ 备注 │ - │
|
||||
│ │ 单位 │ 台 │ 所属分类 │ 电源模块 │ │ │
|
||||
│ └────────────┴────────────┴────────────┴────────────┴────────────┴────────┘│
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 性能参数 [添加] │
|
||||
│ ┌────────────────┬──────────────────────────────┬────────────────┬────────┐│
|
||||
│ │ 参数名 │ 参数值 │ 单位 │ 操作 ││
|
||||
│ ├────────────────┼──────────────────────────────┼────────────────┼────────┤│
|
||||
│ │ 输出电压 │ 24VDC │ V │ [删除] ││
|
||||
│ │ 输出电流 │ 10 │ A │ [删除] ││
|
||||
│ │ ... │ ... │ ... │ ... ││
|
||||
│ └────────────────┴──────────────────────────────┴────────────────┴────────┘│
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 描述 │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────┐│
|
||||
│ │ 明纬/西门子 电源模块 24VDC/10A,输入85-264VAC,效率≥90% ││
|
||||
│ └────────────────────────────────────────────────────────────────────────┘│
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [供应商报价历史] [甲方报价历史] │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Tab 内容区域(报价历史表格) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**布局说明**:
|
||||
- **顶部标题栏**: 显示物料名称和规格型号,右侧返回按钮
|
||||
- **基础信息区域**: 紧凑的网格布局,一行4个字段,可直接编辑
|
||||
- **性能参数区域**: 表格形式展示,支持增删改
|
||||
- **描述区域**: 文本区域展示,支持编辑
|
||||
- **Tab区域**: 仅保留供应商报价历史和甲方报价历史两个Tab
|
||||
|
||||
**组件实现**: [detail.vue](../ruoyi-ui/src/views/bid/material/detail.vue)
|
||||
|
||||
**核心功能**:
|
||||
1. **基础信息网格布局**: 紧凑的flex布局,一行显示多个字段
|
||||
2. **实时编辑**: 点击"编辑"按钮,所有字段变为可编辑状态
|
||||
3. **实时保存**: 点击"保存"调用API,更新本地数据,无需刷新页面
|
||||
4. **性能参数表格**: 支持添加/删除参数
|
||||
5. **描述区域**: 多行文本编辑
|
||||
|
||||
**数据结构**:
|
||||
```javascript
|
||||
// 表单数据
|
||||
form: {
|
||||
materialCode: 'M-EL-006',
|
||||
materialName: '电源模块',
|
||||
brand: '明纬/西门子',
|
||||
spec: '24VDC/10A...',
|
||||
unit: '台',
|
||||
material: '',
|
||||
purpose: '',
|
||||
remark: '',
|
||||
description: '明纬/西门子 电源模块...'
|
||||
}
|
||||
|
||||
// 性能参数数据
|
||||
perfParams: [
|
||||
{ name: '输出电压', value: '24VDC', unit: 'V' },
|
||||
{ name: '输出电流', value: '10', unit: 'A' }
|
||||
]
|
||||
```
|
||||
|
||||
**保存逻辑**:
|
||||
```javascript
|
||||
async handleSave() {
|
||||
const saveData = {
|
||||
materialId: this.materialId,
|
||||
...this.form,
|
||||
performanceParams: JSON.stringify(this.perfParams)
|
||||
};
|
||||
|
||||
await updateMaterial(saveData);
|
||||
this.$message.success("保存成功");
|
||||
this.isEditing = false;
|
||||
|
||||
// 更新本地数据(无需刷新页面)
|
||||
this.material = { ...this.material, ...saveData };
|
||||
}
|
||||
```
|
||||
// ...
|
||||
]
|
||||
|
||||
// 性能参数数据
|
||||
perfParams: [
|
||||
{ name: '输出电压', value: '24VDC', unit: 'V' },
|
||||
{ name: '输出电流', value: '10', unit: 'A' },
|
||||
// ...
|
||||
]
|
||||
|
||||
// 描述数据
|
||||
description: '明纬/西门子 电源模块 24VDC/10A...'
|
||||
```
|
||||
|
||||
**保存逻辑**:
|
||||
```javascript
|
||||
async handleSave() {
|
||||
const saveData = {
|
||||
materialId: this.material.materialId,
|
||||
materialCode: this.basicInfoData[0].value,
|
||||
materialName: this.basicInfoData[1].value,
|
||||
// ... 其他字段
|
||||
performanceParams: JSON.stringify(this.perfParams),
|
||||
description: this.description
|
||||
};
|
||||
|
||||
await updateMaterial(saveData);
|
||||
this.$message.success("保存成功");
|
||||
this.isEditing = false;
|
||||
|
||||
// 更新本地数据(无需刷新页面)
|
||||
this.$emit('update:material', { ...this.material, ...saveData });
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.3 Tab 2: 供应商报价历史
|
||||
|
||||
供应商信息完整展示,调整列宽使内容在一行显示:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 供应商报价历史 [导出Excel] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 报价日期 │ 供应商信息 │ 单价(元) │ 数量 │ 交期 │ 状态 │
|
||||
│ ───────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ 2026-05-20 │ 华顺达电子科技有限公司 │ ¥6,200.00 │ 5 │ 7天 │ [已中标] │
|
||||
│ │ 联系人: 张经理 13800138000│ │ │ │ │
|
||||
│ ───────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ 2026-05-18 │ 博远精密机械有限公司 │ ¥6,500.00 │ 5 │ 10天 │ [未中标] │
|
||||
│ │ 联系人: 李工 13900139000 │ │ │ │ │
|
||||
│ ───────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ 2026-05-15 │ 瑞达工控设备有限公司 │ ¥6,800.00 │ 5 │ 5天 │ [未中标] │
|
||||
│ │ 联系人: 王总 13700137000 │ │ │ │ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**表格列宽配置**:
|
||||
| 列名 | 宽度 | 说明 |
|
||||
|------|------|------|
|
||||
| 报价日期 | 100px | 日期格式:YYYY-MM-DD |
|
||||
| 供应商信息 | 280px | 供应商名称+联系人+电话,完整一行展示 |
|
||||
| 单价 | 120px | 右对齐,保留2位小数 |
|
||||
| 数量 | 80px | 居中对齐 |
|
||||
| 交期 | 80px | 居中对齐,带单位"天" |
|
||||
| 状态 | 100px | 标签形式展示 |
|
||||
|
||||
**Vue组件实现**:
|
||||
```vue
|
||||
<template>
|
||||
<div class="supplier-quote-tab">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport">导出Excel</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="quoteList" border v-loading="loading">
|
||||
<el-table-column label="报价日期" prop="submitTime" width="100" align="center" />
|
||||
|
||||
<!-- 供应商信息合并展示 -->
|
||||
<el-table-column label="供应商信息" min-width="280">
|
||||
<template slot-scope="scope">
|
||||
<div class="supplier-info">
|
||||
<div class="supplier-name">{{ scope.row.supplierName }}</div>
|
||||
<div class="supplier-contact">
|
||||
<span>联系人: {{ scope.row.contact || '-' }}</span>
|
||||
<span v-if="scope.row.phone">{{ scope.row.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="单价(元)" prop="unitPrice" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span style="color: #f56c6c; font-weight: bold;">¥{{ formatPrice(scope.row.unitPrice) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="数量" prop="quantity" width="80" align="center" />
|
||||
|
||||
<el-table-column label="交期" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.deliveryDays }}天</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.isWinning === '1'" type="success" size="small">已中标</el-tag>
|
||||
<el-tag v-else type="info" size="small">未中标</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.supplier-info {
|
||||
line-height: 1.5;
|
||||
}
|
||||
.supplier-name {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.supplier-contact {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.supplier-contact span {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**数据来源**: `biz_quotation_item` → `biz_quotation` → `biz_supplier`
|
||||
|
||||
#### 3.3.4 Tab 3: 甲方报价历史
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 我方给甲方的历史报价 [导出Excel] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 报价日期 甲方名称 单价(元) 折扣率 成交价 状态 │
|
||||
│ ───────────────────────────────────────────────────────── │
|
||||
│ 2026-05-25 福安德科技 ¥8,000 95% ¥7,600 已接受│
|
||||
│ 2026-05-22 福安德科技 ¥8,200 90% ¥7,380 已拒绝│
|
||||
│ 2026-04-15 某某自动化 ¥8,500 100% ¥8,500 已接受│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**数据来源**: `biz_client_quote_item` → `biz_client_quote`
|
||||
|
||||
#### 3.3.5 同品类物料横向对比(基于供应商报价)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 同品类物料横向对比 3个物料 │
|
||||
│ [选择对比物料(仅显示有报价的物料)... ] │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ PLC控制器 │ │ 变频器 │ │
|
||||
│ │ CPU模块... / 西门子S7-1200 │ │ 三相380V... / ABB/西门子 │ │
|
||||
│ │ 3家报价 │ │ 2家报价 │ │
|
||||
│ ├─────────────────────────────┤ ├─────────────────────────────┤ │
|
||||
│ │ 规格型号 │ CPU模块... │ │ 规格型号 │ 三相380V... │ │
|
||||
│ │ 厂家/品牌│ 西门子S7-1200 │ │ 厂家/品牌│ ABB/西门子 │ │
|
||||
│ │ 材质 │ — │ │ 材质 │ — │ │
|
||||
│ │ 单位 │ 套 │ │ 单位 │ 台 │ │
|
||||
│ ├─────────────────────────────┤ ├─────────────────────────────┤ │
|
||||
│ │ ───── 供应商报价明细 ───── │ │ ───── 供应商报价明细 ───── │ │
|
||||
│ ├─────────────────────────────┤ ├─────────────────────────────┤ │
|
||||
│ │ 供应商 单价 交期 │ │ 供应商 单价 交期 │ │
|
||||
│ │ 广州联盟... ¥3,020 30天 │ │ 华顺达... ¥6,200 7天 │ │
|
||||
│ │ 深圳华顺... ¥3,180 25天 │ │ 博远... ¥6,500 10天 │ │
|
||||
│ │ 上海瑞达... ¥3,450 20天 │ │ │ │
|
||||
│ ├─────────────────────────────┤ ├─────────────────────────────┤ │
|
||||
│ │ 最低¥3020 │ 最高¥3450 │ │ 最低¥6200 │ 最高¥6500 │ │
|
||||
│ │ 均价¥3216 │ 价差¥430(+14.2%)│ │ 均价¥6350 │ 价差¥300(+4.8%) │ │
|
||||
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**功能说明**:
|
||||
- **自动加载**: 进入页面自动加载同品类有报价的物料,默认选中前3个
|
||||
- **实时更新**: 选择框变更时即时加载对应报价明细
|
||||
- **视觉区分**: 基础信息区域(浅蓝背景)与供应商报价区域(白色背景)通过分隔线明确区分
|
||||
- **报价排序**: 按单价从低到高排序,最低价绿色高亮并标记"最低"标签
|
||||
- **统计汇总**: 每个物料卡片底部展示最低价、最高价、均价、价差百分比
|
||||
- **错误处理**: 加载失败显示错误提示和重试按钮
|
||||
- **响应式**: 适配桌面端、平板和手机屏幕
|
||||
|
||||
**交互流程**:
|
||||
1. 用户进入物料详情页 → 系统自动加载同品类物料列表
|
||||
2. 默认选中前3个物料 → 自动加载这些物料的供应商报价明细
|
||||
3. 用户可在选择框中增删物料 → 报价明细实时更新
|
||||
4. 加载过程中显示loading状态 → 失败显示错误提示和重试按钮
|
||||
|
||||
**数据来源**:
|
||||
- 物料信息: `biz_material`
|
||||
- 供应商报价: `biz_quotation_item` + `biz_quotation` + `biz_supplier`
|
||||
- 关联关系: `biz_rfq_item` (物料与报价明细的关联)
|
||||
|
||||
---
|
||||
|
||||
## 四、后端API设计
|
||||
|
||||
### 4.1 物料管理API
|
||||
|
||||
| 接口 | 方法 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| `/bid/material/list` | GET | 物料列表 | bid:material:list |
|
||||
| `/bid/material/{id}` | GET | 物料详情 | bid:material:query |
|
||||
| `/bid/material` | POST | 新增物料 | bid:material:add |
|
||||
| `/bid/material` | PUT | 修改物料 | bid:material:edit |
|
||||
| `/bid/material/{id}` | DELETE | 删除物料 | bid:material:remove |
|
||||
|
||||
### 4.2 物料详情API
|
||||
|
||||
| 接口 | 方法 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| `/bid/material/{id}/detail` | GET | 物料完整详情(含性能参数) | bid:material:detail |
|
||||
| `/bid/material/{id}/supplier-quotes` | GET | 供应商历史报价清单 | bid:material:supplierQuotes |
|
||||
| `/bid/material/{id}/client-quotes` | GET | 甲方历史报价清单 | bid:material:clientQuotes |
|
||||
| `/bid/material/{id}/comparison` | GET | 横向对比信息 | bid:material:comparison |
|
||||
|
||||
### 4.3 同名称物料横向对比API(基于物料名称精确匹配)
|
||||
|
||||
| 接口 | 方法 | 说明 | 权限 | 备注 |
|
||||
|------|------|------|------|------|
|
||||
| `/bid/material/same-name/{materialName}` | GET | 按名称精确匹配查询物料 | bid:material:list | 返回名称相同但规格/品牌不同的物料(含无报价) |
|
||||
| `/bid/material/quote-comparison` | POST | 获取物料报价对比明细 | bid:material:list | 根据物料ID列表返回各供应商报价 |
|
||||
|
||||
**GET /bid/material/same-name/{materialName}?excludeId=1** 响应示例:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"materialId": 2,
|
||||
"materialName": "PLC控制器",
|
||||
"spec": "CPU模块,数字量I/O 40点,以太网接口",
|
||||
"brand": "西门子S7-1200",
|
||||
"material": "",
|
||||
"purpose": "",
|
||||
"unit": "套",
|
||||
"categoryName": "控制器"
|
||||
},
|
||||
{
|
||||
"materialId": 3,
|
||||
"materialName": "PLC控制器",
|
||||
"spec": "CPU模块,数字量I/O 64点,以太网接口",
|
||||
"brand": "三菱FX5U",
|
||||
"material": "",
|
||||
"purpose": "",
|
||||
"unit": "套",
|
||||
"categoryName": "控制器"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**POST /bid/material/quote-comparison** 请求/响应示例:
|
||||
```json
|
||||
// 请求
|
||||
[1, 2, 3]
|
||||
|
||||
// 响应
|
||||
[
|
||||
{
|
||||
"material_id": 1,
|
||||
"unit_price": 6200.00,
|
||||
"total_price": 31000.00,
|
||||
"delivery_days": 7,
|
||||
"quotation_id": 101,
|
||||
"quote_no": "Q20250520001",
|
||||
"submit_time": "2026-05-20T10:30:00",
|
||||
"quote_status": "2",
|
||||
"supplier_id": 5,
|
||||
"supplier_name": "华顺达电子科技有限公司",
|
||||
"supplier_contact": "张经理",
|
||||
"supplier_phone": "13800138000"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 4.4 横向对比管理API(可维护对标关系)
|
||||
|
||||
| 接口 | 方法 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| `/bid/material/comparison` | GET | 查询对比组列表 | bid:comparison:list |
|
||||
| `/bid/material/comparison` | POST | 添加对标物料 | bid:comparison:add |
|
||||
| `/bid/material/comparison/{id}` | PUT | 修改对标信息 | bid:comparison:edit |
|
||||
| `/bid/material/comparison/{id}` | DELETE | 删除对标物料 | bid:comparison:remove |
|
||||
| `/bid/material/comparison/recommend` | GET | 获取智能推荐 | bid:comparison:recommend |
|
||||
|
||||
---
|
||||
|
||||
## 五、前端组件设计
|
||||
|
||||
### 5.1 目录结构
|
||||
|
||||
```
|
||||
src/views/bid/material/
|
||||
├── index.vue # 物料列表页
|
||||
├── detail.vue # 物料详情页(主框架)
|
||||
└── components/
|
||||
├── DetailHeader.vue # 顶部信息卡片
|
||||
├── BasicInfoTab.vue # Tab1: 基础信息
|
||||
├── SupplierQuoteTab.vue # Tab2: 供应商报价
|
||||
├── ClientQuoteTab.vue # Tab3: 甲方报价
|
||||
└── ComparisonTab.vue # Tab4: 横向对比
|
||||
```
|
||||
|
||||
### 5.2 关键组件说明
|
||||
|
||||
#### 5.2.1 性能参数动态表单
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="performance-params-form">
|
||||
<el-button type="primary" size="small" @click="addRow">+ 添加参数</el-button>
|
||||
<el-table :data="params" border size="small" style="margin-top: 10px">
|
||||
<el-table-column label="参数名" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.name" placeholder="如: 功率" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数值" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.value" placeholder="如: 37" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.unit" placeholder="如: kW" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" style="color: #f56c6c" @click="removeRow(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 5.2.2 横向对比组件 (ComparisonTab.vue)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="comparison-tab">
|
||||
<!-- 操作栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">添加对标</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 对比表格 -->
|
||||
<el-table :data="comparisonData" border v-loading="loading">
|
||||
<el-table-column type="selection" width="55" align="center" fixed="left" />
|
||||
<el-table-column label="对比项" prop="attrName" width="120" fixed="left" />
|
||||
|
||||
<!-- 当前物料列 -->
|
||||
<el-table-column :label="currentMaterial.brand + ' ' + currentMaterial.spec" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="getHighlightClass(scope.row, 'current')">{{ scope.row.currentValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 对标物料列(动态生成) -->
|
||||
<el-table-column
|
||||
v-for="item in counterpartList"
|
||||
:key="item.counterpartId"
|
||||
:label="item.brand + ' ' + item.spec"
|
||||
align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="getHighlightClass(scope.row, item.counterpartId)">{{ scope.row[item.counterpartId] }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 智能推荐 -->
|
||||
<el-card class="recommend-card" v-if="recommendation">
|
||||
<div slot="header">
|
||||
<span>💡 智能推荐</span>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>推荐选型:</strong> {{ recommendation.brand }} {{ recommendation.spec }}</p>
|
||||
<p><strong>推荐理由:</strong> {{ recommendation.reason }}</p>
|
||||
<p><strong>价格优势:</strong> {{ recommendation.priceAdvantage }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加对标对话框 -->
|
||||
<el-dialog title="添加对标物料" :visible.sync="open" width="600px">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="对标物料" prop="counterpartId">
|
||||
<el-select v-model="form.counterpartId" filterable placeholder="请选择对标物料" style="width: 100%">
|
||||
<el-option
|
||||
v-for="m in materialOptions"
|
||||
:key="m.materialId"
|
||||
:label="m.brand + ' ' + m.spec + ' - ' + m.materialName"
|
||||
:value="m.materialId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="对比类型" prop="comparisonType">
|
||||
<el-radio-group v-model="form.comparisonType">
|
||||
<el-radio label="same_spec">同规格</el-radio>
|
||||
<el-radio label="similar">相似规格</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌等级" prop="brandLevel">
|
||||
<el-select v-model="form.brandLevel" placeholder="请选择品牌等级" style="width: 100%">
|
||||
<el-option label="国产一线" value="国产一线" />
|
||||
<el-option label="国产二线" value="国产二线" />
|
||||
<el-option label="进口一线" value="进口一线" />
|
||||
<el-option label="进口二线" value="进口二线" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="open = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['materialId'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
comparisonData: [],
|
||||
counterpartList: [],
|
||||
currentMaterial: {},
|
||||
recommendation: null,
|
||||
open: false,
|
||||
multiple: true,
|
||||
materialOptions: [],
|
||||
form: {},
|
||||
rules: {
|
||||
counterpartId: [{ required: true, message: '请选择对标物料', trigger: 'change' }],
|
||||
comparisonType: [{ required: true, message: '请选择对比类型', trigger: 'change' }],
|
||||
brandLevel: [{ required: true, message: '请选择品牌等级', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getComparisonData()
|
||||
},
|
||||
methods: {
|
||||
getComparisonData() {
|
||||
// 获取横向对比数据
|
||||
this.loading = true
|
||||
// API调用: GET /bid/material/{id}/comparison
|
||||
// 数据处理...
|
||||
},
|
||||
getHighlightClass(row, type) {
|
||||
// 根据数值比较返回高亮样式
|
||||
// 优于当前物料: green, 劣于当前物料: red
|
||||
},
|
||||
handleAdd() {
|
||||
this.open = true
|
||||
this.getMaterialOptions()
|
||||
},
|
||||
getMaterialOptions() {
|
||||
// 获取可选物料列表(排除当前物料和已添加的对标物料)
|
||||
},
|
||||
submitForm() {
|
||||
// 提交添加对标
|
||||
// API调用: POST /bid/material/comparison
|
||||
},
|
||||
handleDelete() {
|
||||
// 删除对标
|
||||
// API调用: DELETE /bid/material/comparison/{id}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recommend-card {
|
||||
margin-top: 20px;
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
.highlight-green {
|
||||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight-red {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、实现优先级
|
||||
|
||||
| 优先级 | 任务 | 涉及文件 | 预计工时 |
|
||||
|--------|------|---------|---------|
|
||||
| P0 | 数据库字段扩展 | `sql/20250527/01_material_alter.sql` | 0.5h |
|
||||
| P0 | 物料列表页字段更新 | `material/index.vue` | 2h |
|
||||
| P0 | 新增/编辑表单更新 | `material/index.vue` 对话框 | 3h |
|
||||
| P1 | 性能参数动态表单组件 | `components/PerformanceParamsForm.vue` | 3h |
|
||||
| P1 | 物料详情页框架 | `material/detail.vue` | 4h |
|
||||
| P1 | Tab 1: 基础信息 | `components/BasicInfoTab.vue` | 2h |
|
||||
| P1 | Tab 2: 供应商历史报价 | `components/SupplierQuoteTab.vue` | 3h |
|
||||
| P2 | Tab 3: 甲方历史报价 | `components/ClientQuoteTab.vue` | 3h |
|
||||
| P2 | Tab 4: 横向对比 | `components/ComparisonTab.vue` | 4h |
|
||||
| P2 | 横向对比管理功能 | 添加/编辑/删除对标物料 | 3h |
|
||||
| P3 | 智能推荐算法 | 综合评分计算 | 2h |
|
||||
| P3 | 后端API实现 | `BizMaterialController.java` | 4h |
|
||||
|
||||
**总计**: 约 33.5 工时
|
||||
|
||||
---
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
1. **数据迁移**: 执行SQL脚本前请备份数据库
|
||||
2. **字段变更**:
|
||||
- `spec` 字段保持为"规格型号",**不变更**
|
||||
- `brand` 字段改为"厂家/品牌",**变更注释**
|
||||
- `remark` 字段保持为"备注",**不变更**
|
||||
3. **JSON存储**: 性能参数使用JSON格式,便于灵活扩展
|
||||
4. **权限控制**: 详情页需要单独配置菜单权限
|
||||
5. **图片上传**: 物料图片使用单独的上传接口,存储URL
|
||||
|
||||
---
|
||||
|
||||
## 八、相关文件
|
||||
|
||||
- SQL脚本: `sql/20250527/01_material_alter.sql`
|
||||
- 开发文档: `doc/智采系统-物料信息模块开发文档.md`
|
||||
- 完整方案: `doc/福安德智慧报价平台-完整开发方案.md`
|
||||
957
doc/福安德智慧报价平台-完整开发方案.md
Normal file
957
doc/福安德智慧报价平台-完整开发方案.md
Normal file
@@ -0,0 +1,957 @@
|
||||
# 福安德智慧报价平台 — 完整开发方案
|
||||
|
||||
> 基于 RuoYi-Vue v3.9.2 (Spring Boot 4.0.3 + JDK 17 + Vue 2 + Element UI)
|
||||
> 融合 PPT《报价比对系统》5大板块 + XLSX《电气自动化设备综合管理系统_V1》27个Sheet数据
|
||||
> 对接现有项目12个已完成模块
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [一、项目全景与数据资产](#一项目全景与数据资产)
|
||||
- [二、板块一:物料信息](#二板块一物料信息)
|
||||
- [三、板块二:物料详情](#三板块二物料详情)
|
||||
- [四、板块三:供应商管理](#四板块三供应商管理)
|
||||
- [五、板块四:报价商门户(供应商端)](#五板块四报价商门户供应商端)
|
||||
- [六、板块五:甲方报价单](#六板块五甲方报价单)
|
||||
- [七、报价单管理与指派系统](#七报价单管理与指派系统)
|
||||
- [八、电气设备数据初始化方案](#八电气设备数据初始化方案)
|
||||
- [九、开发优先级与里程碑](#九开发优先级与里程碑)
|
||||
- [十、关键接口清单](#十关键接口清单)
|
||||
- [十一、权限与安全设计](#十一权限与安全设计)
|
||||
|
||||
---
|
||||
|
||||
## 一、项目全景与数据资产
|
||||
|
||||
### 1.1 PPT五大板块映射
|
||||
|
||||
| PPT板块 | 对应功能 | 现有进度 | 开发类型 |
|
||||
|---------|---------|---------|---------|
|
||||
| 智采系统-物料信息 | 物料CRUD + 分类树 + 性能参数JSON | `biz_material` 已有基础字段 | 扩展增强 |
|
||||
| 智采系统-物料详情 | 详情页 + 3个Tab(历史报价/甲方报价/同比) | 未开发 | 新建 |
|
||||
| 智采系统-供应商管理 | 供货清单 + 报价单管理 + 指派界面 | `biz_supplier` 已有基础 | 扩展增强 |
|
||||
| 智采系统-报价商 | 供应商门户(已报项/可报项/报价界面) | 未开发 | 新建 |
|
||||
| 给甲方报价单 | 子RFQ拆解 + 成本回填 + 历史版本 + 横向对比 | `biz_client_quote` 基础已有 | 核心增强 |
|
||||
|
||||
### 1.2 XLSX数据资产清单
|
||||
|
||||
Excel中包含了完整的电气设备业务数据,共27个Sheet:
|
||||
|
||||
| Sheet | 名称 | 行数 | 用途 |
|
||||
|-------|------|------|------|
|
||||
| 1 | 00_导航首页 | 24 | 系统导航和功能入口说明 |
|
||||
| 2 | 01_项目报价单(fad给甲方) | 52 | 甲方报价模板(含品牌/型号/数量/折扣率) |
|
||||
| 3 | 02_报价汇总 | 27 | 按类汇总 + 加价系数 + 对外报价 |
|
||||
| 4 | 10_变频器比价 | 48 | 汇川/禾望/西门子/ABB 三家比价 |
|
||||
| 5 | 11_整流单元比价 | 37 | 整流/制动/回馈单元比价 |
|
||||
| 6 | 12_PLC比价 | 15 | CPU+模块比价 |
|
||||
| 7 | 13_低压电器比价 | 22 | 断路器/接触器/热继电器比价 |
|
||||
| 8 | 14_电缆线槽比价 | 25 | 电缆/控制线/线槽综合比价 |
|
||||
| 9-19 | 20_变频器型号库 ~ 30_网关型号库 | 321+ | 全品类型号数据库 |
|
||||
| 20 | 31_品牌汇总 | 22 | 各品牌型号数量统计 |
|
||||
|
||||
### 1.3 核心业务流程
|
||||
|
||||
```
|
||||
甲方需求 → 创建甲方报价单 → 拆解为内部RFQ(子报价单)
|
||||
→ 指派供应商(可报项) → 供应商报价 → 比价选择
|
||||
→ 成本回填甲方报价单 → 生成最终甲方报价 → 甲方确认
|
||||
→ 生成采购单 → 交付 → 评价/异议处理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、板块一:物料信息
|
||||
|
||||
### 2.1 现有基础
|
||||
|
||||
`biz_material` 表已有字段:
|
||||
- `material_id`, `tenant_id`, `category_id`, `material_code`, `material_name`
|
||||
- `spec`, `unit`, `brand`, `description`
|
||||
- `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`
|
||||
|
||||
### 2.2 扩展字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE biz_material ADD COLUMN (
|
||||
-- PPT要求字段
|
||||
manufacturer VARCHAR(100) COMMENT '厂家/品牌(与brand一致,作为冗余筛选)',
|
||||
model_size VARCHAR(200) COMMENT '型号/尺寸(如MD500T37G, TMY-60×6)',
|
||||
performance_params TEXT COMMENT '性能参数(JSON格式: {"功率":"37kW","电压":"380V"})',
|
||||
material VARCHAR(100) COMMENT '材质(铜/铝合金/PVC等)',
|
||||
purpose VARCHAR(500) COMMENT '用途(如"通用负载","风机水泵")',
|
||||
remarks TEXT COMMENT '备注',
|
||||
category_tree_path VARCHAR(500) COMMENT '分类路径(冗余,如"1/5/23")',
|
||||
spec_comparison_group VARCHAR(100) COMMENT '横向对比分组标识',
|
||||
|
||||
-- 扩展业务字段
|
||||
min_stock INT DEFAULT 0 COMMENT '最低库存预警',
|
||||
unit_weight DECIMAL(10,4) COMMENT '单重(kg)',
|
||||
image_url VARCHAR(500) COMMENT '物料图片',
|
||||
data_source VARCHAR(20) DEFAULT 'manual' COMMENT '数据来源(manual/import/excel)',
|
||||
version INT DEFAULT 1 COMMENT '乐观锁版本号'
|
||||
);
|
||||
|
||||
-- 型号唯一索引(防止重复录入同规格物料)
|
||||
ALTER TABLE biz_material ADD UNIQUE INDEX idx_model_manufacturer (manufacturer, model_size);
|
||||
```
|
||||
|
||||
### 2.3 性能参数JSON规范
|
||||
|
||||
性能参数使用JSON格式存储,前端动态渲染为键值对表格:
|
||||
|
||||
```json
|
||||
// 变频器示例
|
||||
{
|
||||
"功率": "37kW",
|
||||
"电压": "380V",
|
||||
"电流": "72A",
|
||||
"过载能力": "150%/60s",
|
||||
"通讯接口": "RS485/PROFINET",
|
||||
"防护等级": "IP20"
|
||||
}
|
||||
|
||||
// 电缆示例
|
||||
{
|
||||
"芯数×截面": "3×70+1×35",
|
||||
"额定电压": "0.6/1kV",
|
||||
"外径": "42mm",
|
||||
"载流量": "210A",
|
||||
"护套材质": "PVC铠装"
|
||||
}
|
||||
```
|
||||
|
||||
**前端组件**:使用Element UI动态表单(`el-form` + `el-table`),每行=参数名+参数值+单位,支持增删行。保存时组装为JSON字符串,读取时`JSON.parse`后渲染为只读表格。
|
||||
|
||||
### 2.4 前端页面结构
|
||||
|
||||
```
|
||||
物料信息列表页 (/bid/material)
|
||||
├── 左侧:物料分类树 (复用 biz_material_category)
|
||||
│ ├── 树节点点击 → 右侧表格过滤
|
||||
│ └── 面包屑导航(基于category_tree_path)
|
||||
├── 右侧:物料表格
|
||||
│ ├── 顶部筛选栏
|
||||
│ │ ├── 厂家下拉(去重,从manufacturer字段DISTINCT)
|
||||
│ │ ├── 型号模糊搜索(输入前缀智能联想)
|
||||
│ │ ├── 分类级联选择器
|
||||
│ │ └── 关键词搜索
|
||||
│ ├── 表格列:编码 | 名称 | 厂家 | 型号 | 材质 | 用途 | 单位 | 操作
|
||||
│ └── 操作按钮:详情 | 编辑 | 删除 | 加入对比篮
|
||||
└── 新增/编辑对话框(抽屉形式)
|
||||
├── 基础信息(名称/分类/厂家/型号/材质/用途/单位)
|
||||
├── 性能参数动态表单(支持增删行)
|
||||
├── 规格明细(可扩展多规格)
|
||||
└── 图片上传(物料图/铭牌)
|
||||
```
|
||||
|
||||
### 2.5 后端接口扩展
|
||||
|
||||
在 `BizMaterialController` 中新增:
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/bid/material/detail/{id}` | GET | 物料详情(含性能参数解析+历史报价摘要) |
|
||||
| `/bid/material/manufacturer/list` | GET | 厂家下拉列表(去重) |
|
||||
| `/bid/material/import` | POST | Excel批量导入(EasyExcel解析) |
|
||||
| `/bid/material/export` | GET | 导出为Excel模板 |
|
||||
| `/bid/material/suggest` | GET | 型号智能联想(q参数前缀匹配) |
|
||||
| `/bid/material/comparison-group/{groupId}` | GET | 按对比组获取物料 |
|
||||
|
||||
---
|
||||
|
||||
## 三、板块二:物料详情
|
||||
|
||||
### 3.1 页面架构
|
||||
|
||||
采用"1+N"信息架构——1个基础信息区 + N个动态关联Tab:
|
||||
|
||||
```
|
||||
物料详情页 (/bid/material/detail/:id)
|
||||
├── 顶部基础信息卡片(两栏)
|
||||
│ ├── 左栏(16/24):基础信息表(物料ID/厂家/型号/材质/用途)
|
||||
│ │ └── 性能参数表格(从JSON动态渲染)
|
||||
│ └── 右栏(8/24):价格趋势ECharts迷你折线图
|
||||
│ └── 近6个月最低报价走势(从biz_quotation_item聚合)
|
||||
├── Tab 1:供应商历史报价清单
|
||||
├── Tab 2:我方给甲方的历史报价
|
||||
└── Tab 3:不同品牌/规格同比
|
||||
```
|
||||
|
||||
### 3.2 Tab 1:供应商历史报价清单
|
||||
|
||||
**数据来源**:`biz_quotation_item` → `biz_quotation` → `biz_supplier`
|
||||
|
||||
**展示字段**:
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| 报价日期 | biz_quotation.submit_time |
|
||||
| 供应商名称 | biz_supplier.supplier_name |
|
||||
| 单价(元) | biz_quotation_item.unit_price |
|
||||
| 数量 | biz_quotation_item.quantity |
|
||||
| 交期(周) | biz_quotation_item.delivery_days / 7 |
|
||||
| 报价单号 | biz_quotation.quote_no |
|
||||
| 操作 | 查看详情(点击下钻到报价单) |
|
||||
|
||||
**关键交互**:
|
||||
- 点击单价 → 抽屉展示原始报价单详情
|
||||
- 价格预警:高于近3个月平均价15% → 行背景红色高亮
|
||||
- 按时间倒序排列
|
||||
|
||||
**后端API**:
|
||||
```java
|
||||
// BizMaterialController
|
||||
@GetMapping("/{id}/supplier-quotes")
|
||||
public AjaxResult getSupplierQuoteHistory(@PathVariable Long id) {
|
||||
// 查询 biz_quotation_item JOIN biz_quotation JOIN biz_supplier
|
||||
// WHERE material_id = id ORDER BY submit_time DESC
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Tab 2:我方给甲方的历史报价
|
||||
|
||||
**数据来源**:`biz_client_quote_item` → `biz_client_quote`
|
||||
|
||||
**展示字段**:
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| 报价日期 | client_quote.create_time |
|
||||
| 甲方名称 | client_quote.client_name |
|
||||
| 我方含税单价 | client_quote_item.unit_price |
|
||||
| 折扣率 | client_quote_item.discount_rate |
|
||||
| 最终成交价 | 含税单价 × 折扣率 |
|
||||
| 状态 | sent/accepted/rejected(带标签) |
|
||||
| 关联RFQ | client_quote.rfq_id → biz_rfq.rfq_no |
|
||||
|
||||
**功能扩展**:
|
||||
- 落选标注原因(新增client_quote_item.reject_reason字段)
|
||||
- 版本对比(基于parent_quote_id)
|
||||
|
||||
### 3.4 Tab 3:不同品牌/规格同比
|
||||
|
||||
#### 新建对比关系表
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz_material_comparison (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
group_id VARCHAR(50) NOT NULL COMMENT '对比组ID(如"VFD-37kW-380V")',
|
||||
group_name VARCHAR(200) COMMENT '对比组名称(如"37kW 380V变频器对比组")',
|
||||
material_id BIGINT NOT NULL COMMENT '物料ID',
|
||||
counterpart_brand VARCHAR(100) COMMENT '对标品牌',
|
||||
counterpart_model VARCHAR(200) COMMENT '对标型号',
|
||||
comparison_notes TEXT COMMENT '对比说明(差异点)',
|
||||
sort_order INT DEFAULT 0 COMMENT '组内排序',
|
||||
status CHAR(1) DEFAULT '0' COMMENT '状态',
|
||||
create_by VARCHAR(64),
|
||||
create_time DATETIME,
|
||||
update_by VARCHAR(64),
|
||||
update_time DATETIME,
|
||||
UNIQUE KEY uk_group_material (group_id, material_id),
|
||||
KEY idx_material (material_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物料横向对比组';
|
||||
```
|
||||
|
||||
**功能逻辑**:
|
||||
|
||||
1. **管理员维护对比组**(在物料详情页可直接操作)
|
||||
- 为某个物料添加"对标物料"(跨品牌)
|
||||
- 如汇川MD500T37G ↔ 禾望HV500-37G ↔ 西门子G120 PM240 37kW
|
||||
|
||||
2. **自动对比展示**
|
||||
- 系统自动从 biz_comparison 对比结果中拉取对标物料的最新供应商报价
|
||||
- 对比维度:单价 | 功率 | 过载能力 | 通讯接口 | 防护等级 | 适用场合
|
||||
|
||||
3. **数据初始化**
|
||||
- 利用Excel Sheet 10-14的比价数据预置对比组
|
||||
- 如Sheet 10中"37kW变频器"对应的汇川/禾望/西门子/ABB四家数据
|
||||
|
||||
#### 前端展示
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 37kW 380V变频器 对比组 │
|
||||
├──────────────┬──────────┬──────────┬──────────┬────────────┤
|
||||
│ │ 汇川 │ 禾望 │ 西门子 │ ABB │
|
||||
│ │ MD500T37G │ HV500-37G│ G120 37kW│ ACS880-01 │
|
||||
├──────────────┼──────────┼──────────┼──────────┼────────────┤
|
||||
│ 最低报价(元) │ 6,200 │ 5,800 │ 10,500 │ 8,900 │
|
||||
│ 功率(kW) │ 37 │ 37 │ 37 │ 37 │
|
||||
│ 过载能力 │ 150%/60s │ 150%/60s │ 140%/60s │ 150%/60s │
|
||||
│ 通讯接口 │ RS485 │ RS485 │ PROFINET │ PROFINET │
|
||||
│ 防护等级 │ IP20 │ IP20 │ IP20 │ IP21 │
|
||||
│ 综合评价 │ ★★★★☆ │ ★★★★★ │ ★★★☆☆ │ ★★★★☆ │
|
||||
├──────────────┼──────────┴──────────┴──────────┴────────────┤
|
||||
│ 推荐选型 │ 禾望HV500-37G (性价比最优) │
|
||||
└──────────────┴─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、板块三:供应商管理
|
||||
|
||||
### 4.1 现有基础
|
||||
|
||||
`biz_supplier` 表已有字段:supplier_id, supplier_name, contact, phone, email, address, status
|
||||
|
||||
### 4.2 供应商详情页重构
|
||||
|
||||
在现有 `/bid/supplier` 基础上,将列表页改为"列表+详情"结构,点击供应商进入详情页:
|
||||
|
||||
```
|
||||
供应商详情页 (/bid/supplier/detail/:id)
|
||||
├── Tab 1: 基础信息 (现有功能)
|
||||
│ ├── 供应商名称/联系人/电话/邮箱/地址
|
||||
│ └── 资质文件上传(营业执照/授权证书)
|
||||
├── Tab 2: 供货清单 (新建)
|
||||
│ ├── 供应商-物料关系维护
|
||||
│ └── 历史报价快速查看
|
||||
├── Tab 3: 报价单管理 (增强)
|
||||
│ ├── 参与的RFQ列表
|
||||
│ └── 报价单状态跟踪
|
||||
├── Tab 4: 历史报价趋势 (新建)
|
||||
│ └── ECharts折线图
|
||||
└── Tab 5: 账号管理 (新建)
|
||||
└── 供应商登录账号创建/权限管理
|
||||
```
|
||||
|
||||
### 4.3 供货清单
|
||||
|
||||
#### 新建供应商-物料关系表
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz_supplier_material (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
supplier_id BIGINT NOT NULL,
|
||||
material_id BIGINT NOT NULL,
|
||||
supply_status TINYINT DEFAULT 1 COMMENT '0停供 1正常 2备选',
|
||||
last_quote_price DECIMAL(18,4) COMMENT '最新报价(缓存)',
|
||||
last_quote_time DATETIME COMMENT '最新报价时间',
|
||||
delivery_cycle INT DEFAULT 0 COMMENT '供应周期(天)',
|
||||
min_order_qty DECIMAL(15,4) DEFAULT 1 COMMENT '最小起订量',
|
||||
preferred TINYINT DEFAULT 0 COMMENT '是否优选供应商',
|
||||
create_time DATETIME,
|
||||
UNIQUE KEY uk_supplier_material (supplier_id, material_id),
|
||||
KEY idx_supplier (supplier_id),
|
||||
KEY idx_material (material_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商物料供应关系';
|
||||
```
|
||||
|
||||
**前端交互**:
|
||||
- 点击物料名称 → 跳转物料详情页(`/bid/material/detail/:id`)
|
||||
- 支持在物料详情页"添加供应商"反向维护
|
||||
- 批量导入:根据Excel比价数据自动建立供应关系
|
||||
|
||||
### 4.4 报价单管理
|
||||
|
||||
供应商参与的RFQ及报价单管理,增强现有 `biz_rfq`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE biz_rfq ADD COLUMN (
|
||||
contract_no VARCHAR(100) COMMENT '合同号',
|
||||
client_name VARCHAR(100) COMMENT '甲方名称',
|
||||
project_name VARCHAR(200) COMMENT '项目名称',
|
||||
delivery_required_date DATE COMMENT '要求交货日期',
|
||||
delivery_weeks INT COMMENT '要求交货周期(周)',
|
||||
quote_deadline DATETIME COMMENT '供应商报价截止日',
|
||||
total_budget DECIMAL(18,4) COMMENT '预算总额',
|
||||
winner_supplier_id BIGINT COMMENT '中标供应商ID',
|
||||
winner_quotation_id BIGINT COMMENT '中标报价单ID'
|
||||
);
|
||||
```
|
||||
|
||||
**报价单列表页字段**:询价单号 | 合同号 | 甲方 | 项目名称 | 状态 | 报价截止日 | 已报价/邀请数 | 操作
|
||||
|
||||
**状态流转**:`draft(草稿) → published(已发布) → in_progress(报价中) → closed(已关闭) → completed(已完成)`
|
||||
|
||||
### 4.5 指派供应商界面
|
||||
|
||||
**触发点**:RFQ发布时选择被邀请的供应商
|
||||
|
||||
```
|
||||
┌─ 指派供应商对话框 ─────────────────────────────────┐
|
||||
│ │
|
||||
│ 询价单号: RFQ-2026-001 │
|
||||
│ 标题: 自动化生产线控制系统采购 │
|
||||
│ │
|
||||
│ ☐ 全选 搜索供应商: [______] │
|
||||
│ │
|
||||
│ ☑ 深圳市华顺达电子有限公司 上次合作: 2026-01 │
|
||||
│ ☑ 苏州博远精密设备有限公司 上次合作: 2026-01 │
|
||||
│ ☐ 上海瑞达工控科技有限公司 新供应商 │
|
||||
│ ☑ 广州联盟电气集团 报价历史优秀 │
|
||||
│ ☐ 武汉华中精工机械有限公司 评价4.8分 │
|
||||
│ │
|
||||
│ 发送方式: [📧 邮件通知] [💬 站内信] [📱 短信通知] │
|
||||
│ │
|
||||
│ [取消] [确认指派] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**指派后**:系统向供应商发送通知,供应商登录后可看到"可报项"
|
||||
|
||||
---
|
||||
|
||||
## 五、板块四:报价商门户(供应商端)
|
||||
|
||||
### 5.1 账户体系设计
|
||||
|
||||
```sql
|
||||
-- 在 sys_user 增加供应商类型字段(已有 tenant_id)
|
||||
ALTER TABLE sys_user ADD COLUMN (
|
||||
user_type TINYINT DEFAULT 1 COMMENT '1内部员工 2供应商账号',
|
||||
supplier_id BIGINT COMMENT '关联供应商ID(仅user_type=2时有效)',
|
||||
account_status TINYINT DEFAULT 1 COMMENT '0待审核 1正常 2冻结'
|
||||
);
|
||||
```
|
||||
|
||||
**权限控制**:
|
||||
- 供应商账号(`user_type=2`) → 仅能看到本供应商数据(通过`supplier_id`过滤)
|
||||
- RuoYi `@PreAuthorize`注解 + 自定义数据权限拦截器
|
||||
- 路由层面:供应商端访问 `/supplier/**` 路径,内部员工无法访问
|
||||
|
||||
### 5.2 供应商路由体系
|
||||
|
||||
新建独立路由模块,与后台管理路由隔离:
|
||||
|
||||
```javascript
|
||||
// ruoyi-ui/src/router/supplier.js
|
||||
export const supplierRoutes = [
|
||||
{
|
||||
path: '/supplier',
|
||||
component: Layout,
|
||||
hidden: true, // 不在管理后台侧边栏显示
|
||||
children: [
|
||||
{ path: 'dashboard', component: () => import('@/views/supplier/dashboard'), meta: { title: '供应商首页' } },
|
||||
{ path: 'rfq/pending', component: () => import('@/views/supplier/rfq-pending'), meta: { title: '可报项' } },
|
||||
{ path: 'rfq/quoted', component: () => import('@/views/supplier/rfq-quoted'), meta: { title: '已报项' } },
|
||||
{ path: 'quote/edit/:rfqId', component: () => import('@/views/supplier/quote-edit'), meta: { title: '报价填写' } },
|
||||
{ path: 'profile', component: () => import('@/views/supplier/profile'), meta: { title: '我的信息' } },
|
||||
]
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 5.3 供应商首页 (`/supplier/dashboard`)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ 欢迎您,深圳市华顺达电子有限公司 │
|
||||
├──────────┬──────────┬──────────┬──────────┬───────────────┤
|
||||
│ 可报项 │ 已报项 │ 中标数 │ 中标率 │ 累计成交额 │
|
||||
│ 3 │ 12 │ 5 │ 41.7% │ ¥1,286,500 │
|
||||
├──────────┴──────────┴──────────┴──────────┴───────────────┤
|
||||
│ ⏰ 紧急提醒:RFQ-2026-003 报价截止还剩 2天! │
|
||||
│ 📋 最新中标的报价单:RFQ-2026-001 (2026-01-15中标) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.4 可报项 (`/supplier/rfq/pending`)
|
||||
|
||||
**数据来源**:`biz_rfq_supplier` WHERE `supplier_id = ?` AND `status = 'published'` AND `quote_deadline > NOW()`
|
||||
|
||||
**列表字段**:
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| RFQ编号 | biz_rfq.rfq_no |
|
||||
| 甲方/项目 | biz_rfq.client_name / project_name |
|
||||
| 报价截止日 | biz_rfq.quote_deadline (红色<3天) |
|
||||
| 物料数量 | biz_rfq_item COUNT |
|
||||
| 操作 | 立即报价(按钮) |
|
||||
|
||||
**紧急提醒**:截止前24小时发送站内信(复用RuoYi消息中心)
|
||||
|
||||
### 5.5 已报项 (`/supplier/rfq/quoted`)
|
||||
|
||||
**数据来源**:`biz_quotation` WHERE `supplier_id = ?`
|
||||
|
||||
**列表字段**:
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| RFQ编号 | rfq_no |
|
||||
| 报价日期 | quotation.create_time |
|
||||
| 物料数量 | quotation_item COUNT |
|
||||
| 报价总额 | quotation.total_amount |
|
||||
| 状态 | pending(待开标)/won(已中标)/lost(未中标) |
|
||||
| 操作 | 查看详情 / 修改报价(截止前可改) |
|
||||
|
||||
### 5.6 报价填写界面 (`/supplier/quote/edit/:rfqId`)
|
||||
|
||||
采用**分步表单**:
|
||||
|
||||
```
|
||||
Step 1 → 物料清单确认 (只读展示RFQ物料)
|
||||
Step 2 → 逐项填价 (核心步骤)
|
||||
Step 3 → 预览提交
|
||||
```
|
||||
|
||||
**Step 2 填价界面**:
|
||||
|
||||
```
|
||||
物料清单表格:
|
||||
┌──────────┬────────┬──────┬──────┬──────────┬────────┬──────────┐
|
||||
│ 物料名称 │ 规格 │ 数量 │ 单位 │ 单价(元)* │ 交期(周)│ 备注 │
|
||||
├──────────┼────────┼──────┼──────┼──────────┼────────┼──────────┤
|
||||
│ PLC控制器 │ CPU... │ 5 │ 套 │ [___] │ [___] │ [______] │
|
||||
│ 变频器 │ 三相...│ 8 │ 台 │ [___] │ [___] │ [______] │
|
||||
│ 电源模块 │ 24V... │ 20 │ 台 │ [___] │ [___] │ [______] │
|
||||
└──────────┴────────┴──────┴──────┴──────────┴────────┴──────────┘
|
||||
含税/未税切换: [●含税] [○未税] 自动计算增值税13%
|
||||
```
|
||||
|
||||
**关键交互**:
|
||||
- 必填校验:单价 > 0,交期 ≤ RFQ要求
|
||||
- 智能提示:当输入单价低于该物料历史最低价的80%时 → 警告弹窗"报价异常偏低,请确认"
|
||||
- 附件上传:产品彩页、资质证书、型式试验报告(新表`biz_quotation_attachment`)
|
||||
- 自动保存草稿(LocalStorage + 后端每3分钟自动保存)
|
||||
- 提交前预览比价结果(提示"您的报价在当前排名第X/共Y家")
|
||||
|
||||
### 5.7 供应商端API
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/supplier/dashboard/stats` | GET | 统计数据总览 |
|
||||
| `/supplier/rfq/pending` | GET | 可报项列表(分页) |
|
||||
| `/supplier/rfq/quoted` | GET | 已报项列表(分页) |
|
||||
| `/supplier/quote/draft/{rfqId}` | GET | 获取草稿报价 |
|
||||
| `/supplier/quote/save-draft` | POST | 保存报价草稿 |
|
||||
| `/supplier/quote/submit` | POST | 提交报价 |
|
||||
| `/supplier/quote/{id}/attachments` | POST | 上传附件 |
|
||||
| `/supplier/profile` | GET/PUT | 供应商信息维护 |
|
||||
|
||||
---
|
||||
|
||||
## 六、板块五:甲方报价单
|
||||
|
||||
### 6.1 现有基础
|
||||
|
||||
`biz_client_quote` 已有基础CRUD,`biz_client_quote_item` 已有明细表。现有页面已支持基本的新建/编辑/PDF导出。
|
||||
|
||||
### 6.2 数据模型增强
|
||||
|
||||
```sql
|
||||
-- 增强甲方报价单
|
||||
ALTER TABLE biz_client_quote ADD COLUMN (
|
||||
rfq_id BIGINT COMMENT '关联内部RFQ',
|
||||
cost_total DECIMAL(18,4) COMMENT '内部成本合计(供应商中标价汇总)',
|
||||
margin_rate DECIMAL(5,2) COMMENT '毛利率%',
|
||||
quote_version INT DEFAULT 1 COMMENT '报价版本号',
|
||||
parent_quote_id BIGINT COMMENT '父报价单ID(版本迭代)',
|
||||
project_name VARCHAR(200) COMMENT '项目名称',
|
||||
validity_date DATETIME COMMENT '报价有效期',
|
||||
payment_terms VARCHAR(500) COMMENT '付款条件',
|
||||
delivery_terms VARCHAR(500) COMMENT '交货条款',
|
||||
status VARCHAR(20) COMMENT 'draft/sent/accepted/rejected/revised',
|
||||
margin_rate_setting DECIMAL(5,2) DEFAULT 15.00 COMMENT '加价系数(%)',
|
||||
tax_rate DECIMAL(4,2) DEFAULT 13.00 COMMENT '税率(%)'
|
||||
);
|
||||
|
||||
-- 新建甲方报价-物料-供应商关联表(记录每个物料选定的供应商)
|
||||
CREATE TABLE biz_client_quote_item_source (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
quote_item_id BIGINT NOT NULL COMMENT 'biz_client_quote_item.id',
|
||||
supplier_id BIGINT COMMENT '选定的供应商ID',
|
||||
supplier_quote_id BIGINT COMMENT '供应商报价单ID(biz_quotation.quotation_id)',
|
||||
unit_cost DECIMAL(18,4) COMMENT '供应商中标单价(含税)',
|
||||
selected_reason VARCHAR(500) COMMENT '选择该供应商原因',
|
||||
internal_note TEXT COMMENT '内部备注',
|
||||
create_by VARCHAR(64),
|
||||
create_time DATETIME,
|
||||
update_by VARCHAR(64),
|
||||
update_time DATETIME,
|
||||
KEY idx_quote_item (quote_item_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='甲方报价物料来源';
|
||||
```
|
||||
|
||||
### 6.3 业务流程图
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ 甲方询价需求 │
|
||||
└──────┬───────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 创建甲方报价单 │ ← 手动输入甲方需求物料
|
||||
│ (biz_client_quote)│
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 生成内部RFQ │ ← 从甲方物料自动拆解为RFQ
|
||||
│ (点击"发起询价") │ 按品类分多个子RFQ
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 指派供应商报价 │ ← 每个子RFQ指派不同供应商
|
||||
│ (供应商端报价) │ 变频器→A/B/C, 电缆→D/E
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 比价选择供应商 │ ← 使用现有智慧比价引擎
|
||||
│ (选定最优方案) │
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 成本回填 │ ← 自动回填选中供应商价格到
|
||||
│ (自动/手动同步) │ biz_client_quote_item_source
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 计算对外报价 │ ← 成本 × (1 + 加价系数)
|
||||
│ 生成PDF → 发送甲方 │ 可手动调整覆盖
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
### 6.4 甲方报价单详情页重构
|
||||
|
||||
```
|
||||
甲方报价单详情页 (/bid/clientquote/detail/:id)
|
||||
├── 顶部操作栏
|
||||
│ ├── 保存草稿 | 导出PDF | 发送甲方 | 版本迭代
|
||||
│ └── 状态标签(draft/sent/accepted/rejected)
|
||||
│
|
||||
├── 基本信息卡片
|
||||
│ ├── 客户名称 | 项目名称 | 有效期 | 币种
|
||||
│ ├── 关联RFQ(下拉选择) | 合同号
|
||||
│ └── 付款条件 | 交货条款
|
||||
│
|
||||
├── 物料清单表格(可编辑)
|
||||
│ ├── 行数据:序号 | 类别 | 物料名称 | 品牌 | 型号规格
|
||||
│ │ | 数量 | 单位 | 供应商中标价(成本)
|
||||
│ │ | 加价系数 | 对外含税单价 | 小计
|
||||
│ ├── 操作:添加行 | 删除行 | 批量导入
|
||||
│ └── 特殊列:询价状态(未询价/报价中/已回价/已定价)
|
||||
│ └── 已定价 → 显示供应商名称,点击查看来源
|
||||
│ └── 未询价 → 点击"发起询价"→ 弹出选择供应商 → 生成子RFQ
|
||||
│
|
||||
├── 右侧成本汇总面板(固定)
|
||||
│ ├── 物料成本合计: ¥XXX (自动汇总供应商中标价)
|
||||
│ ├── 加价系数: 15% (可调整)
|
||||
│ ├── 对外报价(含税): ¥XXX = 成本 × (1 + 加价系数)
|
||||
│ ├── 预计毛利率: XX.X% (实时计算)
|
||||
│ └── 操作按钮:同步最新采购价 | 重新计算
|
||||
│
|
||||
├── Tab: 智能选型对比
|
||||
│ └── 选中物料 → 弹出对比窗口 → 展示同规格多品牌对比
|
||||
│ → 一键替换物料型号和成本
|
||||
│
|
||||
└── Tab: 历史版本
|
||||
└── 版本列表(通过parent_quote_id关联)
|
||||
→ 版本对比(高亮显示差异)
|
||||
```
|
||||
|
||||
### 6.5 子RFQ生成逻辑
|
||||
|
||||
```java
|
||||
// 从甲方报价单生成内部RFQ
|
||||
@PostMapping("/bid/clientquote/{id}/generateRfq")
|
||||
public AjaxResult generateSubRfq(@PathVariable Long id, @RequestBody GenerateRfqRequest req) {
|
||||
// 1. 获取甲方报价单主表
|
||||
// 2. 根据选中的行物料,按category_id分组
|
||||
// 如: 变频器类一组,PLC类一组,电缆类一组
|
||||
// 3. 为每组创建子RFQ:
|
||||
// - rfq_title = 甲方报价单项目名 + "(" + 类别名称 + ")"
|
||||
// - deadline = 甲方报价单有效期前7天
|
||||
// - parent_client_quote_id = 甲方报价单ID
|
||||
// 4. 将选中物料复制到biz_rfq_item
|
||||
// 5. 指派供应商(从req.supplier_ids)
|
||||
// 6. 返回生成的RFQ ID列表
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 成本回填机制
|
||||
|
||||
**自动回填触发点**:
|
||||
1. 采购单确认时(`biz_purchase_order.status → 'confirmed'`)
|
||||
2. 甲方报价单页面手动点击"同步最新采购价"
|
||||
|
||||
**回填逻辑**:
|
||||
```java
|
||||
// 自动回填
|
||||
@PostMapping("/bid/clientquote/{id}/syncCost")
|
||||
public AjaxResult syncCost(@PathVariable Long id) {
|
||||
// 1. 获取所有子RFQ的最新成交报价
|
||||
// 2. 将选中供应商的成交价写入biz_client_quote_item_source
|
||||
// 3. 更新biz_client_quote.cost_total
|
||||
// 4. 重新计算margin_rate
|
||||
// 5. 记录价格变动日志
|
||||
}
|
||||
```
|
||||
|
||||
**价格变动日志表**:
|
||||
```sql
|
||||
CREATE TABLE biz_price_change_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
source_type VARCHAR(20) COMMENT '来源类型(quotation/client_quote)',
|
||||
source_id BIGINT COMMENT '来源ID',
|
||||
material_id BIGINT COMMENT '物料ID',
|
||||
old_price DECIMAL(18,4),
|
||||
new_price DECIMAL(18,4),
|
||||
change_reason VARCHAR(500) COMMENT '变更原因',
|
||||
operator VARCHAR(64),
|
||||
create_time DATETIME
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='价格变动日志';
|
||||
```
|
||||
|
||||
### 6.7 两个报价历史
|
||||
|
||||
**历史1:我方给甲方的报价历史**
|
||||
- 数据源:`biz_client_quote` 通过 `parent_quote_id` 形成版本链
|
||||
- 功能:版本对比 → 选择两个版本高亮显示价格差异
|
||||
|
||||
**历史2:供应商给我方的报价历史**
|
||||
- 数据源:`biz_quotation_item` 中该物料的历史记录
|
||||
- 功能:在甲方报价单物料行点击"查看成本来源" → 抽屉展示时间轴
|
||||
|
||||
---
|
||||
|
||||
## 七、报价单管理与指派系统
|
||||
|
||||
### 7.1 报价单详情页 (`/bid/rfq/detail/:id`)
|
||||
|
||||
```
|
||||
RFQ详情页 (/bid/rfq/detail/:id)
|
||||
├── 顶部:基础信息(合同号/甲方/截止日/状态)
|
||||
├── 左侧:已邀请供应商列表
|
||||
│ ├── 供应商名称 | 报价状态(未报价/已报价/已逾期)
|
||||
│ ├── 已报价 → 显示报价金额及操作(查看详情)
|
||||
│ └── 未报价 → 显示"催办"按钮(发送通知)
|
||||
├── 右侧:物料明细及各家报价对比表格
|
||||
│ ├── 与XLSX Sheet 10-14格式一致
|
||||
│ ├── 行:物料,列:各供应商单价(最低价绿色高亮)
|
||||
│ └── 差额/差价自动计算
|
||||
├── 底部操作栏
|
||||
│ ├── 确认中标 → 弹出选择中标供应商对话框
|
||||
│ ├── 重新询价 → 复制当前RFQ生成新版本
|
||||
│ ├── 导出比价报告(PDF) → 复用现有导出功能
|
||||
│ └── 关闭询价
|
||||
```
|
||||
|
||||
### 7.2 供应商报价填写界面(供应商端)
|
||||
|
||||
见5.6节详细描述。
|
||||
|
||||
### 7.3 比价报告PDF
|
||||
|
||||
模板格式参考XLSX Sheet 10-14,包含:
|
||||
- 表头:RFQ编号、项目名称、日期
|
||||
- 物料对比表:每行展示各供应商报价、最低价标记
|
||||
- 推荐方案:综合评分推荐最优供应商
|
||||
- 统计摘要:总价对比、节省金额
|
||||
|
||||
---
|
||||
|
||||
## 八、电气设备数据初始化方案
|
||||
|
||||
### 8.1 数据范围
|
||||
|
||||
利用XLSX Sheet 20-31的型号库数据,预置如下初始数据:
|
||||
|
||||
| 品类 | 型号数量 | 数据来源Sheet | 对应品牌 |
|
||||
|------|---------|--------------|---------|
|
||||
| 变频器 | ~322 | 31_品牌汇总 | 汇川/禾望/西门子/ABB |
|
||||
| 整流制动单元 | ~100 | 21_整流制动单元库 | 汇川/禾望/西门子 |
|
||||
| PLC CPU | ~55 | 22_PLC_CPU型号库 | 西门子/三菱/欧姆龙 |
|
||||
| PLC扩展模块 | ~98 | 23_PLC_扩展模块库 | 西门子/三菱/欧姆龙 |
|
||||
| 低压电器 | ~75 | 24_低压电器型号库 | 正泰/施耐德 |
|
||||
| 触摸屏 | ~20 | 25_触摸屏型号库 | 威纶通/昆仑通态/西门子 |
|
||||
| 铜牌母线 | ~20 | 26_铜牌母线型号库 | TMY铜排 |
|
||||
| 动力电缆 | ~23 | 27_电缆型号库 | YJV22系列 |
|
||||
| 控制线 | ~21 | 28_控制线型号库 | KVVP系列 |
|
||||
| 线槽 | ~18 | 29_线槽型号库 | PVC/钢制 |
|
||||
| 工业网关 | ~12 | 30_网关型号库 | 汇川/研华 |
|
||||
| **合计** | **~764** | | |
|
||||
|
||||
### 8.2 供应商初始化
|
||||
|
||||
基于XLSX比价数据中的供应商,初始化以下供应商:
|
||||
|
||||
| 供应商 | 供应品类 | 数据范围 |
|
||||
|--------|---------|---------|
|
||||
| A供应商(汇川/正泰等) | 变频器/整流/低压电器 | Sheet 10-14 |
|
||||
| B供应商(禾望等) | 变频器/整流/PLC | Sheet 10-14 |
|
||||
| C供应商(西门子/施耐德等) | 变频器/PLC/低压电器 | Sheet 10-14 |
|
||||
|
||||
### 8.3 对比组初始化
|
||||
|
||||
根据Sheet 10-14比价数据,建立初始对比组:
|
||||
|
||||
| 对比组 | 包含品牌 | 数据来源 |
|
||||
|--------|---------|---------|
|
||||
| 37kW 380V变频器 | 汇川MD500T37G / 禾望HV500-37G / 西门子G120 / ABB ACS880 | Sheet 10 |
|
||||
| 55kW 380V变频器 | 汇川MD500T55G / 禾望HV500-55G / 西门子G120 PM240 | Sheet 10 |
|
||||
| 整流单元75kW | MDR100T75 / HBD-075 / BLM-075 | Sheet 11 |
|
||||
| CPU 1515-2 PN | 三家供应商报价对比 | Sheet 12 |
|
||||
| 断路器NXM-125S | 正泰/施耐德/其他 | Sheet 13 |
|
||||
| 动力电缆YJV 3×70+1×35 | 三家供应商报价对比 | Sheet 14 |
|
||||
|
||||
### 8.4 导入策略
|
||||
|
||||
**方案一:SQL脚本导入(推荐首批)**
|
||||
- 将型号库数据编写为 `INSERT` 语句
|
||||
- 写入 `sql/demo_data.sql` 或新建 `sql/excel_import_data.sql`
|
||||
- 每个物料生成 `material_code` (如 `M-EL-001`)
|
||||
|
||||
**方案二:Excel批量导入功能(后续维护)**
|
||||
- 通过 `/bid/material/import` 接口
|
||||
- 上传Excel文件 → EasyExcel解析 → 数据校验 → 批量写入
|
||||
- 提供下载导入模板功能
|
||||
|
||||
---
|
||||
|
||||
## 九、开发优先级与里程碑
|
||||
|
||||
### 第一阶段:基础数据层(第1-2周)
|
||||
|
||||
| 优先级 | 任务 | 涉及文件 |
|
||||
|--------|------|---------|
|
||||
| P0 | 物料表字段扩展 + 性能参数JSON动态表单 | `biz_material` + 前端物料表单 |
|
||||
| P0 | 物料分类树+表格联动 | 复用现有 `BizMaterialCategoryController` |
|
||||
| P1 | 物料详情页基础框架 + Tab1(供应商历史报价) | 新建 `material/detail.vue` |
|
||||
| P1 | 供应商-物料关系表 + 供货清单Tab | 新建 `biz_supplier_material` |
|
||||
| P1 | 物料横向对比组表 + 维护界面 | 新建 `biz_material_comparison` |
|
||||
| P2 | Excel电气设备型号库数据导入(首批~764条) | 扩充 `demo_data.sql` |
|
||||
|
||||
### 第二阶段:业务流程层(第3-5周)
|
||||
|
||||
| 优先级 | 任务 | 涉及文件 |
|
||||
|--------|------|---------|
|
||||
| P0 | RFQ增强(合同号/甲方/截止日/指派界面) | `biz_rfq` 扩展 + RFQ详情页重构 |
|
||||
| P0 | 供应商门户独立路由 + 可报项/已报项列表 | 新建 `views/supplier/*` |
|
||||
| P0 | 供应商报价填写界面(分步表单) | 新建 `supplier/quote-edit.vue` |
|
||||
| P1 | 供应商账号管理(账号创建/权限分配) | `sys_user` user_type扩展 |
|
||||
| P1 | 甲方报价单增强(子RFQ生成) | `BizClientQuoteController` |
|
||||
| P1 | 成本回填机制(自动+手动触发) | 新建 `biz_client_quote_item_source` |
|
||||
| P2 | 物料详情Tab2(甲方历史报价) | 扩展 `material/detail.vue` |
|
||||
|
||||
### 第三阶段:智能分析层(第6-8周)
|
||||
|
||||
| 优先级 | 任务 | 涉及文件 |
|
||||
|--------|------|---------|
|
||||
| P1 | 物料详情Tab3(横向同比+推荐算法) | 扩展 `material/detail.vue` |
|
||||
| P1 | 甲方报价单智能选型对比 | `BizComparisonService` 扩展 |
|
||||
| P2 | 价格趋势ECharts折线图(物料详情页) | 物料详情右栏 |
|
||||
| P2 | 供应商报价历史趋势ECharts | 供应商详情Tab4 |
|
||||
| P2 | 价格预警机制(高于均价15%红色高亮) | 前端表格渲染 |
|
||||
| P3 | 比价报告PDF增强 | 复用现有JSPDF导出 |
|
||||
|
||||
### 第四阶段:优化完善(第9-10周)
|
||||
|
||||
| 优先级 | 任务 | 涉及文件 |
|
||||
|--------|------|---------|
|
||||
| P2 | 版本对比(甲方报价单历史版本diff) | 扩展 `client-quote` |
|
||||
| P2 | 批量导入Excel型号库(维护界面) | `/bid/material/import` |
|
||||
| P3 | 供应商端多语言/国际化 | i18n配置 |
|
||||
| P3 | 性能优化(物料列表大数据量分页) | 后端分页+前端虚拟滚动 |
|
||||
| P3 | Docker部署配置更新 | `deploy/` |
|
||||
|
||||
---
|
||||
|
||||
## 十、关键接口清单
|
||||
|
||||
### 物料模块
|
||||
| 接口 | 方法 | 说明 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| `/bid/material/detail/{id}` | GET | 物料详情+历史报价摘要 | P0 |
|
||||
| `/bid/material/{id}/supplier-quotes` | GET | 供应商历史报价清单 | P0 |
|
||||
| `/bid/material/{id}/client-quotes` | GET | 我方给甲方历史报价 | P1 |
|
||||
| `/bid/material/{id}/comparison` | GET | 横向对比数据 | P1 |
|
||||
| `/bid/material/manufacturer/list` | GET | 厂家下拉列表(去重) | P0 |
|
||||
| `/bid/material/import` | POST | Excel批量导入 | P1 |
|
||||
| `/bid/material/export` | GET | 导出模板 | P1 |
|
||||
| `/bid/material/suggest` | GET | 型号智能联想 | P2 |
|
||||
|
||||
### 供应商模块
|
||||
| 接口 | 方法 | 说明 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| `/bid/supplier/{id}/materials` | GET | 供货清单(分页) | P0 |
|
||||
| `/bid/supplier/{id}/materials` | POST | 添加供应物料 | P1 |
|
||||
| `/bid/supplier/{id}/quotes` | GET | 报价单管理列表 | P1 |
|
||||
| `/bid/supplier/{id}/price-trend` | GET | 历史报价趋势 | P2 |
|
||||
| `/bid/supplier/{id}/accounts` | GET | 供应商账号列表 | P1 |
|
||||
| `/bid/supplier/{id}/accounts` | POST | 创建供应商账号 | P2 |
|
||||
|
||||
### RFQ模块
|
||||
| 接口 | 方法 | 说明 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| `/bid/rfq/{id}/assign` | POST | 指派供应商 | P0 |
|
||||
| `/bid/rfq/{id}/comparison` | GET | 比价结果(现有增强) | P0 |
|
||||
| `/bid/rfq/{id}/bid-report` | GET | 导出比价报告PDF | P1 |
|
||||
| `/bid/rfq/{id}/winner` | POST | 确认中标供应商 | P1 |
|
||||
|
||||
### 供应商端
|
||||
| 接口 | 方法 | 说明 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| `/supplier/dashboard/stats` | GET | 统计数据总览 | P0 |
|
||||
| `/supplier/rfq/pending` | GET | 可报项列表 | P0 |
|
||||
| `/supplier/rfq/quoted` | GET | 已报项列表 | P0 |
|
||||
| `/supplier/quote/draft/{rfqId}` | GET | 获取草稿报价 | P0 |
|
||||
| `/supplier/quote/save-draft` | POST | 保存报价草稿 | P0 |
|
||||
| `/supplier/quote/submit` | POST | 提交报价 | P0 |
|
||||
| `/supplier/quote/{id}/attachments` | POST | 上传附件 | P1 |
|
||||
|
||||
### 甲方报价单
|
||||
| 接口 | 方法 | 说明 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| `/bid/clientquote/{id}/generateRfq` | POST | 生成子RFQ | P0 |
|
||||
| `/bid/clientquote/{id}/syncCost` | POST | 同步采购成本 | P1 |
|
||||
| `/bid/clientquote/{id}/compareSpec` | GET | 智能选型对比 | P1 |
|
||||
| `/bid/clientquote/{id}/versions` | GET | 历史版本列表 | P2 |
|
||||
| `/bid/clientquote/version-compare` | POST | 版本对比diff | P2 |
|
||||
| `/bid/clientquote/{id}/export-pdf` | GET | 导出PDF | P0(现有) |
|
||||
|
||||
---
|
||||
|
||||
## 十一、权限与安全设计
|
||||
|
||||
### 11.1 权限矩阵
|
||||
|
||||
| 角色 | 物料 | 供应商 | RFQ | 供应商报价 | 比价 | 甲方报价 | 供应商门户 |
|
||||
|------|------|--------|-----|-----------|------|---------|-----------|
|
||||
| 管理员 | CRUD | CRUD | CRUD | CRUD | 查看 | CRUD | 不可访问 |
|
||||
| 采购员 | 查看 | 查看/编辑 | CRUD | 查看 | 查看/操作 | 查看 | 不可访问 |
|
||||
| 销售员 | 查看 | 查看 | 查看 | 查看 | 查看 | 创建/编辑 | 不可访问 |
|
||||
| 供应商 | 查看(仅关联) | 查看(仅自己) | 查看(受邀) | 查看(仅自己) | 不可见 | 不可见 | 报价/查看 |
|
||||
|
||||
### 11.2 数据权限
|
||||
|
||||
- **租户隔离**:所有业务表已有 `tenant_id`,查询SQL必须带 `tenant_id = ?`
|
||||
- **供应商隔离**:供应商账号 `user_type=2`,所有查询追加 `supplier_id = ?`
|
||||
- **价格脱敏**:供应商看不到其他供应商的报价价格(但可以看到自己的排名)
|
||||
|
||||
### 11.3 安全措施
|
||||
|
||||
- **价格字段加密**:敏感价格字段在数据库中使用AES加密存储,接口返回时按角色脱敏
|
||||
- **乐观锁防并发**:`biz_quotation` 增加 `version` 字段,供应商报价提交时CAS检查
|
||||
- **操作日志**:所有CRUD操作使用RuoYi `@Log` 注解记录
|
||||
- **文件上传校验**:限制附件类型(pdf/jpg/png),大小≤10MB
|
||||
|
||||
---
|
||||
|
||||
## 附录:Excel数据对照表
|
||||
|
||||
### XLSX → 系统表映射
|
||||
|
||||
| Excel Sheet | 对应系统表 | 说明 |
|
||||
|------------|-----------|------|
|
||||
| 01_项目报价单 | `biz_client_quote` + `biz_client_quote_item` | 甲方报价模板映射 |
|
||||
| 02_报价汇总 | `biz_client_quote` (汇总字段) | 汇总逻辑直接实现 |
|
||||
| 10-14_比价表 | `biz_quotation` + `biz_comparison`(已有) | 比价数据初始化来源 |
|
||||
| 20-30_型号库 | `biz_material` | 初始物料数据(764条) |
|
||||
| 31_品牌汇总 | 统计查询 | 直接SQL统计实现 |
|
||||
|
||||
### XLSX比价表 → 系统比价引擎字段映射
|
||||
|
||||
| Excel列 | 系统字段 | 说明 |
|
||||
|---------|---------|------|
|
||||
| A供应商型号 | quotation_item.material_name/spec | 报价物料 |
|
||||
| A供应商报价 | quotation_item.unit_price | 供应商A报价单价 |
|
||||
| B供应商型号 | quotation_item.material_name/spec | 报价物料 |
|
||||
| B供应商报价 | quotation_item.unit_price | 供应商B报价单价 |
|
||||
| 最低价 | BizComparisonVO.minPrice | 当前最低价(系统自动计算) |
|
||||
| 推荐 | BizComparisonVO.recommendedSupplier | 评分最优推荐 |
|
||||
|
||||
---
|
||||
|
||||
> 本文档基于PPT《报价比对系统》(6页) + XLSX《电气自动化设备综合管理系统_V1》(27Sheet) + 现有项目源码(RuoYi-Vue v3.9.2, 12个已完成模块)综合分析生成,可直接作为迭代开发的技术参考。
|
||||
>
|
||||
> 最后更新:2026-05-27
|
||||
@@ -54,4 +54,65 @@ public class BizMaterialController extends BaseController {
|
||||
public AjaxResult remove(@PathVariable Long[] materialIds) {
|
||||
return toAjax(service.deleteBizMaterialByIds(materialIds));
|
||||
}
|
||||
|
||||
// ========== 物料详情页接口 ==========
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:material:detail')")
|
||||
@GetMapping("/{id}/supplier-quotes")
|
||||
public AjaxResult supplierQuotes(@PathVariable Long id) {
|
||||
return success(service.selectSupplierQuotesByMaterialId(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:material:detail')")
|
||||
@GetMapping("/{id}/client-quotes")
|
||||
public AjaxResult clientQuotes(@PathVariable Long id) {
|
||||
return success(service.selectClientQuotesByMaterialId(id));
|
||||
}
|
||||
|
||||
@GetMapping("/manufacturer/list")
|
||||
public AjaxResult manufacturerList() {
|
||||
return success(service.selectManufacturerList());
|
||||
}
|
||||
|
||||
// ========== 物料横向对比 ==========
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:material:list')")
|
||||
@PostMapping("/compare")
|
||||
public AjaxResult compare(@RequestBody List<Long> materialIds) {
|
||||
if (materialIds == null || materialIds.isEmpty()) {
|
||||
return error("请至少选择两个物料进行对比");
|
||||
}
|
||||
if (materialIds.size() > 10) {
|
||||
return error("一次最多对比10个物料");
|
||||
}
|
||||
return success(service.compareMaterials(materialIds));
|
||||
}
|
||||
|
||||
// ========== 同类型物料横向对比 ==========
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:material:list')")
|
||||
@GetMapping("/selectable-for-comparison")
|
||||
public AjaxResult getSelectableMaterials() {
|
||||
return success(service.selectMaterialsWithSupplierQuotes());
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:material:list')")
|
||||
@GetMapping("/same-name/{materialName}")
|
||||
public AjaxResult getSameNameMaterials(@PathVariable String materialName,
|
||||
@RequestParam(required = false) Long excludeId) {
|
||||
List<BizMaterial> list = service.selectMaterialsByExactName(materialName, excludeId);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('bid:material:list')")
|
||||
@PostMapping("/quote-comparison")
|
||||
public AjaxResult getQuoteComparison(@RequestBody List<Long> materialIds) {
|
||||
if (materialIds == null || materialIds.isEmpty()) {
|
||||
return error("请至少选择一个物料");
|
||||
}
|
||||
if (materialIds.size() > 10) {
|
||||
return error("一次最多对比10个物料");
|
||||
}
|
||||
return success(service.selectSupplierQuoteComparison(materialIds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ spring:
|
||||
master:
|
||||
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: password
|
||||
password: 135827
|
||||
# 从库数据源
|
||||
slave:
|
||||
# 从数据源开关/默认关闭
|
||||
|
||||
@@ -9,7 +9,6 @@ public class BizMaterial extends BaseEntity {
|
||||
private String materialCode;
|
||||
private String materialName;
|
||||
private String spec;
|
||||
private String modelNo;
|
||||
private String unit;
|
||||
private String brand;
|
||||
private String description;
|
||||
@@ -17,6 +16,12 @@ public class BizMaterial extends BaseEntity {
|
||||
// search helper
|
||||
private String categoryName;
|
||||
|
||||
// 新增字段
|
||||
private String performanceParams;
|
||||
private String material;
|
||||
private String purpose;
|
||||
private String imageUrl;
|
||||
|
||||
public Long getMaterialId() { return materialId; }
|
||||
public void setMaterialId(Long materialId) { this.materialId = materialId; }
|
||||
public Long getTenantId() { return tenantId; }
|
||||
@@ -29,8 +34,6 @@ public class BizMaterial extends BaseEntity {
|
||||
public void setMaterialName(String materialName) { this.materialName = materialName; }
|
||||
public String getSpec() { return spec; }
|
||||
public void setSpec(String spec) { this.spec = spec; }
|
||||
public String getModelNo() { return modelNo; }
|
||||
public void setModelNo(String modelNo) { this.modelNo = modelNo; }
|
||||
public String getUnit() { return unit; }
|
||||
public void setUnit(String unit) { this.unit = unit; }
|
||||
public String getBrand() { return brand; }
|
||||
@@ -41,4 +44,12 @@ public class BizMaterial extends BaseEntity {
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
|
||||
public String getPerformanceParams() { return performanceParams; }
|
||||
public void setPerformanceParams(String performanceParams) { this.performanceParams = performanceParams; }
|
||||
public String getMaterial() { return material; }
|
||||
public void setMaterial(String material) { this.material = material; }
|
||||
public String getPurpose() { return purpose; }
|
||||
public void setPurpose(String purpose) { this.purpose = purpose; }
|
||||
public String getImageUrl() { return imageUrl; }
|
||||
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.system.mapper.bid;
|
||||
|
||||
import com.ruoyi.system.domain.bid.BizMaterial;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BizMaterialMapper {
|
||||
List<BizMaterial> selectBizMaterialList(BizMaterial query);
|
||||
@@ -10,4 +12,21 @@ public interface BizMaterialMapper {
|
||||
int updateBizMaterial(BizMaterial record);
|
||||
int deleteBizMaterialById(Long id);
|
||||
int deleteBizMaterialByIds(Long[] ids);
|
||||
|
||||
// 物料详情页
|
||||
List<Map<String, Object>> selectSupplierQuotesByMaterialId(Long materialId);
|
||||
|
||||
// 批量物料对比
|
||||
List<BizMaterial> selectBizMaterialByIds(List<Long> materialIds);
|
||||
List<Map<String, Object>> selectSupplierPriceSummaryByMaterialIds(List<Long> materialIds);
|
||||
List<Map<String, Object>> selectBestSupplierOfferByMaterialIds(List<Long> materialIds);
|
||||
List<Map<String, Object>> selectClientQuotesByMaterialId(Long materialId);
|
||||
List<String> selectManufacturerList();
|
||||
|
||||
// 同类型物料横向对比
|
||||
List<BizMaterial> selectMaterialsWithSupplierQuotes();
|
||||
List<Map<String, Object>> selectSupplierQuoteComparison(List<Long> materialIds);
|
||||
|
||||
// 根据物料名称精确匹配(同名称不同规格/品牌对比)
|
||||
List<BizMaterial> selectMaterialsByExactName(@Param("materialName") String materialName, @Param("excludeId") Long excludeId);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.system.service.bid;
|
||||
|
||||
import com.ruoyi.system.domain.bid.BizMaterial;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IBizMaterialService {
|
||||
List<BizMaterial> selectBizMaterialList(BizMaterial query);
|
||||
@@ -10,4 +11,18 @@ public interface IBizMaterialService {
|
||||
int updateBizMaterial(BizMaterial record);
|
||||
int deleteBizMaterialById(Long id);
|
||||
int deleteBizMaterialByIds(Long[] ids);
|
||||
|
||||
List<Map<String, Object>> selectSupplierQuotesByMaterialId(Long materialId);
|
||||
List<Map<String, Object>> selectClientQuotesByMaterialId(Long materialId);
|
||||
List<String> selectManufacturerList();
|
||||
|
||||
// 批量物料对比
|
||||
Map<String, Object> compareMaterials(List<Long> materialIds);
|
||||
|
||||
// 同类型物料横向对比
|
||||
List<BizMaterial> selectMaterialsWithSupplierQuotes();
|
||||
List<Map<String, Object>> selectSupplierQuoteComparison(List<Long> materialIds);
|
||||
|
||||
// 根据物料名称精确匹配(同名称不同规格/品牌对比)
|
||||
List<BizMaterial> selectMaterialsByExactName(String materialName, Long excludeId);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import com.ruoyi.system.mapper.bid.BizMaterialMapper;
|
||||
import com.ruoyi.system.service.bid.IBizMaterialService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class BizMaterialServiceImpl implements IBizMaterialService {
|
||||
@@ -41,4 +42,70 @@ public class BizMaterialServiceImpl implements IBizMaterialService {
|
||||
public int deleteBizMaterialByIds(Long[] ids) {
|
||||
return mapper.deleteBizMaterialByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> selectSupplierQuotesByMaterialId(Long materialId) {
|
||||
return mapper.selectSupplierQuotesByMaterialId(materialId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> selectClientQuotesByMaterialId(Long materialId) {
|
||||
return mapper.selectClientQuotesByMaterialId(materialId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> selectManufacturerList() {
|
||||
return mapper.selectManufacturerList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> compareMaterials(List<Long> materialIds) {
|
||||
if (materialIds == null || materialIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 1. 获取物料基本信息
|
||||
List<BizMaterial> materials = mapper.selectBizMaterialByIds(materialIds);
|
||||
|
||||
// 2. 获取供应商报价汇总
|
||||
List<Map<String, Object>> priceSummaries = mapper.selectSupplierPriceSummaryByMaterialIds(materialIds);
|
||||
Map<Long, Map<String, Object>> priceMap = new HashMap<>();
|
||||
for (Map<String, Object> row : priceSummaries) {
|
||||
Long mid = ((Number) row.get("material_id")).longValue();
|
||||
priceMap.put(mid, row);
|
||||
}
|
||||
|
||||
// 3. 获取最优报价详情
|
||||
List<Map<String, Object>> bestOffers = mapper.selectBestSupplierOfferByMaterialIds(materialIds);
|
||||
Map<Long, Map<String, Object>> bestOfferMap = new HashMap<>();
|
||||
for (Map<String, Object> row : bestOffers) {
|
||||
Long mid = ((Number) row.get("material_id")).longValue();
|
||||
bestOfferMap.put(mid, row);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("materials", materials);
|
||||
result.put("priceMap", priceMap);
|
||||
result.put("bestOfferMap", bestOfferMap);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMaterial> selectMaterialsWithSupplierQuotes() {
|
||||
return mapper.selectMaterialsWithSupplierQuotes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> selectSupplierQuoteComparison(List<Long> materialIds) {
|
||||
if (materialIds == null || materialIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return mapper.selectSupplierQuoteComparison(materialIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizMaterial> selectMaterialsByExactName(String materialName, Long excludeId) {
|
||||
return mapper.selectMaterialsByExactName(materialName, excludeId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<result property="materialCode" column="material_code"/>
|
||||
<result property="materialName" column="material_name"/>
|
||||
<result property="spec" column="spec"/>
|
||||
<result property="modelNo" column="model_no"/>
|
||||
<result property="unit" column="unit"/>
|
||||
<result property="brand" column="brand"/>
|
||||
<result property="description" column="description"/>
|
||||
@@ -18,27 +17,40 @@
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="performanceParams" column="performance_params"/>
|
||||
<result property="material" column="material"/>
|
||||
<result property="purpose" column="purpose"/>
|
||||
<result property="imageUrl" column="image_url"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectBizMaterialList" resultMap="BaseRM">
|
||||
SELECT m.* FROM biz_material m
|
||||
SELECT m.*, c.category_name AS category_name
|
||||
FROM biz_material m
|
||||
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
|
||||
<where>
|
||||
<if test="tenantId != null"> AND m.tenant_id = #{tenantId}</if>
|
||||
<if test="categoryId != null"> AND m.category_id = #{categoryId}</if>
|
||||
<if test="materialCode != null and materialCode != ''"> AND m.material_code LIKE CONCAT('%',#{materialCode},'%')</if>
|
||||
<if test="materialName != null and materialName != ''"> AND m.material_name LIKE CONCAT('%',#{materialName},'%')</if>
|
||||
<if test="status != null and status != ''"> AND m.status = #{status}</if>
|
||||
<if test="materialCode != null and materialCode != ''"> AND m.material_code LIKE CONCAT('%',#{materialCode},'%')</if>
|
||||
<if test="materialName != null and materialName != ''"> AND m.material_name LIKE CONCAT('%',#{materialName},'%')</if>
|
||||
<if test="brand != null and brand != ''"> AND m.brand LIKE CONCAT('%',#{brand},'%')</if>
|
||||
<if test="spec != null and spec != ''"> AND m.spec LIKE CONCAT('%',#{spec},'%')</if>
|
||||
<if test="status != null and status != ''"> AND m.status = #{status}</if>
|
||||
</where>
|
||||
ORDER BY m.material_id DESC
|
||||
</select>
|
||||
|
||||
<select id="selectBizMaterialById" resultMap="BaseRM">
|
||||
SELECT * FROM biz_material WHERE material_id=#{id}
|
||||
SELECT m.*, c.category_name AS category_name
|
||||
FROM biz_material m
|
||||
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
|
||||
WHERE m.material_id=#{id}
|
||||
</select>
|
||||
|
||||
<insert id="insertBizMaterial" useGeneratedKeys="true" keyProperty="materialId">
|
||||
INSERT INTO biz_material(tenant_id,category_id,material_code,material_name,spec,model_no,unit,brand,description,status,create_by,create_time)
|
||||
VALUES(#{tenantId},#{categoryId},#{materialCode},#{materialName},#{spec},#{modelNo},#{unit},#{brand},#{description},#{status},#{createBy},NOW())
|
||||
INSERT INTO biz_material(tenant_id,category_id,material_code,material_name,spec,unit,brand,
|
||||
description,status,performance_params,material,purpose,image_url,create_by,create_time)
|
||||
VALUES(#{tenantId},#{categoryId},#{materialCode},#{materialName},#{spec},#{unit},#{brand},
|
||||
#{description},#{status},#{performanceParams},#{material},#{purpose},#{imageUrl},#{createBy},NOW())
|
||||
</insert>
|
||||
|
||||
<update id="updateBizMaterial">
|
||||
@@ -47,12 +59,15 @@
|
||||
<if test="materialName != null">material_name=#{materialName},</if>
|
||||
<if test="materialCode != null">material_code=#{materialCode},</if>
|
||||
<if test="spec != null">spec=#{spec},</if>
|
||||
<if test="modelNo != null">model_no=#{modelNo},</if>
|
||||
<if test="unit != null">unit=#{unit},</if>
|
||||
<if test="brand != null">brand=#{brand},</if>
|
||||
<if test="description != null">description=#{description},</if>
|
||||
<if test="categoryId != null">category_id=#{categoryId},</if>
|
||||
<if test="status != null">status=#{status},</if>
|
||||
<if test="performanceParams != null">performance_params=#{performanceParams},</if>
|
||||
<if test="material != null">material=#{material},</if>
|
||||
<if test="purpose != null">purpose=#{purpose},</if>
|
||||
<if test="imageUrl != null">image_url=#{imageUrl},</if>
|
||||
update_by=#{updateBy}, update_time=NOW()
|
||||
</set>
|
||||
WHERE material_id=#{materialId}
|
||||
@@ -63,4 +78,129 @@
|
||||
DELETE FROM biz_material WHERE material_id IN
|
||||
<foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- 供应商报价历史:通过 rfq_item 关联物料 -->
|
||||
<select id="selectSupplierQuotesByMaterialId" resultType="java.util.Map">
|
||||
SELECT qi.item_id, qi.unit_price, qi.total_price, qi.delivery_days,
|
||||
q.quotation_id, q.quote_no, q.submit_time, q.status AS quote_status,
|
||||
s.supplier_id, s.supplier_name
|
||||
FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
JOIN biz_supplier s ON q.supplier_id = s.supplier_id
|
||||
JOIN biz_rfq_item ri ON qi.rfq_item_id = ri.item_id
|
||||
WHERE ri.material_id = #{materialId}
|
||||
ORDER BY q.submit_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 甲方报价历史:通过物料名称关联 -->
|
||||
<select id="selectClientQuotesByMaterialId" resultType="java.util.Map">
|
||||
SELECT cqi.item_id, cqi.spec, cqi.model_no, cqi.unit, cqi.quantity,
|
||||
cqi.cost_price, cqi.unit_price, cqi.total_price, cqi.delivery_days,
|
||||
cq.quote_id, cq.quote_no, cq.client_name, cq.status AS quote_status, cq.create_time
|
||||
FROM biz_client_quote_item cqi
|
||||
JOIN biz_client_quote cq ON cqi.quote_id = cq.quote_id
|
||||
WHERE cqi.material_name = (SELECT material_name FROM biz_material WHERE material_id = #{materialId})
|
||||
ORDER BY cq.create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- ========== 批量物料对比 ========== -->
|
||||
<select id="selectBizMaterialByIds" resultMap="BaseRM">
|
||||
SELECT m.*, c.category_name AS category_name
|
||||
FROM biz_material m
|
||||
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
|
||||
WHERE m.material_id IN
|
||||
<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY m.material_id
|
||||
</select>
|
||||
|
||||
<!-- 按物料分组统计供应商报价汇总 -->
|
||||
<select id="selectSupplierPriceSummaryByMaterialIds" resultType="java.util.Map">
|
||||
SELECT ri.material_id,
|
||||
MIN(qi.unit_price) AS min_price,
|
||||
MAX(qi.unit_price) AS max_price,
|
||||
ROUND(AVG(qi.unit_price), 2) AS avg_price,
|
||||
COUNT(DISTINCT q.supplier_id) AS supplier_count
|
||||
FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
JOIN biz_rfq_item ri ON qi.rfq_item_id = ri.item_id
|
||||
WHERE ri.material_id IN
|
||||
<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY ri.material_id
|
||||
</select>
|
||||
|
||||
<!-- 每个物料最低价对应的供应商详情 -->
|
||||
<select id="selectBestSupplierOfferByMaterialIds" resultType="java.util.Map">
|
||||
SELECT t.material_id, t.unit_price, t.delivery_days, s.supplier_name, s.contact, s.phone
|
||||
FROM (
|
||||
SELECT ri.material_id, qi.unit_price, qi.delivery_days, qi.quotation_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY ri.material_id ORDER BY qi.unit_price ASC, qi.delivery_days ASC) AS rn
|
||||
FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
JOIN biz_rfq_item ri ON qi.rfq_item_id = ri.item_id
|
||||
WHERE ri.material_id IN
|
||||
<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
AND qi.unit_price IS NOT NULL
|
||||
) t
|
||||
JOIN biz_quotation q ON t.quotation_id = q.quotation_id
|
||||
JOIN biz_supplier s ON q.supplier_id = s.supplier_id
|
||||
WHERE t.rn = 1
|
||||
</select>
|
||||
|
||||
<!-- 获取厂家/品牌列表(去重) -->
|
||||
<select id="selectManufacturerList" resultType="java.lang.String">
|
||||
SELECT DISTINCT brand FROM biz_material
|
||||
WHERE brand IS NOT NULL AND brand != ''
|
||||
ORDER BY brand
|
||||
</select>
|
||||
|
||||
<!-- 根据物料名称精确匹配查询(用于同名称不同规格/品牌对比) -->
|
||||
<select id="selectMaterialsByExactName" resultMap="BaseRM">
|
||||
SELECT m.*, c.category_name AS category_name
|
||||
FROM biz_material m
|
||||
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
|
||||
WHERE m.material_name = #{materialName}
|
||||
AND m.material_id != #{excludeId}
|
||||
ORDER BY m.material_id DESC
|
||||
</select>
|
||||
|
||||
<!-- ========== 同类型物料横向对比 ========== -->
|
||||
|
||||
<!-- 获取所有有供应商报价记录的物料列表(用于对比选择框) -->
|
||||
<select id="selectMaterialsWithSupplierQuotes" resultMap="BaseRM">
|
||||
SELECT DISTINCT m.*, c.category_name AS category_name
|
||||
FROM biz_material m
|
||||
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
JOIN biz_rfq_item ri ON qi.rfq_item_id = ri.item_id
|
||||
WHERE ri.material_id = m.material_id
|
||||
)
|
||||
ORDER BY m.material_name, m.material_id DESC
|
||||
</select>
|
||||
|
||||
<!-- 根据物料ID列表获取各供应商的详细报价明细(用于对比展示) -->
|
||||
<select id="selectSupplierQuoteComparison" resultType="java.util.Map">
|
||||
SELECT
|
||||
ri.material_id,
|
||||
qi.unit_price,
|
||||
qi.total_price,
|
||||
qi.delivery_days,
|
||||
q.quotation_id,
|
||||
q.quote_no,
|
||||
q.submit_time,
|
||||
q.status AS quote_status,
|
||||
s.supplier_id,
|
||||
s.supplier_name,
|
||||
s.contact AS supplier_contact,
|
||||
s.phone AS supplier_phone
|
||||
FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
JOIN biz_supplier s ON q.supplier_id = s.supplier_id
|
||||
JOIN biz_rfq_item ri ON qi.rfq_item_id = ri.item_id
|
||||
WHERE ri.material_id IN
|
||||
<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY ri.material_id, qi.unit_price ASC, q.submit_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
const baseUrl = '/bid/material'
|
||||
|
||||
export const listMaterial = (params) => request({ url: baseUrl + '/list', method: 'get', params })
|
||||
export const getMaterial = (id) => request({ url: baseUrl + '/' + id, method: 'get' })
|
||||
export const addMaterial = (data) => request({ url: baseUrl, method: 'post', data })
|
||||
export const updateMaterial = (data) => request({ url: baseUrl, method: 'put', data })
|
||||
export const delMaterial = (ids) => request({ url: baseUrl + '/' + ids, method: 'delete' })
|
||||
|
||||
// 物料详情页
|
||||
export const getSupplierQuotes = (id) => request({ url: baseUrl + '/' + id + '/supplier-quotes', method: 'get' })
|
||||
export const getClientQuotes = (id) => request({ url: baseUrl + '/' + id + '/client-quotes', method: 'get' })
|
||||
export const listManufacturer = () => request({ url: baseUrl + '/manufacturer/list', method: 'get' })
|
||||
export const compareMaterials = (data) => request({ url: baseUrl + '/compare', method: 'post', data })
|
||||
|
||||
// 同类型物料横向对比
|
||||
export const getSelectableMaterials = () => request({ url: baseUrl + '/selectable-for-comparison', method: 'get' })
|
||||
export const getQuoteComparison = (data) => request({ url: baseUrl + '/quote-comparison', method: 'post', data })
|
||||
|
||||
// 同名称不同规格/品牌对比
|
||||
export const getSameNameMaterials = (materialName, excludeId) => request({
|
||||
url: baseUrl + '/same-name/' + encodeURIComponent(materialName),
|
||||
method: 'get',
|
||||
params: excludeId ? { excludeId } : undefined
|
||||
})
|
||||
|
||||
@@ -176,3 +176,28 @@ aside {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
表格展示优化 - 表头略大于数据,字体优化
|
||||
========================================== */
|
||||
.el-table thead th {
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.el-table__body td {
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.el-table__body .el-table__row {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.el-table--border th,
|
||||
.el-table--border td {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,20 @@ export const constantRoutes = [
|
||||
|
||||
// 动态路由,基于用户权限动态去加载
|
||||
export const dynamicRoutes = [
|
||||
{
|
||||
path: '/bid/material/detail',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
permissions: ['bid:material:detail'],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/bid/material/detail'),
|
||||
name: 'MaterialDetail',
|
||||
meta: { title: '物料详情', activeMenu: '/bid/material' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/user-auth',
|
||||
component: Layout,
|
||||
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
},
|
||||
resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, rfqNo: null, rfqTitle: null }; this.getList(); },
|
||||
goCompare(row) {
|
||||
this.$router.push({ path: "/procurement/comparison/detail", query: { rfqId: row.rfqId } });
|
||||
this.$router.push({ path: "/bid/comparison/detail", query: { rfqId: row.rfqId } });
|
||||
},
|
||||
goRfqDetail(row) {
|
||||
this.$router.push({ path: "/procurement/rfq/detail", query: { rfqId: row.rfqId } });
|
||||
|
||||
330
ruoyi-ui/src/views/bid/material/components/BasicInfoTab.vue
Normal file
330
ruoyi-ui/src/views/bid/material/components/BasicInfoTab.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="basic-info-tab">
|
||||
<!-- 操作栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-if="!isEditing"
|
||||
type="primary"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
@click="handleEdit">编辑</el-button>
|
||||
<template v-else>
|
||||
<el-button
|
||||
type="success"
|
||||
icon="el-icon-check"
|
||||
size="mini"
|
||||
@click="handleSave">保存</el-button>
|
||||
<el-button
|
||||
icon="el-icon-close"
|
||||
size="mini"
|
||||
@click="handleCancel">取消</el-button>
|
||||
</template>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 基础信息表格 -->
|
||||
<div class="section-title">基础信息</div>
|
||||
<el-table
|
||||
:data="basicInfoData"
|
||||
border
|
||||
size="small"
|
||||
:show-header="false"
|
||||
class="info-table">
|
||||
<el-table-column width="120" align="right" class-name="label-column">
|
||||
<template slot-scope="scope">
|
||||
<span class="info-label">{{ scope.row.label }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column>
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="isEditing"
|
||||
v-model="scope.row.value"
|
||||
size="small"
|
||||
:placeholder="'请输入' + scope.row.label" />
|
||||
<span v-else class="info-value">{{ scope.row.value || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="120" align="right" class-name="label-column">
|
||||
<template slot-scope="scope">
|
||||
<span class="info-label">{{ scope.row.label2 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column>
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="isEditing"
|
||||
v-model="scope.row.value2"
|
||||
size="small"
|
||||
:placeholder="'请输入' + scope.row.label2" />
|
||||
<span v-else class="info-value">{{ scope.row.value2 || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 性能参数表格 -->
|
||||
<div class="section-title">
|
||||
<span>性能参数</span>
|
||||
<el-button
|
||||
v-if="isEditing"
|
||||
type="primary"
|
||||
size="mini"
|
||||
icon="el-icon-plus"
|
||||
@click="addParam">添加参数</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="perfParams"
|
||||
border
|
||||
size="small"
|
||||
class="param-table">
|
||||
<el-table-column label="参数名" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="isEditing"
|
||||
v-model="scope.row.name"
|
||||
size="small"
|
||||
placeholder="如:输出电压" />
|
||||
<span v-else>{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数值" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="isEditing"
|
||||
v-model="scope.row.value"
|
||||
size="small"
|
||||
placeholder="如:24VDC" />
|
||||
<span v-else>{{ scope.row.value }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="isEditing"
|
||||
v-model="scope.row.unit"
|
||||
size="small"
|
||||
placeholder="如:V" />
|
||||
<span v-else>{{ scope.row.unit }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isEditing" label="操作" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
style="color: #f56c6c"
|
||||
@click="deleteParam(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty
|
||||
v-if="!perfParams || perfParams.length === 0"
|
||||
description="暂无性能参数"
|
||||
:image-size="80" />
|
||||
|
||||
<!-- 描述表格 -->
|
||||
<div class="section-title">描述</div>
|
||||
<el-table
|
||||
:data="[{label: '描述', value: description}]"
|
||||
border
|
||||
size="small"
|
||||
:show-header="false"
|
||||
class="desc-table">
|
||||
<el-table-column width="120" align="right" class-name="label-column">
|
||||
<template>
|
||||
<span class="info-label">描述</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column>
|
||||
<template>
|
||||
<el-input
|
||||
v-if="isEditing"
|
||||
v-model="description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入物料描述" />
|
||||
<div v-else class="description-content">{{ description || '暂无描述' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { updateMaterial } from "@/api/bid/material";
|
||||
|
||||
export default {
|
||||
name: "BasicInfoTab",
|
||||
props: {
|
||||
material: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
basicInfoData: [],
|
||||
perfParams: [],
|
||||
description: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
material: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.initData();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 初始化数据
|
||||
initData() {
|
||||
// 基础信息数据(两列布局)
|
||||
this.basicInfoData = [
|
||||
{
|
||||
label: '物料编码', value: this.material.materialCode || '',
|
||||
label2: '所属分类', value2: this.material.categoryName || ''
|
||||
},
|
||||
{
|
||||
label: '物料名称', value: this.material.materialName || '',
|
||||
label2: '厂家/品牌', value2: this.material.brand || ''
|
||||
},
|
||||
{
|
||||
label: '规格型号', value: this.material.spec || '',
|
||||
label2: '材质', value2: this.material.material || ''
|
||||
},
|
||||
{
|
||||
label: '型号', value: this.material.modelNo || '',
|
||||
label2: '用途', value2: this.material.purpose || ''
|
||||
},
|
||||
{
|
||||
label: '单位', value: this.material.unit || '',
|
||||
label2: '备注', value2: this.material.remark || ''
|
||||
}
|
||||
];
|
||||
|
||||
// 性能参数数据
|
||||
this.perfParams = this.material.perfArray && this.material.perfArray.length > 0
|
||||
? JSON.parse(JSON.stringify(this.material.perfArray))
|
||||
: [];
|
||||
|
||||
// 描述
|
||||
this.description = this.material.description || '';
|
||||
},
|
||||
|
||||
// 进入编辑模式
|
||||
handleEdit() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
// 保存(实时保存,无需刷新)
|
||||
async handleSave() {
|
||||
try {
|
||||
// 构建保存的数据
|
||||
const saveData = {
|
||||
materialId: this.material.materialId,
|
||||
materialCode: this.basicInfoData[0].value,
|
||||
materialName: this.basicInfoData[1].value,
|
||||
spec: this.basicInfoData[2].value,
|
||||
modelNo: this.basicInfoData[3].value,
|
||||
unit: this.basicInfoData[4].value,
|
||||
brand: this.basicInfoData[1].value2,
|
||||
material: this.basicInfoData[2].value2,
|
||||
purpose: this.basicInfoData[3].value2,
|
||||
remark: this.basicInfoData[4].value2,
|
||||
performanceParams: JSON.stringify(this.perfParams),
|
||||
description: this.description
|
||||
};
|
||||
|
||||
// 调用API保存
|
||||
await updateMaterial(saveData);
|
||||
|
||||
this.$message.success("保存成功");
|
||||
this.isEditing = false;
|
||||
|
||||
// 更新本地数据
|
||||
this.$emit('update:material', { ...this.material, ...saveData, perfArray: this.perfParams });
|
||||
} catch (error) {
|
||||
this.$message.error("保存失败:" + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 取消编辑
|
||||
handleCancel() {
|
||||
this.isEditing = false;
|
||||
this.initData(); // 恢复原始数据
|
||||
},
|
||||
|
||||
// 添加性能参数
|
||||
addParam() {
|
||||
this.perfParams.push({ name: '', value: '', unit: '' });
|
||||
},
|
||||
|
||||
// 删除性能参数
|
||||
deleteParam(index) {
|
||||
this.perfParams.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.basic-info-tab {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 20px 0 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-table >>> .label-column {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.param-table {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.desc-table {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.description-content {
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
min-height: 60px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
>>> .el-table .cell {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
>>> .el-input__inner {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
>>> .el-textarea__inner {
|
||||
min-height: 80px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-bottom:10px; text-align:right">
|
||||
<el-button size="mini" icon="el-icon-download" @click="exportExcel">导出Excel</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" border size="small">
|
||||
<el-table-column label="报价日期" prop="create_time" width="160" />
|
||||
<el-table-column label="甲方名称" prop="client_name" width="160" />
|
||||
<el-table-column label="成本价(元)" prop="cost_price" width="120">
|
||||
<template slot-scope="scope">¥{{ scope.row.cost_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价(元)" prop="unit_price" width="120">
|
||||
<template slot-scope="scope">¥{{ scope.row.unit_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成交价(元)" prop="total_price" width="120">
|
||||
<template slot-scope="scope">¥{{ scope.row.total_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价单号" prop="quote_no" width="150" />
|
||||
<el-table-column label="状态" prop="quote_status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.quote_status === 'accepted' ? 'success' : 'danger'" size="small">
|
||||
{{ scope.row.quote_status === 'accepted' ? '已接受' : '已拒绝' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && !list.length" description="暂无甲方报价" :image-size="80" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getClientQuotes } from "@/api/bid/material";
|
||||
|
||||
export default {
|
||||
name: "ClientQuoteTab",
|
||||
props: { materialId: [Number, String] },
|
||||
data() { return { loading: false, list: [] }; },
|
||||
created() { this.loadData(); },
|
||||
methods: {
|
||||
loadData() {
|
||||
if (!this.materialId) return;
|
||||
this.loading = true;
|
||||
getClientQuotes(this.materialId).then(res => {
|
||||
this.list = res.data || [];
|
||||
this.loading = false;
|
||||
}).catch(() => { this.loading = false; });
|
||||
},
|
||||
exportExcel() {
|
||||
this.$message.info('导出功能开发中');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
719
ruoyi-ui/src/views/bid/material/components/CompareSection.vue
Normal file
719
ruoyi-ui/src/views/bid/material/components/CompareSection.vue
Normal file
@@ -0,0 +1,719 @@
|
||||
<template>
|
||||
<div class="compare-section">
|
||||
<!-- 顶部控制栏 -->
|
||||
<div class="compare-bar">
|
||||
<div class="compare-bar-left">
|
||||
<span class="compare-section-title">同品类物料横向对比</span>
|
||||
<el-tag v-if="selectedMaterials.length" size="small" type="primary" style="margin-left:8px">
|
||||
{{ selectedMaterials.length }} 个物料
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="compare-bar-right">
|
||||
<span class="select-hint">选择需要对比的物料:</span>
|
||||
<el-select
|
||||
v-model="targetIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
placeholder="同名称不同规格/品牌的物料..."
|
||||
size="small"
|
||||
style="width:400px"
|
||||
:loading="loadingMaterials"
|
||||
@change="handleSelectionChange">
|
||||
<el-option
|
||||
v-for="m in sameNameMaterials"
|
||||
:key="m.materialId"
|
||||
:label="buildOptionLabel(m)"
|
||||
:value="m.materialId" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loadingMaterials || loadingQuote" class="compare-status">
|
||||
<i class="el-icon-loading"></i> 正在加载数据,请稍候…
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="loadError" class="compare-error">
|
||||
<i class="el-icon-warning-outline"></i>
|
||||
<span class="error-text">{{ loadError }}</span>
|
||||
<el-button size="small" type="primary" icon="el-icon-refresh" @click="retryLoad">重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据内容:卡片式布局 -->
|
||||
<div v-else-if="selectedMaterials.length" class="compare-content">
|
||||
<div class="quote-comparison-grid">
|
||||
<div
|
||||
v-for="mat in selectedMaterials"
|
||||
:key="mat.materialId"
|
||||
class="material-quote-card">
|
||||
|
||||
<!-- 卡片头部:物料名称 + 规格/品牌 -->
|
||||
<div class="mat-card-header">
|
||||
<div class="mat-title-group">
|
||||
<span class="mat-name">{{ mat.materialName }}</span>
|
||||
<span class="mat-subtitle">{{ mat.spec || '—' }} / {{ mat.brand || '—' }}</span>
|
||||
</div>
|
||||
<el-tag v-if="quoteMap[mat.materialId] && quoteMap[mat.materialId].length"
|
||||
size="mini" type="success" effect="plain">
|
||||
{{ quoteMap[mat.materialId].length }} 家报价
|
||||
</el-tag>
|
||||
<el-tag v-else size="mini" type="info" effect="plain">暂无报价</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息区 -->
|
||||
<div class="mat-basic-body">
|
||||
<div class="basic-info-grid">
|
||||
<div class="basic-info-item">
|
||||
<span class="basic-label">规格型号</span>
|
||||
<span class="basic-value">{{ mat.spec || '—' }}</span>
|
||||
</div>
|
||||
<div class="basic-info-item">
|
||||
<span class="basic-label">厂家/品牌</span>
|
||||
<span class="basic-value">{{ mat.brand || '—' }}</span>
|
||||
</div>
|
||||
<div class="basic-info-item">
|
||||
<span class="basic-label">材质</span>
|
||||
<span class="basic-value">{{ mat.material || '—' }}</span>
|
||||
</div>
|
||||
<div class="basic-info-item">
|
||||
<span class="basic-label">单位</span>
|
||||
<span class="basic-value">{{ mat.unit || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="section-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-text">
|
||||
<i class="el-icon-s-data"></i> 供应商报价明细
|
||||
</span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
|
||||
<!-- 供应商报价区 -->
|
||||
<div class="mat-quote-section">
|
||||
<div v-if="quoteMap[mat.materialId] && quoteMap[mat.materialId].length" class="quote-list">
|
||||
<div class="quote-header-row">
|
||||
<span class="qh-col supplier-col">供应商</span>
|
||||
<span class="qh-col price-col">单价(元)</span>
|
||||
<span class="qh-col delivery-col">交期(天)</span>
|
||||
<span class="qh-col quote-no-col">报价单号</span>
|
||||
<span class="qh-col date-col">报价日期</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(quote, qIdx) in quoteMap[mat.materialId]"
|
||||
:key="qIdx"
|
||||
class="quote-row"
|
||||
:class="{ 'is-lowest': isLowestPrice(mat.materialId, qIdx) }">
|
||||
<div class="qr-col supplier-col">
|
||||
<span class="supplier-name" :title="quote.supplier_name">{{ quote.supplier_name }}</span>
|
||||
<span v-if="quote.supplier_contact || quote.supplier_phone" class="supplier-contact">
|
||||
{{ quote.supplier_contact || '' }} {{ quote.supplier_phone || '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="qr-col price-col">
|
||||
<span class="price-value">¥{{ formatPrice(quote.unit_price) }}</span>
|
||||
<span v-if="isLowestPrice(mat.materialId, qIdx)" class="lowest-tag">最低</span>
|
||||
</div>
|
||||
<div class="qr-col delivery-col">{{ quote.delivery_days || '—' }}</div>
|
||||
<div class="qr-col quote-no-col" :title="quote.quote_no">{{ quote.quote_no }}</div>
|
||||
<div class="qr-col date-col">{{ formatDate(quote.submit_time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="quote-empty">
|
||||
<i class="el-icon-info"></i>
|
||||
<span>暂无供应商报价数据</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 汇总统计(仅在有报价时显示) -->
|
||||
<div v-if="quoteMap[mat.materialId] && quoteMap[mat.materialId].length" class="quote-summary-bar">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">最低价</span>
|
||||
<span class="summary-value price-lowest">¥{{ formatPrice(getMinPrice(mat.materialId)) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">最高价</span>
|
||||
<span class="summary-value">¥{{ formatPrice(getMaxPrice(mat.materialId)) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">均价</span>
|
||||
<span class="summary-value">¥{{ formatPrice(getAvgPrice(mat.materialId)) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">最高价差</span>
|
||||
<span class="summary-value price-diff">{{ getPriceDiff(mat.materialId) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="cmp-hint">
|
||||
<i class="el-icon-info"></i>
|
||||
<span>暂无同名称不同规格/品牌的物料可对比</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSameNameMaterials, getQuoteComparison } from "@/api/bid/material";
|
||||
|
||||
export default {
|
||||
name: "CompareSection",
|
||||
props: {
|
||||
materialId: { type: [Number, String], required: true },
|
||||
material: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingMaterials: false,
|
||||
loadingQuote: false,
|
||||
loadError: null,
|
||||
sameNameMaterials: [],
|
||||
targetIds: [],
|
||||
quoteMap: {},
|
||||
_loadingLock: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentMaterial() {
|
||||
return this.material || {};
|
||||
},
|
||||
selectedMaterials() {
|
||||
const others = this.sameNameMaterials.filter(m => this.targetIds.includes(m.materialId));
|
||||
return [this.currentMaterial, ...others];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'material.materialName': {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) this.initLoad();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildOptionLabel(m) {
|
||||
const parts = [m.materialName];
|
||||
if (m.spec) parts.push(m.spec);
|
||||
if (m.brand) parts.push(m.brand);
|
||||
return parts.join(' / ');
|
||||
},
|
||||
|
||||
async initLoad() {
|
||||
this.loadError = null;
|
||||
await this.loadSameNameMaterials();
|
||||
if (this.sameNameMaterials.length) {
|
||||
const allIds = this.sameNameMaterials.map(m => m.materialId);
|
||||
this._loadingLock = true;
|
||||
await this.loadQuoteComparison(allIds);
|
||||
this.targetIds = allIds;
|
||||
this._loadingLock = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadSameNameMaterials() {
|
||||
this.loadingMaterials = true;
|
||||
try {
|
||||
const res = await getSameNameMaterials(this.currentMaterial.materialName, Number(this.materialId));
|
||||
this.sameNameMaterials = res.data || [];
|
||||
} catch (err) {
|
||||
this.sameNameMaterials = [];
|
||||
this.loadError = '加载物料列表失败:' + (err.message || '网络异常');
|
||||
} finally {
|
||||
this.loadingMaterials = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleSelectionChange(selectedIds) {
|
||||
if (this._loadingLock) return;
|
||||
this.loadError = null;
|
||||
if (selectedIds.length) {
|
||||
await this.loadQuoteComparison(selectedIds);
|
||||
} else {
|
||||
this.quoteMap = {};
|
||||
}
|
||||
},
|
||||
|
||||
async loadQuoteComparison(materialIds) {
|
||||
if (!materialIds || !materialIds.length) return;
|
||||
this.loadingQuote = true;
|
||||
this.loadError = null;
|
||||
try {
|
||||
const ids = [Number(this.materialId), ...materialIds];
|
||||
const res = await getQuoteComparison(ids);
|
||||
const data = res.data || [];
|
||||
const map = {};
|
||||
data.forEach(item => {
|
||||
const mid = item.material_id;
|
||||
if (!map[mid]) map[mid] = [];
|
||||
map[mid].push(item);
|
||||
});
|
||||
Object.keys(map).forEach(mid => {
|
||||
map[mid].sort((a, b) => {
|
||||
const pa = Number(a.unit_price) || 0;
|
||||
const pb = Number(b.unit_price) || 0;
|
||||
return pa - pb;
|
||||
});
|
||||
});
|
||||
this.quoteMap = map;
|
||||
} catch (err) {
|
||||
this.loadError = '加载报价明细失败:' + (err.message || '网络异常');
|
||||
this.quoteMap = {};
|
||||
} finally {
|
||||
this.loadingQuote = false;
|
||||
}
|
||||
},
|
||||
|
||||
retryLoad() {
|
||||
this.loadError = null;
|
||||
this.initLoad();
|
||||
},
|
||||
|
||||
formatPrice(val) {
|
||||
if (val === null || val === undefined || val === '') return '—';
|
||||
return Number(val).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 4
|
||||
});
|
||||
},
|
||||
|
||||
formatDate(val) {
|
||||
if (!val) return '—';
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.getTime())) return val;
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
getMinPrice(materialId) {
|
||||
const quotes = this.quoteMap[materialId];
|
||||
if (!quotes || !quotes.length) return null;
|
||||
return Math.min(...quotes.map(q => Number(q.unit_price) || 0));
|
||||
},
|
||||
|
||||
getMaxPrice(materialId) {
|
||||
const quotes = this.quoteMap[materialId];
|
||||
if (!quotes || !quotes.length) return null;
|
||||
return Math.max(...quotes.map(q => Number(q.unit_price) || 0));
|
||||
},
|
||||
|
||||
getAvgPrice(materialId) {
|
||||
const quotes = this.quoteMap[materialId];
|
||||
if (!quotes || !quotes.length) return null;
|
||||
const sum = quotes.reduce((acc, q) => acc + (Number(q.unit_price) || 0), 0);
|
||||
return sum / quotes.length;
|
||||
},
|
||||
|
||||
getPriceDiff(materialId) {
|
||||
const min = this.getMinPrice(materialId);
|
||||
const max = this.getMaxPrice(materialId);
|
||||
if (min === null || max === null) return '—';
|
||||
const diff = max - min;
|
||||
if (diff === 0) return '无差异';
|
||||
const pct = ((diff / min) * 100).toFixed(1);
|
||||
return `¥${diff.toFixed(2)} (+${pct}%)`;
|
||||
},
|
||||
|
||||
isLowestPrice(materialId, quoteIndex) {
|
||||
const quotes = this.quoteMap[materialId];
|
||||
if (!quotes || !quotes.length) return false;
|
||||
return quoteIndex === 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ========== 整体容器 ========== */
|
||||
.compare-section {
|
||||
margin-top: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== 顶部控制栏 ========== */
|
||||
.compare-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 14px 20px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.compare-bar-left { display: flex; align-items: center; }
|
||||
.compare-bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.select-hint {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.compare-section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.compare-section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: linear-gradient(180deg, #409eff 0%, #2c3e50 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ========== 状态提示 ========== */
|
||||
.compare-status {
|
||||
text-align: center;
|
||||
padding: 50px 20px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
.compare-status i {
|
||||
font-size: 22px;
|
||||
margin-right: 8px;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
@keyframes rotating {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.compare-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 50px 20px;
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
}
|
||||
.compare-error i {
|
||||
font-size: 36px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.compare-error .error-text {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.cmp-hint {
|
||||
text-align: center;
|
||||
padding: 50px 20px;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cmp-hint i { margin-right: 6px; }
|
||||
|
||||
/* ========== 内容区域 ========== */
|
||||
.compare-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* ========== 卡片网格 ========== */
|
||||
.quote-comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* ========== 物料卡片 ========== */
|
||||
.material-quote-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.material-quote-card:hover {
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.mat-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.mat-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mat-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1a2c4e;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.mat-subtitle {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 基础信息区 */
|
||||
.mat-basic-body {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f0f4f8 100%);
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.basic-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 20px;
|
||||
}
|
||||
.basic-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.basic-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
min-width: 56px;
|
||||
}
|
||||
.basic-value {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.section-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
}
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, #dcdfe6 50%, transparent 100%);
|
||||
}
|
||||
.divider-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.divider-text i {
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 供应商报价区 */
|
||||
.mat-quote-section {
|
||||
padding: 0 8px 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quote-list { font-size: 13px; }
|
||||
|
||||
.quote-header-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr 0.7fr 1.3fr 0.9fr;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.qh-col {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.qh-col:first-child { text-align: left; }
|
||||
|
||||
.quote-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr 0.7fr 1.3fr 0.9fr;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
margin: 0 4px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.quote-row:hover { background: #f8f9fb; }
|
||||
.quote-row.is-lowest {
|
||||
background: linear-gradient(90deg, #f0f9eb 0%, #e8f5e0 100%);
|
||||
border-left: 3px solid #67c23a;
|
||||
}
|
||||
|
||||
.qr-col {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.qr-col:first-child { text-align: left; }
|
||||
|
||||
.supplier-name {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.supplier-contact {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-weight: 700;
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
}
|
||||
.lowest-tag {
|
||||
display: inline-block;
|
||||
background: #67c23a;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.qr-col.delivery-col,
|
||||
.qr-col.quote-no-col,
|
||||
.qr-col.date-col {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 汇总统计 */
|
||||
.quote-summary-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1px;
|
||||
background: #ebeef5;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
background: #fff;
|
||||
gap: 4px;
|
||||
}
|
||||
.summary-label {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
}
|
||||
.price-lowest { color: #67c23a; }
|
||||
.price-diff { color: #e6a23c; font-size: 13px; }
|
||||
|
||||
/* 空状态 */
|
||||
.quote-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50px 20px;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
gap: 8px;
|
||||
}
|
||||
.quote-empty i { font-size: 32px; }
|
||||
|
||||
/* ========== 响应式适配 ========== */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.quote-comparison-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.compare-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.compare-bar-right {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.compare-bar-right .el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
.quote-comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.quote-header-row,
|
||||
.quote-row {
|
||||
grid-template-columns: 1.4fr 1fr 0.7fr 1fr 0.8fr;
|
||||
gap: 2px;
|
||||
padding: 8px 6px;
|
||||
margin: 0;
|
||||
}
|
||||
.basic-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.quote-header-row,
|
||||
.quote-row {
|
||||
grid-template-columns: 1.2fr 1fr 0.6fr;
|
||||
font-size: 12px;
|
||||
}
|
||||
.quote-header-row .quote-no-col,
|
||||
.quote-header-row .date-col,
|
||||
.quote-row .quote-no-col,
|
||||
.quote-row .date-col {
|
||||
display: none;
|
||||
}
|
||||
.quote-summary-bar {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
37
ruoyi-ui/src/views/bid/material/components/DetailHeader.vue
Normal file
37
ruoyi-ui/src/views/bid/material/components/DetailHeader.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="detail-header">
|
||||
<div class="header-left">
|
||||
<h2 style="margin:0 0 8px 0">
|
||||
{{ material.brand || '' }} {{ material.materialName || '' }} {{ material.spec || '' }}
|
||||
</h2>
|
||||
<el-tag size="small" v-if="material.categoryName">{{ material.categoryName }}</el-tag>
|
||||
<el-tag size="small" type="success" v-if="material.material">{{ material.material }}</el-tag>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button icon="el-icon-arrow-left" size="small" @click="$emit('back')">返回列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions :column="4" border size="small" style="margin-top:12px">
|
||||
<el-descriptions-item label="物料编码">{{ material.materialCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="厂家/品牌">{{ material.brand || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="规格型号">{{ material.spec || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">{{ material.unit || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="材质">{{ material.material || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用途">{{ material.purpose || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ material.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DetailHeader",
|
||||
props: { material: { type: Object, default: () => ({}) } }
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-header { display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
.header-right { flex-shrink: 0; }
|
||||
</style>
|
||||
235
ruoyi-ui/src/views/bid/material/components/SupplierQuoteTab.vue
Normal file
235
ruoyi-ui/src/views/bid/material/components/SupplierQuoteTab.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="supplier-quote-tab">
|
||||
<!-- 操作栏 -->
|
||||
<div class="tab-toolbar">
|
||||
<span class="tab-title">供应商报价记录</span>
|
||||
<el-button size="mini" icon="el-icon-download" @click="exportExcel">导出Excel</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 报价表格 -->
|
||||
<el-table
|
||||
:data="list"
|
||||
v-loading="loading"
|
||||
border
|
||||
size="small"
|
||||
class="quote-table"
|
||||
:header-cell-style="headerStyle">
|
||||
<el-table-column label="报价日期" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="date-cell">{{ formatDate(scope.row.submit_time) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 供应商信息完整一行展示 -->
|
||||
<el-table-column label="供应商信息" min-width="280">
|
||||
<template slot-scope="scope">
|
||||
<div class="supplier-info">
|
||||
<div class="supplier-name">{{ scope.row.supplier_name }}</div>
|
||||
<div class="supplier-contact" v-if="scope.row.contact || scope.row.phone">
|
||||
<span v-if="scope.row.contact">{{ scope.row.contact }}</span>
|
||||
<span v-if="scope.row.phone" class="phone">{{ scope.row.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="单价(元)" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-cell">¥{{ formatPrice(scope.row.unit_price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="总价(元)" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span class="price-cell">¥{{ formatPrice(scope.row.total_price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交期" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="days-cell">{{ scope.row.delivery_days }}天</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="报价单号" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="quote-no">{{ scope.row.quote_no }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag
|
||||
:type="getStatusType(scope.row.quote_status)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
class="status-tag">
|
||||
{{ getStatusText(scope.row.quote_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!loading && !list.length" description="暂无供应商报价" :image-size="80">
|
||||
<template #description>
|
||||
<span class="empty-text">暂无供应商报价记录</span>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSupplierQuotes } from "@/api/bid/material";
|
||||
|
||||
export default {
|
||||
name: "SupplierQuoteTab",
|
||||
props: { materialId: [Number, String] },
|
||||
data() { return { loading: false, list: [] }; },
|
||||
created() { this.loadData(); },
|
||||
methods: {
|
||||
loadData() {
|
||||
if (!this.materialId) return;
|
||||
this.loading = true;
|
||||
getSupplierQuotes(this.materialId).then(res => {
|
||||
this.list = res.data || [];
|
||||
this.loading = false;
|
||||
}).catch(() => { this.loading = false; });
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
||||
},
|
||||
formatPrice(price) {
|
||||
if (!price && price !== 0) return '-';
|
||||
return Number(price).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
},
|
||||
getStatusType(status) {
|
||||
const map = { 'awarded': 'success', 'submitted': 'primary', 'rejected': 'danger', 'draft': 'info' };
|
||||
return map[status] || 'info';
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = { 'awarded': '已中标', 'submitted': '已报价', 'rejected': '已拒绝', 'draft': '草稿' };
|
||||
return map[status] || status;
|
||||
},
|
||||
headerStyle() {
|
||||
return {
|
||||
background: '#f5f7fa',
|
||||
color: '#606266',
|
||||
fontWeight: 600,
|
||||
fontSize: '13px'
|
||||
};
|
||||
},
|
||||
exportExcel() {
|
||||
this.$message.info('导出功能开发中');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.supplier-quote-tab {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.tab-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.quote-table {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
>>> .el-table__header-wrapper {
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* 日期单元格 */
|
||||
.date-cell {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 供应商信息 */
|
||||
.supplier-info {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.supplier-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.supplier-contact {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.supplier-contact .phone {
|
||||
margin-left: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 价格单元格 */
|
||||
.price-cell {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 交期单元格 */
|
||||
.days-cell {
|
||||
font-size: 13px;
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 报价单号 */
|
||||
.quote-no {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 行悬停效果 */
|
||||
>>> .el-table__row:hover {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
</style>
|
||||
456
ruoyi-ui/src/views/bid/material/detail.vue
Normal file
456
ruoyi-ui/src/views/bid/material/detail.vue
Normal file
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="detail-header">
|
||||
<div class="header-title">
|
||||
<span class="material-name">{{ material.materialName }}</span>
|
||||
<el-button icon="el-icon-back" size="small" @click="goBack">返回列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息卡片 -->
|
||||
<div class="basic-info-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">基础信息</span>
|
||||
<div class="card-actions">
|
||||
<el-button v-if="!isEditing" size="small" icon="el-icon-edit" @click="handleEdit">编辑</el-button>
|
||||
<template v-else>
|
||||
<el-button size="small" type="success" icon="el-icon-check" @click="handleSave">保存</el-button>
|
||||
<el-button size="small" icon="el-icon-close" @click="handleCancel">取消</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述网格:3列 x 2行基础字段 -->
|
||||
<div class="desc-grid">
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">物料编码</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.materialCode" size="small" placeholder="请输入物料编码" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.materialCode }">{{ material.materialCode || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">物料名称</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.materialName" size="small" placeholder="请输入物料名称" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.materialName }">{{ material.materialName || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">厂家/品牌</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.brand" size="small" placeholder="如:汇川" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.brand }">{{ material.brand || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">规格型号</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.spec" size="small" placeholder="如:MD500T37G" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.spec }">{{ material.spec || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">材质</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.material" size="small" placeholder="如:铝合金" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.material }">{{ material.material || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">用途</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.purpose" size="small" placeholder="如:通用负载" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.purpose }">{{ material.purpose || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">单位</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.unit" size="small" placeholder="如:台" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.unit }">{{ material.unit || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">所属分类</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.categoryName" size="small" disabled />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.categoryName }">{{ material.categoryName || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<span class="grid-label">备注</span>
|
||||
<div class="grid-value-wrap">
|
||||
<el-input v-if="isEditing" v-model="form.remark" size="small" placeholder="备注信息" />
|
||||
<span v-else class="grid-value" :class="{ empty: !material.remark }">{{ material.remark || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能参数区域 -->
|
||||
<div class="sub-section">
|
||||
<div class="sub-header">
|
||||
<span>性能参数</span>
|
||||
<el-button v-if="isEditing" size="mini" icon="el-icon-plus" @click="addParam">添加</el-button>
|
||||
</div>
|
||||
<el-table v-if="perfParams.length" :data="perfParams" border size="small" style="width:100%">
|
||||
<el-table-column label="参数名">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="isEditing" v-model="scope.row.name" size="small" placeholder="如:输出电压" />
|
||||
<span v-else>{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数值">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="isEditing" v-model="scope.row.value" size="small" placeholder="如:24VDC" />
|
||||
<span v-else>{{ scope.row.value }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="isEditing" v-model="scope.row.unit" size="small" placeholder="如:V" />
|
||||
<span v-else>{{ scope.row.unit }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isEditing" label="操作" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" style="color:#f56c6c" @click="deleteParam(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="!perfParams.length" class="sub-empty">暂无性能参数</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述区域已移除 -->
|
||||
</div>
|
||||
|
||||
<!-- 下方Tab区域 - 仅报价历史 -->
|
||||
<el-tabs v-model="activeTab" type="border-card" style="margin-top:16px">
|
||||
<el-tab-pane label="供应商报价历史" name="supplier">
|
||||
<SupplierQuoteTab :material-id="materialId" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="甲方报价历史" name="client">
|
||||
<ClientQuoteTab :material-id="materialId" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 同名称不同规格/品牌物料横向对比 -->
|
||||
<CompareSection :material-id="materialId" :material="material" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMaterial, updateMaterial } from "@/api/bid/material";
|
||||
import SupplierQuoteTab from "./components/SupplierQuoteTab";
|
||||
import ClientQuoteTab from "./components/ClientQuoteTab";
|
||||
import CompareSection from "./components/CompareSection";
|
||||
|
||||
export default {
|
||||
name: "MaterialDetail",
|
||||
components: { SupplierQuoteTab, ClientQuoteTab, CompareSection },
|
||||
data() {
|
||||
return {
|
||||
materialId: null,
|
||||
material: {},
|
||||
form: {},
|
||||
perfParams: [],
|
||||
activeTab: "supplier",
|
||||
isEditing: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.materialId = this.$route.query && this.$route.query.id;
|
||||
this.loadMaterial();
|
||||
},
|
||||
methods: {
|
||||
loadMaterial() {
|
||||
if (!this.materialId) return;
|
||||
getMaterial(this.materialId).then(res => {
|
||||
this.material = res.data || {};
|
||||
// 解析性能参数JSON
|
||||
if (this.material.performanceParams) {
|
||||
try {
|
||||
const parsed = JSON.parse(this.material.performanceParams);
|
||||
this.perfParams = Array.isArray(parsed) ? parsed : [];
|
||||
} catch { this.perfParams = []; }
|
||||
} else {
|
||||
this.perfParams = [];
|
||||
}
|
||||
// 初始化表单数据
|
||||
this.form = { ...this.material };
|
||||
});
|
||||
},
|
||||
|
||||
// 进入编辑模式
|
||||
handleEdit() {
|
||||
this.isEditing = true;
|
||||
this.form = { ...this.material };
|
||||
},
|
||||
|
||||
// 保存(实时保存,无需刷新)
|
||||
async handleSave() {
|
||||
try {
|
||||
const saveData = {
|
||||
materialId: this.materialId,
|
||||
...this.form,
|
||||
performanceParams: JSON.stringify(this.perfParams)
|
||||
};
|
||||
|
||||
await updateMaterial(saveData);
|
||||
this.$message.success("保存成功");
|
||||
this.isEditing = false;
|
||||
|
||||
// 更新本地数据
|
||||
this.material = { ...this.material, ...saveData };
|
||||
this.material.perfArray = this.perfParams;
|
||||
} catch (error) {
|
||||
this.$message.error("保存失败:" + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// 取消编辑
|
||||
handleCancel() {
|
||||
this.isEditing = false;
|
||||
this.form = { ...this.material };
|
||||
// 恢复性能参数
|
||||
if (this.material.performanceParams) {
|
||||
try {
|
||||
const parsed = JSON.parse(this.material.performanceParams);
|
||||
this.perfParams = Array.isArray(parsed) ? parsed : [];
|
||||
} catch { this.perfParams = []; }
|
||||
} else {
|
||||
this.perfParams = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 添加性能参数
|
||||
addParam() {
|
||||
this.perfParams.push({ name: '', value: '', unit: '' });
|
||||
},
|
||||
|
||||
// 删除性能参数
|
||||
deleteParam(index) {
|
||||
this.perfParams.splice(index, 1);
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$router.push('/bid/material');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ========= 顶部标题 ========= */
|
||||
.detail-header {
|
||||
background: linear-gradient(135deg, #1a2c4e 0%, #2c3e50 100%);
|
||||
padding: 18px 24px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.header-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.material-name {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
.header-title .el-button {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
.header-title .el-button:hover {
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-color: rgba(255,255,255,0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ========= 基础信息卡片 ========= */
|
||||
.basic-info-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.basic-info-card:hover {
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.card-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(180deg, #409eff 0%, #2c3e50 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.card-actions .el-button {
|
||||
padding: 6px 14px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.card-actions .el-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* ========= 描述网格(3列) ========= */
|
||||
.desc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1px;
|
||||
background: #ebeef5;
|
||||
padding: 1px;
|
||||
margin: 16px 24px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
min-height: 44px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.grid-item:hover {
|
||||
background: #f8f9fb;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #f8f9fb 0%, #f0f2f5 100%);
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 14px;
|
||||
border-right: 1px solid #ebeef5;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.grid-value-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.grid-value {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid-value.empty {
|
||||
color: #c0c4cc;
|
||||
font-style: normal;
|
||||
}
|
||||
.grid-value.empty::before {
|
||||
content: '待填写';
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.grid-value-wrap >>> .el-input__inner {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.grid-value-wrap >>> .el-input__inner:focus {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
/* ========= 子区域(性能参数) ========= */
|
||||
.sub-section {
|
||||
padding: 16px 24px;
|
||||
margin: 0 24px 16px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.sub-empty {
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
padding: 24px;
|
||||
font-size: 13px;
|
||||
background: #f8f9fb;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
}
|
||||
|
||||
/* Tab 区域美化 */
|
||||
>>> .el-tabs--border-card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06);
|
||||
}
|
||||
>>> .el-tabs--border-card > .el-tabs__header {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
}
|
||||
>>> .el-tabs--border-card > .el-tabs__header .el-tabs__item {
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
>>> .el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active {
|
||||
background: #fff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
>>> .el-tabs--border-card > .el-tabs__content {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,11 @@
|
||||
<el-form-item label="物料编码" prop="materialCode">
|
||||
<el-input v-model="queryParams.materialCode" placeholder="请输入物料编码" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="厂家/品牌" prop="brand">
|
||||
<el-select v-model="queryParams.brand" placeholder="全部品牌" clearable style="width:160px" filterable>
|
||||
<el-option v-for="b in brandList" :key="b" :label="b" :value="b" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属分类">
|
||||
<el-select v-model="queryParams.categoryId" placeholder="全部分类" clearable style="width:160px">
|
||||
<el-option v-for="c in flatCategories" :key="c.categoryId"
|
||||
@@ -28,22 +33,37 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="materialList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="物料编码" prop="materialCode" width="130" />
|
||||
<el-table-column label="物料名称" prop="materialName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="所属分类" prop="categoryName" width="130" />
|
||||
<el-table-column label="规格" prop="spec" :show-overflow-tooltip="true" width="130" />
|
||||
<el-table-column label="型号" prop="modelNo" :show-overflow-tooltip="true" width="130" />
|
||||
<el-table-column label="单位" prop="unit" width="70" />
|
||||
<el-table-column label="品牌" prop="brand" width="100" />
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="materialList"
|
||||
@selection-change="handleSelectionChange"
|
||||
border
|
||||
stripe
|
||||
style="width:100%"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 700, fontSize: '13px' }"
|
||||
:cell-style="{ fontSize: '13px', color: '#606266' }">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="物料编码" prop="materialCode" width="140" header-align="center" align="center" />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="所属分类" prop="categoryName" width="130" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="厂家/品牌" prop="brand" width="130" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="规格型号" prop="spec" min-width="160" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="材质" prop="material" width="90" header-align="center" align="center" />
|
||||
<el-table-column label="用途" prop="purpose" min-width="160" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="性能参数" width="200" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.performanceParams">{{ parsePerfParams(scope.row.performanceParams) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="140">
|
||||
<el-table-column label="操作" align="center" width="210" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-document" @click="handleDetail(scope.row)">详情</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
@@ -51,7 +71,8 @@
|
||||
</el-table>
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="640px" append-to-body>
|
||||
<!-- 新增/修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
@@ -72,29 +93,64 @@
|
||||
<el-input v-model="form.materialName" placeholder="请输入物料名称" />
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格" prop="spec">
|
||||
<el-input v-model="form.spec" placeholder="如:500W/220V" />
|
||||
<el-col :span="8">
|
||||
<el-form-item label="厂家/品牌" prop="brand">
|
||||
<el-input v-model="form.brand" placeholder="如:汇川" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="型号" prop="modelNo">
|
||||
<el-input v-model="form.modelNo" placeholder="如:XD-2023A" />
|
||||
<el-col :span="8">
|
||||
<el-form-item label="规格型号" prop="spec">
|
||||
<el-input v-model="form.spec" placeholder="如:MD500T37G" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-input v-model="form.unit" placeholder="台/件" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-input v-model="form.unit" placeholder="如:台/件/米" />
|
||||
<el-form-item label="材质" prop="material">
|
||||
<el-input v-model="form.material" placeholder="如:铜/铝合金/PVC" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="品牌" prop="brand">
|
||||
<el-input v-model="form.brand" />
|
||||
<el-form-item label="用途" prop="purpose">
|
||||
<el-input v-model="form.purpose" placeholder="如:通用负载" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="备注信息" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性能参数">
|
||||
<div class="perf-params-form">
|
||||
<el-button type="success" size="mini" icon="el-icon-plus" @click="addPerfRow">添加参数</el-button>
|
||||
<el-table :data="perfParams" border size="small" style="margin-top:8px" max-height="240">
|
||||
<el-table-column label="参数名" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.name" placeholder="如: 功率" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数值" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.value" placeholder="如: 37" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.unit" placeholder="如: kW" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" style="color:#f56c6c" icon="el-icon-delete" @click="removePerfRow(scope.$index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" rows="2" />
|
||||
</el-form-item>
|
||||
@@ -108,7 +164,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listMaterial, getMaterial, addMaterial, updateMaterial, delMaterial } from "@/api/bid/material";
|
||||
import { listMaterial, getMaterial, addMaterial, updateMaterial, delMaterial, listManufacturer } from "@/api/bid/material";
|
||||
import { getCategoryList } from "@/api/bid/category";
|
||||
|
||||
export default {
|
||||
@@ -117,9 +173,11 @@ export default {
|
||||
return {
|
||||
loading: false, multiple: true, total: 0, materialList: [],
|
||||
open: false, title: "",
|
||||
queryParams: { pageNum: 1, pageSize: 10, materialName: null, materialCode: null, categoryId: null },
|
||||
queryParams: { pageNum: 1, pageSize: 10, materialName: null, materialCode: null, categoryId: null, brand: null, spec: null },
|
||||
form: {},
|
||||
flatCategories: [],
|
||||
brandList: [],
|
||||
perfParams: [],
|
||||
rules: {
|
||||
materialCode: [{ required: true, message: "物料编码不能为空", trigger: "blur" }],
|
||||
materialName: [{ required: true, message: "物料名称不能为空", trigger: "blur" }],
|
||||
@@ -129,6 +187,7 @@ export default {
|
||||
created() {
|
||||
this.getList();
|
||||
this.loadCategories();
|
||||
this.loadBrands();
|
||||
},
|
||||
methods: {
|
||||
loadCategories() {
|
||||
@@ -136,6 +195,9 @@ export default {
|
||||
this.flatCategories = this.flattenTree(res.data || [], 0, "");
|
||||
});
|
||||
},
|
||||
loadBrands() {
|
||||
listManufacturer().then(res => { this.brandList = res.data || []; });
|
||||
},
|
||||
flattenTree(nodes, depth, prefix) {
|
||||
let result = [];
|
||||
for (const n of nodes) {
|
||||
@@ -157,21 +219,70 @@ export default {
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
|
||||
resetQuery() { this.resetForm("queryForm"); this.handleQuery(); },
|
||||
handleSelectionChange(sel) { this.multiple = !sel.length; this.ids = sel.map(s => s.materialId); },
|
||||
handleAdd() { this.reset(); this.open = true; this.title = "新增物料"; },
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.perfParams = [];
|
||||
this.open = true;
|
||||
this.title = "新增物料";
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.reset();
|
||||
getMaterial(row.materialId).then(res => { this.form = res.data; this.open = true; this.title = "修改物料"; });
|
||||
getMaterial(row.materialId).then(res => {
|
||||
this.form = res.data;
|
||||
// 解析性能参数JSON → table
|
||||
this.perfParams = this.parsePerfParamsToArray(this.form.performanceParams);
|
||||
this.open = true;
|
||||
this.title = "修改物料";
|
||||
});
|
||||
},
|
||||
handleDetail(row) {
|
||||
this.$router.push({ path: '/bid/material/detail', query: { id: row.materialId } });
|
||||
},
|
||||
handleDelete(row) {
|
||||
const ids = row.materialId || (this.ids || []).join(",");
|
||||
this.$modal.confirm("确认删除?").then(() => delMaterial(ids)).then(() => { this.getList(); this.$modal.msgSuccess("删除成功"); });
|
||||
},
|
||||
handleStatusChange(row) { updateMaterial(row); },
|
||||
reset() { this.form = { status: "0" }; this.resetForm && this.resetForm("form"); },
|
||||
// 性能参数
|
||||
addPerfRow() {
|
||||
this.perfParams.push({ name: '', value: '', unit: '' });
|
||||
},
|
||||
removePerfRow(index) {
|
||||
this.perfParams.splice(index, 1);
|
||||
},
|
||||
parsePerfParams(jsonStr) {
|
||||
if (!jsonStr) return '-';
|
||||
try {
|
||||
const obj = JSON.parse(jsonStr);
|
||||
const arr = Object.keys(obj).map(k => ({ name: k, value: obj[k] }));
|
||||
return arr.map(p => p.name + ': ' + p.value).join('; ');
|
||||
} catch { return jsonStr; }
|
||||
},
|
||||
parsePerfParamsToArray(jsonStr) {
|
||||
if (!jsonStr) return [];
|
||||
try {
|
||||
// Try [{name,value,unit}] format first
|
||||
const arr = JSON.parse(jsonStr);
|
||||
if (Array.isArray(arr)) return arr;
|
||||
// Try {key: value} format
|
||||
return Object.keys(arr).map(k => ({ name: k, value: arr[k], unit: '' }));
|
||||
} catch { return []; }
|
||||
},
|
||||
reset() {
|
||||
this.form = { status: "0" };
|
||||
this.perfParams = [];
|
||||
this.resetForm && this.resetForm("form");
|
||||
},
|
||||
cancel() { this.open = false; this.reset(); },
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (!valid) return;
|
||||
// 性能参数数组 → JSON string
|
||||
if (this.perfParams.length) {
|
||||
this.form.performanceParams = JSON.stringify(this.perfParams);
|
||||
} else {
|
||||
this.form.performanceParams = null;
|
||||
}
|
||||
const action = this.form.materialId ? updateMaterial : addMaterial;
|
||||
action(this.form).then(() => { this.$modal.msgSuccess("操作成功"); this.open = false; this.getList(); });
|
||||
});
|
||||
@@ -179,3 +290,59 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
background: #fff;
|
||||
padding: 16px 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 搜索表单样式 */
|
||||
.el-form--inline .el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.el-form--inline .el-form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 操作按钮区域间距 */
|
||||
.mb8 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 表格行高 */
|
||||
>>> .el-table .el-table__row {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
/* 表头字体加粗 */
|
||||
>>> .el-table thead th {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 开关居中 */
|
||||
>>> .el-table .el-switch {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 对话框内表格边距 */
|
||||
.perf-params-form {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 搜索框宽度统 */
|
||||
.el-input--small {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.el-form-item:last-child .el-input--small {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* 按钮组间距 */
|
||||
.el-button + .el-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
53
sql/20250527/01_material_alter.sql
Normal file
53
sql/20250527/01_material_alter.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
-- ========================================================
|
||||
-- 物料信息模块数据库扩展脚本
|
||||
-- 日期: 2025-05-27
|
||||
-- 说明: 根据智采系统-物料信息需求扩展biz_material表
|
||||
-- ========================================================
|
||||
|
||||
-- 1. 扩展物料表字段
|
||||
-- 注意: spec字段保持为"规格型号"不变
|
||||
-- 注意: remark字段保持不变
|
||||
-- 注意: brand字段改为"厂家/品牌"
|
||||
|
||||
-- 使用存储过程判断字段是否存在,避免IF NOT EXISTS语法错误
|
||||
DELIMITER //
|
||||
|
||||
CREATE PROCEDURE AddColumnIfNotExists(IN tableName VARCHAR(64), IN colName VARCHAR(64), IN colDef VARCHAR(255))
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = tableName
|
||||
AND column_name = colName
|
||||
) THEN
|
||||
SET @sql = CONCAT('ALTER TABLE ', tableName, ' ADD COLUMN ', colName, ' ', colDef);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 添加性能参数字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'performance_params', "TEXT COMMENT '性能参数(JSON格式)'");
|
||||
|
||||
-- 添加材质字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'material', "VARCHAR(100) DEFAULT '' COMMENT '材质(铜/铝合金/PVC等)'");
|
||||
|
||||
-- 添加用途字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'purpose', "VARCHAR(500) DEFAULT '' COMMENT '用途'");
|
||||
|
||||
-- 添加物料图片URL字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'image_url', "VARCHAR(500) DEFAULT '' COMMENT '物料图片URL'");
|
||||
|
||||
-- 删除存储过程
|
||||
DROP PROCEDURE IF EXISTS AddColumnIfNotExists;
|
||||
|
||||
-- 2. 修改brand字段注释为"厂家/品牌"
|
||||
ALTER TABLE biz_material MODIFY COLUMN brand VARCHAR(100) COMMENT '厂家/品牌';
|
||||
|
||||
-- 3. 添加物料详情页菜单配置(使用INSERT IGNORE避免重复)
|
||||
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES
|
||||
(2011, '物料详情', 2001, 1, 'detail', 'bid/material/detail', NULL, 1, 0, 'C', '1', '0', 'bid:material:detail', '#', 'admin', NOW(), '', '', '');
|
||||
52
sql/20260527/01_material_extension.sql
Normal file
52
sql/20260527/01_material_extension.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- ========================================================
|
||||
-- 物料信息模块数据库扩展脚本(二期)
|
||||
-- 日期: 2026-05-27
|
||||
-- 说明:
|
||||
-- 1. 新增字段: performance_params, material, purpose, image_url
|
||||
-- 2. 修改 brand 字段注释为"厂家/品牌"
|
||||
-- 3. 添加物料详情页菜单配置
|
||||
-- ========================================================
|
||||
|
||||
DELIMITER //
|
||||
|
||||
CREATE PROCEDURE AddColumnIfNotExists(IN tableName VARCHAR(64), IN colName VARCHAR(64), IN colDef VARCHAR(255))
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = tableName
|
||||
AND column_name = colName
|
||||
) THEN
|
||||
SET @sql = CONCAT('ALTER TABLE ', tableName, ' ADD COLUMN ', colName, ' ', colDef);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 添加性能参数字段(JSON格式存储)
|
||||
CALL AddColumnIfNotExists('biz_material', 'performance_params', "TEXT COMMENT '性能参数(JSON格式)'");
|
||||
|
||||
-- 添加材质字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'material', "VARCHAR(100) DEFAULT '' COMMENT '材质(铜/铝合金/PVC等)'");
|
||||
|
||||
-- 添加用途字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'purpose', "VARCHAR(500) DEFAULT '' COMMENT '用途'");
|
||||
|
||||
-- 添加物料图片URL字段
|
||||
CALL AddColumnIfNotExists('biz_material', 'image_url', "VARCHAR(500) DEFAULT '' COMMENT '物料图片URL'");
|
||||
|
||||
-- 删除存储过程
|
||||
DROP PROCEDURE IF EXISTS AddColumnIfNotExists;
|
||||
|
||||
-- 修改brand字段注释为"厂家/品牌"
|
||||
ALTER TABLE biz_material MODIFY COLUMN brand VARCHAR(100) COMMENT '厂家/品牌';
|
||||
|
||||
-- 添加物料详情页菜单配置
|
||||
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES
|
||||
(2011, '物料详情', 2001, 1, 'detail', 'bid/material/detail', NULL, 1, 0, 'C', '1', '0', 'bid:material:detail', '#', 'admin', NOW(), '', '', ''),
|
||||
(2012, '物料新增', 2001, 2, 'add', 'bid/material/add', NULL, 1, 0, 'C', '1', '0', 'bid:material:add', '#', 'admin', NOW(), '', '', ''),
|
||||
(2013, '物料修改', 2001, 3, 'edit', 'bid/material/edit', NULL, 1, 0, 'C', '1', '0', 'bid:material:edit', '#', 'admin', NOW(), '', '', '');
|
||||
@@ -230,6 +230,7 @@ VALUES
|
||||
(2003,'报价请求',2000,3,'rfq','bid/rfq/index',NULL,1,0,'C','0','0','bid:rfq:list','form','admin',NOW(),'','',''),
|
||||
(2004,'供应商报价',2000,4,'quotation','bid/quotation/index',NULL,1,0,'C','0','0','bid:quotation:list','money','admin',NOW(),'','',''),
|
||||
(2005,'智慧比价',2000,5,'comparison','bid/comparison/index',NULL,1,0,'C','0','0','bid:comparison:list','chart','admin',NOW(),'','',''),
|
||||
(2051,'比价详情',2005,1,'detail','bid/comparison/detail',NULL,1,0,'C','1','0','bid:comparison:detail','#','admin',NOW(),'','',''),
|
||||
(2006,'采购单',2000,6,'purchaseorder','bid/purchaseorder/index',NULL,1,0,'C','0','0','bid:purchaseorder:list','shopping','admin',NOW(),'','',''),
|
||||
(2007,'供应商评价',2000,7,'evaluation','bid/evaluation/index',NULL,1,0,'C','0','0','bid:evaluation:list','star','admin',NOW(),'','',''),
|
||||
(2008,'订单异议',2000,8,'objection','bid/objection/index',NULL,1,0,'C','0','0','bid:objection:list','warning','admin',NOW(),'','',''),
|
||||
|
||||
34
sql/client_quote_fix.sql
Normal file
34
sql/client_quote_fix.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE IF NOT EXISTS biz_client_quote (
|
||||
quote_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
quote_no VARCHAR(50) DEFAULT '',
|
||||
client_name VARCHAR(200) DEFAULT '',
|
||||
rfq_id BIGINT DEFAULT NULL,
|
||||
rfq_no VARCHAR(50) DEFAULT '',
|
||||
rfq_title VARCHAR(200) DEFAULT '',
|
||||
status VARCHAR(20) DEFAULT 'draft',
|
||||
validity_date DATETIME DEFAULT NULL,
|
||||
total_amount DECIMAL(15,4) DEFAULT 0,
|
||||
currency VARCHAR(10) DEFAULT 'CNY',
|
||||
remark TEXT,
|
||||
create_by VARCHAR(64) DEFAULT '',
|
||||
create_time DATETIME,
|
||||
update_time DATETIME,
|
||||
PRIMARY KEY (quote_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='甲方报价单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS biz_client_quote_item (
|
||||
item_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
quote_id BIGINT NOT NULL,
|
||||
material_name VARCHAR(200) DEFAULT '',
|
||||
spec VARCHAR(500) DEFAULT '',
|
||||
model_no VARCHAR(200) DEFAULT '',
|
||||
unit VARCHAR(50) DEFAULT '',
|
||||
quantity DECIMAL(15,4) DEFAULT 0,
|
||||
cost_price DECIMAL(15,4) DEFAULT 0,
|
||||
unit_price DECIMAL(15,4) DEFAULT 0,
|
||||
total_price DECIMAL(15,4) DEFAULT 0,
|
||||
delivery_days INT DEFAULT 0,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
PRIMARY KEY (item_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='甲方报价明细';
|
||||
Reference in New Issue
Block a user