zhangwencui
8 天以前 1fc62060649ca9e15ea3481098e614c75a1e7fad
生产订单加工序生产进度
已添加1个文件
已修改3个文件
440 ■■■■■ 文件已修改
src/views/productionManagement/processStatistics/index.vue 299 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderEdit/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>
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);