From 1fc62060649ca9e15ea3481098e614c75a1e7fad Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期三, 06 五月 2026 11:03:13 +0800
Subject: [PATCH] 生产订单加工序生产进度
---
src/views/productionManagement/processStatistics/index.vue | 299 +++++++++++++++++++++++++++++++++++++
src/views/productionManagement/productionOrder/index.vue | 129 +++++++++++++++
src/views/productionManagement/workOrderEdit/index.vue | 4
src/views/productionManagement/workOrderManagement/index.vue | 8
4 files changed, 431 insertions(+), 9 deletions(-)
diff --git a/src/views/productionManagement/processStatistics/index.vue b/src/views/productionManagement/processStatistics/index.vue
new file mode 100644
index 0000000..08d0324
--- /dev/null
+++ b/src/views/productionManagement/processStatistics/index.vue
@@ -0,0 +1,299 @@
+<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>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 93fc177..c37358b 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/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 {
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
index 693fea0..5d97b41 100644
--- a/src/views/productionManagement/workOrderEdit/index.vue
+++ b/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);
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index dda27fc..5cfa144 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/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);
--
Gitblit v1.9.3