yyb
2026-05-22 552ec6b7d8ccc56c379da195fc6c9c74312b1070
src/pages/productionManagement/productionOrder/index.vue
@@ -1,344 +1,546 @@
<template>
   <view class="production-order">
      <!-- 使用通用页面头部组件 -->
      <PageHeader title="生产订单" @back="goBack" />
      <!-- 搜索区域 -->
      <view class="search-section">
         <view class="search-bar">
            <view class="search-input">
               <up-input
                  class="search-text"
                  placeholder="请输入订单号或产品名称"
                  v-model="searchForm.keyword"
                  @change="handleQuery"
                  clearable
               />
            </view>
            <view class="filter-button" @click="handleQuery">
               <up-icon name="search" size="24" color="#999"></up-icon>
            </view>
         </view>
      </view>
      <!-- 列表区域 -->
      <scroll-view scroll-y class="list-container" v-if="tableData.length > 0" @scrolltolower="loadMore">
         <view v-for="(item, index) in tableData" :key="item.id || index">
            <view class="ledger-item">
               <view class="item-header">
                  <view class="item-left">
                     <view class="document-icon">
                        <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                     </view>
                     <text class="item-id">{{ item.npsNo }}</text>
                  </view>
                  <view class="item-right">
                     <up-tag :text="getStatusText(item.status)" :type="getStatusType(item.status)" size="mini" />
                  </view>
               </view>
               <up-divider></up-divider>
               <view class="item-details">
                  <view class="detail-row">
                     <text class="detail-label">产品名称</text>
                     <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
                  </view>
                  <view class="detail-row">
                     <text class="detail-label">规格型号</text>
                     <text class="detail-value">{{ item.model || '-' }}</text>
                  </view>
                  <view class="detail-row">
                     <text class="detail-label">订单数量</text>
                     <text class="detail-value">{{ item.quantity || 0 }}</text>
                  </view>
                  <view class="detail-row">
                     <text class="detail-label">完成进度</text>
                     <view class="progress-box">
                        <up-line-progress :percentage="toProgressPercentage(item.completionStatus)"
                                    :activeColor="progressColor(item.completionStatus)"
                                    height="10"></up-line-progress>
                        <text class="progress-text">{{ item.completeQuantity || 0 }} / {{ item.quantity || 0 }}</text>
                     </view>
                  </view>
                  <view class="detail-row">
                     <text class="detail-label">计划完成</text>
                     <text class="detail-value">{{ formatDate(item.planCompleteTime) }}</text>
                  </view>
               </view>
               <view class="item-footer">
                  <view class="action-btns">
                     <up-button type="primary" size="mini" plain text="来源" @click="goSource(item)"></up-button>
                     <up-button type="success" size="mini" plain text="领料详情" @click="goPickingDetail(item)"></up-button>
                  </view>
               </view>
            </view>
         </view>
         <up-loadmore :status="loadStatus" v-if="tableData.length >= page.size" />
      </scroll-view>
      <view v-else class="no-data">
         <up-empty mode="data" text="暂无生产订单数据"></up-empty>
      </view>
   </view>
  <view class="production-order">
    <!-- 使用通用页面头部组件 -->
    <PageHeader title="生产订单"
                @back="goBack" />
    <!-- 搜索区域 -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入订单号或产品名称"
                    v-model="searchForm.keyword"
                    @change="handleQuery"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- 列表区域 -->
    <scroll-view scroll-y
                 class="list-container"
                 v-if="tableData.length > 0"
                 @scrolltolower="loadMore">
      <view v-for="(item, index) in tableData"
            :key="item.id || index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.npsNo }}</text>
            </view>
            <view class="item-right">
              <up-tag :text="getStatusText(item.status)"
                      :type="getStatusType(item.status)"
                      size="mini" />
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">产品名称</text>
              <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.model || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">订单数量</text>
              <text class="detail-value">{{ item.quantity || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">完成进度</text>
              <view class="progress-box">
                <up-line-progress :percentage="toProgressPercentage(item.completionStatus)"
                                  :activeColor="progressColor(item.completionStatus)"
                                  height="10"></up-line-progress>
                <text class="progress-text">{{ item.completeQuantity || 0 }} / {{ item.quantity || 0 }}</text>
              </view>
            </view>
            <!-- 工序生产进度展示 -->
            <view class="detail-row process-row">
              <text class="detail-label">工序进度</text>
              <scroll-view scroll-x
                           class="process-scroll">
                <view class="process-container">
                  <view v-for="(process, pIdx) in item.processRouteStatus"
                        :key="pIdx"
                        class="process-item">
                    <view class="process-node">
                      <view class="node-circle"
                            :class="{ 'is-complete': process.percentage >= 100 }">
                        <text class="node-percentage"
                              :style="{ color: process.percentage >= 100 ? '#52c41a' : (process.percentage >= 70 ? '#f56c6c' : '#3c9cff') }">{{ process.percentage }}%</text>
                      </view>
                      <text class="node-name">{{ process.name }}</text>
                    </view>
                    <view v-if="pIdx < item.processRouteStatus.length - 1"
                          class="node-line"></view>
                  </view>
                  <view v-if="!item.processRouteStatus || !item.processRouteStatus.length"
                        class="no-process">-</view>
                </view>
              </scroll-view>
            </view>
            <view class="detail-row">
              <text class="detail-label">计划完成</text>
              <text class="detail-value">{{ formatDate(item.planCompleteTime) }}</text>
            </view>
          </view>
          <view class="item-footer">
            <view class="action-btns">
              <up-button type="info"
                         size="small"
                         plain
                         text="生产追溯"
                         @click="goTraceability(item)"></up-button>
              <up-button type="info"
                         size="small"
                         plain
                         text="工艺路线"
                         @click="goProcessRoute(item)"></up-button>
              <up-button type="primary"
                         size="small"
                         plain
                         text="来源"
                         @click="goSource(item)"></up-button>
              <up-button type="success"
                         size="small"
                         plain
                         text="领料详情"
                         @click="goPickingDetail(item)"></up-button>
            </view>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus"
                   v-if="tableData.length >= page.size" />
    </scroll-view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无生产订单数据"></up-empty>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance } from "vue";
import { onShow } from '@dcloudio/uni-app';
import dayjs from "dayjs";
import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
import PageHeader from "@/components/PageHeader.vue";
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import {
    productOrderListPage,
    getOrderProcessRouteMain,
  } from "@/api/productionManagement/productionOrder.js";
  import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
const { proxy } = getCurrentInstance();
  const { proxy } = getCurrentInstance();
// 加载状态
const loading = ref(false);
const loadStatus = ref('loadmore');
// 列表数据
const tableData = ref([]);
  // 加载状态
  const loading = ref(false);
  const loadStatus = ref("loadmore");
  // 列表数据
  const tableData = ref([]);
// 分页配置
const page = reactive({
   current: 1,
   size: 10,
   total: 0,
});
  // 分页配置
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
// 搜索表单数据
const data = reactive({
   searchForm: {
      keyword: "",
   },
});
const { searchForm } = toRefs(data);
  // 搜索表单数据
  const data = reactive({
    searchForm: {
      keyword: "",
    },
  });
  const { searchForm } = toRefs(data);
// 返回上一页
const goBack = () => {
   uni.navigateBack();
};
  // 返回上一页
  const goBack = () => {
    uni.navigateBack();
  };
// 格式化日期
const formatDate = (date) => {
   return date ? dayjs(date).format('YYYY-MM-DD') : '-';
};
  // 格式化日期
  const formatDate = date => {
    return date ? dayjs(date).format("YYYY-MM-DD") : "-";
  };
// 获取状态文本
const getStatusText = (status) => {
   const statusMap = {
      1: "待开始",
      2: "进行中",
      3: "已完成",
      4: "已取消",
   };
   return statusMap[status] || "未知";
};
  // 获取状态文本
  const getStatusText = status => {
    const statusMap = {
      1: "待开始",
      2: "进行中",
      3: "已完成",
      4: "已取消",
      5: "已结束",
    };
    return statusMap[status] || "未知";
  };
// 获取状态类型
const getStatusType = (status) => {
   const typeMap = {
      1: "primary",
      2: "warning",
      3: "success",
      4: "danger",
   };
   return typeMap[status] || "info";
};
  // 获取状态类型
  const getStatusType = status => {
    const typeMap = {
      1: "primary",
      2: "warning",
      3: "success",
      4: "info",
      5: "error",
    };
    return typeMap[status] || "info";
  };
// 完成进度百分比
const toProgressPercentage = (val) => {
   const n = Number(val);
   if (!Number.isFinite(n)) return 0;
   if (n <= 0) return 0;
   if (n >= 100) return 100;
   return Math.round(n);
};
  // 完成进度百分比
  const toProgressPercentage = val => {
    const n = Number(val);
    if (!Number.isFinite(n)) return 0;
    if (n <= 0) return 0;
    if (n >= 100) return 100;
    return Math.round(n);
  };
// 进度条颜色
const progressColor = (percentage) => {
   const p = toProgressPercentage(percentage);
   if (p < 30) return "#f56c6c";
   if (p < 50) return "#e6a23c";
   if (p < 80) return "#409eff";
   return "#67c23a";
};
  // 进度条颜色
  const progressColor = percentage => {
    const p = toProgressPercentage(percentage);
    if (p < 30) return "#f56c6c";
    if (p < 50) return "#e6a23c";
    if (p < 80) return "#409eff";
    return "#67c23a";
  };
// 查询列表
const handleQuery = () => {
   page.current = 1;
   tableData.value = [];
   getList();
};
  // 查询列表
  const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
  };
// 加载更多
const loadMore = () => {
   if (loadStatus.value === 'nomore' || loading.value) return;
   page.current++;
   getList();
};
  // 加载更多
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
// 获取列表数据
const getList = () => {
   loading.value = true;
   loadStatus.value = 'loading';
   const params = {
      current: page.current,
      size: page.size,
      npsNo: searchForm.value.keyword,
      productName: searchForm.value.keyword
   };
   productOrderListPage(params).then((res) => {
      loading.value = false;
      const records = res.data.records || [];
      if (page.current === 1) {
         tableData.value = records;
      } else {
         tableData.value = [...tableData.value, ...records];
      }
      if (records.length < page.size) {
         loadStatus.value = 'nomore';
      } else {
         loadStatus.value = 'loadmore';
      }
      page.total = res.data.total || 0;
   }).catch(() => {
      loading.value = false;
      loadStatus.value = 'loadmore';
      uni.showToast({
         title: '加载失败',
         icon: 'error'
      });
   });
};
  // 获取列表数据
  const getList = () => {
    loading.value = true;
    loadStatus.value = "loading";
// 跳转来源
const goSource = (item) => {
   uni.navigateTo({
      url: `/pages/productionManagement/productionOrder/source?id=${item.id}&productName=${encodeURIComponent(item.productName)}&model=${encodeURIComponent(item.model)}&quantity=${item.quantity}`
   });
};
    const params = {
      current: page.current,
      size: page.size,
      npsNo: searchForm.value.keyword,
      productName: searchForm.value.keyword,
    };
// 跳转领料详情
const goPickingDetail = (item) => {
   uni.navigateTo({
      url: `/pages/productionManagement/productionOrder/pickingDetail?id=${item.id}&npsNo=${item.npsNo}`
   });
};
    productOrderListPage(params)
      .then(async res => {
        const records = res.data.records || [];
// 页面显示时加载数据
onShow(() => {
   handleQuery();
});
        // 为每个订单并行查询工序进度
        const processPromises = records.map(async item => {
          if (item.npsNo) {
            try {
              const workOrderRes = await productWorkOrderPage({
                npsNo: item.npsNo,
                size: 100,
              });
              const workOrders = workOrderRes.data.records || [];
              const processRouteStatus = workOrders.map(wo => ({
                name: wo.operationName || "未知工序",
                percentage:
                  Number(wo.completionStatus) > 100
                    ? 100
                    : Number(wo.completionStatus || 0),
              }));
              return { ...item, processRouteStatus };
            } catch (error) {
              console.error(`获取工单 ${item.npsNo} 进度失败:`, error);
              return { ...item, processRouteStatus: [] };
            }
          }
          return { ...item, processRouteStatus: [] };
        });
        const updatedRecords = await Promise.all(processPromises);
        loading.value = false;
        if (page.current === 1) {
          tableData.value = updatedRecords;
        } else {
          tableData.value = [...tableData.value, ...updatedRecords];
        }
        if (updatedRecords.length < page.size) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
        }
        page.total = res.data.total || 0;
      })
      .catch(() => {
        loading.value = false;
        loadStatus.value = "loadmore";
        uni.showToast({
          title: "加载失败",
          icon: "error",
        });
      });
  };
  // 跳转工艺路线 (BOM)
  const goProcessRoute = item => {
    getOrderProcessRouteMain(item.id)
      .then(res => {
        const data = res.data || {};
        if (!data.id) {
          uni.showToast({ title: "未找到工艺路线", icon: "none" });
          return;
        }
        uni.navigateTo({
          url: `/pages/productionManagement/processRoute/items?id=${
            data.id
          }&bomId=${data.orderBomId}&processRouteCode=${
            data.processRouteCode || ""
          }&productName=${encodeURIComponent(
            item.productName || ""
          )}&model=${encodeURIComponent(item.model || "")}&orderId=${
            item.id
          }&type=order`,
        });
      })
      .catch(() => {
        uni.showToast({ title: "获取路线失败", icon: "none" });
      });
  };
  // 跳转来源
  const goSource = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/productionOrder/source?id=${
        item.id
      }&productName=${encodeURIComponent(
        item.productName
      )}&model=${encodeURIComponent(item.model)}&quantity=${item.quantity}`,
    });
  };
  // 跳转领料详情
  const goPickingDetail = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/productionOrder/pickingDetail?id=${item.id}&npsNo=${item.npsNo}`,
    });
  };
  // 跳转生产追溯
  const goTraceability = item => {
    uni.navigateTo({
      url: `/pages/productionManagement/productionTraceability/index?npsNo=${item.npsNo}`,
    });
  };
  // 页面显示时加载数据
  onShow(() => {
    handleQuery();
  });
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
  @import "@/styles/sales-common.scss";
.production-order {
   min-height: 100vh;
   background: #f8f9fa;
   display: flex;
   flex-direction: column;
}
  .production-order {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
.list-container {
   flex: 1;
   height: 0;
}
  .list-container {
    flex: 1;
    height: 0;
  }
.ledger-item {
   background: #fff;
   margin: 20rpx;
   padding: 24rpx;
   border-radius: 16rpx;
   box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
   .item-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 12rpx;
      .item-left {
         display: flex;
         align-items: center;
         .document-icon {
            width: 44rpx;
            height: 44rpx;
            background: #3c9cff;
            border-radius: 10rpx;
            display: flex;
            justify-content: center;
            align-items: center;
            margin-right: 20rpx;
         }
         .item-id {
            font-size: 30rpx;
            font-weight: bold;
            color: #333;
         }
      }
   }
   .item-details {
      padding: 16rpx 0;
      .detail-row {
         display: flex;
         justify-content: space-between;
         align-items: flex-start;
         margin-bottom: 16rpx;
         .detail-label {
            font-size: 26rpx;
            color: #999;
            min-width: 140rpx;
         }
         .detail-value {
            font-size: 26rpx;
            color: #333;
            text-align: right;
            &.font-bold {
               font-weight: bold;
            }
         }
         .progress-box {
            flex: 1;
            margin-left: 40rpx;
            .progress-text {
               font-size: 22rpx;
               color: #999;
               margin-top: 4rpx;
               display: block;
               text-align: right;
            }
         }
      }
   }
   .item-footer {
      padding-top: 20rpx;
      border-top: 1rpx solid #f0f0f0;
      display: flex;
      justify-content: flex-end;
      .action-btns {
         display: flex;
         gap: 20rpx;
      }
   }
}
  .ledger-item {
    background: #fff;
    margin: 20rpx;
    padding: 24rpx;
    border-radius: 16rpx;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.no-data {
   padding-top: 200rpx;
}
    .item-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 12rpx;
      .item-left {
        display: flex;
        align-items: center;
        .document-icon {
          width: 44rpx;
          height: 44rpx;
          background: #3c9cff;
          border-radius: 10rpx;
          display: flex;
          justify-content: center;
          align-items: center;
          margin-right: 20rpx;
        }
        .item-id {
          font-size: 30rpx;
          font-weight: bold;
          color: #333;
        }
      }
    }
    .item-details {
      padding: 16rpx 0;
      .detail-row {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        margin-bottom: 16rpx;
        .detail-label {
          font-size: 26rpx;
          color: #999;
          min-width: 140rpx;
        }
        .detail-value {
          font-size: 26rpx;
          color: #333;
          text-align: right;
          &.font-bold {
            font-weight: bold;
          }
        }
        .progress-box {
          flex: 1;
          margin-left: 40rpx;
          .progress-text {
            font-size: 22rpx;
            color: #999;
            margin-top: 4rpx;
            display: block;
            text-align: right;
          }
        }
        &.process-row {
          flex-direction: column;
          margin: 20rpx 0;
          .process-scroll {
            width: 100%;
            margin-top: 16rpx;
            .process-container {
              display: flex;
              align-items: flex-start;
              padding: 10rpx 0;
              min-height: 120rpx;
              .process-item {
                display: flex;
                align-items: center;
                .process-node {
                  display: flex;
                  flex-direction: column;
                  align-items: center;
                  width: 100rpx;
                  .node-circle {
                    width: 60rpx;
                    height: 60rpx;
                    border-radius: 50%;
                    border: 2rpx solid #3c9cff;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    background: #fff;
                    margin-bottom: 8rpx;
                    .node-percentage {
                      font-size: 18rpx;
                      color: #3c9cff;
                      font-weight: bold;
                    }
                    &.is-complete {
                      border-color: #52c41a;
                      background: #f6ffed;
                      .node-percentage {
                        color: #52c41a;
                      }
                    }
                  }
                  .node-name {
                    font-size: 20rpx;
                    color: #666;
                    text-align: center;
                    width: 120rpx;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                  }
                }
                .node-line {
                  width: 40rpx;
                  height: 2rpx;
                  background: #e8e8e8;
                  margin: -30rpx 0 0 0;
                }
              }
              .no-process {
                font-size: 24rpx;
                color: #ccc;
              }
            }
          }
        }
      }
    }
    .item-footer {
      padding-top: 20rpx;
      border-top: 1rpx solid #f0f0f0;
      display: flex;
      justify-content: flex-end;
      .action-btns {
        display: flex;
        gap: 20rpx;
      }
    }
  }
  .no-data {
    padding-top: 200rpx;
  }
</style>