Files
erp-next/ruoyi-ui/src/views/bid/material/detail.vue
王文昊 c7d4c2b0ad feat(bid): 完成物料分类体系全功能开发
1. 新增物料分类删除校验,支持校验子分类和关联物料
2. 优化物料列表查询,支持按分类及其后代分类筛选
3. 重构物料详情和编辑页面,使用级联选择器选择分类
4. 新增分类管理页面,支持增删改查分类树形结构
5. 新增物料分类初始化SQL脚本,导入标准分类体系
2026-06-17 10:12:34 +08:00

496 lines
15 KiB
Vue
Raw Permalink 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-cascader
v-if="isEditing"
v-model="form.categoryId"
:options="categoryTreeData"
:props="{ label: 'categoryName', value: 'categoryId', children: 'children', checkStrictly: true, emitPath: false }"
placeholder="请选择分类"
clearable
filterable
size="small"
style="width:100%" />
<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 { getCategoryList } from "@/api/bid/category";
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,
categoryTreeData: []
};
},
created() {
this.materialId = this.$route.query && this.$route.query.id;
this.loadMaterial();
this.loadCategories();
},
methods: {
loadCategories() {
getCategoryList().then(res => {
this.categoryTreeData = res.data || [];
});
},
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;
// 更新本地数据,同步分类名称显示
const categoryName = this.findCategoryName(this.form.categoryId);
this.material = { ...this.material, ...saveData, categoryName };
this.material.perfArray = this.perfParams;
} catch (error) {
this.$message.error("保存失败:" + error.message);
}
},
// 根据categoryId从分类树中查找分类名称
findCategoryName(categoryId) {
if (!categoryId) return '';
const search = (nodes) => {
for (const n of nodes) {
if (n.categoryId === categoryId) return n.categoryName;
if (n.children && n.children.length) {
const found = search(n.children);
if (found) return found;
}
}
return '';
};
return search(this.categoryTreeData);
},
// 取消编辑
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, #333333 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, #e4393c 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: #e4393c;
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: #e4393c;
font-weight: 600;
}
>>> .el-tabs--border-card > .el-tabs__content {
padding: 0;
}
</style>