已添加5个文件
已修改6个文件
2532 ■■■■■ 文件已修改
src/api/productionManagement/productionCosting.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductMain.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/processStatistics/index.vue 370 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionAccounting/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionOrder/index.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReporting/ledger.vue 412 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionTraceability/index.vue 947 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionCosting.js
@@ -8,4 +8,22 @@
    method: "get",
    params: query,
  });
}
// å·¦è¾¹è¡¨æ ¼çš„æŽ¥å£ (汇总)
export function salesLedgerProductionAccountingList(query) {
  return request({
    url: "/productionAccount/listPage",
    method: "get",
    params: query,
  });
}
// å³è¾¹è¡¨æ ¼çš„æŽ¥å£ (明细)
export function salesLedgerProductionAccountingListProductionDetails(query) {
  return request({
    url: "/productionAccount/listProductionDetails",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/productionOrder.js
@@ -10,6 +10,15 @@
  });
}
// ç”Ÿäº§è®¢å•溯源详情
export function getOrderDetail(npsNo) {
  return request({
    url: "/productionOrder/ordeDetail",
    method: "get",
    params: { npsNo },
  });
}
// èŽ·å–ç”Ÿäº§è®¢å•æ¥æºæ•°æ®
export function getProductOrderSource(id) {
  return request({
src/api/productionManagement/productionProductMain.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
// ç”Ÿäº§æŠ¥å·¥é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§æŠ¥å·¥ä¸»è¡¨
export function productionProductMainListPage(query) {
    return request({
        url: "/productionProductMain/listPage",
        method: "get",
        params: query,
    });
}
// åˆ é™¤æŠ¥å·¥
export function productionReportDelete(query) {
    return request({
        url: "/productionProductMain/delete",
        method: "get",
        params: query,
    });
}
// æŸ¥è¯¢æŠ•入列表
export function productionProductInputListPage(query) {
    return request({
        url: "/productionProductInput/listPage",
        method: "get",
        params: query,
    });
}
src/api/productionManagement/workOrder.js
@@ -41,3 +41,12 @@
    responseType: "blob",
  });
}
// èŽ·å–å·¥åºç»Ÿè®¡æ•°æ®
export function getOperationStatistics(query) {
  return request({
    url: "/productionOperationTask/getOperation",
    method: "get",
    params: query,
  });
}
src/pages.json
@@ -824,6 +824,13 @@
      }
    },
    {
      "path": "pages/productionManagement/productionReporting/ledger",
      "style": {
        "navigationBarTitleText": "报工台账",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/workOrder/index",
      "style": {
        "navigationBarTitleText": "生产工单",
@@ -851,13 +858,27 @@
        "navigationStyle": "custom"
      }
    },
    // {
    //   "path": "pages/productionManagement/productionCosting/index",
    //   "style": {
    //     "navigationBarTitleText": "生产核算",
    //     "navigationStyle": "custom"
    //   }
    // },
    {
      "path": "pages/productionManagement/productionAccounting/index",
      "style": {
        "navigationBarTitleText": "生产核算",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/productionTraceability/index",
      "style": {
        "navigationBarTitleText": "生产追溯",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/processStatistics/index",
      "style": {
        "navigationBarTitleText": "工序生产实况",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inventoryManagement/receiptManagement/index",
      "style": {
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>
src/pages/productionManagement/productionAccounting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <view class="production-accounting">
    <PageHeader title="生产核算"
                @back="goBack" />
    <!-- ç­›é€‰åŒºåŸŸ -->
    <view class="filter-section">
      <view class="date-type-selector">
        <up-tabs :list="dateTypeList"
                 :current="currentDateTypeIndex"
                 @change="handleDateTypeChange"
                 :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
                 lineWidth="30"
                 lineHeight="3" />
      </view>
      <view class="date-picker-bar"
            @click="showDatePicker = true">
        <view class="date-display">
          <up-icon name="calendar"
                   size="20"
                   color="#2979ff"></up-icon>
          <text class="date-text">{{ dateDisplayText }}</text>
        </view>
        <up-icon name="arrow-right"
                 size="16"
                 color="#999"></up-icon>
      </view>
    </view>
    <!-- æ±‡æ€»åˆ—表 -->
    <view class="summary-section"
          v-if="!showDetail">
      <view class="section-header">
        <text class="section-title">生产人员汇总</text>
      </view>
      <view class="ledger-list"
            v-if="summaryList.length > 0">
        <view v-for="(item, index) in summaryList"
              :key="index"
              class="ledger-item"
              @click="handleRowClick(item)">
          <view class="item-header">
            <view class="item-left">
              <view class="user-icon">
                <up-icon name="account"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.schedulingUserName || '未知' }}</text>
            </view>
            <view class="item-right">
              <up-icon name="arrow-right"
                       size="16"
                       color="#999"></up-icon>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-grid">
              <view class="grid-item">
                <text class="grid-label">产量</text>
                <text class="grid-value">{{ item.finishedNum || 0 }}</text>
              </view>
              <view class="grid-item">
                <text class="grid-label">工资</text>
                <text class="grid-value highlight">Â¥{{ item.wages || 0 }}</text>
              </view>
              <view class="grid-item">
                <text class="grid-label">合格率</text>
                <text class="grid-value">{{ formatOutputRate(item.outputRate) }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无汇总数据" />
      </view>
    </view>
    <!-- æ˜Žç»†åˆ—表 (点击汇总行后显示) -->
    <view class="detail-section"
          v-else>
      <view class="section-header back-bar"
            @click="showDetail = false">
        <up-icon name="arrow-left"
                 size="16"
                 color="#2979ff"></up-icon>
        <text class="back-text">返回汇总 ({{ currentUserName }})</text>
      </view>
      <view class="ledger-list"
            v-if="detailList.length > 0">
        <view v-for="(item, index) in detailList"
              :key="index"
              class="ledger-item no-click">
          <view class="item-header">
            <view class="item-left">
              <view class="product-icon">
                <up-icon name="shopping-cart"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.productName }}</text>
            </view>
            <view class="item-tag">
              <text class="tag-text">{{ item.schedulingDate }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.productModelName }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工序</text>
              <text class="detail-value">{{ item.process }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产数量</text>
              <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工时定额</text>
              <text class="detail-value">{{ item.workHours }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工资</text>
              <text class="detail-value highlight">Â¥{{ item.wages }}</text>
            </view>
          </view>
        </view>
        <up-loadmore :status="loadStatus"
                     @loadmore="getDetailList" />
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无明细数据" />
      </view>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker :show="showDatePicker"
                        v-model="pickerValue"
                        :mode="currentDateType === 'day' ? 'date' : 'year-month'"
                        @confirm="handleDateConfirm"
                        @cancel="showDatePicker = false" />
  </view>
</template>
<script setup>
  import { ref, reactive, computed, onMounted } from "vue";
  import {
    salesLedgerProductionAccountingList,
    salesLedgerProductionAccountingListProductionDetails,
  } from "@/api/productionManagement/productionCosting";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  // ç­›é€‰ç›¸å…³
  const dateTypeList = [{ name: "日" }, { name: "月" }];
  const currentDateTypeIndex = ref(0);
  const currentDateType = computed(() =>
    currentDateTypeIndex.value === 0 ? "day" : "month"
  );
  const showDatePicker = ref(false);
  const pickerValue = ref(Date.now());
  const selectedDate = ref(dayjs().format("YYYY-MM-DD"));
  const dateDisplayText = computed(() => {
    return currentDateType.value === "day"
      ? selectedDate.value
      : dayjs(selectedDate.value).format("YYYY-MM");
  });
  // æ•°æ®ç›¸å…³
  const summaryList = ref([]);
  const detailList = ref([]);
  const showDetail = ref(false);
  const currentUserName = ref("");
  const loadStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 20,
    total: 0,
  });
  const page1 = reactive({
    current: 1,
    size: 20,
    total: 0,
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // åˆ‡æ¢æ—¥æœŸç±»åž‹
  const handleDateTypeChange = index => {
    currentDateTypeIndex.value = index.index;
    if (currentDateType.value === "day") {
      selectedDate.value = dayjs().format("YYYY-MM-DD");
    } else {
      selectedDate.value = dayjs().startOf("month").format("YYYY-MM-DD");
    }
    reloadData();
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const handleDateConfirm = e => {
    selectedDate.value = dayjs(e.value).format("YYYY-MM-DD");
    showDatePicker.value = false;
    reloadData();
  };
  // æ ¼å¼åŒ–合格率
  const formatOutputRate = val => {
    if (val == null || val === "") return "-";
    return parseFloat(val).toFixed(2) + "%";
  };
  // åŠ è½½æ±‡æ€»åˆ—è¡¨
  const getSummaryList = () => {
    uni.showLoading({ title: "加载中..." });
    const params = {
      dateType: currentDateType.value,
      entryDate: currentDateType.value === "day" ? selectedDate.value : undefined,
      entryDateStart:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD")
          : undefined,
      entryDateEnd:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD")
          : undefined,
      pageNum: page.current,
      pageSize: page.size,
    };
    salesLedgerProductionAccountingList(params)
      .then(res => {
        summaryList.value = res.data.records || [];
        page.total = res.data.total || 0;
      })
      .finally(() => {
        uni.hideLoading();
      });
  };
  // åŠ è½½æ˜Žç»†åˆ—è¡¨
  const getDetailList = (isLoadMore = false) => {
    if (!isLoadMore) {
      page1.current = 1;
      detailList.value = [];
    }
    loadStatus.value = "loading";
    const params = {
      schedulingUserName: currentUserName.value,
      dateType: currentDateType.value,
      entryDate: currentDateType.value === "day" ? selectedDate.value : undefined,
      entryDateStart:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD")
          : undefined,
      entryDateEnd:
        currentDateType.value === "month"
          ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD")
          : undefined,
      pageNum: page1.current,
      pageSize: page1.size,
    };
    salesLedgerProductionAccountingListProductionDetails(params)
      .then(res => {
        const records = res.data.records || [];
        detailList.value = isLoadMore ? [...detailList.value, ...records] : records;
        page1.total = res.data.total || 0;
        if (detailList.value.length >= page1.total) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
          page1.current++;
        }
      })
      .catch(() => {
        loadStatus.value = "loadmore";
      });
  };
  // ç‚¹å‡»æ±‡æ€»è¡Œ
  const handleRowClick = item => {
    currentUserName.value = item.schedulingUserName;
    showDetail.value = true;
    getDetailList();
  };
  // é‡æ–°åŠ è½½æ•°æ®
  const reloadData = () => {
    page.current = 1;
    showDetail.value = false;
    getSummaryList();
  };
  onMounted(() => {
    getSummaryList();
  });
</script>
<style scoped lang="scss">
  .production-accounting {
    background-color: #f5f7fa;
    min-height: 100vh;
    padding-bottom: 30rpx;
    .filter-section {
      background-color: #ffffff;
      padding: 20rpx 30rpx;
      margin-bottom: 20rpx;
      .date-type-selector {
        margin-bottom: 20rpx;
      }
      .date-picker-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background-color: #f0f4ff;
        padding: 16rpx 24rpx;
        border-radius: 8rpx;
        .date-display {
          display: flex;
          align-items: center;
          gap: 12rpx;
          .date-text {
            font-size: 28rpx;
            color: #2979ff;
            font-weight: bold;
          }
        }
      }
    }
    .section-header {
      padding: 20rpx 30rpx;
      .section-title {
        font-size: 30rpx;
        font-weight: bold;
        color: #333;
        border-left: 8rpx solid #2979ff;
        padding-left: 16rpx;
      }
      &.back-bar {
        display: flex;
        align-items: center;
        gap: 10rpx;
        background-color: #ffffff;
        margin-bottom: 20rpx;
        .back-text {
          font-size: 28rpx;
          color: #2979ff;
        }
      }
    }
    .ledger-list {
      padding: 0 20rpx;
      .ledger-item {
        background-color: #ffffff;
        border-radius: 16rpx;
        padding: 24rpx;
        margin-bottom: 20rpx;
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
        &:active {
          background-color: #f9f9f9;
        }
        &.no-click:active {
          background-color: #ffffff;
        }
        .item-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 16rpx;
          .item-left {
            display: flex;
            align-items: center;
            gap: 12rpx;
            .user-icon,
            .product-icon {
              width: 48rpx;
              height: 48rpx;
              background: linear-gradient(135deg, #2979ff, #64a1ff);
              border-radius: 8rpx;
              display: flex;
              align-items: center;
              justify-content: center;
            }
            .item-id {
              font-size: 28rpx;
              font-weight: bold;
              color: #333;
            }
          }
          .item-tag {
            background-color: #f0f4ff;
            padding: 4rpx 12rpx;
            border-radius: 4rpx;
            .tag-text {
              font-size: 24rpx;
              color: #2979ff;
            }
          }
        }
        .item-details {
          padding-top: 10rpx;
          .detail-grid {
            display: flex;
            justify-content: space-between;
            .grid-item {
              display: flex;
              flex-direction: column;
              align-items: center;
              flex: 1;
              .grid-label {
                font-size: 24rpx;
                color: #999;
                margin-bottom: 8rpx;
              }
              .grid-value {
                font-size: 28rpx;
                color: #333;
                font-weight: 500;
                &.highlight {
                  color: #ff5a5f;
                }
              }
            }
          }
          .detail-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 12rpx;
            .detail-label {
              font-size: 26rpx;
              color: #999;
            }
            .detail-value {
              font-size: 26rpx;
              color: #333;
              &.highlight {
                color: #ff5a5f;
                font-weight: bold;
              }
            }
          }
        }
      }
    }
    .no-data {
      padding-top: 100rpx;
    }
  }
</style>
src/pages/productionManagement/productionOrder/index.vue
@@ -67,6 +67,31 @@
                <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>
@@ -75,17 +100,22 @@
          <view class="item-footer">
            <view class="action-btns">
              <up-button type="info"
                         size="mini"
                         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="mini"
                         size="small"
                         plain
                         text="来源"
                         @click="goSource(item)"></up-button>
              <up-button type="success"
                         size="mini"
                         size="small"
                         plain
                         text="领料详情"
                         @click="goPickingDetail(item)"></up-button>
@@ -112,6 +142,7 @@
    productOrderListPage,
    getOrderProcessRouteMain,
  } from "@/api/productionManagement/productionOrder.js";
  import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
  import PageHeader from "@/components/PageHeader.vue";
  const { proxy } = getCurrentInstance();
@@ -166,7 +197,7 @@
      2: "warning",
      3: "success",
      4: "info",
      5: "danger",
      5: "error",
    };
    return typeMap[status] || "info";
  };
@@ -216,16 +247,44 @@
    };
    productOrderListPage(params)
      .then(res => {
        loading.value = false;
      .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 || [];
              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 = records;
          tableData.value = updatedRecords;
        } else {
          tableData.value = [...tableData.value, ...records];
          tableData.value = [...tableData.value, ...updatedRecords];
        }
        if (records.length < page.size) {
        if (updatedRecords.length < page.size) {
          loadStatus.value = "nomore";
        } else {
          loadStatus.value = "loadmore";
@@ -283,6 +342,13 @@
  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}`,
    });
  };
@@ -380,6 +446,84 @@
            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;
              }
            }
          }
        }
      }
    }
src/pages/productionManagement/productionReporting/ledger.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,412 @@
<template>
  <view class="reporting-ledger">
    <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 class="ledger-list">
        <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.productNo || '-' }}</text>
              </view>
              <view class="item-tag">
                <text class="create-time">{{ formatDate(item.createTime) }}</text>
              </view>
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">报工人员</text>
                <text class="detail-value highlight">{{ item.nickName || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">所属工序</text>
                <view class="detail-value">
                  <up-tag :text="item.process || '-'"
                          type="primary"
                          size="mini"
                          plain />
                </view>
              </view>
              <view class="detail-row">
                <text class="detail-label">工单编号</text>
                <text class="detail-value">{{ item.workOrderNo || '-' }}</text>
              </view>
              <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.productModelName || '-' }}</text>
              </view>
              <view class="quantity-section">
                <view class="qty-item">
                  <text class="qty-label">产出数量</text>
                  <text class="qty-value success">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
                </view>
                <view class="qty-item">
                  <text class="qty-label">报废数量</text>
                  <text class="qty-value error">{{ item.scrapQty || 0 }}</text>
                </view>
              </view>
              <view class="item-footer">
                <view class="action-buttons">
                  <up-button type="primary"
                             size="small"
                             plain
                             text="查看投入"
                             @click="handleShowInput(item)"></up-button>
                  <up-button type="info"
                             size="small"
                             plain
                             text="参数详情"
                             @click="handleShowParams(item)"></up-button>
                  <up-button type="error"
                             size="small"
                             plain
                             text="删除"
                             @click="handleDelete(item)"></up-button>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
      <up-loadmore :status="loadStatus"
                   v-if="tableData.length >= page.size" />
    </scroll-view>
    <view v-else
          class="empty-state">
      <up-empty mode="data"
                text="暂无报工台账数据"></up-empty>
    </view>
    <!-- æŠ•入详情弹窗 -->
    <up-modal :show="inputVisible"
              title="投入详情"
              @confirm="inputVisible = false">
      <view class="modal-content scroll-view">
        <view v-if="inputList.length > 0">
          <view v-for="(input, idx) in inputList"
                :key="idx"
                class="detail-item">
            <view class="detail-row">
              <text class="detail-label">投入产品</text>
              <text class="detail-value font-bold">{{ input.productName }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ input.model }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">投入数量</text>
              <text class="detail-value highlight">{{ input.quantity }} {{ input.unit }}</text>
            </view>
            <up-divider></up-divider>
          </view>
        </view>
        <up-empty v-else
                  mode="data"
                  text="暂无投入数据" />
      </view>
    </up-modal>
    <!-- å‚数详情弹窗 -->
    <up-modal :show="paramsVisible"
              title="参数详情"
              @confirm="paramsVisible = false">
      <view class="modal-content">
        <view v-if="currentParams.length > 0">
          <view v-for="(param, idx) in currentParams"
                :key="idx"
                class="detail-row">
            <text class="detail-label">{{ param.paramName }}</text>
            <text class="detail-value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? '(' + param.unit + ')' : '' }}</text>
          </view>
        </view>
        <up-empty v-else
                  mode="data"
                  text="暂无参数数据" />
      </view>
    </up-modal>
  </view>
</template>
<script setup>
  import { ref, reactive, onMounted } from "vue";
  import dayjs from "dayjs";
  import {
    productionProductMainListPage,
    productionReportDelete,
    productionProductInputListPage,
  } from "@/api/productionManagement/productionProductMain.js";
  import PageHeader from "@/components/PageHeader.vue";
  import modal from "@/plugins/modal";
  const tableData = ref([]);
  const loading = ref(false);
  const loadStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  const searchForm = reactive({
    keyword: "",
  });
  // æŠ•入详情相关
  const inputVisible = ref(false);
  const inputList = ref([]);
  // å‚数详情相关
  const paramsVisible = ref(false);
  const currentParams = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const formatDate = date => {
    return date ? dayjs(date).format("YYYY-MM-DD HH:mm") : "-";
  };
  const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    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,
      workOrderNo: searchForm.keyword,
    };
    productionProductMainListPage(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";
        modal.msgError("加载失败");
      });
  };
  const handleShowInput = item => {
    modal.loading("加载中...");
    productionProductInputListPage({
      productMainId: item.id,
      current: 1,
      size: 100,
    })
      .then(res => {
        modal.closeLoading();
        inputList.value = res.data.records || [];
        inputVisible.value = true;
      })
      .catch(() => {
        modal.closeLoading();
        modal.msgError("加载投入数据失败");
      });
  };
  const handleShowParams = item => {
    currentParams.value = item.productionOperationParamList || [];
    paramsVisible.value = true;
  };
  const handleDelete = item => {
    uni.showModal({
      title: "提示",
      content: "确定要删除该报工记录吗?",
      success: res => {
        if (res.confirm) {
          productionReportDelete({ id: item.id }).then(res => {
            if (res.code === 200) {
              modal.msgSuccess("删除成功");
              handleQuery();
            } else {
              modal.msgError(res.msg || "删除失败");
            }
          });
        }
      },
    });
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .reporting-ledger {
    min-height: 100vh;
    background-color: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .list-container {
    flex: 1;
    height: 0;
  }
  .ledger-item {
    .item-header {
      .item-tag {
        .create-time {
          font-size: 24rpx;
          color: #999;
        }
      }
    }
    .item-details {
      .quantity-section {
        display: flex;
        background-color: #f9f9f9;
        border-radius: 8rpx;
        padding: 20rpx;
        margin: 20rpx 0;
        .qty-item {
          flex: 1;
          display: flex;
          flex-direction: column;
          align-items: center;
          &:first-child {
            border-right: 1rpx solid #eee;
          }
          .qty-label {
            font-size: 24rpx;
            color: #999;
            margin-bottom: 8rpx;
          }
          .qty-value {
            font-size: 32rpx;
            font-weight: bold;
            &.success {
              color: #52c41a;
            }
            &.error {
              color: #f56c6c;
            }
          }
        }
      }
      .item-footer {
        padding-top: 20rpx;
        border-top: 1rpx solid #f0f0f0;
        .action-buttons {
          display: flex;
          justify-content: flex-end;
          gap: 20rpx;
        }
      }
    }
  }
  .modal-content {
    width: 100%;
    padding: 20rpx 0;
    max-height: 60vh;
    overflow-y: auto;
    .detail-item {
      padding-bottom: 20rpx;
    }
    .detail-row {
      display: flex;
      justify-content: space-between;
      margin-bottom: 16rpx;
      .detail-label {
        font-size: 26rpx;
        color: #999;
      }
      .detail-value {
        font-size: 26rpx;
        color: #333;
        &.font-bold {
          font-weight: bold;
        }
        &.highlight {
          color: #3c9cff;
        }
      }
    }
  }
  .empty-state {
    padding-top: 200rpx;
  }
</style>
src/pages/productionManagement/productionTraceability/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,947 @@
<template>
  <view class="production-traceability">
    <PageHeader title="生产追溯"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar"
            @click="openNpsNoSelector">
        <view class="search-input">
          <text v-if="!selectedNpsNo"
                class="placeholder">请选择生产订单号</text>
          <text v-else
                class="value">{{ selectedNpsNoLabel }}</text>
        </view>
        <view class="search-button">
          <up-icon name="arrow-down"
                   size="20"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å†…容区域 -->
    <view class="content-container"
          v-if="rowData.productionOrderDto">
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <view class="info-card">
        <view class="card-title">基础信息</view>
        <view class="info-grid">
          <view class="info-item">
            <text class="label">生产订单号</text>
            <text class="value">{{ rowData.productionOrderDto.npsNo || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">产品名称</text>
            <text class="value">{{ rowData.productionOrderDto.productName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">产品规格</text>
            <text class="value">{{ rowData.productionOrderDto.model || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">计划数量</text>
            <text class="value">{{ rowData.productionOrderDto.quantity || 0 }} {{ rowData.productionOrderDto.unit || '' }}</text>
          </view>
          <view class="info-item">
            <text class="label">当前状态</text>
            <up-tag :text="getStatusText(rowData.productionOrderDto.status)"
                    style="width:100rpx"
                    :type="getStatusType(rowData.productionOrderDto.status)"
                    size="mini" />
          </view>
          <view class="info-item">
            <text class="label">客户名称</text>
            <text class="value">{{ rowData.productionOrderDto.customerName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="label">开始日期</text>
            <text class="value">{{ formatDate(rowData.productionOrderDto.startTime) }}</text>
          </view>
          <view class="info-item full-width">
            <text class="label">完成进度</text>
            <view class="progress-container">
              <up-line-progress :percentage="formatProgress(rowData.productionOrderDto.completionStatus)"
                                :activeColor="progressColor(rowData.productionOrderDto.completionStatus)"
                                height="8"></up-line-progress>
              <text class="progress-text">{{ formatProgress(rowData.productionOrderDto.completionStatus) }}%</text>
            </view>
          </view>
        </view>
      </view>
      <!-- å·¥å•信息 -->
      <view class="work-order-section"
            v-if="rowData.productionRecords && rowData.productionRecords.length > 0">
        <view class="section-title">工单信息</view>
        <view v-for="(item, index) in rowData.productionRecords"
              :key="index"
              class="work-order-card">
          <view class="card-header">
            <text class="work-order-no">{{ item.workOrder.workOrderNo }}</text>
            <text class="progress-tag"
                  :style="{ color: progressColor(item.workOrder.completionStatus) }">{{ item.workOrder.completionStatus || 0 }}%</text>
          </view>
          <view class="card-content">
            <view class="content-row">
              <text class="label">产品/规格:</text>
              <text class="value">{{ item.workOrder.productName }} / {{ item.workOrder.model }}</text>
            </view>
            <view class="content-row">
              <text class="label">需求/完成:</text>
              <text class="value">{{ item.workOrder.planQuantity }} / {{ item.workOrder.completeQuantity }}</text>
            </view>
          </view>
          <view class="card-footer">
            <up-button type="primary"
                       size="small"
                       plain
                       text="报工记录"
                       @click="handleShowReports(item)"></up-button>
            <up-button type="success"
                       size="small"
                       plain
                       text="质检信息"
                       @click="handleShowQuality(item)"></up-button>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data-minor">
        <up-empty mode="data"
                  text="暂无工单信息"
                  icon-size="40"></up-empty>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="search"
                text="请选择生产订单号查看追溯信息"></up-empty>
    </view>
    <!-- ç”Ÿäº§è®¢å•号选择弹窗 -->
    <up-popup :show="showNpsNoSelector"
              mode="bottom"
              @close="showNpsNoSelector = false"
              round="10">
      <view class="selector-popup">
        <view class="popup-header">
          <text class="popup-title">选择生产订单号</text>
          <up-icon name="close"
                   size="20"
                   @click="showNpsNoSelector = false"></up-icon>
        </view>
        <view class="search-box">
          <up-search placeholder="输入关键字搜索"
                     v-model="npsNoQuery"
                     :show-action="false"
                     @change="handleNpsNoSearch"
                     @search="handleNpsNoSearch"
                     :loading="npsNoLoading"></up-search>
        </view>
        <scroll-view scroll-y
                     class="options-list">
          <view v-for="item in npsNoOptions"
                :key="item.id"
                class="option-item"
                @click="onSelectNpsNo(item)">
            <view class="option-main">
              <text class="nps-no">{{ item.npsNo }}</text>
              <text class="product-info">{{ item.productName }} / {{ item.model }}</text>
            </view>
            <up-icon v-if="selectedNpsNo === item.id"
                     name="checkbox-mark"
                     color="#3c9cff"
                     size="20"></up-icon>
          </view>
          <view v-if="npsNoOptions.length === 0"
                class="no-options">
            <text>{{ npsNoLoading ? '加载中...' : '暂无选项' }}</text>
          </view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- æŠ¥å·¥è¯¦æƒ…弹窗 -->
    <up-popup :show="reportPopupVisible"
              mode="bottom"
              @close="reportPopupVisible = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">生产报工详情</text>
          <up-icon name="close"
                   size="20"
                   @click="reportPopupVisible = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="popup-scroll">
          <view class="detail-info">
            <view class="info-row"><text class="label">工单号:</text><text class="value">{{ detailData.workOrder.workOrderNo }}</text></view>
            <view class="info-row"><text class="label">计划/完成:</text><text class="value">{{ detailData.workOrder.planQuantity }} / {{ detailData.workOrder.completeQuantity }}</text></view>
            <view class="info-row"><text class="label">实际时间:</text><text class="value">{{ formatDate(detailData.workOrder.actualStartTime) }} è‡³ {{ formatDate(detailData.workOrder.actualEndTime) }}</text></view>
          </view>
          <view class="list-title">报工明细</view>
          <view v-for="(report, idx) in detailData.reports"
                :key="idx"
                class="detail-item">
            <view class="item-main">
              <view class="item-row"><text class="label">报工单号:</text><text class="value">{{ report.productNo }}</text></view>
              <view class="item-row"><text class="label">创建人:</text><text class="value">{{ report.userName }}</text></view>
              <view class="item-row"><text class="label">创建时间:</text><text class="value">{{ formatDate(report.createTime, '{y}-{m}-{d} {h}:{i}') }}</text></view>
            </view>
            <view class="item-actions">
              <text class="action-link"
                    @click="showParams(report.productionOperationParamList)">参数详情</text>
              <text class="action-link green"
                    @click="handleShowInput(report.id)">投入详情</text>
            </view>
          </view>
          <view v-if="!detailData.reports || detailData.reports.length === 0"
                class="no-data-minor">暂无报工明细</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- æŠ•入详情弹窗 -->
    <up-popup :show="inputPopupVisible"
              mode="bottom"
              @close="inputPopupVisible = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">投入信息详情</text>
          <up-icon name="close"
                   size="20"
                   @click="inputPopupVisible = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="popup-scroll">
          <view class="input-list-popup">
            <view v-for="(item, idx) in inputListData"
                  :key="idx"
                  class="input-item">
              <view class="input-row"><text class="label">物料名称:</text><text class="value">{{ item.materialName }}</text></view>
              <view class="input-row"><text class="label">规格型号:</text><text class="value">{{ item.model }}</text></view>
              <view class="input-row"><text class="label">投入数量:</text><text class="value">{{ item.quantity }} {{ item.unit }}</text></view>
              <view class="input-row"><text class="label">批次号:</text><text class="value">{{ item.batchNo }}</text></view>
            </view>
            <view v-if="!inputListData || inputListData.length === 0"
                  class="no-data-minor">{{ inputLoading ? '加载中...' : '暂无投入记录' }}</view>
          </view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- è´¨æ£€è¯¦æƒ…弹窗 -->
    <up-popup :show="qualityPopupVisible"
              mode="bottom"
              @close="qualityPopupVisible = false"
              round="10">
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">质检详情</text>
          <up-icon name="close"
                   size="20"
                   @click="qualityPopupVisible = false"></up-icon>
        </view>
        <scroll-view scroll-y
                     class="popup-scroll">
          <view v-for="(record, idx) in qualityRecords"
                :key="idx"
                class="quality-record">
            <view class="record-title">检测记录 {{ idx + 1 }}</view>
            <view class="info-grid">
              <view class="info-item"><text class="label">检测日期</text><text class="value">{{ formatDate(record.createTime) }}</text></view>
              <view class="info-item"><text class="label">检测结果</text><up-tag style="width:100rpx"
                        :text="record.checkResult || '待检测'"
                        :type="record.checkResult === '合格' ? 'success' : 'error'"
                        size="mini" /></view>
              <view class="info-item"><text class="label">检验员</text><text class="value">{{ record.userName }}</text></view>
              <view class="info-item"><text class="label">数量</text><text class="value">{{ record.quantity }} {{ record.unit }}</text></view>
            </view>
            <view class="params-table">
              <view class="table-header">
                <text class="col">指标</text>
                <text class="col">标准值</text>
                <text class="col">实际值</text>
              </view>
              <view v-for="(param, pIdx) in record.inspectParamList"
                    :key="pIdx"
                    class="table-row">
                <text class="col">{{ param.parameterItem }}</text>
                <text class="col">{{ param.standardValue }}</text>
                <text class="col"
                      :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text>
              </view>
            </view>
          </view>
          <view v-if="!qualityRecords || qualityRecords.length === 0"
                class="no-data-minor">暂无质检记录</view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- å‚数详情弹窗 -->
    <up-modal :show="paramModalVisible"
              title="参数详情"
              @confirm="paramModalVisible = false">
      <view class="modal-content">
        <view v-for="(param, idx) in currentParams"
              :key="idx"
              class="param-row">
          <text class="label">{{ param.paramName }}:</text>
          <text class="value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? param.unit : '' }}</text>
        </view>
        <view v-if="!currentParams || currentParams.length === 0"
              class="no-data-minor">暂无参数数据</view>
      </view>
    </up-modal>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    getOrderDetail,
    productOrderListPage,
  } from "@/api/productionManagement/productionOrder";
  import { productionProductInputListPage } from "@/api/productionManagement/productionProductMain";
  import PageHeader from "@/components/PageHeader.vue";
  import { parseTime } from "@/utils/ruoyi";
  // é€‰æ‹©å™¨ç›¸å…³
  const showNpsNoSelector = ref(false);
  const npsNoQuery = ref("");
  const npsNoOptions = ref([]);
  const npsNoLoading = ref(false);
  const selectedNpsNo = ref(null);
  const selectedNpsNoLabel = ref("");
  const rowData = reactive({
    productionOrderDto: null,
    productionRecords: [],
  });
  // æŠ¥å·¥è¯¦æƒ…
  const reportPopupVisible = ref(false);
  const detailData = ref({ workOrder: {}, reports: [] });
  // æŠ•入详情
  const inputPopupVisible = ref(false);
  const inputListData = ref([]);
  const inputLoading = ref(false);
  // è´¨æ£€è¯¦æƒ…
  const qualityPopupVisible = ref(false);
  const qualityRecords = ref([]);
  // å‚数详情
  const paramModalVisible = ref(false);
  const currentParams = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const openNpsNoSelector = () => {
    showNpsNoSelector.value = true;
    if (npsNoOptions.value.length === 0) {
      handleNpsNoSearch();
    }
  };
  const handleNpsNoSearch = async () => {
    npsNoLoading.value = true;
    try {
      const res = await productOrderListPage({
        npsNo: npsNoQuery.value || "",
        pageNum: 1,
        pageSize: 50,
      });
      npsNoOptions.value = res.data?.records || res.rows || [];
    } catch (error) {
      console.error(error);
    } finally {
      npsNoLoading.value = false;
    }
  };
  const onSelectNpsNo = async item => {
    selectedNpsNo.value = item.id;
    selectedNpsNoLabel.value = item.npsNo;
    showNpsNoSelector.value = false;
    uni.showLoading({ title: "加载中..." });
    try {
      const res = await getOrderDetail(item.npsNo);
      if (res.code === 200 && res.data) {
        const { productionOrder, workOrderList } = res.data;
        rowData.productionOrderDto = productionOrder || item;
        rowData.productionRecords = workOrderList || [];
      } else {
        rowData.productionOrderDto = item;
        rowData.productionRecords = [];
      }
    } catch (error) {
      console.error(error);
      rowData.productionOrderDto = item;
      rowData.productionRecords = [];
      uni.showToast({ title: "获取详情失败", icon: "none" });
    } finally {
      uni.hideLoading();
    }
  };
  onLoad(async options => {
    if (options.npsNo) {
      uni.showLoading({ title: "加载中..." });
      try {
        const res = await productOrderListPage({
          npsNo: options.npsNo,
          pageNum: 1,
          pageSize: 10,
        });
        const records = res.data?.records || res.rows || [];
        if (records.length > 0) {
          onSelectNpsNo(records[0]);
        } else {
          uni.showToast({ title: "未找到相关订单", icon: "none" });
        }
      } catch (error) {
        console.error(error);
      } finally {
        uni.hideLoading();
      }
    }
  });
  const getStatusText = status => {
    const statusMap = { 1: "待开始", 2: "进行中", 3: "已完成", 5: "已结束" };
    return statusMap[status] || "已取消";
  };
  const getStatusType = status => {
    const typeMap = { 1: "primary", 2: "warning", 3: "success", 5: "error" };
    return typeMap[status] || "info";
  };
  const formatDate = (date, pattern = "{y}-{m}-{d}") => {
    return parseTime(date, pattern) || "-";
  };
  const formatProgress = val => {
    const p = parseFloat(val || 0);
    return p >= 100 ? 100 : p;
  };
  const progressColor = percentage => {
    if (percentage < 30) return "#f56c6c";
    if (percentage < 70) return "#e6a23c";
    return "#67c23a";
  };
  const handleShowReports = row => {
    detailData.value = {
      workOrder: row.workOrder || {},
      reports: (row.reportList || []).map(r => ({
        ...r.reportMain,
        id: r.reportMain.id,
        productionOperationParamList: r.reportParamList || [],
      })),
    };
    reportPopupVisible.value = true;
  };
  const handleShowInput = async reportId => {
    inputPopupVisible.value = true;
    inputLoading.value = true;
    inputListData.value = [];
    try {
      const res = await productionProductInputListPage({
        productMainId: reportId,
        pageNum: 1,
        pageSize: 100,
      });
      inputListData.value = res.data?.records || res.rows || [];
    } catch (error) {
      console.error(error);
      uni.showToast({ title: "获取投入信息失败", icon: "none" });
    } finally {
      inputLoading.value = false;
    }
  };
  const handleShowQuality = row => {
    const inspects = row.inspectList || [];
    qualityRecords.value = inspects.map(i => ({
      ...i.inspect,
      reportNo: i.reportNo,
      userName: i.reportMain?.userName || "-",
      inspectParamList: i.inspectParamList || [],
    }));
    qualityPopupVisible.value = true;
  };
  const showParams = params => {
    currentParams.value = params || [];
    paramModalVisible.value = true;
  };
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .production-traceability {
    min-height: 100vh;
    background-color: #f5f7fa;
  }
  .search-section {
    background-color: #fff;
    padding: 20rpx 24rpx;
    margin-bottom: 20rpx;
  }
  .search-bar {
    display: flex;
    align-items: center;
    background-color: #f2f2f2;
    border-radius: 8rpx;
    padding: 0 20rpx;
    height: 80rpx;
    .search-input {
      flex: 1;
      display: flex;
      align-items: center;
      .placeholder {
        font-size: 28rpx;
        color: #999;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
      }
    }
    .search-button {
      padding: 0 10rpx;
    }
  }
  .selector-popup {
    background: #fff;
    padding: 30rpx;
    max-height: 70vh;
    display: flex;
    flex-direction: column;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 24rpx;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
      }
    }
    .search-box {
      margin-bottom: 20rpx;
    }
    .options-list {
      flex: 1;
      overflow: hidden;
    }
    .option-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 24rpx 0;
      border-bottom: 1rpx solid #f0f0f0;
      .option-main {
        flex: 1;
        .nps-no {
          font-size: 28rpx;
          font-weight: bold;
          color: #333;
          display: block;
          margin-bottom: 4rpx;
        }
        .product-info {
          font-size: 24rpx;
          color: #999;
        }
      }
    }
    .no-options {
      text-align: center;
      padding: 40rpx;
      color: #999;
      font-size: 26rpx;
    }
  }
  .content-container {
    padding: 0 24rpx 40rpx;
  }
  .info-card {
    background: #fff;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 24rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .card-title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
      margin-bottom: 24rpx;
      padding-left: 16rpx;
      border-left: 8rpx solid #3c9cff;
    }
  }
  .info-grid {
    display: flex;
    flex-wrap: wrap;
    .info-item {
      width: 50%;
      margin-bottom: 20rpx;
      display: flex;
      flex-direction: column;
      &.full-width {
        width: 100%;
      }
      .label {
        font-size: 24rpx;
        color: #999;
        margin-bottom: 8rpx;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        word-break: break-all;
      }
    }
  }
  .progress-container {
    display: flex;
    align-items: center;
    gap: 20rpx;
    up-line-progress {
      flex: 1;
    }
    .progress-text {
      font-size: 24rpx;
      color: #666;
      min-width: 60rpx;
    }
  }
  .section-title {
    font-size: 32rpx;
    font-weight: bold;
    color: #333;
    margin: 32rpx 0 20rpx;
  }
  .work-order-card {
    background: #fff;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20rpx;
      padding-bottom: 16rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .work-order-no {
        font-size: 28rpx;
        font-weight: bold;
        color: #3c9cff;
      }
      .progress-tag {
        font-size: 28rpx;
        font-weight: bold;
      }
    }
    .card-content {
      .content-row {
        margin-bottom: 12rpx;
        font-size: 26rpx;
        .label {
          color: #999;
        }
        .value {
          color: #333;
        }
      }
    }
    .card-footer {
      display: flex;
      justify-content: flex-end;
      gap: 20rpx;
      margin-top: 20rpx;
    }
  }
  .popup-content {
    background: #fff;
    padding: 30rpx;
    max-height: 80vh;
    display: flex;
    flex-direction: column;
    border-radius: 20rpx 20rpx 0 0;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 30rpx;
      .popup-title {
        font-size: 34rpx;
        font-weight: bold;
        color: #333;
      }
    }
    .popup-scroll {
      flex: 1;
      overflow: hidden;
    }
  }
  .detail-info {
    background: #f8f9fa;
    padding: 24rpx;
    border-radius: 16rpx;
    margin-bottom: 30rpx;
    flex-direction: column;
    .info-row {
      margin-bottom: 12rpx;
      font-size: 28rpx;
      display: flex;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #999;
        min-width: 140rpx;
      }
      .value {
        color: #333;
        flex: 1;
        font-weight: 500;
      }
    }
  }
  .list-title {
    font-size: 30rpx;
    font-weight: bold;
    margin-bottom: 20rpx;
    color: #333;
    display: flex;
    align-items: center;
    &::before {
      content: "";
      width: 6rpx;
      height: 28rpx;
      background: #3c9cff;
      margin-right: 12rpx;
      border-radius: 4rpx;
    }
  }
  .detail-item {
    background: #fff;
    border: 1rpx solid #f0f0f0;
    border-radius: 12rpx;
    padding: 24rpx;
    margin-bottom: 20rpx;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .item-main {
      flex: 1;
      .item-row {
        font-size: 26rpx;
        margin-bottom: 8rpx;
        display: flex;
        &:last-child {
          margin-bottom: 0;
        }
        .label {
          color: #999;
          min-width: 130rpx;
        }
        .value {
          color: #333;
          flex: 1;
        }
      }
    }
    .item-actions {
      display: flex;
      flex-direction: column;
      gap: 16rpx;
      padding-left: 20rpx;
      border-left: 1rpx solid #f0f0f0;
      .action-link {
        font-size: 26rpx;
        color: #3c9cff;
        white-space: nowrap;
        &.green {
          color: #52c41a;
        }
      }
    }
  }
  .quality-record {
    background: #fff;
    border: 1rpx solid #f0f0f0;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 30rpx;
    .record-title {
      font-size: 30rpx;
      font-weight: bold;
      color: #3c9cff;
      margin-bottom: 24rpx;
      display: flex;
      justify-content: space-between;
    }
  }
  .params-table {
    margin-top: 24rpx;
    border: 1rpx solid #f0f0f0;
    border-radius: 12rpx;
    overflow: hidden;
    .table-header {
      display: flex;
      background: #f8f9fa;
      padding: 20rpx 16rpx;
      font-size: 26rpx;
      font-weight: bold;
      color: #666;
    }
    .table-row {
      display: flex;
      padding: 20rpx 16rpx;
      font-size: 26rpx;
      border-top: 1rpx solid #f0f0f0;
      color: #333;
      &:nth-child(even) {
        background: #fafafa;
      }
    }
    .col {
      flex: 1;
      text-align: center;
      word-break: break-all;
    }
  }
  .modal-content {
    padding: 30rpx;
    .param-row {
      margin-bottom: 20rpx;
      font-size: 28rpx;
      display: flex;
      &:last-child {
        margin-bottom: 0;
      }
      .label {
        color: #666;
        min-width: 160rpx;
      }
      .value {
        color: #333;
        font-weight: 500;
        flex: 1;
      }
    }
  }
  .input-list-popup {
    .input-item {
      background: #fff;
      border: 1rpx solid #f0f0f0;
      border-radius: 12rpx;
      padding: 20rpx;
      margin-bottom: 20rpx;
      .input-row {
        display: flex;
        font-size: 26rpx;
        margin-bottom: 8rpx;
        &:last-child {
          margin-bottom: 0;
        }
        .label {
          color: #999;
          min-width: 160rpx;
        }
        .value {
          color: #333;
          flex: 1;
        }
      }
    }
  }
  .error-text {
    color: #f56c6c;
    font-weight: bold;
  }
  .no-data-minor {
    text-align: center;
    padding: 60rpx 40rpx;
    color: #999;
    font-size: 28rpx;
  }
</style>
src/pages/works.vue
@@ -589,14 +589,26 @@
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "生产报工",
    },
    // {
    //   icon: "/static/images/icon/shengchanbaogong.svg",
    //   label: "生产工单",
    // },
    // {
    //   icon: "/static/images/icon/shengchanhesuan@2x.svg",
    //   label: "生产核算",
    // },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "报工台账",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "生产核算",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "生产追溯",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "工序生产实况",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "工序实况",
    },
  ]);
  // è®¾å¤‡ç®¡ç†åŠŸèƒ½æ•°æ®
@@ -860,11 +872,26 @@
      case "生产报工":
        getcode();
        break;
      case "报工台账":
        uni.navigateTo({
          url: "/pages/productionManagement/productionReporting/ledger",
        });
        break;
      case "生产核算":
        uni.navigateTo({
          url: "/pages/productionManagement/productionAccounting/index",
        });
        break;
      case "生产追溯":
        uni.navigateTo({
          url: "/pages/productionManagement/productionTraceability/index",
        });
        break;
      case "工序生产实况":
        uni.navigateTo({
          url: "/pages/productionManagement/processStatistics/index",
        });
        break;
      case "设备台账":
        uni.navigateTo({
          url: "/pages/equipmentManagement/ledger/index",