From 8a67b9a356a75423372a4cb62aa1408b134c89d9 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期三, 06 五月 2026 16:27:21 +0800
Subject: [PATCH] 工序生产实况模块开发

---
 src/pages.json                                             |    7 +
 src/api/productionManagement/workOrder.js                  |    9 +
 src/pages/works.vue                                        |   13 +
 src/pages/productionManagement/processStatistics/index.vue |  370 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 399 insertions(+), 0 deletions(-)

diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
index ffdd6a4..d3e0033 100644
--- a/src/api/productionManagement/workOrder.js
+++ b/src/api/productionManagement/workOrder.js
@@ -41,3 +41,12 @@
     responseType: "blob",
   });
 }
+
+// 鑾峰彇宸ュ簭缁熻鏁版嵁
+export function getOperationStatistics(query) {
+  return request({
+    url: "/productionOperationTask/getOperation",
+    method: "get",
+    params: query,
+  });
+}
diff --git a/src/pages.json b/src/pages.json
index 78e18df..f2353d0 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -873,6 +873,13 @@
       }
     },
     {
+      "path": "pages/productionManagement/processStatistics/index",
+      "style": {
+        "navigationBarTitleText": "宸ュ簭鐢熶骇瀹炲喌",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/inventoryManagement/receiptManagement/index",
       "style": {
         "navigationBarTitleText": "鑷畾涔夊叆搴�",
diff --git a/src/pages/productionManagement/processStatistics/index.vue b/src/pages/productionManagement/processStatistics/index.vue
new file mode 100644
index 0000000..a7cdd95
--- /dev/null
+++ b/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>
diff --git a/src/pages/works.vue b/src/pages/works.vue
index c6f3cc6..437e36f 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -601,6 +601,14 @@
       icon: "/static/images/icon/shengchanbaogong1.svg",
       label: "鐢熶骇杩芥函",
     },
+    {
+      icon: "/static/images/icon/shengchanbaogong1.svg",
+      label: "宸ュ簭鐢熶骇瀹炲喌",
+    },
+    {
+      icon: "/static/images/icon/shengchanbaogong1.svg",
+      label: "宸ュ簭瀹炲喌",
+    },
   ]);
 
   // 璁惧绠$悊鍔熻兘鏁版嵁
@@ -879,6 +887,11 @@
           url: "/pages/productionManagement/productionTraceability/index",
         });
         break;
+      case "宸ュ簭鐢熶骇瀹炲喌":
+        uni.navigateTo({
+          url: "/pages/productionManagement/processStatistics/index",
+        });
+        break;
       case "璁惧鍙拌处":
         uni.navigateTo({
           url: "/pages/equipmentManagement/ledger/index",

--
Gitblit v1.9.3