Files
erp-next/ruoyi-ui/src/views/bid/material/detail.vue
王文昊 e521b0dfeb feat(bid): 完成物料管理模块全功能开发
1. 新增物料详情页路由、菜单与接口,支持查看物料报价与信息
2. 重构物料列表页面,新增品牌筛选、表格样式优化与详情跳转
3. 扩展物料实体与数据库字段,新增材质、用途、性能参数等字段
4. 新增供应商/甲方报价查询、批量对比、同名称物料匹配接口
5. 新增物料详情组件,包含基础信息、供应商报价、甲方报价标签页
6. 修复比价路由跳转路径错误,调整数据库密码配置
7. 新增物料相关SQL脚本与初始化数据
2026-05-29 08:58:58 +08:00

457 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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