| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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"> |
| | | <el-row :gutter="16"> |
| | | <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="item.percentage" |
| | | :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> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const searchForm = reactive({ |
| | | dateRange: ["2026-01-30", "2026-04-30"], |
| | | }); |
| | | |
| | | const statsData = ref([ |
| | | { name: "è£
é
", total: 100, planned: 90, good: 85, bad: 5, percentage: 100 }, |
| | | { name: "å å·¥", total: 120, planned: 110, good: 105, bad: 5, percentage: 60 }, |
| | | { name: "å
è£
", total: 80, planned: 70, good: 65, bad: 5, percentage: 92.86 }, |
| | | { name: "æ¸
æ´", total: 90, planned: 80, good: 75, bad: 5, percentage: 33.75 }, |
| | | { name: "çæ¥", total: 110, planned: 100, good: 95, bad: 5, percentage: 50 }, |
| | | { name: "æ¶è£
", total: 85, planned: 75, good: 70, bad: 5, percentage: 82.35 }, |
| | | { |
| | | name: "è´¨æ£", |
| | | total: 130, |
| | | planned: 120, |
| | | good: 115, |
| | | bad: 5, |
| | | percentage: 100, |
| | | }, |
| | | { name: "æç£¨", total: 95, planned: 85, good: 80, bad: 5, percentage: 84.21 }, |
| | | { |
| | | name: "忣", |
| | | total: 105, |
| | | planned: 95, |
| | | good: 90, |
| | | bad: 5, |
| | | percentage: 55.71, |
| | | }, |
| | | { |
| | | name: "å·æ¼", |
| | | total: 120, |
| | | planned: 110, |
| | | good: 105, |
| | | bad: 5, |
| | | percentage: 77.5, |
| | | }, |
| | | { name: "ç»è£
", total: 100, planned: 90, good: 85, bad: 5, percentage: 25 }, |
| | | { |
| | | name: "æ¸
æ´", |
| | | total: 105, |
| | | planned: 95, |
| | | good: 90, |
| | | bad: 5, |
| | | percentage: 15.71, |
| | | }, |
| | | { |
| | | name: "廿²¹", |
| | | total: 125, |
| | | planned: 115, |
| | | good: 110, |
| | | bad: 5, |
| | | percentage: 100, |
| | | }, |
| | | { |
| | | name: "é
¸æ´", |
| | | total: 130, |
| | | planned: 120, |
| | | good: 115, |
| | | bad: 5, |
| | | percentage: 78.46, |
| | | }, |
| | | { |
| | | name: "ç»çº¿", |
| | | total: 140, |
| | | planned: 130, |
| | | good: 125, |
| | | bad: 5, |
| | | percentage: 89.29, |
| | | }, |
| | | ]); |
| | | |
| | | const getProgressColor = percentage => { |
| | | if (percentage >= 100) return "#67c23a"; |
| | | return "#409eff"; |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | console.log("Query with:", searchForm); |
| | | // è¿éå¯ä»¥æ·»å æ¥è¯¢é»è¾ |
| | | }; |
| | | </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> |
| | |
| | | :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" |
| | |
| | | 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"; |
| | |
| | | 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", |
| | |
| | | { |
| | | label: "宿æ°é", |
| | | prop: "completeQuantity", |
| | | }, |
| | | { |
| | | label: "å·¥åºç产è¿åº¦", |
| | | prop: "processRouteStatus", |
| | | dataType: "slot", |
| | | slot: "processRouteStatus", |
| | | width: processColumnWidth.value, |
| | | }, |
| | | { |
| | | dataType: "slot", |
| | |
| | | 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; |
| | |
| | | .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 { |
| | |
| | | </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" |
| | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | workOrderNo: "", |
| | | productOrderNpsNo: "", |
| | | npsNo: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | |
| | | </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" |
| | |
| | | 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(); |
| | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | workOrderNo: "", |
| | | productOrderNpsNo: "", |
| | | npsNo: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |