| src/api/productionManagement/workOrder.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/processStatistics/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/productionOrder/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/workOrderEdit/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/workOrderManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/productionManagement/workOrder.js
@@ -86,3 +86,12 @@ data, }); } // è·åå·¥åºç»è®¡æ°æ® export function getOperationStatistics(query) { return request({ url: "/productionOperationTask/getOperation", method: "get", params: query, }); } src/views/productionManagement/processStatistics/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,268 @@ <template> <div class="app-container"> <div class="search-bar"> <el-form :model="searchForm" inline> <el-form-item label="æ¥æåºé´:"> <el-date-picker v-model="searchForm.dateRange" type="daterange" range-separator="è³" start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ" value-format="YYYY-MM-DD" style="width: 240px" /> </el-form-item> <el-form-item> <el-button type="primary" icon="Search" @click="handleQuery">æç´¢</el-button> </el-form-item> </el-form> </div> <div class="stats-grid" v-loading="loading"> <el-row :gutter="16" v-if="statsData.length > 0"> <el-col v-for="(item, index) in statsData" :key="index" :xs="24" :sm="12" :md="8" :lg="4.8" :xl="4.8" class="mb-16"> <div class="stats-card"> <div class="card-header"> <span class="process-name">{{ item.name }}</span> <div class="header-stats"> <div class="stat-row"> <span class="label">è®¡åæ°</span> <span class="value">{{ item.planned }}</span> </div> <div class="stat-row"> <span class="label">è¯åæ°</span> <span class="value">{{ item.good }}</span> </div> <div class="stat-row"> <span class="label">ä¸è¯åæ°</span> <span class="value">{{ item.bad }}</span> </div> </div> </div> <div class="card-body"> <div class="main-stat"> <div class="big-number">{{ item.total }}</div> <div class="sub-label">çäº§ä»»å¡æ°</div> </div> </div> <div class="card-footer"> <div class="progress-info"> <span class="progress-label">è¿åº¦:</span> <el-progress :percentage="Math.min(item.percentage, 100)" :color="getProgressColor(item.percentage)" :stroke-width="10" :show-text="false" class="flex-1" /> <span class="percentage-text">{{ item.percentage }}%</span> </div> </div> </div> </el-col> </el-row> <el-empty v-else description="ææ æ°æ®" /> </div> </div> </template> <script setup> import { reactive, ref, onMounted } from "vue"; import dayjs from "dayjs"; import { getOperationStatistics } from "@/api/productionManagement/workOrder.js"; const loading = ref(false); const searchForm = reactive({ dateRange: [], }); const statsData = ref([]); const getProgressColor = percentage => { if (percentage >= 100) return "#67c23a"; if (percentage >= 50) return "#409eff"; if (percentage >= 25) return "#e6a23c"; return "red"; }; const getList = () => { loading.value = true; const params = { startDate: searchForm.dateRange?.[0] || "", endDate: searchForm.dateRange?.[1] || "", }; 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(); }; onMounted(() => { getList(); }); </script> <style scoped lang="scss"> .app-container { padding: 20px; background-color: #f0f2f5; min-height: calc(100vh - 84px); } .search-bar { background: #fff; padding: 15px 20px 0; border-radius: 4px; margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); } .mb-16 { margin-bottom: 16px; } // 模æ lg="4.8" å 为 element 䏿¯æ 24/5 @media only screen and (min-width: 1200px) { .el-col-lg-4-8 { width: 20%; max-width: 20%; flex: 0 0 20%; } } .stats-card { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); transition: transform 0.3s; &:hover { transform: translateY(-2px); } .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; .process-name { background-color: #e6f7ff; color: #1890ff; padding: 2px 8px; border-radius: 4px; font-size: 14px; font-weight: 500; } .header-stats { text-align: right; .stat-row { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-bottom: 2px; .label { font-size: 12px; color: #909399; } .value { font-size: 13px; color: #303133; font-weight: bold; min-width: 24px; } } } } .card-body { padding: 10px 0; .main-stat { .big-number { font-size: 28px; font-weight: bold; color: #303133; line-height: 1; } .sub-label { font-size: 14px; color: #606266; margin-top: 8px; font-weight: 500; } } } .card-footer { margin-top: 16px; .progress-info { display: flex; align-items: center; gap: 8px; .progress-label { font-size: 12px; color: #909399; white-space: nowrap; } .flex-1 { flex: 1; } .percentage-text { font-size: 12px; color: #606266; min-width: 45px; text-align: right; } } } } // ä¿®æ£ el-col å¸å±éé 5 å :deep(.el-row) { display: flex; flex-wrap: wrap; } @media only screen and (min-width: 1200px) { .el-col-lg-4\.8 { flex: 0 0 20%; max-width: 20%; } } </style> src/views/productionManagement/productionOrder/index.vue
@@ -75,6 +75,26 @@ :color="progressColor(toProgressPercentage(row?.completionStatus))" :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> </template> <template #processRouteStatus="{ row }"> <div v-if="row.processRouteStatus && row.processRouteStatus.length" class="process-progress-container"> <div v-for="(item, index) in row.processRouteStatus" :key="index" class="process-step"> <div class="step-content"> <div class="step-circle" :class="{ 'is-completed': item.percentage >= 100 }"> <span class="step-percentage" :style="{ color: item.percentage >= 70 ? item.percentage >= 100 ? '#67c23a' : '#f56c6c' : '#000' }">{{ item.percentage }}%</span> </div> <div class="step-name">{{ item.name }}</div> </div> <div v-if="index < row.processRouteStatus.length - 1" class="step-line"></div> </div> </div> <span v-else>-</span> </template> </PIMTable> </div> <el-dialog v-model="bindRouteDialogVisible" @@ -215,6 +235,7 @@ getProductOrderSource, updateProductOrder, } from "@/api/productionManagement/productionOrder.js"; import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js"; import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js"; import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue"; import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue"; @@ -241,7 +262,17 @@ total: 0, }); const tableColumn = ref([ const processColumnWidth = computed(() => { if (!tableData.value || tableData.value.length === 0) return "200px"; const maxProcesses = Math.max( ...tableData.value.map(row => row.processRouteStatus?.length || 0) ); if (maxProcesses === 0) return "100px"; // æ¯ä¸ªå·¥åºåå 36px + çº¿æ¡ 30px = 66pxï¼é¢å¤å 60px è¾¹è·åæåç©ºé´ return `${maxProcesses * 66 + 60}px`; }); const tableColumn = computed(() => [ { label: "ç产订åå·", prop: "npsNo", @@ -296,6 +327,13 @@ { label: "宿æ°é", prop: "completeQuantity", }, { label: "å·¥åºç产è¿åº¦", prop: "processRouteStatus", dataType: "slot", slot: "processRouteStatus", width: processColumnWidth.value, }, { dataType: "slot", @@ -630,10 +668,35 @@ const params = { ...searchForm.value, ...page }; params.entryDate = undefined; productOrderListPage(params) .then(res => { tableLoading.value = false; tableData.value = res.data.records; .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 || []; // æç §å·¥åºé¡ºåºæåºï¼å¦ææé¡ºåºå段ï¼å设为 orderNum ææè¿å顺åºï¼ // 转æ¢ä¸º processRouteStatus æ ¼å¼ const processRouteStatus = workOrders.map(wo => ({ name: wo.operationName || "æªç¥å·¥åº", percentage: wo.completionStatus > 100 ? 100 : wo.completionStatus, })); return { ...item, processRouteStatus }; } catch (error) { console.error(`è·åå·¥å ${item.npsNo} è¿åº¦å¤±è´¥:`, error); return { ...item, processRouteStatus: [] }; } } return { ...item, processRouteStatus: [] }; }); tableData.value = await Promise.all(processPromises); page.total = res.data.total; tableLoading.value = false; }) .catch(() => { tableLoading.value = false; @@ -813,6 +876,64 @@ .table_list { margin-top: unset; } .process-progress-container { display: inline-flex; align-items: center; padding: 10px 0; white-space: nowrap; .process-step { display: flex; align-items: center; position: relative; .step-content { display: flex; flex-direction: column; align-items: center; z-index: 1; .step-circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #409eff; display: flex; align-items: center; justify-content: center; background-color: #fff; margin-bottom: 4px; .step-percentage { font-size: 11px; font-weight: bold; } &.is-completed { border-color: #67c23a; .step-percentage { color: #67c23a; } } } .step-name { font-size: 12px; color: #606266; white-space: nowrap; } } .step-line { width: 30px; height: 1px; background-color: #dcdfe6; margin: 0 -2px; margin-top: -20px; // åä¸å移以对é½åå¿ } } } </style> <style lang="scss"> .status-cell { src/views/productionManagement/workOrderEdit/index.vue
@@ -13,7 +13,7 @@ </div> <div class="search-item"> <span class="search_title">ç产订åå·ï¼</span> <el-input v-model="searchForm.productOrderNpsNo" <el-input v-model="searchForm.npsNo" style="width: 240px" placeholder="请è¾å ¥" @change="handleQuery" @@ -259,7 +259,7 @@ const data = reactive({ searchForm: { workOrderNo: "", productOrderNpsNo: "", npsNo: "", }, }); const { searchForm } = toRefs(data); src/views/productionManagement/workOrderManagement/index.vue
@@ -13,7 +13,7 @@ </div> <div class="search-item"> <span class="search_title">ç产订åå·ï¼</span> <el-input v-model="searchForm.productOrderNpsNo" <el-input v-model="searchForm.npsNo" style="width: 240px" placeholder="请è¾å ¥" @change="handleQuery" @@ -265,7 +265,9 @@ import QRCode from "qrcode"; import { getCurrentInstance, reactive, toRefs } from "vue"; import MaterialDialog from "./components/MaterialDialog.vue"; const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue")); const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue") ); import useUserStore from "@/store/modules/user"; const { proxy } = getCurrentInstance(); @@ -525,7 +527,7 @@ const data = reactive({ searchForm: { workOrderNo: "", productOrderNpsNo: "", npsNo: "", }, }); const { searchForm } = toRefs(data);