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 }) } 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", }); }; // çç¹æç»å页ï¼onlyDiff=true åªçå·²å½å ¥ä¸æå·®å¼çè¡ï¼ export const detailPage = params => { return request({ url: "/stockCheck/detailPage", method: "get", params, }); }; // æ¹éå½å ¥å®çï¼body 为æ°ç»ï¼ 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, }); }; 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, }); }; 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 }ï¼ids æ¯æéå·åéçå¤ä¸ª 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, }); }; src/api/productionManagement/bom.js
@@ -80,3 +80,32 @@ method: "delete", }); } // --- BOM çæ¬å¯ç¨/åç¨ãé½å¥åæ --- // BOM å¯ç¨/åç¨ï¼status: 1å¯ç¨ 0åç¨ï¼å¯ç¨æ¶å端èªå¨åç¨åè§æ ¼å ¶å®çæ¬ï¼ export function changeStatus(data) { return request({ url: "/technologyBom/changeStatus", method: "put", data: data, }); } // BOM é½å¥åæï¼data: { 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, }); } 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, }); } 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, }); } 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, }); }; // æ¹éå é¤ï¼data 为 id æ°ç» export const batchDelete = data => { return request({ url: "/pieceRateConfig/batchDelete", method: "delete", data, }); }; src/api/productionManagement/productionProductMain.js
@@ -27,3 +27,21 @@ params: query, }); } // è®¡ä»¶å·¥èµæ±æ»ï¼æäººåï¼ export function wageSummary(query) { return request({ url: "/productionProductMain/wageSummary", method: "get", params: query, }); } // 产éç»è®¡ï¼dimension: device/operation/productï¼ export function outputStat(query) { return request({ url: "/productionProductMain/outputStat", method: "get", params: query, }); } 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, }) } src/api/qualityManagement/productionTrace.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,28 @@ import request from "@/utils/request"; // æ£åè¿½æº¯ï¼æåæ¹å· -> å ¨é¾è·¯ 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, }); }; 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: { // åºç¨åç§° 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": "è§ç¨ä¸èµè´¨", 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> 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ï¼disabled å ç´ ä¼åæç¹å»äºä»¶ï¼ // æ 让 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> 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> 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 || "æ æ¥æºBOM" }}</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> 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; 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)}`, }); }; // å¯ç¨/åç¨ï¼status=1 æ¶å端èªå¨åç¨åè§æ ¼å ¶å®çæ¬ const toggleStatus = item => { const enabled = isEnabled(item); const nextStatus = enabled ? 0 : 1; const actionText = enabled ? "åç¨" : "å¯ç¨"; uni.showModal({ title: "æç¤º", content: `ç¡®å®${actionText}该BOMçæ¬åï¼`, 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> 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); // æç»ï¼æ¥å£è¿åå ¨éæ°ç»ï¼åç«¯åæ¹æ¸²æï¼ 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> 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> 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(); } }; // 维度ï¼device / 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> 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> 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">å·¥èµå£å¾</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> 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, }; 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 { 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')">æ£å追溯</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> <!-- ================= æ£å追溯 ================= --> <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(""); // å½åæ¹åï¼forward æ£å / 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(); }; // 跳转æ£å追溯并åå¡«æåæ¹å· 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> 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("æäº¤æå"); // æ°å¢ä» ä¿åèç¨¿ï¼æ£å¼æäº¤å¨å表页ãæäº¤ãæé®ï¼ä¼çæå ¥åºè®°å½ï¼ 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; } 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> 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; 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("æäº¤æå"); // æ°å¢ä» ä¿åèç¨¿ï¼æ£å¼æäº¤å¨å表页ãæäº¤ãæé®ï¼ä¼çæå ¥åºè®°å½ï¼ 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; 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> 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; 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("æäº¤æå"); // æ°å¢ä» ä¿åèç¨¿ï¼æ£å¼æäº¤å¨å表页ãæäº¤ãæé® 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; } 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> 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; 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", 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 æç¤ºçæ é¢ */