From 03a05815d7ea5612206b6cfe5d6537b4d4cedc0a Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 15 九月 2026 11:41:32 +0800
Subject: [PATCH] 天津潜宇塑胶app 1.添加采购需求、库存盘点、计件单价配置、产量统计、材料消耗核算、成本核算分析页面
---
src/pages.json | 49
src/api/basicData/product.js | 16
src/api/inventoryManagement/stockCheck.js | 72
src/pages/productionDesign/bom/index.vue | 279 +
src/api/qualityManagement/materialInspection.js | 18
src/pages/qualityManagement/finalInspection/add.vue | 57
src/pages/qualityManagement/materialInspection/add.vue | 124
src/api/productionManagement/pieceRateConfig.js | 47
src/pages/productionManagement/materialConsumption/index.vue | 507 +++
src/api/inventoryManagement/stockInventory.js | 10
src/pages/procurementManagement/procurementDemand/index.vue | 315 ++
src/utils/common.ts | 15
src/pages/procurementManagement/procurementLedger/detail.vue | 68
src/api/productionManagement/materialConsumption.js | 11
src/pages/productionManagement/productionTraceability/index.vue | 1970 +++++++------
src/pages/qualityManagement/processInspection/index.vue | 316 ++
src/pages/productionManagement/productionAccounting/index.vue | 12
src/pages/productionManagement/costAccounting/index.vue | 662 ++++
src/api/productionManagement/bom.js | 29
src/pages/inventoryManagement/stockCheck/index.vue | 832 +++++
src/pages/inventoryManagement/stockCheck/detail.vue | 626 ++++
src/pages/qualityManagement/materialInspection/detail.vue | 14
src/pages/qualityManagement/processInspection/add.vue | 61
src/pages/qualityManagement/finalInspection/detail.vue | 14
src/api/qualityManagement/productionTrace.js | 28
src/pages/works.vue | 54
src/pages/qualityManagement/finalInspection/index.vue | 315 ++
src/pages/inventoryManagement/stockManagement/index.vue | 264 +
src/pages/productionManagement/outputStatistics/index.vue | 412 ++
src/pages/qualityManagement/processInspection/detail.vue | 14
src/pages/productionManagement/productionReport/index.vue | 46
src/api/productionManagement/productionProductMain.js | 18
src/api/productionManagement/costAccounting.js | 29
src/api/procurementManagement/procurementDemand.js | 28
src/config.js | 4
src/pages/productionManagement/productionReporting/ledger.vue | 74
src/pages/qualityManagement/materialInspection/index.vue | 298 +
src/pages/productionManagement/pieceRateConfig/index.vue | 698 ++++
38 files changed, 7,345 insertions(+), 1,061 deletions(-)
diff --git a/src/api/basicData/product.js b/src/api/basicData/product.js
index 10f8fd1..f6512fe 100644
--- a/src/api/basicData/product.js
+++ b/src/api/basicData/product.js
@@ -56,3 +56,19 @@
params: query
})
}
+// 鏉$爜绮剧‘鏌ヨ瑙勬牸锛堟壂鐮佸洖濉級
+export function modelByBarcode(query) {
+ return request({
+ url: '/basic/product/model/byBarcode',
+ method: 'get',
+ params: query
+ })
+}
+// 瑙勬牸鍒嗛〉鏌ヨ锛堟敮鎸佹潯鐮佹ā绯婏級
+export function pageModel(query) {
+ return request({
+ url: '/basic/product/pageModel',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/src/api/inventoryManagement/stockCheck.js b/src/api/inventoryManagement/stockCheck.js
new file mode 100644
index 0000000..d2985a9
--- /dev/null
+++ b/src/api/inventoryManagement/stockCheck.js
@@ -0,0 +1,72 @@
+import request from "@/utils/request";
+
+// 鍙戣捣鐩樼偣锛堜笉浼� productModelIds = 鍏ㄩ儴搴撳瓨琛屽揩鐓э級
+export const start = params => {
+ return request({
+ url: "/stockCheck/start",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鐩樼偣鍗曞垎椤靛垪琛�
+export const listPage = params => {
+ return request({
+ url: "/stockCheck/listPage",
+ method: "get",
+ params,
+ });
+};
+
+// 鐩樼偣鍗曡鎯�
+export const getDetail = id => {
+ return request({
+ url: `/stockCheck/${id}`,
+ method: "get",
+ });
+};
+
+// 鐩樼偣鏄庣粏鍒嗛〉锛坥nlyDiff=true 鍙湅宸插綍鍏ヤ笖鏈夊樊寮傜殑琛岋級
+export const detailPage = params => {
+ return request({
+ url: "/stockCheck/detailPage",
+ method: "get",
+ params,
+ });
+};
+
+// 鎵归噺褰曞叆瀹炵洏锛坆ody 涓烘暟缁勶級
+export const entry = params => {
+ return request({
+ url: "/stockCheck/entry",
+ method: "put",
+ data: params,
+ });
+};
+
+// 瀹屾垚鐩樼偣锛堝樊寮傜敓鎴愬緟瀹℃牳璋冭处鍗曟嵁锛�
+export const finish = params => {
+ return request({
+ url: "/stockCheck/finish",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鍙栨秷鐩樼偣
+export const cancel = params => {
+ return request({
+ url: "/stockCheck/cancel",
+ method: "post",
+ data: params,
+ });
+};
+
+// 宸紓鍒嗘瀽鍒楄〃
+export const diffList = params => {
+ return request({
+ url: "/stockCheck/diffList",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index 328657c..26a049a 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -1,4 +1,5 @@
import request from "@/utils/request";
+
// 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
export const getStockInventoryListPage = params => {
return request({
@@ -76,3 +77,12 @@
params: { productModelId },
});
};
+
+// 鍒嗛〉鏌ヨ搴撳瓨棰勮鍒楄〃锛堝彲鐢ㄦ暟閲� 鈮� 棰勮鍊硷紝棰勮鍊�>0锛�
+export const getStockWarnPage = params => {
+ return request({
+ url: "/stockInventory/warnPage",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/api/procurementManagement/procurementDemand.js b/src/api/procurementManagement/procurementDemand.js
new file mode 100644
index 0000000..a90840d
--- /dev/null
+++ b/src/api/procurementManagement/procurementDemand.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 閲囪喘闇�姹傚垎椤垫煡璇�
+export const listPage = params => {
+ return request({
+ url: "/procurementDemand/listPage",
+ method: "get",
+ params,
+ });
+};
+
+// 閲囪喘闇�姹傜姸鎬佸彉鏇达紙params: { ids, status }锛宨ds 鏀寔閫楀彿鍒嗛殧鐨勫涓� id锛�
+export const changeStatus = params => {
+ return request({
+ url: "/procurementDemand/changeStatus",
+ method: "put",
+ params,
+ });
+};
+
+// 鍒犻櫎閲囪喘闇�姹傦紙body 涓� id 鏁扮粍锛屼粎寰呭鐞嗗彲鍒狅級
+export const del = ids => {
+ return request({
+ url: "/procurementDemand/del",
+ method: "delete",
+ data: ids,
+ });
+};
diff --git a/src/api/productionManagement/bom.js b/src/api/productionManagement/bom.js
index cb42000..e232630 100644
--- a/src/api/productionManagement/bom.js
+++ b/src/api/productionManagement/bom.js
@@ -80,3 +80,32 @@
method: "delete",
});
}
+
+// --- BOM 鐗堟湰鍚敤/鍋滅敤銆侀綈濂楀垎鏋� ---
+
+// BOM 鍚敤/鍋滅敤锛坰tatus: 1鍚敤 0鍋滅敤锛屽惎鐢ㄦ椂鍚庣鑷姩鍋滅敤鍚岃鏍煎叾瀹冪増鏈級
+export function changeStatus(data) {
+ return request({
+ url: "/technologyBom/changeStatus",
+ method: "put",
+ data: data,
+ });
+}
+
+// BOM 榻愬鍒嗘瀽锛坉ata: { bomId, demandQty }锛�
+export function kitCheck(data) {
+ return request({
+ url: "/technologyBom/kitCheck",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂哄彛鐢熸垚閲囪喘闇�姹傦紙data 鍚� kitCheck锛�
+export function generateDemand(data) {
+ return request({
+ url: "/technologyBom/kitCheck/generateDemand",
+ method: "post",
+ data: data,
+ });
+}
diff --git a/src/api/productionManagement/costAccounting.js b/src/api/productionManagement/costAccounting.js
new file mode 100644
index 0000000..73c20ca
--- /dev/null
+++ b/src/api/productionManagement/costAccounting.js
@@ -0,0 +1,29 @@
+// 鎴愭湰鏍哥畻鍒嗘瀽椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鎴愭湰鏍哥畻鏄庣粏锛堟寜鐢熶骇璁㈠崟锛�
+export function costStat(query) {
+ return request({
+ url: "/costAccounting/stat",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鎴愭湰鏋勬垚姹囨�伙紙groupBy: month / product锛�
+export function costSummary(query) {
+ return request({
+ url: "/costAccounting/summary",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缂轰环鐗╂枡娓呭崟
+export function costMissingPrice(query) {
+ return request({
+ url: "/costAccounting/missingPrice",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/materialConsumption.js b/src/api/productionManagement/materialConsumption.js
new file mode 100644
index 0000000..13cadee
--- /dev/null
+++ b/src/api/productionManagement/materialConsumption.js
@@ -0,0 +1,11 @@
+// 鏉愭枡娑堣�楁牳绠楅〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鏉愭枡娑堣�楁牳绠楀垪琛紙鎸夌敓浜ц鍗暶楃墿鏂欒繑鍥烇紝闈炲垎椤碉級
+export function stat(query) {
+ return request({
+ url: "/materialConsumption/stat",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/pieceRateConfig.js b/src/api/productionManagement/pieceRateConfig.js
new file mode 100644
index 0000000..cfabb24
--- /dev/null
+++ b/src/api/productionManagement/pieceRateConfig.js
@@ -0,0 +1,47 @@
+// 璁′欢鍗曚环閰嶇疆鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ璁′欢鍗曚环閰嶇疆
+export const listPage = params => {
+ return request({
+ url: "/pieceRateConfig/listPage",
+ method: "get",
+ params,
+ });
+};
+
+// 鏂板璁′欢鍗曚环閰嶇疆
+export const add = data => {
+ return request({
+ url: "/pieceRateConfig/add",
+ method: "post",
+ data,
+ });
+};
+
+// 淇敼璁′欢鍗曚环閰嶇疆
+export const update = data => {
+ return request({
+ url: "/pieceRateConfig/update",
+ method: "put",
+ data,
+ });
+};
+
+// 鍚敤/鍋滅敤
+export const changeStatus = data => {
+ return request({
+ url: "/pieceRateConfig/changeStatus",
+ method: "put",
+ data,
+ });
+};
+
+// 鎵归噺鍒犻櫎锛宒ata 涓� id 鏁扮粍
+export const batchDelete = data => {
+ return request({
+ url: "/pieceRateConfig/batchDelete",
+ method: "delete",
+ data,
+ });
+};
diff --git a/src/api/productionManagement/productionProductMain.js b/src/api/productionManagement/productionProductMain.js
index 4242e1b..1145965 100644
--- a/src/api/productionManagement/productionProductMain.js
+++ b/src/api/productionManagement/productionProductMain.js
@@ -27,3 +27,21 @@
params: query,
});
}
+
+// 璁′欢宸ヨ祫姹囨�伙紙鎸変汉鍛橈級
+export function wageSummary(query) {
+ return request({
+ url: "/productionProductMain/wageSummary",
+ method: "get",
+ params: query,
+ });
+}
+
+// 浜ч噺缁熻锛坉imension: device/operation/product锛�
+export function outputStat(query) {
+ return request({
+ url: "/productionProductMain/outputStat",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/qualityManagement/materialInspection.js b/src/api/qualityManagement/materialInspection.js
index 6f458b4..e1a2b9e 100644
--- a/src/api/qualityManagement/materialInspection.js
+++ b/src/api/qualityManagement/materialInspection.js
@@ -163,4 +163,22 @@
url: "/productProcess/list",
method: "get",
});
+}
+
+// 鏌ヨ涓嶅悎鏍艰褰曞垪琛紙inspectState锛�0寰呭鐞�/1宸插鐞嗭級
+export function qualityUnqualifiedListPage(query) {
+ return request({
+ url: '/quality/qualityUnqualified/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 澶勭悊涓嶅悎鏍艰褰曪紙涓嶄紶 dealName/dealTime锛屽悗绔嚜鍔ㄥ彇褰撳墠鐧诲綍浜�/鏃堕棿锛�
+export function qualityUnqualifiedDeal(data) {
+ return request({
+ url: '/quality/qualityUnqualified/deal',
+ method: 'post',
+ data: data,
+ })
}
\ No newline at end of file
diff --git a/src/api/qualityManagement/productionTrace.js b/src/api/qualityManagement/productionTrace.js
new file mode 100644
index 0000000..f900032
--- /dev/null
+++ b/src/api/qualityManagement/productionTrace.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 姝e悜杩芥函锛氭垚鍝佹壒鍙� -> 鍏ㄩ摼璺�
+export const traceForward = params => {
+ return request({
+ url: "/quality/trace/forward",
+ method: "get",
+ params,
+ });
+};
+
+// 鍙嶅悜杩芥函锛氬師鏂欐壒鍙� -> 鍙楀奖鍝嶆垚鍝�
+export const traceReverse = params => {
+ return request({
+ url: "/quality/trace/reverse",
+ method: "get",
+ params,
+ });
+};
+
+// 鎵瑰彿鍊欓�変笅鎷夛紙type: finished | material锛�
+export const traceBatchOptions = params => {
+ return request({
+ url: "/quality/trace/batchOptions",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/config.js b/src/config.js
index cc37beb..9fe25a9 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,7 +1,7 @@
// 搴旂敤鍏ㄥ眬閰嶇疆
const config = {
- baseUrl: "http://1.15.17.182:9072",
- fileUrl: "http://1.15.17.182:9071",
+ baseUrl: "http://1.15.17.182:9116",
+ fileUrl: "http://1.15.17.182:9115",
// 搴旂敤淇℃伅
appInfo: {
// 搴旂敤鍚嶇О
diff --git a/src/pages.json b/src/pages.json
index 1aa49ad..ce3fc60 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -353,6 +353,13 @@
}
},
{
+ "path": "pages/procurementManagement/procurementDemand/index",
+ "style": {
+ "navigationBarTitleText": "閲囪喘闇�姹�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/basicData/supplierManage/index",
"style": {
"navigationBarTitleText": "渚涘簲鍟嗙鐞�",
@@ -894,6 +901,34 @@
}
},
{
+ "path": "pages/productionManagement/pieceRateConfig/index",
+ "style": {
+ "navigationBarTitleText": "璁′欢鍗曚环閰嶇疆",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/outputStatistics/index",
+ "style": {
+ "navigationBarTitleText": "浜ч噺缁熻",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/materialConsumption/index",
+ "style": {
+ "navigationBarTitleText": "鏉愭枡娑堣�楁牳绠�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/costAccounting/index",
+ "style": {
+ "navigationBarTitleText": "鎴愭湰鏍哥畻鍒嗘瀽",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/inventoryManagement/receiptManagement/index",
"style": {
"navigationBarTitleText": "鑷畾涔夊叆搴�",
@@ -922,6 +957,20 @@
}
},
{
+ "path": "pages/inventoryManagement/stockCheck/index",
+ "style": {
+ "navigationBarTitleText": "搴撳瓨鐩樼偣",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/stockCheck/detail",
+ "style": {
+ "navigationBarTitleText": "鐩樼偣璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/safeProduction/safeQualifications/index",
"style": {
"navigationBarTitleText": "瑙勭▼涓庤祫璐�",
diff --git a/src/pages/inventoryManagement/stockCheck/detail.vue b/src/pages/inventoryManagement/stockCheck/detail.vue
new file mode 100644
index 0000000..d320220
--- /dev/null
+++ b/src/pages/inventoryManagement/stockCheck/detail.vue
@@ -0,0 +1,626 @@
+<template>
+ <view class="stock-check-detail">
+ <PageHeader title="鐩樼偣璇︽儏"
+ @back="goBack" />
+
+ <scroll-view class="scroll-body"
+ scroll-y
+ @scrolltolower="loadMore">
+ <!-- 涓昏〃淇℃伅 -->
+ <view class="main-card">
+ <view class="main-header">
+ <text class="main-no">{{ mainInfo.checkNo || '-' }}</text>
+ <up-tag :text="getStatusText(mainInfo.status)"
+ :type="getStatusType(mainInfo.status)"
+ size="mini" />
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣鍚嶇О</text>
+ <text class="detail-value">{{ mainInfo.checkName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣鏃ユ湡</text>
+ <text class="detail-value">{{ formatDate(mainInfo.checkDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣浜�</text>
+ <text class="detail-value">{{ mainInfo.checker || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣绉嶇被鏁�</text>
+ <text class="detail-value">{{ Number(mainInfo.totalKinds) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸紓绉嶇被鏁�</text>
+ <text class="detail-value"
+ :class="{ danger: Number(mainInfo.diffKinds) > 0 }">{{ Number(mainInfo.diffKinds) || 0 }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="mainInfo.remark">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ mainInfo.remark }}</text>
+ </view>
+ </view>
+
+ <!-- 宸ュ叿鏉� -->
+ <view class="toolbar">
+ <view class="switch-row">
+ <text class="switch-label">鍙湅宸紓</text>
+ <up-switch v-model="onlyDiff"
+ size="small"
+ activeColor="#2979ff"
+ @change="handleQuery"></up-switch>
+ </view>
+ <view class="toolbar-btns">
+ <up-button type="primary"
+ size="small"
+ plain
+ text="宸紓鍒嗘瀽"
+ @click="openDiffPopup"></up-button>
+ </view>
+ </view>
+
+ <!-- 鏄庣粏鍒楄〃 -->
+ <view v-if="detailList.length > 0">
+ <view v-for="row in detailList"
+ :key="row.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ row.productName || '-' }}</text>
+ </view>
+ <up-tag v-if="Number(row.adjusted) === 1"
+ text="宸茬敓鎴愯皟璐�"
+ type="success"
+ size="mini" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ row.productModel || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ row.unit || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵瑰彿</text>
+ <text class="detail-value">{{ row.batchNo || '-' }}</text>
+ </view>
+ <view class="qty-row">
+ <view class="qty-item">
+ <text class="qty-label">璐﹂潰鏁伴噺</text>
+ <text class="qty-value">{{ formatQty(row.bookQty) }}</text>
+ </view>
+ <view class="qty-item">
+ <text class="qty-label">瀹炵洏鏁伴噺</text>
+ <up-input class="qty-input"
+ type="digit"
+ :disabled="isFinished"
+ v-model="row.actualQty"
+ placeholder="鏈綍鍏�"
+ @change="onQtyChange(row)"
+ @blur="onQtyBlur(row)" />
+ </view>
+ <view class="qty-item">
+ <text class="qty-label">宸紓</text>
+ <text class="qty-value"
+ :class="diffClass(row)">{{ formatDiff(row) }}</text>
+ </view>
+ </view>
+ <view class="detail-row remark-row">
+ <text class="detail-label">澶囨敞</text>
+ <up-input class="remark-input"
+ :disabled="isFinished"
+ v-model="row.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ maxlength="200"
+ @change="onRemarkChange(row)" />
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus" />
+ </view>
+ <view v-else-if="!loading"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鐩樼偣鏄庣粏"></up-empty>
+ </view>
+ <view class="tip-text"
+ v-if="!isFinished">
+ 鎻愮ず锛氬畬鎴愮洏鐐瑰悗宸紓灏嗕互寰呭鏍稿嚭鍏ュ簱璁板綍褰㈠紡瀛樺湪锛岄渶鍦ㄥ叆搴撹褰�/鍑哄簱璁板綍椤靛鏍搁�氳繃鍚庡簱瀛樻墠鍙樺姩銆�
+ </view>
+ </scroll-view>
+
+ <!-- 搴曢儴鎿嶄綔 -->
+ <view class="footer-btns">
+ <up-button type="primary"
+ text="鎵归噺淇濆瓨"
+ :loading="saving"
+ :disabled="isFinished"
+ @click="handleSave"></up-button>
+ <up-button v-if="!isFinished"
+ type="success"
+ text="瀹屾垚鐩樼偣"
+ @click="handleFinish"></up-button>
+ </view>
+
+ <!-- 宸紓鍒嗘瀽寮圭獥 -->
+ <up-popup :show="showDiffPopup"
+ mode="bottom"
+ :round="10"
+ closeable
+ @close="showDiffPopup = false">
+ <view class="form-popup">
+ <view class="popup-header">
+ <text class="popup-title">宸紓鍒嗘瀽</text>
+ </view>
+ <scroll-view class="popup-content"
+ scroll-y>
+ <view v-if="diffRows.length">
+ <view v-for="(row, index) in diffRows"
+ :key="row.id || index"
+ class="diff-item">
+ <view class="diff-head">
+ <text class="diff-name">{{ row.productName || '-' }}</text>
+ <text class="diff-qty"
+ :class="Number(row.diffQty) > 0 ? 'diff-up' : 'diff-down'">{{ formatSigned(row.diffQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ row.productModel || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵瑰彿</text>
+ <text class="detail-value">{{ row.batchNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璐﹂潰 / 瀹炵洏</text>
+ <text class="detail-value">{{ formatQty(row.bookQty) }} / {{ formatQty(row.actualQty) }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="spec-empty-box">
+ <up-empty mode="data"
+ text="鏆傛棤宸紓鏁版嵁"></up-empty>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { toast } from "@/utils/common";
+ import {
+ getDetail,
+ detailPage,
+ entry,
+ finish,
+ diffList,
+ } from "@/api/inventoryManagement/stockCheck.js";
+
+ const mainId = ref(null);
+ const mainInfo = ref({});
+ const detailList = ref([]);
+ const loading = ref(false);
+ const saving = ref(false);
+ const loadStatus = ref("loadmore");
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const onlyDiff = ref(false);
+ const dirtyIds = ref([]);
+
+ const showDiffPopup = ref(false);
+ const diffRows = ref([]);
+
+ const isFinished = computed(() => Number(mainInfo.value.status) !== 0);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const formatDate = val => (val ? dayjs(val).format("YYYY-MM-DD") : "-");
+
+ const getStatusText = status => {
+ const map = { 0: "鐩樼偣涓�", 1: "宸插畬鎴�", 2: "宸插彇娑�" };
+ return map[status] || "鏈煡";
+ };
+
+ const getStatusType = status => {
+ const map = { 0: "warning", 1: "success", 2: "info" };
+ return map[status] || "info";
+ };
+
+ const formatQty = val => {
+ if (val === null || val === undefined || val === "") return "-";
+ return Number(val) || 0;
+ };
+
+ const formatSigned = val => {
+ const n = Number(val) || 0;
+ return n > 0 ? `+${n}` : `${n}`;
+ };
+
+ const formatDiff = row => {
+ const a = row.actualQty;
+ if (a === null || a === undefined || a === "") return "-";
+ const d = Number(a) - (Number(row.bookQty) || 0);
+ if (!Number.isFinite(d)) return "-";
+ return formatSigned(Number(d.toFixed(4)));
+ };
+
+ const diffClass = row => {
+ const a = row.actualQty;
+ if (a === null || a === undefined || a === "") return "";
+ const d = Number(a) - (Number(row.bookQty) || 0);
+ if (d > 0) return "diff-up";
+ if (d < 0) return "diff-down";
+ return "";
+ };
+
+ const loadMain = async () => {
+ try {
+ const res = await getDetail(mainId.value);
+ mainInfo.value = res?.data || {};
+ } catch (e) {
+ // 璇锋眰澶辫触宸茬敱 request 缁熶竴鎻愮ず
+ }
+ };
+
+ const getList = () => {
+ if (loading.value) return;
+ loading.value = true;
+ loadStatus.value = "loading";
+ detailPage({
+ mainId: mainId.value,
+ onlyDiff: onlyDiff.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ const records = res?.data?.records || [];
+ detailList.value =
+ page.current === 1 ? records : [...detailList.value, ...records];
+ page.total = res?.data?.total || 0;
+ loadStatus.value =
+ detailList.value.length >= page.total ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ loadStatus.value = "loadmore";
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ detailList.value = [];
+ dirtyIds.value = [];
+ getList();
+ };
+
+ const loadMore = () => {
+ if (loadStatus.value === "nomore" || loading.value) return;
+ page.current++;
+ getList();
+ };
+
+ const markDirty = row => {
+ if (row.id == null) return;
+ if (!dirtyIds.value.includes(row.id)) dirtyIds.value.push(row.id);
+ };
+
+ const onQtyChange = row => {
+ markDirty(row);
+ };
+
+ const onRemarkChange = row => {
+ markDirty(row);
+ };
+
+ // 瀹炵洏鏁伴噺涓嶅厑璁歌礋鏁�
+ const onQtyBlur = row => {
+ const val = row.actualQty;
+ if (val === null || val === undefined || val === "") return;
+ let n = Number(val);
+ if (!Number.isFinite(n)) {
+ row.actualQty = "";
+ return;
+ }
+ if (n < 0) {
+ n = 0;
+ toast("瀹炵洏鏁伴噺涓嶈兘涓鸿礋鏁帮紝宸茬疆涓� 0");
+ }
+ row.actualQty = n;
+ markDirty(row);
+ };
+
+ const handleSave = async () => {
+ if (isFinished.value) {
+ toast("鐩樼偣宸茬粨鏉燂紝鏃犳硶褰曞叆");
+ return;
+ }
+ const rows = detailList.value.filter(
+ r => dirtyIds.value.includes(r.id) && r.actualQty !== "" && r.actualQty !== null && r.actualQty !== undefined
+ );
+ if (!rows.length) {
+ toast("娌℃湁闇�瑕佷繚瀛樼殑瀹炵洏褰曞叆");
+ return;
+ }
+ const negative = rows.find(r => Number(r.actualQty) < 0);
+ if (negative) {
+ toast("瀹炵洏鏁伴噺涓嶈兘涓鸿礋鏁�");
+ return;
+ }
+ saving.value = true;
+ uni.showLoading({ title: "鎻愪氦涓�...", mask: true });
+ try {
+ await entry(
+ rows.map(r => ({
+ id: r.id,
+ actualQty: Number(r.actualQty) || 0,
+ remark: r.remark || undefined,
+ }))
+ );
+ uni.hideLoading();
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ dirtyIds.value = [];
+ loadMain();
+ handleQuery();
+ } catch (e) {
+ uni.hideLoading();
+ } finally {
+ saving.value = false;
+ }
+ };
+
+ const handleFinish = () => {
+ uni.showModal({
+ title: "瀹屾垚鐩樼偣",
+ content: "宸紓灏嗙敓鎴愯皟璐﹀崟鎹紝瀹℃牳閫氳繃鍚庤处闈㈢敓鏁堬紝鏄惁缁х画锛�",
+ success: async res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ try {
+ const r = await finish({ id: mainId.value });
+ uni.hideLoading();
+ uni.showToast({ title: r?.msg || "鐩樼偣宸插畬鎴�", icon: "none" });
+ loadMain();
+ handleQuery();
+ } catch (e) {
+ uni.hideLoading();
+ }
+ },
+ });
+ };
+
+ const openDiffPopup = async () => {
+ showDiffPopup.value = true;
+ try {
+ const res = await diffList({ mainId: mainId.value });
+ diffRows.value = res?.data || [];
+ } catch (e) {
+ diffRows.value = [];
+ }
+ };
+
+ onLoad(options => {
+ mainId.value = options?.id;
+ if (!mainId.value) {
+ toast("缂哄皯鐩樼偣鍗曞弬鏁�");
+ return;
+ }
+ loadMain();
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .stock-check-detail {
+ height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .scroll-body {
+ flex: 1;
+ height: 0;
+ padding: 20rpx 20rpx 120rpx;
+ box-sizing: border-box;
+ }
+
+ .main-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ .main-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-bottom: 16rpx;
+ margin-bottom: 16rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .main-no {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #303133;
+ }
+ }
+ }
+
+ .detail-value.danger {
+ color: #ee0a24;
+ font-weight: 600;
+ }
+
+ .toolbar {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 16rpx 24rpx;
+ margin-bottom: 20rpx;
+
+ .switch-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 0 16rpx;
+
+ .switch-label {
+ font-size: 28rpx;
+ color: #333;
+ }
+ }
+
+ .toolbar-btns {
+ display: flex;
+ gap: 20rpx;
+ justify-content: flex-end;
+ }
+ }
+
+ .qty-row {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ margin: 16rpx 0;
+
+ .qty-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .qty-label {
+ font-size: 22rpx;
+ color: #999;
+ margin-bottom: 8rpx;
+ }
+
+ .qty-value {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #303133;
+
+ &.diff-up {
+ color: #52c41a;
+ }
+
+ &.diff-down {
+ color: #f56c6c;
+ }
+ }
+
+ .qty-input {
+ width: 100%;
+ background: #f5f7fa;
+ border-radius: 8rpx;
+ padding: 0 8rpx;
+ }
+ }
+ }
+
+ .remark-row {
+ align-items: center;
+
+ .remark-input {
+ flex: 1;
+ background: #f5f7fa;
+ border-radius: 8rpx;
+ padding: 0 12rpx;
+ margin-left: 16rpx;
+ }
+ }
+
+ .no-data {
+ padding-top: 150rpx;
+ }
+
+ .tip-text {
+ font-size: 22rpx;
+ color: #909399;
+ line-height: 1.6;
+ padding: 16rpx 8rpx 40rpx;
+ }
+
+ .form-popup {
+ height: 70vh;
+ display: flex;
+ flex-direction: column;
+ background: #f5f5f5;
+ }
+
+ .popup-content {
+ flex: 1;
+ height: 0;
+ padding: 12px;
+ }
+
+ .diff-item {
+ background: #fff;
+ border-radius: 10px;
+ padding: 12px;
+ margin-bottom: 10px;
+
+ .diff-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+
+ .diff-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .diff-qty {
+ font-size: 16px;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .diff-up {
+ color: #52c41a;
+ }
+
+ .diff-down {
+ color: #f56c6c;
+ }
+
+ .spec-empty-box {
+ padding-top: 60px;
+ }
+
+ .popup-footer {
+ display: flex;
+ gap: 12px;
+ padding: 12px 16px;
+ background: #fff;
+ border-top: 1px solid #f0f0f0;
+ }
+
+ .btn-cancel,
+ .btn-confirm {
+ flex: 1;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/stockCheck/index.vue b/src/pages/inventoryManagement/stockCheck/index.vue
new file mode 100644
index 0000000..48fd49d
--- /dev/null
+++ b/src/pages/inventoryManagement/stockCheck/index.vue
@@ -0,0 +1,832 @@
+<template>
+ <view class="stock-check-page">
+ <PageHeader title="搴撳瓨鐩樼偣"
+ @back="goBack" />
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-row">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="searchForm.checkNo"
+ placeholder="鐩樼偣鍗曞彿"
+ clearable
+ @confirm="handleQuery" />
+ </view>
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="searchForm.checkName"
+ placeholder="鐩樼偣鍚嶇О"
+ clearable
+ @confirm="handleQuery" />
+ </view>
+ </view>
+ <view class="filter-row">
+ <view class="filter-item"
+ @click="showStatusSheet = true">
+ <text class="filter-text">{{ statusText }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#909399"></up-icon>
+ </view>
+ <view class="filter-item date-item"
+ @click="showCalendar = true">
+ <up-icon name="calendar"
+ size="16"
+ color="#909399"></up-icon>
+ <text class="filter-text"
+ :class="{ placeholder: !searchForm.beginTime }">{{ dateRangeText }}</text>
+ <up-icon v-if="searchForm.beginTime"
+ name="close-circle-fill"
+ size="16"
+ color="#c0c4cc"
+ @click.stop="handleClearDate"></up-icon>
+ </view>
+ <view class="search-btn">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click="handleQuery"></up-button>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍒楄〃鍖哄煙 -->
+ <scroll-view scroll-y
+ class="list-container"
+ v-if="tableData.length > 0"
+ @scrolltolower="loadMore">
+ <view v-for="item in tableData"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.checkNo || '-' }}</text>
+ </view>
+ <view class="item-right">
+ <up-tag :text="getStatusText(item.status)"
+ :type="getStatusType(item.status)"
+ size="mini" />
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣鍚嶇О</text>
+ <text class="detail-value">{{ item.checkName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣鏃ユ湡</text>
+ <text class="detail-value">{{ formatDate(item.checkDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣浜�</text>
+ <text class="detail-value">{{ item.checker || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐩樼偣绉嶇被鏁�</text>
+ <text class="detail-value">{{ Number(item.totalKinds) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸紓绉嶇被鏁�</text>
+ <text class="detail-value"
+ :class="{ danger: Number(item.diffKinds) > 0 }">{{ Number(item.diffKinds) || 0 }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button type="info"
+ size="small"
+ plain
+ text="璇︽儏"
+ @click="goDetail(item)"></up-button>
+ <up-button v-if="item.status === 0"
+ type="success"
+ size="small"
+ plain
+ text="瀹屾垚"
+ @click="handleFinish(item)"></up-button>
+ <up-button v-if="item.status === 0"
+ type="error"
+ size="small"
+ plain
+ text="鍙栨秷"
+ @click="handleCancel(item)"></up-button>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus" />
+ </scroll-view>
+ <view v-else-if="!loading"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鐩樼偣鏁版嵁"></up-empty>
+ </view>
+
+ <!-- 搴曢儴鎿嶄綔 -->
+ <view class="footer-btns">
+ <up-button type="primary"
+ text="鍙戣捣鐩樼偣"
+ @click="openAdd"></up-button>
+ </view>
+
+ <!-- 鍙戣捣鐩樼偣寮圭獥 -->
+ <up-popup :show="showAddPopup"
+ mode="bottom"
+ :round="10"
+ closeable
+ @close="showAddPopup = false">
+ <view class="form-popup">
+ <view class="popup-header">
+ <text class="popup-title">鍙戣捣鐩樼偣</text>
+ </view>
+ <scroll-view class="popup-content"
+ scroll-y>
+ <view class="form-field">
+ <text class="field-label required">鐩樼偣鍚嶇О</text>
+ <up-input v-model="form.checkName"
+ placeholder="璇疯緭鍏ョ洏鐐瑰悕绉�"
+ maxlength="100" />
+ </view>
+ <view class="form-field">
+ <text class="field-label">鐩樼偣鏃ユ湡</text>
+ <view class="pick-field"
+ @click="showDatePicker = true">
+ <up-input class="pick-input"
+ v-model="form.checkDate"
+ readonly
+ placeholder="璇烽�夋嫨鐩樼偣鏃ユ湡" />
+ <up-icon name="arrow-right"
+ color="#c0c4cc"></up-icon>
+ </view>
+ </view>
+ <view class="form-field">
+ <text class="field-label">鐩樼偣浜�</text>
+ <up-input v-model="form.checker"
+ placeholder="璇疯緭鍏ョ洏鐐逛汉" />
+ </view>
+ <view class="form-field">
+ <text class="field-label">澶囨敞</text>
+ <up-input v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ maxlength="200" />
+ </view>
+ <view class="form-field">
+ <text class="field-label">鎸囧畾浜у搧瑙勬牸</text>
+ <view class="spec-box">
+ <view v-if="selectedModels.length"
+ class="spec-tags">
+ <view v-for="m in selectedModels"
+ :key="m.id"
+ class="spec-tag"
+ @click="removeModel(m.id)">
+ <text class="spec-tag-text">{{ m.model || m.productName }}</text>
+ <up-icon name="close"
+ size="12"
+ color="#2979ff"></up-icon>
+ </view>
+ </view>
+ <text v-else
+ class="spec-empty">鏈�夋嫨鏃跺鍏ㄩ儴搴撳瓨琛屽仛蹇収</text>
+ <up-button type="primary"
+ size="small"
+ plain
+ text="閫夋嫨瑙勬牸"
+ @click="openSpecPicker"></up-button>
+ </view>
+ </view>
+ </scroll-view>
+ <view class="popup-footer">
+ <up-button class="btn-cancel"
+ text="鍙栨秷"
+ @click="showAddPopup = false"></up-button>
+ <up-button class="btn-confirm"
+ type="primary"
+ text="纭"
+ :loading="submitting"
+ @click="handleSubmit"></up-button>
+ </view>
+ </view>
+ </up-popup>
+
+ <!-- 瑙勬牸閫夋嫨寮圭獥 -->
+ <up-popup :show="showSpecPopup"
+ mode="bottom"
+ :round="10"
+ closeable
+ @close="showSpecPopup = false">
+ <view class="form-popup">
+ <view class="popup-header">
+ <text class="popup-title">閫夋嫨浜у搧瑙勬牸</text>
+ </view>
+ <view class="spec-category">
+ <scroll-view scroll-x
+ class="category-scroll">
+ <view class="category-list">
+ <view v-for="p in productOptions"
+ :key="p.id"
+ class="category-tag"
+ :class="{ active: p.id === activeProductId }"
+ @click="selectProduct(p)">
+ <text>{{ p.productName }}</text>
+ </view>
+ </view>
+ </scroll-view>
+ </view>
+ <scroll-view class="popup-content"
+ scroll-y>
+ <view v-if="modelOptions.length">
+ <view v-for="m in modelOptions"
+ :key="m.id"
+ class="model-item"
+ @click="toggleModel(m)">
+ <view class="model-info">
+ <text class="model-name">{{ m.model || '-' }}</text>
+ <text class="model-unit">{{ m.unit || '' }}</text>
+ </view>
+ <up-icon :name="isSelected(m.id) ? 'checkmark-circle-fill' : 'circle'"
+ size="22"
+ :color="isSelected(m.id) ? '#2979ff' : '#c0c4cc'"></up-icon>
+ </view>
+ </view>
+ <view v-else
+ class="spec-empty-box">
+ <up-empty mode="data"
+ text="鏆傛棤瑙勬牸鏁版嵁"></up-empty>
+ </view>
+ </scroll-view>
+ <view class="popup-footer">
+ <up-button class="btn-cancel"
+ text="娓呯┖"
+ @click="selectedModels = []"></up-button>
+ <up-button class="btn-confirm"
+ type="primary"
+ text="纭畾"
+ @click="showSpecPopup = false"></up-button>
+ </view>
+ </view>
+ </up-popup>
+
+ <!-- 鐘舵�佺瓫閫� -->
+ <up-action-sheet :show="showStatusSheet"
+ :actions="statusActions"
+ title="鐩樼偣鐘舵��"
+ @select="onStatusSelect"
+ @close="showStatusSheet = false"></up-action-sheet>
+
+ <!-- 鏃ユ湡鍖洪棿 -->
+ <up-calendar :show="showCalendar"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2022-01-01"
+ :monthNum="monthNum"
+ @confirm="onDateConfirm"
+ @close="showCalendar = false"></up-calendar>
+
+ <!-- 鐩樼偣鏃ユ湡 -->
+ <up-popup :show="showDatePicker"
+ mode="bottom"
+ @close="showDatePicker = false">
+ <up-datetime-picker :show="true"
+ v-model="dateValue"
+ mode="date"
+ @confirm="onCheckDateConfirm"
+ @cancel="showDatePicker = false" />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { toast } from "@/utils/common";
+ import { productTreeList, modelList } from "@/api/basicData/product.js";
+ import {
+ start,
+ listPage,
+ finish,
+ cancel,
+ } from "@/api/inventoryManagement/stockCheck.js";
+
+ const loading = ref(false);
+ const submitting = ref(false);
+ const loadStatus = ref("loadmore");
+ const tableData = ref([]);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+
+ const searchForm = reactive({
+ checkNo: "",
+ checkName: "",
+ status: null,
+ beginTime: "",
+ endTime: "",
+ });
+
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const monthNum = computed(() => {
+ const min = dayjs("2022-01-01");
+ const max = dayjs(maxDate);
+ return max.diff(min, "month") + 1;
+ });
+ const showCalendar = ref(false);
+ const showStatusSheet = ref(false);
+ const showDatePicker = ref(false);
+ const dateValue = ref(new Date().getTime());
+
+ const statusActions = ref([
+ { name: "鍏ㄩ儴", value: null },
+ { name: "鐩樼偣涓�", value: 0 },
+ { name: "宸插畬鎴�", value: 1 },
+ { name: "宸插彇娑�", value: 2 },
+ ]);
+
+ const statusText = computed(() => {
+ const hit = statusActions.value.find(a => a.value === searchForm.status);
+ return hit ? hit.name : "鍏ㄩ儴";
+ });
+
+ const dateRangeText = computed(() => {
+ if (searchForm.beginTime && searchForm.endTime) {
+ return `${searchForm.beginTime.slice(0, 10)} 鑷� ${searchForm.endTime.slice(0, 10)}`;
+ }
+ return "閫夋嫨鏃ユ湡鍖洪棿";
+ });
+
+ // 鍙戣捣鐩樼偣琛ㄥ崟
+ const showAddPopup = ref(false);
+ const form = reactive({
+ checkName: "",
+ checkDate: dayjs().format("YYYY-MM-DD"),
+ checker: "",
+ remark: "",
+ });
+
+ // 瑙勬牸閫夋嫨
+ const showSpecPopup = ref(false);
+ const productOptions = ref([]);
+ const modelOptions = ref([]);
+ const activeProductId = ref(null);
+ const selectedModels = ref([]);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const formatDate = val => (val ? dayjs(val).format("YYYY-MM-DD") : "-");
+
+ const getStatusText = status => {
+ const map = { 0: "鐩樼偣涓�", 1: "宸插畬鎴�", 2: "宸插彇娑�" };
+ return map[status] || "鏈煡";
+ };
+
+ const getStatusType = status => {
+ const map = { 0: "warning", 1: "success", 2: "info" };
+ return map[status] || "info";
+ };
+
+ const getList = () => {
+ loading.value = true;
+ loadStatus.value = "loading";
+ listPage({
+ current: page.current,
+ size: page.size,
+ checkNo: searchForm.checkNo,
+ checkName: searchForm.checkName,
+ status: searchForm.status,
+ beginTime: searchForm.beginTime,
+ endTime: searchForm.endTime,
+ })
+ .then(res => {
+ const records = res?.data?.records || [];
+ tableData.value =
+ page.current === 1 ? records : [...tableData.value, ...records];
+ page.total = res?.data?.total || 0;
+ loadStatus.value =
+ tableData.value.length >= page.total ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ loadStatus.value = "loadmore";
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+ };
+
+ const loadMore = () => {
+ if (loadStatus.value === "nomore" || loading.value) return;
+ page.current++;
+ getList();
+ };
+
+ const onStatusSelect = item => {
+ searchForm.status = item.value;
+ showStatusSheet.value = false;
+ handleQuery();
+ };
+
+ const onDateConfirm = e => {
+ const list = Array.isArray(e) ? e : [e];
+ searchForm.beginTime = `${list[0]} 00:00:00`;
+ searchForm.endTime = `${list[list.length - 1]} 23:59:59`;
+ showCalendar.value = false;
+ handleQuery();
+ };
+
+ const handleClearDate = () => {
+ searchForm.beginTime = "";
+ searchForm.endTime = "";
+ handleQuery();
+ };
+
+ const onCheckDateConfirm = e => {
+ form.checkDate = dayjs(e.value).format("YYYY-MM-DD");
+ showDatePicker.value = false;
+ };
+
+ const openAdd = () => {
+ form.checkName = "";
+ form.checkDate = dayjs().format("YYYY-MM-DD");
+ form.checker = "";
+ form.remark = "";
+ selectedModels.value = [];
+ showAddPopup.value = true;
+ };
+
+ const openSpecPicker = async () => {
+ showSpecPopup.value = true;
+ if (productOptions.value.length) return;
+ try {
+ const res = await productTreeList();
+ const list = Array.isArray(res) ? res : res?.data || [];
+ productOptions.value = list
+ .filter(item => item.parentId === null)
+ .map(item => ({ id: item.id, productName: item.productName }));
+ if (productOptions.value.length) {
+ selectProduct(productOptions.value[0]);
+ }
+ } catch (e) {
+ // 璇锋眰澶辫触宸茬敱 request 缁熶竴鎻愮ず
+ }
+ };
+
+ const selectProduct = async p => {
+ activeProductId.value = p.id;
+ modelOptions.value = [];
+ try {
+ const res = await modelList({ id: p.id });
+ const list = Array.isArray(res) ? res : res?.data || [];
+ modelOptions.value = list.map(m => ({
+ id: m.id,
+ model: m.model,
+ unit: m.unit,
+ productName: p.productName,
+ }));
+ } catch (e) {
+ // 璇锋眰澶辫触宸茬敱 request 缁熶竴鎻愮ず
+ }
+ };
+
+ const isSelected = id => selectedModels.value.some(m => m.id === id);
+
+ const toggleModel = m => {
+ const index = selectedModels.value.findIndex(item => item.id === m.id);
+ if (index > -1) {
+ selectedModels.value.splice(index, 1);
+ } else {
+ selectedModels.value.push(m);
+ }
+ };
+
+ const removeModel = id => {
+ const index = selectedModels.value.findIndex(item => item.id === id);
+ if (index > -1) selectedModels.value.splice(index, 1);
+ };
+
+ const handleSubmit = async () => {
+ if (!form.checkName || !form.checkName.trim()) {
+ toast("璇疯緭鍏ョ洏鐐瑰悕绉�");
+ return;
+ }
+ submitting.value = true;
+ uni.showLoading({ title: "鎻愪氦涓�...", mask: true });
+ try {
+ await start({
+ checkName: form.checkName.trim(),
+ checkDate: form.checkDate,
+ checker: form.checker,
+ remark: form.remark,
+ productModelIds: selectedModels.value.map(m => m.id),
+ });
+ uni.hideLoading();
+ uni.showToast({ title: "鍙戣捣鎴愬姛", icon: "success" });
+ showAddPopup.value = false;
+ handleQuery();
+ } catch (e) {
+ uni.hideLoading();
+ } finally {
+ submitting.value = false;
+ }
+ };
+
+ const goDetail = item => {
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/stockCheck/detail?id=${item.id}`,
+ });
+ };
+
+ const handleFinish = item => {
+ uni.showModal({
+ title: "瀹屾垚鐩樼偣",
+ content: "宸紓灏嗙敓鎴愯皟璐﹀崟鎹紝瀹℃牳閫氳繃鍚庤处闈㈢敓鏁堬紝鏄惁缁х画锛�",
+ success: async res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ try {
+ const r = await finish({ id: item.id });
+ uni.hideLoading();
+ uni.showToast({ title: r?.msg || "鐩樼偣宸插畬鎴�", icon: "none" });
+ handleQuery();
+ } catch (e) {
+ uni.hideLoading();
+ }
+ },
+ });
+ };
+
+ const handleCancel = item => {
+ uni.showModal({
+ title: "鍙栨秷鐩樼偣",
+ content: "鍙栨秷鍚庤鐩樼偣鍗曚笉鍙啀褰曞叆锛屾槸鍚︾户缁紵",
+ success: async res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ try {
+ const r = await cancel({ id: item.id });
+ uni.hideLoading();
+ uni.showToast({ title: r?.msg || "宸插彇娑�", icon: "none" });
+ handleQuery();
+ } catch (e) {
+ uni.hideLoading();
+ }
+ },
+ });
+ };
+
+ onShow(() => {
+ handleQuery();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .stock-check-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .search-section {
+ background: #fff;
+ padding: 10px 12px;
+ }
+
+ .search-row {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 10px;
+
+ .search-input {
+ flex: 1;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ }
+ }
+
+ .filter-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ .filter-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ height: 34px;
+ font-size: 13px;
+ color: #333;
+
+ &.date-item {
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .filter-text {
+ font-size: 13px;
+ color: #333;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.placeholder {
+ color: #999;
+ }
+ }
+ }
+
+ .search-btn {
+ width: 70px;
+ }
+ }
+
+ .list-container {
+ flex: 1;
+ height: 0;
+ padding: 20rpx 20rpx 140rpx;
+ box-sizing: border-box;
+ }
+
+ .detail-value.danger {
+ color: #ee0a24;
+ font-weight: 600;
+ }
+
+ .action-buttons {
+ display: flex;
+ gap: 12px;
+ padding: 16rpx 0 0;
+ justify-content: flex-end;
+ }
+
+ .no-data {
+ padding-top: 150rpx;
+ }
+
+ .form-popup {
+ height: 70vh;
+ display: flex;
+ flex-direction: column;
+ background: #f5f5f5;
+ }
+
+ .popup-content {
+ flex: 1;
+ height: 0;
+ padding: 12px;
+ }
+
+ .form-field {
+ background: #fff;
+ border-radius: 10px;
+ padding: 12px;
+ margin-bottom: 12px;
+ }
+
+ // readonly 鐨� up-input 浼氭妸鍘熺敓 input 缃负 disabled锛宒isabled 鍏冪礌浼氬悶鎺夌偣鍑讳簨浠讹紝
+ // 鏁呰 input 涓嶅弬涓庢寚閽堜簨浠讹紝鐢卞灞� .pick-field 鎺ョ鐐瑰嚮
+ .pick-field {
+ display: flex;
+ align-items: center;
+
+ .pick-input {
+ flex: 1;
+ }
+
+ :deep(input) {
+ pointer-events: none;
+ }
+ }
+
+ .field-label {
+ display: block;
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 8px;
+
+ &.required::before {
+ content: "*";
+ color: #ff0000;
+ margin-right: 4px;
+ }
+ }
+
+ .spec-box {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .spec-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .spec-tag {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: #ecf5ff;
+ color: #2979ff;
+ border-radius: 6px;
+ padding: 4px 10px;
+
+ .spec-tag-text {
+ font-size: 12px;
+ }
+ }
+
+ .spec-empty {
+ font-size: 12px;
+ color: #999;
+ }
+
+ .spec-category {
+ background: #fff;
+ padding: 8px 0;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .category-scroll {
+ white-space: nowrap;
+ }
+
+ .category-list {
+ display: inline-flex;
+ gap: 10px;
+ padding: 0 12px;
+ }
+
+ .category-tag {
+ padding: 6px 14px;
+ border-radius: 20px;
+ background: #f5f5f5;
+ color: #666;
+ font-size: 13px;
+ white-space: nowrap;
+
+ &.active {
+ background: #2979ff;
+ color: #fff;
+ }
+ }
+
+ .model-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #fff;
+ border-radius: 10px;
+ padding: 14px 12px;
+ margin-bottom: 8px;
+
+ .model-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .model-name {
+ font-size: 14px;
+ color: #333;
+ }
+
+ .model-unit {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+
+ .spec-empty-box {
+ padding-top: 60px;
+ }
+
+ .popup-footer {
+ display: flex;
+ gap: 12px;
+ padding: 12px 16px;
+ background: #fff;
+ border-top: 1px solid #f0f0f0;
+ }
+
+ .btn-cancel,
+ .btn-confirm {
+ flex: 1;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/index.vue b/src/pages/inventoryManagement/stockManagement/index.vue
index 45d27ad..49e1758 100644
--- a/src/pages/inventoryManagement/stockManagement/index.vue
+++ b/src/pages/inventoryManagement/stockManagement/index.vue
@@ -2,39 +2,141 @@
<view class="app-container">
<PageHeader title="搴撳瓨绠$悊"
@back="goBack" />
- <view v-if="loading"
- class="loading-state">
- <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ <!-- 搴撳瓨棰勮寮�鍏� -->
+ <view class="warn-bar">
+ <view class="warn-left">
+ <text class="warn-label">鍙湅棰勮</text>
+ <up-switch v-model="warnOnly"
+ size="small"
+ activeColor="#f56c6c"
+ @change="handleWarnToggle"></up-switch>
+ </view>
</view>
+ <!-- 棰勮鍒楄〃 -->
+ <template v-if="warnOnly">
+ <view class="warn-search">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="warnSearch.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @confirm="handleWarnQuery" />
+ </view>
+ <view class="filter-button"
+ @click="handleWarnQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <scroll-view scroll-y
+ class="warn-list"
+ v-if="warnList.length > 0"
+ @scrolltolower="loadWarnMore">
+ <view v-for="item in warnList"
+ :key="item.id"
+ class="ledger-item warn-item"
+ :class="{ 'is-warn': isWarnRow(item) }">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon warn-icon">
+ <up-icon name="warning"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.productName || '-' }}</text>
+ </view>
+ <up-tag text="棰勮"
+ type="error"
+ size="mini"
+ v-if="isWarnRow(item)" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏁伴噺</text>
+ <text class="detail-value">{{ formatQty(item.qualitity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閿佸畾鏁伴噺</text>
+ <text class="detail-value">{{ formatQty(item.lockedQuantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙敤鏁伴噺</text>
+ <text class="detail-value"
+ :class="{ 'warn-value': isWarnRow(item) }">{{ formatQty(item.un_locked_quantity ?? item.unLockedQuantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">棰勮鍊�</text>
+ <text class="detail-value">{{ formatQty(item.warnNum) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵瑰彿</text>
+ <text class="detail-value">{{ item.batchNo || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="warnStatus" />
+ </scroll-view>
+ <view v-else-if="!warnLoading"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤棰勮搴撳瓨"></up-empty>
+ </view>
+ </template>
+ <!-- 搴撳瓨鍒楄〃 -->
<template v-else>
- <up-tabs :list="tabs"
- @click="handleTabClick"
- :current="activeTab" />
- <swiper class="swiper-box"
- :current="activeTab"
- @change="handleSwiperChange">
- <swiper-item class="swiper-item"
- v-for="tab in products"
- :key="tab.id">
- <record :product-id="tab.id"
- v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
- </swiper-item>
- </swiper>
+ <view v-if="loading"
+ class="loading-state">
+ <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ </view>
+ <template v-else>
+ <up-tabs :list="tabs"
+ @click="handleTabClick"
+ :current="activeTab" />
+ <swiper class="swiper-box"
+ :current="activeTab"
+ @change="handleSwiperChange">
+ <swiper-item class="swiper-item"
+ v-for="tab in products"
+ :key="tab.id">
+ <record :product-id="tab.id"
+ v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
+ </swiper-item>
+ </swiper>
+ </template>
</template>
</view>
</template>
<script setup>
- import { ref, onMounted } from "vue";
+ import { ref, reactive, onMounted } from "vue";
import PageHeader from "@/components/PageHeader.vue";
import Record from "./Record.vue";
import { productTreeList } from "@/api/basicData/product.js";
+ import { getStockWarnPage } from "@/api/inventoryManagement/stockInventory.js";
const activeTab = ref(0);
const tabs = ref([]);
const products = ref([]);
const loading = ref(false);
const initializedTabs = ref([]);
+
+ // 棰勮
+ const warnOnly = ref(false);
+ const warnLoading = ref(false);
+ const warnStatus = ref("loadmore");
+ const warnList = ref([]);
+ const warnPage = reactive({ current: 1, size: 10, total: 0 });
+ const warnSearch = reactive({ productName: "" });
const handleTabClick = item => {
activeTab.value = item.index;
@@ -74,12 +176,74 @@
uni.navigateBack();
};
+ // 棰勮鐩稿叧
+ const formatQty = val => {
+ if (val === null || val === undefined || val === "") return "-";
+ return Number(val) || 0;
+ };
+
+ const getAvailable = item => {
+ const val = item.un_locked_quantity ?? item.unLockedQuantity;
+ return Number(val) || 0;
+ };
+
+ // 鍙敤鏁伴噺 鈮� 棰勮鍊� 涓� 棰勮鍊�>0
+ const isWarnRow = item => {
+ const warnNum = Number(item.warnNum) || 0;
+ if (warnNum <= 0) return false;
+ return getAvailable(item) <= warnNum;
+ };
+
+ const getWarnList = () => {
+ warnLoading.value = true;
+ warnStatus.value = "loading";
+ getStockWarnPage({
+ current: warnPage.current,
+ size: warnPage.size,
+ productName: warnSearch.productName,
+ })
+ .then(res => {
+ const records = res?.data?.records || [];
+ warnList.value =
+ warnPage.current === 1 ? records : [...warnList.value, ...records];
+ warnPage.total = res?.data?.total || 0;
+ warnStatus.value =
+ warnList.value.length >= warnPage.total ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ warnStatus.value = "loadmore";
+ })
+ .finally(() => {
+ warnLoading.value = false;
+ });
+ };
+
+ const handleWarnQuery = () => {
+ warnPage.current = 1;
+ warnList.value = [];
+ getWarnList();
+ };
+
+ const loadWarnMore = () => {
+ if (warnStatus.value === "nomore" || warnLoading.value) return;
+ warnPage.current++;
+ getWarnList();
+ };
+
+ const handleWarnToggle = () => {
+ if (warnOnly.value && !warnList.value.length) {
+ handleWarnQuery();
+ }
+ };
+
onMounted(() => {
fetchProducts();
});
</script>
<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
.app-container {
display: flex;
flex-direction: column;
@@ -101,4 +265,70 @@
:deep(.up-tabs) {
background-color: #fff;
}
+
+ .warn-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #fff;
+ padding: 16rpx 30rpx;
+
+ .warn-left {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ }
+
+ .warn-label {
+ font-size: 28rpx;
+ color: #333;
+ }
+ }
+
+ .warn-search {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ background: #fff;
+ padding: 16rpx 30rpx;
+
+ .search-input {
+ flex: 1;
+ background: #f5f5f5;
+ border-radius: 40rpx;
+ padding: 0 24rpx;
+ height: 72rpx;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .warn-list {
+ flex: 1;
+ height: 0;
+ padding: 20rpx;
+ box-sizing: border-box;
+ }
+
+ .warn-item {
+ padding: 24rpx;
+
+ &.is-warn {
+ border: 2rpx solid #f56c6c;
+ }
+
+ .warn-icon {
+ background: linear-gradient(135deg, #f56c6c, #c0392b);
+ }
+
+ .warn-value {
+ color: #f56c6c;
+ font-weight: 600;
+ }
+ }
+
+ .no-data {
+ flex: 1;
+ padding-top: 150rpx;
+ }
</style>
diff --git a/src/pages/procurementManagement/procurementDemand/index.vue b/src/pages/procurementManagement/procurementDemand/index.vue
new file mode 100644
index 0000000..b55f77b
--- /dev/null
+++ b/src/pages/procurementManagement/procurementDemand/index.vue
@@ -0,0 +1,315 @@
+<template>
+ <view class="demand-page">
+ <PageHeader title="閲囪喘闇�姹�"
+ @back="goBack" />
+
+ <!-- 绛涢�夊尯鍩� -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="queryParams.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ </view>
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="queryParams.productModel"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="showStatusSheet = true">
+ <text class="filter-text">{{ statusLabel }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍒楄〃鍖哄煙 -->
+ <view v-if="list.length > 0"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="shopping-cart"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.sourceBomNo || "鏃犳潵婧怋OM" }}</text>
+ </view>
+ <up-tag :text="statusText(item.status)"
+ :type="statusType(item.status)"
+ size="mini" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ item.productName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.productModel || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">缂哄彛闇�姹傞噺</text>
+ <text class="detail-value danger">{{ num(item.demandQuantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴撳瓨鍌ㄥ蹇収</text>
+ <text class="detail-value">{{ num(item.stockQuantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏉ユ簮BOM缂栧彿</text>
+ <text class="detail-value">{{ item.sourceBomNo || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒涘缓鏃堕棿</text>
+ <text class="detail-value">{{ item.createTime || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button v-if="isPending(item)"
+ class="action-btn"
+ size="small"
+ type="primary"
+ @click="handleChangeStatus(item, 1)">鏍囪宸茶浆閲囪喘</up-button>
+ <up-button v-if="isPending(item)"
+ class="action-btn"
+ size="small"
+ type="warning"
+ @click="handleChangeStatus(item, 2)">鍙栨秷</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ plain
+ :disabled="!isPending(item)"
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty text="鏆傛棤閲囪喘闇�姹�"
+ mode="data"></up-empty>
+ </view>
+
+ <!-- 鐘舵�佺瓫閫� -->
+ <up-action-sheet :show="showStatusSheet"
+ :actions="statusOptions"
+ title="閫夋嫨鐘舵��"
+ @select="handleStatusSelect"
+ @close="showStatusSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import { listPage, changeStatus, del } from "@/api/procurementManagement/procurementDemand";
+ import PageHeader from "@/components/PageHeader.vue";
+
+ const STATUS_MAP = {
+ 0: { text: "寰呭鐞�", type: "warning" },
+ 1: { text: "宸茶浆閲囪喘", type: "success" },
+ 2: { text: "宸插彇娑�", type: "info" },
+ };
+
+ const queryParams = reactive({
+ productName: "",
+ productModel: "",
+ status: "",
+ });
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const showStatusSheet = ref(false);
+ const statusOptions = [
+ { name: "鍏ㄩ儴", value: "" },
+ { name: "寰呭鐞�", value: 0 },
+ { name: "宸茶浆閲囪喘", value: 1 },
+ { name: "宸插彇娑�", value: 2 },
+ ];
+ const statusLabel = ref("鍏ㄩ儴鐘舵��");
+
+ // decimal 鍙兘鏄瓧绗︿覆锛岀粺涓�杞暟瀛楀厹搴�
+ const num = val => Number(val) || 0;
+
+ const statusText = status => (STATUS_MAP[Number(status)] || STATUS_MAP[0]).text;
+ const statusType = status => (STATUS_MAP[Number(status)] || STATUS_MAP[0]).type;
+ // 浠呭緟澶勭悊鍙搷浣滐紙鍒犻櫎涔熺敱鍚庣鍏滃簳鏍¢獙锛�
+ const isPending = item => Number(item.status) === 0;
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+ pageStatus.value = "loading";
+ listPage({
+ current: page.current,
+ size: page.size,
+ productName: queryParams.productName,
+ productModel: queryParams.productModel,
+ status: queryParams.status,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+
+ page.total = total;
+ if (list.value.length >= total) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current++;
+ }
+ })
+ .catch(() => {
+ // 閿欒鎻愮ず鐢� request 缁熶竴澶勭悊
+ pageStatus.value = "loadmore";
+ });
+ };
+
+ const handleStatusSelect = action => {
+ queryParams.status = action.value;
+ statusLabel.value = action.value === "" ? "鍏ㄩ儴鐘舵��" : action.name;
+ showStatusSheet.value = false;
+ handleSearch();
+ };
+
+ const handleChangeStatus = (item, status) => {
+ const actionText = status === 1 ? "鏍囪涓哄凡杞噰璐�" : "鍙栨秷";
+ uni.showModal({
+ title: "鎻愮ず",
+ content: `纭畾灏嗚閲囪喘闇�姹�${actionText}鍚楋紵`,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ changeStatus({ ids: item.id, status })
+ .then(() => {
+ uni.hideLoading();
+ uni.showToast({ title: "鎿嶄綔鎴愬姛", icon: "none" });
+ handleSearch();
+ })
+ .catch(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ const handleDelete = item => {
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭畾鍒犻櫎璇ラ噰璐渶姹傚悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ del([item.id])
+ .then(() => {
+ uni.hideLoading();
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "none" });
+ handleSearch();
+ })
+ .catch(() => {
+ // 闈炲緟澶勭悊鐘舵�佸悗绔細杩斿洖鎻愮ず锛屾澶勪笉鍐嶉噸澶嶅脊绐�
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onReachBottom(() => {
+ getList();
+ });
+
+ onShow(() => {
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .demand-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ }
+
+ .search-bar + .search-bar {
+ margin-top: 16rpx;
+ }
+
+ // 鍏叡鏍峰紡閲� .filter-button 鏄� 40px 瑙佹柟鐨勫浘鏍囨寜閽紝杩欓噷瑕佹斁鐘舵�佹枃妗堬紝闇�鎸夊唴瀹规拺寮�
+ .filter-button {
+ width: auto;
+ min-width: 40px;
+ padding: 0 12px;
+ flex-shrink: 0;
+ background: #f5f5f5;
+ }
+
+ .filter-text {
+ font-size: 24rpx;
+ color: #333;
+ margin-right: 4rpx;
+ white-space: nowrap;
+ }
+
+ .no-data {
+ padding-top: 100rpx;
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15rpx;
+ padding: 0 30rpx 30rpx;
+ flex-wrap: wrap;
+ }
+
+ .action-btn {
+ width: calc(33.33% - 10rpx);
+ margin: 0 !important;
+ margin-bottom: 15rpx !important;
+ }
+</style>
diff --git a/src/pages/procurementManagement/procurementLedger/detail.vue b/src/pages/procurementManagement/procurementLedger/detail.vue
index 397d863..72cfc73 100644
--- a/src/pages/procurementManagement/procurementLedger/detail.vue
+++ b/src/pages/procurementManagement/procurementLedger/detail.vue
@@ -204,6 +204,14 @@
:disabled="!canEditProducts"
@click="openSpecificationPicker(idx)" />
<template #right>
+ <view v-if="canEditProducts"
+ class="scan-entry"
+ @click.stop="scanBarcode(idx)">
+ <up-icon name="scan"
+ size="18"
+ color="#3c9cff"></up-icon>
+ <text class="scan-entry-text">鎵爜</text>
+ </view>
<up-icon name="arrow-right"
v-if="canEditProducts"
@click="openSpecificationPicker(idx)"></up-icon>
@@ -338,7 +346,11 @@
<script setup>
import { onMounted, ref, computed } from "vue";
- import { modelList, productTreeList } from "@/api/basicData/product.js";
+ import {
+ modelList,
+ productTreeList,
+ modelByBarcode,
+ } from "@/api/basicData/product.js";
import useUserStore from "@/store/modules/user";
import { calculateTaxExclusiveTotalPrice } from "@/utils/summarizeTable";
import { formatDateToYMD } from "@/utils/ruoyi";
@@ -447,6 +459,7 @@
name: model.text,
value: model.value,
unit: model.unit,
+ subname: model.barcode ? `鏉$爜锛�${model.barcode}` : "",
}));
});
@@ -670,14 +683,48 @@
showCategoryPicker.value = false;
};
- // 鑾峰彇瑙勬牸鍨嬪彿
+ // 鑾峰彇瑙勬牸鍨嬪彿锛堣繑鍥� Promise锛屼究浜庢壂鐮佸悗椤哄簭鍥炲~锛�
const getModels = value => {
- modelList({ id: value }).then(res => {
- modelOptions.value = res.map(user => ({
+ return modelList({ id: value }).then(res => {
+ const list = res?.data || res || [];
+ modelOptions.value = list.map(user => ({
text: user.model,
value: user.id,
unit: user.unit,
+ barcode: user.barcode,
}));
+ });
+ };
+
+ // 鎵爜鎸夋潯鐮佺簿纭洖濉骇鍝佸ぇ绫�+瑙勬牸鍨嬪彿
+ const scanBarcode = idx => {
+ if (!canEditProducts.value) return;
+ uni.scanCode({
+ scanType: ["barCode", "qrCode"],
+ success: async res => {
+ const barcode = (res.result || "").trim();
+ if (!barcode) return;
+ uni.showLoading({ title: "鍖归厤涓�...", mask: true });
+ try {
+ const resp = await modelByBarcode({ barcode });
+ const info = resp?.data || resp;
+ if (info && info.productId) {
+ currentProductIndex.value = idx;
+ // 鍏堝姞杞借浜у搧涓嬬殑瑙勬牸鍒楄〃锛屼繚璇佷笌鎵嬪姩閫夋嫨鏃剁殑 UI 鐘舵�佷竴鑷�
+ await getModels(info.productId);
+ const row = productData.value[idx];
+ row.productCategory = info.productName || row.productCategory;
+ row.specificationModel = info.model || "";
+ row.productModelId = info.id || "";
+ row.unit = info.unit || "";
+ }
+ uni.hideLoading();
+ uni.showToast({ title: "宸插尮閰嶈鏍�", icon: "success" });
+ } catch (e) {
+ // 鏈尮閰嶇瓑閿欒宸叉湁鍏ㄥ眬 toast锛岃繖閲屽彧鎭㈠ loading
+ uni.hideLoading();
+ }
+ },
});
};
@@ -1119,6 +1166,19 @@
<style scoped lang="scss">
@import "@/static/scss/form-common.scss";
+ // 瑙勬牸琛屾壂鐮佸叆鍙�
+ .scan-entry {
+ display: flex;
+ align-items: center;
+ margin-right: 8px;
+
+ .scan-entry-text {
+ font-size: 13px;
+ color: #3c9cff;
+ margin-left: 2px;
+ }
+ }
+
.approval-process {
background: #fff;
margin: 16px;
diff --git a/src/pages/productionDesign/bom/index.vue b/src/pages/productionDesign/bom/index.vue
index 97ba5f6..ea79eba 100644
--- a/src/pages/productionDesign/bom/index.vue
+++ b/src/pages/productionDesign/bom/index.vue
@@ -33,9 +33,14 @@
</view>
<text class="item-id">{{ item.bomNo || "-" }}</text>
</view>
- <up-tag :text="'V' + (item.version || '1.0')"
- type="primary"
- size="mini" />
+ <view class="item-right">
+ <up-tag :text="'V' + (item.version || '1.0')"
+ type="primary"
+ size="mini" />
+ <up-tag :text="isEnabled(item) ? '鍚敤' : '鍋滅敤'"
+ :type="isEnabled(item) ? 'success' : 'info'"
+ size="mini" />
+ </view>
</view>
<up-divider></up-divider>
<view class="item-details">
@@ -57,6 +62,15 @@
size="small"
type="primary"
@click="goStructure(item)">鏌ョ湅璇︽儏</up-button>
+ <up-button class="action-btn"
+ size="small"
+ :type="isEnabled(item) ? 'warning' : 'success'"
+ @click="toggleStatus(item)">{{ isEnabled(item) ? "鍋滅敤" : "鍚敤" }}</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ plain
+ @click="openKitCheck(item)">榻愬鍒嗘瀽</up-button>
</view>
</view>
<up-loadmore :status="pageStatus" />
@@ -66,13 +80,92 @@
<up-empty text="鏆傛棤BOM鏁版嵁"
mode="list"></up-empty>
</view>
+
+ <!-- 榻愬鍒嗘瀽寮圭獥 -->
+ <up-popup :show="showKitPopup"
+ mode="bottom"
+ round="10"
+ @close="showKitPopup = false">
+ <view class="kit-popup">
+ <view class="popup-header">
+ <text class="popup-title">榻愬鍒嗘瀽 - {{ currentBom.bomNo || "" }}</text>
+ </view>
+ <view class="kit-form">
+ <text class="kit-form-label">闇�姹傛暟閲�</text>
+ <view class="kit-form-input">
+ <up-number-box v-model="demandQty"
+ :min="0"
+ :step="1"></up-number-box>
+ </view>
+ <up-button type="primary"
+ size="small"
+ :loading="kitLoading"
+ text="鍒嗘瀽"
+ @click="handleKitCheck"></up-button>
+ </view>
+ <scroll-view scroll-y
+ class="kit-result">
+ <view v-if="kitResult.length > 0">
+ <view v-for="(row, index) in kitResult"
+ :key="index"
+ class="kit-item">
+ <view class="kit-item-header">
+ <text class="kit-name">{{ row.productName || "-" }}</text>
+ <text class="kit-model">{{ row.model || "-" }}</text>
+ <up-tag v-if="num(row.shortageQty) > 0"
+ text="缂哄彛"
+ type="error"
+ size="mini" />
+ </view>
+ <view class="kit-grid">
+ <view class="kit-cell">
+ <text class="kit-label">鍗曚綅</text>
+ <text class="kit-value">{{ row.unit || "-" }}</text>
+ </view>
+ <view class="kit-cell">
+ <text class="kit-label">闇�姹傛暟閲�</text>
+ <text class="kit-value">{{ num(row.requiredQty) }}</text>
+ </view>
+ <view class="kit-cell">
+ <text class="kit-label">鍙敤搴撳瓨</text>
+ <text class="kit-value">{{ num(row.availableQty) }}</text>
+ </view>
+ <view class="kit-cell">
+ <text class="kit-label">缂哄彛鏁伴噺</text>
+ <text class="kit-value"
+ :class="{ danger: num(row.shortageQty) > 0 }">{{ num(row.shortageQty) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="kit-empty">
+ <up-empty mode="data"
+ :text="kitDone ? '搴撳瓨榻愬锛屾棤缂哄彛' : '璇疯緭鍏ラ渶姹傛暟閲忓悗鐐瑰嚮鍒嗘瀽'"></up-empty>
+ </view>
+ </scroll-view>
+ <view class="filter-actions">
+ <up-button type="primary"
+ size="default"
+ :disabled="!kitDone"
+ text="鐢熸垚閲囪喘闇�姹�"
+ @click="handleGenerateDemand"></up-button>
+ </view>
+ </view>
+ </up-popup>
</view>
</template>
<script setup>
import { reactive, ref } from "vue";
import { onReachBottom, onShow } from "@dcloudio/uni-app";
- import { listPage } from "@/api/productionManagement/bom";
+ import {
+ listPage,
+ changeStatus,
+ kitCheck,
+ generateDemand,
+ } from "@/api/productionManagement/bom";
+ import PageHeader from "@/components/PageHeader.vue";
const queryParams = reactive({
productName: "",
@@ -85,6 +178,15 @@
size: 3,
total: 0,
});
+
+ // 鍚庣涓嶈繑鍥� status 鏃舵寜鍚敤灞曠ず锛堝吋瀹硅�佹暟鎹級
+ const isEnabled = item =>
+ item.status === undefined ||
+ item.status === null ||
+ Number(item.status) === 1;
+
+ // decimal 鍙兘鏄瓧绗︿覆锛岀粺涓�杞暟瀛楀厹搴�
+ const num = val => Number(val) || 0;
const goBack = () => {
uni.navigateBack();
@@ -125,7 +227,7 @@
}
})
.catch(() => {
- uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+ // 閿欒鎻愮ず鐢� request 缁熶竴澶勭悊
pageStatus.value = "loadmore";
});
};
@@ -142,6 +244,87 @@
item.remark || ""
)}&version=${encodeURIComponent(item.version || 1)}`,
});
+ };
+
+ // 鍚敤/鍋滅敤锛宻tatus=1 鏃跺悗绔嚜鍔ㄥ仠鐢ㄥ悓瑙勬牸鍏跺畠鐗堟湰
+ const toggleStatus = item => {
+ const enabled = isEnabled(item);
+ const nextStatus = enabled ? 0 : 1;
+ const actionText = enabled ? "鍋滅敤" : "鍚敤";
+ uni.showModal({
+ title: "鎻愮ず",
+ content: `纭畾${actionText}璇OM鐗堟湰鍚楋紵`,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ changeStatus({ id: item.id, status: nextStatus })
+ .then(() => {
+ uni.hideLoading();
+ uni.showToast({ title: `${actionText}鎴愬姛`, icon: "none" });
+ handleSearch();
+ })
+ .catch(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ // --- 榻愬鍒嗘瀽 ---
+ const showKitPopup = ref(false);
+ const kitLoading = ref(false);
+ const kitDone = ref(false);
+ const currentBom = ref({});
+ const demandQty = ref(1);
+ const kitResult = ref([]);
+
+ const openKitCheck = item => {
+ currentBom.value = item;
+ demandQty.value = 1;
+ kitResult.value = [];
+ kitDone.value = false;
+ showKitPopup.value = true;
+ };
+
+ const handleKitCheck = () => {
+ const qty = Number(demandQty.value);
+ if (!Number.isFinite(qty) || qty <= 0) {
+ uni.showToast({ title: "闇�姹傛暟閲忓繀椤诲ぇ浜�0", icon: "none" });
+ return;
+ }
+ kitLoading.value = true;
+ kitCheck({ bomId: currentBom.value.id, demandQty: qty })
+ .then(res => {
+ kitResult.value = res?.data || [];
+ kitDone.value = true;
+ })
+ .catch(() => {
+ // 閿欒鎻愮ず鐢� request 缁熶竴澶勭悊
+ kitResult.value = [];
+ kitDone.value = false;
+ })
+ .finally(() => {
+ kitLoading.value = false;
+ });
+ };
+
+ const handleGenerateDemand = () => {
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ generateDemand({
+ bomId: currentBom.value.id,
+ demandQty: Number(demandQty.value),
+ })
+ .then(res => {
+ uni.hideLoading();
+ uni.showModal({
+ title: "鎻愮ず",
+ content: String(res?.data ?? "閲囪喘闇�姹傜敓鎴愬畬鎴�"),
+ showCancel: false,
+ });
+ })
+ .catch(() => {
+ uni.hideLoading();
+ });
};
onReachBottom(() => {
@@ -172,8 +355,92 @@
}
.action-btn {
- width: calc(50% - 15rpx);
+ width: calc(33.33% - 10rpx);
margin: 0 !important;
margin-bottom: 15rpx !important;
}
+
+ // --- 榻愬鍒嗘瀽寮圭獥 ---
+ .kit-popup {
+ padding-bottom: 20rpx;
+ }
+
+ .kit-form {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ padding: 20rpx 30rpx;
+
+ .kit-form-label {
+ font-size: 28rpx;
+ color: #333;
+ }
+
+ .kit-form-input {
+ flex: 1;
+ }
+ }
+
+ .kit-result {
+ height: 600rpx;
+ padding: 0 30rpx;
+ }
+
+ .kit-item {
+ border: 1rpx solid #f0f0f0;
+ border-radius: 12rpx;
+ padding: 16rpx;
+ margin-bottom: 16rpx;
+ }
+
+ .kit-item-header {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 12rpx;
+
+ .kit-name {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 500;
+ }
+
+ .kit-model {
+ font-size: 24rpx;
+ color: #999;
+ flex: 1;
+ }
+ }
+
+ .kit-grid {
+ display: flex;
+ flex-wrap: wrap;
+
+ .kit-cell {
+ width: 50%;
+ display: flex;
+ align-items: center;
+ margin-bottom: 8rpx;
+
+ .kit-label {
+ font-size: 24rpx;
+ color: #777;
+ min-width: 110rpx;
+ }
+
+ .kit-value {
+ font-size: 24rpx;
+ color: #333;
+
+ &.danger {
+ color: #ee0a24;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+
+ .kit-empty {
+ padding: 40rpx 0;
+ }
</style>
diff --git a/src/pages/productionManagement/costAccounting/index.vue b/src/pages/productionManagement/costAccounting/index.vue
new file mode 100644
index 0000000..d3465b7
--- /dev/null
+++ b/src/pages/productionManagement/costAccounting/index.vue
@@ -0,0 +1,662 @@
+<template>
+ <view class="cost-accounting">
+ <PageHeader title="鎴愭湰鏍哥畻鍒嗘瀽"
+ @back="goBack" />
+
+ <!-- 椤堕儴椤电 -->
+ <view class="main-tabs">
+ <up-tabs :list="mainTabs"
+ :current="mainTabIndex"
+ @change="handleMainTabChange"
+ :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
+ lineWidth="40"
+ lineHeight="3" />
+ </view>
+
+ <!-- 鍏叡绛涢�� -->
+ <view class="search-section">
+ <view class="filter-bar">
+ <view class="date-input"
+ @click="showCalendar = true">
+ <up-icon name="calendar"
+ size="18"
+ color="#999"></up-icon>
+ <text class="date-text"
+ :class="{ placeholder: !query.startDate }">{{ dateRangeText }}</text>
+ <up-icon v-if="query.startDate"
+ name="close-circle-fill"
+ size="16"
+ color="#c0c4cc"
+ @click.stop="handleClearDate"></up-icon>
+ </view>
+ </view>
+ <view class="filter-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="query.npsNo"
+ placeholder="鐢熶骇璁㈠崟鍙�"
+ clearable
+ @change="handleQuery" />
+ </view>
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="query.productName"
+ placeholder="鎴愬搧鍚嶇О/瑙勬牸"
+ clearable
+ @change="handleQuery" />
+ </view>
+ </view>
+ <view class="filter-actions-top">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click="handleQuery"></up-button>
+ </view>
+ </view>
+
+ <view v-if="loading"
+ class="loading-box">
+ <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ </view>
+
+ <template v-else>
+ <!-- ============ 1. 鏍哥畻鏄庣粏 ============ -->
+ <template v-if="mainTabIndex === 0">
+ <view v-if="visibleStatRows.length > 0"
+ class="ledger-list">
+ <view v-for="row in visibleStatRows"
+ :key="row.productionOrderId ?? row.npsNo"
+ class="ledger-item no-click">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="rmb-circle"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ row.npsNo || "-" }}</text>
+ </view>
+ <text class="item-index">{{ row.orderDate || "鏃犳棩鏈�" }}</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鎴愬搧</text>
+ <text class="detail-value">{{ row.finishedProduct || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜ч噺</text>
+ <text class="detail-value">{{ fmtNum(row.outputQty) }}{{ row.unit ? ` ${row.unit}` : "" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏉愭枡鎴愭湰</text>
+ <text class="detail-value">{{ fmtMoney(row.materialCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜哄伐鎴愭湰</text>
+ <text class="detail-value"
+ :class="{ warn: isPositive(row.noRateRows) }">{{ fmtMoney(row.laborCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鑳借�楁垚鏈�</text>
+ <text class="detail-value">{{ fmtMoney(row.energyCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎬绘垚鏈�</text>
+ <text class="detail-value highlight">{{ fmtMoney(row.totalCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅鎴愭湰</text>
+ <text class="detail-value">{{ fmtUnitCost(row.unitCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閿�鍞崟浠�</text>
+ <text class="detail-value">{{ fmtMoney(row.salePrice) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">姣涘埄</text>
+ <text class="detail-value"
+ :class="{ danger: isNegative(row.grossProfit) }">{{ fmtMoney(row.grossProfit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">姣涘埄鐜�</text>
+ <text class="detail-value"
+ :class="{ danger: isNegative(row.grossProfitRate) }">{{ fmtPercent(row.grossProfitRate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浠锋牸瑕嗙洊鐜�</text>
+ <text class="detail-value"
+ :class="{ warn: isLowCoverage(row.priceCoverageRate) }">{{ fmtPercent(row.priceCoverageRate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">缂轰环鐗╂枡鏁�</text>
+ <text class="detail-value"
+ :class="{ warn: isPositive(row.missingPriceKinds) }">{{ fmtCount(row.missingPriceKinds) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏃犲伐璧勬爣鍑嗚褰�</text>
+ <text class="detail-value"
+ :class="{ warn: isPositive(row.noRateRows) }">{{ fmtCount(row.noRateRows) }}</text>
+ </view>
+ <view v-if="isPositive(row.missingPriceKinds)"
+ class="warn-tip">瀛樺湪缂轰环鐗╂枡锛屾潗鏂欐垚鏈浣庝及锛屽彲鍦ㄣ�岀己浠风墿鏂欍�嶉〉鏌ユ竻鍗曞悗琛ュ綍閲囪喘鍗曚环</view>
+ <view v-if="isPositive(row.noRateRows)"
+ class="warn-tip">瀛樺湪鏃犲伐璧勬爣鍑嗙殑鎶ュ伐璁板綍锛屼汉宸ユ垚鏈笉鍙俊锛岄渶琛ュ綍宸ユ椂鎴栫淮鎶よ浠跺崟浠�</view>
+ </view>
+ </view>
+ <up-loadmore :status="statStatus" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鎴愭湰鏍哥畻鏁版嵁"></up-empty>
+ </view>
+ </template>
+
+ <!-- ============ 2. 鏋勬垚姹囨�� ============ -->
+ <template v-else-if="mainTabIndex === 1">
+ <view class="sub-tabs">
+ <up-tabs :list="groupTabs"
+ :current="groupIndex"
+ @change="handleGroupChange"
+ :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
+ lineWidth="30"
+ lineHeight="3" />
+ </view>
+ <view v-if="summaryList.length > 0"
+ class="ledger-list">
+ <view v-for="(row, index) in summaryList"
+ :key="index"
+ class="ledger-item no-click">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="grid"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ row.groupKey || "鏈~鏃ユ湡" }}</text>
+ </view>
+ <text class="item-index">{{ fmtCount(row.orderCount) }} 鍗�</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <!-- 鎴愭湰鏋勬垚鍗犳瘮 -->
+ <view class="rate-bar">
+ <view class="rate-seg seg-material"
+ :style="{ width: segWidth(row.materialRate) }"></view>
+ <view class="rate-seg seg-labor"
+ :style="{ width: segWidth(row.laborRate) }"></view>
+ <view class="rate-seg seg-energy"
+ :style="{ width: segWidth(row.energyRate) }"></view>
+ </view>
+ <view class="rate-legend">
+ <view class="legend-item">
+ <text class="legend-dot seg-material"></text>
+ <text class="legend-text">鏉愭枡 {{ fmtPercent(row.materialRate) }}</text>
+ </view>
+ <view class="legend-item">
+ <text class="legend-dot seg-labor"></text>
+ <text class="legend-text">浜哄伐 {{ fmtPercent(row.laborRate) }}</text>
+ </view>
+ <view class="legend-item">
+ <text class="legend-dot seg-energy"></text>
+ <text class="legend-text">鑳借�� {{ fmtPercent(row.energyRate) }}</text>
+ </view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜ч噺</text>
+ <text class="detail-value">{{ fmtNum(row.outputQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏉愭枡鎴愭湰</text>
+ <text class="detail-value">{{ fmtMoney(row.materialCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜哄伐鎴愭湰</text>
+ <text class="detail-value">{{ fmtMoney(row.laborCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鑳借�楁垚鏈�</text>
+ <text class="detail-value">{{ fmtMoney(row.energyCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎬绘垚鏈�</text>
+ <text class="detail-value highlight">{{ fmtMoney(row.totalCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅鎴愭湰</text>
+ <text class="detail-value">{{ fmtUnitCost(row.unitCost) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">姣涘埄</text>
+ <text class="detail-value"
+ :class="{ danger: isNegative(row.grossProfit) }">{{ fmtMoney(row.grossProfit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">姣涘埄鐜�</text>
+ <text class="detail-value"
+ :class="{ danger: isNegative(row.grossProfitRate) }">{{ fmtPercent(row.grossProfitRate) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤姹囨�绘暟鎹�"></up-empty>
+ </view>
+ </template>
+
+ <!-- ============ 3. 缂轰环鐗╂枡 ============ -->
+ <template v-else>
+ <view v-if="missingList.length > 0"
+ class="ledger-list">
+ <view v-for="(row, index) in missingList"
+ :key="row.productModelId ?? index"
+ class="ledger-item no-click">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon warn-icon">
+ <up-icon name="warning"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ row.materialName || "-" }}</text>
+ </view>
+ <text class="item-index">{{ fmtCount(row.orderCount) }} 鍗�</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ row.materialModel || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ row.unit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">娑堣�楅噺</text>
+ <text class="detail-value"
+ :class="{ warn: true }">{{ fmtNum(row.inputQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏈�鍚庢姇鍏ユ椂闂�</text>
+ <text class="detail-value">{{ row.lastInputTime || "-" }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤缂轰环鐗╂枡"></up-empty>
+ </view>
+ </template>
+ </template>
+
+ <!-- 鏃ユ湡鍖洪棿閫夋嫨 -->
+ <up-calendar :show="showCalendar"
+ mode="range"
+ :maxDate="maxDate"
+ :minDate="minDate"
+ :monthNum="monthNum"
+ :defaultDate="defaultDateRange"
+ @confirm="onDateConfirm"
+ @close="showCalendar = false"></up-calendar>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { getWeekRange } from "@/utils/common";
+ import {
+ costStat,
+ costSummary,
+ costMissingPrice,
+ } from "@/api/productionManagement/costAccounting.js";
+
+ const mainTabs = [{ name: "鏍哥畻鏄庣粏" }, { name: "鏋勬垚姹囨��" }, { name: "缂轰环鐗╂枡" }];
+ const mainTabIndex = ref(0);
+
+ // 鏃ユ湡鍖洪棿锛氶粯璁ゆ湰鍛紙鍛ㄤ竴 ~ 浠婂ぉ锛�
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const minDate = "2022-01-01";
+ // 鏈堜唤鏁伴渶涓� minDate 涓ユ牸涓�鑷达紝灏戜竴涓湀浼氭妸褰撳墠鏈堟尋鍑哄垪琛ㄥ鑷存棩鍘嗗仠鍦ㄨ捣濮嬫湀
+ const monthNum = computed(() => {
+ const max = dayjs(maxDate);
+ const min = dayjs(minDate);
+ return (max.year() - min.year()) * 12 + (max.month() - min.month()) + 1;
+ });
+ const showCalendar = ref(false);
+ const weekRange = getWeekRange();
+ const query = reactive({
+ startDate: weekRange.startDate,
+ endDate: weekRange.endDate,
+ npsNo: "",
+ productName: "",
+ });
+
+ const dateRangeText = computed(() =>
+ query.startDate && query.endDate
+ ? `${query.startDate} 鑷� ${query.endDate}`
+ : "璇烽�夋嫨鏃ユ湡鍖洪棿"
+ );
+ // 鎵撳紑鏃ュ巻鏃跺洖鏄惧綋鍓嶅尯闂�
+ const defaultDateRange = computed(() =>
+ query.startDate && query.endDate
+ ? [query.startDate, query.endDate]
+ : []
+ );
+
+ const loading = ref(false);
+
+ // 鏄庣粏锛堟帴鍙h繑鍥炲叏閲忔暟缁勶紝鍓嶇鍒嗘壒娓叉煋锛�
+ const statRows = ref([]);
+ const pageSize = 10;
+ const showCount = ref(pageSize);
+ const statStatus = ref("loadmore");
+ const visibleStatRows = computed(() => statRows.value.slice(0, showCount.value));
+
+ // 姹囨��
+ const groupTabs = [
+ { name: "鎸夋湀浠�", value: "month" },
+ { name: "鎸夋垚鍝�", value: "product" },
+ ];
+ const groupIndex = ref(0);
+ const groupBy = computed(() => groupTabs[groupIndex.value].value);
+ const summaryList = ref([]);
+
+ // 缂轰环鐗╂枡
+ const missingList = ref([]);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // ============ 鏁板�兼牸寮忓寲 ============
+ const EMPTY = "鈥�";
+ const isBlank = v => v === null || v === undefined || v === "";
+ const toFinite = v => {
+ if (isBlank(v)) return null;
+ const n = Number(v);
+ return Number.isFinite(n) ? n : null;
+ };
+ const fmtNum = v => {
+ const n = toFinite(v);
+ return n === null ? EMPTY : String(n);
+ };
+ const fmtCount = v => {
+ const n = toFinite(v);
+ return n === null ? EMPTY : String(n);
+ };
+ const fmtMoney = v => {
+ const n = toFinite(v);
+ return n === null ? EMPTY : n.toFixed(2);
+ };
+ // 鍗曚綅鎴愭湰鍚庣淇濈暀 4 浣�
+ const fmtUnitCost = v => {
+ const n = toFinite(v);
+ return n === null ? EMPTY : n.toFixed(4);
+ };
+ const fmtPercent = v => {
+ const n = toFinite(v);
+ return n === null ? EMPTY : `${n.toFixed(2)}%`;
+ };
+ const isPositive = v => (toFinite(v) ?? 0) > 0;
+ const isNegative = v => (toFinite(v) ?? 0) < 0;
+ const isLowCoverage = v => {
+ const n = toFinite(v);
+ return n !== null && n < 100;
+ };
+ // 鍗犳瘮鏉℃瀹斤紙鍗犳瘮涓� null 鏃舵寜 0 澶勭悊锛�
+ const segWidth = v => {
+ const n = toFinite(v) ?? 0;
+ return `${Math.max(0, Math.min(n, 100))}%`;
+ };
+
+ // ============ 鏌ヨ ============
+ const buildParams = () => {
+ const params = {};
+ if (query.startDate) params.startDate = query.startDate;
+ if (query.endDate) params.endDate = query.endDate;
+ if (query.npsNo) params.npsNo = query.npsNo;
+ if (query.productName) params.productName = query.productName;
+ return params;
+ };
+
+ const syncStatStatus = () => {
+ statStatus.value =
+ showCount.value >= statRows.value.length ? "nomore" : "loadmore";
+ };
+
+ const fetchStat = async () => {
+ try {
+ const res = await costStat(buildParams());
+ const rows = res?.data ?? res ?? [];
+ statRows.value = Array.isArray(rows) ? rows : [];
+ showCount.value = pageSize;
+ syncStatStatus();
+ } catch (e) {
+ // 閿欒鏂囨宸茬敱 request 缁熶竴 toast
+ statRows.value = [];
+ }
+ };
+
+ const fetchSummary = async () => {
+ try {
+ const res = await costSummary({ ...buildParams(), groupBy: groupBy.value });
+ const rows = res?.data ?? res ?? [];
+ summaryList.value = Array.isArray(rows) ? rows : [];
+ } catch (e) {
+ summaryList.value = [];
+ }
+ };
+
+ const fetchMissing = async () => {
+ try {
+ const res = await costMissingPrice(buildParams());
+ const rows = res?.data ?? res ?? [];
+ missingList.value = Array.isArray(rows) ? rows : [];
+ } catch (e) {
+ missingList.value = [];
+ }
+ };
+
+ const loadCurrentTab = async () => {
+ loading.value = true;
+ try {
+ if (mainTabIndex.value === 0) await fetchStat();
+ else if (mainTabIndex.value === 1) await fetchSummary();
+ else await fetchMissing();
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const handleQuery = () => {
+ loadCurrentTab();
+ };
+
+ const handleMainTabChange = item => {
+ mainTabIndex.value = item.index;
+ loadCurrentTab();
+ };
+
+ const handleGroupChange = item => {
+ groupIndex.value = item.index;
+ fetchSummary();
+ };
+
+ const onDateConfirm = e => {
+ query.startDate = e[0];
+ query.endDate = e[e.length - 1];
+ showCalendar.value = false;
+ handleQuery();
+ };
+
+ const handleClearDate = () => {
+ query.startDate = "";
+ query.endDate = "";
+ handleQuery();
+ };
+
+ onShow(() => {
+ loadCurrentTab();
+ });
+
+ // 浠呮槑缁嗛渶瑕佸垎椤垫覆鏌�
+ onReachBottom(() => {
+ if (mainTabIndex.value !== 0) return;
+ if (statStatus.value === "nomore") return;
+ showCount.value += pageSize;
+ syncStatStatus();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .cost-accounting {
+ min-height: 100vh;
+ background: #f8f9fa;
+ }
+
+ .main-tabs {
+ background: #fff;
+ margin-bottom: 10px;
+ }
+
+ .sub-tabs {
+ background: #fff;
+ padding: 0 10px 8px;
+ }
+
+ .loading-box {
+ padding-top: 150rpx;
+ display: flex;
+ justify-content: center;
+ }
+
+ .filter-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ & + .filter-bar {
+ margin-top: 12px;
+ }
+ }
+
+ .date-input,
+ .search-input {
+ flex: 1;
+ height: 40px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .date-input .date-text {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+ }
+
+ .placeholder {
+ color: #999;
+ }
+
+ .filter-actions-top {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 12px;
+ }
+
+ .item-index {
+ font-size: 12px;
+ color: #999;
+ background: #f5f5f5;
+ padding: 2px 8px;
+ border-radius: 12px;
+ }
+
+ .detail-value.warn {
+ color: #ff9800;
+ font-weight: 500;
+ }
+
+ .warn-tip {
+ margin-top: 8px;
+ font-size: 11px;
+ color: #ff9800;
+ line-height: 1.6;
+ }
+
+ .warn-icon {
+ background: linear-gradient(135deg, #ff9800, #f57c00);
+ }
+
+ // 鎴愭湰鏋勬垚鍗犳瘮鏉�
+ .rate-bar {
+ display: flex;
+ height: 12px;
+ border-radius: 6px;
+ overflow: hidden;
+ background: #f0f0f0;
+ margin-bottom: 8px;
+
+ .rate-seg {
+ height: 100%;
+ }
+ }
+
+ .rate-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 12px;
+
+ .legend-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .legend-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
+ }
+
+ .legend-text {
+ font-size: 11px;
+ color: #777;
+ }
+ }
+
+ .seg-material {
+ background: #2979ff;
+ }
+
+ .seg-labor {
+ background: #ff9800;
+ }
+
+ .seg-energy {
+ background: #4caf50;
+ }
+
+ .ledger-item.no-click:active {
+ transform: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+</style>
diff --git a/src/pages/productionManagement/materialConsumption/index.vue b/src/pages/productionManagement/materialConsumption/index.vue
new file mode 100644
index 0000000..04ccb29
--- /dev/null
+++ b/src/pages/productionManagement/materialConsumption/index.vue
@@ -0,0 +1,507 @@
+<template>
+ <view class="material-consumption">
+ <PageHeader title="鏉愭枡娑堣�楁牳绠�"
+ @back="goBack" />
+ <!-- 绛涢�夊尯鍩� -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ョ敓浜ц鍗曞彿"
+ v-model="searchForm.npsNo"
+ @change="handleQuery"
+ clearable />
+ </view>
+ </view>
+ <view class="search-bar mt-16">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ユ垚鍝佸悕绉�/瑙勬牸"
+ v-model="searchForm.productName"
+ @change="handleQuery"
+ clearable />
+ </view>
+ </view>
+ <view class="filter-actions-row">
+ <view class="switch-item">
+ <text class="switch-label">鍙湅宸紓</text>
+ <up-switch v-model="searchForm.onlyDiff"
+ size="18"
+ @change="handleQuery" />
+ </view>
+ </view>
+ <view class="date-picker-container">
+ <view class="date-input"
+ @click="showCalendar = true">
+ <up-icon name="calendar"
+ size="20"
+ color="#999"></up-icon>
+ <text class="date-text"
+ :class="{ 'placeholder': !searchForm.startDate }">{{ dateRangeText }}</text>
+ <view v-if="searchForm.startDate"
+ class="clear-icon-wrapper"
+ @click.stop="handleClearDate">
+ <up-icon name="close-circle-fill"
+ size="18"
+ color="#c0c4cc"></up-icon>
+ </view>
+ </view>
+ <view class="search-btn-wrapper">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click.stop="handleQuery"></up-button>
+ </view>
+ </view>
+ </view>
+ <!-- 姹囨�� -->
+ <view class="summary-bar"
+ v-if="groupList.length > 0">
+ <text class="summary-text">鍏� {{ groupList.length }} 涓鍗� / {{ allRows.length }} 琛岀墿鏂�</text>
+ </view>
+ <!-- 鍒楄〃鍖哄煙锛堟寜鐢熶骇璁㈠崟鍒嗙粍锛� -->
+ <view v-if="loading && groupList.length === 0"
+ class="loading-box">
+ <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ </view>
+ <view class="ledger-list"
+ v-else-if="groupList.length > 0">
+ <view class="ledger-item"
+ v-for="group in visibleGroups"
+ :key="group.key">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ group.npsNo }}</text>
+ </view>
+ <view class="item-right">
+ <text class="item-index">{{ group.items.length }} 椤圭墿鏂�</text>
+ </view>
+ </view>
+ <view class="order-product">
+ <text class="order-product-label">鎴愬搧</text>
+ <text class="order-product-name">{{ group.finishedProduct }}</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="material-list">
+ <view class="material-item"
+ :class="{ 'row-warn': isDiffWarn(m) }"
+ v-for="(m, mi) in group.items"
+ :key="mi">
+ <view class="material-header">
+ <view class="material-name-box">
+ <text class="material-name">{{ m.materialName || "-" }}</text>
+ <text class="material-model">{{ m.materialModel || "-" }}</text>
+ </view>
+ <view class="material-tags">
+ <text class="off-bom-tag"
+ v-if="isOffBom(m)">BOM澶�</text>
+ <text class="diff-tag"
+ :class="{ warn: isDiffWarn(m) }">{{ fmtRate(m.diffRate) }}</text>
+ </view>
+ </view>
+ <view class="material-grid">
+ <view class="detail-cell">
+ <text class="cell-label">鍗曚綅</text>
+ <text class="cell-value">{{ m.unit || "-" }}</text>
+ </view>
+ <view class="detail-cell">
+ <text class="cell-label">BOM鍗曡��</text>
+ <text class="cell-value">{{ fmtQty(m.unitQuantity) }}</text>
+ </view>
+ <view class="detail-cell">
+ <text class="cell-label">瀹屽伐鏁伴噺</text>
+ <text class="cell-value">{{ fmtQty(m.completeQty) }}</text>
+ </view>
+ <view class="detail-cell">
+ <text class="cell-label">鏍囧噯娑堣��</text>
+ <text class="cell-value">{{ fmtQty(m.standardQty) }}</text>
+ </view>
+ <view class="detail-cell">
+ <text class="cell-label">瀹為檯娑堣��</text>
+ <text class="cell-value">{{ fmtQty(m.actualQty) }}</text>
+ </view>
+ <view class="detail-cell">
+ <text class="cell-label">宸紓鏁伴噺</text>
+ <text class="cell-value"
+ :class="{ warn: isDiffWarn(m) }">{{ fmtQty(m.diffQty) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus"
+ v-if="groupList.length >= pageSize" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鏁版嵁"></up-empty>
+ </view>
+ <!-- 鏃ュ巻閫夋嫨鍣� -->
+ <up-calendar :show="showCalendar"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2026-01-01"
+ :monthNum="monthNum"
+ :defaultDate="defaultDateRange"
+ @confirm="onDateConfirm"
+ @close="showCalendar = false"></up-calendar>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { stat } from "@/api/productionManagement/materialConsumption.js";
+ import { getWeekRange } from "@/utils/common";
+
+ const loading = ref(false);
+ const showCalendar = ref(false);
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const monthNum = computed(() => {
+ const min = dayjs("2022-02-01");
+ const max = dayjs(maxDate);
+ return max.diff(min, "month") + 1;
+ });
+
+ // 榛樿鏌ヨ鏈懆锛堝懆涓� ~ 浠婂ぉ锛�
+ const weekRange = getWeekRange();
+ const searchForm = reactive({
+ startDate: weekRange.startDate,
+ endDate: weekRange.endDate,
+ npsNo: "",
+ productName: "",
+ onlyDiff: false,
+ });
+
+ // 鍏ㄩ噺鏄庣粏琛岋紙鎺ュ彛闈炲垎椤碉紝鍓嶇鎸夎鍗曞垎缁勫悗鍒嗘壒娓叉煋锛�
+ const allRows = ref([]);
+ const pageSize = 10;
+ const showCount = ref(pageSize);
+ const pageStatus = ref("loadmore");
+
+ const dateRangeText = computed(() => {
+ if (searchForm.startDate && searchForm.endDate) {
+ return `${searchForm.startDate} 鑷� ${searchForm.endDate}`;
+ }
+ return "璇烽�夋嫨鏃ユ湡鍖洪棿";
+ });
+
+ // 鎵撳紑鏃ュ巻鏃跺洖鏄惧綋鍓嶅尯闂�
+ const defaultDateRange = computed(() =>
+ searchForm.startDate && searchForm.endDate
+ ? [searchForm.startDate, searchForm.endDate]
+ : []
+ );
+
+ // 鎸夌敓浜ц鍗曞垎缁�
+ const groupList = computed(() => {
+ const map = new Map();
+ (allRows.value || []).forEach(row => {
+ const key = String(row.productionOrderId ?? row.npsNo ?? "unknown");
+ if (!map.has(key)) {
+ map.set(key, {
+ key,
+ npsNo: row.npsNo || "-",
+ finishedProduct: row.finishedProduct || "-",
+ items: [],
+ });
+ }
+ map.get(key).items.push(row);
+ });
+ return Array.from(map.values());
+ });
+
+ const visibleGroups = computed(() => groupList.value.slice(0, showCount.value));
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鏁板�煎厹搴�
+ const toNum = v => Number(v) || 0;
+ const fmtQty = v => {
+ if (v === null || v === undefined || v === "") return "-";
+ const n = Number(v);
+ if (Number.isNaN(n)) return "-";
+ return n.toFixed(2);
+ };
+ const fmtRate = v => {
+ if (v === null || v === undefined || v === "") return "-";
+ const n = Number(v);
+ if (Number.isNaN(n)) return "-";
+ return (n > 0 ? "+" : "") + n.toFixed(2) + "%";
+ };
+ // 宸紓鐜囩粷瀵瑰�� > 10 鏍囩孩
+ const isDiffWarn = row => {
+ if (row?.diffRate === null || row?.diffRate === undefined || row?.diffRate === "") {
+ return false;
+ }
+ return Math.abs(Number(row.diffRate)) > 10;
+ };
+ // BOM澶栨姇鍏ワ細鍗曡�椾笌鏍囧噯娑堣�楀潎涓�0
+ const isOffBom = row => toNum(row?.unitQuantity) === 0 && toNum(row?.standardQty) === 0;
+
+ const buildParams = () => {
+ const params = {};
+ if (searchForm.startDate) params.startDate = searchForm.startDate;
+ if (searchForm.endDate) params.endDate = searchForm.endDate;
+ if (searchForm.npsNo) params.npsNo = searchForm.npsNo;
+ if (searchForm.productName) params.productName = searchForm.productName;
+ if (searchForm.onlyDiff) params.onlyDiff = true;
+ return params;
+ };
+
+ const syncStatus = () => {
+ pageStatus.value =
+ showCount.value >= groupList.value.length ? "nomore" : "loadmore";
+ };
+
+ const getList = async () => {
+ loading.value = true;
+ try {
+ const res = await stat(buildParams());
+ const rows = res?.data ?? res ?? [];
+ allRows.value = Array.isArray(rows) ? rows : [];
+ showCount.value = pageSize;
+ syncStatus();
+ } catch (e) {
+ // 閿欒鏂囨宸茬敱 request 缁熶竴 toast
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const handleQuery = () => {
+ getList();
+ };
+
+ const onDateConfirm = e => {
+ searchForm.startDate = e[0];
+ searchForm.endDate = e[e.length - 1];
+ showCalendar.value = false;
+ getList();
+ };
+
+ const handleClearDate = () => {
+ searchForm.startDate = "";
+ searchForm.endDate = "";
+ getList();
+ };
+
+ onShow(() => {
+ getList();
+ });
+
+ onReachBottom(() => {
+ if (pageStatus.value === "nomore" || pageStatus.value === "loading") return;
+ pageStatus.value = "loading";
+ setTimeout(() => {
+ showCount.value += pageSize;
+ syncStatus();
+ }, 200);
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .material-consumption {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .mt-16 {
+ margin-top: 16rpx;
+ }
+
+ .filter-actions-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 16rpx;
+ }
+
+ .switch-item {
+ display: flex;
+ align-items: center;
+ }
+
+ .switch-label {
+ font-size: 14px;
+ color: #333;
+ margin-right: 12rpx;
+ }
+
+ .date-picker-container {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-top: 16rpx;
+
+ .date-input {
+ flex: 1;
+ height: 80rpx;
+ background-color: #f5f7fa;
+ border: 1rpx solid #e4e7ed;
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ padding: 0 24rpx;
+ margin-right: 20rpx;
+
+ .date-text {
+ font-size: 28rpx;
+ color: #303133;
+ margin-left: 16rpx;
+ flex: 1;
+
+ &.placeholder {
+ color: #c0c4cc;
+ }
+ }
+ }
+
+ .search-btn-wrapper {
+ width: 140rpx;
+ }
+ }
+
+ .loading-box {
+ display: flex;
+ justify-content: center;
+ padding-top: 100rpx;
+ }
+
+ .summary-bar {
+ padding: 0 20px 4px;
+
+ .summary-text {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+
+ .order-product {
+ display: flex;
+ align-items: center;
+ padding-bottom: 12px;
+
+ .order-product-label {
+ font-size: 12px;
+ color: #999;
+ background: #f5f5f5;
+ padding: 2px 8px;
+ border-radius: 4px;
+ margin-right: 8px;
+ }
+
+ .order-product-name {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ flex: 1;
+ }
+ }
+
+ .material-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #f5f5f5;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &.row-warn {
+ background: rgba(238, 10, 36, 0.04);
+ }
+
+ .material-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 10px;
+
+ .material-name-box {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ flex-wrap: wrap;
+
+ .material-name {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ margin-right: 8px;
+ }
+
+ .material-model {
+ font-size: 12px;
+ color: #999;
+ }
+ }
+
+ .material-tags {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ }
+
+ .off-bom-tag {
+ font-size: 11px;
+ color: #ff9800;
+ border: 1px solid #ffcc80;
+ border-radius: 4px;
+ padding: 0 6px;
+ margin-right: 8px;
+ }
+
+ .diff-tag {
+ font-size: 12px;
+ color: #999;
+
+ &.warn {
+ color: #ee0a24;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .material-grid {
+ display: flex;
+ flex-wrap: wrap;
+
+ .detail-cell {
+ width: 33.33%;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 8px;
+
+ .cell-label {
+ font-size: 11px;
+ color: #999;
+ margin-bottom: 2px;
+ }
+
+ .cell-value {
+ font-size: 13px;
+ color: #333;
+
+ &.warn {
+ color: #ee0a24;
+ font-weight: 600;
+ }
+ }
+ }
+ }
+ }
+</style>
diff --git a/src/pages/productionManagement/outputStatistics/index.vue b/src/pages/productionManagement/outputStatistics/index.vue
new file mode 100644
index 0000000..d35ecb2
--- /dev/null
+++ b/src/pages/productionManagement/outputStatistics/index.vue
@@ -0,0 +1,412 @@
+<template>
+ <view class="output-statistics">
+ <PageHeader title="浜ч噺缁熻"
+ @back="goBack" />
+
+ <!-- 椤堕儴椤电锛氫骇閲忕粺璁� / 璁′欢宸ヨ祫姹囨�� -->
+ <view class="main-tabs">
+ <up-tabs :list="mainTabs"
+ :current="mainTabIndex"
+ @change="handleMainTabChange"
+ :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
+ lineWidth="40"
+ lineHeight="3" />
+ </view>
+
+ <!-- ============ 浜ч噺缁熻 ============ -->
+ <template v-if="mainTabIndex === 0">
+ <view class="search-section">
+ <view class="dimension-tabs">
+ <up-tabs :list="dimensionTabs"
+ :current="dimensionIndex"
+ @change="handleDimensionChange"
+ :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
+ lineWidth="30"
+ lineHeight="3" />
+ </view>
+ <view class="filter-bar">
+ <view class="date-input"
+ @click="showOutputCalendar = true">
+ <up-icon name="calendar"
+ size="18"
+ color="#999"></up-icon>
+ <text class="date-text"
+ :class="{ placeholder: !outputForm.startDate }">{{ outputDateText }}</text>
+ </view>
+ <view v-if="dimension === 'device'"
+ class="device-input"
+ @click="openDeviceSheet">
+ <text :class="{ placeholder: !outputForm.deviceName }">{{ outputForm.deviceName || "閫夋嫨璁惧" }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <view class="filter-actions-top">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click="handleOutputSearch"></up-button>
+ </view>
+ </view>
+
+ <view v-if="outputList.length > 0"
+ class="ledger-list">
+ <view v-for="(item, index) in outputList"
+ :key="index"
+ class="ledger-item no-click">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="list-dot"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.groupName || "鏈寚瀹�" }}</text>
+ </view>
+ <text class="item-index">{{ item.workCount || 0 }} 鍗�</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜ч噺</text>
+ <text class="detail-value highlight">{{ Number(item.quantity) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ簾鏁伴噺</text>
+ <text class="detail-value danger">{{ Number(item.scrapQty) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ユ椂(h)</text>
+ <text class="detail-value">{{ Number(item.workHour) || 0 }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鏁版嵁"></up-empty>
+ </view>
+ </template>
+
+ <!-- ============ 璁′欢宸ヨ祫姹囨�� ============ -->
+ <template v-else>
+ <view class="search-section">
+ <view class="filter-bar">
+ <view class="date-input"
+ @click="showWageCalendar = true">
+ <up-icon name="calendar"
+ size="18"
+ color="#999"></up-icon>
+ <text class="date-text"
+ :class="{ placeholder: !wageForm.startDate }">{{ wageDateText }}</text>
+ </view>
+ <up-input class="name-input"
+ v-model="wageForm.userName"
+ placeholder="浜哄憳濮撳悕"
+ clearable
+ @change="handleWageSearch" />
+ </view>
+ <view class="filter-actions-top">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click="handleWageSearch"></up-button>
+ </view>
+ </view>
+
+ <view v-if="wageList.length > 0"
+ class="ledger-list">
+ <view v-for="(item, index) in wageList"
+ :key="index"
+ class="ledger-item no-click">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="account"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.userName || "鏈煡" }}</text>
+ </view>
+ <text class="item-index">{{ item.workCount || 0 }} 鍗�</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鐢熶骇鏁伴噺</text>
+ <text class="detail-value">{{ Number(item.quantity) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎬诲伐鏃�(h)</text>
+ <text class="detail-value">{{ Number(item.workHour) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁′欢宸ヨ祫</text>
+ <text class="detail-value">楼{{ Number(item.pieceWage) || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴斿彂鍚堣</text>
+ <text class="detail-value highlight">楼{{ Number(item.wages) || 0 }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鏁版嵁"></up-empty>
+ </view>
+ </template>
+
+ <!-- 鏃ユ湡鍖洪棿閫夋嫨 -->
+ <up-calendar :show="showOutputCalendar"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2022-01-01"
+ :monthNum="monthNum"
+ @confirm="onOutputDateConfirm"
+ @close="showOutputCalendar = false"></up-calendar>
+ <up-calendar :show="showWageCalendar"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2022-01-01"
+ :monthNum="monthNum"
+ @confirm="onWageDateConfirm"
+ @close="showWageCalendar = false"></up-calendar>
+
+ <!-- 璁惧绛涢�� -->
+ <up-action-sheet :show="showDeviceSheet"
+ :actions="deviceOptions"
+ title="閫夋嫨璁惧"
+ @select="selectDevice"
+ @close="showDeviceSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+ import {
+ outputStat,
+ wageSummary,
+ } from "@/api/productionManagement/productionProductMain.js";
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 椤堕儴椤电
+ const mainTabs = [{ name: "浜ч噺缁熻" }, { name: "璁′欢宸ヨ祫姹囨��" }];
+ const mainTabIndex = ref(0);
+ const handleMainTabChange = item => {
+ mainTabIndex.value = item.index;
+ if (mainTabIndex.value === 0) {
+ handleOutputSearch();
+ } else {
+ handleWageSearch();
+ }
+ };
+
+ // 缁村害锛歞evice / operation / product
+ const dimensionTabs = [
+ { name: "鎸夎澶�", value: "device" },
+ { name: "鎸夊伐搴�", value: "operation" },
+ { name: "鎸変骇鍝�", value: "product" },
+ ];
+ const dimensionIndex = ref(0);
+ const dimension = computed(() => dimensionTabs[dimensionIndex.value].value);
+ const handleDimensionChange = item => {
+ dimensionIndex.value = item.index;
+ handleOutputSearch();
+ };
+
+ // 鏃ユ湡鍖洪棿
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const monthNum = computed(() => dayjs(maxDate).diff(dayjs("2022-02-01"), "month") + 1);
+ const showOutputCalendar = ref(false);
+ const showWageCalendar = ref(false);
+
+ // 浜ч噺缁熻
+ const outputForm = reactive({
+ startDate: dayjs().startOf("month").format("YYYY-MM-DD"),
+ endDate: dayjs().endOf("month").format("YYYY-MM-DD"),
+ deviceId: "",
+ deviceName: "",
+ });
+ const outputList = ref([]);
+ const outputDateText = computed(() =>
+ outputForm.startDate && outputForm.endDate
+ ? `${outputForm.startDate} 鑷� ${outputForm.endDate}`
+ : "璇烽�夋嫨鏃ユ湡鍖洪棿"
+ );
+ const onOutputDateConfirm = e => {
+ outputForm.startDate = e[0];
+ outputForm.endDate = e[e.length - 1];
+ showOutputCalendar.value = false;
+ handleOutputSearch();
+ };
+
+ // 璁′欢宸ヨ祫姹囨��
+ const wageForm = reactive({
+ startDate: dayjs().startOf("month").format("YYYY-MM-DD"),
+ endDate: dayjs().endOf("month").format("YYYY-MM-DD"),
+ userName: "",
+ });
+ const wageList = ref([]);
+ const wageDateText = computed(() =>
+ wageForm.startDate && wageForm.endDate
+ ? `${wageForm.startDate} 鑷� ${wageForm.endDate}`
+ : "璇烽�夋嫨鏃ユ湡鍖洪棿"
+ );
+ const onWageDateConfirm = e => {
+ wageForm.startDate = e[0];
+ wageForm.endDate = e[e.length - 1];
+ showWageCalendar.value = false;
+ handleWageSearch();
+ };
+
+ // 璁惧涓嬫媺
+ const showDeviceSheet = ref(false);
+ const deviceOptions = ref([]);
+ const loadDevices = async () => {
+ if (deviceOptions.value.length > 0) return;
+ try {
+ const { data } = await getDeviceLedger();
+ const rows = Array.isArray(data) ? data : data?.records || [];
+ deviceOptions.value = rows.map(item => ({
+ name: item.deviceName || item.name || "-",
+ value: item.id,
+ }));
+ } catch (e) {
+ console.error("鍔犺浇璁惧鍒楄〃澶辫触", e);
+ }
+ };
+ const openDeviceSheet = () => {
+ loadDevices();
+ showDeviceSheet.value = true;
+ };
+ const selectDevice = e => {
+ outputForm.deviceId = e.value;
+ outputForm.deviceName = e.name;
+ showDeviceSheet.value = false;
+ handleOutputSearch();
+ };
+
+ // 鏌ヨ锛氫骇閲忕粺璁�
+ const handleOutputSearch = () => {
+ outputStat({
+ dimension: dimension.value,
+ startDate: outputForm.startDate || undefined,
+ endDate: outputForm.endDate || undefined,
+ deviceId: dimension.value === "device" ? outputForm.deviceId || undefined : undefined,
+ })
+ .then(res => {
+ outputList.value = Array.isArray(res.data) ? res.data : res.data?.records || [];
+ })
+ .catch(() => {
+ outputList.value = [];
+ });
+ };
+
+ // 鏌ヨ锛氳浠跺伐璧勬眹鎬�
+ const handleWageSearch = () => {
+ wageSummary({
+ startDate: wageForm.startDate || undefined,
+ endDate: wageForm.endDate || undefined,
+ userName: wageForm.userName || undefined,
+ })
+ .then(res => {
+ wageList.value = Array.isArray(res.data) ? res.data : res.data?.records || [];
+ })
+ .catch(() => {
+ wageList.value = [];
+ });
+ };
+
+ onShow(() => {
+ handleOutputSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .output-statistics {
+ min-height: 100vh;
+ background: #f8f9fa;
+ }
+
+ .main-tabs {
+ background: #fff;
+ margin-bottom: 10px;
+ }
+
+ .dimension-tabs {
+ margin-bottom: 16px;
+ }
+
+ .filter-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .date-input,
+ .device-input {
+ flex: 1;
+ height: 40px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .date-input .date-text,
+ .device-input {
+ font-size: 14px;
+ color: #333;
+ }
+
+ .device-input {
+ justify-content: space-between;
+ }
+
+ .placeholder {
+ color: #999;
+ }
+
+ .name-input {
+ flex: 1;
+ height: 40px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ }
+
+ .filter-actions-top {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 12px;
+ }
+
+ .item-index {
+ font-size: 12px;
+ color: #999;
+ background: #f5f5f5;
+ padding: 2px 8px;
+ border-radius: 12px;
+ }
+
+ .ledger-item.no-click:active {
+ transform: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+</style>
diff --git a/src/pages/productionManagement/pieceRateConfig/index.vue b/src/pages/productionManagement/pieceRateConfig/index.vue
new file mode 100644
index 0000000..3688220
--- /dev/null
+++ b/src/pages/productionManagement/pieceRateConfig/index.vue
@@ -0,0 +1,698 @@
+<template>
+ <view class="sales-account">
+ <PageHeader title="璁′欢鍗曚环閰嶇疆"
+ @back="goBack" />
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="filter-row">
+ <up-input class="filter-input"
+ v-model="queryParams.operationName"
+ placeholder="宸ュ簭鍚嶇О"
+ clearable
+ @change="handleSearch" />
+ <up-input class="filter-input"
+ v-model="queryParams.productModel"
+ placeholder="浜у搧瑙勬牸"
+ clearable
+ @change="handleSearch" />
+ <view class="filter-status"
+ @click="showStatusSheet = true">
+ <text :class="{ placeholder: statusText === '' }">{{ statusText || "鍏ㄩ儴鐘舵��" }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <view class="filter-actions-top">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click="handleSearch"></up-button>
+ </view>
+ </view>
+
+ <!-- 鍒楄〃鍖哄煙 -->
+ <view v-if="list.length > 0"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="setting"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.productModel || "-" }}</text>
+ </view>
+ <up-tag :text="item.status == 1 ? '鍚敤' : '鍋滅敤'"
+ :type="item.status == 1 ? 'success' : 'info'"
+ size="mini" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">璁′欢鍗曚环</text>
+ <text class="detail-value highlight">楼{{ Number(item.unitPrice) || 0 }} / 浠�</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭鍚嶇О</text>
+ <text class="detail-value">{{ item.operationName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒涘缓鏃堕棿</text>
+ <text class="detail-value">{{ item.createTime || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ :type="item.status == 1 ? 'warning' : 'success'"
+ @click="handleChangeStatus(item)">{{ item.status == 1 ? "鍋滅敤" : "鍚敤" }}</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="handleEdit(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鏁版嵁"></up-empty>
+ </view>
+
+ <!-- 鏂板鎸夐挳 -->
+ <view class="fab-button"
+ @click="handleAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <up-popup v-model:show="showForm"
+ mode="bottom"
+ :round="20"
+ :safeAreaInsetBottom="true"
+ @close="showForm = false">
+ <view class="form-popup">
+ <view class="popup-header">
+ <text class="popup-title">{{ form.id ? "缂栬緫璁′欢鍗曚环" : "鏂板璁′欢鍗曚环" }}</text>
+ </view>
+ <view class="popup-body">
+ <up-form labelWidth="120">
+ <up-form-item label="浜у搧"
+ required>
+ <view class="pick-field"
+ @click="showProductTree = true">
+ <up-input class="pick-input"
+ v-model="form.productName"
+ placeholder="璇烽�夋嫨浜у搧"
+ readonly />
+ <up-icon name="arrow-right"
+ color="#999999"></up-icon>
+ </view>
+ </up-form-item>
+ <up-form-item label="瑙勬牸鍨嬪彿"
+ required>
+ <view class="pick-field"
+ @click="openModelSheet">
+ <up-input class="pick-input"
+ v-model="form.model"
+ placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+ readonly />
+ <up-icon name="arrow-right"
+ color="#999999"></up-icon>
+ </view>
+ </up-form-item>
+ <up-form-item label="宸ュ簭鍚嶇О"
+ required>
+ <up-input v-model="form.operationName"
+ placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璁′欢鍗曚环"
+ required>
+ <up-input v-model="form.unitPrice"
+ type="number"
+ placeholder="璇疯緭鍏ュ崟浠凤紙鍏�/浠讹級" />
+ <text class="unit-text">鍏�/浠�</text>
+ </up-form-item>
+ <up-form-item label="澶囨敞">
+ <up-input v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable />
+ </up-form-item>
+ </up-form>
+ </view>
+ <view class="popup-footer">
+ <up-button text="鍙栨秷"
+ type="info"
+ plain
+ :customStyle="{ flex: 1, marginRight: '12px' }"
+ @click="showForm = false" />
+ <up-button text="淇濆瓨"
+ type="primary"
+ :customStyle="{ flex: 1 }"
+ :loading="submitting"
+ @click="handleSubmit" />
+ </view>
+ </view>
+ </up-popup>
+
+ <!-- 浜у搧鏍戦�夋嫨鍣� -->
+ <up-popup v-model:show="showProductTree"
+ mode="bottom"
+ :round="true"
+ :closeable="true"
+ @close="showProductTree = false">
+ <view class="tree-selector">
+ <view class="tree-header">
+ <text class="tree-title">閫夋嫨浜у搧</text>
+ </view>
+ <scroll-view scroll-y
+ class="tree-content">
+ <view v-for="(node, index) in productOptions"
+ :key="index"
+ class="tree-node">
+ <view v-if="node.children && node.children.length > 0"
+ class="tree-node-header"
+ @click="toggleNode(node)">
+ <up-icon :name="node.expanded ? 'arrow-down' : 'arrow-right'"
+ class="tree-node-icon" />
+ <text class="tree-node-label">{{ node.label }}</text>
+ </view>
+ <view v-else
+ class="tree-node-header"
+ @click="selectTreeNode(node)">
+ <text class="tree-node-icon-placeholder"></text>
+ <text class="tree-node-label">{{ node.label }}</text>
+ <up-icon name="checkmark"
+ v-if="form.productId == node.value"
+ class="tree-node-check" />
+ </view>
+ <view v-if="node.children && node.children.length > 0 && node.expanded"
+ class="tree-node-children">
+ <view v-for="(child, childIndex) in node.children"
+ :key="childIndex"
+ class="tree-node">
+ <view v-if="child.children && child.children.length > 0"
+ class="tree-node-header"
+ @click="toggleNode(child)">
+ <up-icon :name="child.expanded ? 'arrow-down' : 'arrow-right'"
+ class="tree-node-icon" />
+ <text class="tree-node-label">{{ child.label }}</text>
+ </view>
+ <view v-else
+ class="tree-node-header"
+ @click="selectTreeNode(child)">
+ <text class="tree-node-icon-placeholder"></text>
+ <text class="tree-node-label">{{ child.label }}</text>
+ <up-icon name="checkmark"
+ v-if="form.productId == child.value"
+ class="tree-node-check" />
+ </view>
+ <view v-if="child.children && child.children.length > 0 && child.expanded"
+ class="tree-node-children">
+ <view v-for="(leaf, leafIndex) in child.children"
+ :key="leafIndex"
+ class="tree-node-header"
+ @click="selectTreeNode(leaf)">
+ <text class="tree-node-icon-placeholder"></text>
+ <text class="tree-node-label">{{ leaf.label }}</text>
+ <up-icon name="checkmark"
+ v-if="form.productId == leaf.value"
+ class="tree-node-check" />
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+
+ <!-- 瑙勬牸鍨嬪彿閫夋嫨鍣� -->
+ <up-action-sheet :show="showModelSheet"
+ :actions="modelOptions"
+ title="閫夋嫨瑙勬牸鍨嬪彿"
+ @select="selectModel"
+ @close="showModelSheet = false" />
+
+ <!-- 鐘舵�佺瓫閫� -->
+ <up-action-sheet :show="showStatusSheet"
+ :actions="statusOptions"
+ title="閫夋嫨鐘舵��"
+ @select="selectStatus"
+ @close="showStatusSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { productTreeList, modelList } from "@/api/basicData/product.js";
+ import {
+ listPage,
+ add,
+ update,
+ changeStatus,
+ batchDelete,
+ } from "@/api/productionManagement/pieceRateConfig.js";
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鍒楄〃涓庡垎椤�
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+ const loading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+
+ const queryParams = reactive({
+ operationName: "",
+ productModel: "",
+ status: "",
+ });
+ const statusText = ref("");
+
+ // 鐘舵�佺瓫閫夐�夐」
+ const statusOptions = [
+ { name: "鍏ㄩ儴鐘舵��", value: "" },
+ { name: "鍚敤", value: 1 },
+ { name: "鍋滅敤", value: 0 },
+ ];
+
+ const showStatusSheet = ref(false);
+
+ const selectStatus = e => {
+ queryParams.status = e.value;
+ statusText.value = e.name === "鍏ㄩ儴鐘舵��" ? "" : e.name;
+ showStatusSheet.value = false;
+ handleSearch();
+ };
+
+ // 鍏煎涓嶅悓灞傜骇鐨勫垪琛ㄨ繑鍥炵粨鏋�
+ const pickList = res => {
+ const rows = res?.data?.records || res?.data || res?.records || res;
+ return Array.isArray(rows) ? rows : [];
+ };
+
+ const getList = () => {
+ if (loading.value || pageStatus.value === "loading") return;
+ loading.value = true;
+ pageStatus.value = "loading";
+ const params = {
+ current: page.current,
+ size: page.size,
+ operationName: queryParams.operationName || undefined,
+ productModel: queryParams.productModel || undefined,
+ status: queryParams.status === "" ? undefined : queryParams.status,
+ };
+ listPage(params)
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+ list.value =
+ page.current === 1 ? records : [...list.value, ...records];
+ page.total = total;
+ if (list.value.length >= total || records.length < page.size) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current++;
+ }
+ })
+ .catch(() => {
+ pageStatus.value = "loadmore";
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ list.value = [];
+ pageStatus.value = "loadmore";
+ getList();
+ };
+
+ // ============ 琛ㄥ崟 ============
+ const showForm = ref(false);
+ const submitting = ref(false);
+ const form = ref({
+ id: null,
+ productId: "",
+ productName: "",
+ productModelId: "",
+ model: "",
+ operationName: "",
+ unitPrice: "",
+ remark: "",
+ });
+
+ // 浜у搧鏍�
+ const showProductTree = ref(false);
+ const productOptions = ref([]);
+ const showModelSheet = ref(false);
+ const modelOptions = ref([]);
+
+ const convertIdToValue = data => {
+ return (data || []).map(item => {
+ const { id, children, ...rest } = item;
+ const newItem = { ...rest, value: id };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+ return newItem;
+ });
+ };
+
+ const findNodeById = (nodes, id) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === id) return nodes[i].label;
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const found = findNodeById(nodes[i].children, id);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+
+ const getProductOptions = () => {
+ if (productOptions.value.length > 0) return;
+ productTreeList()
+ .then(res => {
+ productOptions.value = convertIdToValue(pickList(res));
+ })
+ .catch(() => {});
+ };
+
+ const toggleNode = node => {
+ node.expanded = !node.expanded;
+ };
+
+ const selectTreeNode = node => {
+ if (node.children && node.children.length > 0) return;
+ form.value.productId = node.value;
+ form.value.productName = node.label;
+ showProductTree.value = false;
+ getModels(node.value);
+ };
+
+ const getModels = productId => {
+ form.value.productModelId = "";
+ form.value.model = "";
+ modelOptions.value = [];
+ modelList({ id: productId })
+ .then(res => {
+ modelOptions.value = pickList(res).map(item => ({
+ name: item.model,
+ value: item.id,
+ }));
+ })
+ .catch(() => {
+ modelOptions.value = [];
+ });
+ };
+
+ const openModelSheet = () => {
+ if (!form.value.productId) {
+ uni.showToast({ title: "璇峰厛閫夋嫨浜у搧", icon: "none" });
+ return;
+ }
+ showModelSheet.value = true;
+ };
+
+ const selectModel = e => {
+ form.value.productModelId = e.value;
+ form.value.model = e.name;
+ showModelSheet.value = false;
+ };
+
+ const handleAdd = () => {
+ form.value = {
+ id: null,
+ productId: "",
+ productName: "",
+ productModelId: "",
+ model: "",
+ operationName: "",
+ unitPrice: "",
+ remark: "",
+ };
+ getProductOptions();
+ showForm.value = true;
+ };
+
+ const handleEdit = item => {
+ form.value = {
+ id: item.id,
+ productId: item.productId || "",
+ productName: item.productName || "",
+ productModelId: item.productModelId || "",
+ model: item.productModel || "",
+ operationName: item.operationName || "",
+ unitPrice: item.unitPrice ?? "",
+ remark: item.remark || "",
+ };
+ getProductOptions();
+ if (item.productModelId) {
+ modelList({ id: item.productId })
+ .then(res => {
+ modelOptions.value = pickList(res).map(m => ({
+ name: m.model,
+ value: m.id,
+ }));
+ })
+ .catch(() => {});
+ }
+ showForm.value = true;
+ };
+
+ const handleSubmit = () => {
+ if (!form.value.productModelId) {
+ uni.showToast({ title: "璇烽�夋嫨瑙勬牸鍨嬪彿", icon: "none" });
+ return;
+ }
+ if (!form.value.operationName) {
+ uni.showToast({ title: "璇疯緭鍏ュ伐搴忓悕绉�", icon: "none" });
+ return;
+ }
+ const unitPrice = Number(form.value.unitPrice);
+ if (!(unitPrice > 0)) {
+ uni.showToast({ title: "璁′欢鍗曚环蹇呴』澶т簬0", icon: "none" });
+ return;
+ }
+ submitting.value = true;
+ const payload = {
+ productModelId: form.value.productModelId,
+ productModel: form.value.model,
+ operationName: form.value.operationName,
+ unitPrice: unitPrice,
+ remark: form.value.remark,
+ };
+ const req = form.value.id
+ ? update({ ...payload, id: form.value.id })
+ : add(payload);
+ req
+ .then(() => {
+ uni.showToast({ title: form.value.id ? "淇敼鎴愬姛" : "鏂板鎴愬姛" });
+ showForm.value = false;
+ handleSearch();
+ })
+ .catch(() => {})
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ const handleChangeStatus = item => {
+ const nextStatus = item.status == 1 ? 0 : 1;
+ changeStatus({ id: item.id, status: nextStatus })
+ .then(() => {
+ uni.showToast({ title: nextStatus == 1 ? "宸插惎鐢�" : "宸插仠鐢�" });
+ handleSearch();
+ })
+ .catch(() => {});
+ };
+
+ const handleDelete = item => {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭畾瑕佸垹闄よ璁′欢鍗曚环閰嶇疆鍚楋紵",
+ success: res => {
+ if (res.confirm) {
+ batchDelete([item.id])
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛" });
+ handleSearch();
+ })
+ .catch(() => {});
+ }
+ },
+ });
+ };
+
+ onReachBottom(() => {
+ if (pageStatus.value === "nomore") return;
+ if (pageStatus.value === "loading") return;
+ getList();
+ });
+
+ onShow(() => {
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .filter-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .filter-input {
+ flex: 1;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ height: 40px;
+ }
+
+ .filter-status {
+ flex: 1;
+ height: 40px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ color: #333;
+
+ .placeholder {
+ color: #999;
+ }
+ }
+ }
+
+ .filter-actions-top {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 12px;
+ }
+
+ .form-popup {
+ background: #fff;
+
+ .popup-header {
+ padding: 16px;
+ border-bottom: 1px solid #f5f5f5;
+ text-align: center;
+ }
+
+ .popup-title {
+ font-size: 16px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ .popup-body {
+ padding: 8px 16px;
+ max-height: 60vh;
+ overflow-y: auto;
+ }
+
+ .unit-text {
+ margin-left: 8px;
+ color: #909399;
+ font-size: 12px;
+ }
+
+ // 閫夋嫨绫诲瓧娈碉細readonly 鐨� up-input 浼氭妸鍘熺敓 input 缃负 disabled锛�
+ // disabled 鍏冪礌浼氬悶鎺夌偣鍑讳簨浠跺鑷村灞傛敹涓嶅埌锛屾晠璁� input 涓嶅弬涓庢寚閽堜簨浠讹紝鐢� .pick-field 鎺ョ
+ .pick-field {
+ flex: 1;
+ display: flex;
+ align-items: center;
+
+ .pick-input {
+ flex: 1;
+ }
+
+ :deep(input) {
+ pointer-events: none;
+ }
+ }
+
+ .popup-footer {
+ display: flex;
+ padding: 12px 16px 20px;
+ }
+ }
+
+ .tree-selector {
+ padding: 16px;
+
+ .tree-header {
+ text-align: center;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f5f5f5;
+ }
+
+ .tree-title {
+ font-size: 16px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ .tree-content {
+ max-height: 60vh;
+ }
+
+ .tree-node-header {
+ display: flex;
+ align-items: center;
+ padding: 12px 4px;
+ border-bottom: 1px solid #f7f7f7;
+
+ .tree-node-icon {
+ margin-right: 8px;
+ }
+
+ .tree-node-icon-placeholder {
+ width: 16px;
+ margin-right: 8px;
+ }
+
+ .tree-node-label {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+ }
+ }
+
+ .tree-node-children {
+ padding-left: 24px;
+ }
+ }
+</style>
diff --git a/src/pages/productionManagement/productionAccounting/index.vue b/src/pages/productionManagement/productionAccounting/index.vue
index 87ec382..ad6b4de 100644
--- a/src/pages/productionManagement/productionAccounting/index.vue
+++ b/src/pages/productionManagement/productionAccounting/index.vue
@@ -124,6 +124,18 @@
<text class="detail-value">{{ item.process }}</text>
</view>
<view class="detail-row">
+ <text class="detail-label">宸ヨ祫鍙e緞</text>
+ <view class="detail-value">
+ <up-tag :text="item.wageMode || '璁℃椂'"
+ :type="item.wageMode === '璁′欢' ? 'success' : 'info'"
+ size="mini" />
+ </view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浣跨敤璁惧</text>
+ <text class="detail-value">{{ item.deviceName || "-" }}</text>
+ </view>
+ <view class="detail-row">
<text class="detail-label">鐢熶骇鏁伴噺</text>
<text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
</view>
diff --git a/src/pages/productionManagement/productionReport/index.vue b/src/pages/productionManagement/productionReport/index.vue
index ac92038..1d20f0a 100644
--- a/src/pages/productionManagement/productionReport/index.vue
+++ b/src/pages/productionManagement/productionReport/index.vue
@@ -40,6 +40,15 @@
@click="openProducerPicker"
suffix-icon="arrow-down" />
</u-form-item>
+ <!-- 浣跨敤璁惧 -->
+ <u-form-item label="浣跨敤璁惧"
+ prop="deviceId">
+ <u-input v-model="form.deviceName"
+ placeholder="璇烽�夋嫨浣跨敤璁惧锛堥�夊~锛�"
+ readonly
+ @click="openDevicePicker"
+ suffix-icon="arrow-down" />
+ </u-form-item>
<!-- 宸ユ椂 -->
<u-form-item label="宸ユ椂"
v-if="form.type == 0"
@@ -115,6 +124,12 @@
title="閫夋嫨鐢熶骇浜�"
@select="onProducerConfirm"
@close="showProducerPicker = false" />
+ <!-- 浣跨敤璁惧閫夋嫨鍣� -->
+ <up-action-sheet :show="showDevicePicker"
+ :actions="deviceList"
+ title="閫夋嫨浣跨敤璁惧"
+ @select="onDeviceConfirm"
+ @close="showDevicePicker = false" />
<!-- 鍙傛暟閫夋嫨鍣� -->
<up-action-sheet :show="showParamSelect"
:actions="paramOptions"
@@ -142,6 +157,7 @@
});
};
import { addProductMain } from "@/api/productionManagement/productionReporting";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { getInfo } from "@/api/login";
import { userListNoPageByTenantId } from "@/api/system/user";
import {
@@ -167,10 +183,39 @@
workHour: 0,
type: null,
paramGroups: {},
+ deviceId: "",
+ deviceName: "",
});
const showProducerPicker = ref(false);
const producerList = ref([]);
+
+ // 浣跨敤璁惧閫夋嫨
+ const showDevicePicker = ref(false);
+ const deviceList = ref([]);
+
+ const openDevicePicker = async () => {
+ if (deviceList.value.length === 0) {
+ try {
+ const { data } = await getDeviceLedger();
+ const rows = Array.isArray(data) ? data : data?.records || [];
+ deviceList.value = rows.map(item => ({
+ name: item.deviceName || item.name || "-",
+ value: item.id,
+ }));
+ } catch (error) {
+ console.error("鍔犺浇璁惧鍒楄〃澶辫触:", error);
+ return;
+ }
+ }
+ showDevicePicker.value = true;
+ };
+
+ const onDeviceConfirm = e => {
+ form.value.deviceId = e.value;
+ form.value.deviceName = e.name;
+ showDevicePicker.value = false;
+ };
const params = ref([]);
const dictOptions = ref({});
@@ -361,6 +406,7 @@
form.value.productionOrderRoutingOperationId,
productionOrderId: form.value.productionOrderId,
workHour: form.value.workHour,
+ deviceId: form.value.deviceId || undefined,
productionOperationParamList: productionOperationParamList,
};
diff --git a/src/pages/productionManagement/productionReporting/ledger.vue b/src/pages/productionManagement/productionReporting/ledger.vue
index fb5d1dd..98d0250 100644
--- a/src/pages/productionManagement/productionReporting/ledger.vue
+++ b/src/pages/productionManagement/productionReporting/ledger.vue
@@ -19,6 +19,14 @@
color="#999"></up-icon>
</view>
</view>
+ <!-- 璁惧绛涢�� -->
+ <view class="device-filter"
+ @click="openDeviceFilter">
+ <text :class="{ placeholder: !searchForm.deviceName }">{{ searchForm.deviceName || "鍏ㄩ儴璁惧" }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999"></up-icon>
+ </view>
</view>
<!-- 鍒楄〃鍖哄煙 -->
<scroll-view scroll-y
@@ -47,6 +55,10 @@
<view class="detail-row">
<text class="detail-label">鎶ュ伐浜哄憳</text>
<text class="detail-value highlight">{{ item.nickName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浣跨敤璁惧</text>
+ <text class="detail-value">{{ item.deviceName || "-" }}</text>
</view>
<view class="detail-row">
<text class="detail-label">宸ユ椂(h)</text>
@@ -169,6 +181,12 @@
text="鏆傛棤鍙傛暟鏁版嵁" />
</view>
</up-modal>
+ <!-- 璁惧绛涢�� -->
+ <up-action-sheet :show="showDeviceSheet"
+ :actions="deviceOptions"
+ title="閫夋嫨璁惧"
+ @select="selectDevice"
+ @close="showDeviceSheet = false" />
</view>
</template>
@@ -181,6 +199,7 @@
productionProductInputListPage,
} from "@/api/productionManagement/productionProductMain.js";
import PageHeader from "@/components/PageHeader.vue";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import modal from "@/plugins/modal";
const tableData = ref([]);
@@ -195,7 +214,44 @@
const searchForm = reactive({
keyword: "",
+ deviceId: "",
+ deviceName: "",
});
+
+ // 璁惧绛涢��
+ const showDeviceSheet = ref(false);
+ const deviceOptions = ref([
+ { name: "鍏ㄩ儴璁惧", value: "" },
+ ]);
+
+ const loadDevices = async () => {
+ if (deviceOptions.value.length > 1) return;
+ try {
+ const { data } = await getDeviceLedger();
+ const rows = Array.isArray(data) ? data : data?.records || [];
+ deviceOptions.value = [
+ { name: "鍏ㄩ儴璁惧", value: "" },
+ ...rows.map(item => ({
+ name: item.deviceName || item.name || "-",
+ value: item.id,
+ })),
+ ];
+ } catch (error) {
+ console.error("鍔犺浇璁惧鍒楄〃澶辫触:", error);
+ }
+ };
+
+ const openDeviceFilter = () => {
+ loadDevices();
+ showDeviceSheet.value = true;
+ };
+
+ const selectDevice = e => {
+ searchForm.deviceId = e.value;
+ searchForm.deviceName = e.value ? e.name : "";
+ showDeviceSheet.value = false;
+ handleQuery();
+ };
// 鎶曞叆璇︽儏鐩稿叧
const inputVisible = ref(false);
@@ -233,6 +289,7 @@
current: page.current,
size: page.size,
workOrderNo: searchForm.keyword,
+ deviceId: searchForm.deviceId || undefined,
};
productionProductMainListPage(params)
@@ -321,6 +378,23 @@
height: 0;
}
+ .device-filter {
+ margin-top: 12px;
+ height: 40px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ padding: 0 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ color: #333;
+
+ .placeholder {
+ color: #999;
+ }
+ }
+
.ledger-item {
.item-header {
.item-tag {
diff --git a/src/pages/productionManagement/productionTraceability/index.vue b/src/pages/productionManagement/productionTraceability/index.vue
index d187543..7a55b5f 100644
--- a/src/pages/productionManagement/productionTraceability/index.vue
+++ b/src/pages/productionManagement/productionTraceability/index.vue
@@ -2,510 +2,887 @@
<view class="production-traceability">
<PageHeader title="鐢熶骇杩芥函"
@back="goBack" />
+ <!-- 鏂瑰悜椤电 -->
+ <view class="trace-tabs">
+ <view class="tab-item"
+ :class="{ active: direction === 'forward' }"
+ @click="switchDirection('forward')">姝e悜杩芥函</view>
+ <view class="tab-item"
+ :class="{ active: direction === 'reverse' }"
+ @click="switchDirection('reverse')">鍙嶅悜杩芥函</view>
+ </view>
<!-- 鎼滅储鍖哄煙 -->
<view class="search-section">
- <view class="search-bar"
- @click="openNpsNoSelector">
+ <view class="search-bar">
<view class="search-input">
- <text v-if="!selectedNpsNo"
- class="placeholder">璇烽�夋嫨鐢熶骇璁㈠崟鍙�</text>
- <text v-else
- class="value">{{ selectedNpsNoLabel }}</text>
- </view>
- <view class="search-button">
- <up-icon name="arrow-down"
- size="20"
+ <up-icon name="search"
+ size="18"
color="#999"></up-icon>
+ <up-input class="search-text"
+ v-model="batchNo"
+ :placeholder="placeholder"
+ border="none"
+ clearable
+ @input="onBatchInput"
+ @focus="onBatchFocus"
+ @clear="onBatchClear" />
</view>
</view>
+ <!-- 鎵瑰彿鍊欓�変笅鎷� -->
+ <view class="option-panel"
+ v-if="showOptions">
+ <view class="option-item"
+ v-for="(opt, index) in batchOptions"
+ :key="index"
+ @click="onSelectOption(opt)">
+ <view class="option-top">
+ <text class="option-no">{{ opt.batchNo || '-' }}</text>
+ <text class="option-qty">{{ num(opt.qty) }}{{ opt.unit || '' }}</text>
+ </view>
+ <view class="option-bottom">
+ <text class="option-name">{{ text(opt.productName) }} / {{ text(opt.productModel) }}</text>
+ <text class="option-time">{{ opt.inTime || '-' }}</text>
+ </view>
+ </view>
+ <view class="option-empty"
+ v-if="!optionsLoading && batchOptions.length === 0">鏃犲尮閰嶆壒鍙凤紝鍙洿鎺ヨ緭鍏ュ悗鏌ヨ</view>
+ </view>
+ <view class="search-tip">{{ direction === 'forward' ? '杈撳叆鎴愬搧鎵瑰彿锛岃拷婧敓浜у叏閾捐矾' : '杈撳叆鍘熸枡鎵瑰彿锛屽畾浣嶅彈褰卞搷鐨勬垚鍝佽寖鍥�' }}</view>
+ <view class="btn-row">
+ <up-button type="primary"
+ size="small"
+ text="鏌ヨ"
+ @click="handleQuery"></up-button>
+ </view>
</view>
- <!-- 鍐呭鍖哄煙 -->
- <view class="content-container"
- v-if="rowData.productionOrderDto">
- <!-- 鍩虹淇℃伅 -->
- <view class="info-card">
- <view class="card-title">鍩虹淇℃伅</view>
- <view class="base-info">
- <view class="info-row">
- <text class="label">鐢熶骇璁㈠崟鍙凤細</text>
- <text class="value">{{ rowData.productionOrderDto?.npsNo || '-' }}</text>
- </view>
- <view class="info-row">
- <text class="label">浜у搧鍚嶇О锛�</text>
- <text class="value">{{ rowData.productionOrderDto?.productName || '-' }}</text>
- </view>
- <view class="info-row">
- <text class="label">瑙勬牸鍨嬪彿锛�</text>
- <text class="value">{{ rowData.productionOrderDto?.model || '-' }}</text>
- </view>
- <view class="info-row">
- <text class="label">璁″垝鏁伴噺锛�</text>
- <text class="value">{{ rowData.productionOrderDto?.quantity || 0 }} {{ rowData.productionOrderDto?.unit }}</text>
- </view>
- <view class="info-row">
- <text class="label">褰撳墠鐘舵�侊細</text>
- <view class="value">
- <up-tag :text="getStatusText(rowData.productionOrderDto?.status)"
- :type="getStatusType(rowData.productionOrderDto?.status)"
+ <!-- 鍔犺浇涓� -->
+ <view v-if="loading"
+ class="loading-box">
+ <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ </view>
+ <template v-else>
+ <!-- 鏈煡璇㈡彁绀� -->
+ <view v-if="!queried"
+ class="no-data">
+ <up-empty mode="search"
+ :text="direction === 'forward' ? '璇疯緭鍏ユ垚鍝佹壒鍙锋煡璇㈣拷婧摼璺�' : '璇疯緭鍏ュ師鏂欐壒鍙锋煡璇㈠奖鍝嶈寖鍥�'"></up-empty>
+ </view>
+ <!-- ================= 姝e悜杩芥函 ================= -->
+ <view v-else-if="direction === 'forward'"
+ class="result-container">
+ <view v-if="forwardEmpty"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏈煡璇㈠埌璇ユ壒鍙风殑杩芥函璁板綍"></up-empty>
+ </view>
+ <template v-else>
+ <view class="result-title">鐢熶骇璁㈠崟閾捐矾锛坽{ forwardData.chains.length }}锛�</view>
+ <view v-for="(chain, ci) in forwardData.chains"
+ :key="ci"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ text(chain.npsNo) }}</text>
+ </view>
+ <up-tag :text="orderStatusText(chain.orderStatus)"
+ :type="orderStatusType(chain.orderStatus)"
size="mini" />
</view>
- </view>
- <view class="info-row">
- <text class="label">瀹㈡埛鍚嶇О锛�</text>
- <text class="value">{{ rowData.productionOrderDto?.customerName || '-' }}</text>
- </view>
- <view class="info-row">
- <text class="label">寮�濮嬫棩鏈燂細</text>
- <text class="value">{{ formatDate(rowData.productionOrderDto?.startTime) }}</text>
- </view>
- <view class="info-row">
- <text class="label">瀹屾垚杩涘害锛�</text>
- <view class="value progress-box">
- <up-line-progress :percentage="formatProgress(rowData.productionOrderDto?.completionStatus)"
- :activeColor="progressColor(formatProgress(rowData.productionOrderDto?.completionStatus))"
- :showText="true" />
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鎴愬搧</text>
+ <text class="detail-value font-bold">{{ text(chain.finishedProduct) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ text(chain.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝鏁伴噺</text>
+ <text class="detail-value">{{ num(chain.planQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹屽伐鏁伴噺</text>
+ <text class="detail-value highlight">{{ num(chain.completeQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁㈠崟鍒涘缓鏃堕棿</text>
+ <text class="detail-value">{{ text(chain.orderCreateTime) }}</text>
+ </view>
+ </view>
+ <!-- 鍛戒腑鐨勫叆搴撴壒娆� -->
+ <view class="chip-wrap"
+ v-if="chain.batches && chain.batches.length">
+ <text class="chip"
+ v-for="(b, bi) in chain.batches"
+ :key="bi">{{ text(b.batchNo) }} 路 {{ recordTypeText(b.recordType) }} 路 {{ num(b.qty) }}{{ chain.unit || '' }}</text>
+ </view>
+ <!-- 鎶ュ伐宸ュ簭 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(ci, 'reports')">
+ <text class="collapse-title">鎶ュ伐宸ュ簭</text>
+ <text class="collapse-count">{{ (chain.reports || []).length }}</text>
+ <up-icon :name="isSectionOpen(ci, 'reports') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(ci, 'reports')">
+ <view class="sub-item"
+ v-for="(r, ri) in chain.reports"
+ :key="ri">
+ <view class="detail-row">
+ <text class="detail-label">宸ュ崟鍙�</text>
+ <text class="detail-value font-bold">{{ text(r.workOrderNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭</text>
+ <text class="detail-value">{{ text(r.operationName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐浜�</text>
+ <text class="detail-value">{{ text(r.userName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁惧</text>
+ <text class="detail-value">{{ text(r.deviceName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐鍗曞彿</text>
+ <text class="detail-value">{{ text(r.productNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜у嚭鏁伴噺</text>
+ <text class="detail-value">{{ num(r.outputQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ簾鏁伴噺</text>
+ <text class="detail-value danger">{{ num(r.scrapQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐鏃堕棿</text>
+ <text class="detail-value">{{ text(r.reportTime) }}</text>
+ </view>
+ </view>
+ <view class="sub-empty"
+ v-if="!(chain.reports || []).length">鏆傛棤鎶ュ伐璁板綍</view>
+ </view>
+ </view>
+ <!-- 鎶曞叆鐗╂枡 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(ci, 'inputs')">
+ <text class="collapse-title">鎶曞叆鐗╂枡</text>
+ <text class="collapse-count">{{ (chain.inputs || []).length }}</text>
+ <up-icon :name="isSectionOpen(ci, 'inputs') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(ci, 'inputs')">
+ <view class="sub-item"
+ v-for="(m, mi) in chain.inputs"
+ :key="mi">
+ <view class="detail-row">
+ <text class="detail-label">鐗╂枡</text>
+ <text class="detail-value font-bold">{{ text(m.materialName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ text(m.materialModel) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曞叆鏁伴噺</text>
+ <text class="detail-value">{{ num(m.inputQty) }} {{ text(m.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭</text>
+ <text class="detail-value">{{ text(m.operationName) }}</text>
+ </view>
+ </view>
+ <view class="sub-empty"
+ v-if="!(chain.inputs || []).length">鏆傛棤鎶曞叆鐗╂枡</view>
+ </view>
+ </view>
+ <!-- 棰嗘枡鎵规 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(ci, 'picks')">
+ <text class="collapse-title">棰嗘枡鎵规</text>
+ <text class="collapse-count">{{ (chain.picks || []).length }}</text>
+ <up-icon :name="isSectionOpen(ci, 'picks') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(ci, 'picks')">
+ <view class="sub-item"
+ v-for="(p, pi) in chain.picks"
+ :key="pi">
+ <view class="detail-row">
+ <text class="detail-label">鍘熸枡鎵瑰彿</text>
+ <text class="detail-value link"
+ @click="jumpToReverse(p.batchNo)">{{ text(p.batchNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐗╂枡</text>
+ <text class="detail-value">{{ text(p.materialName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ text(p.materialModel) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">棰嗙敤鏁伴噺</text>
+ <text class="detail-value">{{ num(p.quantity) }} {{ text(p.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭</text>
+ <text class="detail-value">{{ text(p.operationName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">棰嗘枡鏃堕棿</text>
+ <text class="detail-value">{{ text(p.pickTime) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">渚涘簲鍟�</text>
+ <text class="detail-value">{{ text(p.supplierName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閲囪喘鍚堝悓鍙�</text>
+ <text class="detail-value">{{ text(p.contractNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍘熸枡妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ text(p.checkResult) }}</text>
+ </view>
+ </view>
+ <view class="sub-empty"
+ v-if="!(chain.picks || []).length">鏆傛棤棰嗘枡璁板綍</view>
+ </view>
+ </view>
+ <!-- 妫�楠屽崟 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(ci, 'inspects')">
+ <text class="collapse-title">妫�楠屽崟</text>
+ <text class="collapse-count">{{ (chain.inspects || []).length }}</text>
+ <up-icon :name="isSectionOpen(ci, 'inspects') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(ci, 'inspects')">
+ <view class="sub-item"
+ v-for="(ins, ii) in chain.inspects"
+ :key="ii">
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀被鍨�</text>
+ <text class="detail-value font-bold">{{ text(ins.inspectTypeName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠屽憳</text>
+ <text class="detail-value">{{ text(ins.checkName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏁伴噺</text>
+ <text class="detail-value">{{ num(ins.quantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ num(ins.qualifiedQuantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value danger">{{ num(ins.unqualifiedQuantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ text(ins.checkResult) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠屾椂闂�</text>
+ <text class="detail-value">{{ text(ins.checkTime) }}</text>
+ </view>
+ <!-- 涓嶅悎鏍煎鐞� -->
+ <view class="unqualified-box"
+ v-if="(ins.unqualifiedList || []).length">
+ <view class="unqualified-title">涓嶅悎鏍煎鐞�</view>
+ <view class="inner-item"
+ v-for="(u, ui) in ins.unqualifiedList"
+ :key="ui">
+ <view class="detail-row">
+ <text class="detail-label">缂洪櫡鐜拌薄</text>
+ <text class="detail-value danger">{{ text(u.defectivePhenomena) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏁伴噺</text>
+ <text class="detail-value">{{ num(u.quantity) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶勭悊缁撴灉</text>
+ <text class="detail-value">{{ text(u.dealResult) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶勭悊浜�</text>
+ <text class="detail-value">{{ text(u.dealName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶勭悊鏃堕棿</text>
+ <text class="detail-value">{{ text(u.dealTime) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view class="sub-empty"
+ v-if="!(chain.inspects || []).length">鏆傛棤妫�楠岃褰�</view>
+ </view>
</view>
</view>
- </view>
+ <!-- 鍑哄簱娴佸悜 -->
+ <view class="result-title">鍑哄簱娴佸悜锛坽{ forwardData.outbounds.length }}锛�</view>
+ <view v-for="(o, oi) in forwardData.outbounds"
+ :key="oi"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon out">
+ <up-icon name="arrow-upward"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ text(o.batchNo) }}</text>
+ </view>
+ <text class="type-tag">{{ recordTypeText(o.recordType) }}</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value font-bold">{{ text(o.productName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ text(o.productModel) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍑哄簱鏁伴噺</text>
+ <text class="detail-value highlight">{{ num(o.qty) }} {{ text(o.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍑哄簱鏃堕棿</text>
+ <text class="detail-value">{{ text(o.outTime) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ text(o.remark) }}</text>
+ </view>
+ </view>
+ </view>
+ <view class="sub-empty card"
+ v-if="!forwardData.outbounds.length">鏆傛棤鍑哄簱娴佸悜璁板綍</view>
+ </template>
</view>
- <!-- 宸ュ崟淇℃伅 -->
- <view class="work-order-section"
- v-if="rowData.productionRecords && rowData.productionRecords.length > 0">
- <view class="section-title">宸ュ崟淇℃伅</view>
- <view v-for="(item, index) in rowData.productionRecords"
- :key="index"
- class="work-order-card">
- <view class="card-header">
- <text class="work-order-no">{{ item.workOrder.workOrderNo }}</text>
- <text class="progress-tag"
- :style="{ color: progressColor(item.workOrder.completionStatus) }">{{ item.workOrder.completionStatus || 0 }}%</text>
- </view>
- <view class="card-content">
- <view class="content-row">
- <text class="label">浜у搧/瑙勬牸锛�</text>
- <text class="value">{{ item.workOrder.productName }} / {{ item.workOrder.model }}</text>
- </view>
- <view class="content-row">
- <text class="label">褰撳墠宸ュ簭锛�</text>
- <text class="value">{{ item.workOrder.operationName || '-' }}</text>
- </view>
- <view class="content-row">
- <text class="label">闇�姹�/瀹屾垚锛�</text>
- <text class="value">{{ item.workOrder.planQuantity }} / {{ item.workOrder.completeQuantity }}</text>
- </view>
- <view class="content-row">
- <text class="label">鎶ュ簾鏁伴噺锛�</text>
- <text class="value error-text">{{ item.workOrder.scrapQty || 0 }}</text>
- </view>
- </view>
- <view class="card-footer">
- <up-button type="primary"
- size="small"
- plain
- text="鎶ュ伐璁板綍"
- @click="handleShowReports(item)"></up-button>
- <up-button type="success"
- size="small"
- plain
- text="璐ㄦ淇℃伅"
- @click="handleShowQuality(item)"></up-button>
- </view>
- </view>
- </view>
+ <!-- ================= 鍙嶅悜杩芥函 ================= -->
<view v-else
- class="no-data-minor">
- <up-empty mode="data"
- text="鏆傛棤宸ュ崟淇℃伅"
- icon-size="40"></up-empty>
- </view>
- </view>
- <view v-else
- class="no-data">
- <up-empty mode="search"
- text="璇烽�夋嫨鐢熶骇璁㈠崟鍙锋煡鐪嬭拷婧俊鎭�"></up-empty>
- </view>
- <!-- 鐢熶骇璁㈠崟鍙烽�夋嫨寮圭獥 -->
- <up-popup :show="showNpsNoSelector"
- mode="bottom"
- @close="showNpsNoSelector = false"
- round="10">
- <view class="selector-popup">
- <view class="popup-header">
- <text class="popup-title">閫夋嫨鐢熶骇璁㈠崟鍙�</text>
- <up-icon name="close"
- size="20"
- @click="showNpsNoSelector = false"></up-icon>
+ class="result-container">
+ <view v-if="reverseEmpty"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏈煡璇㈠埌璇ユ壒鍙风殑杩芥函璁板綍"></up-empty>
</view>
- <view class="search-box">
- <up-search placeholder="杈撳叆鍏抽敭瀛楁悳绱�"
- v-model="npsNoQuery"
- :show-action="false"
- @change="handleNpsNoSearch"
- @search="handleNpsNoSearch"
- :loading="npsNoLoading"></up-search>
- </view>
- <scroll-view scroll-y
- class="options-list">
- <view v-for="item in npsNoOptions"
- :key="item.id"
- class="option-item"
- @click="onSelectNpsNo(item)">
- <view class="option-main">
- <text class="nps-no">{{ item.npsNo }}</text>
- <text class="product-info">{{ item.productName }} / {{ item.model }}</text>
- </view>
- <up-icon v-if="selectedNpsNo === item.id"
- name="checkbox-mark"
- color="#3c9cff"
- size="20"></up-icon>
- </view>
- <view v-if="npsNoOptions.length === 0"
- class="no-options">
- <text>{{ npsNoLoading ? '鍔犺浇涓�...' : '鏆傛棤閫夐」' }}</text>
- </view>
- </scroll-view>
- </view>
- </up-popup>
- <!-- 鎶ュ伐璇︽儏寮圭獥 -->
- <up-popup :show="reportPopupVisible"
- mode="bottom"
- @close="reportPopupVisible = false"
- round="10">
- <view class="popup-content">
- <view class="popup-header">
- <text class="popup-title">鐢熶骇鎶ュ伐璇︽儏</text>
- <up-icon name="close"
- size="20"
- @click="reportPopupVisible = false"></up-icon>
- </view>
- <scroll-view scroll-y
- class="popup-scroll">
- <view class="detail-info">
- <view class="info-row"><text class="label">宸ュ崟鍙凤細</text><text class="value">{{ detailData.workOrder.workOrderNo }}</text></view>
- <view class="info-row"><text class="label">璁″垝/瀹屾垚锛�</text><text class="value">{{ detailData.workOrder.planQuantity }} / {{ detailData.workOrder.completeQuantity }}</text></view>
- <view class="info-row"><text class="label">瀹為檯鏃堕棿锛�</text><text class="value">{{ formatDate(detailData.workOrder.actualStartTime) }} 鑷� {{ formatDate(detailData.workOrder.actualEndTime) }}</text></view>
- </view>
- <view class="list-title">鎶ュ伐鏄庣粏</view>
- <view v-for="(report, idx) in detailData.reports"
- :key="idx"
- class="detail-item">
- <view class="item-main">
- <view class="item-row"><text class="label">鎶ュ伐鍗曞彿锛�</text><text class="value">{{ report.productNo }}</text></view>
- <view class="item-row"><text class="label">浜у嚭鏁伴噺锛�</text><text class="value">{{ report.quantity || 0 }}</text></view>
- <view class="item-row"><text class="label">鎶ュ簾鏁伴噺锛�</text><text class="value error-text">{{ report.scrapQty || 0 }}</text></view>
- <view class="item-row"><text class="label">宸ユ椂(h)锛�</text><text class="value">{{ report.workHour || 0 }}</text></view>
- <view class="item-row"><text class="label">鍒涘缓浜猴細</text><text class="value">{{ report.userName }}</text></view>
- <view class="item-row"><text class="label">鍒涘缓鏃堕棿锛�</text><text class="value">{{ formatDate(report.createTime, '{y}-{m}-{d} {h}:{i}') }}</text></view>
- </view>
- <view class="item-actions">
- <text class="action-link"
- @click="showParams(report.productionOperationParamList)">鍙傛暟璇︽儏</text>
- <text class="action-link green"
- @click="handleShowInput(report.id)">鎶曞叆璇︽儏</text>
- </view>
- </view>
- <view v-if="!detailData.reports || detailData.reports.length === 0"
- class="no-data-minor">鏆傛棤鎶ュ伐鏄庣粏</view>
- </scroll-view>
- </view>
- </up-popup>
- <!-- 鎶曞叆璇︽儏寮圭獥 -->
- <up-popup :show="inputPopupVisible"
- mode="bottom"
- @close="inputPopupVisible = false"
- round="10">
- <view class="popup-content">
- <view class="popup-header">
- <text class="popup-title">鎶曞叆淇℃伅璇︽儏</text>
- <up-icon name="close"
- size="20"
- @click="inputPopupVisible = false"></up-icon>
- </view>
- <scroll-view scroll-y
- class="popup-scroll">
- <view class="input-list-popup">
- <view v-for="(item, idx) in inputListData"
- :key="idx"
- class="quality-record">
- <view class="record-title">鎶曞叆璁板綍 {{ idx + 1 }}</view>
- <view class="info-grid">
- <view class="info-item"><text class="label">鎶ュ伐鍗曞彿</text><text class="value">{{ item.productNo || '-' }}</text></view>
- <view class="info-item"><text class="label">鎶曞叆鏁伴噺</text><text class="value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text></view>
- <view class="info-item full-width"><text class="label">鎶曞叆浜у搧鍚嶇О</text><text class="value">{{ item.productName || '-' }}</text></view>
- <view class="info-item full-width"><text class="label">鎶曞叆浜у搧鍨嬪彿</text><text class="value">{{ item.model || '-' }}</text></view>
+ <template v-else>
+ <!-- 鍘熸枡鏉ユ簮 -->
+ <view class="result-title">鍘熸枡鏉ユ簮</view>
+ <view class="ledger-item"
+ v-if="reverseData.material">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon material">
+ <up-icon name="cube"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ text(reverseData.material.materialName) }}</text>
</view>
+ <text class="type-tag">{{ recordTypeText(reverseData.material.recordType) }}</text>
</view>
- <view v-if="!inputListData || inputListData.length === 0"
- class="no-data-minor">{{ inputLoading ? '鍔犺浇涓�...' : '鏆傛棤鎶曞叆璁板綍' }}</view>
- </view>
- </scroll-view>
- </view>
- </up-popup>
- <!-- 璐ㄦ璇︽儏寮圭獥 -->
- <up-popup :show="qualityPopupVisible"
- mode="bottom"
- @close="qualityPopupVisible = false"
- round="10">
- <view class="popup-content">
- <view class="popup-header">
- <text class="popup-title">璐ㄦ璇︽儏</text>
- <up-icon name="close"
- size="20"
- @click="qualityPopupVisible = false"></up-icon>
- </view>
- <scroll-view scroll-y
- class="popup-scroll">
- <view v-for="(record, idx) in qualityRecords"
- :key="idx"
- class="quality-record">
- <view class="record-title">妫�娴嬭褰� {{ idx + 1 }}</view>
- <view class="info-grid">
- <view class="info-item"><text class="label">妫�娴嬫棩鏈�</text><text class="value">{{ formatDate(record.createTime) }}</text></view>
- <view class="info-item"><text class="label">妫�娴嬬粨鏋�</text><up-tag style="width:100rpx"
- :text="record.checkResult || '寰呮娴�'"
- :type="record.checkResult === '鍚堟牸' ? 'success' : 'error'"
- size="mini" /></view>
- <view class="info-item"><text class="label">妫�楠屽憳</text><text class="value">{{ record.userName }}</text></view>
- <view class="info-item"><text class="label">鏁伴噺</text><text class="value">{{ record.quantity }} {{ record.unit }}</text></view>
- <view class="info-item"><text class="label">鎶ュ伐鍗曞彿</text><text class="value">{{ record.reportNo || '-' }}</text></view>
- <view class="info-item"><text class="label">浜у搧鍚嶇О</text><text class="value">{{ record.productName || '-' }}</text></view>
- <view class="info-item"><text class="label">瑙勬牸鍨嬪彿</text><text class="value">{{ record.model || '-' }}</text></view>
- <view class="info-item"><text class="label">妫�娴嬪崟浣�</text><text class="value">{{ record.checkCompany || '-' }}</text></view>
- </view>
- <view class="params-table">
- <view class="table-header">
- <text class="col">鎸囨爣</text>
- <text class="col">鍗曚綅</text>
- <text class="col">鏍囧噯鍊�</text>
- <text class="col">鍐呮帶鍊�</text>
- <text class="col">瀹為檯鍊�</text>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍘熸枡鎵瑰彿</text>
+ <text class="detail-value font-bold">{{ text(reverseData.material.batchNo) }}</text>
</view>
- <view v-for="(param, pIdx) in record.inspectParamList"
- :key="pIdx"
- class="table-row">
- <text class="col">{{ param.parameterItem }}</text>
- <text class="col">{{ param.unit || '-' }}</text>
- <text class="col">{{ param.standardValue }}</text>
- <text class="col">{{ param.controlValue || '-' }}</text>
- <text class="col"
- :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ text(reverseData.material.materialModel) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ text(reverseData.material.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍏ュ簱鏁伴噺</text>
+ <text class="detail-value">{{ num(reverseData.material.qty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍏ュ簱鏃堕棿</text>
+ <text class="detail-value">{{ text(reverseData.material.inTime) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ text(reverseData.material.checkResult) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠屾暟閲�</text>
+ <text class="detail-value">{{ num(reverseData.material.inspectQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ num(reverseData.material.qualifiedQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value danger">{{ num(reverseData.material.unqualifiedQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠屾椂闂�</text>
+ <text class="detail-value">{{ text(reverseData.material.checkTime) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">渚涘簲鍟�</text>
+ <text class="detail-value">{{ text(reverseData.material.supplierName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閲囪喘鍚堝悓鍙�</text>
+ <text class="detail-value">{{ text(reverseData.material.contractNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閲囪喘鏃ユ湡</text>
+ <text class="detail-value">{{ text(reverseData.material.entryDate) }}</text>
</view>
</view>
</view>
- <view v-if="!qualityRecords || qualityRecords.length === 0"
- class="no-data-minor">鏆傛棤璐ㄦ璁板綍</view>
- </scroll-view>
+ <view class="ledger-item"
+ v-else>
+ <view class="sub-empty">璇ユ壒鍙锋棤鍏ュ簱璁板綍</view>
+ </view>
+ <!-- 鍙楀奖鍝嶇殑鐢熶骇璁㈠崟 -->
+ <view class="result-title">鍙楀奖鍝嶇敓浜ц鍗曪紙{{ reverseData.orders.length }}锛�</view>
+ <view v-for="(order, oi) in reverseData.orders"
+ :key="oi"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ text(order.npsNo) }}</text>
+ </view>
+ <up-tag :text="orderStatusText(order.orderStatus)"
+ :type="orderStatusType(order.orderStatus)"
+ size="mini" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鎴愬搧</text>
+ <text class="detail-value font-bold">{{ text(order.finishedProduct) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝鏁伴噺</text>
+ <text class="detail-value">{{ num(order.planQty) }} {{ text(order.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹屽伐鏁伴噺</text>
+ <text class="detail-value">{{ num(order.completeQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁㈠崟鍒涘缓鏃堕棿</text>
+ <text class="detail-value">{{ text(order.orderCreateTime) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏈鍗曢鐢ㄦ暟閲�</text>
+ <text class="detail-value highlight">{{ num(order.pickedQty) }}</text>
+ </view>
+ </view>
+ <!-- 棰嗙敤鏄庣粏 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(oi, 'rpicks')">
+ <text class="collapse-title">棰嗙敤鏄庣粏</text>
+ <text class="collapse-count">{{ (order.picks || []).length }}</text>
+ <up-icon :name="isSectionOpen(oi, 'rpicks') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(oi, 'rpicks')">
+ <view class="sub-item"
+ v-for="(p, pi) in order.picks"
+ :key="pi">
+ <view class="detail-row">
+ <text class="detail-label">鎵瑰彿</text>
+ <text class="detail-value font-bold">{{ text(p.batchNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐗╂枡</text>
+ <text class="detail-value">{{ text(p.materialName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ text(p.materialModel) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">棰嗙敤鏁伴噺</text>
+ <text class="detail-value">{{ num(p.quantity) }} {{ text(p.unit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭</text>
+ <text class="detail-value">{{ text(p.operationName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">棰嗘枡鏃堕棿</text>
+ <text class="detail-value">{{ text(p.pickTime) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">渚涘簲鍟�</text>
+ <text class="detail-value">{{ text(p.supplierName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閲囪喘鍚堝悓鍙�</text>
+ <text class="detail-value">{{ text(p.contractNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ text(p.checkResult) }}</text>
+ </view>
+ </view>
+ <view class="sub-empty"
+ v-if="!(order.picks || []).length">鏆傛棤棰嗙敤璁板綍</view>
+ </view>
+ </view>
+ <!-- 鎶ュ伐宸ュ簭 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(oi, 'rreports')">
+ <text class="collapse-title">鎶ュ伐宸ュ簭</text>
+ <text class="collapse-count">{{ (order.reports || []).length }}</text>
+ <up-icon :name="isSectionOpen(oi, 'rreports') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(oi, 'rreports')">
+ <view class="sub-item"
+ v-for="(r, ri) in order.reports"
+ :key="ri">
+ <view class="detail-row">
+ <text class="detail-label">宸ュ崟鍙�</text>
+ <text class="detail-value font-bold">{{ text(r.workOrderNo) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭</text>
+ <text class="detail-value">{{ text(r.operationName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐浜�</text>
+ <text class="detail-value">{{ text(r.userName) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜у嚭鏁伴噺</text>
+ <text class="detail-value">{{ num(r.outputQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ簾鏁伴噺</text>
+ <text class="detail-value danger">{{ num(r.scrapQty) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐鏃堕棿</text>
+ <text class="detail-value">{{ text(r.reportTime) }}</text>
+ </view>
+ </view>
+ <view class="sub-empty"
+ v-if="!(order.reports || []).length">鏆傛棤鎶ュ伐璁板綍</view>
+ </view>
+ </view>
+ <!-- 浜у嚭鎴愬搧鎵瑰彿 -->
+ <view class="collapse-block">
+ <view class="collapse-head"
+ @click="toggleSection(oi, 'batches')">
+ <text class="collapse-title">浜у嚭鎴愬搧鎵瑰彿</text>
+ <text class="collapse-count">{{ (order.finishedBatches || []).length }}</text>
+ <up-icon :name="isSectionOpen(oi, 'batches') ? 'arrow-up' : 'arrow-down'"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view class="collapse-body"
+ v-show="isSectionOpen(oi, 'batches')">
+ <view class="chip-wrap">
+ <text class="chip link-chip"
+ v-for="(b, bi) in order.finishedBatches"
+ :key="bi"
+ @click="jumpToForward(b.batchNo)">{{ text(b.batchNo) }} 路 {{ num(b.qty) }}{{ order.unit || '' }}</text>
+ </view>
+ <view class="sub-empty"
+ v-if="!(order.finishedBatches || []).length">鏆傛棤鎴愬搧鎵规</view>
+ </view>
+ </view>
+ </view>
+ <view class="sub-empty card"
+ v-if="!reverseData.orders.length">璇ユ壒鍙锋殏鏃犵敓浜ц鍗曢鐢ㄨ褰�</view>
+ </template>
</view>
- </up-popup>
- <!-- 鍙傛暟璇︽儏寮圭獥 -->
- <up-modal :show="paramModalVisible"
- title="鍙傛暟璇︽儏"
- @confirm="paramModalVisible = false">
- <view class="modal-content">
- <view v-for="(param, idx) in currentParams"
- :key="idx"
- class="param-row">
- <text class="label">{{ param.paramName }}锛�</text>
- <text class="value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? param.unit : '' }}</text>
- </view>
- <view v-if="!currentParams || currentParams.length === 0"
- class="no-data-minor">鏆傛棤鍙傛暟鏁版嵁</view>
- </view>
- </up-modal>
+ </template>
</view>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
- import {
- getOrderDetail,
- productOrderListPage,
- } from "@/api/productionManagement/productionOrder";
- import { productionProductInputListPage } from "@/api/productionManagement/productionProductMain";
import PageHeader from "@/components/PageHeader.vue";
- import { parseTime } from "@/utils/ruoyi";
+ import {
+ traceForward,
+ traceReverse,
+ traceBatchOptions,
+ } from "@/api/qualityManagement/productionTrace.js";
- // 閫夋嫨鍣ㄧ浉鍏�
- const showNpsNoSelector = ref(false);
- const npsNoQuery = ref("");
- const npsNoOptions = ref([]);
- const npsNoLoading = ref(false);
- const selectedNpsNo = ref(null);
- const selectedNpsNoLabel = ref("");
+ // 褰撳墠鏂瑰悜锛歠orward 姝e悜 / reverse 鍙嶅悜
+ const direction = ref("forward");
+ const batchNo = ref("");
+ const loading = ref(false);
+ // 鏄惁宸叉墽琛岃繃鏌ヨ
+ const queried = ref(false);
- const rowData = reactive({
- productionOrderDto: null,
- productionRecords: [],
- });
+ // 鎵瑰彿鍊欓�変笅鎷�
+ const batchOptions = ref([]);
+ const showOptions = ref(false);
+ const optionsLoading = ref(false);
+ let searchTimer = null;
- // 鎶ュ伐璇︽儏
- const reportPopupVisible = ref(false);
- const detailData = ref({ workOrder: {}, reports: [] });
+ const forwardData = reactive({ batchNo: "", chains: [], outbounds: [] });
+ const reverseData = reactive({ batchNo: "", material: null, orders: [] });
+ // 鎶樺彔闈㈡澘灞曞紑鐘舵��
+ const openMap = reactive({});
- // 鎶曞叆璇︽儏
- const inputPopupVisible = ref(false);
- const inputListData = ref([]);
- const inputLoading = ref(false);
+ const placeholder = computed(() =>
+ direction.value === "forward" ? "璇疯緭鍏ユ垨閫夋嫨鎴愬搧鎵瑰彿" : "璇疯緭鍏ユ垨閫夋嫨鍘熸枡鎵瑰彿"
+ );
- // 璐ㄦ璇︽儏
- const qualityPopupVisible = ref(false);
- const qualityRecords = ref([]);
+ const forwardEmpty = computed(
+ () =>
+ forwardData.chains.length === 0 && forwardData.outbounds.length === 0
+ );
+ const reverseEmpty = computed(
+ () => !reverseData.material && reverseData.orders.length === 0
+ );
- // 鍙傛暟璇︽儏
- const paramModalVisible = ref(false);
- const currentParams = ref([]);
+ // 鐢熶骇璁㈠崟鐘舵�佸瓧鍏革紙涓庣敓浜ц鍗曞垪琛ㄩ〉淇濇寔涓�鑷达級
+ const ORDER_STATUS_TEXT = {
+ 1: "寰呭紑濮�",
+ 2: "杩涜涓�",
+ 3: "宸插畬鎴�",
+ 4: "宸插彇娑�",
+ 5: "宸茬粨鏉�",
+ };
+ const ORDER_STATUS_TYPE = {
+ 1: "primary",
+ 2: "warning",
+ 3: "success",
+ 4: "info",
+ 5: "error",
+ };
+ // 鍏ュ簱/鍑哄簱绫诲瀷瀛楀吀
+ const RECORD_TYPE_TEXT = {
+ 0: "鑷畾涔夊叆搴�",
+ 1: "鑷畾涔夊嚭搴�",
+ 2: "鐢熶骇鎶ュ伐鍏ュ簱",
+ 3: "鐢熶骇鎶ュ伐鍑哄簱",
+ 6: "璐ㄦ鍚堟牸鍏ュ簱",
+ 7: "閲囪喘鍏ュ簱",
+ 8: "閿�鍞嚭搴�",
+ 10: "閲囪喘鍏ュ簱",
+ 13: "閿�鍞彂璐у嚭搴�",
+ };
+
+ const num = val => {
+ const n = Number(val);
+ return Number.isFinite(n) ? n : 0;
+ };
+ const text = val =>
+ val === null || val === undefined || val === "" ? "-" : val;
+ const orderStatusText = status =>
+ ORDER_STATUS_TEXT[status] ?? text(status);
+ const orderStatusType = status => ORDER_STATUS_TYPE[status] || "info";
+ const recordTypeText = type => {
+ if (type === null || type === undefined || type === "") return "-";
+ return RECORD_TYPE_TEXT[String(type)] || `绫诲瀷${type}`;
+ };
const goBack = () => {
uni.navigateBack();
};
- const openNpsNoSelector = () => {
- showNpsNoSelector.value = true;
- if (npsNoOptions.value.length === 0) {
- handleNpsNoSearch();
- }
+ // 鎶樺彔闈㈡澘
+ const isSectionOpen = (key, name) => !!openMap[key + "-" + name];
+ const toggleSection = (key, name) => {
+ const k = key + "-" + name;
+ openMap[k] = !openMap[k];
};
- const handleNpsNoSearch = async () => {
- npsNoLoading.value = true;
- try {
- const res = await productOrderListPage({
- npsNo: npsNoQuery.value || "",
- pageNum: 1,
- pageSize: 50,
+ // 鍔犺浇鎵瑰彿鍊欓��
+ const loadBatchOptions = () => {
+ optionsLoading.value = true;
+ traceBatchOptions({
+ type: direction.value === "forward" ? "finished" : "material",
+ keyword: batchNo.value || "",
+ limit: 50,
+ })
+ .then(res => {
+ batchOptions.value = res.data || [];
+ })
+ .catch(() => {
+ batchOptions.value = [];
+ })
+ .finally(() => {
+ optionsLoading.value = false;
});
- npsNoOptions.value = res.data?.records || res.rows || [];
- } catch (error) {
- console.error(error);
- } finally {
- npsNoLoading.value = false;
- }
};
- const onSelectNpsNo = async item => {
- selectedNpsNo.value = item.id;
- selectedNpsNoLabel.value = item.npsNo;
- showNpsNoSelector.value = false;
-
- uni.showLoading({ title: "鍔犺浇涓�..." });
- try {
- const res = await getOrderDetail(item.npsNo);
- if (res.code === 200 && res.data) {
- const { productionOrder, workOrderList } = res.data;
- rowData.productionOrderDto = productionOrder || item;
- rowData.productionRecords = workOrderList || [];
- } else {
- rowData.productionOrderDto = item;
- rowData.productionRecords = [];
- }
- } catch (error) {
- console.error(error);
- rowData.productionOrderDto = item;
- rowData.productionRecords = [];
- uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "none" });
- } finally {
- uni.hideLoading();
- }
+ const onBatchInput = () => {
+ showOptions.value = true;
+ if (searchTimer) clearTimeout(searchTimer);
+ searchTimer = setTimeout(() => {
+ loadBatchOptions();
+ }, 300);
};
- onLoad(async options => {
- if (options.npsNo) {
- uni.showLoading({ title: "鍔犺浇涓�..." });
- try {
- const res = await productOrderListPage({
- npsNo: options.npsNo,
- pageNum: 1,
- pageSize: 10,
+ const onBatchFocus = () => {
+ showOptions.value = true;
+ loadBatchOptions();
+ };
+
+ const onBatchClear = () => {
+ batchNo.value = "";
+ batchOptions.value = [];
+ showOptions.value = false;
+ };
+
+ const onSelectOption = opt => {
+ batchNo.value = opt.batchNo || "";
+ showOptions.value = false;
+ handleQuery();
+ };
+
+ // 鍒囨崲鏂瑰悜
+ const switchDirection = dir => {
+ if (direction.value === dir) return;
+ direction.value = dir;
+ batchNo.value = "";
+ batchOptions.value = [];
+ showOptions.value = false;
+ queried.value = false;
+ forwardData.chains = [];
+ forwardData.outbounds = [];
+ reverseData.material = null;
+ reverseData.orders = [];
+ };
+
+ // 鏌ヨ杩芥函鏁版嵁
+ const handleQuery = () => {
+ const no = (batchNo.value || "").trim();
+ if (!no) {
+ uni.showToast({ title: "璇疯緭鍏ユ壒鍙�", icon: "none" });
+ return;
+ }
+ showOptions.value = false;
+ loading.value = true;
+ if (direction.value === "forward") {
+ traceForward({ batchNo: no })
+ .then(res => {
+ const data = res.data || {};
+ forwardData.batchNo = data.batchNo || no;
+ forwardData.chains = data.chains || [];
+ forwardData.outbounds = data.outbounds || [];
+ queried.value = true;
+ })
+ .catch(() => {
+ // 閿欒鏂囨鐢� request 缁熶竴鎻愮ず
+ })
+ .finally(() => {
+ loading.value = false;
});
- const records = res.data?.records || res.rows || [];
- if (records.length > 0) {
- onSelectNpsNo(records[0]);
- } else {
- uni.showToast({ title: "鏈壘鍒扮浉鍏宠鍗�", icon: "none" });
- }
- } catch (error) {
- console.error(error);
- } finally {
- uni.hideLoading();
+ } else {
+ traceReverse({ batchNo: no })
+ .then(res => {
+ const data = res.data || {};
+ reverseData.batchNo = data.batchNo || no;
+ reverseData.material = data.material || null;
+ reverseData.orders = data.orders || [];
+ queried.value = true;
+ })
+ .catch(() => {
+ // 閿欒鏂囨鐢� request 缁熶竴鎻愮ず
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ }
+ };
+
+ // 璺宠浆鍙嶅悜杩芥函骞跺洖濉師鏂欐壒鍙�
+ const jumpToReverse = no => {
+ if (!no) return;
+ direction.value = "reverse";
+ batchNo.value = no;
+ batchOptions.value = [];
+ showOptions.value = false;
+ queried.value = false;
+ forwardData.chains = [];
+ forwardData.outbounds = [];
+ reverseData.material = null;
+ reverseData.orders = [];
+ uni.pageScrollTo({ scrollTop: 0, duration: 0 });
+ handleQuery();
+ };
+
+ // 璺宠浆姝e悜杩芥函骞跺洖濉垚鍝佹壒鍙�
+ const jumpToForward = no => {
+ if (!no) return;
+ direction.value = "forward";
+ batchNo.value = no;
+ batchOptions.value = [];
+ showOptions.value = false;
+ queried.value = false;
+ forwardData.chains = [];
+ forwardData.outbounds = [];
+ reverseData.material = null;
+ reverseData.orders = [];
+ uni.pageScrollTo({ scrollTop: 0, duration: 0 });
+ handleQuery();
+ };
+
+ onLoad(options => {
+ // 鏀寔浠庡叾瀹冮〉闈㈠甫鎵瑰彿杩涘叆
+ if (options && options.batchNo) {
+ try {
+ batchNo.value = decodeURIComponent(options.batchNo);
+ } catch (e) {
+ batchNo.value = options.batchNo;
}
+ direction.value = options.type === "material" ? "reverse" : "forward";
+ handleQuery();
}
});
-
- const getStatusText = status => {
- const statusMap = { 1: "寰呭紑濮�", 2: "杩涜涓�", 3: "宸插畬鎴�", 5: "宸茬粨鏉�" };
- return statusMap[status] || "宸插彇娑�";
- };
-
- const getStatusType = status => {
- const typeMap = { 1: "primary", 2: "warning", 3: "success", 5: "error" };
- return typeMap[status] || "info";
- };
-
- const formatDate = (date, pattern = "{y}-{m}-{d}") => {
- return parseTime(date, pattern) || "-";
- };
-
- const formatProgress = val => {
- const p = parseFloat(val || 0);
- return p >= 100 ? 100 : p;
- };
-
- const progressColor = percentage => {
- if (percentage < 30) return "#f56c6c";
- if (percentage < 70) return "#e6a23c";
- return "#67c23a";
- };
-
- const handleShowReports = row => {
- detailData.value = {
- workOrder: row.workOrder || {},
- reports: (row.reportList || []).map(r => ({
- ...r.reportMain,
- ...(r.reportOutputList ? r.reportOutputList[0] : {}),
- id: r.reportMain.id,
- productionOperationParamList: r.reportParamList || [],
- })),
- };
- reportPopupVisible.value = true;
- };
-
- const handleShowInput = async reportId => {
- inputPopupVisible.value = true;
- inputLoading.value = true;
- inputListData.value = [];
- try {
- const res = await productionProductInputListPage({
- productMainId: reportId,
- pageNum: 1,
- pageSize: 100,
- });
- inputListData.value = res.data?.records || res.rows || [];
- } catch (error) {
- console.error(error);
- uni.showToast({ title: "鑾峰彇鎶曞叆淇℃伅澶辫触", icon: "none" });
- } finally {
- inputLoading.value = false;
- }
- };
-
- const handleShowQuality = row => {
- const inspects = row.inspectList || [];
- qualityRecords.value = inspects.map(i => ({
- ...i.inspect,
- reportNo: i.reportNo,
- productName: row.workOrder?.productName || "-",
- model: row.workOrder?.model || "-",
- userName: i.reportMain?.userName || "-",
- inspectParamList: i.inspectParamList || [],
- }));
- qualityPopupVisible.value = true;
- };
-
- const showParams = params => {
- currentParams.value = params || [];
- paramModalVisible.value = true;
- };
</script>
<style scoped lang="scss">
@@ -513,520 +890,285 @@
.production-traceability {
min-height: 100vh;
- background-color: #f5f7fa;
+ background: #f8f9fa;
+ padding-bottom: 40rpx;
+ }
+
+ // 鏂瑰悜椤电
+ .trace-tabs {
+ display: flex;
+ background: #ffffff;
+
+ .tab-item {
+ flex: 1;
+ text-align: center;
+ padding: 24rpx 0;
+ font-size: 28rpx;
+ color: #666;
+ position: relative;
+
+ &.active {
+ color: #2979ff;
+ font-weight: 600;
+
+ &::after {
+ content: "";
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ transform: translateX(-50%);
+ width: 80rpx;
+ height: 6rpx;
+ background: #2979ff;
+ border-radius: 4rpx;
+ }
+ }
+ }
}
.search-section {
- background-color: #fff;
- padding: 20rpx 24rpx;
- margin-bottom: 20rpx;
- }
-
- .search-bar {
- display: flex;
- align-items: center;
- background-color: #f2f2f2;
- border-radius: 8rpx;
- padding: 0 20rpx;
- height: 80rpx;
+ position: relative;
+ border-bottom: 1rpx solid #f0f0f0;
.search-input {
- flex: 1;
- display: flex;
- align-items: center;
-
- .placeholder {
- font-size: 28rpx;
- color: #999;
- }
-
- .value {
- font-size: 28rpx;
- color: #333;
- font-weight: 500;
- }
+ height: 76rpx;
}
- .search-button {
- padding: 0 10rpx;
- }
- }
-
- .selector-popup {
- background: #fff;
- padding: 30rpx;
- max-height: 70vh;
- display: flex;
- flex-direction: column;
-
- .popup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
-
- .popup-title {
- font-size: 32rpx;
- font-weight: bold;
- }
+ .search-text {
+ font-size: 28rpx;
}
- .search-box {
- margin-bottom: 20rpx;
- }
-
- .options-list {
- flex: 1;
- overflow: hidden;
- }
-
- .option-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 24rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
-
- .option-main {
- flex: 1;
- .nps-no {
- font-size: 28rpx;
- font-weight: bold;
- color: #333;
- display: block;
- margin-bottom: 4rpx;
- }
- .product-info {
- font-size: 24rpx;
- color: #999;
- }
- }
- }
-
- .no-options {
- text-align: center;
- padding: 40rpx;
+ .search-tip {
+ font-size: 22rpx;
color: #999;
- font-size: 26rpx;
+ margin-top: 12rpx;
}
- }
- .content-container {
- padding: 0 24rpx 40rpx;
- }
-
- .info-card {
- background: #fff;
- border-radius: 16rpx;
- padding: 24rpx;
- margin-bottom: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
-
- .card-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- margin-bottom: 24rpx;
- padding-left: 16rpx;
- border-left: 8rpx solid #3c9cff;
- }
- }
-
- .info-grid {
- display: flex;
- flex-wrap: wrap;
-
- .info-item {
- width: 50%;
- margin-bottom: 20rpx;
+ .btn-row {
display: flex;
- flex-direction: column;
-
- &.full-width {
- width: 100%;
- }
-
- .label {
- font-size: 24rpx;
- color: #999;
- margin-bottom: 8rpx;
- }
-
- .value {
- font-size: 28rpx;
- color: #333;
- word-break: break-all;
- }
- }
- }
-
- .progress-container {
- display: flex;
- align-items: center;
- gap: 20rpx;
-
- up-line-progress {
- flex: 1;
- }
-
- .progress-text {
- font-size: 24rpx;
- color: #666;
- min-width: 60rpx;
- }
- }
-
- .section-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- margin: 32rpx 0 20rpx;
- }
-
- .work-order-card {
- background: #fff;
- border-radius: 16rpx;
- padding: 24rpx;
- margin-bottom: 20rpx;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
-
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20rpx;
- padding-bottom: 16rpx;
- border-bottom: 1rpx solid #f0f0f0;
-
- .work-order-no {
- font-size: 28rpx;
- font-weight: bold;
- color: #3c9cff;
- }
-
- .progress-tag {
- font-size: 28rpx;
- font-weight: bold;
- }
- }
-
- .card-content {
- .content-row {
- margin-bottom: 12rpx;
- font-size: 26rpx;
-
- .label {
- color: #999;
- }
-
- .value {
- color: #333;
- }
- }
- }
-
- .card-footer {
- display: flex;
- justify-content: flex-end;
gap: 20rpx;
margin-top: 20rpx;
}
}
- .base-info {
+ // 鎵瑰彿鍊欓�変笅鎷�
+ .option-panel {
+ margin-top: 12rpx;
background: #fff;
- padding: 24rpx;
- border-radius: 16rpx;
- margin-bottom: 30rpx;
+ border: 1rpx solid #ebeef5;
+ border-radius: 12rpx;
+ max-height: 520rpx;
+ overflow-y: auto;
- .info-row {
- margin-bottom: 16rpx;
- font-size: 28rpx;
- display: flex;
- align-items: center;
+ .option-item {
+ padding: 18rpx 20rpx;
+ border-bottom: 1rpx solid #f5f5f5;
&:last-child {
- margin-bottom: 0;
+ border-bottom: none;
}
- .label {
- color: #999;
- min-width: 180rpx;
+ &:active {
+ background: #f5f7fa;
}
- .value {
- color: #333;
- flex: 1;
- font-weight: 500;
- &.progress-box {
+ .option-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .option-no {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 600;
flex: 1;
+ margin-right: 16rpx;
+ }
+
+ .option-qty {
+ font-size: 24rpx;
+ color: #2979ff;
+ }
+ }
+
+ .option-bottom {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 8rpx;
+
+ .option-name {
+ font-size: 24rpx;
+ color: #666;
+ flex: 1;
+ margin-right: 16rpx;
+ }
+
+ .option-time {
+ font-size: 22rpx;
+ color: #999;
}
}
}
+
+ .option-empty {
+ padding: 30rpx 20rpx;
+ text-align: center;
+ font-size: 24rpx;
+ color: #999;
+ }
}
- .info-grid {
+ .loading-box {
+ display: flex;
+ justify-content: center;
+ padding-top: 120rpx;
+ }
+
+ .result-container {
+ padding: 20rpx;
+ }
+
+ .result-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #333;
+ margin: 10rpx 0 16rpx;
+
+ &::before {
+ content: "";
+ display: inline-block;
+ width: 6rpx;
+ height: 26rpx;
+ background: #2979ff;
+ border-radius: 4rpx;
+ margin-right: 12rpx;
+ vertical-align: middle;
+ }
+ }
+
+ .type-tag {
+ font-size: 22rpx;
+ color: #2979ff;
+ background: #ecf5ff;
+ border-radius: 6rpx;
+ padding: 4rpx 12rpx;
+ }
+
+ .document-icon {
+ &.out {
+ background: #ff9900;
+ }
+ &.material {
+ background: #52c41a;
+ }
+ }
+
+ .font-bold {
+ font-weight: 500;
+ }
+
+ .detail-value.link,
+ .chip.link-chip {
+ color: #2979ff;
+ text-decoration: underline;
+ }
+
+ // 鎵规鏍囩
+ .chip-wrap {
display: flex;
flex-wrap: wrap;
- padding: 10rpx 0;
+ gap: 12rpx;
+ padding-bottom: 12rpx;
- .info-item {
- width: 50%;
- margin-bottom: 20rpx;
+ .chip {
+ font-size: 22rpx;
+ color: #333;
+ background: #f5f7fa;
+ border-radius: 8rpx;
+ padding: 8rpx 16rpx;
+ }
+ }
+
+ // 鎶樺彔闈㈡澘
+ .collapse-block {
+ border-top: 1rpx solid #f0f0f0;
+
+ .collapse-head {
display: flex;
- flex-direction: column;
+ align-items: center;
+ padding: 24rpx 0;
- &.full-width {
- width: 100%;
- }
-
- .label {
- font-size: 24rpx;
- color: #999;
- margin-bottom: 4rpx;
- }
- .value {
+ .collapse-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
- }
- }
- .popup-content {
- background: #fff;
- padding: 30rpx;
- max-height: 80vh;
- display: flex;
- flex-direction: column;
- border-radius: 20rpx 20rpx 0 0;
-
- .popup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 30rpx;
-
- .popup-title {
- font-size: 34rpx;
- font-weight: bold;
- color: #333;
+ .collapse-count {
+ font-size: 22rpx;
+ color: #2979ff;
+ background: #ecf5ff;
+ border-radius: 20rpx;
+ padding: 2rpx 14rpx;
+ margin: 0 12rpx 0 auto;
}
}
- .popup-scroll {
- flex: 1;
- overflow: hidden;
+ .collapse-body {
+ padding-bottom: 16rpx;
}
}
- .detail-info {
+ .sub-item {
background: #f8f9fa;
- padding: 24rpx;
- border-radius: 16rpx;
- margin-bottom: 30rpx;
- flex-direction: column;
+ border-radius: 10rpx;
+ padding: 16rpx;
+ margin-bottom: 12rpx;
- .info-row {
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .inner-item {
+ padding: 12rpx;
+ margin-top: 10rpx;
+ background: #fff5f5;
+ border-radius: 8rpx;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+
+ .unqualified-box {
+ margin-top: 16rpx;
+ padding-top: 16rpx;
+ border-top: 1rpx dashed #e4e7ed;
+
+ .unqualified-title {
+ font-size: 24rpx;
+ color: #ee0a24;
+ font-weight: 500;
margin-bottom: 12rpx;
- font-size: 28rpx;
- display: flex;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .label {
- color: #999;
- min-width: 140rpx;
- }
- .value {
- color: #333;
- flex: 1;
- font-weight: 500;
- }
}
}
- .list-title {
- font-size: 30rpx;
- font-weight: bold;
- margin-bottom: 20rpx;
- color: #333;
- display: flex;
- align-items: center;
-
- &::before {
- content: "";
- width: 6rpx;
- height: 28rpx;
- background: #3c9cff;
- margin-right: 12rpx;
- border-radius: 4rpx;
- }
- }
-
- .detail-item {
- background: #fff;
- border: 1rpx solid #f0f0f0;
- border-radius: 12rpx;
- padding: 24rpx;
- margin-bottom: 20rpx;
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .item-main {
- flex: 1;
- .item-row {
- font-size: 26rpx;
- margin-bottom: 8rpx;
- display: flex;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .label {
- color: #999;
- min-width: 130rpx;
- }
- .value {
- color: #333;
- flex: 1;
- }
- }
- }
-
- .item-actions {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- padding-left: 20rpx;
- border-left: 1rpx solid #f0f0f0;
-
- .action-link {
- font-size: 26rpx;
- color: #3c9cff;
- white-space: nowrap;
-
- &.green {
- color: #52c41a;
- }
- }
- }
- }
-
- .quality-record {
- background: #fff;
- border: 1rpx solid #f0f0f0;
- border-radius: 16rpx;
- padding: 24rpx;
- margin-bottom: 30rpx;
-
- .record-title {
- font-size: 30rpx;
- font-weight: bold;
- color: #3c9cff;
- margin-bottom: 24rpx;
- display: flex;
- justify-content: space-between;
- }
- }
-
- .params-table {
- margin-top: 24rpx;
- border: 1rpx solid #f0f0f0;
- border-radius: 12rpx;
- overflow: hidden;
-
- .table-header {
- display: flex;
- background: #f8f9fa;
- padding: 20rpx 16rpx;
- font-size: 26rpx;
- font-weight: bold;
- color: #666;
- }
-
- .table-row {
- display: flex;
- padding: 20rpx 16rpx;
- font-size: 26rpx;
- border-top: 1rpx solid #f0f0f0;
- color: #333;
-
- &:nth-child(even) {
- background: #fafafa;
- }
- }
-
- .col {
- flex: 1;
- text-align: center;
- word-break: break-all;
- }
- }
-
- .modal-content {
- padding: 30rpx;
- .param-row {
- margin-bottom: 20rpx;
- font-size: 28rpx;
- display: flex;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .label {
- color: #666;
- min-width: 160rpx;
- }
- .value {
- color: #333;
- font-weight: 500;
- flex: 1;
- }
- }
- }
-
- .input-list-popup {
- .input-item {
- background: #fff;
- border: 1rpx solid #f0f0f0;
- border-radius: 12rpx;
- padding: 20rpx;
- margin-bottom: 20rpx;
-
- .input-row {
- display: flex;
- font-size: 26rpx;
- margin-bottom: 8rpx;
- &:last-child {
- margin-bottom: 0;
- }
- .label {
- color: #999;
- min-width: 160rpx;
- }
- .value {
- color: #333;
- flex: 1;
- }
- }
- }
- }
-
- .error-text {
- color: #f56c6c;
- font-weight: bold;
- }
-
- .no-data-minor {
- text-align: center;
- padding: 60rpx 40rpx;
+ .sub-empty {
+ font-size: 24rpx;
color: #999;
- font-size: 28rpx;
+ text-align: center;
+ padding: 24rpx 0;
+
+ &.card {
+ background: #fff;
+ border-radius: 12rpx;
+ margin-bottom: 16rpx;
+ }
+ }
+
+ .no-data {
+ padding-top: 100rpx;
}
</style>
diff --git a/src/pages/qualityManagement/finalInspection/add.vue b/src/pages/qualityManagement/finalInspection/add.vue
index 94321dd..5bd64db 100644
--- a/src/pages/qualityManagement/finalInspection/add.vue
+++ b/src/pages/qualityManagement/finalInspection/add.vue
@@ -29,10 +29,10 @@
<up-input v-model="form.productName"
placeholder="璇烽�夋嫨浜у搧"
readonly
- @click="showProductTree = true"
- :disabled="isEdit" />
+ @click="!isReadonly && (showProductTree = true)"
+ :disabled="isEdit || isReadonly" />
<template #right>
- <up-icon @click="showProductTree = true"
+ <up-icon @click="!isReadonly && (showProductTree = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -43,9 +43,9 @@
<up-input v-model="form.model"
placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
readonly
- :disabled="isEdit" />
+ :disabled="isEdit || isReadonly" />
<template #right>
- <up-icon @click="showModelSheet = true"
+ <up-icon @click="!isReadonly && (showModelSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -55,7 +55,8 @@
border-bottom>
<up-input v-model="testStandardDisplay"
placeholder="璇烽�夋嫨鎸囨爣"
- readonly />
+ readonly
+ :disabled="isReadonly" />
<template #right>
<up-icon @click="openTestStandardSheet"
name="arrow-right" />
@@ -75,13 +76,14 @@
<up-input v-model="form.quantity"
type="number"
placeholder="璇疯緭鍏ユ暟閲�"
- :disabled="processQuantityDisabled" />
+ :disabled="processQuantityDisabled || isReadonly" />
</up-form-item>
<up-form-item label="妫�娴嬪崟浣�"
prop="checkCompany"
border-bottom>
<up-input v-model="form.checkCompany"
placeholder="璇疯緭鍏ユ娴嬪崟浣�"
+ :disabled="isReadonly"
clearable />
</up-form-item>
<up-form-item label="妫�娴嬬粨鏋�"
@@ -91,9 +93,10 @@
<up-input v-model="form.checkResult"
placeholder="璇烽�夋嫨妫�娴嬬粨鏋�"
readonly
+ :disabled="isReadonly"
@click="showResultSheet" />
<template #right>
- <up-icon @click="showResultSheet = true"
+ <up-icon @click="!isReadonly && (showResultSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -103,9 +106,10 @@
<up-input v-model="form.checkName"
placeholder="璇烽�夋嫨妫�楠屽憳"
readonly
+ :disabled="isReadonly"
@click="showInspectorSheet" />
<template #right>
- <up-icon @click="showInspectorSheet = true"
+ <up-icon @click="!isReadonly && (showInspectorSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -115,7 +119,8 @@
border-bottom>
<up-input v-model="form.checkTime"
placeholder="璇烽�夋嫨妫�娴嬫棩鏈�"
- readonly />
+ readonly
+ :disabled="isReadonly" />
<!-- <template #right>
<up-icon name="calendar"
@click="showDatePicker"></up-icon>
@@ -155,6 +160,7 @@
<text class="step-label">妫�楠屽�硷細</text>
<up-input v-model="item.testValue"
placeholder="璇疯緭鍏ユ楠屽��"
+ :disabled="isReadonly"
clearable
border-bottom
class="step-input" />
@@ -174,14 +180,16 @@
size="default"
@click="goBack"
class="bottom-btn">
- 鍙栨秷
+ {{ isReadonly ? '杩斿洖' : '鍙栨秷' }}
</up-button>
- <up-button type="primary"
+ <!-- 宸叉彁浜ゅ崟鎹彧璇伙紝闅愯棌淇濆瓨/鎻愪氦 -->
+ <up-button v-if="!isReadonly"
+ type="primary"
size="default"
@click="submitForm"
:loading="loading"
class="bottom-btn">
- {{ isEdit ? '淇濆瓨' : '鎻愪氦' }}
+ 淇濆瓨
</up-button>
</view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
@@ -465,6 +473,9 @@
return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
+ // 宸叉彁浜わ紙inspectState=1锛夊崟鎹暣浣撳彧璇�
+ const isReadonly = computed(() => Number(form.value?.inspectState) === 1);
+
// 杩斿洖涓婁竴椤�
const goBack = () => {
uni.navigateBack();
@@ -472,11 +483,13 @@
// 鏄剧ず鏃ユ湡閫夋嫨鍣�
const showDatePicker = () => {
+ if (isReadonly.value) return;
showDate.value = true;
};
// 纭鏃ユ湡閫夋嫨
const confirmDate = e => {
+ if (isReadonly.value) return;
form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
};
@@ -597,7 +610,7 @@
});
};
const openTestStandardSheet = () => {
- console.log("openTestStandardSheet");
+ if (isReadonly.value) return;
showTestStandardSheet.value = true;
};
@@ -674,6 +687,11 @@
// 鎻愪氦琛ㄥ崟
const submitForm = async () => {
console.log("submitForm", form.value, tableData.value);
+ // 宸叉彁浜ゅ崟鎹姝繚瀛�/鎻愪氦
+ if (isReadonly.value) {
+ showToast("妫�楠屽崟宸叉彁浜わ紝涓嶅厑璁镐慨鏀�");
+ return;
+ }
try {
// await formRef.value.validate();
if (!form.value.productModelId) {
@@ -713,21 +731,22 @@
const data = { ...form.value, qualityInspectParams: tableData.value };
data.quantity = Number(data.quantity);
if (isEdit.value) {
- const res = await qualityInspectUpdate(data);
+ await qualityInspectUpdate(data);
showToast("淇濆瓨鎴愬姛");
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
- const res = await qualityInspectAdd(data);
- showToast("鎻愪氦鎴愬姛");
+ // 鏂板浠呬繚瀛樿崏绋匡紝姝e紡鎻愪氦鍦ㄥ垪琛ㄩ〉銆屾彁浜ゃ�嶆寜閽紙浼氱敓鎴愬叆搴撹褰曪級
+ await qualityInspectAdd(data);
+ showToast("淇濆瓨鎴愬姛");
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} catch (error) {
- console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
- showToast("鎻愪氦澶辫触锛岃閲嶈瘯");
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鎻愪氦澶辫触:", error);
} finally {
loading.value = false;
}
diff --git a/src/pages/qualityManagement/finalInspection/detail.vue b/src/pages/qualityManagement/finalInspection/detail.vue
index d274a28..574147e 100644
--- a/src/pages/qualityManagement/finalInspection/detail.vue
+++ b/src/pages/qualityManagement/finalInspection/detail.vue
@@ -24,7 +24,7 @@
<u-tag :type="getStateTagType(detailData.inspectState)"
size="small"
class="status-tag">
- {{ detailData.inspectState ? '宸叉彁浜�' : '鏈彁浜�' }}
+ {{ detailData.inspectState ? '宸叉彁浜�' : '寰呮彁浜�' }}
</u-tag>
</view>
</view>
@@ -66,6 +66,18 @@
<text class="detail-label">妫�娴嬪崟浣�</text>
<text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
</view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ detailData.qualifiedQuantity != null ? detailData.qualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value">{{ detailData.unqualifiedQuantity != null ? detailData.unqualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ detailData.checkResult || '-' }}</text>
+ </view>
<!-- <view class="detail-row">
<text class="detail-label">妫�楠屾爣鍑�</text>
<text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
diff --git a/src/pages/qualityManagement/finalInspection/index.vue b/src/pages/qualityManagement/finalInspection/index.vue
index 0e860ff..a8692d1 100644
--- a/src/pages/qualityManagement/finalInspection/index.vue
+++ b/src/pages/qualityManagement/finalInspection/index.vue
@@ -26,6 +26,17 @@
color="#999"></up-icon>
</view>
</view>
+ <!-- 妫�楠岀姸鎬佺瓫閫� -->
+ <view class="filter-row">
+ <view class="filter-item"
+ @click="showStateSheet = true">
+ <text class="filter-label">妫�楠岀姸鎬�</text>
+ <text class="filter-value">{{ stateFilterLabel }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ </view>
<!-- 鏃ユ湡閫夋嫨 -->
<!-- <view class="date-range"
v-if="searchForm.entryDate">
@@ -85,7 +96,7 @@
<u-tag :type="getStateTagType(item.inspectState)"
size="mini"
class="status-tag">
- {{ item.inspectState ? '宸叉彁浜�' : '鏈彁浜�' }}
+ {{ item.inspectState ? '宸叉彁浜�' : '寰呮彁浜�' }}
</u-tag>
</view>
</view>
@@ -114,6 +125,18 @@
<view class="detail-row">
<text class="detail-label">妫�娴嬪崟浣�</text>
<text class="detail-value">{{ item.checkCompany || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ item.qualifiedQuantity != null ? item.qualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value">{{ item.unqualifiedQuantity != null ? item.unqualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ item.checkResult || '-' }}</text>
</view>
</view>
<!-- 鎿嶄綔鎸夐挳 -->
@@ -152,6 +175,15 @@
:disabled="item.inspectState || item.checkName !== ''"
@click.stop="assignInspector(item)">
鍒嗛厤妫�楠屽憳
+ </u-button>
+ </view>
+ <view class="action-buttons">
+ <u-button type="error"
+ size="small"
+ class="action-btn"
+ :disabled="!(Number(item.unqualifiedQuantity) > 0)"
+ @click.stop="openUnqualified(item)">
+ 涓嶅悎鏍煎鐞�
</u-button>
</view>
</view>
@@ -223,6 +255,56 @@
:actions="userSheetOptions"
@select="selectInspector"
title="閫夋嫨妫�楠屽憳" />
+ <!-- 妫�楠岀姸鎬佺瓫閫� -->
+ <up-action-sheet :show="showStateSheet"
+ :actions="stateSheetOptions"
+ @select="selectState"
+ @close="showStateSheet = false"
+ title="閫夋嫨妫�楠岀姸鎬�" />
+ <!-- 涓嶅悎鏍煎鐞嗗脊绐� -->
+ <up-popup v-model:show="showUnqualifiedPopup"
+ mode="bottom"
+ round>
+ <view class="unqualified-dialog">
+ <view class="dialog-header">
+ <text class="dialog-title">涓嶅悎鏍煎鐞�</text>
+ <up-icon name="close"
+ size="20"
+ color="#999"
+ @click="showUnqualifiedPopup = false"></up-icon>
+ </view>
+ <view class="dialog-content">
+ <view v-if="unqualifiedLoading"
+ class="unqualified-empty">鍔犺浇涓�...</view>
+ <view v-else-if="unqualifiedList.length === 0"
+ class="unqualified-empty">鏆傛棤寰呭鐞嗕笉鍚堟牸璁板綍</view>
+ <view v-for="(row, idx) in unqualifiedList"
+ :key="idx"
+ class="unqualified-item">
+ <view class="unqualified-info">
+ <text class="unqualified-label">涓嶅悎鏍肩幇璞�</text>
+ <text class="unqualified-value">{{ row.defectivePhenomena || '-' }}</text>
+ </view>
+ <view class="unqualified-info">
+ <text class="unqualified-label">鏁伴噺</text>
+ <text class="unqualified-value">{{ row.quantity != null ? row.quantity : '-' }}</text>
+ </view>
+ <u-button type="primary"
+ size="small"
+ class="unqualified-btn"
+ @click="openDealSheet(row)">
+ 澶勭悊
+ </u-button>
+ </view>
+ </view>
+ </view>
+ </up-popup>
+ <!-- 澶勭悊缁撴灉閫夋嫨 -->
+ <up-action-sheet :show="showDealSheet"
+ :actions="dealSheetOptions"
+ @select="selectDeal"
+ @close="showDealSheet = false"
+ title="閫夋嫨澶勭悊缁撴灉" />
</view>
</template>
@@ -235,8 +317,13 @@
submitQualityInspect,
qualityInspectUpdate,
qualityInspectListPage,
+ qualityUnqualifiedListPage,
+ qualityUnqualifiedDeal,
} from "@/api/qualityManagement/materialInspection.js";
import { userListNoPage } from "@/api/system/user.js";
+
+ // 鍑哄巶/鎴愬搧妫�楠� inspectType=2
+ const currentInspectType = 2;
// 鏄剧ず鎻愮ず淇℃伅
const showToast = message => {
@@ -294,6 +381,121 @@
value: item.nickName,
}));
});
+
+ // 妫�楠岀姸鎬佺瓫閫�
+ const showStateSheet = ref(false);
+ const stateFilter = ref("");
+ const stateSheetOptions = [
+ { name: "鍏ㄩ儴", value: "" },
+ { name: "寰呮彁浜�", value: 0 },
+ { name: "宸叉彁浜�", value: 1 },
+ ];
+ const stateFilterLabel = computed(() => {
+ const hit = stateSheetOptions.find(o => o.value === stateFilter.value);
+ return hit ? hit.name : "鍏ㄩ儴";
+ });
+ const selectState = e => {
+ stateFilter.value = e.value;
+ showStateSheet.value = false;
+ getList();
+ };
+
+ // 涓嶅悎鏍煎鐞嗙浉鍏�
+ const showUnqualifiedPopup = ref(false);
+ const unqualifiedLoading = ref(false);
+ const unqualifiedList = ref([]);
+ const currentInspectRow = ref(null);
+ const showDealSheet = ref(false);
+ const currentUnqualifiedRow = ref(null);
+ // 杩囩▼/鎴愬搧绫绘敮鎸� 杩斾慨/杩斿伐/鎶ュ簾/璁╂鏀捐
+ const dealSheetOptions = computed(() => {
+ return ["杩斾慨", "杩斿伐", "鎶ュ簾", "璁╂鏀捐"].map(name => ({
+ name,
+ value: name,
+ }));
+ });
+
+ // 鎵撳紑涓嶅悎鏍煎鐞嗗脊绐楋細鎸夋楠屽崟id鎷夊彇寰呭鐞嗕笉鍚堟牸璁板綍
+ const openUnqualified = async item => {
+ currentInspectRow.value = item;
+ showUnqualifiedPopup.value = true;
+ unqualifiedLoading.value = true;
+ unqualifiedList.value = [];
+ try {
+ const res = await qualityUnqualifiedListPage({
+ inspectType: currentInspectType,
+ inspectState: 0,
+ inspectId: item.id,
+ current: -1,
+ size: -1,
+ });
+ let records = res.data?.records || res.data || [];
+ // 鑻ュ悗绔湭鎸夋楠屽崟杩囨护锛屽墠绔厹搴曟寜妫�楠屽崟id杩囨护
+ const hasInspectRef = records.some(
+ r => r.inspectId != null || r.qualityInspectId != null
+ );
+ if (hasInspectRef) {
+ records = records.filter(
+ r => (r.inspectId ?? r.qualityInspectId) == item.id
+ );
+ }
+ // 鍙繚鐣欏緟澶勭悊璁板綍
+ unqualifiedList.value = records.filter(
+ r => Number(r.inspectState) !== 1
+ );
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鑾峰彇涓嶅悎鏍艰褰曞け璐�:", error);
+ unqualifiedList.value = [];
+ } finally {
+ unqualifiedLoading.value = false;
+ }
+ };
+
+ // 鎵撳紑澶勭悊缁撴灉閫夋嫨
+ const openDealSheet = row => {
+ currentUnqualifiedRow.value = row;
+ showDealSheet.value = true;
+ };
+
+ // 閫夋嫨澶勭悊缁撴灉
+ const selectDeal = e => {
+ showDealSheet.value = false;
+ if (!currentUnqualifiedRow.value) return;
+ const result = e.value;
+ // 杩斾慨/杩斿伐浼氳嚜鍔ㄥ垱寤鸿繑宸ョ敓浜ц鍗曪紝鍏堜簩娆$‘璁�
+ if (result === "杩斾慨" || result === "杩斿伐") {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "灏嗚嚜鍔ㄥ垱寤鸿繑宸ョ敓浜ц鍗曪紙FG鍗曞彿锛夛紝鏄惁缁х画锛�",
+ success: res => {
+ if (res.confirm) {
+ doDeal(currentUnqualifiedRow.value, result);
+ }
+ },
+ });
+ return;
+ }
+ doDeal(currentUnqualifiedRow.value, result);
+ };
+
+ // 鎻愪氦涓嶅悎鏍煎鐞�
+ const doDeal = async (row, dealResult) => {
+ try {
+ await qualityUnqualifiedDeal({ id: row.id, dealResult });
+ showToast("澶勭悊鎴愬姛");
+ if (currentInspectRow.value) {
+ await openUnqualified(currentInspectRow.value);
+ }
+ if (unqualifiedList.value.length === 0) {
+ showUnqualifiedPopup.value = false;
+ }
+ getList();
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("涓嶅悎鏍煎鐞嗗け璐�:", error);
+ }
+ };
// 杩斿洖涓婁竴椤�
const goBack = () => {
@@ -371,7 +573,9 @@
tableLoading.value = true;
const params = { ...searchForm.value, ...page.value };
params.entryDate = undefined;
- qualityInspectListPage({ ...params, inspectType: 2 })
+ params.inspectState =
+ stateFilter.value === "" ? undefined : stateFilter.value;
+ qualityInspectListPage({ ...params, inspectType: currentInspectType })
.then(res => {
tableLoading.value = false;
inspectionList.value = res.data.records || [];
@@ -430,25 +634,30 @@
};
// 鎻愪氦妫�楠�
- const submitInspection = async item => {
+ const submitInspection = item => {
if (!item) {
showToast("鍙傛暟閿欒");
return;
}
- try {
- const res = await submitQualityInspect({ id: item.id });
- if (res.code === 200) {
- showToast("鎻愪氦鎴愬姛");
- setTimeout(() => {
- getList();
- }, 1000);
- } else {
- showToast("鎻愪氦澶辫触锛�" + (res.msg || "鏈煡閿欒"));
- }
- } catch (error) {
- console.error("鎻愪氦澶辫触:", error);
- showToast("鎻愪氦澶辫触锛岃閲嶈瘯");
- }
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝鏄惁缁х画锛�",
+ success: async res => {
+ if (!res.confirm) return;
+ try {
+ await submitQualityInspect({ id: item.id });
+ showToast(
+ "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+ );
+ setTimeout(() => {
+ getList();
+ }, 1500);
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鎻愪氦澶辫触:", error);
+ }
+ },
+ });
};
// 鍒嗛厤妫�楠屽憳
@@ -551,6 +760,78 @@
padding: 5px;
}
+ // 妫�楠岀姸鎬佺瓫閫�
+ .filter-row {
+ display: flex;
+ align-items: center;
+ margin-top: 10px;
+ }
+
+ .filter-item {
+ display: flex;
+ align-items: center;
+ padding: 4px 10px;
+ background: #f2f3f5;
+ border-radius: 12px;
+ }
+
+ .filter-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 4px;
+ }
+
+ .filter-value {
+ font-size: 12px;
+ color: #2979ff;
+ margin-right: 4px;
+ }
+
+ // 涓嶅悎鏍煎鐞嗗脊绐�
+ .unqualified-dialog {
+ background: #ffffff;
+ border-radius: 16px 16px 0 0;
+ padding: 20px;
+ max-height: 70vh;
+ overflow-y: auto;
+ }
+
+ .unqualified-empty {
+ padding: 30px 0;
+ text-align: center;
+ font-size: 13px;
+ color: #999;
+ }
+
+ .unqualified-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .unqualified-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-right: 10px;
+ }
+
+ .unqualified-label {
+ font-size: 12px;
+ color: #999;
+ }
+
+ .unqualified-value {
+ font-size: 13px;
+ color: #333;
+ margin-top: 2px;
+ }
+
+ .unqualified-btn {
+ width: 70px;
+ }
+
// 缁熻鍗$墖
.stats-cards {
display: flex;
diff --git a/src/pages/qualityManagement/materialInspection/add.vue b/src/pages/qualityManagement/materialInspection/add.vue
index e04391f..3ed5666 100644
--- a/src/pages/qualityManagement/materialInspection/add.vue
+++ b/src/pages/qualityManagement/materialInspection/add.vue
@@ -16,9 +16,9 @@
<up-input v-model="form.supplier"
placeholder="璇烽�夋嫨渚涘簲鍟�"
readonly
- :disabled="supplierQuantityDisabled" />
+ :disabled="supplierQuantityDisabled || isReadonly" />
<template #right>
- <up-icon @click="showSupplierSheet = true"
+ <up-icon @click="!isReadonly && (showSupplierSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -29,10 +29,10 @@
<up-input v-model="form.productName"
placeholder="璇烽�夋嫨浜у搧"
readonly
- @click="showProductTree = true"
- :disabled="isEdit" />
+ @click="!isReadonly && (showProductTree = true)"
+ :disabled="isEdit || isReadonly" />
<template #right>
- <up-icon @click="showProductTree = true"
+ <up-icon @click="!isReadonly && (showProductTree = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -43,9 +43,17 @@
<up-input v-model="form.model"
placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
readonly
- :disabled="isEdit" />
+ :disabled="isEdit || isReadonly" />
<template #right>
- <up-icon @click="showModelSheet = true"
+ <view v-if="!isEdit"
+ class="scan-entry"
+ @click.stop="scanBarcode">
+ <up-icon name="scan"
+ size="18"
+ color="#3c9cff" />
+ <text class="scan-entry-text">鎵爜</text>
+ </view>
+ <up-icon @click="!isReadonly && (showModelSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -55,7 +63,8 @@
border-bottom>
<up-input v-model="testStandardDisplay"
placeholder="璇烽�夋嫨鎸囨爣"
- readonly />
+ readonly
+ :disabled="isReadonly" />
<template #right>
<up-icon @click="openTestStandardSheet"
name="arrow-right" />
@@ -75,13 +84,14 @@
<up-input v-model="form.quantity"
type="number"
placeholder="璇疯緭鍏ユ暟閲�"
- :disabled="supplierQuantityDisabled" />
+ :disabled="supplierQuantityDisabled || isReadonly" />
</up-form-item>
<up-form-item label="妫�娴嬪崟浣�"
prop="checkCompany"
border-bottom>
<up-input v-model="form.checkCompany"
placeholder="璇疯緭鍏ユ娴嬪崟浣�"
+ :disabled="isReadonly"
clearable />
</up-form-item>
<up-form-item label="妫�娴嬬粨鏋�"
@@ -91,9 +101,10 @@
<up-input v-model="form.checkResult"
placeholder="璇烽�夋嫨妫�娴嬬粨鏋�"
readonly
+ :disabled="isReadonly"
@click="showResultSheet" />
<template #right>
- <up-icon @click="showResultSheet = true"
+ <up-icon @click="!isReadonly && (showResultSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -103,9 +114,10 @@
<up-input v-model="form.checkName"
placeholder="璇烽�夋嫨妫�楠屽憳"
readonly
+ :disabled="isReadonly"
@click="showInspectorSheet" />
<template #right>
- <up-icon @click="showInspectorSheet = true"
+ <up-icon @click="!isReadonly && (showInspectorSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -116,6 +128,7 @@
<up-input v-model="form.checkTime"
placeholder="璇烽�夋嫨妫�娴嬫棩鏈�"
readonly
+ :disabled="isReadonly"
@click="showDatePicker" />
<!-- <template #right>
<up-icon name="arrow-right"
@@ -163,6 +176,7 @@
<text class="step-label">妫�楠屽�硷細</text>
<up-input v-model="item.testValue"
placeholder="璇疯緭鍏ユ楠屽��"
+ :disabled="isReadonly"
clearable
border-bottom
class="step-input" />
@@ -182,14 +196,16 @@
size="default"
@click="goBack"
class="bottom-btn">
- 鍙栨秷
+ {{ isReadonly ? '杩斿洖' : '鍙栨秷' }}
</up-button>
- <up-button type="primary"
+ <!-- 宸叉彁浜ゅ崟鎹彧璇伙紝闅愯棌淇濆瓨/鎻愪氦 -->
+ <up-button v-if="!isReadonly"
+ type="primary"
size="default"
@click="submitForm"
:loading="loading"
class="bottom-btn">
- {{ isEdit ? '淇濆瓨' : '鎻愪氦' }}
+ 淇濆瓨
</up-button>
</view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
@@ -319,7 +335,11 @@
import PageHeader from "@/components/PageHeader.vue";
import dayjs from "dayjs";
import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
- import { modelList, productTreeList } from "@/api/basicData/product.js";
+ import {
+ modelList,
+ productTreeList,
+ modelByBarcode,
+ } from "@/api/basicData/product.js";
import {
qualityInspectAdd,
qualityInspectUpdate,
@@ -421,6 +441,7 @@
return modelOptions.value.map(item => ({
name: item.model,
value: item.id,
+ subname: item.barcode ? `鏉$爜锛�${item.barcode}` : "",
}));
});
@@ -477,6 +498,9 @@
return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
+ // 宸叉彁浜わ紙inspectState=1锛夊崟鎹暣浣撳彧璇�
+ const isReadonly = computed(() => Number(form.value?.inspectState) === 1);
+
// 杩斿洖涓婁竴椤�
const goBack = () => {
uni.navigateBack();
@@ -484,6 +508,7 @@
// 鏄剧ず鏃ユ湡閫夋嫨鍣�
const showDatePicker = () => {
+ if (isReadonly.value) return;
showDate.value = true;
};
@@ -610,7 +635,7 @@
});
};
const openTestStandardSheet = () => {
- console.log("openTestStandardSheet");
+ if (isReadonly.value) return;
showTestStandardSheet.value = true;
};
@@ -640,19 +665,51 @@
}
};
- // 鑾峰彇鍨嬪彿鍒楄〃
+ // 鑾峰彇鍨嬪彿鍒楄〃锛堣繑鍥� Promise锛屼究浜庢壂鐮佸悗椤哄簭鍥炲~锛�
const getModels = value => {
form.value.productModelId = "";
form.value.unit = "";
modelOptions.value = [];
currentProductId.value = value;
form.value.productName = findNodeById(productOptions.value, value);
- modelList({ id: value }).then(res => {
- modelOptions.value = res;
+ const task = modelList({ id: value }).then(res => {
+ modelOptions.value = res?.data || res || [];
});
if (currentProductId.value) {
getList();
}
+ return task;
+ };
+
+ // 鎵爜鎸夋潯鐮佺簿纭洖濉骇鍝�+瑙勬牸鍨嬪彿
+ const scanBarcode = () => {
+ if (isEdit.value) return;
+ uni.scanCode({
+ scanType: ["barCode", "qrCode"],
+ success: async res => {
+ const barcode = (res.result || "").trim();
+ if (!barcode) return;
+ uni.showLoading({ title: "鍖归厤涓�...", mask: true });
+ try {
+ const resp = await modelByBarcode({ barcode });
+ const info = resp?.data || resp;
+ if (info && info.productId) {
+ // 鍏堟寜 productId 鑱斿姩鍑轰骇鍝佸眰锛屼繚璇� UI 鐘舵�佷笌鎵嬪姩閫夋嫨涓�鑷�
+ await getModels(info.productId);
+ form.value.productId = info.productId;
+ form.value.productName = info.productName || form.value.productName;
+ form.value.productModelId = info.id;
+ form.value.model = info.model || "";
+ form.value.unit = info.unit || "";
+ }
+ uni.hideLoading();
+ uni.showToast({ title: "宸插尮閰嶈鏍�", icon: "success" });
+ } catch (e) {
+ // 鏈尮閰嶇瓑閿欒宸叉湁鍏ㄥ眬 toast锛岃繖閲屽彧鎭㈠ loading
+ uni.hideLoading();
+ }
+ },
+ });
};
// 鑾峰彇鎸囨爣鍒楄〃
@@ -687,6 +744,11 @@
// 鎻愪氦琛ㄥ崟
const submitForm = async () => {
console.log("submitForm", form.value, tableData.value);
+ // 宸叉彁浜ゅ崟鎹姝繚瀛�/鎻愪氦
+ if (isReadonly.value) {
+ showToast("妫�楠屽崟宸叉彁浜わ紝涓嶅厑璁镐慨鏀�");
+ return;
+ }
try {
// await formRef.value.validate();
if (!form.value.productModelId) {
@@ -726,21 +788,22 @@
const data = { ...form.value, qualityInspectParams: tableData.value };
data.quantity = Number(data.quantity);
if (isEdit.value) {
- const res = await qualityInspectUpdate(data);
+ await qualityInspectUpdate(data);
showToast("淇濆瓨鎴愬姛");
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
- const res = await qualityInspectAdd(data);
- showToast("鎻愪氦鎴愬姛");
+ // 鏂板浠呬繚瀛樿崏绋匡紝姝e紡鎻愪氦鍦ㄥ垪琛ㄩ〉銆屾彁浜ゃ�嶆寜閽紙浼氱敓鎴愬叆搴撹褰曪級
+ await qualityInspectAdd(data);
+ showToast("淇濆瓨鎴愬姛");
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} catch (error) {
- console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
- showToast("鎻愪氦澶辫触锛岃閲嶈瘯");
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鎻愪氦澶辫触:", error);
} finally {
loading.value = false;
}
@@ -912,6 +975,19 @@
<style scoped lang="scss">
@import "@/static/scss/form-common.scss";
+ // 瑙勬牸琛屾壂鐮佸叆鍙�
+ .scan-entry {
+ display: flex;
+ align-items: center;
+ margin-right: 8px;
+
+ .scan-entry-text {
+ font-size: 13px;
+ color: #3c9cff;
+ margin-left: 2px;
+ }
+ }
+
.material-inspection-add {
min-height: 100vh;
background: #f8f9fa;
diff --git a/src/pages/qualityManagement/materialInspection/detail.vue b/src/pages/qualityManagement/materialInspection/detail.vue
index 12ff7a3..c215539 100644
--- a/src/pages/qualityManagement/materialInspection/detail.vue
+++ b/src/pages/qualityManagement/materialInspection/detail.vue
@@ -24,7 +24,7 @@
<u-tag :type="getStateTagType(detailData.inspectState)"
size="small"
class="status-tag">
- {{ detailData.inspectState ? '宸叉彁浜�' : '鏈彁浜�' }}
+ {{ detailData.inspectState ? '宸叉彁浜�' : '寰呮彁浜�' }}
</u-tag>
</view>
</view>
@@ -66,6 +66,18 @@
<text class="detail-label">妫�娴嬪崟浣�</text>
<text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
</view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ detailData.qualifiedQuantity != null ? detailData.qualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value">{{ detailData.unqualifiedQuantity != null ? detailData.unqualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ detailData.checkResult || '-' }}</text>
+ </view>
<!-- <view class="detail-row">
<text class="detail-label">妫�楠屾爣鍑�</text>
<text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
diff --git a/src/pages/qualityManagement/materialInspection/index.vue b/src/pages/qualityManagement/materialInspection/index.vue
index eb4a140..7b44b22 100644
--- a/src/pages/qualityManagement/materialInspection/index.vue
+++ b/src/pages/qualityManagement/materialInspection/index.vue
@@ -26,6 +26,17 @@
color="#999"></up-icon>
</view>
</view>
+ <!-- 妫�楠岀姸鎬佺瓫閫� -->
+ <view class="filter-row">
+ <view class="filter-item"
+ @click="showStateSheet = true">
+ <text class="filter-label">妫�楠岀姸鎬�</text>
+ <text class="filter-value">{{ stateFilterLabel }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ </view>
<!-- 鏃ユ湡閫夋嫨 -->
<!-- <view class="date-range"
v-if="searchForm.entryDate">
@@ -85,7 +96,7 @@
<u-tag :type="getStateTagType(item.inspectState)"
size="mini"
class="status-tag">
- {{ item.inspectState ? '宸叉彁浜�' : '鏈彁浜�' }}
+ {{ item.inspectState ? '宸叉彁浜�' : '寰呮彁浜�' }}
</u-tag>
</view>
</view>
@@ -114,6 +125,18 @@
<view class="detail-row">
<text class="detail-label">妫�娴嬪崟浣�</text>
<text class="detail-value">{{ item.checkCompany || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ item.qualifiedQuantity != null ? item.qualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value">{{ item.unqualifiedQuantity != null ? item.unqualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ item.checkResult || '-' }}</text>
</view>
</view>
<!-- 鎿嶄綔鎸夐挳 -->
@@ -152,6 +175,15 @@
:disabled="item.inspectState || item.checkName !== ''"
@click.stop="assignInspector(item)">
鍒嗛厤妫�楠屽憳
+ </u-button>
+ </view>
+ <view class="action-buttons">
+ <u-button type="error"
+ size="small"
+ class="action-btn"
+ :disabled="!(Number(item.unqualifiedQuantity) > 0)"
+ @click.stop="openUnqualified(item)">
+ 涓嶅悎鏍煎鐞�
</u-button>
</view>
</view>
@@ -223,6 +255,56 @@
:actions="userSheetOptions"
@select="selectInspector"
title="閫夋嫨妫�楠屽憳" />
+ <!-- 妫�楠岀姸鎬佺瓫閫� -->
+ <up-action-sheet :show="showStateSheet"
+ :actions="stateSheetOptions"
+ @select="selectState"
+ @close="showStateSheet = false"
+ title="閫夋嫨妫�楠岀姸鎬�" />
+ <!-- 涓嶅悎鏍煎鐞嗗脊绐� -->
+ <up-popup v-model:show="showUnqualifiedPopup"
+ mode="bottom"
+ round>
+ <view class="unqualified-dialog">
+ <view class="dialog-header">
+ <text class="dialog-title">涓嶅悎鏍煎鐞�</text>
+ <up-icon name="close"
+ size="20"
+ color="#999"
+ @click="showUnqualifiedPopup = false"></up-icon>
+ </view>
+ <view class="dialog-content">
+ <view v-if="unqualifiedLoading"
+ class="unqualified-empty">鍔犺浇涓�...</view>
+ <view v-else-if="unqualifiedList.length === 0"
+ class="unqualified-empty">鏆傛棤寰呭鐞嗕笉鍚堟牸璁板綍</view>
+ <view v-for="(row, idx) in unqualifiedList"
+ :key="idx"
+ class="unqualified-item">
+ <view class="unqualified-info">
+ <text class="unqualified-label">涓嶅悎鏍肩幇璞�</text>
+ <text class="unqualified-value">{{ row.defectivePhenomena || '-' }}</text>
+ </view>
+ <view class="unqualified-info">
+ <text class="unqualified-label">鏁伴噺</text>
+ <text class="unqualified-value">{{ row.quantity != null ? row.quantity : '-' }}</text>
+ </view>
+ <u-button type="primary"
+ size="small"
+ class="unqualified-btn"
+ @click="openDealSheet(row)">
+ 澶勭悊
+ </u-button>
+ </view>
+ </view>
+ </view>
+ </up-popup>
+ <!-- 澶勭悊缁撴灉閫夋嫨 -->
+ <up-action-sheet :show="showDealSheet"
+ :actions="dealSheetOptions"
+ @select="selectDeal"
+ @close="showDealSheet = false"
+ title="閫夋嫨澶勭悊缁撴灉" />
</view>
</template>
@@ -235,8 +317,13 @@
submitQualityInspect,
qualityInspectUpdate,
qualityInspectListPage,
+ qualityUnqualifiedListPage,
+ qualityUnqualifiedDeal,
} from "@/api/qualityManagement/materialInspection.js";
import { userListNoPage } from "@/api/system/user.js";
+
+ // 鍘熸枡妫�楠� inspectType=0
+ const currentInspectType = 0;
// 鏄剧ず鎻愮ず淇℃伅
const showToast = message => {
@@ -294,6 +381,104 @@
value: item.nickName,
}));
});
+
+ // 妫�楠岀姸鎬佺瓫閫�
+ const showStateSheet = ref(false);
+ const stateFilter = ref("");
+ const stateSheetOptions = [
+ { name: "鍏ㄩ儴", value: "" },
+ { name: "寰呮彁浜�", value: 0 },
+ { name: "宸叉彁浜�", value: 1 },
+ ];
+ const stateFilterLabel = computed(() => {
+ const hit = stateSheetOptions.find(o => o.value === stateFilter.value);
+ return hit ? hit.name : "鍏ㄩ儴";
+ });
+ const selectState = e => {
+ stateFilter.value = e.value;
+ showStateSheet.value = false;
+ getList();
+ };
+
+ // 涓嶅悎鏍煎鐞嗙浉鍏�
+ const showUnqualifiedPopup = ref(false);
+ const unqualifiedLoading = ref(false);
+ const unqualifiedList = ref([]);
+ const currentInspectRow = ref(null);
+ const showDealSheet = ref(false);
+ const currentUnqualifiedRow = ref(null);
+ // 鍘熸枡绫讳粎鏀寔 鎶ュ簾/璁╂鏀捐
+ const dealSheetOptions = computed(() => {
+ return ["鎶ュ簾", "璁╂鏀捐"].map(name => ({ name, value: name }));
+ });
+
+ // 鎵撳紑涓嶅悎鏍煎鐞嗗脊绐楋細鎸夋楠屽崟id鎷夊彇寰呭鐞嗕笉鍚堟牸璁板綍
+ const openUnqualified = async item => {
+ currentInspectRow.value = item;
+ showUnqualifiedPopup.value = true;
+ unqualifiedLoading.value = true;
+ unqualifiedList.value = [];
+ try {
+ const res = await qualityUnqualifiedListPage({
+ inspectType: currentInspectType,
+ inspectState: 0,
+ inspectId: item.id,
+ current: -1,
+ size: -1,
+ });
+ let records = res.data?.records || res.data || [];
+ // 鑻ュ悗绔湭鎸夋楠屽崟杩囨护锛屽墠绔厹搴曟寜妫�楠屽崟id杩囨护
+ const hasInspectRef = records.some(
+ r => r.inspectId != null || r.qualityInspectId != null
+ );
+ if (hasInspectRef) {
+ records = records.filter(
+ r => (r.inspectId ?? r.qualityInspectId) == item.id
+ );
+ }
+ // 鍙繚鐣欏緟澶勭悊璁板綍
+ unqualifiedList.value = records.filter(
+ r => Number(r.inspectState) !== 1
+ );
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鑾峰彇涓嶅悎鏍艰褰曞け璐�:", error);
+ unqualifiedList.value = [];
+ } finally {
+ unqualifiedLoading.value = false;
+ }
+ };
+
+ // 鎵撳紑澶勭悊缁撴灉閫夋嫨
+ const openDealSheet = row => {
+ currentUnqualifiedRow.value = row;
+ showDealSheet.value = true;
+ };
+
+ // 閫夋嫨澶勭悊缁撴灉
+ const selectDeal = e => {
+ showDealSheet.value = false;
+ if (!currentUnqualifiedRow.value) return;
+ doDeal(currentUnqualifiedRow.value, e.value);
+ };
+
+ // 鎻愪氦涓嶅悎鏍煎鐞�
+ const doDeal = async (row, dealResult) => {
+ try {
+ await qualityUnqualifiedDeal({ id: row.id, dealResult });
+ showToast("澶勭悊鎴愬姛");
+ if (currentInspectRow.value) {
+ await openUnqualified(currentInspectRow.value);
+ }
+ if (unqualifiedList.value.length === 0) {
+ showUnqualifiedPopup.value = false;
+ }
+ getList();
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("涓嶅悎鏍煎鐞嗗け璐�:", error);
+ }
+ };
// 杩斿洖涓婁竴椤�
const goBack = () => {
@@ -371,7 +556,9 @@
tableLoading.value = true;
const params = { ...searchForm.value, ...page.value };
params.entryDate = undefined;
- qualityInspectListPage({ ...params, inspectType: 0 })
+ params.inspectState =
+ stateFilter.value === "" ? undefined : stateFilter.value;
+ qualityInspectListPage({ ...params, inspectType: currentInspectType })
.then(res => {
tableLoading.value = false;
inspectionList.value = res.data.records || [];
@@ -429,25 +616,30 @@
};
// 鎻愪氦妫�楠�
- const submitInspection = async item => {
+ const submitInspection = item => {
if (!item) {
showToast("鍙傛暟閿欒");
return;
}
- try {
- const res = await submitQualityInspect({ id: item.id });
- if (res.code === 200) {
- showToast("鎻愪氦鎴愬姛");
- setTimeout(() => {
- getList();
- }, 1000);
- } else {
- showToast("鎻愪氦澶辫触锛�" + (res.msg || "鏈煡閿欒"));
- }
- } catch (error) {
- console.error("鎻愪氦澶辫触:", error);
- showToast("鎻愪氦澶辫触锛岃閲嶈瘯");
- }
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝鏄惁缁х画锛�",
+ success: async res => {
+ if (!res.confirm) return;
+ try {
+ await submitQualityInspect({ id: item.id });
+ showToast(
+ "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+ );
+ setTimeout(() => {
+ getList();
+ }, 1500);
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鎻愪氦澶辫触:", error);
+ }
+ },
+ });
};
// 鍒嗛厤妫�楠屽憳
@@ -550,6 +742,78 @@
padding: 5px;
}
+ // 妫�楠岀姸鎬佺瓫閫�
+ .filter-row {
+ display: flex;
+ align-items: center;
+ margin-top: 10px;
+ }
+
+ .filter-item {
+ display: flex;
+ align-items: center;
+ padding: 4px 10px;
+ background: #f2f3f5;
+ border-radius: 12px;
+ }
+
+ .filter-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 4px;
+ }
+
+ .filter-value {
+ font-size: 12px;
+ color: #2979ff;
+ margin-right: 4px;
+ }
+
+ // 涓嶅悎鏍煎鐞嗗脊绐�
+ .unqualified-dialog {
+ background: #ffffff;
+ border-radius: 16px 16px 0 0;
+ padding: 20px;
+ max-height: 70vh;
+ overflow-y: auto;
+ }
+
+ .unqualified-empty {
+ padding: 30px 0;
+ text-align: center;
+ font-size: 13px;
+ color: #999;
+ }
+
+ .unqualified-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .unqualified-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-right: 10px;
+ }
+
+ .unqualified-label {
+ font-size: 12px;
+ color: #999;
+ }
+
+ .unqualified-value {
+ font-size: 13px;
+ color: #333;
+ margin-top: 2px;
+ }
+
+ .unqualified-btn {
+ width: 70px;
+ }
+
// 缁熻鍗$墖
.stats-cards {
display: flex;
diff --git a/src/pages/qualityManagement/processInspection/add.vue b/src/pages/qualityManagement/processInspection/add.vue
index 2f6a6d0..2fd5984 100644
--- a/src/pages/qualityManagement/processInspection/add.vue
+++ b/src/pages/qualityManagement/processInspection/add.vue
@@ -16,9 +16,9 @@
<up-input v-model="form.process"
placeholder="璇烽�夋嫨宸ュ簭"
readonly
- :disabled="processQuantityDisabled" />
+ :disabled="processQuantityDisabled || isReadonly" />
<template #right>
- <up-icon @click="showprocessSheet = true"
+ <up-icon @click="!isReadonly && (showprocessSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -29,10 +29,10 @@
<up-input v-model="form.productName"
placeholder="璇烽�夋嫨浜у搧"
readonly
- @click="showProductTree = true"
- :disabled="isEdit" />
+ @click="!isReadonly && (showProductTree = true)"
+ :disabled="isEdit || isReadonly" />
<template #right>
- <up-icon @click="showProductTree = true"
+ <up-icon @click="!isReadonly && (showProductTree = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -43,9 +43,9 @@
<up-input v-model="form.model"
placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
readonly
- :disabled="isEdit" />
+ :disabled="isEdit || isReadonly" />
<template #right>
- <up-icon @click="showModelSheet = true"
+ <up-icon @click="!isReadonly && (showModelSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -55,7 +55,8 @@
border-bottom>
<up-input v-model="testStandardDisplay"
placeholder="璇烽�夋嫨鎸囨爣"
- readonly />
+ readonly
+ :disabled="isReadonly" />
<template #right>
<up-icon @click="openTestStandardSheet"
name="arrow-right" />
@@ -75,13 +76,14 @@
<up-input v-model="form.quantity"
type="number"
placeholder="璇疯緭鍏ユ暟閲�"
- :disabled="processQuantityDisabled" />
+ :disabled="processQuantityDisabled || isReadonly" />
</up-form-item>
<up-form-item label="妫�娴嬪崟浣�"
prop="checkCompany"
border-bottom>
<up-input v-model="form.checkCompany"
placeholder="璇疯緭鍏ユ娴嬪崟浣�"
+ :disabled="isReadonly"
clearable />
</up-form-item>
<up-form-item label="妫�娴嬬粨鏋�"
@@ -91,9 +93,10 @@
<up-input v-model="form.checkResult"
placeholder="璇烽�夋嫨妫�娴嬬粨鏋�"
readonly
+ :disabled="isReadonly"
@click="showResultSheet" />
<template #right>
- <up-icon @click="showResultSheet = true"
+ <up-icon @click="!isReadonly && (showResultSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -103,9 +106,10 @@
<up-input v-model="form.checkName"
placeholder="璇烽�夋嫨妫�楠屽憳"
readonly
+ :disabled="isReadonly"
@click="showInspectorSheet" />
<template #right>
- <up-icon @click="showInspectorSheet = true"
+ <up-icon @click="!isReadonly && (showInspectorSheet = true)"
name="arrow-right" />
</template>
</up-form-item>
@@ -115,7 +119,8 @@
border-bottom>
<up-input v-model="form.checkTime"
placeholder="璇烽�夋嫨妫�娴嬫棩鏈�"
- readonly />
+ readonly
+ :disabled="isReadonly" />
<!-- <template #right>
<up-icon name="calendar"
@click="showDatePicker"></up-icon>
@@ -155,6 +160,7 @@
<text class="step-label">妫�楠屽�硷細</text>
<up-input v-model="item.testValue"
placeholder="璇疯緭鍏ユ楠屽��"
+ :disabled="isReadonly"
clearable
border-bottom
class="step-input" />
@@ -174,14 +180,16 @@
size="default"
@click="goBack"
class="bottom-btn">
- 鍙栨秷
+ {{ isReadonly ? '杩斿洖' : '鍙栨秷' }}
</up-button>
- <up-button type="primary"
+ <!-- 宸叉彁浜ゅ崟鎹彧璇伙紝闅愯棌淇濆瓨/鎻愪氦 -->
+ <up-button v-if="!isReadonly"
+ type="primary"
size="default"
@click="submitForm"
:loading="loading"
class="bottom-btn">
- {{ isEdit ? '淇濆瓨' : '鎻愪氦' }}
+ 淇濆瓨
</up-button>
</view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
@@ -469,6 +477,9 @@
return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
+ // 宸叉彁浜わ紙inspectState=1锛夊崟鎹暣浣撳彧璇�
+ const isReadonly = computed(() => Number(form.value?.inspectState) === 1);
+
// 杩斿洖涓婁竴椤�
const goBack = () => {
uni.navigateBack();
@@ -476,11 +487,13 @@
// 鏄剧ず鏃ユ湡閫夋嫨鍣�
const showDatePicker = () => {
+ if (isReadonly.value) return;
showDate.value = true;
};
// 纭鏃ユ湡閫夋嫨
const confirmDate = e => {
+ if (isReadonly.value) return;
form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
};
@@ -601,7 +614,7 @@
});
};
const openTestStandardSheet = () => {
- console.log("openTestStandardSheet");
+ if (isReadonly.value) return;
showTestStandardSheet.value = true;
};
@@ -678,6 +691,11 @@
// 鎻愪氦琛ㄥ崟
const submitForm = async () => {
console.log("submitForm", form.value, tableData.value);
+ // 宸叉彁浜ゅ崟鎹姝繚瀛�/鎻愪氦
+ if (isReadonly.value) {
+ showToast("妫�楠屽崟宸叉彁浜わ紝涓嶅厑璁镐慨鏀�");
+ return;
+ }
try {
// await formRef.value.validate();
if (!form.value.productModelId) {
@@ -717,21 +735,22 @@
const data = { ...form.value, qualityInspectParams: tableData.value };
data.quantity = Number(data.quantity);
if (isEdit.value) {
- const res = await qualityInspectUpdate(data);
+ await qualityInspectUpdate(data);
showToast("淇濆瓨鎴愬姛");
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
- const res = await qualityInspectAdd(data);
- showToast("鎻愪氦鎴愬姛");
+ // 鏂板浠呬繚瀛樿崏绋匡紝姝e紡鎻愪氦鍦ㄥ垪琛ㄩ〉銆屾彁浜ゃ�嶆寜閽�
+ await qualityInspectAdd(data);
+ showToast("淇濆瓨鎴愬姛");
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} catch (error) {
- console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
- showToast("鎻愪氦澶辫触锛岃閲嶈瘯");
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鎻愪氦澶辫触:", error);
} finally {
loading.value = false;
}
diff --git a/src/pages/qualityManagement/processInspection/detail.vue b/src/pages/qualityManagement/processInspection/detail.vue
index 798aeee..06ebd41 100644
--- a/src/pages/qualityManagement/processInspection/detail.vue
+++ b/src/pages/qualityManagement/processInspection/detail.vue
@@ -24,7 +24,7 @@
<u-tag :type="getStateTagType(detailData.inspectState)"
size="small"
class="status-tag">
- {{ detailData.inspectState ? '宸叉彁浜�' : '鏈彁浜�' }}
+ {{ detailData.inspectState ? '宸叉彁浜�' : '寰呮彁浜�' }}
</u-tag>
</view>
</view>
@@ -66,6 +66,18 @@
<text class="detail-label">妫�娴嬪崟浣�</text>
<text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
</view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ detailData.qualifiedQuantity != null ? detailData.qualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value">{{ detailData.unqualifiedQuantity != null ? detailData.unqualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ detailData.checkResult || '-' }}</text>
+ </view>
<!-- <view class="detail-row">
<text class="detail-label">妫�楠屾爣鍑�</text>
<text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
diff --git a/src/pages/qualityManagement/processInspection/index.vue b/src/pages/qualityManagement/processInspection/index.vue
index 40358ef..8a58c07 100644
--- a/src/pages/qualityManagement/processInspection/index.vue
+++ b/src/pages/qualityManagement/processInspection/index.vue
@@ -26,6 +26,17 @@
color="#999"></up-icon>
</view>
</view>
+ <!-- 妫�楠岀姸鎬佺瓫閫� -->
+ <view class="filter-row">
+ <view class="filter-item"
+ @click="showStateSheet = true">
+ <text class="filter-label">妫�楠岀姸鎬�</text>
+ <text class="filter-value">{{ stateFilterLabel }}</text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ </view>
<!-- 鏃ユ湡閫夋嫨 -->
<!-- <view class="date-range"
v-if="searchForm.entryDate">
@@ -85,7 +96,7 @@
<u-tag :type="getStateTagType(item.inspectState)"
size="mini"
class="status-tag">
- {{ item.inspectState ? '宸叉彁浜�' : '鏈彁浜�' }}
+ {{ item.inspectState ? '宸叉彁浜�' : '寰呮彁浜�' }}
</u-tag>
</view>
</view>
@@ -114,6 +125,18 @@
<view class="detail-row">
<text class="detail-label">妫�娴嬪崟浣�</text>
<text class="detail-value">{{ item.checkCompany || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堟牸鏁伴噺</text>
+ <text class="detail-value">{{ item.qualifiedQuantity != null ? item.qualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅悎鏍兼暟閲�</text>
+ <text class="detail-value">{{ item.unqualifiedQuantity != null ? item.unqualifiedQuantity : '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�楠岀粨璁�</text>
+ <text class="detail-value">{{ item.checkResult || '-' }}</text>
</view>
</view>
<!-- 鎿嶄綔鎸夐挳 -->
@@ -152,6 +175,15 @@
:disabled="item.inspectState || item.checkName !== ''"
@click.stop="assignInspector(item)">
鍒嗛厤妫�楠屽憳
+ </u-button>
+ </view>
+ <view class="action-buttons">
+ <u-button type="error"
+ size="small"
+ class="action-btn"
+ :disabled="!(Number(item.unqualifiedQuantity) > 0)"
+ @click.stop="openUnqualified(item)">
+ 涓嶅悎鏍煎鐞�
</u-button>
</view>
</view>
@@ -223,6 +255,56 @@
:actions="userSheetOptions"
@select="selectInspector"
title="閫夋嫨妫�楠屽憳" />
+ <!-- 妫�楠岀姸鎬佺瓫閫� -->
+ <up-action-sheet :show="showStateSheet"
+ :actions="stateSheetOptions"
+ @select="selectState"
+ @close="showStateSheet = false"
+ title="閫夋嫨妫�楠岀姸鎬�" />
+ <!-- 涓嶅悎鏍煎鐞嗗脊绐� -->
+ <up-popup v-model:show="showUnqualifiedPopup"
+ mode="bottom"
+ round>
+ <view class="unqualified-dialog">
+ <view class="dialog-header">
+ <text class="dialog-title">涓嶅悎鏍煎鐞�</text>
+ <up-icon name="close"
+ size="20"
+ color="#999"
+ @click="showUnqualifiedPopup = false"></up-icon>
+ </view>
+ <view class="dialog-content">
+ <view v-if="unqualifiedLoading"
+ class="unqualified-empty">鍔犺浇涓�...</view>
+ <view v-else-if="unqualifiedList.length === 0"
+ class="unqualified-empty">鏆傛棤寰呭鐞嗕笉鍚堟牸璁板綍</view>
+ <view v-for="(row, idx) in unqualifiedList"
+ :key="idx"
+ class="unqualified-item">
+ <view class="unqualified-info">
+ <text class="unqualified-label">涓嶅悎鏍肩幇璞�</text>
+ <text class="unqualified-value">{{ row.defectivePhenomena || '-' }}</text>
+ </view>
+ <view class="unqualified-info">
+ <text class="unqualified-label">鏁伴噺</text>
+ <text class="unqualified-value">{{ row.quantity != null ? row.quantity : '-' }}</text>
+ </view>
+ <u-button type="primary"
+ size="small"
+ class="unqualified-btn"
+ @click="openDealSheet(row)">
+ 澶勭悊
+ </u-button>
+ </view>
+ </view>
+ </view>
+ </up-popup>
+ <!-- 澶勭悊缁撴灉閫夋嫨 -->
+ <up-action-sheet :show="showDealSheet"
+ :actions="dealSheetOptions"
+ @select="selectDeal"
+ @close="showDealSheet = false"
+ title="閫夋嫨澶勭悊缁撴灉" />
</view>
</template>
@@ -235,8 +317,13 @@
submitQualityInspect,
qualityInspectUpdate,
qualityInspectListPage,
+ qualityUnqualifiedListPage,
+ qualityUnqualifiedDeal,
} from "@/api/qualityManagement/materialInspection.js";
import { userListNoPage } from "@/api/system/user.js";
+
+ // 杩囩▼妫�楠� inspectType=1
+ const currentInspectType = 1;
// 鏄剧ず鎻愮ず淇℃伅
const showToast = message => {
@@ -294,6 +381,121 @@
value: item.nickName,
}));
});
+
+ // 妫�楠岀姸鎬佺瓫閫�
+ const showStateSheet = ref(false);
+ const stateFilter = ref("");
+ const stateSheetOptions = [
+ { name: "鍏ㄩ儴", value: "" },
+ { name: "寰呮彁浜�", value: 0 },
+ { name: "宸叉彁浜�", value: 1 },
+ ];
+ const stateFilterLabel = computed(() => {
+ const hit = stateSheetOptions.find(o => o.value === stateFilter.value);
+ return hit ? hit.name : "鍏ㄩ儴";
+ });
+ const selectState = e => {
+ stateFilter.value = e.value;
+ showStateSheet.value = false;
+ getList();
+ };
+
+ // 涓嶅悎鏍煎鐞嗙浉鍏�
+ const showUnqualifiedPopup = ref(false);
+ const unqualifiedLoading = ref(false);
+ const unqualifiedList = ref([]);
+ const currentInspectRow = ref(null);
+ const showDealSheet = ref(false);
+ const currentUnqualifiedRow = ref(null);
+ // 杩囩▼/鎴愬搧绫绘敮鎸� 杩斾慨/杩斿伐/鎶ュ簾/璁╂鏀捐
+ const dealSheetOptions = computed(() => {
+ return ["杩斾慨", "杩斿伐", "鎶ュ簾", "璁╂鏀捐"].map(name => ({
+ name,
+ value: name,
+ }));
+ });
+
+ // 鎵撳紑涓嶅悎鏍煎鐞嗗脊绐楋細鎸夋楠屽崟id鎷夊彇寰呭鐞嗕笉鍚堟牸璁板綍
+ const openUnqualified = async item => {
+ currentInspectRow.value = item;
+ showUnqualifiedPopup.value = true;
+ unqualifiedLoading.value = true;
+ unqualifiedList.value = [];
+ try {
+ const res = await qualityUnqualifiedListPage({
+ inspectType: currentInspectType,
+ inspectState: 0,
+ inspectId: item.id,
+ current: -1,
+ size: -1,
+ });
+ let records = res.data?.records || res.data || [];
+ // 鑻ュ悗绔湭鎸夋楠屽崟杩囨护锛屽墠绔厹搴曟寜妫�楠屽崟id杩囨护
+ const hasInspectRef = records.some(
+ r => r.inspectId != null || r.qualityInspectId != null
+ );
+ if (hasInspectRef) {
+ records = records.filter(
+ r => (r.inspectId ?? r.qualityInspectId) == item.id
+ );
+ }
+ // 鍙繚鐣欏緟澶勭悊璁板綍
+ unqualifiedList.value = records.filter(
+ r => Number(r.inspectState) !== 1
+ );
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鑾峰彇涓嶅悎鏍艰褰曞け璐�:", error);
+ unqualifiedList.value = [];
+ } finally {
+ unqualifiedLoading.value = false;
+ }
+ };
+
+ // 鎵撳紑澶勭悊缁撴灉閫夋嫨
+ const openDealSheet = row => {
+ currentUnqualifiedRow.value = row;
+ showDealSheet.value = true;
+ };
+
+ // 閫夋嫨澶勭悊缁撴灉
+ const selectDeal = e => {
+ showDealSheet.value = false;
+ if (!currentUnqualifiedRow.value) return;
+ const result = e.value;
+ // 杩斾慨/杩斿伐浼氳嚜鍔ㄥ垱寤鸿繑宸ョ敓浜ц鍗曪紝鍏堜簩娆$‘璁�
+ if (result === "杩斾慨" || result === "杩斿伐") {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "灏嗚嚜鍔ㄥ垱寤鸿繑宸ョ敓浜ц鍗曪紙FG鍗曞彿锛夛紝鏄惁缁х画锛�",
+ success: res => {
+ if (res.confirm) {
+ doDeal(currentUnqualifiedRow.value, result);
+ }
+ },
+ });
+ return;
+ }
+ doDeal(currentUnqualifiedRow.value, result);
+ };
+
+ // 鎻愪氦涓嶅悎鏍煎鐞�
+ const doDeal = async (row, dealResult) => {
+ try {
+ await qualityUnqualifiedDeal({ id: row.id, dealResult });
+ showToast("澶勭悊鎴愬姛");
+ if (currentInspectRow.value) {
+ await openUnqualified(currentInspectRow.value);
+ }
+ if (unqualifiedList.value.length === 0) {
+ showUnqualifiedPopup.value = false;
+ }
+ getList();
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("涓嶅悎鏍煎鐞嗗け璐�:", error);
+ }
+ };
// 杩斿洖涓婁竴椤�
const goBack = () => {
@@ -371,7 +573,9 @@
tableLoading.value = true;
const params = { ...searchForm.value, ...page.value };
params.entryDate = undefined;
- qualityInspectListPage({ ...params, inspectType: 1 })
+ params.inspectState =
+ stateFilter.value === "" ? undefined : stateFilter.value;
+ qualityInspectListPage({ ...params, inspectType: currentInspectType })
.then(res => {
tableLoading.value = false;
inspectionList.value = res.data.records || [];
@@ -429,25 +633,31 @@
};
// 鎻愪氦妫�楠�
- const submitInspection = async item => {
+ const submitInspection = item => {
if (!item) {
showToast("鍙傛暟閿欒");
return;
}
- try {
- const res = await submitQualityInspect({ id: item.id });
- if (res.code === 200) {
- showToast("鎻愪氦鎴愬姛");
- setTimeout(() => {
- getList();
- }, 1000);
- } else {
- showToast("鎻愪氦澶辫触锛�" + (res.msg || "鏈煡閿欒"));
- }
- } catch (error) {
- console.error("鎻愪氦澶辫触:", error);
- showToast("鎻愪氦澶辫触锛岃閲嶈瘯");
- }
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "鎻愪氦鍚庡皢鐢熸垚涓嶅悎鏍煎鐞嗗崟涓斾笉鍙慨鏀癸紝鏄惁缁х画锛�",
+ success: async res => {
+ if (!res.confirm) return;
+ try {
+ await submitQualityInspect({ id: item.id });
+ // 杩囩▼妫�鎻愪氦涓嶄骇鐢熷叆搴撹褰�
+ showToast(
+ "鎻愪氦鎴愬姛锛屼笉鍚堟牸閮ㄥ垎宸茬敓鎴愬鐞嗗崟锛堣繃绋嬫楠屼笉浜х敓鍏ュ簱璁板綍锛�"
+ );
+ setTimeout(() => {
+ getList();
+ }, 1500);
+ } catch (error) {
+ // request.ts 宸茬粺涓� toast 鍚庣 msg锛岃繖閲屽彧璁板綍
+ console.error("鎻愪氦澶辫触:", error);
+ }
+ },
+ });
};
// 鍒嗛厤妫�楠屽憳
@@ -550,6 +760,78 @@
padding: 5px;
}
+ // 妫�楠岀姸鎬佺瓫閫�
+ .filter-row {
+ display: flex;
+ align-items: center;
+ margin-top: 10px;
+ }
+
+ .filter-item {
+ display: flex;
+ align-items: center;
+ padding: 4px 10px;
+ background: #f2f3f5;
+ border-radius: 12px;
+ }
+
+ .filter-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 4px;
+ }
+
+ .filter-value {
+ font-size: 12px;
+ color: #2979ff;
+ margin-right: 4px;
+ }
+
+ // 涓嶅悎鏍煎鐞嗗脊绐�
+ .unqualified-dialog {
+ background: #ffffff;
+ border-radius: 16px 16px 0 0;
+ padding: 20px;
+ max-height: 70vh;
+ overflow-y: auto;
+ }
+
+ .unqualified-empty {
+ padding: 30px 0;
+ text-align: center;
+ font-size: 13px;
+ color: #999;
+ }
+
+ .unqualified-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .unqualified-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-right: 10px;
+ }
+
+ .unqualified-label {
+ font-size: 12px;
+ color: #999;
+ }
+
+ .unqualified-value {
+ font-size: 13px;
+ color: #333;
+ margin-top: 2px;
+ }
+
+ .unqualified-btn {
+ width: 70px;
+ }
+
// 缁熻鍗$墖
.stats-cards {
display: flex;
diff --git a/src/pages/works.vue b/src/pages/works.vue
index 9e89ddf..60e1a9a 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -391,6 +391,10 @@
icon: "/static/images/icon/gongyingshangdangan.svg",
label: "渚涘簲鍟嗘。妗�",
},
+ {
+ icon: "/static/images/icon/caigouguanli.svg",
+ label: "閲囪喘闇�姹�",
+ },
]);
// 璐㈠姟绠$悊鍔熻兘鏁版嵁
@@ -470,6 +474,10 @@
{
icon: "/static/images/icon/kucunguanli.svg",
label: "搴撳瓨绠$悊",
+ },
+ {
+ icon: "/static/images/icon/kucunguanli.svg",
+ label: "搴撳瓨鐩樼偣",
},
]);
@@ -647,6 +655,22 @@
icon: "/static/images/icon/shengchanhesuan.svg",
label: "鐢熶骇鏍哥畻",
},
+ {
+ icon: "/static/images/icon/shengchanbaogong.svg",
+ label: "璁′欢鍗曚环閰嶇疆",
+ },
+ {
+ icon: "/static/images/icon/shengchanshikuang.svg",
+ label: "浜ч噺缁熻",
+ },
+ {
+ icon: "/static/images/icon/shengchanhesuan.svg",
+ label: "鏉愭枡娑堣�楁牳绠�",
+ },
+ {
+ icon: "/static/images/icon/shengchanshikuang.svg",
+ label: "鎴愭湰鏍哥畻鍒嗘瀽",
+ },
]);
// 璁惧绠$悊鍔熻兘鏁版嵁
@@ -774,6 +798,11 @@
case "渚涘簲鍟嗘。妗�":
uni.navigateTo({
url: "/pages/basicData/supplierManage/index",
+ });
+ break;
+ case "閲囪喘闇�姹�":
+ uni.navigateTo({
+ url: "/pages/procurementManagement/procurementDemand/index",
});
break;
case "鍏嚭绠$悊":
@@ -924,6 +953,26 @@
url: "/pages/productionManagement/productionAccounting/index",
});
break;
+ case "璁′欢鍗曚环閰嶇疆":
+ uni.navigateTo({
+ url: "/pages/productionManagement/pieceRateConfig/index",
+ });
+ break;
+ case "浜ч噺缁熻":
+ uni.navigateTo({
+ url: "/pages/productionManagement/outputStatistics/index",
+ });
+ break;
+ case "鏉愭枡娑堣�楁牳绠�":
+ uni.navigateTo({
+ url: "/pages/productionManagement/materialConsumption/index",
+ });
+ break;
+ case "鎴愭湰鏍哥畻鍒嗘瀽":
+ uni.navigateTo({
+ url: "/pages/productionManagement/costAccounting/index",
+ });
+ break;
case "鐢熶骇杩芥函":
uni.navigateTo({
url: "/pages/productionManagement/productionTraceability/index",
@@ -1054,6 +1103,11 @@
url: "/pages/inventoryManagement/stockManagement/index",
});
break;
+ case "搴撳瓨鐩樼偣":
+ uni.navigateTo({
+ url: "/pages/inventoryManagement/stockCheck/index",
+ });
+ break;
case "鍙嶉鐧昏":
uni.navigateTo({
url: "/pages/customerService/feedbackRegistration/index",
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 25d4147..737af34 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -1,3 +1,5 @@
+import dayjs from 'dayjs'
+
/**
* 鏄剧ず娑堟伅鎻愮ず妗�
* @param content 鎻愮ず鐨勬爣棰�
@@ -10,6 +12,19 @@
}
/**
+* 鏈懆鏃ユ湡鍖洪棿锛堝懆涓� ~ 浠婂ぉ锛�
+* dayjs 鏈厤缃� locale 鏃朵竴鍛ㄤ粠鍛ㄦ棩绠楄捣锛岃繖閲屾樉寮忔寜鍛ㄤ竴璧风畻
+*/
+export function getWeekRange() {
+ const today = dayjs()
+ const offset = (today.day() + 6) % 7
+ return {
+ startDate: today.subtract(offset, 'day').format('YYYY-MM-DD'),
+ endDate: today.format('YYYY-MM-DD')
+ }
+}
+
+/**
* 鏄剧ず妯℃�佸脊绐�
* @param content 鎻愮ず鐨勬爣棰�
*/
--
Gitblit v1.9.3