feat(bid): 完成物料管理模块全功能开发

1. 新增物料详情页路由、菜单与接口,支持查看物料报价与信息
2. 重构物料列表页面,新增品牌筛选、表格样式优化与详情跳转
3. 扩展物料实体与数据库字段,新增材质、用途、性能参数等字段
4. 新增供应商/甲方报价查询、批量对比、同名称物料匹配接口
5. 新增物料详情组件,包含基础信息、供应商报价、甲方报价标签页
6. 修复比价路由跳转路径错误,调整数据库密码配置
7. 新增物料相关SQL脚本与初始化数据
This commit is contained in:
2026-05-29 08:58:58 +08:00
parent c718ec4076
commit e521b0dfeb
26 changed files with 4871 additions and 41 deletions

View 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>