Files
erp-next/ruoyi-ui/src/views/bid/material/index.vue
王文昊 24ab178ec1 feat(bid): 新增甲方履约订单管理模块
1.  新增甲方履约菜单分类,包含待发、在途、签收三个子菜单并配置权限
2.  重构发货单号生成逻辑,支持区分供应商和甲方履约订单前缀
3.  新增甲方发货单生成功能,可从确认的甲方报价单一键创建
4.  新增京东红主题样式并支持快速切换
5.  优化物料发货记录查询,兼容两种履约订单的客户信息关联
6.  修复订单详情弹窗的空值判断和异常捕获逻辑
7.  新增配套SQL脚本用于菜单初始化和数据修复
2026-06-15 11:09:56 +08:00

421 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">
<!-- 标签页 -->
<el-tabs v-model="activeTab">
<!-- Tab 1: 物料列表原有内容 -->
<el-tab-pane label="物料列表" name="list">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="queryParams.materialName" placeholder="请输入物料名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<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"
:label="c.categoryName" :value="c.categoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
</el-row>
<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: '12px', color: '#606266' }"
size="small">
<el-table-column type="selection" width="44" align="center" />
<el-table-column label="物料编码" prop="materialCode" width="120" header-align="center" align="center" />
<el-table-column label="物料名称" prop="materialName" min-width="130" :show-overflow-tooltip="true" />
<el-table-column label="分类" prop="categoryName" width="100" :show-overflow-tooltip="true" />
<el-table-column label="品牌" prop="brand" width="120" :show-overflow-tooltip="true" />
<el-table-column label="规格型号" prop="spec" min-width="140" :show-overflow-tooltip="true" />
<el-table-column label="材质" prop="material" width="80" :show-overflow-tooltip="true" />
<el-table-column label="用途" prop="purpose" min-width="100" :show-overflow-tooltip="true" />
<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="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-tab-pane>
<!-- Tab 2: 发货记录新增 -->
<el-tab-pane label="发货记录" name="records">
<el-select v-model="recordMaterialId" filterable placeholder="选择物料" style="width:400px" @change="loadRecords" clearable>
<el-option v-for="m in materialOptions" :key="m.materialId"
:label="m.materialCode + ' | ' + (m.materialName || '')" :value="m.materialId" />
</el-select>
<el-table v-loading="recordLoading" :data="recordList" border stripe size="small" style="width:100%;margin-top:12px">
<el-table-column label="发货单号" width="150">
<template slot-scope="s">{{ s.row.do_no || '-' }}</template>
</el-table-column>
<el-table-column label="类型" width="60" align="center">
<template slot-scope="s"><el-tag :type="s.row.type==='client'?'primary':'warning'" size="mini" effect="plain">{{ s.row.type==='client'?'甲方':'供应商' }}</el-tag></template>
</el-table-column>
<el-table-column label="供应商" width="140" show-overflow-tooltip>
<template slot-scope="s">{{ s.row.type==='client' ? '——' : (s.row.supplier_name || '-') }}</template>
</el-table-column>
<el-table-column label="甲方客户" width="140" show-overflow-tooltip>
<template slot-scope="s">{{ s.row.client_name || '-' }}</template>
</el-table-column>
<el-table-column label="数量" prop="quantity" width="75" align="right" />
<el-table-column label="单价" width="95" align="right"><template slot-scope="s">¥{{ s.row.unit_price || 0 }}</template></el-table-column>
<el-table-column label="小计" width="95" align="right"><template slot-scope="s">¥{{ s.row.total_price || 0 }}</template></el-table-column>
<el-table-column label="交货期" prop="delivery_date" width="90" align="center" />
<el-table-column label="结单" prop="actual_close_date" width="90" align="center" />
<el-table-column label="状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="recordStatusType(s.row.delivery_status)" size="small" effect="dark">{{ recordStatusLabel(s.row.delivery_status) }}</el-tag>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!recordMaterialId && !recordLoading" description="请先选择物料" />
<el-empty v-if="recordMaterialId && !recordList.length && !recordLoading" description="该物料暂无发货记录" />
</el-tab-pane>
</el-tabs>
<!-- 新增/修改对话框 -->
<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">
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="form.materialCode" placeholder="请输入物料编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属分类">
<el-select v-model="form.categoryId" placeholder="请选择分类" clearable style="width:100%">
<el-option v-for="c in flatCategories" :key="c.categoryId"
:label="c.indentName" :value="c.categoryId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="form.materialName" placeholder="请输入物料名称" />
</el-form-item>
<el-row>
<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="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="material">
<el-input v-model="form.material" placeholder="如:铜/铝合金/PVC" />
</el-form-item>
</el-col>
<el-col :span="12">
<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>
</el-form>
<div slot="footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listMaterial, getMaterial, addMaterial, updateMaterial, delMaterial, listManufacturer } from "@/api/bid/material";
import { getCategoryList } from "@/api/bid/category";
import request from '@/utils/request'
export default {
name: "Material",
data() {
return {
loading: false, multiple: true, total: 0, materialList: [],
open: false, title: "",
queryParams: { pageNum: 1, pageSize: 10, materialName: null, materialCode: null, categoryId: null, brand: null, spec: null },
form: {},
flatCategories: [],
brandList: [],
perfParams: [],
// 发货记录
activeTab: "list",
recordMaterialId: null,
recordLoading: false,
recordList: [],
materialOptions: [],
rules: {
materialCode: [{ required: true, message: "物料编码不能为空", trigger: "blur" }],
materialName: [{ required: true, message: "物料名称不能为空", trigger: "blur" }],
}
};
},
created() {
this.getList();
this.loadCategories();
this.loadBrands();
this.loadMaterialOptions();
},
methods: {
loadCategories() {
getCategoryList().then(res => {
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) {
result.push({ ...n, indentName: prefix + n.categoryName });
if (n.children && n.children.length) {
result = result.concat(this.flattenTree(n.children, depth + 1, prefix + " "));
}
}
return result;
},
getList() {
this.loading = true;
listMaterial(this.queryParams).then(res => {
this.materialList = res.rows;
this.total = res.total;
this.loading = false;
});
},
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.perfParams = [];
this.open = true;
this.title = "新增物料";
},
handleUpdate(row) {
this.reset();
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); },
// 性能参数
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(); });
});
},
// ═══════════ 发货记录 ═══════════
loadMaterialOptions() {
listMaterial({ pageSize: 999 }).then(r => { this.materialOptions = r.rows || [] }).catch(() => {})
},
loadRecords(materialId) {
if (!materialId) { this.recordList = []; return }
this.recordLoading = true
request({ url: '/bid/delivery/materialRecords/' + materialId, method: 'get' })
.then(r => { this.recordList = (r.data || []).map(d => ({
...d, deliveryDate: d.deliveryDate ? d.deliveryDate.substring(0, 10) : '',
actualCloseDate: d.actualCloseDate ? d.actualCloseDate.substring(0, 10) : ''
})); this.recordLoading = false })
.catch(() => { this.recordLoading = false })
},
recordStatusType(s) { return { pending: "warning", transit: "primary", history: "success" }[s] || "" },
recordStatusLabel(s) { return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-" },
// 兼容 snake_case 和 camelCase
fmtRow(r) { return r }
}
};
</script>
<style scoped>
/* ═══ 京东主题 — 页面级变量覆盖 ═══ */
.app-container {
background: var(--bg-page);
padding: 16px 20px;
border-radius: var(--radius-base);
}
/* 紧凑表格行 */
.el-table td { padding: 4px 4px !important; }
.el-table th { padding: 6px 4px !important; }
/* 圆角按钮 */
.el-button--mini { border-radius: var(--radius-base) !important; }
/* 搜索按钮(适配京东红) */
.search-btn { background: var(--brand-primary); color: #fff; border: none; border-radius: var(--radius-base); }
.search-btn:hover { background: var(--brand-primary-hover); }
/* 搜索表单样式 */
.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>