From 7179a06d3a8c824ee711a701e99114205fce9bcb Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 06 五月 2026 13:31:24 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro

---
 src/api/productionManagement/workOrder.js                    |    9 +
 src/views/productionManagement/processStatistics/index.vue   |  268 ++++++++++++++++++++++++++++++++++++++
 src/views/productionManagement/productionOrder/index.vue     |  129 +++++++++++++++++
 src/views/productionManagement/workOrderEdit/index.vue       |    4 
 src/views/productionManagement/workOrderManagement/index.vue |    8 
 5 files changed, 409 insertions(+), 9 deletions(-)

diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
index 2e2fe2d..b3050c9 100644
--- a/src/api/productionManagement/workOrder.js
+++ b/src/api/productionManagement/workOrder.js
@@ -86,3 +86,12 @@
     data,
   });
 }
+
+// 鑾峰彇宸ュ簭缁熻鏁版嵁
+export function getOperationStatistics(query) {
+  return request({
+    url: "/productionOperationTask/getOperation",
+    method: "get",
+    params: query,
+  });
+}
diff --git a/src/views/productionManagement/processStatistics/index.vue b/src/views/productionManagement/processStatistics/index.vue
new file mode 100644
index 0000000..25fa531
--- /dev/null
+++ b/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>
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 49c33bf..1cde5ea 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 8003890..600b274 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