3.1 供货商管理页面 - 移除了右侧面板的"供货清单"Tab - 报价历史板块新增搜索功能(物料名称/报价单号/状态/日期范围) - 后端 Mapper 改造支持动态 SQL 过滤 3.2 报价请求与供应商报价关联 - 新增"供应商报价汇总"弹窗,展示 RFQ 下所有供应商的报价对比 - 报价单号改为可点击链接,跳转到供应商报价列表并按单号搜索 3.3 智慧比价详情页 - 修复了比价详情页路由(在 router/index.js 中补充) - 移除了评分维度展示(价格/交期/质量/服务评分条、综合分标签) - 精简为纯粹的供应商价格对比视图 3.4 其他修复 - 首页快捷操作路径修正(/bid/xxx → /xxx) - 停用 bid 目录后受影响的 router.push 路径全部修复 - biz_tenant 表缺失修复(创建建表 SQL 并执行) - 比价详情页路由注册补充 - goCompare 跳转路径修正
462 lines
14 KiB
Vue
462 lines
14 KiB
Vue
<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() {
|
||
// 优先返回上一页,如果没有历史记录则返回物料列表
|
||
if (window.history.length > 1) {
|
||
this.$router.back();
|
||
} else {
|
||
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>
|