From 80ef3a1fb42f37cde2e9753317e118c50379b095 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 10 九月 2026 16:35:44 +0800
Subject: [PATCH] 天津-潜宇塑胶 1.设备、生产、BOM、质量、产品、仓储物流模块需求更改
---
src/views/basicData/product/index.vue | 18
src/api/inventoryManagement/stockCheck.js | 112 +
src/views/productionManagement/workOrder/index.vue | 29
src/views/qualityManagement/rawMaterialInspection/index.vue | 22
src/views/procurementManagement/procurementDemand/index.vue | 308 +++
src/views/productionManagement/productStructure/KitCheckDialog.vue | 216 ++
src/views/qualityManagement/finalInspection/index.vue | 22
src/api/productionManagement/pieceRateConfig.js | 57
src/views/productionManagement/costAccounting/index.vue | 371 ++++
src/api/inventoryManagement/stockInventory.js | 19
src/views/qualityManagement/processInspection/index.vue | 22
src/views/productionManagement/productStructure/index.vue | 70
src/views/productionManagement/workOrderManagement/index.vue | 26
src/api/productionManagement/productionStat.js | 40
src/api/productionManagement/materialConsumption.js | 21
src/views/productionManagement/pieceRateConfig/index.vue | 450 +++++
src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue | 171 ++
src/views/productionManagement/productionTraceability/index.vue | 3
src/views/inventoryManagement/stockManagement/index.vue | 5
src/views/productionManagement/materialConsumption/index.vue | 199 ++
vite.config.js | 2
src/api/productionManagement/productBom.js | 37
src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue | 24
src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue | 432 +++++
src/views/qualityManagement/processInspection/components/formDia.vue | 4
src/views/qualityManagement/finalInspection/components/formDia.vue | 4
src/views/qualityManagement/qualityTraceability/index.vue | 305 +++
src/views/productionManagement/productionCosting/index.vue | 13
src/views/inventoryManagement/stockCheck/components/CheckDetail.vue | 383 ++++
src/views/inventoryManagement/stockManagement/StockWarn.vue | 190 ++
docs/.analysis/build-code-docx.cjs | 77
src/api/qualityManagement/qualityTrace.js | 48
src/views/qualityManagement/nonconformingManagement/components/formDia.vue | 32
src/views/productionManagement/productionReporting/index.vue | 29
src/views/productionManagement/outputStatistics/index.vue | 350 ++++
src/views/inventoryManagement/stockCheck/index.vue | 511 ++++++
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue | 4
src/api/productionManagement/costAccounting.js | 57
src/api/procurementManagement/procurementDemand.js | 42
39 files changed, 4,706 insertions(+), 19 deletions(-)
diff --git a/docs/.analysis/build-code-docx.cjs b/docs/.analysis/build-code-docx.cjs
new file mode 100644
index 0000000..1adc90e
--- /dev/null
+++ b/docs/.analysis/build-code-docx.cjs
@@ -0,0 +1,77 @@
+const fs = require('fs');
+const path = require('path');
+const { Document, Packer, Paragraph, TextRun, HeadingLevel } = require('docx');
+
+const ROOT = path.join(__dirname, '..', '..');
+const OUT = path.join(ROOT, '绯荤粺妯″潡婧愪唬鐮乢绾〉闈�.docx');
+
+const viewDirs = [
+ 'src/views/basicData',
+ 'src/views/salesManagement',
+ 'src/views/procurementManagement',
+ 'src/views/personnelManagement',
+ 'src/views/inventoryManagement',
+ 'src/views/equipmentManagement',
+];
+
+const CODE_FONT = { ascii: 'Consolas', hAnsi: 'Consolas', eastAsia: '寰蒋闆呴粦' };
+const PATH_FONT = { ascii: 'Consolas', hAnsi: 'Consolas', eastAsia: '寰蒋闆呴粦' };
+
+function collectVue(relDir) {
+ const abs = path.join(ROOT, relDir);
+ const result = [];
+ (function walk(d) {
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
+ const full = path.join(d, e.name);
+ if (e.isDirectory()) walk(full);
+ else if (e.name.endsWith('.vue')) result.push(full);
+ }
+ })(abs);
+ return result;
+}
+
+// 鍘绘帀 <style> ... </style> 鏍峰紡鍧�
+function stripStyle(code) {
+ return code.replace(/<style[\s\S]*?<\/style\s*>/g, '');
+}
+
+function codeParagraph(code) {
+ const lines = code.split(/\r?\n/);
+ while (lines.length && lines[lines.length - 1] === '') lines.pop();
+ const runs = lines.map((line, i) => {
+ const last = i === lines.length - 1;
+ return new TextRun({ text: line, font: CODE_FONT, size: 16, break: last ? undefined : 1 });
+ });
+ return new Paragraph({ children: runs, spacing: { before: 40, after: 200 } });
+}
+
+async function main() {
+ const files = [];
+ for (const d of viewDirs) {
+ for (const abs of collectVue(d)) {
+ files.push({ abs, rel: path.relative(ROOT, abs).replace(/\\/g, '/') });
+ }
+ }
+ files.sort((a, b) => a.rel.localeCompare(b.rel));
+
+ const children = [];
+ for (const f of files) {
+ children.push(new Paragraph({
+ heading: HeadingLevel.HEADING_1,
+ children: [new TextRun({ text: f.rel, font: PATH_FONT })],
+ }));
+ children.push(codeParagraph(stripStyle(fs.readFileSync(f.abs, 'utf8'))));
+ }
+
+ const doc = new Document({
+ styles: { default: { document: { run: { font: CODE_FONT, size: 16 } } } },
+ sections: [{ properties: {}, children }],
+ });
+ const buf = await Packer.toBuffer(doc);
+ fs.writeFileSync(OUT, buf);
+ console.log('宸茬敓鎴�:', OUT);
+ console.log('鏂囦欢鏁�:', files.length);
+ console.log('澶у皬:', (buf.length / 1024).toFixed(0), 'KB');
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/src/api/inventoryManagement/stockCheck.js b/src/api/inventoryManagement/stockCheck.js
new file mode 100644
index 0000000..e42617f
--- /dev/null
+++ b/src/api/inventoryManagement/stockCheck.js
@@ -0,0 +1,112 @@
+// 搴撳瓨鐩樼偣鎺ュ彛
+import request from "@/utils/request";
+
+// 鍙戣捣鐩樼偣
+export function start(data) {
+ return request({
+ url: "/stockCheck/start",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鐩樼偣鍗曞垎椤�
+export function listPage(query) {
+ return request({
+ url: "/stockCheck/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鐩樼偣鍗曡鎯�
+export function getById(id) {
+ return request({
+ url: "/stockCheck/" + id,
+ method: "get",
+ });
+}
+
+// 鐩樼偣鏄庣粏鍒嗛〉
+export function detailPage(query) {
+ return request({
+ url: "/stockCheck/detailPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 褰曞叆瀹炵洏
+export function entry(data) {
+ return request({
+ url: "/stockCheck/entry",
+ method: "put",
+ data: data,
+ });
+}
+
+// 瀹屾垚鐩樼偣
+export function finish(data) {
+ return request({
+ url: "/stockCheck/finish",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍙栨秷鐩樼偣
+export function cancel(data) {
+ return request({
+ url: "/stockCheck/cancel",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鐩樼偣鍗曪紙body 涓� id 鏁扮粍锛屽崟涓垹闄や紶 [id]锛�
+export function remove(ids) {
+ return request({
+ url: "/stockCheck/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 宸紓鍒嗘瀽鍒楄〃
+export function diffList(query) {
+ return request({
+ url: "/stockCheck/diffList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 宸紓鍒嗘瀽瀵煎嚭
+export function diffListExport(query) {
+ return request({
+ url: "/stockCheck/diffList/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 鐩樼偣鏄庣粏瀵煎嚭
+export function detailExport(query) {
+ return request({
+ url: "/stockCheck/detailExport",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 鐩樼偣鍗曞鍑�
+export function exportList(query) {
+ return request({
+ url: "/stockCheck/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index 539eedc..ec28652 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -103,3 +103,22 @@
});
};
+// 搴撳瓨棰勮鍒嗛〉
+export const warnPage = (params) => {
+ return request({
+ url: "/stockInventory/warnPage",
+ method: "get",
+ params,
+ });
+};
+
+// 搴撳瓨棰勮瀵煎嚭
+export const warnExport = (params) => {
+ return request({
+ url: "/stockInventory/warnExport",
+ method: "get",
+ params,
+ responseType: "blob",
+ });
+};
+
diff --git a/src/api/procurementManagement/procurementDemand.js b/src/api/procurementManagement/procurementDemand.js
new file mode 100644
index 0000000..a30b953
--- /dev/null
+++ b/src/api/procurementManagement/procurementDemand.js
@@ -0,0 +1,42 @@
+// 閲囪喘闇�姹傛帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/procurementDemand/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鎵归噺鐘舵�佸彉鏇�
+export function changeStatus(ids, status) {
+ return request({
+ url: "/procurementDemand/changeStatus",
+ method: "put",
+ params: {
+ ids: Array.isArray(ids) ? ids.join(",") : ids,
+ status,
+ },
+ });
+}
+
+// 鍒犻櫎
+export function del(ids) {
+ return request({
+ url: "/procurementDemand/del",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 瀵煎嚭
+export function exportDemand(query) {
+ return request({
+ url: "/procurementDemand/export",
+ method: "post",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/productionManagement/costAccounting.js b/src/api/productionManagement/costAccounting.js
new file mode 100644
index 0000000..53e6343
--- /dev/null
+++ b/src/api/productionManagement/costAccounting.js
@@ -0,0 +1,57 @@
+// 鎴愭湰鏍哥畻鍒嗘瀽鎺ュ彛
+import request from "@/utils/request";
+
+// 1. 鎴愭湰鏍哥畻鏄庣粏
+export function costStat(query) {
+ return request({
+ url: "/costAccounting/stat",
+ method: "get",
+ params: query,
+ });
+}
+
+// 2. 鎴愭湰鏋勬垚姹囨��
+export function costSummary(query) {
+ return request({
+ url: "/costAccounting/summary",
+ method: "get",
+ params: query,
+ });
+}
+
+// 3. 缂轰环鐗╂枡娓呭崟
+export function missingPrice(query) {
+ return request({
+ url: "/costAccounting/missingPrice",
+ method: "get",
+ params: query,
+ });
+}
+
+// 瀵煎嚭锛堝潎涓� GET + blob锛�
+export function costStatExport(query) {
+ return request({
+ url: "/costAccounting/stat/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+export function costSummaryExport(query) {
+ return request({
+ url: "/costAccounting/summary/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+export function missingPriceExport(query) {
+ return request({
+ url: "/costAccounting/missingPrice/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/productionManagement/materialConsumption.js b/src/api/productionManagement/materialConsumption.js
new file mode 100644
index 0000000..11eda91
--- /dev/null
+++ b/src/api/productionManagement/materialConsumption.js
@@ -0,0 +1,21 @@
+// 鏉愭枡娑堣�楁牳绠楁帴鍙�
+import request from "@/utils/request";
+
+// 鏍哥畻鍒楄〃
+export function stat(query) {
+ return request({
+ url: "/materialConsumption/stat",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏍哥畻瀵煎嚭
+export function statExport(query) {
+ return request({
+ url: "/materialConsumption/stat/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/productionManagement/pieceRateConfig.js b/src/api/productionManagement/pieceRateConfig.js
new file mode 100644
index 0000000..e19d9b3
--- /dev/null
+++ b/src/api/productionManagement/pieceRateConfig.js
@@ -0,0 +1,57 @@
+// 璁′欢鍗曚环閰嶇疆鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/pieceRateConfig/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/pieceRateConfig/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂栬緫
+export function update(data) {
+ return request({
+ url: "/pieceRateConfig/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍚敤/鍋滅敤
+export function changeStatus(data) {
+ return request({
+ url: "/pieceRateConfig/changeStatus",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鎵归噺鍒犻櫎
+export function batchDelete(ids) {
+ return request({
+ url: "/pieceRateConfig/batchDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 瀵煎嚭
+export function exportConfig(query) {
+ return request({
+ url: "/pieceRateConfig/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/productionManagement/productBom.js b/src/api/productionManagement/productBom.js
index 517208b..4dc53f7 100644
--- a/src/api/productionManagement/productBom.js
+++ b/src/api/productionManagement/productBom.js
@@ -72,3 +72,40 @@
responseType: "blob",
});
}
+
+// BOM鍚敤/鍋滅敤
+export function changeStatus(data) {
+ return request({
+ url: "/technologyBom/changeStatus",
+ method: "put",
+ data: data,
+ });
+}
+
+// BOM榻愬鍒嗘瀽
+export function kitCheck(data) {
+ return request({
+ url: "/technologyBom/kitCheck",
+ method: "post",
+ data: data,
+ });
+}
+
+// 榻愬鍒嗘瀽瀵煎嚭
+export function kitCheckExport(data) {
+ return request({
+ url: "/technologyBom/kitCheck/export",
+ method: "post",
+ data: data,
+ responseType: "blob",
+ });
+}
+
+// 缂哄彛鐢熸垚閲囪喘闇�姹�
+export function generateDemand(data) {
+ return request({
+ url: "/technologyBom/kitCheck/generateDemand",
+ method: "post",
+ data: data,
+ });
+}
diff --git a/src/api/productionManagement/productionStat.js b/src/api/productionManagement/productionStat.js
new file mode 100644
index 0000000..8b0ea5f
--- /dev/null
+++ b/src/api/productionManagement/productionStat.js
@@ -0,0 +1,40 @@
+// 鐢熶骇缁熻鎺ュ彛锛堣浠跺伐璧勬眹鎬� + 浜ч噺缁熻锛�
+import request from "@/utils/request";
+
+// 璁′欢宸ヨ祫姹囨��
+export function wageSummary(query) {
+ return request({
+ url: "/productionProductMain/wageSummary",
+ method: "get",
+ params: query,
+ });
+}
+
+// 璁′欢宸ヨ祫姹囨�诲鍑�
+export function wageSummaryExport(query) {
+ return request({
+ url: "/productionProductMain/wageSummary/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 浜ч噺缁熻
+export function outputStat(query) {
+ return request({
+ url: "/productionProductMain/outputStat",
+ method: "get",
+ params: query,
+ });
+}
+
+// 浜ч噺缁熻瀵煎嚭
+export function outputStatExport(query) {
+ return request({
+ url: "/productionProductMain/outputStat/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/qualityManagement/qualityTrace.js b/src/api/qualityManagement/qualityTrace.js
new file mode 100644
index 0000000..e0c606a
--- /dev/null
+++ b/src/api/qualityManagement/qualityTrace.js
@@ -0,0 +1,48 @@
+import request from '@/utils/request'
+
+// 姝e悜杩芥函锛氭垚鍝佹壒鍙� -> 鍏ㄩ摼璺�
+export function forwardTrace(batchNo) {
+ return request({
+ url: '/quality/trace/forward',
+ method: 'get',
+ params: { batchNo },
+ })
+}
+
+// 鍙嶅悜杩芥函锛氬師鏂欐壒鍙� -> 鍙楀奖鍝嶆垚鍝�
+export function reverseTrace(batchNo) {
+ return request({
+ url: '/quality/trace/reverse',
+ method: 'get',
+ params: { batchNo },
+ })
+}
+
+// 鎵瑰彿鍊欓�夛紙鎼滅储涓嬫媺锛夛紝type: finished | material
+export function batchOptions(type, keyword, limit = 50) {
+ return request({
+ url: '/quality/trace/batchOptions',
+ method: 'get',
+ params: { type, keyword, limit },
+ })
+}
+
+// 姝e悜杩芥函瀵煎嚭
+export function forwardExport(batchNo) {
+ return request({
+ url: '/quality/trace/forward/export',
+ method: 'get',
+ params: { batchNo },
+ responseType: 'blob',
+ })
+}
+
+// 鍙嶅悜杩芥函瀵煎嚭
+export function reverseExport(batchNo) {
+ return request({
+ url: '/quality/trace/reverse/export',
+ method: 'get',
+ params: { batchNo },
+ responseType: 'blob',
+ })
+}
diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
index b05b215..b16c1ad 100644
--- a/src/views/basicData/product/index.vue
+++ b/src/views/basicData/product/index.vue
@@ -164,6 +164,17 @@
</el-form-item>
</el-col>
</el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鏉″舰鐮侊細"
+ prop="barcode">
+ <el-input v-model="modelForm.barcode"
+ placeholder="璇疯緭鍏ユ潯褰㈢爜锛堥�夊~锛�"
+ clearable
+ @keydown.enter.prevent />
+ </el-form-item>
+ </el-col>
+ </el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
@@ -305,6 +316,11 @@
prop: "unit",
},
{
+ label: "鏉″舰鐮�",
+ prop: "barcode",
+ minWidth: 140,
+ },
+ {
dataType: "action",
label: "鎿嶄綔",
align: "center",
@@ -342,6 +358,7 @@
model: "",
unit: "",
productCode: "",
+ barcode: "",
},
modelRules: {
model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
@@ -420,6 +437,7 @@
modelForm.value.model = "";
modelForm.value.unit = "";
modelForm.value.productCode = "";
+ modelForm.value.barcode = "";
modelForm.value.id = "";
if (type === "edit") {
modelForm.value = { ...data };
diff --git a/src/views/inventoryManagement/stockCheck/components/CheckDetail.vue b/src/views/inventoryManagement/stockCheck/components/CheckDetail.vue
new file mode 100644
index 0000000..95c4f9e
--- /dev/null
+++ b/src/views/inventoryManagement/stockCheck/components/CheckDetail.vue
@@ -0,0 +1,383 @@
+<template>
+ <div class="check-detail">
+ <div class="detail-header">
+ <span class="detail-title">鐩樼偣璇︽儏</span>
+ <el-button link
+ type="primary"
+ @click="handleBack">杩斿洖鍒楄〃</el-button>
+ </div>
+
+ <el-descriptions :column="3"
+ border
+ style="margin-bottom: 16px">
+ <el-descriptions-item label="鐩樼偣鍗曞彿">
+ {{ main.checkNo || "-" }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐩樼偣鍚嶇О">
+ {{ main.checkName || "-" }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐩樼偣鏃ユ湡">
+ {{ main.checkDate || "-" }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐩樼偣浜�">
+ {{ main.checker || "-" }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="statusTypeMap[mainStatus] || 'info'">
+ {{ statusTextMap[mainStatus] || "鏈煡" }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="绉嶇被鏁�/宸紓鏁�">
+ {{ main.totalKinds ?? 0 }} / {{ main.diffKinds ?? 0 }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞"
+ :span="3">
+ {{ main.remark || "-" }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <div class="detail-toolbar">
+ <div>
+ <span style="margin-right: 8px">鍙湅宸紓</span>
+ <el-switch v-model="onlyDiff"
+ @change="handleQuery" />
+ </div>
+ <div>
+ <el-button type="primary"
+ :disabled="readOnly"
+ @click="saveEntry">淇濆瓨褰曞叆</el-button>
+ <el-button type="success"
+ :disabled="readOnly"
+ @click="finishCheck">瀹屾垚鐩樼偣</el-button>
+ <el-button @click="openDiffDialog">宸紓鍒嗘瀽</el-button>
+ <el-button @click="handleExportDetail">瀵煎嚭鏄庣粏</el-button>
+ </div>
+ </div>
+
+ <PIMTable rowKey="id"
+ :column="detailColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
+ <template #actualQty="{ row }">
+ <el-input-number v-if="!readOnly"
+ v-model="row.actualQty"
+ :min="0"
+ :precision="4"
+ :controls="false"
+ size="small"
+ style="width: 100%" />
+ <span v-else>{{ row.actualQty ?? "-" }}</span>
+ </template>
+ <template #diffQty="{ row }">
+ <span :style="{ color: diffColor(row) }">{{ diffText(row) }}</span>
+ </template>
+ <template #adjusted="{ row }">
+ <el-tag :type="row.adjusted == 1 ? 'success' : 'info'">
+ {{ row.adjusted == 1 ? "宸茶皟璐�" : "鏈皟璐�" }}
+ </el-tag>
+ </template>
+ <template #remark="{ row }">
+ <el-input v-if="!readOnly"
+ v-model="row.remark"
+ size="small"
+ placeholder="澶囨敞" />
+ <span v-else>{{ row.remark || "-" }}</span>
+ </template>
+ </PIMTable>
+
+ <el-dialog v-model="diffDialogVisible"
+ title="宸紓鍒嗘瀽"
+ width="90%">
+ <div style="text-align: right; margin-bottom: 10px">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="diffExportLoading"
+ @click="handleExportDiff">瀵煎嚭宸紓</el-button>
+ </div>
+ <el-table :data="diffData"
+ border
+ height="420">
+ <el-table-column type="index"
+ label="搴忓彿"
+ width="60"
+ align="center" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName"
+ show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="productModel"
+ show-overflow-tooltip />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="80" />
+ <el-table-column label="鎵瑰彿"
+ prop="batchNo"
+ show-overflow-tooltip />
+ <el-table-column label="璐﹂潰鏁伴噺"
+ prop="bookQty"
+ width="120" />
+ <el-table-column label="瀹炵洏鏁伴噺"
+ prop="actualQty"
+ width="120" />
+ <el-table-column label="宸紓鏁伴噺"
+ prop="diffQty"
+ width="120" />
+ <el-table-column label="宸茶皟璐�"
+ width="100"
+ align="center">
+ <template #default="scope">
+ <el-tag :type="scope.row.adjusted == 1 ? 'success' : 'info'">
+ {{ scope.row.adjusted == 1 ? "宸茶皟璐�" : "鏈皟璐�" }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞"
+ prop="remark"
+ show-overflow-tooltip />
+ </el-table>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, getCurrentInstance, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ getById,
+ detailPage,
+ entry,
+ finish,
+ diffList,
+ diffListExport,
+ detailExport,
+ } from "@/api/inventoryManagement/stockCheck.js";
+
+ const props = defineProps({
+ mainId: {
+ type: [Number, String],
+ default: "",
+ },
+ });
+ const emit = defineEmits(["back"]);
+
+ const { proxy } = getCurrentInstance();
+
+ const statusTextMap = { 0: "鐩樼偣涓�", 1: "宸插畬鎴�", 2: "宸插彇娑�" };
+ const statusTypeMap = { 0: "warning", 1: "success", 2: "info" };
+
+ const main = ref({});
+ const onlyDiff = ref(false);
+
+ const mainStatus = computed(() => main.value.status ?? main.value.checkStatus);
+ const readOnly = computed(() => mainStatus.value != 0);
+
+ const detailColumn = ref([
+ { label: "浜у搧鍚嶇О", prop: "productName", minWidth: 140 },
+ { label: "瑙勬牸鍨嬪彿", prop: "productModel", minWidth: 140 },
+ { label: "鍗曚綅", prop: "unit", width: 80 },
+ { label: "鎵瑰彿", prop: "batchNo", minWidth: 120 },
+ { label: "璐﹂潰鏁伴噺", prop: "bookQty", width: 110 },
+ { label: "瀹炵洏鏁伴噺", prop: "actualQty", width: 140, dataType: "slot", slot: "actualQty" },
+ { label: "宸紓", prop: "diffQty", width: 110, dataType: "slot", slot: "diffQty" },
+ { label: "宸茶皟璐�", prop: "adjusted", width: 100, dataType: "slot", slot: "adjusted" },
+ { label: "澶囨敞", prop: "remark", minWidth: 140, dataType: "slot", slot: "remark" },
+ ]);
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const diffDialogVisible = ref(false);
+ const diffData = ref([]);
+ const diffExportLoading = ref(false);
+
+ const diffColor = row => {
+ const diff = diffValue(row);
+ if (diff === null) return "";
+ if (diff > 0) return "#67C23A";
+ if (diff < 0) return "#F56C6C";
+ return "";
+ };
+
+ const diffValue = row => {
+ const actual = row.actualQty;
+ const book = row.bookQty;
+ if (actual === null || actual === undefined || actual === "") return null;
+ return Number(actual) - Number(book ?? 0);
+ };
+
+ const diffText = row => {
+ const diff = diffValue(row);
+ return diff === null ? "-" : diff;
+ };
+
+ const loadMain = () => {
+ if (!props.mainId) return;
+ getById(props.mainId).then(res => {
+ main.value = res?.data || {};
+ });
+ };
+
+ const loadDetail = () => {
+ tableLoading.value = true;
+ detailPage({
+ mainId: props.mainId,
+ onlyDiff: onlyDiff.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ page.total = res?.data?.total || 0;
+ })
+ .catch(() => {})
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ loadDetail();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ loadDetail();
+ };
+
+ const saveEntry = () => {
+ const rows = tableData.value
+ .filter(item => item.actualQty !== null && item.actualQty !== undefined)
+ .map(item => ({
+ id: item.id,
+ actualQty: item.actualQty,
+ remark: item.remark,
+ }));
+ if (rows.length === 0) {
+ proxy.$modal.msgWarning("璇峰厛褰曞叆瀹炵洏鏁伴噺");
+ return;
+ }
+ entry(rows)
+ .then(() => {
+ proxy.$modal.msgSuccess("淇濆瓨鎴愬姛");
+ loadMain();
+ loadDetail();
+ })
+ .catch(() => {});
+ };
+
+ const finishCheck = () => {
+ ElMessageBox.confirm(
+ "宸紓灏嗙敓鎴愯皟璐﹀崟鎹紝瀹℃牳閫氳繃鍚庤处闈㈢敓鏁堬紝纭瀹屾垚鐩樼偣锛�",
+ "鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(() => {
+ finish({ id: props.mainId })
+ .then(res => {
+ proxy.$modal.msgSuccess(res?.data || "鎿嶄綔鎴愬姛");
+ loadMain();
+ loadDetail();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ const openDiffDialog = () => {
+ diffDialogVisible.value = true;
+ diffList({ mainId: props.mainId }).then(res => {
+ diffData.value = res?.data || [];
+ });
+ };
+
+ const exportBlob = async (requestFn, query, filename) => {
+ const blobData = await requestFn(query);
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, filename);
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ };
+
+ const handleExportDetail = async () => {
+ try {
+ await exportBlob(
+ detailExport,
+ { mainId: props.mainId },
+ `鐩樼偣鏄庣粏_${main.value.checkNo || props.mainId}.xlsx`
+ );
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ }
+ };
+
+ const handleExportDiff = async () => {
+ diffExportLoading.value = true;
+ try {
+ await exportBlob(
+ diffListExport,
+ { mainId: props.mainId },
+ `鐩樼偣宸紓_${main.value.checkNo || props.mainId}.xlsx`
+ );
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ diffExportLoading.value = false;
+ }
+ };
+
+ const handleBack = () => {
+ emit("back");
+ };
+
+ onMounted(() => {
+ loadMain();
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .detail-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ }
+ .detail-title {
+ font-size: 16px;
+ font-weight: 600;
+ }
+ .detail-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ }
+</style>
diff --git a/src/views/inventoryManagement/stockCheck/index.vue b/src/views/inventoryManagement/stockCheck/index.vue
new file mode 100644
index 0000000..99c5401
--- /dev/null
+++ b/src/views/inventoryManagement/stockCheck/index.vue
@@ -0,0 +1,511 @@
+<template>
+ <div class="app-container">
+ <template v-if="viewMode === 'list'">
+ <PageHeader content="搴撳瓨鐩樼偣" />
+ <div class="search_form">
+ <el-form :model="queryParams"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="鐩樼偣鍗曞彿锛�">
+ <el-input v-model="queryParams.checkNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐩樼偣鍚嶇О锛�">
+ <el-input v-model="queryParams.checkName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐘舵�侊細">
+ <el-select v-model="queryParams.status"
+ placeholder="鍏ㄩ儴"
+ clearable
+ style="width: 160px"
+ @change="handleQuery">
+ <el-option label="鐩樼偣涓�"
+ :value="0" />
+ <el-option label="宸插畬鎴�"
+ :value="1" />
+ <el-option label="宸插彇娑�"
+ :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐩樼偣鏃堕棿锛�">
+ <el-date-picker v-model="dateRange"
+ type="datetimerange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫椂闂�"
+ end-placeholder="缁撴潫鏃堕棿"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="primary"
+ @click="openStartDialog">鍙戣捣鐩樼偣</el-button>
+ <el-button type="danger"
+ plain
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete">鎵归噺鍒犻櫎</el-button>
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ :tableLoading="tableLoading"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination">
+ <template #status="{ row }">
+ <el-tag :type="statusTypeMap[getStatus(row)] || 'info'">
+ {{ statusTextMap[getStatus(row)] || "鏈煡" }}
+ </el-tag>
+ </template>
+ </PIMTable>
+ </div>
+ </template>
+
+ <CheckDetail v-else
+ :main-id="currentId"
+ @back="backToList" />
+
+ <el-dialog v-model="startDialogVisible"
+ title="鍙戣捣鐩樼偣"
+ width="520px">
+ <el-form ref="startFormRef"
+ :model="startForm"
+ :rules="startRules"
+ label-width="100px">
+ <el-form-item label="鐩樼偣鍚嶇О锛�"
+ prop="checkName">
+ <el-input v-model="startForm.checkName"
+ placeholder="璇疯緭鍏ョ洏鐐瑰悕绉�"
+ clearable />
+ </el-form-item>
+ <el-form-item label="鐩樼偣鏃ユ湡锛�">
+ <el-date-picker v-model="startForm.checkDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="鐩樼偣浜猴細">
+ <el-input v-model="startForm.checker"
+ placeholder="璇疯緭鍏ョ洏鐐逛汉"
+ clearable />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鑼冨洿锛�">
+ <el-select v-model="startForm.productModelIds"
+ multiple
+ filterable
+ clearable
+ placeholder="鐣欑┖琛ㄧず鍏ㄩ儴搴撳瓨"
+ style="width: 100%">
+ <el-option v-for="item in modelOptions"
+ :key="item.id"
+ :label="item.productName ? item.productName + ' ' + item.model : item.model"
+ :value="item.id" />
+ </el-select>
+ <el-button link
+ type="primary"
+ @click="openProductSelect"
+ style="margin-top: 6px">
+ 閫夋嫨浜у搧瑙勬牸
+ </el-button>
+ </el-form-item>
+ <el-form-item label="澶囨敞锛�">
+ <el-input v-model="startForm.remark"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitStart">纭</el-button>
+ <el-button @click="startDialogVisible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <ProductSelectDialog v-model="productSelectVisible"
+ @confirm="handleProductSelected" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ start,
+ listPage,
+ finish,
+ cancel,
+ remove,
+ exportList,
+ } from "@/api/inventoryManagement/stockCheck.js";
+ import CheckDetail from "./components/CheckDetail.vue";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+
+ const { proxy } = getCurrentInstance();
+
+ const statusTextMap = { 0: "鐩樼偣涓�", 1: "宸插畬鎴�", 2: "宸插彇娑�" };
+ const statusTypeMap = { 0: "warning", 1: "success", 2: "info" };
+
+ const getStatus = row => row.status ?? row.checkStatus;
+
+ const tableColumn = ref([
+ { label: "鐩樼偣鍗曞彿", prop: "checkNo", minWidth: 150 },
+ { label: "鐩樼偣鍚嶇О", prop: "checkName", minWidth: 140 },
+ { label: "鐩樼偣鏃ユ湡", prop: "checkDate", width: 120 },
+ { label: "鐩樼偣浜�", prop: "checker", width: 100 },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "slot",
+ slot: "status",
+ width: 100,
+ },
+ { label: "鐩樼偣绉嶇被鏁�", prop: "totalKinds", width: 110 },
+ { label: "宸紓绉嶇被鏁�", prop: "diffKinds", width: 110 },
+ { label: "澶囨敞", prop: "remark", minWidth: 140 },
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 300,
+ operation: [
+ {
+ name: "缁х画鐩樼偣",
+ type: "text",
+ showHide: row => getStatus(row) == 0,
+ clickFun: row => {
+ toDetail(row);
+ },
+ },
+ {
+ name: "瀹屾垚",
+ type: "text",
+ color: "#67C23A",
+ showHide: row => getStatus(row) == 0,
+ clickFun: row => {
+ handleFinish(row);
+ },
+ },
+ {
+ name: "鍙栨秷",
+ type: "text",
+ color: "#909399",
+ showHide: row => getStatus(row) == 0,
+ clickFun: row => {
+ handleCancel(row);
+ },
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ showHide: row => getStatus(row) != 0,
+ clickFun: row => {
+ toDetail(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ]);
+
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const exportLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const queryParams = reactive({
+ checkNo: "",
+ checkName: "",
+ status: "",
+ });
+ const dateRange = ref([]);
+
+ const viewMode = ref("list");
+ const currentId = ref("");
+
+ const startDialogVisible = ref(false);
+ const startFormRef = ref(null);
+ const productSelectVisible = ref(false);
+ const modelOptions = ref([]);
+ const startForm = reactive({
+ checkName: "",
+ checkDate: "",
+ checker: "",
+ remark: "",
+ productModelIds: [],
+ });
+ const startRules = {
+ checkName: [{ required: true, message: "璇疯緭鍏ョ洏鐐瑰悕绉�", trigger: "blur" }],
+ };
+
+ const buildQuery = () => ({
+ checkNo: queryParams.checkNo || undefined,
+ checkName: queryParams.checkName || undefined,
+ status:
+ queryParams.status === "" ||
+ queryParams.status === null ||
+ queryParams.status === undefined
+ ? undefined
+ : queryParams.status,
+ beginTime: dateRange.value?.[0] || undefined,
+ endTime: dateRange.value?.[1] || undefined,
+ });
+
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ listPage({
+ pageNum: page.current,
+ pageSize: page.size,
+ ...buildQuery(),
+ })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ selectedRows.value = [];
+ page.total = res?.data?.total || 0;
+ })
+ .catch(() => {})
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const resetQuery = () => {
+ queryParams.checkNo = "";
+ queryParams.checkName = "";
+ queryParams.status = "";
+ dateRange.value = [];
+ handleQuery();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const toDetail = row => {
+ currentId.value = row.id;
+ viewMode.value = "detail";
+ };
+
+ const backToList = () => {
+ viewMode.value = "list";
+ getList();
+ };
+
+ // 鍙戣捣鐩樼偣
+ const openStartDialog = () => {
+ startForm.checkName = "";
+ startForm.checkDate = "";
+ startForm.checker = "";
+ startForm.remark = "";
+ startForm.productModelIds = [];
+ modelOptions.value = [];
+ startDialogVisible.value = true;
+ };
+
+ const openProductSelect = () => {
+ productSelectVisible.value = true;
+ };
+
+ const handleProductSelected = rows => {
+ (rows || []).forEach(row => {
+ if (!startForm.productModelIds.includes(row.id) && !modelOptions.value.some(m => m.id === row.id)) {
+ modelOptions.value.push(row);
+ }
+ });
+ const ids = (rows || []).map(row => row.id);
+ ids.forEach(id => {
+ if (!startForm.productModelIds.includes(id)) {
+ startForm.productModelIds.push(id);
+ }
+ });
+ };
+
+ const submitStart = () => {
+ startFormRef.value?.validate(valid => {
+ if (!valid) {
+ return;
+ }
+ const submitData = {
+ checkName: startForm.checkName,
+ checkDate: startForm.checkDate || undefined,
+ checker: startForm.checker || undefined,
+ remark: startForm.remark || undefined,
+ productModelIds:
+ startForm.productModelIds.length > 0
+ ? startForm.productModelIds
+ : undefined,
+ };
+ start(submitData)
+ .then(() => {
+ proxy.$modal.msgSuccess("鍙戣捣鎴愬姛");
+ startDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {});
+ });
+ };
+
+ // 瀹屾垚
+ const handleFinish = row => {
+ ElMessageBox.confirm(
+ "宸紓灏嗙敓鎴愯皟璐﹀崟鎹紝瀹℃牳閫氳繃鍚庤处闈㈢敓鏁堬紝纭瀹屾垚鐩樼偣锛�",
+ "鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(() => {
+ finish({ id: row.id })
+ .then(res => {
+ proxy.$modal.msgSuccess(res?.data || "鎿嶄綔鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 鍙栨秷
+ const handleCancel = row => {
+ ElMessageBox.confirm("纭鍙栨秷璇ョ洏鐐瑰崟锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ cancel({ id: row.id })
+ .then(() => {
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 鍒犻櫎
+ const handleDelete = row => {
+ ElMessageBox.confirm(
+ `纭鍒犻櫎鐩樼偣鍗曘��${row.checkNo || row.checkName || ""}銆嶏紵`,
+ "鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(() => {
+ remove([row.id])
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 鎵归噺鍒犻櫎
+ const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm(
+ `閫変腑鐨� ${selectedRows.value.length} 鏉$洏鐐瑰崟灏嗚鍒犻櫎锛屾槸鍚︾‘璁ゅ垹闄わ紵`,
+ "鍒犻櫎鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(() => {
+ remove(selectedRows.value.map(item => item.id))
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 瀵煎嚭
+ const handleExport = async () => {
+ exportLoading.value = true;
+ try {
+ const blobData = await exportList(buildQuery());
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, "搴撳瓨鐩樼偣.xlsx");
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/inventoryManagement/stockManagement/StockWarn.vue b/src/views/inventoryManagement/stockManagement/StockWarn.vue
new file mode 100644
index 0000000..17fd68a
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/StockWarn.vue
@@ -0,0 +1,190 @@
+<template>
+ <div class="stock-warn">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="浜у搧澶х被锛�">
+ <el-input v-model="searchForm.productName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿锛�">
+ <el-input v-model="searchForm.model"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鎵瑰彿锛�">
+ <el-input v-model="searchForm.batchNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :rowClassName="tableRowClassName"
+ @pagination="pagination" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import { ElMessage } from "element-plus";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import { warnPage, warnExport } from "@/api/inventoryManagement/stockInventory.js";
+
+ const tableColumn = ref([
+ { label: "浜у搧澶х被", prop: "productName", minWidth: 140 },
+ { label: "瑙勬牸鍨嬪彿", prop: "model", minWidth: 140 },
+ { label: "鎵瑰彿", prop: "batchNo", minWidth: 120 },
+ { label: "鍗曚綅", prop: "unit", width: 80 },
+ { label: "搴撳瓨鏁伴噺", prop: "qualitity", width: 110 },
+ { label: "鍐荤粨鏁伴噺", prop: "lockedQuantity", width: 110 },
+ { label: "鍙敤鏁伴噺", prop: "unLockedQuantity", width: 110 },
+ { label: "棰勮鍊�", prop: "warnNum", width: 100 },
+ { label: "澶囨敞", prop: "remark", minWidth: 140 },
+ { label: "鏈�杩戞洿鏂版椂闂�", prop: "updateTime", width: 170 },
+ ]);
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const exportLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const searchForm = reactive({
+ productName: "",
+ model: "",
+ batchNo: "",
+ });
+
+ const getUnlocked = row => row?.unLockedQuantity ?? row?.unLocked_quantity ?? 0;
+
+ const buildQuery = () => ({
+ productName: searchForm.productName || undefined,
+ model: searchForm.model || undefined,
+ batchNo: searchForm.batchNo || undefined,
+ });
+
+ const getList = () => {
+ tableLoading.value = true;
+ warnPage({
+ // 鍏煎 current/size 涓� pageNum/pageSize 涓ょ鍒嗛〉鍙傛暟
+ current: page.current,
+ size: page.size,
+ pageNum: page.current,
+ pageSize: page.size,
+ ...buildQuery(),
+ })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ page.total = res?.data?.total || 0;
+ })
+ .catch(() => {})
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const resetQuery = () => {
+ searchForm.productName = "";
+ searchForm.model = "";
+ searchForm.batchNo = "";
+ handleQuery();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ // 鍙敤鏁伴噺 <= 棰勮鍊� 涓� 棰勮鍊� > 0 鏃堕珮浜�
+ const tableRowClassName = ({ row }) => {
+ const unlocked = Number(getUnlocked(row));
+ const warn = Number(row?.warnNum ?? 0);
+ if (!Number.isFinite(unlocked) || !Number.isFinite(warn)) {
+ return "";
+ }
+ return warn > 0 && unlocked <= warn ? "row-low-stock" : "";
+ };
+
+ const handleExport = async () => {
+ exportLoading.value = true;
+ try {
+ const blobData = await warnExport(buildQuery());
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, "搴撳瓨棰勮.xlsx");
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ :deep(.row-low-stock td) {
+ background-color: #fde2e2;
+ color: #c45656;
+ }
+
+ :deep(.row-low-stock:hover > td) {
+ background-color: #fcd4d4;
+ }
+</style>
diff --git a/src/views/inventoryManagement/stockManagement/index.vue b/src/views/inventoryManagement/stockManagement/index.vue
index b3aa7ee..6a17fdb 100644
--- a/src/views/inventoryManagement/stockManagement/index.vue
+++ b/src/views/inventoryManagement/stockManagement/index.vue
@@ -8,6 +8,10 @@
:key="tab.id">
<Record :product-id="tab.id" v-if="tab.id === activeTab" />
</el-tab-pane>
+ <el-tab-pane label="搴撳瓨棰勮"
+ name="__warn__">
+ <StockWarn v-if="activeTab === '__warn__'" />
+ </el-tab-pane>
</el-tabs>
</div>
</div>
@@ -17,6 +21,7 @@
import { ref, onMounted } from 'vue';
import { productTreeList } from "@/api/basicData/product.js";
import Record from "@/views/inventoryManagement/stockManagement/Record.vue";
+import StockWarn from "@/views/inventoryManagement/stockManagement/StockWarn.vue";
const products = ref([])
const activeTab = ref(null)
const loading = ref(false)
diff --git a/src/views/procurementManagement/procurementDemand/index.vue b/src/views/procurementManagement/procurementDemand/index.vue
new file mode 100644
index 0000000..e43475f
--- /dev/null
+++ b/src/views/procurementManagement/procurementDemand/index.vue
@@ -0,0 +1,308 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="閲囪喘闇�姹�" />
+ <div class="search_form">
+ <el-form :model="queryParams"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="浜у搧鍚嶇О锛�">
+ <el-input v-model="queryParams.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿锛�">
+ <el-input v-model="queryParams.productModel"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐘舵�侊細">
+ <el-select v-model="queryParams.status"
+ placeholder="鍏ㄩ儴"
+ clearable
+ style="width: 160px"
+ @change="handleQuery">
+ <el-option label="寰呭鐞�"
+ :value="0" />
+ <el-option label="宸茶浆閲囪喘"
+ :value="1" />
+ <el-option label="宸插彇娑�"
+ :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ listPage,
+ changeStatus,
+ del,
+ exportDemand,
+ } from "@/api/procurementManagement/procurementDemand.js";
+
+ const { proxy } = getCurrentInstance();
+
+ const statusTextMap = { 0: "寰呭鐞�", 1: "宸茶浆閲囪喘", 2: "宸插彇娑�" };
+ const statusTypeMap = { 0: "warning", 1: "success", 2: "info" };
+
+ const tableColumn = ref([
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ minWidth: 140,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "productModel",
+ minWidth: 140,
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ width: 80,
+ },
+ {
+ label: "缂哄彛闇�姹傞噺",
+ prop: "demandQuantity",
+ width: 120,
+ },
+ {
+ label: "鐢熸垚鏃跺簱瀛�",
+ prop: "stockQuantity",
+ width: 120,
+ },
+ {
+ label: "鏉ユ簮BOM缂栧彿",
+ prop: "sourceBomNo",
+ minWidth: 160,
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ width: 100,
+ formatData: v => statusTextMap[v] || "寰呭鐞�",
+ formatType: v => statusTypeMap[v] || "warning",
+ },
+ {
+ label: "澶囨敞",
+ prop: "remark",
+ minWidth: 140,
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 170,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 240,
+ operation: [
+ {
+ name: "鏍囪宸茶浆閲囪喘",
+ type: "text",
+ color: "#67C23A",
+ showHide: row => row.status == 0,
+ clickFun: row => {
+ handleChangeStatus(row, 1);
+ },
+ },
+ {
+ name: "鍙栨秷",
+ type: "text",
+ color: "#E6A23C",
+ showHide: row => row.status == 0,
+ clickFun: row => {
+ handleChangeStatus(row, 2);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ showHide: row => row.status == 0,
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ]);
+
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const exportLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const queryParams = reactive({
+ productName: "",
+ productModel: "",
+ status: "",
+ });
+
+ const buildQuery = () => ({
+ productName: queryParams.productName || undefined,
+ productModel: queryParams.productModel || undefined,
+ status:
+ queryParams.status === "" ||
+ queryParams.status === null ||
+ queryParams.status === undefined
+ ? undefined
+ : queryParams.status,
+ });
+
+ // 鏌ヨ鍒楄〃
+ const getList = () => {
+ tableLoading.value = true;
+ listPage({
+ pageNum: page.current,
+ pageSize: page.size,
+ ...buildQuery(),
+ })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ page.total = res?.data?.total || 0;
+ })
+ .catch(() => {})
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const resetQuery = () => {
+ queryParams.productName = "";
+ queryParams.productModel = "";
+ queryParams.status = "";
+ handleQuery();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鐘舵�佸彉鏇�
+ const handleChangeStatus = (row, status) => {
+ const text = status == 1 ? "鏍囪涓哄凡杞噰璐�" : "鍙栨秷璇ラ噰璐渶姹�";
+ ElMessageBox.confirm(`纭${text}锛焋, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ changeStatus([row.id], status)
+ .then(() => {
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 鍒犻櫎
+ const handleDelete = row => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ラ噰璐渶姹傦紵浠呭緟澶勭悊鏁版嵁鍙垹闄ゃ��", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ del([row.id])
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 瀵煎嚭
+ const handleExport = async () => {
+ exportLoading.value = true;
+ try {
+ const blobData = await exportDemand(buildQuery());
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, "閲囪喘闇�姹�.xlsx");
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/costAccounting/index.vue b/src/views/productionManagement/costAccounting/index.vue
new file mode 100644
index 0000000..3e43100
--- /dev/null
+++ b/src/views/productionManagement/costAccounting/index.vue
@@ -0,0 +1,371 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="鎴愭湰鏍哥畻鍒嗘瀽" />
+ <div class="search_form">
+ <el-form :model="queryParams"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="璁㈠崟鏃ユ湡锛�">
+ <el-date-picker v-model="dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐢熶骇璁㈠崟鍙凤細">
+ <el-input v-model="queryParams.npsNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鎴愬搧锛�">
+ <el-input v-model="queryParams.productName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <el-tabs v-model="activeTab"
+ @tab-change="handleTabChange">
+ <!-- 鏍哥畻鏄庣粏 -->
+ <el-tab-pane label="鏍哥畻鏄庣粏"
+ name="stat">
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="statExportLoading"
+ @click="handleStatExport">瀵煎嚭</el-button>
+ </div>
+ <PIMTable rowKey="productionOrderId"
+ :column="statColumn"
+ :tableData="statData"
+ :isShowPagination="false"
+ :tableLoading="statLoading">
+ <template #laborCost="{ row }">
+ <el-tooltip v-if="row.noRateRows > 0"
+ content="瀛樺湪鏃犲伐璧勬爣鍑嗙殑鎶ュ伐璁板綍锛屼汉宸ユ垚鏈彲鑳戒笉鍑嗙‘锛岃琛ュ綍宸ユ椂鎴栫淮鎶よ浠跺崟浠�"
+ placement="top">
+ <span style="color: #e6a23c">{{ formatNum(row.laborCost) }}</span>
+ </el-tooltip>
+ <span v-else>{{ formatNum(row.laborCost) }}</span>
+ </template>
+ <template #grossProfitRate="{ row }">
+ <span :style="{ color: isNegative(row.grossProfitRate) ? '#f56c6c' : '' }">
+ {{ formatRate(row.grossProfitRate) }}
+ </span>
+ </template>
+ <template #missingPriceKinds="{ row }">
+ <span :style="{ color: row.missingPriceKinds > 0 ? '#e6a23c' : '' }">
+ {{ row.missingPriceKinds ?? 0 }}
+ </span>
+ </template>
+ </PIMTable>
+ </el-tab-pane>
+
+ <!-- 鏋勬垚姹囨�� -->
+ <el-tab-pane label="鏋勬垚姹囨��"
+ name="summary">
+ <div class="mb20"
+ style="display: flex; align-items: center; justify-content: space-between;">
+ <el-radio-group v-model="groupBy"
+ @change="getSummary">
+ <el-radio label="month">鎸夋湀浠�</el-radio>
+ <el-radio label="product">鎸夋垚鍝�</el-radio>
+ </el-radio-group>
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="summaryExportLoading"
+ @click="handleSummaryExport">瀵煎嚭</el-button>
+ </div>
+ <PIMTable rowKey="groupKey"
+ :column="summaryColumn"
+ :tableData="summaryData"
+ :isShowPagination="false"
+ :tableLoading="summaryLoading" />
+ </el-tab-pane>
+
+ <!-- 缂轰环鐗╂枡 -->
+ <el-tab-pane label="缂轰环鐗╂枡"
+ name="missing">
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="missingExportLoading"
+ @click="handleMissingExport">瀵煎嚭</el-button>
+ </div>
+ <PIMTable rowKey="productModelId"
+ :column="missingColumn"
+ :tableData="missingData"
+ :isShowPagination="false"
+ :tableLoading="missingLoading" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import { ElMessage } from "element-plus";
+ import dayjs from "dayjs";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ costStat,
+ costSummary,
+ missingPrice,
+ costStatExport,
+ costSummaryExport,
+ missingPriceExport,
+ } from "@/api/productionManagement/costAccounting.js";
+
+ // 榛樿鏌ヨ褰撳墠鏈�
+ const currentMonthRange = () => [
+ dayjs().startOf("month").format("YYYY-MM-DD"),
+ dayjs().endOf("month").format("YYYY-MM-DD"),
+ ];
+
+ const activeTab = ref("stat");
+ const dateRange = ref(currentMonthRange());
+ const queryParams = reactive({
+ npsNo: "",
+ productName: "",
+ });
+ const groupBy = ref("month");
+
+ const statColumn = ref([
+ { label: "鐢熶骇璁㈠崟鍙�", prop: "npsNo", minWidth: 150 },
+ { label: "鎴愬搧", prop: "finishedProduct", minWidth: 140 },
+ { label: "鍗曚綅", prop: "unit", width: 80 },
+ { label: "璁㈠崟鏃ユ湡", prop: "orderDate", width: 120 },
+ { label: "浜ч噺", prop: "outputQty", width: 100 },
+ { label: "鏉愭枡鎴愭湰", prop: "materialCost", width: 110 },
+ { label: "浜哄伐鎴愭湰", prop: "laborCost", width: 110, dataType: "slot", slot: "laborCost" },
+ { label: "鑳借�楁垚鏈�", prop: "energyCost", width: 110 },
+ { label: "鎬绘垚鏈�", prop: "totalCost", width: 110 },
+ { label: "鍗曚綅鎴愭湰", prop: "unitCost", width: 110 },
+ { label: "閿�鍞崟浠�", prop: "salePrice", width: 110, formatData: v => formatNum(v) },
+ { label: "姣涘埄", prop: "grossProfit", width: 110, formatData: v => formatNum(v) },
+ {
+ label: "姣涘埄鐜�(%)",
+ prop: "grossProfitRate",
+ width: 110,
+ dataType: "slot",
+ slot: "grossProfitRate",
+ },
+ { label: "浠锋牸瑕嗙洊鐜�(%)", prop: "priceCoverageRate", width: 120 },
+ {
+ label: "缂轰环鐗╂枡鏁�",
+ prop: "missingPriceKinds",
+ width: 110,
+ dataType: "slot",
+ slot: "missingPriceKinds",
+ },
+ { label: "鏃犲伐璧勬爣鍑嗚褰�", prop: "noRateRows", width: 130 },
+ ]);
+
+ const summaryColumn = ref([
+ { label: "鍒嗙粍", prop: "groupKey", minWidth: 160 },
+ { label: "璁㈠崟鏁�", prop: "orderCount", width: 100 },
+ { label: "浜ч噺", prop: "outputQty", width: 110 },
+ { label: "鏉愭枡鎴愭湰", prop: "materialCost", width: 110 },
+ { label: "鏉愭枡鍗犳瘮(%)", prop: "materialRate", width: 120 },
+ { label: "浜哄伐鎴愭湰", prop: "laborCost", width: 110 },
+ { label: "浜哄伐鍗犳瘮(%)", prop: "laborRate", width: 120 },
+ { label: "鑳借�楁垚鏈�", prop: "energyCost", width: 110 },
+ { label: "鑳借�楀崰姣�(%)", prop: "energyRate", width: 120 },
+ { label: "鎬绘垚鏈�", prop: "totalCost", width: 110 },
+ { label: "鍗曚綅鎴愭湰", prop: "unitCost", width: 110 },
+ { label: "姣涘埄", prop: "grossProfit", width: 110 },
+ { label: "姣涘埄鐜�(%)", prop: "grossProfitRate", width: 110 },
+ ]);
+
+ const missingColumn = ref([
+ { label: "鐗╂枡鍚嶇О", prop: "materialName", minWidth: 160 },
+ { label: "瑙勬牸鍨嬪彿", prop: "materialModel", minWidth: 160 },
+ { label: "鍗曚綅", prop: "unit", width: 80 },
+ { label: "鎶曞叆鏁伴噺", prop: "inputQty", width: 120 },
+ { label: "娑夊強璁㈠崟鏁�", prop: "orderCount", width: 110 },
+ { label: "鏈�杩戞姇鍏ユ椂闂�", prop: "lastInputTime", width: 170 },
+ ]);
+
+ const statData = ref([]);
+ const statLoading = ref(false);
+ const statExportLoading = ref(false);
+ const summaryData = ref([]);
+ const summaryLoading = ref(false);
+ const summaryExportLoading = ref(false);
+ const missingData = ref([]);
+ const missingLoading = ref(false);
+ const missingExportLoading = ref(false);
+
+ const buildQuery = () => ({
+ startDate: dateRange.value?.[0] || undefined,
+ endDate: dateRange.value?.[1] || undefined,
+ npsNo: queryParams.npsNo || undefined,
+ productName: queryParams.productName || undefined,
+ });
+
+ const buildSummaryQuery = () => ({
+ groupBy: groupBy.value,
+ ...buildQuery(),
+ });
+
+ const formatNum = v => {
+ if (v === null || v === undefined || v === "") {
+ return "鈥�";
+ }
+ return v;
+ };
+
+ const isNegative = v => {
+ const num = Number(v);
+ return Number.isFinite(num) && num < 0;
+ };
+
+ const formatRate = v => {
+ if (v === null || v === undefined || v === "") {
+ return "鈥�";
+ }
+ return v;
+ };
+
+ const getStat = () => {
+ statLoading.value = true;
+ costStat(buildQuery())
+ .then(res => {
+ statData.value = res?.data || [];
+ })
+ .catch(() => {})
+ .finally(() => {
+ statLoading.value = false;
+ });
+ };
+
+ const getSummary = () => {
+ summaryLoading.value = true;
+ costSummary(buildSummaryQuery())
+ .then(res => {
+ summaryData.value = res?.data || [];
+ })
+ .catch(() => {})
+ .finally(() => {
+ summaryLoading.value = false;
+ });
+ };
+
+ const getMissing = () => {
+ missingLoading.value = true;
+ missingPrice(buildQuery())
+ .then(res => {
+ missingData.value = res?.data || [];
+ })
+ .catch(() => {})
+ .finally(() => {
+ missingLoading.value = false;
+ });
+ };
+
+ const loadCurrentTab = () => {
+ if (activeTab.value === "stat") {
+ getStat();
+ } else if (activeTab.value === "summary") {
+ getSummary();
+ } else {
+ getMissing();
+ }
+ };
+
+ const handleQuery = () => {
+ loadCurrentTab();
+ };
+
+ const resetQuery = () => {
+ dateRange.value = currentMonthRange();
+ queryParams.npsNo = "";
+ queryParams.productName = "";
+ loadCurrentTab();
+ };
+
+ const handleTabChange = () => {
+ loadCurrentTab();
+ };
+
+ const exportBlob = async (requestFn, query, filename) => {
+ const blobData = await requestFn(query);
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, filename);
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ };
+
+ const handleStatExport = async () => {
+ statExportLoading.value = true;
+ try {
+ await exportBlob(costStatExport, buildQuery(), "鎴愭湰鏍哥畻鏄庣粏.xlsx");
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ statExportLoading.value = false;
+ }
+ };
+
+ const handleSummaryExport = async () => {
+ summaryExportLoading.value = true;
+ try {
+ await exportBlob(costSummaryExport, buildSummaryQuery(), "鎴愭湰鏋勬垚姹囨��.xlsx");
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ summaryExportLoading.value = false;
+ }
+ };
+
+ const handleMissingExport = async () => {
+ missingExportLoading.value = true;
+ try {
+ await exportBlob(missingPriceExport, buildQuery(), "缂轰环鐗╂枡娓呭崟.xlsx");
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ missingExportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getStat();
+ });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/materialConsumption/index.vue b/src/views/productionManagement/materialConsumption/index.vue
new file mode 100644
index 0000000..c46f197
--- /dev/null
+++ b/src/views/productionManagement/materialConsumption/index.vue
@@ -0,0 +1,199 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="鏉愭枡娑堣�楁牳绠�" />
+ <div class="search_form">
+ <el-form :model="queryParams"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="璁㈠崟鏃ユ湡锛�">
+ <el-date-picker v-model="dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐢熶骇璁㈠崟鍙凤細">
+ <el-input v-model="queryParams.npsNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鎴愬搧锛�">
+ <el-input v-model="queryParams.productName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鍙湅宸紓锛�">
+ <el-switch v-model="queryParams.onlyDiff"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="rowKey"
+ :column="tableColumn"
+ :tableData="tableData"
+ :isShowPagination="false"
+ :tableLoading="tableLoading">
+ <template #diffRate="{ row }">
+ <span :style="{ color: diffRateColor(row.diffRate) }">
+ {{ formatDiffRate(row.diffRate) }}
+ </span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+ import { ElMessage } from "element-plus";
+ import dayjs from "dayjs";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import { stat, statExport } from "@/api/productionManagement/materialConsumption.js";
+
+ const { proxy } = getCurrentInstance();
+
+ const tableColumn = ref([
+ { label: "鐢熶骇璁㈠崟鍙�", prop: "npsNo", minWidth: 150 },
+ { label: "鎴愬搧", prop: "finishedProduct", minWidth: 140 },
+ { label: "鐗╂枡鍚嶇О", prop: "materialName", minWidth: 140 },
+ { label: "瑙勬牸鍨嬪彿", prop: "materialModel", minWidth: 140 },
+ { label: "鍗曚綅", prop: "unit", width: 80 },
+ { label: "BOM鍗曡��", prop: "unitQuantity", width: 110 },
+ { label: "瀹屽伐鏁伴噺", prop: "completeQty", width: 110 },
+ { label: "鏍囧噯娑堣��", prop: "standardQty", width: 110 },
+ { label: "瀹為檯娑堣��", prop: "actualQty", width: 110 },
+ { label: "宸紓鏁伴噺", prop: "diffQty", width: 110 },
+ {
+ label: "宸紓鐜�(%)",
+ prop: "diffRate",
+ width: 110,
+ dataType: "slot",
+ slot: "diffRate",
+ },
+ ]);
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const exportLoading = ref(false);
+ // 榛樿鏌ヨ褰撳墠鏈�
+ const currentMonthRange = () => [
+ dayjs().startOf("month").format("YYYY-MM-DD"),
+ dayjs().endOf("month").format("YYYY-MM-DD"),
+ ];
+
+ const dateRange = ref(currentMonthRange());
+ const queryParams = reactive({
+ npsNo: "",
+ productName: "",
+ onlyDiff: false,
+ });
+
+ const buildQuery = () => ({
+ startDate: dateRange.value?.[0] || undefined,
+ endDate: dateRange.value?.[1] || undefined,
+ npsNo: queryParams.npsNo || undefined,
+ productName: queryParams.productName || undefined,
+ onlyDiff: queryParams.onlyDiff ? true : undefined,
+ });
+
+ const formatDiffRate = rate => {
+ if (rate === null || rate === undefined || rate === "") {
+ return "-";
+ }
+ return rate;
+ };
+
+ const diffRateColor = rate => {
+ const num = Number(rate);
+ if (rate === null || rate === undefined || rate === "" || !Number.isFinite(num)) {
+ return "";
+ }
+ return Math.abs(num) > 10 ? "#F56C6C" : "";
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ stat(buildQuery())
+ .then(res => {
+ const list = res?.data || [];
+ tableData.value = list.map((item, index) => ({
+ ...item,
+ rowKey: `${item.productionOrderId}_${item.materialModelId}_${index}`,
+ }));
+ })
+ .catch(() => {})
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ getList();
+ };
+
+ const resetQuery = () => {
+ dateRange.value = currentMonthRange();
+ queryParams.npsNo = "";
+ queryParams.productName = "";
+ queryParams.onlyDiff = false;
+ getList();
+ };
+
+ const handleExport = async () => {
+ exportLoading.value = true;
+ try {
+ const blobData = await statExport(buildQuery());
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, "鏉愭枡娑堣�楁牳绠�.xlsx");
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/outputStatistics/index.vue b/src/views/productionManagement/outputStatistics/index.vue
new file mode 100644
index 0000000..5a7cd7f
--- /dev/null
+++ b/src/views/productionManagement/outputStatistics/index.vue
@@ -0,0 +1,350 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="鐢熶骇缁熻" />
+ <el-tabs v-model="activeTab"
+ @tab-change="handleTabChange">
+ <!-- 璁′欢宸ヨ祫姹囨�� -->
+ <el-tab-pane label="璁′欢宸ヨ祫姹囨��"
+ name="wage">
+ <div class="search_form">
+ <el-form :model="wageQuery"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="鏃ユ湡鑼冨洿锛�">
+ <el-date-picker v-model="wageDateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ @change="handleWageQuery" />
+ </el-form-item>
+ <el-form-item label="浜哄憳濮撳悕锛�">
+ <el-input v-model="wageQuery.userName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleWageQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleWageQuery">鏌ヨ</el-button>
+ <el-button @click="resetWageQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="wageExportLoading"
+ @click="handleWageExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="userId"
+ :column="wageColumn"
+ :tableData="wageData"
+ :isShowPagination="false"
+ :tableLoading="wageLoading" />
+ </div>
+ </el-tab-pane>
+
+ <!-- 浜ч噺缁熻 -->
+ <el-tab-pane label="浜ч噺缁熻"
+ name="output">
+ <div class="search_form">
+ <el-form :model="outputQuery"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="缁熻缁村害锛�">
+ <el-radio-group v-model="outputQuery.dimension"
+ @change="handleDimensionChange">
+ <el-radio label="device">璁惧</el-radio>
+ <el-radio label="operation">宸ュ簭</el-radio>
+ <el-radio label="product">浜у搧</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏃ユ湡鑼冨洿锛�">
+ <el-date-picker v-model="outputDateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ @change="handleOutputQuery" />
+ </el-form-item>
+ <el-form-item v-if="outputQuery.dimension === 'device'"
+ label="璁惧锛�">
+ <el-select v-model="outputQuery.deviceId"
+ placeholder="鍏ㄩ儴"
+ clearable
+ filterable
+ style="width: 200px"
+ @change="handleOutputQuery">
+ <el-option v-for="item in deviceOptions"
+ :key="item.id"
+ :label="item.deviceName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleOutputQuery">鏌ヨ</el-button>
+ <el-button @click="resetOutputQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="outputExportLoading"
+ @click="handleOutputExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="groupId"
+ :column="outputColumn"
+ :tableData="outputData"
+ :isShowPagination="false"
+ :tableLoading="outputLoading" />
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+ import { ElMessage } from "element-plus";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ wageSummary,
+ wageSummaryExport,
+ outputStat,
+ outputStatExport,
+ } from "@/api/productionManagement/productionStat.js";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
+
+ const { proxy } = getCurrentInstance();
+
+ const activeTab = ref("wage");
+ const deviceOptions = ref([]);
+
+ const wageColumn = ref([
+ {
+ label: "鐢熶骇浜�",
+ prop: "userName",
+ minWidth: 120,
+ },
+ {
+ label: "鎶ュ伐鍗曟暟",
+ prop: "workCount",
+ width: 110,
+ },
+ {
+ label: "鐢熶骇鏁伴噺",
+ prop: "quantity",
+ width: 120,
+ },
+ {
+ label: "鎬诲伐鏃�",
+ prop: "workHour",
+ width: 110,
+ },
+ {
+ label: "璁′欢宸ヨ祫",
+ prop: "pieceWage",
+ width: 120,
+ },
+ {
+ label: "搴斿彂鍚堣",
+ prop: "wages",
+ width: 120,
+ },
+ ]);
+
+ const outputColumn = ref([
+ {
+ label: "鍒嗙粍鍚嶇О",
+ prop: "groupName",
+ minWidth: 160,
+ },
+ {
+ label: "鎶ュ伐鍗曟暟",
+ prop: "workCount",
+ width: 110,
+ },
+ {
+ label: "浜ч噺",
+ prop: "quantity",
+ width: 120,
+ },
+ {
+ label: "鎶ュ簾鏁伴噺",
+ prop: "scrapQty",
+ width: 120,
+ },
+ {
+ label: "宸ユ椂",
+ prop: "workHour",
+ width: 110,
+ },
+ ]);
+
+ const wageData = ref([]);
+ const wageLoading = ref(false);
+ const wageExportLoading = ref(false);
+ const wageDateRange = ref([]);
+ const wageQuery = reactive({
+ userName: "",
+ });
+
+ const outputData = ref([]);
+ const outputLoading = ref(false);
+ const outputExportLoading = ref(false);
+ const outputDateRange = ref([]);
+ const outputQuery = reactive({
+ dimension: "device",
+ deviceId: "",
+ });
+
+ const buildWageQuery = () => ({
+ startDate: wageDateRange.value?.[0] || undefined,
+ endDate: wageDateRange.value?.[1] || undefined,
+ userName: wageQuery.userName || undefined,
+ });
+
+ const buildOutputQuery = () => ({
+ dimension: outputQuery.dimension,
+ startDate: outputDateRange.value?.[0] || undefined,
+ endDate: outputDateRange.value?.[1] || undefined,
+ deviceId:
+ outputQuery.dimension === "device" && outputQuery.deviceId
+ ? outputQuery.deviceId
+ : undefined,
+ });
+
+ // 璁′欢宸ヨ祫姹囨��
+ const getWageList = () => {
+ wageLoading.value = true;
+ wageSummary(buildWageQuery())
+ .then(res => {
+ wageData.value = res?.data || [];
+ })
+ .catch(() => {})
+ .finally(() => {
+ wageLoading.value = false;
+ });
+ };
+
+ const handleWageQuery = () => {
+ getWageList();
+ };
+
+ const resetWageQuery = () => {
+ wageQuery.userName = "";
+ wageDateRange.value = [];
+ getWageList();
+ };
+
+ // 浜ч噺缁熻
+ const getOutputList = () => {
+ outputLoading.value = true;
+ outputStat(buildOutputQuery())
+ .then(res => {
+ outputData.value = res?.data || [];
+ })
+ .catch(() => {})
+ .finally(() => {
+ outputLoading.value = false;
+ });
+ };
+
+ const handleOutputQuery = () => {
+ getOutputList();
+ };
+
+ const resetOutputQuery = () => {
+ outputQuery.dimension = "device";
+ outputQuery.deviceId = "";
+ outputDateRange.value = [];
+ getOutputList();
+ };
+
+ const handleDimensionChange = () => {
+ outputQuery.deviceId = "";
+ getOutputList();
+ };
+
+ const handleTabChange = name => {
+ if (name === "wage") {
+ getWageList();
+ } else {
+ getOutputList();
+ }
+ };
+
+ // 瀵煎嚭
+ const exportBlob = async (requestFn, query, filename) => {
+ const blobData = await requestFn(query);
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, filename);
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ };
+
+ const handleWageExport = async () => {
+ wageExportLoading.value = true;
+ try {
+ await exportBlob(wageSummaryExport, buildWageQuery(), "璁′欢宸ヨ祫姹囨��.xlsx");
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ wageExportLoading.value = false;
+ }
+ };
+
+ const handleOutputExport = async () => {
+ outputExportLoading.value = true;
+ try {
+ await exportBlob(outputStatExport, buildOutputQuery(), "浜ч噺缁熻.xlsx");
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ outputExportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getWageList();
+ getDeviceLedger()
+ .then(res => {
+ deviceOptions.value = res.data || [];
+ })
+ .catch(err => {
+ console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+ deviceOptions.value = [];
+ });
+ });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/pieceRateConfig/index.vue b/src/views/productionManagement/pieceRateConfig/index.vue
new file mode 100644
index 0000000..16b0691
--- /dev/null
+++ b/src/views/productionManagement/pieceRateConfig/index.vue
@@ -0,0 +1,450 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="璁′欢鍗曚环閰嶇疆" />
+ <div class="search_form">
+ <el-form :model="queryParams"
+ inline
+ style="margin-bottom: 0;">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�">
+ <el-input v-model="queryParams.productModel"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭鍚嶇О锛�">
+ <el-input v-model="queryParams.operationName"
+ placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐘舵�侊細">
+ <el-select v-model="queryParams.status"
+ placeholder="鍏ㄩ儴"
+ clearable
+ style="width: 160px"
+ @change="handleQuery">
+ <el-option label="鍚敤"
+ :value="1" />
+ <el-option label="鍋滅敤"
+ :value="0" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20"
+ style="text-align: right;">
+ <el-button type="primary"
+ @click="openDialog('add')">鏂板</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleBatchDelete">鍒犻櫎</el-button>
+ <el-button type="warning"
+ plain
+ icon="Download"
+ :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination" />
+ </div>
+
+ <el-dialog v-model="dialogVisible"
+ :title="operationType === 'add' ? '鏂板璁′欢鍗曚环' : '缂栬緫璁′欢鍗曚环'"
+ width="480px"
+ @close="closeDialog">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�"
+ prop="productModel">
+ <el-input v-model="form.productModel"
+ placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+ readonly
+ @click="openProductSelect">
+ <template #append>
+ <el-button icon="Search"
+ @click="openProductSelect" />
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="宸ュ簭鍚嶇О锛�"
+ prop="operationName">
+ <el-input v-model="form.operationName"
+ placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+ clearable />
+ </el-form-item>
+ <el-form-item label="璁′欢鍗曚环锛�"
+ prop="unitPrice">
+ <el-input-number v-model="form.unitPrice"
+ :min="0.0001"
+ :precision="4"
+ :controls="false"
+ placeholder="璇疯緭鍏ヨ浠跺崟浠�"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="澶囨敞锛�">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <ProductSelectDialog v-model="productSelectVisible"
+ single
+ @confirm="handleProductSelected" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ listPage,
+ add,
+ update,
+ changeStatus,
+ batchDelete,
+ exportConfig,
+ } from "@/api/productionManagement/pieceRateConfig.js";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+
+ const { proxy } = getCurrentInstance();
+
+ const tableColumn = ref([
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "productModel",
+ minWidth: 160,
+ },
+ {
+ label: "宸ュ簭鍚嶇О",
+ prop: "operationName",
+ minWidth: 140,
+ },
+ {
+ label: "璁′欢鍗曚环",
+ prop: "unitPrice",
+ width: 120,
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ width: 100,
+ formatData: v => (v == 1 ? "鍚敤" : "鍋滅敤"),
+ formatType: v => (v == 1 ? "success" : "info"),
+ },
+ {
+ label: "澶囨敞",
+ prop: "remark",
+ minWidth: 140,
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 170,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 200,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openDialog("edit", row);
+ },
+ },
+ {
+ name: "鍚敤",
+ type: "text",
+ color: "#67C23A",
+ showHide: row => row.status != 1,
+ clickFun: row => {
+ handleChangeStatus(row, 1);
+ },
+ },
+ {
+ name: "鍋滅敤",
+ type: "text",
+ color: "#909399",
+ showHide: row => row.status == 1,
+ clickFun: row => {
+ handleChangeStatus(row, 0);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ]);
+
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const exportLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const queryParams = reactive({
+ productModel: "",
+ operationName: "",
+ status: "",
+ });
+
+ const dialogVisible = ref(false);
+ const operationType = ref("add");
+ const formRef = ref(null);
+ const productSelectVisible = ref(false);
+ const form = reactive({
+ id: "",
+ productModelId: "",
+ productModel: "",
+ operationName: "",
+ unitPrice: undefined,
+ remark: "",
+ });
+ const rules = {
+ productModel: [{ required: true, message: "璇烽�夋嫨瑙勬牸鍨嬪彿", trigger: "change" }],
+ operationName: [{ required: true, message: "璇疯緭鍏ュ伐搴忓悕绉�", trigger: "blur" }],
+ unitPrice: [{ required: true, message: "璇疯緭鍏ヨ浠跺崟浠�", trigger: "blur" }],
+ };
+
+ const buildQuery = () => ({
+ productModel: queryParams.productModel || undefined,
+ operationName: queryParams.operationName || undefined,
+ status:
+ queryParams.status === "" ||
+ queryParams.status === null ||
+ queryParams.status === undefined
+ ? undefined
+ : queryParams.status,
+ });
+
+ // 鏌ヨ鍒楄〃
+ const getList = () => {
+ tableLoading.value = true;
+ listPage({
+ pageNum: page.current,
+ pageSize: page.size,
+ ...buildQuery(),
+ })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ page.total = res?.data?.total || 0;
+ })
+ .catch(() => {})
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const resetQuery = () => {
+ queryParams.productModel = "";
+ queryParams.operationName = "";
+ queryParams.status = "";
+ handleQuery();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ if (type === "edit" && row) {
+ form.id = row.id;
+ form.productModelId = row.productModelId;
+ form.productModel = row.productModel;
+ form.operationName = row.operationName;
+ form.unitPrice = row.unitPrice;
+ form.remark = row.remark;
+ } else {
+ form.id = "";
+ form.productModelId = "";
+ form.productModel = "";
+ form.operationName = "";
+ form.unitPrice = undefined;
+ form.remark = "";
+ }
+ dialogVisible.value = true;
+ };
+
+ const closeDialog = () => {
+ formRef.value?.resetFields();
+ dialogVisible.value = false;
+ };
+
+ const openProductSelect = () => {
+ productSelectVisible.value = true;
+ };
+
+ const handleProductSelected = rows => {
+ const row = rows && rows[0];
+ if (row) {
+ form.productModelId = row.id;
+ form.productModel = row.model;
+ }
+ };
+
+ // 鎻愪氦
+ const submitForm = () => {
+ formRef.value?.validate(valid => {
+ if (!valid) {
+ return;
+ }
+ const submitData = {
+ id: form.id,
+ productModelId: form.productModelId,
+ operationName: form.operationName,
+ unitPrice: form.unitPrice,
+ remark: form.remark,
+ };
+ const request = operationType.value === "edit" ? update : add;
+ request(submitData)
+ .then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDialog();
+ getList();
+ })
+ .catch(() => {});
+ });
+ };
+
+ // 鍚敤/鍋滅敤
+ const handleChangeStatus = (row, status) => {
+ const text = status == 1 ? "鍚敤" : "鍋滅敤";
+ ElMessageBox.confirm(`纭${text}璇ヨ浠跺崟浠凤紵`, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ changeStatus({ id: row.id, status })
+ .then(() => {
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 鍒犻櫎
+ const handleDelete = row => {
+ removeRows([row.id]);
+ };
+
+ const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ removeRows(selectedRows.value.map(item => item.id));
+ };
+
+ const removeRows = ids => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchDelete(ids)
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 瀵煎嚭
+ const handleExport = async () => {
+ exportLoading.value = true;
+ try {
+ const blobData = await exportConfig(buildQuery());
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, "璁′欢鍗曚环閰嶇疆.xlsx");
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/productStructure/KitCheckDialog.vue b/src/views/productionManagement/productStructure/KitCheckDialog.vue
new file mode 100644
index 0000000..d358737
--- /dev/null
+++ b/src/views/productionManagement/productStructure/KitCheckDialog.vue
@@ -0,0 +1,216 @@
+<template>
+ <el-dialog v-model="visible"
+ title="BOM榻愬鍒嗘瀽"
+ width="900px"
+ @close="handleClose">
+ <div class="bom-info">
+ <span>BOM缂栧彿锛歿{ bom.bomNo || '-' }}</span>
+ <span>浜у搧鍚嶇О锛歿{ bom.productName || '-' }}</span>
+ <span>瑙勬牸鍨嬪彿锛歿{ bom.productModelName || '-' }}</span>
+ </div>
+
+ <el-form :inline="true"
+ @submit.prevent>
+ <el-form-item label="闇�姹傛暟閲�">
+ <el-input-number v-model="demandQty"
+ :min="0"
+ :precision="4"
+ :step="1"
+ :controls="false"
+ placeholder="璇疯緭鍏ラ渶姹傛暟閲�"
+ style="width: 180px" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ :loading="loading"
+ @click="handleAnalyze">寮�濮嬪垎鏋�</el-button>
+ <el-button :disabled="!resultList.length"
+ :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="success"
+ :disabled="!resultList.length"
+ :loading="genLoading"
+ @click="handleGenerate">鐢熸垚閲囪喘闇�姹�</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-table v-loading="loading"
+ :data="resultList"
+ border
+ size="small"
+ max-height="420"
+ style="width: 100%">
+ <el-table-column type="index"
+ label="搴忓彿"
+ width="60"
+ align="center" />
+ <el-table-column label="鐗╂枡鍚嶇О"
+ prop="productName"
+ min-width="140" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model"
+ min-width="140" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="80"
+ align="center" />
+ <el-table-column label="闇�姹傛暟閲�"
+ prop="requiredQty"
+ width="110"
+ align="right" />
+ <el-table-column label="鍙敤搴撳瓨"
+ prop="availableQty"
+ width="110"
+ align="right" />
+ <el-table-column label="缂哄彛鏁伴噺"
+ prop="shortageQty"
+ width="110"
+ align="right">
+ <template #default="{ row }">
+ <span :class="{ shortage: Number(row.shortageQty) > 0 }">{{ row.shortageQty }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { ref, computed, getCurrentInstance } from "vue";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import {
+ kitCheck,
+ kitCheckExport,
+ generateDemand,
+ } from "@/api/productionManagement/productBom.js";
+
+ const props = defineProps({
+ showModel: {
+ type: Boolean,
+ default: false,
+ },
+ bom: {
+ type: Object,
+ default: () => ({}),
+ },
+ });
+
+ const emits = defineEmits(["update:showModel"]);
+ const { proxy } = getCurrentInstance();
+
+ const visible = computed({
+ get() {
+ return props.showModel;
+ },
+ set(val) {
+ emits("update:showModel", val);
+ },
+ });
+
+ const demandQty = ref(undefined);
+ const resultList = ref([]);
+ const loading = ref(false);
+ const exportLoading = ref(false);
+ const genLoading = ref(false);
+
+ // 鏍¢獙骞剁粍瑁呰姹傚弬鏁�
+ const buildParams = () => {
+ if (!props.bom?.id) {
+ proxy.$modal.msgWarning("鏈幏鍙栧埌BOM淇℃伅锛岃閲嶈瘯");
+ return null;
+ }
+ const qty = Number(demandQty.value);
+ if (!qty || qty <= 0) {
+ proxy.$modal.msgWarning("璇疯緭鍏ュぇ浜�0鐨勯渶姹傛暟閲�");
+ return null;
+ }
+ return { bomId: props.bom.id, demandQty: qty };
+ };
+
+ // 寮�濮嬪垎鏋�
+ const handleAnalyze = () => {
+ const params = buildParams();
+ if (!params) return;
+ loading.value = true;
+ kitCheck(params)
+ .then(res => {
+ resultList.value = res?.data || [];
+ if (!resultList.value.length) {
+ proxy.$modal.msgWarning("璇OM涓嬫棤鐗╂枡鑺傜偣");
+ }
+ })
+ .catch(() => {})
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ // 瀵煎嚭
+ const handleExport = async () => {
+ const params = buildParams();
+ if (!params) return;
+ exportLoading.value = true;
+ try {
+ const blobData = await kitCheckExport(params);
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ proxy.$modal.msgWarning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ saveAs(blob, `BOM榻愬鍒嗘瀽_${props.bom.bomNo || props.bom.id}.xlsx`);
+ proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ proxy.$modal.msgError(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ proxy.$modal.msgError("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ // 缂哄彛鐢熸垚閲囪喘闇�姹�
+ const handleGenerate = () => {
+ const params = buildParams();
+ if (!params) return;
+ genLoading.value = true;
+ generateDemand(params)
+ .then(res => {
+ proxy.$modal.msgSuccess(res?.data || "鐢熸垚瀹屾垚");
+ })
+ .catch(() => {})
+ .finally(() => {
+ genLoading.value = false;
+ });
+ };
+
+ const handleClose = () => {
+ demandQty.value = undefined;
+ resultList.value = [];
+ };
+</script>
+
+<style scoped lang="scss">
+ .bom-info {
+ display: flex;
+ gap: 24px;
+ margin-bottom: 16px;
+ font-size: 13px;
+ color: #606266;
+ }
+
+ .shortage {
+ color: #d93025;
+ font-weight: 600;
+ }
+</style>
diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
index 269f098..d99fb48 100644
--- a/src/views/productionManagement/productStructure/index.vue
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -63,6 +63,9 @@
<StructureEdit v-if="showEdit"
v-model:show-model="showEdit"
:record="currentRow" />
+ <!-- 榻愬鍒嗘瀽寮圭獥 -->
+ <KitCheckDialog v-model:show-model="kitCheckVisible"
+ :bom="kitCheckRow" />
<!-- 鏂板/缂栬緫寮圭獥 -->
<el-dialog v-model="dialogVisible"
:title="operationType === 'add' ? '鏂板BOM' : '缂栬緫BOM'"
@@ -138,11 +141,13 @@
batchDelete,
exportBom,
downloadTemplate,
+ changeStatus,
} from "@/api/productionManagement/productBom.js";
import { useRouter } from "vue-router";
import { ElMessageBox } from "element-plus";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
import ImportDialog from "@/components/Dialog/ImportDialog.vue";
+ import KitCheckDialog from "@/views/productionManagement/productStructure/KitCheckDialog.vue";
const router = useRouter();
const { proxy } = getCurrentInstance();
@@ -175,6 +180,14 @@
width: 100,
},
{
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ width: 90,
+ formatData: v => (v == 1 ? "鍚敤" : "鍋滅敤"),
+ formatType: v => (v == 1 ? "success" : "info"),
+ },
+ {
label: "澶囨敞",
prop: "remark",
minWidth: 160,
@@ -184,7 +197,7 @@
label: "鎿嶄綔",
align: "center",
fixed: "right",
- width: 250,
+ width: 330,
operation: [
{
name: "澶嶅埗",
@@ -198,6 +211,31 @@
type: "text",
clickFun: row => {
handleEdit(row);
+ },
+ },
+ {
+ name: "榻愬鍒嗘瀽",
+ type: "text",
+ clickFun: row => {
+ handleKitCheck(row);
+ },
+ },
+ {
+ name: "鍚敤",
+ type: "text",
+ color: "#67C23A",
+ showHide: row => row.status != 1,
+ clickFun: row => {
+ handleChangeStatus(row, 1);
+ },
+ },
+ {
+ name: "鍋滅敤",
+ type: "text",
+ color: "#909399",
+ showHide: row => row.status == 1,
+ clickFun: row => {
+ handleChangeStatus(row, 0);
},
},
{
@@ -215,6 +253,8 @@
const tableData = ref([]);
const tableLoading = ref(false);
const showEdit = ref(false);
+ const kitCheckVisible = ref(false);
+ const kitCheckRow = ref({});
const selectedRows = ref([]);
const currentRow = ref({});
const dialogVisible = ref(false);
@@ -345,6 +385,34 @@
.catch(() => {});
};
+ // 鍚敤/鍋滅敤
+ const handleChangeStatus = (row, status) => {
+ const tip =
+ status == 1
+ ? "纭鍚敤璇OM鐗堟湰锛熷悓浜у搧瑙勬牸鐨勫叾浠栧惎鐢ㄧ増鏈皢鑷姩鍋滅敤銆�"
+ : "纭鍋滅敤璇OM鐗堟湰锛�";
+ ElMessageBox.confirm(tip, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ changeStatus({ id: row.id, status })
+ .then(() => {
+ proxy.$modal.msgSuccess(status == 1 ? "鍚敤鎴愬姛" : "鍋滅敤鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ };
+
+ // 榻愬鍒嗘瀽
+ const handleKitCheck = row => {
+ kitCheckRow.value = row;
+ kitCheckVisible.value = true;
+ };
+
// 缂栬緫
const handleEdit = row => {
operationType.value = "edit";
diff --git a/src/views/productionManagement/productionCosting/index.vue b/src/views/productionManagement/productionCosting/index.vue
index 3e79b93..dfa51a3 100644
--- a/src/views/productionManagement/productionCosting/index.vue
+++ b/src/views/productionManagement/productionCosting/index.vue
@@ -125,6 +125,11 @@
minWidth: 100,
},
{
+ label: "浣跨敤璁惧",
+ prop: "deviceName",
+ minWidth: 100,
+ },
+ {
label: "宸ユ椂锛坔锛�",
prop: "workHour",
minWidth: 100,
@@ -140,6 +145,14 @@
minWidth: 100,
},
{
+ label: "璁¤柂鏂瑰紡",
+ prop: "wageMode",
+ minWidth: 100,
+ dataType: "tag",
+ formatData: v => (v === "璁′欢" ? "璁′欢" : "璁℃椂"),
+ formatType: v => (v === "璁′欢" ? "success" : "info"),
+ },
+ {
label: "宸ヨ祫",
prop: "wages",
minWidth: 100,
diff --git a/src/views/productionManagement/productionReporting/index.vue b/src/views/productionManagement/productionReporting/index.vue
index 28edda2..64e6355 100644
--- a/src/views/productionManagement/productionReporting/index.vue
+++ b/src/views/productionManagement/productionReporting/index.vue
@@ -19,6 +19,19 @@
style="width: 200px;"
@change="handleQuery" />
</el-form-item>
+ <el-form-item label="浣跨敤璁惧:">
+ <el-select v-model="searchForm.deviceId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ style="width: 200px;"
+ @change="handleQuery">
+ <el-option v-for="item in deviceOptions"
+ :key="item.id"
+ :label="item.deviceName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
<el-form-item>
<el-button type="primary"
@click="handleQuery">鎼滅储</el-button>
@@ -162,6 +175,7 @@
} from "@/api/productionManagement/productionReporting.js";
import { productionProductMainListPage } from "@/api/productionManagement/productionProductMain.js";
import { userListNoPageByTenantId } from "@/api/system/user.js";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
import InputModal from "@/views/productionManagement/productionReporting/Input.vue";
const data = reactive({
@@ -169,12 +183,14 @@
nickName: "",
workOrderNo: "",
workOrderStatus: "",
+ deviceId: "",
},
});
const { searchForm } = toRefs(data);
const expandedRowKeys = ref([]);
const expandData = ref([]);
const userList = ref([]);
+ const deviceOptions = ref([]);
const tableColumn = ref([
{
label: "鎶ュ伐鍗曞彿",
@@ -195,6 +211,11 @@
label: "宸ュ簭",
prop: "process",
width: 120,
+ },
+ {
+ label: "浣跨敤璁惧",
+ prop: "deviceName",
+ width: 140,
},
{
label: "宸ュ崟缂栧彿",
@@ -459,6 +480,14 @@
};
onMounted(() => {
getList();
+ getDeviceLedger()
+ .then(res => {
+ deviceOptions.value = res.data || [];
+ })
+ .catch(err => {
+ console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+ deviceOptions.value = [];
+ });
});
</script>
diff --git a/src/views/productionManagement/productionTraceability/index.vue b/src/views/productionManagement/productionTraceability/index.vue
index 39db220..ec630bc 100644
--- a/src/views/productionManagement/productionTraceability/index.vue
+++ b/src/views/productionManagement/productionTraceability/index.vue
@@ -166,6 +166,9 @@
<el-table-column label="鍒涘缓浜�"
prop="userName"
align="center" />
+ <el-table-column label="浣跨敤璁惧"
+ prop="deviceName"
+ align="center" />
<el-table-column label="鍒涘缓鏃堕棿"
align="center">
<template #default="{ row }">
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
index d3b0f93..9ecf945 100644
--- a/src/views/productionManagement/workOrder/index.vue
+++ b/src/views/productionManagement/workOrder/index.vue
@@ -209,6 +209,18 @@
:value="user.userId" />
</el-select>
</el-form-item>
+ <el-form-item label="浣跨敤璁惧">
+ <el-select v-model="reportForm.deviceId"
+ style="width: 300px"
+ placeholder="璇烽�夋嫨浣跨敤璁惧"
+ clearable
+ filterable>
+ <el-option v-for="item in deviceOptions"
+ :key="item.id"
+ :label="item.deviceName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -233,6 +245,7 @@
downProductWorkOrder,
} from "@/api/productionManagement/workOrder.js";
import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
import QRCode from "qrcode";
import { getCurrentInstance, reactive, toRefs } from "vue";
import FilesDia from "./components/filesDia.vue";
@@ -356,6 +369,7 @@
const workOrderFilesRef = ref(null);
const reportFormRef = ref(null);
const userOptions = ref([]);
+ const deviceOptions = ref([]);
const reportForm = reactive({
planQuantity: 0,
quantity: null,
@@ -365,6 +379,7 @@
reportWork: "",
productProcessRouteItemId: "",
userId: "",
+ deviceId: "",
productMainId: null,
});
@@ -596,6 +611,7 @@
const showReportDialog = row => {
currentReportRowData.value = row;
+ reportForm.deviceId = "";
reportForm.planQuantity = row.planQuantity - row.completeQuantity;
reportForm.quantity =
row.quantity !== undefined && row.quantity !== null ? row.quantity : null;
@@ -729,9 +745,22 @@
}
};
+ // 鑾峰彇璁惧涓嬫媺
+ const getDeviceOptions = () => {
+ getDeviceLedger()
+ .then(res => {
+ deviceOptions.value = res.data || [];
+ })
+ .catch(err => {
+ console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+ deviceOptions.value = [];
+ });
+ };
+
onMounted(() => {
getList();
getUserList();
+ getDeviceOptions();
});
</script>
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 06b1dd9..723e013 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -168,6 +168,18 @@
:value="user.userId" />
</el-select>
</el-form-item>
+ <el-form-item label="浣跨敤璁惧">
+ <el-select v-model="reportForm.deviceId"
+ style="width: 300px"
+ placeholder="璇烽�夋嫨浣跨敤璁惧"
+ clearable
+ filterable>
+ <el-option v-for="item in deviceOptions"
+ :key="item.id"
+ :label="item.deviceName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
<!-- 宸ユ椂 -->
<el-form-item label="宸ユ椂"
v-if="currentReportRowData?.type == 0"
@@ -280,6 +292,7 @@
import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js";
import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
import { getDicts } from "@/api/system/dict/data";
import QRCode from "qrcode";
import { getCurrentInstance, reactive, toRefs } from "vue";
@@ -426,6 +439,7 @@
const currentWorkOrderId = ref(null);
const reportFormRef = ref(null);
const userOptions = ref([]);
+ const deviceOptions = ref([]);
const reportForm = reactive({
planQuantity: 0,
quantity: null,
@@ -435,6 +449,7 @@
reportWork: "",
productProcessRouteItemId: "",
userId: "",
+ deviceId: "",
productMainId: null,
productionOrderRoutingOperationId: "",
productionOrderId: "",
@@ -694,6 +709,7 @@
toQuantity(planQuantity - completeQuantity)
);
reportForm.planQuantity = remainingQuantity;
+ reportForm.deviceId = "";
reportForm.quantity = remainingQuantity;
reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
reportForm.workOrderId = row.id;
@@ -801,6 +817,7 @@
scrapQty: isNaN(scrapQty) ? 0 : scrapQty,
userId: reportForm.userId,
userName: reportForm.userName,
+ deviceId: reportForm.deviceId || undefined,
productionOperationTaskId: reportForm.workOrderId,
productProcessRouteItemId: reportForm.productProcessRouteItemId,
reportWork: reportForm.reportWork,
@@ -958,6 +975,15 @@
userOptions.value = res.data;
}
});
+ // 鑾峰彇璁惧鍒楄〃
+ getDeviceLedger()
+ .then(res => {
+ deviceOptions.value = res.data || [];
+ })
+ .catch(err => {
+ console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+ deviceOptions.value = [];
+ });
});
</script>
diff --git a/src/views/qualityManagement/finalInspection/components/formDia.vue b/src/views/qualityManagement/finalInspection/components/formDia.vue
index 94801ff..bc68189 100644
--- a/src/views/qualityManagement/finalInspection/components/formDia.vue
+++ b/src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -301,6 +301,10 @@
// 鎵撳紑寮规
const openDialog = async (type, row) => {
operationType.value = type;
+ // 宸叉彁浜ょ殑妫�楠屽崟寮哄埗鍙
+ if (row?.inspectState === 1) {
+ operationType.value = "view";
+ }
dialogFormVisible.value = true;
// 鍏堟竻绌鸿〃鍗曢獙璇佺姸鎬侊紝閬垮厤闂儊
await nextTick();
diff --git a/src/views/qualityManagement/finalInspection/index.vue b/src/views/qualityManagement/finalInspection/index.vue
index f08ba37..6ffacc5 100644
--- a/src/views/qualityManagement/finalInspection/index.vue
+++ b/src/views/qualityManagement/finalInspection/index.vue
@@ -487,11 +487,23 @@
// 鎻愪环
const submit = async id => {
- const res = await submitQualityInspect({ id: id });
- if (res.code === 200) {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- getList();
- }
+ ElMessageBox.confirm(
+ "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝纭鎻愪氦锛�",
+ "鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(async () => {
+ const res = await submitQualityInspect({ id: id });
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess(
+ "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+ );
+ getList();
+ }
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
};
const handleBatchSubmit = () => {
diff --git a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
index 5e20709..c291ebe 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
@@ -124,10 +124,19 @@
clearable>
<el-option :label="item.label"
:value="item.value"
- v-for="item in rejection_handling"
+ v-for="item in filteredRejectionHandling"
:key="item.value" />
</el-select>
</el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="isReworkResult">
+ <el-col :span="24">
+ <el-alert title="鎻愪氦鍚庡皢鑷姩鍒涘缓杩斿伐鐢熶骇璁㈠崟锛團G鍗曞彿锛�"
+ type="info"
+ :closable="false"
+ show-icon
+ style="margin-bottom: 12px" />
</el-col>
</el-row>
<el-row :gutter="30">
@@ -224,6 +233,27 @@
const modelOptions = ref([]);
const userList = ref([]); // 妫�楠屽憳/澶勭悊浜轰笅鎷夊垪琛�
+ // 鍘熸枡绫伙紙inspectType=0锛変笉鍚堟牸鍙厑璁�"鎶ュ簾/璁╂鏀捐"
+ const filteredRejectionHandling = computed(() => {
+ const data = rejection_handling.value || [];
+ if (form.value.inspectType === 0 || form.value.inspectType === "0") {
+ return data.filter(
+ item => item && (item.label === "鎶ュ簾" || item.label === "璁╂鏀捐")
+ );
+ }
+ return data;
+ });
+
+ // 閫変腑"杩斿伐/杩斾慨"鏃剁殑鎻愮ず
+ const isReworkResult = computed(() => {
+ const item = (rejection_handling.value || []).find(
+ i => i.value === form.value.dealResult
+ );
+ const label = item ? item.label : form.value.dealResult;
+ return label === "杩斿伐" || label === "杩斾慨";
+ });
+
+
// 鎵撳紑寮规
const openDialog = async (type, row) => {
operationType.value = type;
diff --git a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
index 8f4492a..dc6c749 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
@@ -87,6 +87,17 @@
</el-form-item>
</el-col>
</el-row>
+ <el-row v-if="isReworkResult">
+ <el-col :span="24">
+ <el-alert
+ title="鎻愪氦鍚庡皢鑷姩鍒涘缓杩斿伐鐢熶骇璁㈠崟锛團G鍗曞彿锛�"
+ type="info"
+ :closable="false"
+ show-icon
+ style="margin-bottom: 12px"
+ />
+ </el-col>
+ </el-row>
<el-row :gutter="30">
<el-col :span="12">
<el-form-item label="澶勭悊浜猴細" prop="dealName">
@@ -174,13 +185,24 @@
const userList = ref([]); // 澶勭悊浜轰笅鎷夊垪琛�
const filteredRejectionHandling = computed(() => {
- const data = rejection_handling.value;
+ let data = rejection_handling.value;
+ // 鍘熸枡绫讳笉鍚堟牸鍙厑璁�"鎶ュ簾/璁╂鏀捐"
+ if (form.value.inspectType === 0 || form.value.inspectType === "0") {
+ data = data.filter(item => item && (item.label === '鎶ュ簾' || item.label === '璁╂鏀捐'));
+ }
if (form.value.method) {
return data.filter(item => item && item.label && item.label !== '杩斿伐' && item.label !== '杩斾慨')
}
return data
})
+// 閫変腑"杩斿伐/杩斾慨"鏃剁殑鎻愮ず
+const isReworkResult = computed(() => {
+ const item = (rejection_handling.value || []).find(i => i.value === form.value.dealResult);
+ const label = item ? item.label : form.value.dealResult;
+ return label === '杩斿伐' || label === '杩斾慨';
+})
+
// 鎵撳紑寮规
const openDialog = async (type, row) => {
diff --git a/src/views/qualityManagement/processInspection/components/formDia.vue b/src/views/qualityManagement/processInspection/components/formDia.vue
index 370b5f6..ef9f2f8 100644
--- a/src/views/qualityManagement/processInspection/components/formDia.vue
+++ b/src/views/qualityManagement/processInspection/components/formDia.vue
@@ -319,6 +319,10 @@
// 鎵撳紑寮规
const openDialog = async (type, row) => {
operationType.value = type;
+ // 宸叉彁浜ょ殑妫�楠屽崟寮哄埗鍙
+ if (row?.inspectState === 1) {
+ operationType.value = "view";
+ }
getOptions().then(res => {
supplierList.value = res.data;
});
diff --git a/src/views/qualityManagement/processInspection/index.vue b/src/views/qualityManagement/processInspection/index.vue
index 7cecaa2..b77d097 100644
--- a/src/views/qualityManagement/processInspection/index.vue
+++ b/src/views/qualityManagement/processInspection/index.vue
@@ -401,11 +401,23 @@
};
// 鎻愪环
const submit = async id => {
- const res = await submitQualityInspect({ id: id });
- if (res.code === 200) {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- getList();
- }
+ ElMessageBox.confirm(
+ "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝纭鎻愪氦锛�",
+ "鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(async () => {
+ const res = await submitQualityInspect({ id: id });
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess(
+ "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+ );
+ getList();
+ }
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
};
const handleBatchSubmit = () => {
diff --git a/src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue b/src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue
new file mode 100644
index 0000000..56ead8c
--- /dev/null
+++ b/src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue
@@ -0,0 +1,432 @@
+<template>
+ <div v-loading="loading"
+ class="forward-result">
+ <!-- 璁㈠崟鍗� -->
+ <div v-for="chain in data.chains"
+ :key="chain.productionOrderId"
+ class="order-card">
+ <div class="order-head">
+ <div class="order-title">
+ <el-link type="primary"
+ @click="$emit('goto-order', chain.npsNo)">{{ chain.npsNo }}</el-link>
+ <span class="product-name">{{ chain.finishedProduct || '-' }}</span>
+ </div>
+ <el-tag :type="statusType(chain.orderStatus)">{{ statusText(chain.orderStatus) }}</el-tag>
+ </div>
+
+ <div class="order-meta">
+ <span>璁″垝鏁伴噺锛歿{ chain.planQty ?? '-' }} {{ chain.unit || '' }}</span>
+ <span>瀹屽伐鏁伴噺锛歿{ chain.completeQty ?? '-' }} {{ chain.unit || '' }}</span>
+ <span>涓嬪崟鏃堕棿锛歿{ chain.orderCreateTime || '-' }}</span>
+ </div>
+
+ <div v-if="chain.batches && chain.batches.length"
+ class="hit-batches">
+ <span class="hit-label">鍛戒腑鍏ュ簱鎵规锛�</span>
+ <el-tag v-for="batch in chain.batches"
+ :key="batch.batchNo"
+ type="success"
+ effect="plain">
+ {{ batch.batchNo }}锛坽{ batch.qty }} {{ chain.unit || '' }}锛寋{ batch.inTime }}锛寋{ recordTypeLabel(batch.recordType) }}锛�
+ </el-tag>
+ </div>
+
+ <el-timeline class="order-timeline">
+ <!-- 宸ュ簭鎶ュ伐 -->
+ <el-timeline-item v-for="report in sortReports(chain.reports)"
+ :key="report.productMainId + '_' + report.workOrderNo"
+ :timestamp="report.reportTime"
+ type="primary"
+ placement="top">
+ <div class="node-title">宸ュ簭鎶ュ伐锛歿{ report.operationName || '-' }}</div>
+ <el-descriptions :column="3"
+ border
+ size="small">
+ <el-descriptions-item label="宸ュ崟缂栧彿">{{ report.workOrderNo || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ュ伐鍗曞彿">{{ report.productNo || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ュ伐浜�">{{ report.userName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁惧">{{ report.deviceName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="浜у嚭鏁伴噺">{{ report.outputQty ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ュ簾鏁伴噺">{{ report.scrapQty ?? '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <el-collapse class="input-collapse"
+ :model-value="expandedInputs[reportKey(chain, report)] || []"
+ @update:model-value="val => (expandedInputs[reportKey(chain, report)] = val)">
+ <el-collapse-item name="inputs"
+ :title="`鎶曞叆鐗╂枡锛�${inputsFor(chain, report).length}锛塦">
+ <el-table :data="inputsFor(chain, report)"
+ border
+ size="small">
+ <el-table-column label="鐗╂枡鍚嶇О"
+ prop="materialName"
+ align="center" />
+ <el-table-column label="鐗╂枡瑙勬牸"
+ prop="materialModel"
+ align="center" />
+ <el-table-column label="鎶曞叆鏁伴噺"
+ prop="inputQty"
+ align="center" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ align="center" />
+ </el-table>
+ </el-collapse-item>
+ </el-collapse>
+ </el-timeline-item>
+
+ <!-- 棰嗘枡鎵规锛堣鍗曠骇锛� -->
+ <el-timeline-item v-if="chain.picks && chain.picks.length"
+ type="warning"
+ placement="top">
+ <div class="node-title">棰嗘枡鎵规</div>
+ <el-table :data="chain.picks"
+ border
+ size="small">
+ <el-table-column label="鍘熸枡鎵瑰彿"
+ align="center"
+ min-width="200">
+ <template #default="{ row }">
+ <el-link type="primary"
+ @click="$emit('pick-batch', row.batchNo)">{{ row.batchNo }}</el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐗╂枡"
+ prop="materialName"
+ align="center" />
+ <el-table-column label="鐗╂枡瑙勬牸"
+ prop="materialModel"
+ align="center" />
+ <el-table-column label="棰嗘枡鏁伴噺"
+ align="center">
+ <template #default="{ row }">{{ row.quantity }} {{ row.unit || '' }}</template>
+ </el-table-column>
+ <el-table-column label="宸ュ簭"
+ prop="operationName"
+ align="center" />
+ <el-table-column label="棰嗘枡鏃堕棿"
+ prop="pickTime"
+ align="center" />
+ <el-table-column label="渚涘簲鍟�"
+ align="center">
+ <template #default="{ row }">{{ row.supplierName || '-' }}</template>
+ </el-table-column>
+ <el-table-column label="閲囪喘鍚堝悓鍙�"
+ align="center">
+ <template #default="{ row }">{{ row.contractNo || '-' }}</template>
+ </el-table-column>
+ <el-table-column label="鍘熸枡妫�楠岀粨璁�"
+ align="center">
+ <template #default="{ row }">{{ row.checkResult || '-' }}</template>
+ </el-table-column>
+ </el-table>
+ </el-timeline-item>
+
+ <!-- 妫�楠屽崟 -->
+ <el-timeline-item v-if="chain.inspects && chain.inspects.length"
+ type="success"
+ placement="top">
+ <div class="node-title">妫�楠屽崟</div>
+ <el-table :data="chain.inspects"
+ border
+ size="small">
+ <el-table-column label="妫�楠岀被鍨�"
+ prop="inspectTypeName"
+ align="center" />
+ <el-table-column label="宸ュ簭"
+ align="center">
+ <template #default="{ row }">{{ row.process || '-' }}</template>
+ </el-table-column>
+ <el-table-column label="妫�楠屽憳"
+ prop="checkName"
+ align="center" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName"
+ align="center" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model"
+ align="center" />
+ <el-table-column label="鏁伴噺"
+ prop="quantity"
+ align="center" />
+ <el-table-column label="鍚堟牸鏁伴噺"
+ prop="qualifiedQuantity"
+ align="center" />
+ <el-table-column label="涓嶅悎鏍兼暟閲�"
+ prop="unqualifiedQuantity"
+ align="center" />
+ <el-table-column label="妫�娴嬬粨鏋�"
+ align="center">
+ <template #default="{ row }">
+ <el-tag :type="row.checkResult === '鍚堟牸' ? 'success' : 'danger'">
+ {{ row.checkResult || '寰呮娴�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="妫�娴嬫棩鏈�"
+ prop="checkTime"
+ align="center" />
+ <el-table-column label="鎿嶄綔"
+ align="center"
+ width="100">
+ <template #default="{ row }">
+ <el-link type="primary"
+ @click="openInspect(chain, row)">鏌ョ湅璇︽儏</el-link>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+
+ <!-- 鍑哄簱娴佸悜锛堥《灞傦紝鎸夋壒鍙疯仛鍚堬級 -->
+ <div v-if="data.outbounds && data.outbounds.length"
+ class="order-card">
+ <div class="order-head">
+ <div class="order-title">
+ <span class="product-name">鍑哄簱娴佸悜</span>
+ </div>
+ </div>
+ <el-table :data="data.outbounds"
+ border
+ size="small">
+ <el-table-column label="鎵瑰彿"
+ prop="batchNo"
+ align="center" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName"
+ align="center" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="productModel"
+ align="center" />
+ <el-table-column label="鍑哄簱鏁伴噺"
+ align="center">
+ <template #default="{ row }">{{ row.qty }} {{ row.unit || '' }}</template>
+ </el-table-column>
+ <el-table-column label="鍑哄簱绫诲瀷"
+ align="center">
+ <template #default="{ row }">{{ recordTypeLabel(row.recordType) }}</template>
+ </el-table-column>
+ <el-table-column label="鍑哄簱鏃堕棿"
+ prop="outTime"
+ align="center" />
+ <el-table-column label="澶囨敞"
+ align="center">
+ <template #default="{ row }">{{ row.remark || '-' }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 妫�楠屽崟璇︽儏 -->
+ <el-dialog v-model="inspectVisible"
+ title="妫�楠屽崟璇︽儏"
+ width="900px">
+ <div v-if="currentInspect"
+ class="inspect-detail">
+ <el-descriptions :column="3"
+ border
+ size="small">
+ <el-descriptions-item label="妫�楠岀被鍨�">{{ currentInspect.inspectTypeName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="宸ュ簭">{{ currentInspect.process || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="妫�楠屽憳">{{ currentInspect.checkName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ currentInspect.productName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ currentInspect.model || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">{{ currentInspect.unit || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鏁伴噺">{{ currentInspect.quantity ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堟牸鏁伴噺">{{ currentInspect.qualifiedQuantity ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="涓嶅悎鏍兼暟閲�">{{ currentInspect.unqualifiedQuantity ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="妫�娴嬬粨鏋�">
+ <el-tag :type="currentInspect.checkResult === '鍚堟牸' ? 'success' : 'danger'">
+ {{ currentInspect.checkResult || '寰呮娴�' }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="妫�娴嬫棩鏈�">{{ currentInspect.checkTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="涓嶅悎鏍肩幇璞�">{{ currentInspect.defectivePhenomena || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <h4 class="sub-title">涓嶅悎鏍煎鐞�</h4>
+ <el-table :data="currentUnqualified"
+ border
+ size="small">
+ <el-table-column label="搴忓彿"
+ type="index"
+ width="60"
+ align="center" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName"
+ align="center" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model"
+ align="center" />
+ <el-table-column label="鏁伴噺"
+ prop="quantity"
+ align="center" />
+ <el-table-column label="涓嶅悎鏍肩幇璞�"
+ prop="defectivePhenomena"
+ align="center" />
+ <el-table-column label="澶勭悊缁撴灉"
+ prop="dealResult"
+ align="center" />
+ <el-table-column label="澶勭悊浜�"
+ prop="dealName"
+ align="center" />
+ <el-table-column label="澶勭悊鏃ユ湡"
+ prop="dealTime"
+ align="center" />
+ </el-table>
+ </div>
+ <template #footer>
+ <el-button @click="inspectVisible = false">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive } from "vue";
+
+ defineProps({
+ data: {
+ type: Object,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ default: false,
+ },
+ });
+
+ defineEmits(["goto-order", "pick-batch"]);
+
+ const statusTypeMap = { 1: "primary", 2: "warning", 3: "success", 5: "danger" };
+ const statusTextMap = { 1: "寰呭紑濮�", 2: "杩涜涓�", 3: "宸插畬鎴�", 5: "宸茬粨鏉�" };
+ const statusType = status => statusTypeMap[status] || "info";
+ const statusText = status => statusTextMap[status] || "宸插彇娑�";
+
+ const inTypeMap = {
+ 0: "鑷畾涔夊叆搴�",
+ 2: "鐢熶骇鎶ュ伐鍏ュ簱",
+ 6: "璐ㄦ鍚堟牸鍏ュ簱",
+ 7: "閲囪喘鍏ュ簱",
+ 10: "閲囪喘鍏ュ簱",
+ };
+ const outTypeMap = {
+ 1: "鑷畾涔夊嚭搴�",
+ 3: "鐢熶骇鎶ュ伐鍑哄簱",
+ 8: "閿�鍞嚭搴�",
+ 13: "閿�鍞彂璐у嚭搴�",
+ };
+ const recordTypeLabel = type => {
+ const key = String(type ?? "");
+ return inTypeMap[key] || outTypeMap[key] || key || "-";
+ };
+
+ const sortReports = reports =>
+ [...(reports || [])].sort((a, b) =>
+ String(a.reportTime || "").localeCompare(String(b.reportTime || ""))
+ );
+
+ const inputsFor = (chain, report) =>
+ (chain.inputs || []).filter(
+ item => String(item.productMainId) === String(report.productMainId)
+ );
+
+ const reportKey = (chain, report) =>
+ `${chain.productionOrderId}_${report.productMainId}_${report.workOrderNo}`;
+ const expandedInputs = reactive({});
+
+ // 妫�楠屽崟璇︽儏
+ const inspectVisible = ref(false);
+ const currentInspect = ref(null);
+ const currentUnqualified = ref([]);
+ const openInspect = (chain, inspect) => {
+ currentInspect.value = inspect;
+ const own = inspect.unqualifiedList || [];
+ currentUnqualified.value = own.length
+ ? own
+ : (chain.unqualifiedList || []).filter(
+ item => String(item.inspectId) === String(inspect.inspectId)
+ );
+ inspectVisible.value = true;
+ };
+</script>
+
+<style scoped lang="scss">
+ .forward-result {
+ min-height: 200px;
+ }
+
+ .order-card {
+ background-color: #ffffff;
+ border-radius: 10px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+ }
+
+ .order-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #ebeef5;
+ }
+
+ .order-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 16px;
+ }
+
+ .product-name {
+ font-weight: 600;
+ color: #1a1a1a;
+ }
+
+ .order-meta {
+ display: flex;
+ gap: 28px;
+ padding: 12px 0;
+ font-size: 13px;
+ color: #606266;
+ }
+
+ .hit-batches {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background-color: #f0f9eb;
+ border-radius: 6px;
+
+ .hit-label {
+ font-size: 13px;
+ color: #606266;
+ }
+ }
+
+ .order-timeline {
+ margin-top: 20px;
+ padding-left: 4px;
+ }
+
+ .node-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 12px;
+ }
+
+ .input-collapse {
+ margin-top: 12px;
+ }
+
+ .sub-title {
+ margin: 20px 0 12px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ }
+</style>
diff --git a/src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue b/src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue
new file mode 100644
index 0000000..b0f9f8f
--- /dev/null
+++ b/src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue
@@ -0,0 +1,171 @@
+<template>
+ <div v-loading="loading"
+ class="reverse-result">
+ <!-- 鍘熸枡鏉ユ簮 -->
+ <div class="material-card">
+ <h3 class="section-title">鍘熸枡鏉ユ簮</h3>
+ <el-descriptions v-if="data.material"
+ :column="4"
+ border
+ size="small">
+ <el-descriptions-item label="鍘熸枡鎵瑰彿">{{ data.material.batchNo || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鐗╂枡">{{ data.material.materialName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鐗╂枡瑙勬牸">{{ data.material.materialModel || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍏ュ簱绫诲瀷">{{ recordTypeLabel(data.material.recordType) }}</el-descriptions-item>
+ <el-descriptions-item label="鍏ュ簱鏁伴噺">{{ data.material.qty ?? '-' }} {{ data.material.unit || '' }}</el-descriptions-item>
+ <el-descriptions-item label="鍏ュ簱鏃堕棿">{{ data.material.inTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="渚涘簲鍟�">{{ data.material.supplierName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閲囪喘鍚堝悓鍙�">{{ data.material.contractNo || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閲囪喘鏃ユ湡">{{ data.material.entryDate || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="妫�楠屾暟閲�">{{ data.material.inspectQty ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堟牸鏁伴噺">{{ data.material.qualifiedQty ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="涓嶅悎鏍兼暟閲�">{{ data.material.unqualifiedQty ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="妫�楠岀粨璁�">{{ data.material.checkResult || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="妫�楠屾棩鏈�">{{ data.material.checkTime || '-' }}</el-descriptions-item>
+ </el-descriptions>
+ <el-alert v-else
+ type="info"
+ :closable="false"
+ show-icon
+ description="璇ユ壒鍙锋棤鍏ュ簱/閲囪喘鏉ユ簮璁板綍" />
+ </div>
+
+ <!-- 鍙楀奖鍝嶈鍗� -->
+ <div class="order-card">
+ <h3 class="section-title">鍙楀奖鍝嶇敓浜ц鍗曪紙{{ data.orders.length }}锛�</h3>
+ <PIMTable rowKey="productionOrderId"
+ :column="tableColumn"
+ :tableData="data.orders"
+ :page="{ total: data.orders.length }"
+ :isShowPagination="false"
+ height="520"
+ :tableLoading="loading">
+ <template #npsNo="{ row }">
+ <el-link type="primary"
+ @click="$emit('goto-order', row.npsNo)">{{ row.npsNo }}</el-link>
+ </template>
+ <template #orderStatus="{ row }">
+ <el-tag :type="statusType(row.orderStatus)">{{ statusText(row.orderStatus) }}</el-tag>
+ </template>
+ <template #finishedBatches="{ row }">
+ <template v-if="row.finishedBatches && row.finishedBatches.length">
+ <el-tag v-for="batch in row.finishedBatches"
+ :key="batch.batchNo"
+ class="batch-tag"
+ @click="$emit('goto-forward', batch.batchNo)">{{ batch.batchNo }}</el-tag>
+ </template>
+ <span v-else>-</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+
+ defineProps({
+ data: {
+ type: Object,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ default: false,
+ },
+ });
+
+ defineEmits(["goto-order", "goto-forward"]);
+
+ const tableColumn = [
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "npsNo",
+ dataType: "slot",
+ slot: "npsNo",
+ minWidth: 160,
+ },
+ {
+ label: "鎴愬搧",
+ prop: "finishedProduct",
+ minWidth: 220,
+ },
+ {
+ label: "璁″垝鏁伴噺",
+ prop: "planQty",
+ width: 100,
+ },
+ {
+ label: "瀹屽伐鏁伴噺",
+ prop: "completeQty",
+ width: 100,
+ },
+ {
+ label: "璁㈠崟鐘舵��",
+ prop: "orderStatus",
+ dataType: "slot",
+ slot: "orderStatus",
+ width: 100,
+ },
+ {
+ label: "棰嗘枡鏁伴噺",
+ prop: "pickedQty",
+ width: 110,
+ },
+ {
+ label: "浜у嚭鎴愬搧鎵瑰彿",
+ prop: "finishedBatches",
+ dataType: "slot",
+ slot: "finishedBatches",
+ minWidth: 260,
+ },
+ ];
+
+ const statusTypeMap = { 1: "primary", 2: "warning", 3: "success", 5: "danger" };
+ const statusTextMap = { 1: "寰呭紑濮�", 2: "杩涜涓�", 3: "宸插畬鎴�", 5: "宸茬粨鏉�" };
+ const statusType = status => statusTypeMap[status] || "info";
+ const statusText = status => statusTextMap[status] || "宸插彇娑�";
+
+ const inTypeMap = {
+ 0: "鑷畾涔夊叆搴�",
+ 2: "鐢熶骇鎶ュ伐鍏ュ簱",
+ 6: "璐ㄦ鍚堟牸鍏ュ簱",
+ 7: "閲囪喘鍏ュ簱",
+ 10: "閲囪喘鍏ュ簱",
+ };
+ const outTypeMap = {
+ 1: "鑷畾涔夊嚭搴�",
+ 3: "鐢熶骇鎶ュ伐鍑哄簱",
+ 8: "閿�鍞嚭搴�",
+ 13: "閿�鍞彂璐у嚭搴�",
+ };
+ const recordTypeLabel = type => {
+ const key = String(type ?? "");
+ return inTypeMap[key] || outTypeMap[key] || key || "-";
+ };
+</script>
+
+<style scoped lang="scss">
+ .material-card,
+ .order-card {
+ background-color: #ffffff;
+ border-radius: 10px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+ }
+
+ .section-title {
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 16px;
+ color: #1a1a1a;
+ border-bottom: 2px solid #409eff;
+ padding-bottom: 10px;
+ }
+
+ .batch-tag {
+ margin: 2px 6px 2px 0;
+ cursor: pointer;
+ }
+</style>
diff --git a/src/views/qualityManagement/qualityTraceability/index.vue b/src/views/qualityManagement/qualityTraceability/index.vue
new file mode 100644
index 0000000..f9fd777
--- /dev/null
+++ b/src/views/qualityManagement/qualityTraceability/index.vue
@@ -0,0 +1,305 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="璐ㄩ噺杩芥函" />
+ <el-card class="trace-card">
+ <el-tabs v-model="activeTab"
+ @tab-change="handleTabChange">
+ <el-tab-pane label="姝e悜杩芥函锛堟垚鍝� 鈫� 鍘熸枡锛�"
+ name="forward" />
+ <el-tab-pane label="鍙嶅悜杩芥函锛堝師鏂� 鈫� 鎴愬搧锛�"
+ name="reverse" />
+ </el-tabs>
+
+ <div class="search-bar">
+ <el-select v-model="batchNo"
+ filterable
+ remote
+ reserve-keyword
+ clearable
+ :placeholder="placeholder"
+ :loading="batchLoading"
+ :remote-method="handleBatchSearch"
+ style="width: 460px;"
+ @change="handleBatchChange">
+ <el-option v-for="option in batchOpts"
+ :key="option.batchNo"
+ :value="option.batchNo"
+ :label="optionLabel(option)" />
+ <template #footer>
+ <div v-if="batchHasMore"
+ class="select-load-more"
+ @click="loadMoreBatch">
+ <span v-if="batchLoading">鍔犺浇涓�...</span>
+ <span v-else>鐐瑰嚮鍔犺浇鏇村</span>
+ </div>
+ <div v-else-if="batchOpts.length > 0"
+ class="select-load-more no-more">
+ 娌℃湁鏇村浜�
+ </div>
+ </template>
+ </el-select>
+ <el-button type="primary"
+ :loading="loading"
+ @click="handleQuery">鏌ヨ</el-button>
+ <el-button :loading="exportLoading"
+ @click="handleExport">瀵煎嚭</el-button>
+ </div>
+
+ <el-empty v-if="!queried"
+ description="璇疯緭鍏ユ垨閫夋嫨鎵瑰彿鍚庣偣鍑绘煡璇�" />
+ <el-empty v-else-if="isEmptyResult"
+ description="鏈煡璇㈠埌璇ユ壒鍙风殑杩芥函璁板綍" />
+ <ForwardResult v-else-if="isForward"
+ :data="forwardData"
+ :loading="loading"
+ @goto-order="gotoOrder"
+ @pick-batch="goReverse" />
+ <ReverseTable v-else
+ :data="reverseData"
+ :loading="loading"
+ @goto-order="gotoOrder"
+ @goto-forward="goForward" />
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted } from "vue";
+ import { ElMessage } from "element-plus";
+ import { useRouter } from "vue-router";
+ import { saveAs } from "file-saver";
+ import { blobValidate } from "@/utils/ruoyi";
+ import ForwardResult from "./components/ForwardResult.vue";
+ import ReverseTable from "./components/ReverseTable.vue";
+ import {
+ forwardTrace,
+ reverseTrace,
+ batchOptions,
+ forwardExport,
+ reverseExport,
+ } from "@/api/qualityManagement/qualityTrace";
+
+ const router = useRouter();
+
+ const BATCH_PAGE = 50;
+ const BATCH_MAX = 200;
+
+ const activeTab = ref("forward");
+ const isForward = computed(() => activeTab.value === "forward");
+ const batchType = computed(() => (isForward.value ? "finished" : "material"));
+ const placeholder = computed(() =>
+ isForward.value ? "璇疯緭鍏ユ垚鍝佹壒鍙锋悳绱�" : "璇疯緭鍏ュ師鏂欐壒鍙锋悳绱�"
+ );
+
+ // 鎵瑰彿涓嬫媺
+ const batchNo = ref("");
+ const batchOpts = ref([]);
+ const batchLoading = ref(false);
+ const batchKeyword = ref("");
+ const batchLimit = ref(BATCH_PAGE);
+ const batchHasMore = computed(
+ () => batchOpts.value.length >= batchLimit.value && batchLimit.value < BATCH_MAX
+ );
+
+ const loading = ref(false);
+ const exportLoading = ref(false);
+ const queried = ref(false);
+
+ const forwardData = reactive({ batchNo: "", chains: [], outbounds: [] });
+ const reverseData = reactive({ batchNo: "", material: null, orders: [] });
+
+ const isEmptyResult = computed(() => {
+ if (!queried.value) return false;
+ return isForward.value
+ ? forwardData.chains.length === 0 && forwardData.outbounds.length === 0
+ : !reverseData.material && reverseData.orders.length === 0;
+ });
+
+ const optionLabel = option =>
+ [option.batchNo, option.productName, option.productModel]
+ .filter(Boolean)
+ .join(" | ");
+
+ // 鎺ュ彛鏃� offset锛屽彧鑳芥斁澶� limit 鍚庢暣浣撻噸鍙�
+ const fetchBatchOptions = async () => {
+ batchLoading.value = true;
+ try {
+ const res = await batchOptions(
+ batchType.value,
+ batchKeyword.value,
+ batchLimit.value
+ );
+ batchOpts.value = res.code === 200 ? res.data || [] : [];
+ } catch (error) {
+ batchOpts.value = [];
+ } finally {
+ batchLoading.value = false;
+ }
+ };
+
+ const handleBatchSearch = keyword => {
+ batchKeyword.value = keyword || "";
+ batchLimit.value = BATCH_PAGE;
+ fetchBatchOptions();
+ };
+
+ const loadMoreBatch = () => {
+ if (batchLoading.value || !batchHasMore.value) return;
+ batchLimit.value = Math.min(batchLimit.value + BATCH_PAGE, BATCH_MAX);
+ fetchBatchOptions();
+ };
+
+ const handleBatchChange = value => {
+ if (value) handleQuery();
+ };
+
+ const resetResult = () => {
+ forwardData.batchNo = "";
+ forwardData.chains = [];
+ forwardData.outbounds = [];
+ reverseData.batchNo = "";
+ reverseData.material = null;
+ reverseData.orders = [];
+ };
+
+ const handleTabChange = () => {
+ batchNo.value = "";
+ batchOpts.value = [];
+ queried.value = false;
+ resetResult();
+ handleBatchSearch("");
+ };
+
+ const handleQuery = async () => {
+ const keyword = (batchNo.value || "").trim();
+ if (!keyword) {
+ ElMessage.warning("璇疯緭鍏ユ垨閫夋嫨鎵瑰彿");
+ return;
+ }
+ queried.value = true;
+ loading.value = true;
+ try {
+ if (isForward.value) {
+ const res = await forwardTrace(keyword);
+ if (res.code === 200) {
+ forwardData.batchNo = res.data?.batchNo || keyword;
+ forwardData.chains = res.data?.chains || [];
+ forwardData.outbounds = res.data?.outbounds || [];
+ }
+ } else {
+ const res = await reverseTrace(keyword);
+ if (res.code === 200) {
+ reverseData.batchNo = res.data?.batchNo || keyword;
+ reverseData.material = res.data?.material || null;
+ reverseData.orders = res.data?.orders || [];
+ }
+ }
+ } catch (error) {
+ // 闈� 200锛堝"鎵瑰彿涓嶈兘涓虹┖"锛夊凡鐢卞搷搴旀嫤鎴櫒鎻愮ず
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const handleExport = async () => {
+ const keyword = (batchNo.value || "").trim();
+ if (!keyword) {
+ ElMessage.warning("璇峰厛閫夋嫨鎴栬緭鍏ユ壒鍙�");
+ return;
+ }
+ exportLoading.value = true;
+ try {
+ const blobData = isForward.value
+ ? await forwardExport(keyword)
+ : await reverseExport(keyword);
+ if (blobValidate(blobData)) {
+ if (blobData.size === 0) {
+ ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+ return;
+ }
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ const name = isForward.value ? "鎴愬搧鎵规杩芥函" : "鍘熸枡鎵规娴佸悜杩芥函";
+ saveAs(blob, `${name}_${keyword}.xlsx`);
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ } else {
+ const resText = await blobData.text();
+ let rspObj = {};
+ try {
+ rspObj = JSON.parse(resText);
+ } catch (error) {
+ rspObj = {};
+ }
+ ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+ } finally {
+ exportLoading.value = false;
+ }
+ };
+
+ const gotoOrder = npsNo => {
+ if (!npsNo) return;
+ router.push({
+ path: "/productionManagement/productionTraceability",
+ query: { npsNo },
+ });
+ };
+
+ const goReverse = batch => {
+ if (!batch) return;
+ activeTab.value = "reverse";
+ handleTabChange();
+ batchNo.value = batch;
+ handleQuery();
+ };
+
+ const goForward = batch => {
+ if (!batch) return;
+ activeTab.value = "forward";
+ handleTabChange();
+ batchNo.value = batch;
+ handleQuery();
+ };
+
+ onMounted(() => {
+ handleBatchSearch("");
+ });
+</script>
+
+<style scoped lang="scss">
+ .trace-card {
+ min-height: 82vh;
+ }
+
+ .search-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 8px 0 20px;
+ }
+
+ .select-load-more {
+ text-align: center;
+ padding: 8px 0;
+ font-size: 13px;
+ color: #409eff;
+ cursor: pointer;
+ border-top: 1px solid #ebeef5;
+
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ &.no-more {
+ color: #c0c4cc;
+ cursor: default;
+
+ &:hover {
+ background-color: transparent;
+ }
+ }
+ }
+</style>
diff --git a/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue b/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
index 595bf80..44f1d53 100644
--- a/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
+++ b/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -327,6 +327,10 @@
// 鎵撳紑寮规
const openDialog = async (type, row) => {
operationType.value = type;
+ // 宸叉彁浜ょ殑妫�楠屽崟寮哄埗鍙
+ if (row?.inspectState === 1) {
+ operationType.value = "view";
+ }
getOptions().then(res => {
supplierList.value = res.data;
});
diff --git a/src/views/qualityManagement/rawMaterialInspection/index.vue b/src/views/qualityManagement/rawMaterialInspection/index.vue
index 6d2acd5..d4f479e 100644
--- a/src/views/qualityManagement/rawMaterialInspection/index.vue
+++ b/src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -430,11 +430,23 @@
// 鎻愪环
const submit = async id => {
- const res = await submitQualityInspect({ id: id });
- if (res.code === 200) {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- getList();
- }
+ ElMessageBox.confirm(
+ "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝纭鎻愪氦锛�",
+ "鎻愮ず",
+ { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+ )
+ .then(async () => {
+ const res = await submitQualityInspect({ id: id });
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess(
+ "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+ );
+ getList();
+ }
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
};
// 鍏抽棴寮规
diff --git a/vite.config.js b/vite.config.js
index 6b267a7..8e8b7d9 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -8,7 +8,7 @@
const { VITE_APP_ENV } = env;
const baseUrl =
env.VITE_APP_ENV === "development"
- ? "http://localhost:7005"
+ ? "http://192.168.0.10:7005"
: env.VITE_BASE_API;
const javaUrl =
env.VITE_APP_ENV === "development"
--
Gitblit v1.9.3