| 2026-05-06 | zhangwencui | ![]() |
| 2026-05-06 | zhangwencui | ![]() |
| 2026-05-06 | zhangwencui | ![]() |
| 2026-05-06 | zhangwencui | ![]() |
| 2026-05-06 | zhangwencui | ![]() |
| 2026-05-06 | zhangwencui | ![]() |
| src/api/productionManagement/productionCosting.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/productionManagement/productionOrder.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/productionManagement/productionProductMain.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/productionManagement/workOrder.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/productionManagement/processStatistics/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/productionManagement/productionAccounting/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/productionManagement/productionOrder/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/productionManagement/productionReporting/ledger.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/productionManagement/productionTraceability/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/works.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/productionManagement/productionCosting.js
@@ -8,4 +8,22 @@ method: "get", params: query, }); } // å·¦è¾¹è¡¨æ ¼çæ¥å£ (æ±æ») export function salesLedgerProductionAccountingList(query) { return request({ url: "/productionAccount/listPage", method: "get", params: query, }); } // å³è¾¹è¡¨æ ¼çæ¥å£ (æç») export function salesLedgerProductionAccountingListProductionDetails(query) { return request({ url: "/productionAccount/listProductionDetails", method: "get", params: query, }); } src/api/productionManagement/productionOrder.js
@@ -10,6 +10,15 @@ }); } // çäº§è®¢åæº¯æºè¯¦æ export function getOrderDetail(npsNo) { return request({ url: "/productionOrder/ordeDetail", method: "get", params: { npsNo }, }); } // è·åçäº§è®¢åæ¥æºæ°æ® export function getProductOrderSource(id) { return request({ src/api/productionManagement/productionProductMain.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,29 @@ // ç产æ¥å·¥é¡µé¢æ¥å£ import request from "@/utils/request"; // å页æ¥è¯¢ç产æ¥å·¥ä¸»è¡¨ export function productionProductMainListPage(query) { return request({ url: "/productionProductMain/listPage", method: "get", params: query, }); } // å 餿¥å·¥ export function productionReportDelete(query) { return request({ url: "/productionProductMain/delete", method: "get", params: query, }); } // æ¥è¯¢æå ¥å表 export function productionProductInputListPage(query) { return request({ url: "/productionProductInput/listPage", method: "get", params: query, }); } src/api/productionManagement/workOrder.js
@@ -41,3 +41,12 @@ responseType: "blob", }); } // è·åå·¥åºç»è®¡æ°æ® export function getOperationStatistics(query) { return request({ url: "/productionOperationTask/getOperation", method: "get", params: query, }); } src/pages.json
@@ -824,6 +824,13 @@ } }, { "path": "pages/productionManagement/productionReporting/ledger", "style": { "navigationBarTitleText": "æ¥å·¥å°è´¦", "navigationStyle": "custom" } }, { "path": "pages/productionManagement/workOrder/index", "style": { "navigationBarTitleText": "ç产工å", @@ -851,13 +858,27 @@ "navigationStyle": "custom" } }, // { // "path": "pages/productionManagement/productionCosting/index", // "style": { // "navigationBarTitleText": "çäº§æ ¸ç®", // "navigationStyle": "custom" // } // }, { "path": "pages/productionManagement/productionAccounting/index", "style": { "navigationBarTitleText": "çäº§æ ¸ç®", "navigationStyle": "custom" } }, { "path": "pages/productionManagement/productionTraceability/index", "style": { "navigationBarTitleText": "ç产追溯", "navigationStyle": "custom" } }, { "path": "pages/productionManagement/processStatistics/index", "style": { "navigationBarTitleText": "å·¥åºç产å®åµ", "navigationStyle": "custom" } }, { "path": "pages/inventoryManagement/receiptManagement/index", "style": { src/pages/productionManagement/processStatistics/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,370 @@ <template> <view class="process-statistics"> <PageHeader title="å·¥åºç产å®åµ" @back="goBack" /> <!-- æç´¢åºå --> <view class="search-section"> <view class="date-picker-container" @click="showCalendar = true"> <view class="date-input"> <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> <!-- ç»è®¡å¡çå表 --> <scroll-view scroll-y class="stats-list"> <view v-if="loading" class="loading-box"> <up-loading-icon text="å è½½ä¸..."></up-loading-icon> </view> <view v-else-if="statsData.length > 0" class="card-grid"> <view v-for="(item, index) in statsData" :key="index" class="stats-card"> <view class="card-header"> <text class="process-tag">{{ item.name }}</text> <view class="header-details"> <view class="detail-row"> <text class="label">è®¡åæ°</text> <text class="value">{{ item.planned }}</text> </view> <view class="detail-row"> <text class="label">è¯åæ°</text> <text class="value good">{{ item.good }}</text> </view> <view class="detail-row"> <text class="label">ä¸è¯å</text> <text class="value bad">{{ item.bad }}</text> </view> </view> </view> <view class="card-body"> <view class="main-stat"> <text class="big-number">{{ item.total }}</text> <text class="sub-label">çäº§ä»»å¡æ°</text> </view> </view> <view class="card-footer"> <view class="progress-section"> <view class="progress-header"> <text class="progress-label">ç产è¿åº¦</text> <text class="percentage-text">{{ item.percentage }}%</text> </view> <up-line-progress :percentage="Math.min(item.percentage, 100)" :activeColor="getProgressColor(item.percentage)" :show-text="false" height="8"></up-line-progress> </view> </view> </view> </view> <view v-else class="no-data"> <up-empty mode="data" text="ææ å·¥åºç»è®¡æ°æ®"></up-empty> </view> </scroll-view> <!-- æ¥åéæ©å¨ --> <up-calendar :show="showCalendar" mode="range" :maxDate="maxDate" minDate="2026-01-01" :monthNum="monthNum" @confirm="onDateConfirm" @close="showCalendar = false"></up-calendar> </view> </template> <script setup> import { ref, reactive, onMounted, computed } from "vue"; import { getOperationStatistics } from "@/api/productionManagement/workOrder.js"; import PageHeader from "@/components/PageHeader.vue"; import dayjs from "dayjs"; const loading = ref(false); const showCalendar = ref(false); const dateRange = ref([]); 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 minDate = dayjs().subtract(7, "day").format("YYYY-MM-DD"); const searchForm = reactive({ startDate: "", endDate: "", }); const statsData = ref([]); const dateRangeText = computed(() => { if (searchForm.startDate && searchForm.endDate) { return `${searchForm.startDate} è³ ${searchForm.endDate}`; } return "è¯·éæ©æ¥æåºé´"; }); const goBack = () => { uni.navigateBack(); }; const getProgressColor = percentage => { if (percentage >= 100) return "#67c23a"; if (percentage >= 50) return "#3c9cff"; if (percentage >= 25) return "#e6a23c"; return "#f56c6c"; }; const onDateConfirm = e => { searchForm.startDate = e[0]; searchForm.endDate = e[e.length - 1]; showCalendar.value = false; handleQuery(); }; const getList = () => { loading.value = true; const params = { startDate: searchForm.startDate, endDate: searchForm.endDate, }; getOperationStatistics(params) .then(res => { statsData.value = (res.data || []).map(item => ({ name: item.operationName || "-", total: item.productionTaskCount || 0, planned: item.planQuantity || 0, good: item.goodQuantity || 0, bad: item.scrapQty || 0, percentage: Number(item.completionStatus || 0), })); }) .finally(() => { loading.value = false; }); }; const handleQuery = () => { getList(); }; const handleClearDate = () => { searchForm.startDate = ""; searchForm.endDate = ""; handleQuery(); }; onMounted(() => { // é»è®¤æ¶é´ç½®ç©º searchForm.startDate = ""; searchForm.endDate = ""; getList(); }); </script> <style scoped lang="scss"> @import "@/styles/procurement-common.scss"; .process-statistics { min-height: 100vh; background-color: #f5f7fa; display: flex; flex-direction: column; } .search-section { background-color: #fff; padding: 24rpx 30rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02); } .date-picker-container { display: flex; align-items: center; width: 100%; .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; transition: all 0.3s; &:active { background-color: #ebedf0; } .date-text { font-size: 28rpx; color: #303133; margin-left: 16rpx; flex: 1; &.placeholder { color: #c0c4cc; } } .clear-icon { padding: 10rpx; margin-right: -10rpx; } } .search-btn-wrapper { width: 140rpx; } } .stats-list { flex: 1; height: 0; padding: 0 24rpx 40rpx; } .loading-box { display: flex; justify-content: center; padding-top: 100rpx; } .card-grid { display: flex; flex-direction: column; gap: 24rpx; } .stats-card { background: #fff; border-radius: 16rpx; padding: 24rpx; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05); .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 30rpx; .process-tag { background-color: #e6f7ff; color: #1890ff; padding: 6rpx 16rpx; border-radius: 8rpx; font-size: 26rpx; font-weight: bold; } .header-details { display: flex; flex-direction: column; gap: 4rpx; .detail-row { display: flex; justify-content: flex-end; align-items: center; gap: 12rpx; .label { font-size: 22rpx; color: #999; } .value { font-size: 24rpx; color: #333; font-weight: bold; min-width: 60rpx; text-align: right; &.good { color: #52c41a; } &.bad { color: #f56c6c; } } } } } .card-body { padding-bottom: 30rpx; border-bottom: 1rpx solid #f0f0f0; .main-stat { display: flex; flex-direction: column; align-items: center; .big-number { font-size: 56rpx; font-weight: bold; color: #333; line-height: 1; } .sub-label { font-size: 26rpx; color: #666; margin-top: 12rpx; } } } .card-footer { padding-top: 24rpx; .progress-section { .progress-header { display: flex; justify-content: space-between; margin-bottom: 12rpx; .progress-label { font-size: 24rpx; color: #999; } .percentage-text { font-size: 24rpx; font-weight: bold; color: #333; } } } } } .no-data { padding-top: 100rpx; } </style> src/pages/productionManagement/productionAccounting/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,498 @@ <template> <view class="production-accounting"> <PageHeader title="çäº§æ ¸ç®" @back="goBack" /> <!-- çéåºå --> <view class="filter-section"> <view class="date-type-selector"> <up-tabs :list="dateTypeList" :current="currentDateTypeIndex" @change="handleDateTypeChange" :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }" lineWidth="30" lineHeight="3" /> </view> <view class="date-picker-bar" @click="showDatePicker = true"> <view class="date-display"> <up-icon name="calendar" size="20" color="#2979ff"></up-icon> <text class="date-text">{{ dateDisplayText }}</text> </view> <up-icon name="arrow-right" size="16" color="#999"></up-icon> </view> </view> <!-- æ±æ»å表 --> <view class="summary-section" v-if="!showDetail"> <view class="section-header"> <text class="section-title">çäº§äººåæ±æ»</text> </view> <view class="ledger-list" v-if="summaryList.length > 0"> <view v-for="(item, index) in summaryList" :key="index" class="ledger-item" @click="handleRowClick(item)"> <view class="item-header"> <view class="item-left"> <view class="user-icon"> <up-icon name="account" size="16" color="#ffffff"></up-icon> </view> <text class="item-id">{{ item.schedulingUserName || 'æªç¥' }}</text> </view> <view class="item-right"> <up-icon name="arrow-right" size="16" color="#999"></up-icon> </view> </view> <up-divider></up-divider> <view class="item-details"> <view class="detail-grid"> <view class="grid-item"> <text class="grid-label">产é</text> <text class="grid-value">{{ item.finishedNum || 0 }}</text> </view> <view class="grid-item"> <text class="grid-label">å·¥èµ</text> <text class="grid-value highlight">Â¥{{ item.wages || 0 }}</text> </view> <view class="grid-item"> <text class="grid-label">åæ ¼ç</text> <text class="grid-value">{{ formatOutputRate(item.outputRate) }}</text> </view> </view> </view> </view> </view> <view v-else class="no-data"> <up-empty mode="data" text="ææ æ±æ»æ°æ®" /> </view> </view> <!-- æç»å表 (ç¹å»æ±æ»è¡åæ¾ç¤º) --> <view class="detail-section" v-else> <view class="section-header back-bar" @click="showDetail = false"> <up-icon name="arrow-left" size="16" color="#2979ff"></up-icon> <text class="back-text">è¿åæ±æ» ({{ currentUserName }})</text> </view> <view class="ledger-list" v-if="detailList.length > 0"> <view v-for="(item, index) in detailList" :key="index" class="ledger-item no-click"> <view class="item-header"> <view class="item-left"> <view class="product-icon"> <up-icon name="shopping-cart" size="16" color="#ffffff"></up-icon> </view> <text class="item-id">{{ item.productName }}</text> </view> <view class="item-tag"> <text class="tag-text">{{ item.schedulingDate }}</text> </view> </view> <up-divider></up-divider> <view class="item-details"> <view class="detail-row"> <text class="detail-label">è§æ ¼åå·</text> <text class="detail-value">{{ item.productModelName }}</text> </view> <view class="detail-row"> <text class="detail-label">å·¥åº</text> <text class="detail-value">{{ item.process }}</text> </view> <view class="detail-row"> <text class="detail-label">ç产æ°é</text> <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text> </view> <view class="detail-row"> <text class="detail-label">å·¥æ¶å®é¢</text> <text class="detail-value">{{ item.workHours }}</text> </view> <view class="detail-row"> <text class="detail-label">å·¥èµ</text> <text class="detail-value highlight">Â¥{{ item.wages }}</text> </view> </view> </view> <up-loadmore :status="loadStatus" @loadmore="getDetailList" /> </view> <view v-else class="no-data"> <up-empty mode="data" text="ææ æç»æ°æ®" /> </view> </view> <!-- æ¥æéæ©å¨ --> <up-datetime-picker :show="showDatePicker" v-model="pickerValue" :mode="currentDateType === 'day' ? 'date' : 'year-month'" @confirm="handleDateConfirm" @cancel="showDatePicker = false" /> </view> </template> <script setup> import { ref, reactive, computed, onMounted } from "vue"; import { salesLedgerProductionAccountingList, salesLedgerProductionAccountingListProductionDetails, } from "@/api/productionManagement/productionCosting"; import PageHeader from "@/components/PageHeader.vue"; import dayjs from "dayjs"; // çéç¸å ³ const dateTypeList = [{ name: "æ¥" }, { name: "æ" }]; const currentDateTypeIndex = ref(0); const currentDateType = computed(() => currentDateTypeIndex.value === 0 ? "day" : "month" ); const showDatePicker = ref(false); const pickerValue = ref(Date.now()); const selectedDate = ref(dayjs().format("YYYY-MM-DD")); const dateDisplayText = computed(() => { return currentDateType.value === "day" ? selectedDate.value : dayjs(selectedDate.value).format("YYYY-MM"); }); // æ°æ®ç¸å ³ const summaryList = ref([]); const detailList = ref([]); const showDetail = ref(false); const currentUserName = ref(""); const loadStatus = ref("loadmore"); const page = reactive({ current: 1, size: 20, total: 0, }); const page1 = reactive({ current: 1, size: 20, total: 0, }); // è¿åä¸ä¸é¡µ const goBack = () => { uni.navigateBack(); }; // åæ¢æ¥æç±»å const handleDateTypeChange = index => { currentDateTypeIndex.value = index.index; if (currentDateType.value === "day") { selectedDate.value = dayjs().format("YYYY-MM-DD"); } else { selectedDate.value = dayjs().startOf("month").format("YYYY-MM-DD"); } reloadData(); }; // ç¡®è®¤æ¥æéæ© const handleDateConfirm = e => { selectedDate.value = dayjs(e.value).format("YYYY-MM-DD"); showDatePicker.value = false; reloadData(); }; // æ ¼å¼ååæ ¼ç const formatOutputRate = val => { if (val == null || val === "") return "-"; return parseFloat(val).toFixed(2) + "%"; }; // å è½½æ±æ»å表 const getSummaryList = () => { uni.showLoading({ title: "å è½½ä¸..." }); const params = { dateType: currentDateType.value, entryDate: currentDateType.value === "day" ? selectedDate.value : undefined, entryDateStart: currentDateType.value === "month" ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD") : undefined, entryDateEnd: currentDateType.value === "month" ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD") : undefined, pageNum: page.current, pageSize: page.size, }; salesLedgerProductionAccountingList(params) .then(res => { summaryList.value = res.data.records || []; page.total = res.data.total || 0; }) .finally(() => { uni.hideLoading(); }); }; // å è½½æç»å表 const getDetailList = (isLoadMore = false) => { if (!isLoadMore) { page1.current = 1; detailList.value = []; } loadStatus.value = "loading"; const params = { schedulingUserName: currentUserName.value, dateType: currentDateType.value, entryDate: currentDateType.value === "day" ? selectedDate.value : undefined, entryDateStart: currentDateType.value === "month" ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD") : undefined, entryDateEnd: currentDateType.value === "month" ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD") : undefined, pageNum: page1.current, pageSize: page1.size, }; salesLedgerProductionAccountingListProductionDetails(params) .then(res => { const records = res.data.records || []; detailList.value = isLoadMore ? [...detailList.value, ...records] : records; page1.total = res.data.total || 0; if (detailList.value.length >= page1.total) { loadStatus.value = "nomore"; } else { loadStatus.value = "loadmore"; page1.current++; } }) .catch(() => { loadStatus.value = "loadmore"; }); }; // ç¹å»æ±æ»è¡ const handleRowClick = item => { currentUserName.value = item.schedulingUserName; showDetail.value = true; getDetailList(); }; // éæ°å è½½æ°æ® const reloadData = () => { page.current = 1; showDetail.value = false; getSummaryList(); }; onMounted(() => { getSummaryList(); }); </script> <style scoped lang="scss"> .production-accounting { background-color: #f5f7fa; min-height: 100vh; padding-bottom: 30rpx; .filter-section { background-color: #ffffff; padding: 20rpx 30rpx; margin-bottom: 20rpx; .date-type-selector { margin-bottom: 20rpx; } .date-picker-bar { display: flex; justify-content: space-between; align-items: center; background-color: #f0f4ff; padding: 16rpx 24rpx; border-radius: 8rpx; .date-display { display: flex; align-items: center; gap: 12rpx; .date-text { font-size: 28rpx; color: #2979ff; font-weight: bold; } } } } .section-header { padding: 20rpx 30rpx; .section-title { font-size: 30rpx; font-weight: bold; color: #333; border-left: 8rpx solid #2979ff; padding-left: 16rpx; } &.back-bar { display: flex; align-items: center; gap: 10rpx; background-color: #ffffff; margin-bottom: 20rpx; .back-text { font-size: 28rpx; color: #2979ff; } } } .ledger-list { padding: 0 20rpx; .ledger-item { background-color: #ffffff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); &:active { background-color: #f9f9f9; } &.no-click:active { background-color: #ffffff; } .item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; .item-left { display: flex; align-items: center; gap: 12rpx; .user-icon, .product-icon { width: 48rpx; height: 48rpx; background: linear-gradient(135deg, #2979ff, #64a1ff); border-radius: 8rpx; display: flex; align-items: center; justify-content: center; } .item-id { font-size: 28rpx; font-weight: bold; color: #333; } } .item-tag { background-color: #f0f4ff; padding: 4rpx 12rpx; border-radius: 4rpx; .tag-text { font-size: 24rpx; color: #2979ff; } } } .item-details { padding-top: 10rpx; .detail-grid { display: flex; justify-content: space-between; .grid-item { display: flex; flex-direction: column; align-items: center; flex: 1; .grid-label { font-size: 24rpx; color: #999; margin-bottom: 8rpx; } .grid-value { font-size: 28rpx; color: #333; font-weight: 500; &.highlight { color: #ff5a5f; } } } } .detail-row { display: flex; justify-content: space-between; margin-bottom: 12rpx; .detail-label { font-size: 26rpx; color: #999; } .detail-value { font-size: 26rpx; color: #333; &.highlight { color: #ff5a5f; font-weight: bold; } } } } } } .no-data { padding-top: 100rpx; } } </style> src/pages/productionManagement/productionOrder/index.vue
@@ -67,6 +67,31 @@ <text class="progress-text">{{ item.completeQuantity || 0 }} / {{ item.quantity || 0 }}</text> </view> </view> <!-- å·¥åºç产è¿åº¦å±ç¤º --> <view class="detail-row process-row"> <text class="detail-label">å·¥åºè¿åº¦</text> <scroll-view scroll-x class="process-scroll"> <view class="process-container"> <view v-for="(process, pIdx) in item.processRouteStatus" :key="pIdx" class="process-item"> <view class="process-node"> <view class="node-circle" :class="{ 'is-complete': process.percentage >= 100 }"> <text class="node-percentage" :style="{ color: process.percentage >= 100 ? '#52c41a' : (process.percentage >= 70 ? '#f56c6c' : '#3c9cff') }">{{ process.percentage }}%</text> </view> <text class="node-name">{{ process.name }}</text> </view> <view v-if="pIdx < item.processRouteStatus.length - 1" class="node-line"></view> </view> <view v-if="!item.processRouteStatus || !item.processRouteStatus.length" class="no-process">-</view> </view> </scroll-view> </view> <view class="detail-row"> <text class="detail-label">计å宿</text> <text class="detail-value">{{ formatDate(item.planCompleteTime) }}</text> @@ -75,17 +100,22 @@ <view class="item-footer"> <view class="action-btns"> <up-button type="info" size="mini" size="small" plain text="ç产追溯" @click="goTraceability(item)"></up-button> <up-button type="info" size="small" plain text="å·¥èºè·¯çº¿" @click="goProcessRoute(item)"></up-button> <up-button type="primary" size="mini" size="small" plain text="æ¥æº" @click="goSource(item)"></up-button> <up-button type="success" size="mini" size="small" plain text="é¢æè¯¦æ " @click="goPickingDetail(item)"></up-button> @@ -112,6 +142,7 @@ productOrderListPage, getOrderProcessRouteMain, } from "@/api/productionManagement/productionOrder.js"; import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js"; import PageHeader from "@/components/PageHeader.vue"; const { proxy } = getCurrentInstance(); @@ -166,7 +197,7 @@ 2: "warning", 3: "success", 4: "info", 5: "danger", 5: "error", }; return typeMap[status] || "info"; }; @@ -216,16 +247,44 @@ }; productOrderListPage(params) .then(res => { loading.value = false; .then(async res => { const records = res.data.records || []; // 为æ¯ä¸ªè®¢åå¹¶è¡æ¥è¯¢å·¥åºè¿åº¦ const processPromises = records.map(async item => { if (item.npsNo) { try { const workOrderRes = await productWorkOrderPage({ npsNo: item.npsNo, size: 100, }); const workOrders = workOrderRes.data.records || []; const processRouteStatus = workOrders.map(wo => ({ name: wo.operationName || "æªç¥å·¥åº", percentage: Number(wo.completionStatus) > 100 ? 100 : Number(wo.completionStatus || 0), })); return { ...item, processRouteStatus }; } catch (error) { console.error(`è·åå·¥å ${item.npsNo} è¿åº¦å¤±è´¥:`, error); return { ...item, processRouteStatus: [] }; } } return { ...item, processRouteStatus: [] }; }); const updatedRecords = await Promise.all(processPromises); loading.value = false; if (page.current === 1) { tableData.value = records; tableData.value = updatedRecords; } else { tableData.value = [...tableData.value, ...records]; tableData.value = [...tableData.value, ...updatedRecords]; } if (records.length < page.size) { if (updatedRecords.length < page.size) { loadStatus.value = "nomore"; } else { loadStatus.value = "loadmore"; @@ -283,6 +342,13 @@ const goPickingDetail = item => { uni.navigateTo({ url: `/pages/productionManagement/productionOrder/pickingDetail?id=${item.id}&npsNo=${item.npsNo}`, }); }; // 跳转ç产追溯 const goTraceability = item => { uni.navigateTo({ url: `/pages/productionManagement/productionTraceability/index?npsNo=${item.npsNo}`, }); }; @@ -380,6 +446,84 @@ text-align: right; } } &.process-row { flex-direction: column; margin: 20rpx 0; .process-scroll { width: 100%; margin-top: 16rpx; .process-container { display: flex; align-items: flex-start; padding: 10rpx 0; min-height: 120rpx; .process-item { display: flex; align-items: center; .process-node { display: flex; flex-direction: column; align-items: center; width: 100rpx; .node-circle { width: 60rpx; height: 60rpx; border-radius: 50%; border: 2rpx solid #3c9cff; display: flex; align-items: center; justify-content: center; background: #fff; margin-bottom: 8rpx; .node-percentage { font-size: 18rpx; color: #3c9cff; font-weight: bold; } &.is-complete { border-color: #52c41a; background: #f6ffed; .node-percentage { color: #52c41a; } } } .node-name { font-size: 20rpx; color: #666; text-align: center; width: 120rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } .node-line { width: 40rpx; height: 2rpx; background: #e8e8e8; margin: -30rpx 0 0 0; } } .no-process { font-size: 24rpx; color: #ccc; } } } } } } src/pages/productionManagement/productionReporting/ledger.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,412 @@ <template> <view class="reporting-ledger"> <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.keyword" @change="handleQuery" clearable /> </view> <view class="filter-button" @click="handleQuery"> <up-icon name="search" size="24" color="#999"></up-icon> </view> </view> </view> <!-- å表åºå --> <scroll-view scroll-y class="list-container" v-if="tableData.length > 0" @scrolltolower="loadMore"> <view class="ledger-list"> <view v-for="(item, index) in tableData" :key="item.id || index"> <view 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.productNo || '-' }}</text> </view> <view class="item-tag"> <text class="create-time">{{ formatDate(item.createTime) }}</text> </view> </view> <up-divider></up-divider> <view class="item-details"> <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> <view class="detail-value"> <up-tag :text="item.process || '-'" type="primary" size="mini" plain /> </view> </view> <view class="detail-row"> <text class="detail-label">å·¥åç¼å·</text> <text class="detail-value">{{ item.workOrderNo || '-' }}</text> </view> <view class="detail-row"> <text class="detail-label">产ååç§°</text> <text class="detail-value font-bold">{{ item.productName || '-' }}</text> </view> <view class="detail-row"> <text class="detail-label">è§æ ¼åå·</text> <text class="detail-value">{{ item.productModelName || '-' }}</text> </view> <view class="quantity-section"> <view class="qty-item"> <text class="qty-label">äº§åºæ°é</text> <text class="qty-value success">{{ item.quantity || 0 }} {{ item.unit || '' }}</text> </view> <view class="qty-item"> <text class="qty-label">æ¥åºæ°é</text> <text class="qty-value error">{{ item.scrapQty || 0 }}</text> </view> </view> <view class="item-footer"> <view class="action-buttons"> <up-button type="primary" size="small" plain text="æ¥çæå ¥" @click="handleShowInput(item)"></up-button> <up-button type="info" size="small" plain text="åæ°è¯¦æ " @click="handleShowParams(item)"></up-button> <up-button type="error" size="small" plain text="å é¤" @click="handleDelete(item)"></up-button> </view> </view> </view> </view> </view> </view> <up-loadmore :status="loadStatus" v-if="tableData.length >= page.size" /> </scroll-view> <view v-else class="empty-state"> <up-empty mode="data" text="ææ æ¥å·¥å°è´¦æ°æ®"></up-empty> </view> <!-- æå ¥è¯¦æ å¼¹çª --> <up-modal :show="inputVisible" title="æå ¥è¯¦æ " @confirm="inputVisible = false"> <view class="modal-content scroll-view"> <view v-if="inputList.length > 0"> <view v-for="(input, idx) in inputList" :key="idx" class="detail-item"> <view class="detail-row"> <text class="detail-label">æå ¥äº§å</text> <text class="detail-value font-bold">{{ input.productName }}</text> </view> <view class="detail-row"> <text class="detail-label">è§æ ¼åå·</text> <text class="detail-value">{{ input.model }}</text> </view> <view class="detail-row"> <text class="detail-label">æå ¥æ°é</text> <text class="detail-value highlight">{{ input.quantity }} {{ input.unit }}</text> </view> <up-divider></up-divider> </view> </view> <up-empty v-else mode="data" text="ææ æå ¥æ°æ®" /> </view> </up-modal> <!-- åæ°è¯¦æ å¼¹çª --> <up-modal :show="paramsVisible" title="åæ°è¯¦æ " @confirm="paramsVisible = false"> <view class="modal-content"> <view v-if="currentParams.length > 0"> <view v-for="(param, idx) in currentParams" :key="idx" class="detail-row"> <text class="detail-label">{{ param.paramName }}</text> <text class="detail-value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? '(' + param.unit + ')' : '' }}</text> </view> </view> <up-empty v-else mode="data" text="ææ åæ°æ°æ®" /> </view> </up-modal> </view> </template> <script setup> import { ref, reactive, onMounted } from "vue"; import dayjs from "dayjs"; import { productionProductMainListPage, productionReportDelete, productionProductInputListPage, } from "@/api/productionManagement/productionProductMain.js"; import PageHeader from "@/components/PageHeader.vue"; import modal from "@/plugins/modal"; const tableData = ref([]); const loading = ref(false); const loadStatus = ref("loadmore"); const page = reactive({ current: 1, size: 10, total: 0, }); const searchForm = reactive({ keyword: "", }); // æå ¥è¯¦æ ç¸å ³ const inputVisible = ref(false); const inputList = ref([]); // åæ°è¯¦æ ç¸å ³ const paramsVisible = ref(false); const currentParams = ref([]); const goBack = () => { uni.navigateBack(); }; const formatDate = date => { return date ? dayjs(date).format("YYYY-MM-DD HH:mm") : "-"; }; const handleQuery = () => { page.current = 1; tableData.value = []; getList(); }; const loadMore = () => { if (loadStatus.value === "nomore" || loading.value) return; page.current++; getList(); }; const getList = () => { loading.value = true; loadStatus.value = "loading"; const params = { current: page.current, size: page.size, workOrderNo: searchForm.keyword, }; productionProductMainListPage(params) .then(res => { loading.value = false; const records = res.data.records || []; if (page.current === 1) { tableData.value = records; } else { tableData.value = [...tableData.value, ...records]; } if (records.length < page.size) { loadStatus.value = "nomore"; } else { loadStatus.value = "loadmore"; } page.total = res.data.total || 0; }) .catch(() => { loading.value = false; loadStatus.value = "loadmore"; modal.msgError("å 载失败"); }); }; const handleShowInput = item => { modal.loading("å è½½ä¸..."); productionProductInputListPage({ productMainId: item.id, current: 1, size: 100, }) .then(res => { modal.closeLoading(); inputList.value = res.data.records || []; inputVisible.value = true; }) .catch(() => { modal.closeLoading(); modal.msgError("å è½½æå ¥æ°æ®å¤±è´¥"); }); }; const handleShowParams = item => { currentParams.value = item.productionOperationParamList || []; paramsVisible.value = true; }; const handleDelete = item => { uni.showModal({ title: "æç¤º", content: "ç¡®å®è¦å é¤è¯¥æ¥å·¥è®°å½åï¼", success: res => { if (res.confirm) { productionReportDelete({ id: item.id }).then(res => { if (res.code === 200) { modal.msgSuccess("å 餿å"); handleQuery(); } else { modal.msgError(res.msg || "å é¤å¤±è´¥"); } }); } }, }); }; onMounted(() => { getList(); }); </script> <style scoped lang="scss"> @import "@/styles/procurement-common.scss"; .reporting-ledger { min-height: 100vh; background-color: #f8f9fa; display: flex; flex-direction: column; } .list-container { flex: 1; height: 0; } .ledger-item { .item-header { .item-tag { .create-time { font-size: 24rpx; color: #999; } } } .item-details { .quantity-section { display: flex; background-color: #f9f9f9; border-radius: 8rpx; padding: 20rpx; margin: 20rpx 0; .qty-item { flex: 1; display: flex; flex-direction: column; align-items: center; &:first-child { border-right: 1rpx solid #eee; } .qty-label { font-size: 24rpx; color: #999; margin-bottom: 8rpx; } .qty-value { font-size: 32rpx; font-weight: bold; &.success { color: #52c41a; } &.error { color: #f56c6c; } } } } .item-footer { padding-top: 20rpx; border-top: 1rpx solid #f0f0f0; .action-buttons { display: flex; justify-content: flex-end; gap: 20rpx; } } } } .modal-content { width: 100%; padding: 20rpx 0; max-height: 60vh; overflow-y: auto; .detail-item { padding-bottom: 20rpx; } .detail-row { display: flex; justify-content: space-between; margin-bottom: 16rpx; .detail-label { font-size: 26rpx; color: #999; } .detail-value { font-size: 26rpx; color: #333; &.font-bold { font-weight: bold; } &.highlight { color: #3c9cff; } } } } .empty-state { padding-top: 200rpx; } </style> src/pages/productionManagement/productionTraceability/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,947 @@ <template> <view class="production-traceability"> <PageHeader title="ç产追溯" @back="goBack" /> <!-- æç´¢åºå --> <view class="search-section"> <view class="search-bar" @click="openNpsNoSelector"> <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" color="#999"></up-icon> </view> </view> </view> <!-- å 容åºå --> <view class="content-container" v-if="rowData.productionOrderDto"> <!-- åºç¡ä¿¡æ¯ --> <view class="info-card"> <view class="card-title">åºç¡ä¿¡æ¯</view> <view class="info-grid"> <view class="info-item"> <text class="label">ç产订åå·</text> <text class="value">{{ rowData.productionOrderDto.npsNo || '-' }}</text> </view> <view class="info-item"> <text class="label">产ååç§°</text> <text class="value">{{ rowData.productionOrderDto.productName || '-' }}</text> </view> <view class="info-item"> <text class="label">产åè§æ ¼</text> <text class="value">{{ rowData.productionOrderDto.model || '-' }}</text> </view> <view class="info-item"> <text class="label">è®¡åæ°é</text> <text class="value">{{ rowData.productionOrderDto.quantity || 0 }} {{ rowData.productionOrderDto.unit || '' }}</text> </view> <view class="info-item"> <text class="label">å½åç¶æ</text> <up-tag :text="getStatusText(rowData.productionOrderDto.status)" style="width:100rpx" :type="getStatusType(rowData.productionOrderDto.status)" size="mini" /> </view> <view class="info-item"> <text class="label">客æ·åç§°</text> <text class="value">{{ rowData.productionOrderDto.customerName || '-' }}</text> </view> <view class="info-item"> <text class="label">å¼å§æ¥æ</text> <text class="value">{{ formatDate(rowData.productionOrderDto.startTime) }}</text> </view> <view class="info-item full-width"> <text class="label">宿è¿åº¦</text> <view class="progress-container"> <up-line-progress :percentage="formatProgress(rowData.productionOrderDto.completionStatus)" :activeColor="progressColor(rowData.productionOrderDto.completionStatus)" height="8"></up-line-progress> <text class="progress-text">{{ formatProgress(rowData.productionOrderDto.completionStatus) }}%</text> </view> </view> </view> </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.planQuantity }} / {{ item.workOrder.completeQuantity }}</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> </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.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="input-item"> <view class="input-row"><text class="label">ç©æåç§°ï¼</text><text class="value">{{ item.materialName }}</text></view> <view class="input-row"><text class="label">è§æ ¼åå·ï¼</text><text class="value">{{ item.model }}</text></view> <view class="input-row"><text class="label">æå ¥æ°éï¼</text><text class="value">{{ item.quantity }} {{ item.unit }}</text></view> <view class="input-row"><text class="label">æ¹æ¬¡å·ï¼</text><text class="value">{{ item.batchNo }}</text></view> </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> <view class="params-table"> <view class="table-header"> <text class="col">ææ </text> <text class="col">æ åå¼</text> <text class="col">å®é å¼</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.standardValue }}</text> <text class="col" :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text> </view> </view> </view> <view v-if="!qualityRecords || qualityRecords.length === 0" class="no-data-minor">ææ è´¨æ£è®°å½</view> </scroll-view> </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> </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"; // éæ©å¨ç¸å ³ const showNpsNoSelector = ref(false); const npsNoQuery = ref(""); const npsNoOptions = ref([]); const npsNoLoading = ref(false); const selectedNpsNo = ref(null); const selectedNpsNoLabel = ref(""); const rowData = reactive({ productionOrderDto: null, productionRecords: [], }); // æ¥å·¥è¯¦æ const reportPopupVisible = ref(false); const detailData = ref({ workOrder: {}, reports: [] }); // æå ¥è¯¦æ const inputPopupVisible = ref(false); const inputListData = ref([]); const inputLoading = ref(false); // è´¨æ£è¯¦æ const qualityPopupVisible = ref(false); const qualityRecords = ref([]); // åæ°è¯¦æ const paramModalVisible = ref(false); const currentParams = ref([]); const goBack = () => { uni.navigateBack(); }; const openNpsNoSelector = () => { showNpsNoSelector.value = true; if (npsNoOptions.value.length === 0) { handleNpsNoSearch(); } }; const handleNpsNoSearch = async () => { npsNoLoading.value = true; try { const res = await productOrderListPage({ npsNo: npsNoQuery.value || "", pageNum: 1, pageSize: 50, }); 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(); } }; onLoad(async options => { if (options.npsNo) { uni.showLoading({ title: "å è½½ä¸..." }); try { const res = await productOrderListPage({ npsNo: options.npsNo, pageNum: 1, pageSize: 10, }); 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(); } } }); 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, 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, userName: i.reportMain?.userName || "-", inspectParamList: i.inspectParamList || [], })); qualityPopupVisible.value = true; }; const showParams = params => { currentParams.value = params || []; paramModalVisible.value = true; }; </script> <style scoped lang="scss"> @import "@/styles/procurement-common.scss"; .production-traceability { min-height: 100vh; background-color: #f5f7fa; } .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; .search-input { flex: 1; display: flex; align-items: center; .placeholder { font-size: 28rpx; color: #999; } .value { font-size: 28rpx; color: #333; font-weight: 500; } } .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-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; color: #999; font-size: 26rpx; } } .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; 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; } } .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; } } .popup-scroll { flex: 1; overflow: hidden; } } .detail-info { background: #f8f9fa; padding: 24rpx; border-radius: 16rpx; margin-bottom: 30rpx; flex-direction: column; .info-row { 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; color: #999; font-size: 28rpx; } </style> src/pages/works.vue
@@ -589,14 +589,26 @@ icon: "/static/images/icon/shengchanbaogong.svg", label: "ç产æ¥å·¥", }, // { // icon: "/static/images/icon/shengchanbaogong.svg", // label: "ç产工å", // }, // { // icon: "/static/images/icon/shengchanhesuan@2x.svg", // label: "çäº§æ ¸ç®", // }, { icon: "/static/images/icon/shengchanbaogong.svg", label: "æ¥å·¥å°è´¦", }, { icon: "/static/images/icon/shengchanbaogong.svg", label: "çäº§æ ¸ç®", }, { icon: "/static/images/icon/shengchanbaogong.svg", label: "ç产追溯", }, { icon: "/static/images/icon/shengchanbaogong.svg", label: "å·¥åºç产å®åµ", }, { icon: "/static/images/icon/shengchanbaogong.svg", label: "å·¥åºå®åµ", }, ]); // 设å¤ç®¡çåè½æ°æ® @@ -860,11 +872,26 @@ case "ç产æ¥å·¥": getcode(); break; case "æ¥å·¥å°è´¦": uni.navigateTo({ url: "/pages/productionManagement/productionReporting/ledger", }); break; case "çäº§æ ¸ç®": uni.navigateTo({ url: "/pages/productionManagement/productionAccounting/index", }); break; case "ç产追溯": uni.navigateTo({ url: "/pages/productionManagement/productionTraceability/index", }); break; case "å·¥åºç产å®åµ": uni.navigateTo({ url: "/pages/productionManagement/processStatistics/index", }); break; case "设å¤å°è´¦": uni.navigateTo({ url: "/pages/equipmentManagement/ledger/index",