天津潜宇塑胶app
1.添加采购需求、库存盘点、计件单价配置、产量统计、材料消耗核算、成本核算分析页面
已添加13个文件
已修改25个文件
8406 ■■■■ 文件已修改
src/api/basicData/product.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockCheck.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementDemand.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/bom.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/costAccounting.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/materialConsumption.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/pieceRateConfig.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductMain.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/materialInspection.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/productionTrace.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockCheck/detail.vue 626 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockCheck/index.vue 832 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/index.vue 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/procurementDemand/index.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/procurementManagement/procurementLedger/detail.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionDesign/bom/index.vue 279 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/costAccounting/index.vue 662 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/materialConsumption/index.vue 507 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/outputStatistics/index.vue 412 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/pieceRateConfig/index.vue 698 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionAccounting/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReport/index.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionReporting/ledger.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionTraceability/index.vue 1970 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/add.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/detail.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/add.vue 124 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/add.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/detail.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/common.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/product.js
@@ -56,3 +56,19 @@
        params: query
    })
}
// æ¡ç ç²¾ç¡®æŸ¥è¯¢è§„格(扫码回填)
export function modelByBarcode(query) {
    return request({
        url: '/basic/product/model/byBarcode',
        method: 'get',
        params: query
    })
}
// è§„格分页查询(支持条码模糊)
export function pageModel(query) {
    return request({
        url: '/basic/product/pageModel',
        method: 'get',
        params: query
    })
}
src/api/inventoryManagement/stockCheck.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
import request from "@/utils/request";
// å‘起盘点(不传 productModelIds = å…¨éƒ¨åº“存行快照)
export const start = params => {
  return request({
    url: "/stockCheck/start",
    method: "post",
    data: params,
  });
};
// ç›˜ç‚¹å•分页列表
export const listPage = params => {
  return request({
    url: "/stockCheck/listPage",
    method: "get",
    params,
  });
};
// ç›˜ç‚¹å•详情
export const getDetail = id => {
  return request({
    url: `/stockCheck/${id}`,
    method: "get",
  });
};
// ç›˜ç‚¹æ˜Žç»†åˆ†é¡µï¼ˆonlyDiff=true åªçœ‹å·²å½•入且有差异的行)
export const detailPage = params => {
  return request({
    url: "/stockCheck/detailPage",
    method: "get",
    params,
  });
};
// æ‰¹é‡å½•入实盘(body ä¸ºæ•°ç»„)
export const entry = params => {
  return request({
    url: "/stockCheck/entry",
    method: "put",
    data: params,
  });
};
// å®Œæˆç›˜ç‚¹ï¼ˆå·®å¼‚生成待审核调账单据)
export const finish = params => {
  return request({
    url: "/stockCheck/finish",
    method: "post",
    data: params,
  });
};
// å–消盘点
export const cancel = params => {
  return request({
    url: "/stockCheck/cancel",
    method: "post",
    data: params,
  });
};
// å·®å¼‚分析列表
export const diffList = params => {
  return request({
    url: "/stockCheck/diffList",
    method: "get",
    params,
  });
};
src/api/inventoryManagement/stockInventory.js
@@ -1,4 +1,5 @@
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢åº“存记录列表
export const getStockInventoryListPage = params => {
  return request({
@@ -76,3 +77,12 @@
    params: { productModelId },
  });
};
// åˆ†é¡µæŸ¥è¯¢åº“存预警列表(可用数量 â‰¤ é¢„警值,预警值>0)
export const getStockWarnPage = params => {
  return request({
    url: "/stockInventory/warnPage",
    method: "get",
    params,
  });
};
src/api/procurementManagement/procurementDemand.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from "@/utils/request";
// é‡‡è´­éœ€æ±‚分页查询
export const listPage = params => {
  return request({
    url: "/procurementDemand/listPage",
    method: "get",
    params,
  });
};
// é‡‡è´­éœ€æ±‚状态变更(params: { ids, status },ids æ”¯æŒé€—号分隔的多个 id)
export const changeStatus = params => {
  return request({
    url: "/procurementDemand/changeStatus",
    method: "put",
    params,
  });
};
// åˆ é™¤é‡‡è´­éœ€æ±‚(body ä¸º id æ•°ç»„,仅待处理可删)
export const del = ids => {
  return request({
    url: "/procurementDemand/del",
    method: "delete",
    data: ids,
  });
};
src/api/productionManagement/bom.js
@@ -80,3 +80,32 @@
    method: "delete",
  });
}
// --- BOM ç‰ˆæœ¬å¯ç”¨/停用、齐套分析 ---
// BOM å¯ç”¨/停用(status: 1启用 0停用,启用时后端自动停用同规格其它版本)
export function changeStatus(data) {
  return request({
    url: "/technologyBom/changeStatus",
    method: "put",
    data: data,
  });
}
// BOM é½å¥—分析(data: { bomId, demandQty })
export function kitCheck(data) {
  return request({
    url: "/technologyBom/kitCheck",
    method: "post",
    data: data,
  });
}
// ç¼ºå£ç”Ÿæˆé‡‡è´­éœ€æ±‚(data åŒ kitCheck)
export function generateDemand(data) {
  return request({
    url: "/technologyBom/kitCheck/generateDemand",
    method: "post",
    data: data,
  });
}
src/api/productionManagement/costAccounting.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
// æˆæœ¬æ ¸ç®—分析页面接口
import request from "@/utils/request";
// æˆæœ¬æ ¸ç®—明细(按生产订单)
export function costStat(query) {
  return request({
    url: "/costAccounting/stat",
    method: "get",
    params: query,
  });
}
// æˆæœ¬æž„成汇总(groupBy: month / product)
export function costSummary(query) {
  return request({
    url: "/costAccounting/summary",
    method: "get",
    params: query,
  });
}
// ç¼ºä»·ç‰©æ–™æ¸…单
export function costMissingPrice(query) {
  return request({
    url: "/costAccounting/missingPrice",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/materialConsumption.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// ææ–™æ¶ˆè€—核算页面接口
import request from "@/utils/request";
// ææ–™æ¶ˆè€—核算列表(按生产订单×物料返回,非分页)
export function stat(query) {
  return request({
    url: "/materialConsumption/stat",
    method: "get",
    params: query,
  });
}
src/api/productionManagement/pieceRateConfig.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
// è®¡ä»¶å•价配置接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢è®¡ä»¶å•价配置
export const listPage = params => {
  return request({
    url: "/pieceRateConfig/listPage",
    method: "get",
    params,
  });
};
// æ–°å¢žè®¡ä»¶å•价配置
export const add = data => {
  return request({
    url: "/pieceRateConfig/add",
    method: "post",
    data,
  });
};
// ä¿®æ”¹è®¡ä»¶å•价配置
export const update = data => {
  return request({
    url: "/pieceRateConfig/update",
    method: "put",
    data,
  });
};
// å¯ç”¨/停用
export const changeStatus = data => {
  return request({
    url: "/pieceRateConfig/changeStatus",
    method: "put",
    data,
  });
};
// æ‰¹é‡åˆ é™¤ï¼Œdata ä¸º id æ•°ç»„
export const batchDelete = data => {
  return request({
    url: "/pieceRateConfig/batchDelete",
    method: "delete",
    data,
  });
};
src/api/productionManagement/productionProductMain.js
@@ -27,3 +27,21 @@
        params: query,
    });
}
// è®¡ä»¶å·¥èµ„汇总(按人员)
export function wageSummary(query) {
    return request({
        url: "/productionProductMain/wageSummary",
        method: "get",
        params: query,
    });
}
// äº§é‡ç»Ÿè®¡ï¼ˆdimension: device/operation/product)
export function outputStat(query) {
    return request({
        url: "/productionProductMain/outputStat",
        method: "get",
        params: query,
    });
}
src/api/qualityManagement/materialInspection.js
@@ -163,4 +163,22 @@
        url: "/productProcess/list",
        method: "get",
    });
}
// æŸ¥è¯¢ä¸åˆæ ¼è®°å½•列表(inspectState:0待处理/1已处理)
export function qualityUnqualifiedListPage(query) {
    return request({
        url: '/quality/qualityUnqualified/listPage',
        method: 'get',
        params: query,
    })
}
// å¤„理不合格记录(不传 dealName/dealTime,后端自动取当前登录人/时间)
export function qualityUnqualifiedDeal(data) {
    return request({
        url: '/quality/qualityUnqualified/deal',
        method: 'post',
        data: data,
    })
}
src/api/qualityManagement/productionTrace.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from "@/utils/request";
// æ­£å‘追溯:成品批号 -> å…¨é“¾è·¯
export const traceForward = params => {
  return request({
    url: "/quality/trace/forward",
    method: "get",
    params,
  });
};
// åå‘追溯:原料批号 -> å—影响成品
export const traceReverse = params => {
  return request({
    url: "/quality/trace/reverse",
    method: "get",
    params,
  });
};
// æ‰¹å·å€™é€‰ä¸‹æ‹‰ï¼ˆtype: finished | material)
export const traceBatchOptions = params => {
  return request({
    url: "/quality/trace/batchOptions",
    method: "get",
    params,
  });
};
src/config.js
@@ -1,7 +1,7 @@
// åº”用全局配置
const config = {
  baseUrl: "http://1.15.17.182:9072",
  fileUrl: "http://1.15.17.182:9071",
  baseUrl: "http://1.15.17.182:9116",
  fileUrl: "http://1.15.17.182:9115",
  // åº”用信息
  appInfo: {
    // åº”用名称
src/pages.json
@@ -353,6 +353,13 @@
      }
    },
    {
      "path": "pages/procurementManagement/procurementDemand/index",
      "style": {
        "navigationBarTitleText": "采购需求",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/basicData/supplierManage/index",
      "style": {
        "navigationBarTitleText": "供应商管理",
@@ -894,6 +901,34 @@
      }
    },
    {
      "path": "pages/productionManagement/pieceRateConfig/index",
      "style": {
        "navigationBarTitleText": "计件单价配置",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/outputStatistics/index",
      "style": {
        "navigationBarTitleText": "产量统计",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/materialConsumption/index",
      "style": {
        "navigationBarTitleText": "材料消耗核算",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/productionManagement/costAccounting/index",
      "style": {
        "navigationBarTitleText": "成本核算分析",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inventoryManagement/receiptManagement/index",
      "style": {
        "navigationBarTitleText": "自定义入库",
@@ -922,6 +957,20 @@
      }
    },
    {
      "path": "pages/inventoryManagement/stockCheck/index",
      "style": {
        "navigationBarTitleText": "库存盘点",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inventoryManagement/stockCheck/detail",
      "style": {
        "navigationBarTitleText": "盘点详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/safeQualifications/index",
      "style": {
        "navigationBarTitleText": "规程与资质",
src/pages/inventoryManagement/stockCheck/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,626 @@
<template>
  <view class="stock-check-detail">
    <PageHeader title="盘点详情"
                @back="goBack" />
    <scroll-view class="scroll-body"
                 scroll-y
                 @scrolltolower="loadMore">
      <!-- ä¸»è¡¨ä¿¡æ¯ -->
      <view class="main-card">
        <view class="main-header">
          <text class="main-no">{{ mainInfo.checkNo || '-' }}</text>
          <up-tag :text="getStatusText(mainInfo.status)"
                  :type="getStatusType(mainInfo.status)"
                  size="mini" />
        </view>
        <view class="detail-row">
          <text class="detail-label">盘点名称</text>
          <text class="detail-value">{{ mainInfo.checkName || '-' }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">盘点日期</text>
          <text class="detail-value">{{ formatDate(mainInfo.checkDate) }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">盘点人</text>
          <text class="detail-value">{{ mainInfo.checker || '-' }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">盘点种类数</text>
          <text class="detail-value">{{ Number(mainInfo.totalKinds) || 0 }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">差异种类数</text>
          <text class="detail-value"
                :class="{ danger: Number(mainInfo.diffKinds) > 0 }">{{ Number(mainInfo.diffKinds) || 0 }}</text>
        </view>
        <view class="detail-row"
              v-if="mainInfo.remark">
          <text class="detail-label">备注</text>
          <text class="detail-value">{{ mainInfo.remark }}</text>
        </view>
      </view>
      <!-- å·¥å…·æ¡ -->
      <view class="toolbar">
        <view class="switch-row">
          <text class="switch-label">只看差异</text>
          <up-switch v-model="onlyDiff"
                     size="small"
                     activeColor="#2979ff"
                     @change="handleQuery"></up-switch>
        </view>
        <view class="toolbar-btns">
          <up-button type="primary"
                     size="small"
                     plain
                     text="差异分析"
                     @click="openDiffPopup"></up-button>
        </view>
      </view>
      <!-- æ˜Žç»†åˆ—表 -->
      <view v-if="detailList.length > 0">
        <view v-for="row in detailList"
              :key="row.id"
              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">{{ row.productName || '-' }}</text>
            </view>
            <up-tag v-if="Number(row.adjusted) === 1"
                    text="已生成调账"
                    type="success"
                    size="mini" />
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ row.productModel || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">单位</text>
              <text class="detail-value">{{ row.unit || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">批号</text>
              <text class="detail-value">{{ row.batchNo || '-' }}</text>
            </view>
            <view class="qty-row">
              <view class="qty-item">
                <text class="qty-label">账面数量</text>
                <text class="qty-value">{{ formatQty(row.bookQty) }}</text>
              </view>
              <view class="qty-item">
                <text class="qty-label">实盘数量</text>
                <up-input class="qty-input"
                          type="digit"
                          :disabled="isFinished"
                          v-model="row.actualQty"
                          placeholder="未录入"
                          @change="onQtyChange(row)"
                          @blur="onQtyBlur(row)" />
              </view>
              <view class="qty-item">
                <text class="qty-label">差异</text>
                <text class="qty-value"
                      :class="diffClass(row)">{{ formatDiff(row) }}</text>
              </view>
            </view>
            <view class="detail-row remark-row">
              <text class="detail-label">备注</text>
              <up-input class="remark-input"
                        :disabled="isFinished"
                        v-model="row.remark"
                        placeholder="请输入备注"
                        maxlength="200"
                        @change="onRemarkChange(row)" />
            </view>
          </view>
        </view>
        <up-loadmore :status="loadStatus" />
      </view>
      <view v-else-if="!loading"
            class="no-data">
        <up-empty mode="data"
                  text="暂无盘点明细"></up-empty>
      </view>
      <view class="tip-text"
            v-if="!isFinished">
        æç¤ºï¼šå®Œæˆç›˜ç‚¹åŽå·®å¼‚将以待审核出入库记录形式存在,需在入库记录/出库记录页审核通过后库存才变动。
      </view>
    </scroll-view>
    <!-- åº•部操作 -->
    <view class="footer-btns">
      <up-button type="primary"
                 text="批量保存"
                 :loading="saving"
                 :disabled="isFinished"
                 @click="handleSave"></up-button>
      <up-button v-if="!isFinished"
                 type="success"
                 text="完成盘点"
                 @click="handleFinish"></up-button>
    </view>
    <!-- å·®å¼‚分析弹窗 -->
    <up-popup :show="showDiffPopup"
              mode="bottom"
              :round="10"
              closeable
              @close="showDiffPopup = false">
      <view class="form-popup">
        <view class="popup-header">
          <text class="popup-title">差异分析</text>
        </view>
        <scroll-view class="popup-content"
                     scroll-y>
          <view v-if="diffRows.length">
            <view v-for="(row, index) in diffRows"
                  :key="row.id || index"
                  class="diff-item">
              <view class="diff-head">
                <text class="diff-name">{{ row.productName || '-' }}</text>
                <text class="diff-qty"
                      :class="Number(row.diffQty) > 0 ? 'diff-up' : 'diff-down'">{{ formatSigned(row.diffQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">规格型号</text>
                <text class="detail-value">{{ row.productModel || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">批号</text>
                <text class="detail-value">{{ row.batchNo || '-' }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">账面 / å®žç›˜</text>
                <text class="detail-value">{{ formatQty(row.bookQty) }} / {{ formatQty(row.actualQty) }}</text>
              </view>
            </view>
          </view>
          <view v-else
                class="spec-empty-box">
            <up-empty mode="data"
                      text="暂无差异数据"></up-empty>
          </view>
        </scroll-view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  import { toast } from "@/utils/common";
  import {
    getDetail,
    detailPage,
    entry,
    finish,
    diffList,
  } from "@/api/inventoryManagement/stockCheck.js";
  const mainId = ref(null);
  const mainInfo = ref({});
  const detailList = ref([]);
  const loading = ref(false);
  const saving = ref(false);
  const loadStatus = ref("loadmore");
  const page = reactive({ current: 1, size: 10, total: 0 });
  const onlyDiff = ref(false);
  const dirtyIds = ref([]);
  const showDiffPopup = ref(false);
  const diffRows = ref([]);
  const isFinished = computed(() => Number(mainInfo.value.status) !== 0);
  const goBack = () => {
    uni.navigateBack();
  };
  const formatDate = val => (val ? dayjs(val).format("YYYY-MM-DD") : "-");
  const getStatusText = status => {
    const map = { 0: "盘点中", 1: "已完成", 2: "已取消" };
    return map[status] || "未知";
  };
  const getStatusType = status => {
    const map = { 0: "warning", 1: "success", 2: "info" };
    return map[status] || "info";
  };
  const formatQty = val => {
    if (val === null || val === undefined || val === "") return "-";
    return Number(val) || 0;
  };
  const formatSigned = val => {
    const n = Number(val) || 0;
    return n > 0 ? `+${n}` : `${n}`;
  };
  const formatDiff = row => {
    const a = row.actualQty;
    if (a === null || a === undefined || a === "") return "-";
    const d = Number(a) - (Number(row.bookQty) || 0);
    if (!Number.isFinite(d)) return "-";
    return formatSigned(Number(d.toFixed(4)));
  };
  const diffClass = row => {
    const a = row.actualQty;
    if (a === null || a === undefined || a === "") return "";
    const d = Number(a) - (Number(row.bookQty) || 0);
    if (d > 0) return "diff-up";
    if (d < 0) return "diff-down";
    return "";
  };
  const loadMain = async () => {
    try {
      const res = await getDetail(mainId.value);
      mainInfo.value = res?.data || {};
    } catch (e) {
      // è¯·æ±‚失败已由 request ç»Ÿä¸€æç¤º
    }
  };
  const getList = () => {
    if (loading.value) return;
    loading.value = true;
    loadStatus.value = "loading";
    detailPage({
      mainId: mainId.value,
      onlyDiff: onlyDiff.value,
      current: page.current,
      size: page.size,
    })
      .then(res => {
        const records = res?.data?.records || [];
        detailList.value =
          page.current === 1 ? records : [...detailList.value, ...records];
        page.total = res?.data?.total || 0;
        loadStatus.value =
          detailList.value.length >= page.total ? "nomore" : "loadmore";
      })
      .catch(() => {
        loadStatus.value = "loadmore";
      })
      .finally(() => {
        loading.value = false;
      });
  };
  const handleQuery = () => {
    page.current = 1;
    detailList.value = [];
    dirtyIds.value = [];
    getList();
  };
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
  const markDirty = row => {
    if (row.id == null) return;
    if (!dirtyIds.value.includes(row.id)) dirtyIds.value.push(row.id);
  };
  const onQtyChange = row => {
    markDirty(row);
  };
  const onRemarkChange = row => {
    markDirty(row);
  };
  // å®žç›˜æ•°é‡ä¸å…è®¸è´Ÿæ•°
  const onQtyBlur = row => {
    const val = row.actualQty;
    if (val === null || val === undefined || val === "") return;
    let n = Number(val);
    if (!Number.isFinite(n)) {
      row.actualQty = "";
      return;
    }
    if (n < 0) {
      n = 0;
      toast("实盘数量不能为负数,已置为 0");
    }
    row.actualQty = n;
    markDirty(row);
  };
  const handleSave = async () => {
    if (isFinished.value) {
      toast("盘点已结束,无法录入");
      return;
    }
    const rows = detailList.value.filter(
      r => dirtyIds.value.includes(r.id) && r.actualQty !== "" && r.actualQty !== null && r.actualQty !== undefined
    );
    if (!rows.length) {
      toast("没有需要保存的实盘录入");
      return;
    }
    const negative = rows.find(r => Number(r.actualQty) < 0);
    if (negative) {
      toast("实盘数量不能为负数");
      return;
    }
    saving.value = true;
    uni.showLoading({ title: "提交中...", mask: true });
    try {
      await entry(
        rows.map(r => ({
          id: r.id,
          actualQty: Number(r.actualQty) || 0,
          remark: r.remark || undefined,
        }))
      );
      uni.hideLoading();
      uni.showToast({ title: "保存成功", icon: "success" });
      dirtyIds.value = [];
      loadMain();
      handleQuery();
    } catch (e) {
      uni.hideLoading();
    } finally {
      saving.value = false;
    }
  };
  const handleFinish = () => {
    uni.showModal({
      title: "完成盘点",
      content: "差异将生成调账单据,审核通过后账面生效,是否继续?",
      success: async res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        try {
          const r = await finish({ id: mainId.value });
          uni.hideLoading();
          uni.showToast({ title: r?.msg || "盘点已完成", icon: "none" });
          loadMain();
          handleQuery();
        } catch (e) {
          uni.hideLoading();
        }
      },
    });
  };
  const openDiffPopup = async () => {
    showDiffPopup.value = true;
    try {
      const res = await diffList({ mainId: mainId.value });
      diffRows.value = res?.data || [];
    } catch (e) {
      diffRows.value = [];
    }
  };
  onLoad(options => {
    mainId.value = options?.id;
    if (!mainId.value) {
      toast("缺少盘点单参数");
      return;
    }
    loadMain();
    getList();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .stock-check-detail {
    height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .scroll-body {
    flex: 1;
    height: 0;
    padding: 20rpx 20rpx 120rpx;
    box-sizing: border-box;
  }
  .main-card {
    background: #fff;
    border-radius: 16rpx;
    padding: 24rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
    .main-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-bottom: 16rpx;
      margin-bottom: 16rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .main-no {
        font-size: 30rpx;
        font-weight: bold;
        color: #303133;
      }
    }
  }
  .detail-value.danger {
    color: #ee0a24;
    font-weight: 600;
  }
  .toolbar {
    background: #fff;
    border-radius: 16rpx;
    padding: 16rpx 24rpx;
    margin-bottom: 20rpx;
    .switch-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 0 16rpx;
      .switch-label {
        font-size: 28rpx;
        color: #333;
      }
    }
    .toolbar-btns {
      display: flex;
      gap: 20rpx;
      justify-content: flex-end;
    }
  }
  .qty-row {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin: 16rpx 0;
    .qty-item {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      .qty-label {
        font-size: 22rpx;
        color: #999;
        margin-bottom: 8rpx;
      }
      .qty-value {
        font-size: 30rpx;
        font-weight: bold;
        color: #303133;
        &.diff-up {
          color: #52c41a;
        }
        &.diff-down {
          color: #f56c6c;
        }
      }
      .qty-input {
        width: 100%;
        background: #f5f7fa;
        border-radius: 8rpx;
        padding: 0 8rpx;
      }
    }
  }
  .remark-row {
    align-items: center;
    .remark-input {
      flex: 1;
      background: #f5f7fa;
      border-radius: 8rpx;
      padding: 0 12rpx;
      margin-left: 16rpx;
    }
  }
  .no-data {
    padding-top: 150rpx;
  }
  .tip-text {
    font-size: 22rpx;
    color: #909399;
    line-height: 1.6;
    padding: 16rpx 8rpx 40rpx;
  }
  .form-popup {
    height: 70vh;
    display: flex;
    flex-direction: column;
    background: #f5f5f5;
  }
  .popup-content {
    flex: 1;
    height: 0;
    padding: 12px;
  }
  .diff-item {
    background: #fff;
    border-radius: 10px;
    padding: 12px;
    margin-bottom: 10px;
    .diff-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 10px;
      .diff-name {
        font-size: 14px;
        font-weight: 600;
        color: #333;
      }
      .diff-qty {
        font-size: 16px;
        font-weight: 600;
      }
    }
  }
  .diff-up {
    color: #52c41a;
  }
  .diff-down {
    color: #f56c6c;
  }
  .spec-empty-box {
    padding-top: 60px;
  }
  .popup-footer {
    display: flex;
    gap: 12px;
    padding: 12px 16px;
    background: #fff;
    border-top: 1px solid #f0f0f0;
  }
  .btn-cancel,
  .btn-confirm {
    flex: 1;
  }
</style>
src/pages/inventoryManagement/stockCheck/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,832 @@
<template>
  <view class="stock-check-page">
    <PageHeader title="库存盘点"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-row">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="searchForm.checkNo"
                    placeholder="盘点单号"
                    clearable
                    @confirm="handleQuery" />
        </view>
        <view class="search-input">
          <up-input class="search-text"
                    v-model="searchForm.checkName"
                    placeholder="盘点名称"
                    clearable
                    @confirm="handleQuery" />
        </view>
      </view>
      <view class="filter-row">
        <view class="filter-item"
              @click="showStatusSheet = true">
          <text class="filter-text">{{ statusText }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#909399"></up-icon>
        </view>
        <view class="filter-item date-item"
              @click="showCalendar = true">
          <up-icon name="calendar"
                   size="16"
                   color="#909399"></up-icon>
          <text class="filter-text"
                :class="{ placeholder: !searchForm.beginTime }">{{ dateRangeText }}</text>
          <up-icon v-if="searchForm.beginTime"
                   name="close-circle-fill"
                   size="16"
                   color="#c0c4cc"
                   @click.stop="handleClearDate"></up-icon>
        </view>
        <view class="search-btn">
          <up-button type="primary"
                     size="small"
                     text="搜索"
                     @click="handleQuery"></up-button>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <scroll-view scroll-y
                 class="list-container"
                 v-if="tableData.length > 0"
                 @scrolltolower="loadMore">
      <view v-for="item in tableData"
            :key="item.id"
            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.checkNo || '-' }}</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">{{ item.checkName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">盘点日期</text>
            <text class="detail-value">{{ formatDate(item.checkDate) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">盘点人</text>
            <text class="detail-value">{{ item.checker || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">盘点种类数</text>
            <text class="detail-value">{{ Number(item.totalKinds) || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">差异种类数</text>
            <text class="detail-value"
                  :class="{ danger: Number(item.diffKinds) > 0 }">{{ Number(item.diffKinds) || 0 }}</text>
          </view>
        </view>
        <view class="action-buttons">
          <up-button type="info"
                     size="small"
                     plain
                     text="详情"
                     @click="goDetail(item)"></up-button>
          <up-button v-if="item.status === 0"
                     type="success"
                     size="small"
                     plain
                     text="完成"
                     @click="handleFinish(item)"></up-button>
          <up-button v-if="item.status === 0"
                     type="error"
                     size="small"
                     plain
                     text="取消"
                     @click="handleCancel(item)"></up-button>
        </view>
      </view>
      <up-loadmore :status="loadStatus" />
    </scroll-view>
    <view v-else-if="!loading"
          class="no-data">
      <up-empty mode="data"
                text="暂无盘点数据"></up-empty>
    </view>
    <!-- åº•部操作 -->
    <view class="footer-btns">
      <up-button type="primary"
                 text="发起盘点"
                 @click="openAdd"></up-button>
    </view>
    <!-- å‘起盘点弹窗 -->
    <up-popup :show="showAddPopup"
              mode="bottom"
              :round="10"
              closeable
              @close="showAddPopup = false">
      <view class="form-popup">
        <view class="popup-header">
          <text class="popup-title">发起盘点</text>
        </view>
        <scroll-view class="popup-content"
                     scroll-y>
          <view class="form-field">
            <text class="field-label required">盘点名称</text>
            <up-input v-model="form.checkName"
                      placeholder="请输入盘点名称"
                      maxlength="100" />
          </view>
          <view class="form-field">
            <text class="field-label">盘点日期</text>
            <view class="pick-field"
                  @click="showDatePicker = true">
              <up-input class="pick-input"
                        v-model="form.checkDate"
                        readonly
                        placeholder="请选择盘点日期" />
              <up-icon name="arrow-right"
                       color="#c0c4cc"></up-icon>
            </view>
          </view>
          <view class="form-field">
            <text class="field-label">盘点人</text>
            <up-input v-model="form.checker"
                      placeholder="请输入盘点人" />
          </view>
          <view class="form-field">
            <text class="field-label">备注</text>
            <up-input v-model="form.remark"
                      placeholder="请输入备注"
                      maxlength="200" />
          </view>
          <view class="form-field">
            <text class="field-label">指定产品规格</text>
            <view class="spec-box">
              <view v-if="selectedModels.length"
                    class="spec-tags">
                <view v-for="m in selectedModels"
                      :key="m.id"
                      class="spec-tag"
                      @click="removeModel(m.id)">
                  <text class="spec-tag-text">{{ m.model || m.productName }}</text>
                  <up-icon name="close"
                           size="12"
                           color="#2979ff"></up-icon>
                </view>
              </view>
              <text v-else
                    class="spec-empty">未选择时对全部库存行做快照</text>
              <up-button type="primary"
                         size="small"
                         plain
                         text="选择规格"
                         @click="openSpecPicker"></up-button>
            </view>
          </view>
        </scroll-view>
        <view class="popup-footer">
          <up-button class="btn-cancel"
                     text="取消"
                     @click="showAddPopup = false"></up-button>
          <up-button class="btn-confirm"
                     type="primary"
                     text="确认"
                     :loading="submitting"
                     @click="handleSubmit"></up-button>
        </view>
      </view>
    </up-popup>
    <!-- è§„格选择弹窗 -->
    <up-popup :show="showSpecPopup"
              mode="bottom"
              :round="10"
              closeable
              @close="showSpecPopup = false">
      <view class="form-popup">
        <view class="popup-header">
          <text class="popup-title">选择产品规格</text>
        </view>
        <view class="spec-category">
          <scroll-view scroll-x
                       class="category-scroll">
            <view class="category-list">
              <view v-for="p in productOptions"
                    :key="p.id"
                    class="category-tag"
                    :class="{ active: p.id === activeProductId }"
                    @click="selectProduct(p)">
                <text>{{ p.productName }}</text>
              </view>
            </view>
          </scroll-view>
        </view>
        <scroll-view class="popup-content"
                     scroll-y>
          <view v-if="modelOptions.length">
            <view v-for="m in modelOptions"
                  :key="m.id"
                  class="model-item"
                  @click="toggleModel(m)">
              <view class="model-info">
                <text class="model-name">{{ m.model || '-' }}</text>
                <text class="model-unit">{{ m.unit || '' }}</text>
              </view>
              <up-icon :name="isSelected(m.id) ? 'checkmark-circle-fill' : 'circle'"
                       size="22"
                       :color="isSelected(m.id) ? '#2979ff' : '#c0c4cc'"></up-icon>
            </view>
          </view>
          <view v-else
                class="spec-empty-box">
            <up-empty mode="data"
                      text="暂无规格数据"></up-empty>
          </view>
        </scroll-view>
        <view class="popup-footer">
          <up-button class="btn-cancel"
                     text="清空"
                     @click="selectedModels = []"></up-button>
          <up-button class="btn-confirm"
                     type="primary"
                     text="确定"
                     @click="showSpecPopup = false"></up-button>
        </view>
      </view>
    </up-popup>
    <!-- çŠ¶æ€ç­›é€‰ -->
    <up-action-sheet :show="showStatusSheet"
                     :actions="statusActions"
                     title="盘点状态"
                     @select="onStatusSelect"
                     @close="showStatusSheet = false"></up-action-sheet>
    <!-- æ—¥æœŸåŒºé—´ -->
    <up-calendar :show="showCalendar"
                 mode="range"
                 :maxDate="maxDate"
                 minDate="2022-01-01"
                 :monthNum="monthNum"
                 @confirm="onDateConfirm"
                 @close="showCalendar = false"></up-calendar>
    <!-- ç›˜ç‚¹æ—¥æœŸ -->
    <up-popup :show="showDatePicker"
              mode="bottom"
              @close="showDatePicker = false">
      <up-datetime-picker :show="true"
                          v-model="dateValue"
                          mode="date"
                          @confirm="onCheckDateConfirm"
                          @cancel="showDatePicker = false" />
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  import { toast } from "@/utils/common";
  import { productTreeList, modelList } from "@/api/basicData/product.js";
  import {
    start,
    listPage,
    finish,
    cancel,
  } from "@/api/inventoryManagement/stockCheck.js";
  const loading = ref(false);
  const submitting = ref(false);
  const loadStatus = ref("loadmore");
  const tableData = ref([]);
  const page = reactive({ current: 1, size: 10, total: 0 });
  const searchForm = reactive({
    checkNo: "",
    checkName: "",
    status: null,
    beginTime: "",
    endTime: "",
  });
  const maxDate = dayjs().format("YYYY-MM-DD");
  const monthNum = computed(() => {
    const min = dayjs("2022-01-01");
    const max = dayjs(maxDate);
    return max.diff(min, "month") + 1;
  });
  const showCalendar = ref(false);
  const showStatusSheet = ref(false);
  const showDatePicker = ref(false);
  const dateValue = ref(new Date().getTime());
  const statusActions = ref([
    { name: "全部", value: null },
    { name: "盘点中", value: 0 },
    { name: "已完成", value: 1 },
    { name: "已取消", value: 2 },
  ]);
  const statusText = computed(() => {
    const hit = statusActions.value.find(a => a.value === searchForm.status);
    return hit ? hit.name : "全部";
  });
  const dateRangeText = computed(() => {
    if (searchForm.beginTime && searchForm.endTime) {
      return `${searchForm.beginTime.slice(0, 10)} è‡³ ${searchForm.endTime.slice(0, 10)}`;
    }
    return "选择日期区间";
  });
  // å‘起盘点表单
  const showAddPopup = ref(false);
  const form = reactive({
    checkName: "",
    checkDate: dayjs().format("YYYY-MM-DD"),
    checker: "",
    remark: "",
  });
  // è§„格选择
  const showSpecPopup = ref(false);
  const productOptions = ref([]);
  const modelOptions = ref([]);
  const activeProductId = ref(null);
  const selectedModels = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  const formatDate = val => (val ? dayjs(val).format("YYYY-MM-DD") : "-");
  const getStatusText = status => {
    const map = { 0: "盘点中", 1: "已完成", 2: "已取消" };
    return map[status] || "未知";
  };
  const getStatusType = status => {
    const map = { 0: "warning", 1: "success", 2: "info" };
    return map[status] || "info";
  };
  const getList = () => {
    loading.value = true;
    loadStatus.value = "loading";
    listPage({
      current: page.current,
      size: page.size,
      checkNo: searchForm.checkNo,
      checkName: searchForm.checkName,
      status: searchForm.status,
      beginTime: searchForm.beginTime,
      endTime: searchForm.endTime,
    })
      .then(res => {
        const records = res?.data?.records || [];
        tableData.value =
          page.current === 1 ? records : [...tableData.value, ...records];
        page.total = res?.data?.total || 0;
        loadStatus.value =
          tableData.value.length >= page.total ? "nomore" : "loadmore";
      })
      .catch(() => {
        loadStatus.value = "loadmore";
      })
      .finally(() => {
        loading.value = false;
      });
  };
  const handleQuery = () => {
    page.current = 1;
    tableData.value = [];
    getList();
  };
  const loadMore = () => {
    if (loadStatus.value === "nomore" || loading.value) return;
    page.current++;
    getList();
  };
  const onStatusSelect = item => {
    searchForm.status = item.value;
    showStatusSheet.value = false;
    handleQuery();
  };
  const onDateConfirm = e => {
    const list = Array.isArray(e) ? e : [e];
    searchForm.beginTime = `${list[0]} 00:00:00`;
    searchForm.endTime = `${list[list.length - 1]} 23:59:59`;
    showCalendar.value = false;
    handleQuery();
  };
  const handleClearDate = () => {
    searchForm.beginTime = "";
    searchForm.endTime = "";
    handleQuery();
  };
  const onCheckDateConfirm = e => {
    form.checkDate = dayjs(e.value).format("YYYY-MM-DD");
    showDatePicker.value = false;
  };
  const openAdd = () => {
    form.checkName = "";
    form.checkDate = dayjs().format("YYYY-MM-DD");
    form.checker = "";
    form.remark = "";
    selectedModels.value = [];
    showAddPopup.value = true;
  };
  const openSpecPicker = async () => {
    showSpecPopup.value = true;
    if (productOptions.value.length) return;
    try {
      const res = await productTreeList();
      const list = Array.isArray(res) ? res : res?.data || [];
      productOptions.value = list
        .filter(item => item.parentId === null)
        .map(item => ({ id: item.id, productName: item.productName }));
      if (productOptions.value.length) {
        selectProduct(productOptions.value[0]);
      }
    } catch (e) {
      // è¯·æ±‚失败已由 request ç»Ÿä¸€æç¤º
    }
  };
  const selectProduct = async p => {
    activeProductId.value = p.id;
    modelOptions.value = [];
    try {
      const res = await modelList({ id: p.id });
      const list = Array.isArray(res) ? res : res?.data || [];
      modelOptions.value = list.map(m => ({
        id: m.id,
        model: m.model,
        unit: m.unit,
        productName: p.productName,
      }));
    } catch (e) {
      // è¯·æ±‚失败已由 request ç»Ÿä¸€æç¤º
    }
  };
  const isSelected = id => selectedModels.value.some(m => m.id === id);
  const toggleModel = m => {
    const index = selectedModels.value.findIndex(item => item.id === m.id);
    if (index > -1) {
      selectedModels.value.splice(index, 1);
    } else {
      selectedModels.value.push(m);
    }
  };
  const removeModel = id => {
    const index = selectedModels.value.findIndex(item => item.id === id);
    if (index > -1) selectedModels.value.splice(index, 1);
  };
  const handleSubmit = async () => {
    if (!form.checkName || !form.checkName.trim()) {
      toast("请输入盘点名称");
      return;
    }
    submitting.value = true;
    uni.showLoading({ title: "提交中...", mask: true });
    try {
      await start({
        checkName: form.checkName.trim(),
        checkDate: form.checkDate,
        checker: form.checker,
        remark: form.remark,
        productModelIds: selectedModels.value.map(m => m.id),
      });
      uni.hideLoading();
      uni.showToast({ title: "发起成功", icon: "success" });
      showAddPopup.value = false;
      handleQuery();
    } catch (e) {
      uni.hideLoading();
    } finally {
      submitting.value = false;
    }
  };
  const goDetail = item => {
    uni.navigateTo({
      url: `/pages/inventoryManagement/stockCheck/detail?id=${item.id}`,
    });
  };
  const handleFinish = item => {
    uni.showModal({
      title: "完成盘点",
      content: "差异将生成调账单据,审核通过后账面生效,是否继续?",
      success: async res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        try {
          const r = await finish({ id: item.id });
          uni.hideLoading();
          uni.showToast({ title: r?.msg || "盘点已完成", icon: "none" });
          handleQuery();
        } catch (e) {
          uni.hideLoading();
        }
      },
    });
  };
  const handleCancel = item => {
    uni.showModal({
      title: "取消盘点",
      content: "取消后该盘点单不可再录入,是否继续?",
      success: async res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        try {
          const r = await cancel({ id: item.id });
          uni.hideLoading();
          uni.showToast({ title: r?.msg || "已取消", icon: "none" });
          handleQuery();
        } catch (e) {
          uni.hideLoading();
        }
      },
    });
  };
  onShow(() => {
    handleQuery();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .stock-check-page {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .search-section {
    background: #fff;
    padding: 10px 12px;
  }
  .search-row {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
    .search-input {
      flex: 1;
      background: #f5f5f5;
      border-radius: 8px;
      padding: 0 12px;
    }
  }
  .filter-row {
    display: flex;
    align-items: center;
    gap: 10px;
    .filter-item {
      display: flex;
      align-items: center;
      gap: 4px;
      background: #f5f5f5;
      border-radius: 8px;
      padding: 0 12px;
      height: 34px;
      font-size: 13px;
      color: #333;
      &.date-item {
        flex: 1;
        overflow: hidden;
      }
      .filter-text {
        font-size: 13px;
        color: #333;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        &.placeholder {
          color: #999;
        }
      }
    }
    .search-btn {
      width: 70px;
    }
  }
  .list-container {
    flex: 1;
    height: 0;
    padding: 20rpx 20rpx 140rpx;
    box-sizing: border-box;
  }
  .detail-value.danger {
    color: #ee0a24;
    font-weight: 600;
  }
  .action-buttons {
    display: flex;
    gap: 12px;
    padding: 16rpx 0 0;
    justify-content: flex-end;
  }
  .no-data {
    padding-top: 150rpx;
  }
  .form-popup {
    height: 70vh;
    display: flex;
    flex-direction: column;
    background: #f5f5f5;
  }
  .popup-content {
    flex: 1;
    height: 0;
    padding: 12px;
  }
  .form-field {
    background: #fff;
    border-radius: 10px;
    padding: 12px;
    margin-bottom: 12px;
  }
  // readonly çš„ up-input ä¼šæŠŠåŽŸç”Ÿ input ç½®ä¸º disabled,disabled å…ƒç´ ä¼šåžæŽ‰ç‚¹å‡»äº‹ä»¶ï¼Œ
  // æ•…让 input ä¸å‚与指针事件,由外层 .pick-field æŽ¥ç®¡ç‚¹å‡»
  .pick-field {
    display: flex;
    align-items: center;
    .pick-input {
      flex: 1;
    }
    :deep(input) {
      pointer-events: none;
    }
  }
  .field-label {
    display: block;
    font-size: 14px;
    color: #666;
    margin-bottom: 8px;
    &.required::before {
      content: "*";
      color: #ff0000;
      margin-right: 4px;
    }
  }
  .spec-box {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .spec-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .spec-tag {
    display: flex;
    align-items: center;
    gap: 6px;
    background: #ecf5ff;
    color: #2979ff;
    border-radius: 6px;
    padding: 4px 10px;
    .spec-tag-text {
      font-size: 12px;
    }
  }
  .spec-empty {
    font-size: 12px;
    color: #999;
  }
  .spec-category {
    background: #fff;
    padding: 8px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .category-scroll {
    white-space: nowrap;
  }
  .category-list {
    display: inline-flex;
    gap: 10px;
    padding: 0 12px;
  }
  .category-tag {
    padding: 6px 14px;
    border-radius: 20px;
    background: #f5f5f5;
    color: #666;
    font-size: 13px;
    white-space: nowrap;
    &.active {
      background: #2979ff;
      color: #fff;
    }
  }
  .model-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: #fff;
    border-radius: 10px;
    padding: 14px 12px;
    margin-bottom: 8px;
    .model-info {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .model-name {
      font-size: 14px;
      color: #333;
    }
    .model-unit {
      font-size: 12px;
      color: #999;
    }
  }
  .spec-empty-box {
    padding-top: 60px;
  }
  .popup-footer {
    display: flex;
    gap: 12px;
    padding: 12px 16px;
    background: #fff;
    border-top: 1px solid #f0f0f0;
  }
  .btn-cancel,
  .btn-confirm {
    flex: 1;
  }
</style>
src/pages/inventoryManagement/stockManagement/index.vue
@@ -2,39 +2,141 @@
  <view class="app-container">
    <PageHeader title="库存管理"
                @back="goBack" />
    <view v-if="loading"
          class="loading-state">
      <up-loading-icon text="加载中..."></up-loading-icon>
    <!-- åº“存预警开关 -->
    <view class="warn-bar">
      <view class="warn-left">
        <text class="warn-label">只看预警</text>
        <up-switch v-model="warnOnly"
                   size="small"
                   activeColor="#f56c6c"
                   @change="handleWarnToggle"></up-switch>
      </view>
    </view>
    <!-- é¢„警列表 -->
    <template v-if="warnOnly">
      <view class="warn-search">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="warnSearch.productName"
                    placeholder="请输入产品名称"
                    clearable
                    @confirm="handleWarnQuery" />
        </view>
        <view class="filter-button"
              @click="handleWarnQuery">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
      <scroll-view scroll-y
                   class="warn-list"
                   v-if="warnList.length > 0"
                   @scrolltolower="loadWarnMore">
        <view v-for="item in warnList"
              :key="item.id"
              class="ledger-item warn-item"
              :class="{ 'is-warn': isWarnRow(item) }">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon warn-icon">
                <up-icon name="warning"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.productName || '-' }}</text>
            </view>
            <up-tag text="预警"
                    type="error"
                    size="mini"
                    v-if="isWarnRow(item)" />
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <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.unit || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">数量</text>
              <text class="detail-value">{{ formatQty(item.qualitity) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">锁定数量</text>
              <text class="detail-value">{{ formatQty(item.lockedQuantity) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">可用数量</text>
              <text class="detail-value"
                    :class="{ 'warn-value': isWarnRow(item) }">{{ formatQty(item.un_locked_quantity ?? item.unLockedQuantity) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">预警值</text>
              <text class="detail-value">{{ formatQty(item.warnNum) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">批号</text>
              <text class="detail-value">{{ item.batchNo || '-' }}</text>
            </view>
          </view>
        </view>
        <up-loadmore :status="warnStatus" />
      </scroll-view>
      <view v-else-if="!warnLoading"
            class="no-data">
        <up-empty mode="data"
                  text="暂无预警库存"></up-empty>
      </view>
    </template>
    <!-- åº“存列表 -->
    <template v-else>
      <up-tabs :list="tabs"
               @click="handleTabClick"
               :current="activeTab" />
      <swiper class="swiper-box"
              :current="activeTab"
              @change="handleSwiperChange">
        <swiper-item class="swiper-item"
                     v-for="tab in products"
                     :key="tab.id">
          <record :product-id="tab.id"
                  v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
        </swiper-item>
      </swiper>
      <view v-if="loading"
            class="loading-state">
        <up-loading-icon text="加载中..."></up-loading-icon>
      </view>
      <template v-else>
        <up-tabs :list="tabs"
                 @click="handleTabClick"
                 :current="activeTab" />
        <swiper class="swiper-box"
                :current="activeTab"
                @change="handleSwiperChange">
          <swiper-item class="swiper-item"
                       v-for="tab in products"
                       :key="tab.id">
            <record :product-id="tab.id"
                    v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
          </swiper-item>
        </swiper>
      </template>
    </template>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { ref, reactive, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import Record from "./Record.vue";
  import { productTreeList } from "@/api/basicData/product.js";
  import { getStockWarnPage } from "@/api/inventoryManagement/stockInventory.js";
  const activeTab = ref(0);
  const tabs = ref([]);
  const products = ref([]);
  const loading = ref(false);
  const initializedTabs = ref([]);
  // é¢„è­¦
  const warnOnly = ref(false);
  const warnLoading = ref(false);
  const warnStatus = ref("loadmore");
  const warnList = ref([]);
  const warnPage = reactive({ current: 1, size: 10, total: 0 });
  const warnSearch = reactive({ productName: "" });
  const handleTabClick = item => {
    activeTab.value = item.index;
@@ -74,12 +176,74 @@
    uni.navigateBack();
  };
  // é¢„警相关
  const formatQty = val => {
    if (val === null || val === undefined || val === "") return "-";
    return Number(val) || 0;
  };
  const getAvailable = item => {
    const val = item.un_locked_quantity ?? item.unLockedQuantity;
    return Number(val) || 0;
  };
  // å¯ç”¨æ•°é‡ â‰¤ é¢„警值 ä¸” é¢„警值>0
  const isWarnRow = item => {
    const warnNum = Number(item.warnNum) || 0;
    if (warnNum <= 0) return false;
    return getAvailable(item) <= warnNum;
  };
  const getWarnList = () => {
    warnLoading.value = true;
    warnStatus.value = "loading";
    getStockWarnPage({
      current: warnPage.current,
      size: warnPage.size,
      productName: warnSearch.productName,
    })
      .then(res => {
        const records = res?.data?.records || [];
        warnList.value =
          warnPage.current === 1 ? records : [...warnList.value, ...records];
        warnPage.total = res?.data?.total || 0;
        warnStatus.value =
          warnList.value.length >= warnPage.total ? "nomore" : "loadmore";
      })
      .catch(() => {
        warnStatus.value = "loadmore";
      })
      .finally(() => {
        warnLoading.value = false;
      });
  };
  const handleWarnQuery = () => {
    warnPage.current = 1;
    warnList.value = [];
    getWarnList();
  };
  const loadWarnMore = () => {
    if (warnStatus.value === "nomore" || warnLoading.value) return;
    warnPage.current++;
    getWarnList();
  };
  const handleWarnToggle = () => {
    if (warnOnly.value && !warnList.value.length) {
      handleWarnQuery();
    }
  };
  onMounted(() => {
    fetchProducts();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .app-container {
    display: flex;
    flex-direction: column;
@@ -101,4 +265,70 @@
  :deep(.up-tabs) {
    background-color: #fff;
  }
  .warn-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: #fff;
    padding: 16rpx 30rpx;
    .warn-left {
      display: flex;
      align-items: center;
      gap: 12rpx;
    }
    .warn-label {
      font-size: 28rpx;
      color: #333;
    }
  }
  .warn-search {
    display: flex;
    align-items: center;
    gap: 12rpx;
    background: #fff;
    padding: 16rpx 30rpx;
    .search-input {
      flex: 1;
      background: #f5f5f5;
      border-radius: 40rpx;
      padding: 0 24rpx;
      height: 72rpx;
      display: flex;
      align-items: center;
    }
  }
  .warn-list {
    flex: 1;
    height: 0;
    padding: 20rpx;
    box-sizing: border-box;
  }
  .warn-item {
    padding: 24rpx;
    &.is-warn {
      border: 2rpx solid #f56c6c;
    }
    .warn-icon {
      background: linear-gradient(135deg, #f56c6c, #c0392b);
    }
    .warn-value {
      color: #f56c6c;
      font-weight: 600;
    }
  }
  .no-data {
    flex: 1;
    padding-top: 150rpx;
  }
</style>
src/pages/procurementManagement/procurementDemand/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,315 @@
<template>
  <view class="demand-page">
    <PageHeader title="采购需求"
                @back="goBack" />
    <!-- ç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="queryParams.productName"
                    placeholder="请输入产品名称"
                    clearable
                    @change="handleSearch" />
        </view>
      </view>
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="queryParams.productModel"
                    placeholder="请输入规格型号"
                    clearable
                    @change="handleSearch" />
        </view>
        <view class="filter-button"
              @click="showStatusSheet = true">
          <text class="filter-text">{{ statusLabel }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#999999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view v-if="list.length > 0"
          class="ledger-list">
      <view v-for="item in list"
            :key="item.id"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="shopping-cart"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.sourceBomNo || "无来源BOM" }}</text>
          </view>
          <up-tag :text="statusText(item.status)"
                  :type="statusType(item.status)"
                  size="mini" />
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ item.productName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.productModel || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ item.unit || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">缺口需求量</text>
            <text class="detail-value danger">{{ num(item.demandQuantity) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">库存储备快照</text>
            <text class="detail-value">{{ num(item.stockQuantity) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">来源BOM编号</text>
            <text class="detail-value">{{ item.sourceBomNo || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">创建时间</text>
            <text class="detail-value">{{ item.createTime || "-" }}</text>
          </view>
        </view>
        <view class="action-buttons">
          <up-button v-if="isPending(item)"
                     class="action-btn"
                     size="small"
                     type="primary"
                     @click="handleChangeStatus(item, 1)">标记已转采购</up-button>
          <up-button v-if="isPending(item)"
                     class="action-btn"
                     size="small"
                     type="warning"
                     @click="handleChangeStatus(item, 2)">取消</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="error"
                     plain
                     :disabled="!isPending(item)"
                     @click="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <up-loadmore :status="pageStatus" />
    </view>
    <view v-else
          class="no-data">
      <up-empty text="暂无采购需求"
                mode="data"></up-empty>
    </view>
    <!-- çŠ¶æ€ç­›é€‰ -->
    <up-action-sheet :show="showStatusSheet"
                     :actions="statusOptions"
                     title="选择状态"
                     @select="handleStatusSelect"
                     @close="showStatusSheet = false" />
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import { listPage, changeStatus, del } from "@/api/procurementManagement/procurementDemand";
  import PageHeader from "@/components/PageHeader.vue";
  const STATUS_MAP = {
    0: { text: "待处理", type: "warning" },
    1: { text: "已转采购", type: "success" },
    2: { text: "已取消", type: "info" },
  };
  const queryParams = reactive({
    productName: "",
    productModel: "",
    status: "",
  });
  const list = ref([]);
  const pageStatus = ref("loadmore");
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  const showStatusSheet = ref(false);
  const statusOptions = [
    { name: "全部", value: "" },
    { name: "待处理", value: 0 },
    { name: "已转采购", value: 1 },
    { name: "已取消", value: 2 },
  ];
  const statusLabel = ref("全部状态");
  // decimal å¯èƒ½æ˜¯å­—符串,统一转数字兜底
  const num = val => Number(val) || 0;
  const statusText = status => (STATUS_MAP[Number(status)] || STATUS_MAP[0]).text;
  const statusType = status => (STATUS_MAP[Number(status)] || STATUS_MAP[0]).type;
  // ä»…待处理可操作(删除也由后端兜底校验)
  const isPending = item => Number(item.status) === 0;
  const goBack = () => {
    uni.navigateBack();
  };
  const handleSearch = () => {
    page.current = 1;
    pageStatus.value = "loadmore";
    list.value = [];
    getList();
  };
  const getList = () => {
    if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
    pageStatus.value = "loading";
    listPage({
      current: page.current,
      size: page.size,
      productName: queryParams.productName,
      productModel: queryParams.productModel,
      status: queryParams.status,
    })
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        const total = res?.data?.total || res?.total || 0;
        if (page.current === 1) {
          list.value = records;
        } else {
          list.value = [...list.value, ...records];
        }
        page.total = total;
        if (list.value.length >= total) {
          pageStatus.value = "nomore";
        } else {
          pageStatus.value = "loadmore";
          page.current++;
        }
      })
      .catch(() => {
        // é”™è¯¯æç¤ºç”± request ç»Ÿä¸€å¤„理
        pageStatus.value = "loadmore";
      });
  };
  const handleStatusSelect = action => {
    queryParams.status = action.value;
    statusLabel.value = action.value === "" ? "全部状态" : action.name;
    showStatusSheet.value = false;
    handleSearch();
  };
  const handleChangeStatus = (item, status) => {
    const actionText = status === 1 ? "标记为已转采购" : "取消";
    uni.showModal({
      title: "提示",
      content: `确定将该采购需求${actionText}吗?`,
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        changeStatus({ ids: item.id, status })
          .then(() => {
            uni.hideLoading();
            uni.showToast({ title: "操作成功", icon: "none" });
            handleSearch();
          })
          .catch(() => {
            uni.hideLoading();
          });
      },
    });
  };
  const handleDelete = item => {
    uni.showModal({
      title: "删除确认",
      content: "确定删除该采购需求吗?",
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        del([item.id])
          .then(() => {
            uni.hideLoading();
            uni.showToast({ title: "删除成功", icon: "none" });
            handleSearch();
          })
          .catch(() => {
            // éžå¾…处理状态后端会返回提示,此处不再重复弹窗
            uni.hideLoading();
          });
      },
    });
  };
  onReachBottom(() => {
    getList();
  });
  onShow(() => {
    handleSearch();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .demand-page {
    min-height: 100vh;
    background: #f8f9fa;
  }
  .search-bar + .search-bar {
    margin-top: 16rpx;
  }
  // å…¬å…±æ ·å¼é‡Œ .filter-button æ˜¯ 40px è§æ–¹çš„图标按钮,这里要放状态文案,需按内容撑开
  .filter-button {
    width: auto;
    min-width: 40px;
    padding: 0 12px;
    flex-shrink: 0;
    background: #f5f5f5;
  }
  .filter-text {
    font-size: 24rpx;
    color: #333;
    margin-right: 4rpx;
    white-space: nowrap;
  }
  .no-data {
    padding-top: 100rpx;
    text-align: center;
    color: #999;
    font-size: 28rpx;
  }
  .action-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 15rpx;
    padding: 0 30rpx 30rpx;
    flex-wrap: wrap;
  }
  .action-btn {
    width: calc(33.33% - 10rpx);
    margin: 0 !important;
    margin-bottom: 15rpx !important;
  }
</style>
src/pages/procurementManagement/procurementLedger/detail.vue
@@ -204,6 +204,14 @@
                        :disabled="!canEditProducts"
                        @click="openSpecificationPicker(idx)" />
              <template #right>
                <view v-if="canEditProducts"
                      class="scan-entry"
                      @click.stop="scanBarcode(idx)">
                  <up-icon name="scan"
                           size="18"
                           color="#3c9cff"></up-icon>
                  <text class="scan-entry-text">扫码</text>
                </view>
                <up-icon name="arrow-right"
                         v-if="canEditProducts"
                         @click="openSpecificationPicker(idx)"></up-icon>
@@ -338,7 +346,11 @@
<script setup>
  import { onMounted, ref, computed } from "vue";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import {
    modelList,
    productTreeList,
    modelByBarcode,
  } from "@/api/basicData/product.js";
  import useUserStore from "@/store/modules/user";
  import { calculateTaxExclusiveTotalPrice } from "@/utils/summarizeTable";
  import { formatDateToYMD } from "@/utils/ruoyi";
@@ -447,6 +459,7 @@
      name: model.text,
      value: model.value,
      unit: model.unit,
      subname: model.barcode ? `条码:${model.barcode}` : "",
    }));
  });
@@ -670,14 +683,48 @@
    showCategoryPicker.value = false;
  };
  // èŽ·å–è§„æ ¼åž‹å·
  // èŽ·å–è§„æ ¼åž‹å·ï¼ˆè¿”å›ž Promise,便于扫码后顺序回填)
  const getModels = value => {
    modelList({ id: value }).then(res => {
      modelOptions.value = res.map(user => ({
    return modelList({ id: value }).then(res => {
      const list = res?.data || res || [];
      modelOptions.value = list.map(user => ({
        text: user.model,
        value: user.id,
        unit: user.unit,
        barcode: user.barcode,
      }));
    });
  };
  // æ‰«ç æŒ‰æ¡ç ç²¾ç¡®å›žå¡«äº§å“å¤§ç±»+规格型号
  const scanBarcode = idx => {
    if (!canEditProducts.value) return;
    uni.scanCode({
      scanType: ["barCode", "qrCode"],
      success: async res => {
        const barcode = (res.result || "").trim();
        if (!barcode) return;
        uni.showLoading({ title: "匹配中...", mask: true });
        try {
          const resp = await modelByBarcode({ barcode });
          const info = resp?.data || resp;
          if (info && info.productId) {
            currentProductIndex.value = idx;
            // å…ˆåŠ è½½è¯¥äº§å“ä¸‹çš„è§„æ ¼åˆ—è¡¨ï¼Œä¿è¯ä¸Žæ‰‹åŠ¨é€‰æ‹©æ—¶çš„ UI çŠ¶æ€ä¸€è‡´
            await getModels(info.productId);
            const row = productData.value[idx];
            row.productCategory = info.productName || row.productCategory;
            row.specificationModel = info.model || "";
            row.productModelId = info.id || "";
            row.unit = info.unit || "";
          }
          uni.hideLoading();
          uni.showToast({ title: "已匹配规格", icon: "success" });
        } catch (e) {
          // æœªåŒ¹é…ç­‰é”™è¯¯å·²æœ‰å…¨å±€ toast,这里只恢复 loading
          uni.hideLoading();
        }
      },
    });
  };
@@ -1119,6 +1166,19 @@
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  // è§„格行扫码入口
  .scan-entry {
    display: flex;
    align-items: center;
    margin-right: 8px;
    .scan-entry-text {
      font-size: 13px;
      color: #3c9cff;
      margin-left: 2px;
    }
  }
  .approval-process {
    background: #fff;
    margin: 16px;
src/pages/productionDesign/bom/index.vue
@@ -33,9 +33,14 @@
            </view>
            <text class="item-id">{{ item.bomNo || "-" }}</text>
          </view>
          <up-tag :text="'V' + (item.version || '1.0')"
                  type="primary"
                  size="mini" />
          <view class="item-right">
            <up-tag :text="'V' + (item.version || '1.0')"
                    type="primary"
                    size="mini" />
            <up-tag :text="isEnabled(item) ? '启用' : '停用'"
                    :type="isEnabled(item) ? 'success' : 'info'"
                    size="mini" />
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
@@ -57,6 +62,15 @@
                     size="small"
                     type="primary"
                     @click="goStructure(item)">查看详情</up-button>
          <up-button class="action-btn"
                     size="small"
                     :type="isEnabled(item) ? 'warning' : 'success'"
                     @click="toggleStatus(item)">{{ isEnabled(item) ? "停用" : "启用" }}</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     plain
                     @click="openKitCheck(item)">齐套分析</up-button>
        </view>
      </view>
      <up-loadmore :status="pageStatus" />
@@ -66,13 +80,92 @@
      <up-empty text="暂无BOM数据"
                mode="list"></up-empty>
    </view>
    <!-- é½å¥—分析弹窗 -->
    <up-popup :show="showKitPopup"
              mode="bottom"
              round="10"
              @close="showKitPopup = false">
      <view class="kit-popup">
        <view class="popup-header">
          <text class="popup-title">齐套分析 - {{ currentBom.bomNo || "" }}</text>
        </view>
        <view class="kit-form">
          <text class="kit-form-label">需求数量</text>
          <view class="kit-form-input">
            <up-number-box v-model="demandQty"
                           :min="0"
                           :step="1"></up-number-box>
          </view>
          <up-button type="primary"
                     size="small"
                     :loading="kitLoading"
                     text="分析"
                     @click="handleKitCheck"></up-button>
        </view>
        <scroll-view scroll-y
                     class="kit-result">
          <view v-if="kitResult.length > 0">
            <view v-for="(row, index) in kitResult"
                  :key="index"
                  class="kit-item">
              <view class="kit-item-header">
                <text class="kit-name">{{ row.productName || "-" }}</text>
                <text class="kit-model">{{ row.model || "-" }}</text>
                <up-tag v-if="num(row.shortageQty) > 0"
                        text="缺口"
                        type="error"
                        size="mini" />
              </view>
              <view class="kit-grid">
                <view class="kit-cell">
                  <text class="kit-label">单位</text>
                  <text class="kit-value">{{ row.unit || "-" }}</text>
                </view>
                <view class="kit-cell">
                  <text class="kit-label">需求数量</text>
                  <text class="kit-value">{{ num(row.requiredQty) }}</text>
                </view>
                <view class="kit-cell">
                  <text class="kit-label">可用库存</text>
                  <text class="kit-value">{{ num(row.availableQty) }}</text>
                </view>
                <view class="kit-cell">
                  <text class="kit-label">缺口数量</text>
                  <text class="kit-value"
                        :class="{ danger: num(row.shortageQty) > 0 }">{{ num(row.shortageQty) }}</text>
                </view>
              </view>
            </view>
          </view>
          <view v-else
                class="kit-empty">
            <up-empty mode="data"
                      :text="kitDone ? '库存齐套,无缺口' : '请输入需求数量后点击分析'"></up-empty>
          </view>
        </scroll-view>
        <view class="filter-actions">
          <up-button type="primary"
                     size="default"
                     :disabled="!kitDone"
                     text="生成采购需求"
                     @click="handleGenerateDemand"></up-button>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { reactive, ref } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import { listPage } from "@/api/productionManagement/bom";
  import {
    listPage,
    changeStatus,
    kitCheck,
    generateDemand,
  } from "@/api/productionManagement/bom";
  import PageHeader from "@/components/PageHeader.vue";
  const queryParams = reactive({
    productName: "",
@@ -85,6 +178,15 @@
    size: 3,
    total: 0,
  });
  // åŽç«¯ä¸è¿”回 status æ—¶æŒ‰å¯ç”¨å±•示(兼容老数据)
  const isEnabled = item =>
    item.status === undefined ||
    item.status === null ||
    Number(item.status) === 1;
  // decimal å¯èƒ½æ˜¯å­—符串,统一转数字兜底
  const num = val => Number(val) || 0;
  const goBack = () => {
    uni.navigateBack();
@@ -125,7 +227,7 @@
        }
      })
      .catch(() => {
        uni.showToast({ title: "查询失败", icon: "error" });
        // é”™è¯¯æç¤ºç”± request ç»Ÿä¸€å¤„理
        pageStatus.value = "loadmore";
      });
  };
@@ -142,6 +244,87 @@
        item.remark || ""
      )}&version=${encodeURIComponent(item.version || 1)}`,
    });
  };
  // å¯ç”¨/停用,status=1 æ—¶åŽç«¯è‡ªåŠ¨åœç”¨åŒè§„æ ¼å…¶å®ƒç‰ˆæœ¬
  const toggleStatus = item => {
    const enabled = isEnabled(item);
    const nextStatus = enabled ? 0 : 1;
    const actionText = enabled ? "停用" : "启用";
    uni.showModal({
      title: "提示",
      content: `确定${actionText}该BOM版本吗?`,
      success: res => {
        if (!res.confirm) return;
        uni.showLoading({ title: "处理中...", mask: true });
        changeStatus({ id: item.id, status: nextStatus })
          .then(() => {
            uni.hideLoading();
            uni.showToast({ title: `${actionText}成功`, icon: "none" });
            handleSearch();
          })
          .catch(() => {
            uni.hideLoading();
          });
      },
    });
  };
  // --- é½å¥—分析 ---
  const showKitPopup = ref(false);
  const kitLoading = ref(false);
  const kitDone = ref(false);
  const currentBom = ref({});
  const demandQty = ref(1);
  const kitResult = ref([]);
  const openKitCheck = item => {
    currentBom.value = item;
    demandQty.value = 1;
    kitResult.value = [];
    kitDone.value = false;
    showKitPopup.value = true;
  };
  const handleKitCheck = () => {
    const qty = Number(demandQty.value);
    if (!Number.isFinite(qty) || qty <= 0) {
      uni.showToast({ title: "需求数量必须大于0", icon: "none" });
      return;
    }
    kitLoading.value = true;
    kitCheck({ bomId: currentBom.value.id, demandQty: qty })
      .then(res => {
        kitResult.value = res?.data || [];
        kitDone.value = true;
      })
      .catch(() => {
        // é”™è¯¯æç¤ºç”± request ç»Ÿä¸€å¤„理
        kitResult.value = [];
        kitDone.value = false;
      })
      .finally(() => {
        kitLoading.value = false;
      });
  };
  const handleGenerateDemand = () => {
    uni.showLoading({ title: "处理中...", mask: true });
    generateDemand({
      bomId: currentBom.value.id,
      demandQty: Number(demandQty.value),
    })
      .then(res => {
        uni.hideLoading();
        uni.showModal({
          title: "提示",
          content: String(res?.data ?? "采购需求生成完成"),
          showCancel: false,
        });
      })
      .catch(() => {
        uni.hideLoading();
      });
  };
  onReachBottom(() => {
@@ -172,8 +355,92 @@
  }
  .action-btn {
    width: calc(50% - 15rpx);
    width: calc(33.33% - 10rpx);
    margin: 0 !important;
    margin-bottom: 15rpx !important;
  }
  // --- é½å¥—分析弹窗 ---
  .kit-popup {
    padding-bottom: 20rpx;
  }
  .kit-form {
    display: flex;
    align-items: center;
    gap: 16rpx;
    padding: 20rpx 30rpx;
    .kit-form-label {
      font-size: 28rpx;
      color: #333;
    }
    .kit-form-input {
      flex: 1;
    }
  }
  .kit-result {
    height: 600rpx;
    padding: 0 30rpx;
  }
  .kit-item {
    border: 1rpx solid #f0f0f0;
    border-radius: 12rpx;
    padding: 16rpx;
    margin-bottom: 16rpx;
  }
  .kit-item-header {
    display: flex;
    align-items: center;
    gap: 12rpx;
    margin-bottom: 12rpx;
    .kit-name {
      font-size: 28rpx;
      color: #333;
      font-weight: 500;
    }
    .kit-model {
      font-size: 24rpx;
      color: #999;
      flex: 1;
    }
  }
  .kit-grid {
    display: flex;
    flex-wrap: wrap;
    .kit-cell {
      width: 50%;
      display: flex;
      align-items: center;
      margin-bottom: 8rpx;
      .kit-label {
        font-size: 24rpx;
        color: #777;
        min-width: 110rpx;
      }
      .kit-value {
        font-size: 24rpx;
        color: #333;
        &.danger {
          color: #ee0a24;
          font-weight: 500;
        }
      }
    }
  }
  .kit-empty {
    padding: 40rpx 0;
  }
</style>
src/pages/productionManagement/costAccounting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,662 @@
<template>
  <view class="cost-accounting">
    <PageHeader title="成本核算分析"
                @back="goBack" />
    <!-- é¡¶éƒ¨é¡µç­¾ -->
    <view class="main-tabs">
      <up-tabs :list="mainTabs"
               :current="mainTabIndex"
               @change="handleMainTabChange"
               :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
               lineWidth="40"
               lineHeight="3" />
    </view>
    <!-- å…¬å…±ç­›é€‰ -->
    <view class="search-section">
      <view class="filter-bar">
        <view class="date-input"
              @click="showCalendar = true">
          <up-icon name="calendar"
                   size="18"
                   color="#999"></up-icon>
          <text class="date-text"
                :class="{ placeholder: !query.startDate }">{{ dateRangeText }}</text>
          <up-icon v-if="query.startDate"
                   name="close-circle-fill"
                   size="16"
                   color="#c0c4cc"
                   @click.stop="handleClearDate"></up-icon>
        </view>
      </view>
      <view class="filter-bar">
        <view class="search-input">
          <up-input class="search-text"
                    v-model="query.npsNo"
                    placeholder="生产订单号"
                    clearable
                    @change="handleQuery" />
        </view>
        <view class="search-input">
          <up-input class="search-text"
                    v-model="query.productName"
                    placeholder="成品名称/规格"
                    clearable
                    @change="handleQuery" />
        </view>
      </view>
      <view class="filter-actions-top">
        <up-button type="primary"
                   size="small"
                   text="搜索"
                   @click="handleQuery"></up-button>
      </view>
    </view>
    <view v-if="loading"
          class="loading-box">
      <up-loading-icon text="加载中..."></up-loading-icon>
    </view>
    <template v-else>
      <!-- ============ 1. æ ¸ç®—明细 ============ -->
      <template v-if="mainTabIndex === 0">
        <view v-if="visibleStatRows.length > 0"
              class="ledger-list">
          <view v-for="row in visibleStatRows"
                :key="row.productionOrderId ?? row.npsNo"
                class="ledger-item no-click">
            <view class="item-header">
              <view class="item-left">
                <view class="document-icon">
                  <up-icon name="rmb-circle"
                           size="16"
                           color="#ffffff"></up-icon>
                </view>
                <text class="item-id">{{ row.npsNo || "-" }}</text>
              </view>
              <text class="item-index">{{ row.orderDate || "无日期" }}</text>
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">成品</text>
                <text class="detail-value">{{ row.finishedProduct || "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">产量</text>
                <text class="detail-value">{{ fmtNum(row.outputQty) }}{{ row.unit ? ` ${row.unit}` : "" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">材料成本</text>
                <text class="detail-value">{{ fmtMoney(row.materialCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">人工成本</text>
                <text class="detail-value"
                      :class="{ warn: isPositive(row.noRateRows) }">{{ fmtMoney(row.laborCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">能耗成本</text>
                <text class="detail-value">{{ fmtMoney(row.energyCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">总成本</text>
                <text class="detail-value highlight">{{ fmtMoney(row.totalCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">单位成本</text>
                <text class="detail-value">{{ fmtUnitCost(row.unitCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">销售单价</text>
                <text class="detail-value">{{ fmtMoney(row.salePrice) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">毛利</text>
                <text class="detail-value"
                      :class="{ danger: isNegative(row.grossProfit) }">{{ fmtMoney(row.grossProfit) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">毛利率</text>
                <text class="detail-value"
                      :class="{ danger: isNegative(row.grossProfitRate) }">{{ fmtPercent(row.grossProfitRate) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">价格覆盖率</text>
                <text class="detail-value"
                      :class="{ warn: isLowCoverage(row.priceCoverageRate) }">{{ fmtPercent(row.priceCoverageRate) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">缺价物料数</text>
                <text class="detail-value"
                      :class="{ warn: isPositive(row.missingPriceKinds) }">{{ fmtCount(row.missingPriceKinds) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">无工资标准记录</text>
                <text class="detail-value"
                      :class="{ warn: isPositive(row.noRateRows) }">{{ fmtCount(row.noRateRows) }}</text>
              </view>
              <view v-if="isPositive(row.missingPriceKinds)"
                    class="warn-tip">存在缺价物料,材料成本被低估,可在「缺价物料」页查清单后补录采购单价</view>
              <view v-if="isPositive(row.noRateRows)"
                    class="warn-tip">存在无工资标准的报工记录,人工成本不可信,需补录工时或维护计件单价</view>
            </view>
          </view>
          <up-loadmore :status="statStatus" />
        </view>
        <view v-else
              class="no-data">
          <up-empty mode="data"
                    text="暂无成本核算数据"></up-empty>
        </view>
      </template>
      <!-- ============ 2. æž„成汇总 ============ -->
      <template v-else-if="mainTabIndex === 1">
        <view class="sub-tabs">
          <up-tabs :list="groupTabs"
                   :current="groupIndex"
                   @change="handleGroupChange"
                   :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
                   lineWidth="30"
                   lineHeight="3" />
        </view>
        <view v-if="summaryList.length > 0"
              class="ledger-list">
          <view v-for="(row, index) in summaryList"
                :key="index"
                class="ledger-item no-click">
            <view class="item-header">
              <view class="item-left">
                <view class="document-icon">
                  <up-icon name="grid"
                           size="16"
                           color="#ffffff"></up-icon>
                </view>
                <text class="item-id">{{ row.groupKey || "未填日期" }}</text>
              </view>
              <text class="item-index">{{ fmtCount(row.orderCount) }} å•</text>
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <!-- æˆæœ¬æž„成占比 -->
              <view class="rate-bar">
                <view class="rate-seg seg-material"
                      :style="{ width: segWidth(row.materialRate) }"></view>
                <view class="rate-seg seg-labor"
                      :style="{ width: segWidth(row.laborRate) }"></view>
                <view class="rate-seg seg-energy"
                      :style="{ width: segWidth(row.energyRate) }"></view>
              </view>
              <view class="rate-legend">
                <view class="legend-item">
                  <text class="legend-dot seg-material"></text>
                  <text class="legend-text">材料 {{ fmtPercent(row.materialRate) }}</text>
                </view>
                <view class="legend-item">
                  <text class="legend-dot seg-labor"></text>
                  <text class="legend-text">人工 {{ fmtPercent(row.laborRate) }}</text>
                </view>
                <view class="legend-item">
                  <text class="legend-dot seg-energy"></text>
                  <text class="legend-text">能耗 {{ fmtPercent(row.energyRate) }}</text>
                </view>
              </view>
              <view class="detail-row">
                <text class="detail-label">产量</text>
                <text class="detail-value">{{ fmtNum(row.outputQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">材料成本</text>
                <text class="detail-value">{{ fmtMoney(row.materialCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">人工成本</text>
                <text class="detail-value">{{ fmtMoney(row.laborCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">能耗成本</text>
                <text class="detail-value">{{ fmtMoney(row.energyCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">总成本</text>
                <text class="detail-value highlight">{{ fmtMoney(row.totalCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">单位成本</text>
                <text class="detail-value">{{ fmtUnitCost(row.unitCost) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">毛利</text>
                <text class="detail-value"
                      :class="{ danger: isNegative(row.grossProfit) }">{{ fmtMoney(row.grossProfit) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">毛利率</text>
                <text class="detail-value"
                      :class="{ danger: isNegative(row.grossProfitRate) }">{{ fmtPercent(row.grossProfitRate) }}</text>
              </view>
            </view>
          </view>
        </view>
        <view v-else
              class="no-data">
          <up-empty mode="data"
                    text="暂无汇总数据"></up-empty>
        </view>
      </template>
      <!-- ============ 3. ç¼ºä»·ç‰©æ–™ ============ -->
      <template v-else>
        <view v-if="missingList.length > 0"
              class="ledger-list">
          <view v-for="(row, index) in missingList"
                :key="row.productModelId ?? index"
                class="ledger-item no-click">
            <view class="item-header">
              <view class="item-left">
                <view class="document-icon warn-icon">
                  <up-icon name="warning"
                           size="16"
                           color="#ffffff"></up-icon>
                </view>
                <text class="item-id">{{ row.materialName || "-" }}</text>
              </view>
              <text class="item-index">{{ fmtCount(row.orderCount) }} å•</text>
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">规格型号</text>
                <text class="detail-value">{{ row.materialModel || "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">单位</text>
                <text class="detail-value">{{ row.unit || "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">消耗量</text>
                <text class="detail-value"
                      :class="{ warn: true }">{{ fmtNum(row.inputQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">最后投入时间</text>
                <text class="detail-value">{{ row.lastInputTime || "-" }}</text>
              </view>
            </view>
          </view>
        </view>
        <view v-else
              class="no-data">
          <up-empty mode="data"
                    text="暂无缺价物料"></up-empty>
        </view>
      </template>
    </template>
    <!-- æ—¥æœŸåŒºé—´é€‰æ‹© -->
    <up-calendar :show="showCalendar"
                 mode="range"
                 :maxDate="maxDate"
                 :minDate="minDate"
                 :monthNum="monthNum"
                 :defaultDate="defaultDateRange"
                 @confirm="onDateConfirm"
                 @close="showCalendar = false"></up-calendar>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  import { getWeekRange } from "@/utils/common";
  import {
    costStat,
    costSummary,
    costMissingPrice,
  } from "@/api/productionManagement/costAccounting.js";
  const mainTabs = [{ name: "核算明细" }, { name: "构成汇总" }, { name: "缺价物料" }];
  const mainTabIndex = ref(0);
  // æ—¥æœŸåŒºé—´ï¼šé»˜è®¤æœ¬å‘¨ï¼ˆå‘¨ä¸€ ~ ä»Šå¤©ï¼‰
  const maxDate = dayjs().format("YYYY-MM-DD");
  const minDate = "2022-01-01";
  // æœˆä»½æ•°éœ€ä¸Ž minDate ä¸¥æ ¼ä¸€è‡´ï¼Œå°‘一个月会把当前月挤出列表导致日历停在起始月
  const monthNum = computed(() => {
    const max = dayjs(maxDate);
    const min = dayjs(minDate);
    return (max.year() - min.year()) * 12 + (max.month() - min.month()) + 1;
  });
  const showCalendar = ref(false);
  const weekRange = getWeekRange();
  const query = reactive({
    startDate: weekRange.startDate,
    endDate: weekRange.endDate,
    npsNo: "",
    productName: "",
  });
  const dateRangeText = computed(() =>
    query.startDate && query.endDate
      ? `${query.startDate} è‡³ ${query.endDate}`
      : "请选择日期区间"
  );
  // æ‰“开日历时回显当前区间
  const defaultDateRange = computed(() =>
    query.startDate && query.endDate
      ? [query.startDate, query.endDate]
      : []
  );
  const loading = ref(false);
  // æ˜Žç»†ï¼ˆæŽ¥å£è¿”回全量数组,前端分批渲染)
  const statRows = ref([]);
  const pageSize = 10;
  const showCount = ref(pageSize);
  const statStatus = ref("loadmore");
  const visibleStatRows = computed(() => statRows.value.slice(0, showCount.value));
  // æ±‡æ€»
  const groupTabs = [
    { name: "按月份", value: "month" },
    { name: "按成品", value: "product" },
  ];
  const groupIndex = ref(0);
  const groupBy = computed(() => groupTabs[groupIndex.value].value);
  const summaryList = ref([]);
  // ç¼ºä»·ç‰©æ–™
  const missingList = ref([]);
  const goBack = () => {
    uni.navigateBack();
  };
  // ============ æ•°å€¼æ ¼å¼åŒ– ============
  const EMPTY = "—";
  const isBlank = v => v === null || v === undefined || v === "";
  const toFinite = v => {
    if (isBlank(v)) return null;
    const n = Number(v);
    return Number.isFinite(n) ? n : null;
  };
  const fmtNum = v => {
    const n = toFinite(v);
    return n === null ? EMPTY : String(n);
  };
  const fmtCount = v => {
    const n = toFinite(v);
    return n === null ? EMPTY : String(n);
  };
  const fmtMoney = v => {
    const n = toFinite(v);
    return n === null ? EMPTY : n.toFixed(2);
  };
  // å•位成本后端保留 4 ä½
  const fmtUnitCost = v => {
    const n = toFinite(v);
    return n === null ? EMPTY : n.toFixed(4);
  };
  const fmtPercent = v => {
    const n = toFinite(v);
    return n === null ? EMPTY : `${n.toFixed(2)}%`;
  };
  const isPositive = v => (toFinite(v) ?? 0) > 0;
  const isNegative = v => (toFinite(v) ?? 0) < 0;
  const isLowCoverage = v => {
    const n = toFinite(v);
    return n !== null && n < 100;
  };
  // å æ¯”条段宽(占比为 null æ—¶æŒ‰ 0 å¤„理)
  const segWidth = v => {
    const n = toFinite(v) ?? 0;
    return `${Math.max(0, Math.min(n, 100))}%`;
  };
  // ============ æŸ¥è¯¢ ============
  const buildParams = () => {
    const params = {};
    if (query.startDate) params.startDate = query.startDate;
    if (query.endDate) params.endDate = query.endDate;
    if (query.npsNo) params.npsNo = query.npsNo;
    if (query.productName) params.productName = query.productName;
    return params;
  };
  const syncStatStatus = () => {
    statStatus.value =
      showCount.value >= statRows.value.length ? "nomore" : "loadmore";
  };
  const fetchStat = async () => {
    try {
      const res = await costStat(buildParams());
      const rows = res?.data ?? res ?? [];
      statRows.value = Array.isArray(rows) ? rows : [];
      showCount.value = pageSize;
      syncStatStatus();
    } catch (e) {
      // é”™è¯¯æ–‡æ¡ˆå·²ç”± request ç»Ÿä¸€ toast
      statRows.value = [];
    }
  };
  const fetchSummary = async () => {
    try {
      const res = await costSummary({ ...buildParams(), groupBy: groupBy.value });
      const rows = res?.data ?? res ?? [];
      summaryList.value = Array.isArray(rows) ? rows : [];
    } catch (e) {
      summaryList.value = [];
    }
  };
  const fetchMissing = async () => {
    try {
      const res = await costMissingPrice(buildParams());
      const rows = res?.data ?? res ?? [];
      missingList.value = Array.isArray(rows) ? rows : [];
    } catch (e) {
      missingList.value = [];
    }
  };
  const loadCurrentTab = async () => {
    loading.value = true;
    try {
      if (mainTabIndex.value === 0) await fetchStat();
      else if (mainTabIndex.value === 1) await fetchSummary();
      else await fetchMissing();
    } finally {
      loading.value = false;
    }
  };
  const handleQuery = () => {
    loadCurrentTab();
  };
  const handleMainTabChange = item => {
    mainTabIndex.value = item.index;
    loadCurrentTab();
  };
  const handleGroupChange = item => {
    groupIndex.value = item.index;
    fetchSummary();
  };
  const onDateConfirm = e => {
    query.startDate = e[0];
    query.endDate = e[e.length - 1];
    showCalendar.value = false;
    handleQuery();
  };
  const handleClearDate = () => {
    query.startDate = "";
    query.endDate = "";
    handleQuery();
  };
  onShow(() => {
    loadCurrentTab();
  });
  // ä»…明细需要分页渲染
  onReachBottom(() => {
    if (mainTabIndex.value !== 0) return;
    if (statStatus.value === "nomore") return;
    showCount.value += pageSize;
    syncStatStatus();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .cost-accounting {
    min-height: 100vh;
    background: #f8f9fa;
  }
  .main-tabs {
    background: #fff;
    margin-bottom: 10px;
  }
  .sub-tabs {
    background: #fff;
    padding: 0 10px 8px;
  }
  .loading-box {
    padding-top: 150rpx;
    display: flex;
    justify-content: center;
  }
  .filter-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    & + .filter-bar {
      margin-top: 12px;
    }
  }
  .date-input,
  .search-input {
    flex: 1;
    height: 40px;
    background: #f5f5f5;
    border-radius: 8px;
    padding: 0 12px;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .date-input .date-text {
    flex: 1;
    font-size: 14px;
    color: #333;
  }
  .placeholder {
    color: #999;
  }
  .filter-actions-top {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 12px;
  }
  .item-index {
    font-size: 12px;
    color: #999;
    background: #f5f5f5;
    padding: 2px 8px;
    border-radius: 12px;
  }
  .detail-value.warn {
    color: #ff9800;
    font-weight: 500;
  }
  .warn-tip {
    margin-top: 8px;
    font-size: 11px;
    color: #ff9800;
    line-height: 1.6;
  }
  .warn-icon {
    background: linear-gradient(135deg, #ff9800, #f57c00);
  }
  // æˆæœ¬æž„成占比条
  .rate-bar {
    display: flex;
    height: 12px;
    border-radius: 6px;
    overflow: hidden;
    background: #f0f0f0;
    margin-bottom: 8px;
    .rate-seg {
      height: 100%;
    }
  }
  .rate-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 12px;
    .legend-item {
      display: flex;
      align-items: center;
      gap: 4px;
    }
    .legend-dot {
      width: 8px;
      height: 8px;
      border-radius: 2px;
    }
    .legend-text {
      font-size: 11px;
      color: #777;
    }
  }
  .seg-material {
    background: #2979ff;
  }
  .seg-labor {
    background: #ff9800;
  }
  .seg-energy {
    background: #4caf50;
  }
  .ledger-item.no-click:active {
    transform: none;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
</style>
src/pages/productionManagement/materialConsumption/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,507 @@
<template>
  <view class="material-consumption">
    <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.npsNo"
                    @change="handleQuery"
                    clearable />
        </view>
      </view>
      <view class="search-bar mt-16">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入成品名称/规格"
                    v-model="searchForm.productName"
                    @change="handleQuery"
                    clearable />
        </view>
      </view>
      <view class="filter-actions-row">
        <view class="switch-item">
          <text class="switch-label">只看差异</text>
          <up-switch v-model="searchForm.onlyDiff"
                     size="18"
                     @change="handleQuery" />
        </view>
      </view>
      <view class="date-picker-container">
        <view class="date-input"
              @click="showCalendar = true">
          <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>
    <!-- æ±‡æ€» -->
    <view class="summary-bar"
          v-if="groupList.length > 0">
      <text class="summary-text">共 {{ groupList.length }} ä¸ªè®¢å• / {{ allRows.length }} è¡Œç‰©æ–™</text>
    </view>
    <!-- åˆ—表区域(按生产订单分组) -->
    <view v-if="loading && groupList.length === 0"
          class="loading-box">
      <up-loading-icon text="加载中..."></up-loading-icon>
    </view>
    <view class="ledger-list"
          v-else-if="groupList.length > 0">
      <view class="ledger-item"
            v-for="group in visibleGroups"
            :key="group.key">
        <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">{{ group.npsNo }}</text>
          </view>
          <view class="item-right">
            <text class="item-index">{{ group.items.length }} é¡¹ç‰©æ–™</text>
          </view>
        </view>
        <view class="order-product">
          <text class="order-product-label">成品</text>
          <text class="order-product-name">{{ group.finishedProduct }}</text>
        </view>
        <up-divider></up-divider>
        <view class="material-list">
          <view class="material-item"
                :class="{ 'row-warn': isDiffWarn(m) }"
                v-for="(m, mi) in group.items"
                :key="mi">
            <view class="material-header">
              <view class="material-name-box">
                <text class="material-name">{{ m.materialName || "-" }}</text>
                <text class="material-model">{{ m.materialModel || "-" }}</text>
              </view>
              <view class="material-tags">
                <text class="off-bom-tag"
                      v-if="isOffBom(m)">BOM外</text>
                <text class="diff-tag"
                      :class="{ warn: isDiffWarn(m) }">{{ fmtRate(m.diffRate) }}</text>
              </view>
            </view>
            <view class="material-grid">
              <view class="detail-cell">
                <text class="cell-label">单位</text>
                <text class="cell-value">{{ m.unit || "-" }}</text>
              </view>
              <view class="detail-cell">
                <text class="cell-label">BOM单耗</text>
                <text class="cell-value">{{ fmtQty(m.unitQuantity) }}</text>
              </view>
              <view class="detail-cell">
                <text class="cell-label">完工数量</text>
                <text class="cell-value">{{ fmtQty(m.completeQty) }}</text>
              </view>
              <view class="detail-cell">
                <text class="cell-label">标准消耗</text>
                <text class="cell-value">{{ fmtQty(m.standardQty) }}</text>
              </view>
              <view class="detail-cell">
                <text class="cell-label">实际消耗</text>
                <text class="cell-value">{{ fmtQty(m.actualQty) }}</text>
              </view>
              <view class="detail-cell">
                <text class="cell-label">差异数量</text>
                <text class="cell-value"
                      :class="{ warn: isDiffWarn(m) }">{{ fmtQty(m.diffQty) }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <up-loadmore :status="pageStatus"
                   v-if="groupList.length >= pageSize" />
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无数据"></up-empty>
    </view>
    <!-- æ—¥åŽ†é€‰æ‹©å™¨ -->
    <up-calendar :show="showCalendar"
                 mode="range"
                 :maxDate="maxDate"
                 minDate="2026-01-01"
                 :monthNum="monthNum"
                 :defaultDate="defaultDateRange"
                 @confirm="onDateConfirm"
                 @close="showCalendar = false"></up-calendar>
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  import { stat } from "@/api/productionManagement/materialConsumption.js";
  import { getWeekRange } from "@/utils/common";
  const loading = ref(false);
  const showCalendar = ref(false);
  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 weekRange = getWeekRange();
  const searchForm = reactive({
    startDate: weekRange.startDate,
    endDate: weekRange.endDate,
    npsNo: "",
    productName: "",
    onlyDiff: false,
  });
  // å…¨é‡æ˜Žç»†è¡Œï¼ˆæŽ¥å£éžåˆ†é¡µï¼Œå‰ç«¯æŒ‰è®¢å•分组后分批渲染)
  const allRows = ref([]);
  const pageSize = 10;
  const showCount = ref(pageSize);
  const pageStatus = ref("loadmore");
  const dateRangeText = computed(() => {
    if (searchForm.startDate && searchForm.endDate) {
      return `${searchForm.startDate} è‡³ ${searchForm.endDate}`;
    }
    return "请选择日期区间";
  });
  // æ‰“开日历时回显当前区间
  const defaultDateRange = computed(() =>
    searchForm.startDate && searchForm.endDate
      ? [searchForm.startDate, searchForm.endDate]
      : []
  );
  // æŒ‰ç”Ÿäº§è®¢å•分组
  const groupList = computed(() => {
    const map = new Map();
    (allRows.value || []).forEach(row => {
      const key = String(row.productionOrderId ?? row.npsNo ?? "unknown");
      if (!map.has(key)) {
        map.set(key, {
          key,
          npsNo: row.npsNo || "-",
          finishedProduct: row.finishedProduct || "-",
          items: [],
        });
      }
      map.get(key).items.push(row);
    });
    return Array.from(map.values());
  });
  const visibleGroups = computed(() => groupList.value.slice(0, showCount.value));
  const goBack = () => {
    uni.navigateBack();
  };
  // æ•°å€¼å…œåº•
  const toNum = v => Number(v) || 0;
  const fmtQty = v => {
    if (v === null || v === undefined || v === "") return "-";
    const n = Number(v);
    if (Number.isNaN(n)) return "-";
    return n.toFixed(2);
  };
  const fmtRate = v => {
    if (v === null || v === undefined || v === "") return "-";
    const n = Number(v);
    if (Number.isNaN(n)) return "-";
    return (n > 0 ? "+" : "") + n.toFixed(2) + "%";
  };
  // å·®å¼‚率绝对值 > 10 æ ‡çº¢
  const isDiffWarn = row => {
    if (row?.diffRate === null || row?.diffRate === undefined || row?.diffRate === "") {
      return false;
    }
    return Math.abs(Number(row.diffRate)) > 10;
  };
  // BOM外投入:单耗与标准消耗均为0
  const isOffBom = row => toNum(row?.unitQuantity) === 0 && toNum(row?.standardQty) === 0;
  const buildParams = () => {
    const params = {};
    if (searchForm.startDate) params.startDate = searchForm.startDate;
    if (searchForm.endDate) params.endDate = searchForm.endDate;
    if (searchForm.npsNo) params.npsNo = searchForm.npsNo;
    if (searchForm.productName) params.productName = searchForm.productName;
    if (searchForm.onlyDiff) params.onlyDiff = true;
    return params;
  };
  const syncStatus = () => {
    pageStatus.value =
      showCount.value >= groupList.value.length ? "nomore" : "loadmore";
  };
  const getList = async () => {
    loading.value = true;
    try {
      const res = await stat(buildParams());
      const rows = res?.data ?? res ?? [];
      allRows.value = Array.isArray(rows) ? rows : [];
      showCount.value = pageSize;
      syncStatus();
    } catch (e) {
      // é”™è¯¯æ–‡æ¡ˆå·²ç”± request ç»Ÿä¸€ toast
    } finally {
      loading.value = false;
    }
  };
  const handleQuery = () => {
    getList();
  };
  const onDateConfirm = e => {
    searchForm.startDate = e[0];
    searchForm.endDate = e[e.length - 1];
    showCalendar.value = false;
    getList();
  };
  const handleClearDate = () => {
    searchForm.startDate = "";
    searchForm.endDate = "";
    getList();
  };
  onShow(() => {
    getList();
  });
  onReachBottom(() => {
    if (pageStatus.value === "nomore" || pageStatus.value === "loading") return;
    pageStatus.value = "loading";
    setTimeout(() => {
      showCount.value += pageSize;
      syncStatus();
    }, 200);
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .material-consumption {
    min-height: 100vh;
    background: #f8f9fa;
    display: flex;
    flex-direction: column;
  }
  .mt-16 {
    margin-top: 16rpx;
  }
  .filter-actions-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 16rpx;
  }
  .switch-item {
    display: flex;
    align-items: center;
  }
  .switch-label {
    font-size: 14px;
    color: #333;
    margin-right: 12rpx;
  }
  .date-picker-container {
    display: flex;
    align-items: center;
    width: 100%;
    margin-top: 16rpx;
    .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;
      .date-text {
        font-size: 28rpx;
        color: #303133;
        margin-left: 16rpx;
        flex: 1;
        &.placeholder {
          color: #c0c4cc;
        }
      }
    }
    .search-btn-wrapper {
      width: 140rpx;
    }
  }
  .loading-box {
    display: flex;
    justify-content: center;
    padding-top: 100rpx;
  }
  .summary-bar {
    padding: 0 20px 4px;
    .summary-text {
      font-size: 12px;
      color: #999;
    }
  }
  .order-product {
    display: flex;
    align-items: center;
    padding-bottom: 12px;
    .order-product-label {
      font-size: 12px;
      color: #999;
      background: #f5f5f5;
      padding: 2px 8px;
      border-radius: 4px;
      margin-right: 8px;
    }
    .order-product-name {
      font-size: 14px;
      color: #333;
      font-weight: 500;
      flex: 1;
    }
  }
  .material-item {
    padding: 12px 0;
    border-bottom: 1px solid #f5f5f5;
    &:last-child {
      border-bottom: none;
    }
    &.row-warn {
      background: rgba(238, 10, 36, 0.04);
    }
    .material-header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      margin-bottom: 10px;
      .material-name-box {
        display: flex;
        align-items: center;
        flex: 1;
        flex-wrap: wrap;
        .material-name {
          font-size: 14px;
          color: #333;
          font-weight: 500;
          margin-right: 8px;
        }
        .material-model {
          font-size: 12px;
          color: #999;
        }
      }
      .material-tags {
        display: flex;
        align-items: center;
        flex-shrink: 0;
      }
      .off-bom-tag {
        font-size: 11px;
        color: #ff9800;
        border: 1px solid #ffcc80;
        border-radius: 4px;
        padding: 0 6px;
        margin-right: 8px;
      }
      .diff-tag {
        font-size: 12px;
        color: #999;
        &.warn {
          color: #ee0a24;
          font-weight: 600;
        }
      }
    }
    .material-grid {
      display: flex;
      flex-wrap: wrap;
      .detail-cell {
        width: 33.33%;
        display: flex;
        flex-direction: column;
        margin-bottom: 8px;
        .cell-label {
          font-size: 11px;
          color: #999;
          margin-bottom: 2px;
        }
        .cell-value {
          font-size: 13px;
          color: #333;
          &.warn {
            color: #ee0a24;
            font-weight: 600;
          }
        }
      }
    }
  }
</style>
src/pages/productionManagement/outputStatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,412 @@
<template>
  <view class="output-statistics">
    <PageHeader title="产量统计"
                @back="goBack" />
    <!-- é¡¶éƒ¨é¡µç­¾ï¼šäº§é‡ç»Ÿè®¡ / è®¡ä»¶å·¥èµ„汇总 -->
    <view class="main-tabs">
      <up-tabs :list="mainTabs"
               :current="mainTabIndex"
               @change="handleMainTabChange"
               :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
               lineWidth="40"
               lineHeight="3" />
    </view>
    <!-- ============ äº§é‡ç»Ÿè®¡ ============ -->
    <template v-if="mainTabIndex === 0">
      <view class="search-section">
        <view class="dimension-tabs">
          <up-tabs :list="dimensionTabs"
                   :current="dimensionIndex"
                   @change="handleDimensionChange"
                   :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
                   lineWidth="30"
                   lineHeight="3" />
        </view>
        <view class="filter-bar">
          <view class="date-input"
                @click="showOutputCalendar = true">
            <up-icon name="calendar"
                     size="18"
                     color="#999"></up-icon>
            <text class="date-text"
                  :class="{ placeholder: !outputForm.startDate }">{{ outputDateText }}</text>
          </view>
          <view v-if="dimension === 'device'"
                class="device-input"
                @click="openDeviceSheet">
            <text :class="{ placeholder: !outputForm.deviceName }">{{ outputForm.deviceName || "选择设备" }}</text>
            <up-icon name="arrow-down"
                     size="14"
                     color="#999"></up-icon>
          </view>
        </view>
        <view class="filter-actions-top">
          <up-button type="primary"
                     size="small"
                     text="搜索"
                     @click="handleOutputSearch"></up-button>
        </view>
      </view>
      <view v-if="outputList.length > 0"
            class="ledger-list">
        <view v-for="(item, index) in outputList"
              :key="index"
              class="ledger-item no-click">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="list-dot"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.groupName || "未指定" }}</text>
            </view>
            <text class="item-index">{{ item.workCount || 0 }} å•</text>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">产量</text>
              <text class="detail-value highlight">{{ Number(item.quantity) || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">报废数量</text>
              <text class="detail-value danger">{{ Number(item.scrapQty) || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工时(h)</text>
              <text class="detail-value">{{ Number(item.workHour) || 0 }}</text>
            </view>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无数据"></up-empty>
      </view>
    </template>
    <!-- ============ è®¡ä»¶å·¥èµ„汇总 ============ -->
    <template v-else>
      <view class="search-section">
        <view class="filter-bar">
          <view class="date-input"
                @click="showWageCalendar = true">
            <up-icon name="calendar"
                     size="18"
                     color="#999"></up-icon>
            <text class="date-text"
                  :class="{ placeholder: !wageForm.startDate }">{{ wageDateText }}</text>
          </view>
          <up-input class="name-input"
                    v-model="wageForm.userName"
                    placeholder="人员姓名"
                    clearable
                    @change="handleWageSearch" />
        </view>
        <view class="filter-actions-top">
          <up-button type="primary"
                     size="small"
                     text="搜索"
                     @click="handleWageSearch"></up-button>
        </view>
      </view>
      <view v-if="wageList.length > 0"
            class="ledger-list">
        <view v-for="(item, index) in wageList"
              :key="index"
              class="ledger-item no-click">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="account"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.userName || "未知" }}</text>
            </view>
            <text class="item-index">{{ item.workCount || 0 }} å•</text>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">生产数量</text>
              <text class="detail-value">{{ Number(item.quantity) || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">总工时(h)</text>
              <text class="detail-value">{{ Number(item.workHour) || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">计件工资</text>
              <text class="detail-value">Â¥{{ Number(item.pieceWage) || 0 }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">应发合计</text>
              <text class="detail-value highlight">Â¥{{ Number(item.wages) || 0 }}</text>
            </view>
          </view>
        </view>
      </view>
      <view v-else
            class="no-data">
        <up-empty mode="data"
                  text="暂无数据"></up-empty>
      </view>
    </template>
    <!-- æ—¥æœŸåŒºé—´é€‰æ‹© -->
    <up-calendar :show="showOutputCalendar"
                 mode="range"
                 :maxDate="maxDate"
                 minDate="2022-01-01"
                 :monthNum="monthNum"
                 @confirm="onOutputDateConfirm"
                 @close="showOutputCalendar = false"></up-calendar>
    <up-calendar :show="showWageCalendar"
                 mode="range"
                 :maxDate="maxDate"
                 minDate="2022-01-01"
                 :monthNum="monthNum"
                 @confirm="onWageDateConfirm"
                 @close="showWageCalendar = false"></up-calendar>
    <!-- è®¾å¤‡ç­›é€‰ -->
    <up-action-sheet :show="showDeviceSheet"
                     :actions="deviceOptions"
                     title="选择设备"
                     @select="selectDevice"
                     @close="showDeviceSheet = false" />
  </view>
</template>
<script setup>
  import { ref, reactive, computed } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import dayjs from "dayjs";
  import PageHeader from "@/components/PageHeader.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import {
    outputStat,
    wageSummary,
  } from "@/api/productionManagement/productionProductMain.js";
  const goBack = () => {
    uni.navigateBack();
  };
  // é¡¶éƒ¨é¡µç­¾
  const mainTabs = [{ name: "产量统计" }, { name: "计件工资汇总" }];
  const mainTabIndex = ref(0);
  const handleMainTabChange = item => {
    mainTabIndex.value = item.index;
    if (mainTabIndex.value === 0) {
      handleOutputSearch();
    } else {
      handleWageSearch();
    }
  };
  // ç»´åº¦ï¼šdevice / operation / product
  const dimensionTabs = [
    { name: "按设备", value: "device" },
    { name: "按工序", value: "operation" },
    { name: "按产品", value: "product" },
  ];
  const dimensionIndex = ref(0);
  const dimension = computed(() => dimensionTabs[dimensionIndex.value].value);
  const handleDimensionChange = item => {
    dimensionIndex.value = item.index;
    handleOutputSearch();
  };
  // æ—¥æœŸåŒºé—´
  const maxDate = dayjs().format("YYYY-MM-DD");
  const monthNum = computed(() => dayjs(maxDate).diff(dayjs("2022-02-01"), "month") + 1);
  const showOutputCalendar = ref(false);
  const showWageCalendar = ref(false);
  // äº§é‡ç»Ÿè®¡
  const outputForm = reactive({
    startDate: dayjs().startOf("month").format("YYYY-MM-DD"),
    endDate: dayjs().endOf("month").format("YYYY-MM-DD"),
    deviceId: "",
    deviceName: "",
  });
  const outputList = ref([]);
  const outputDateText = computed(() =>
    outputForm.startDate && outputForm.endDate
      ? `${outputForm.startDate} è‡³ ${outputForm.endDate}`
      : "请选择日期区间"
  );
  const onOutputDateConfirm = e => {
    outputForm.startDate = e[0];
    outputForm.endDate = e[e.length - 1];
    showOutputCalendar.value = false;
    handleOutputSearch();
  };
  // è®¡ä»¶å·¥èµ„汇总
  const wageForm = reactive({
    startDate: dayjs().startOf("month").format("YYYY-MM-DD"),
    endDate: dayjs().endOf("month").format("YYYY-MM-DD"),
    userName: "",
  });
  const wageList = ref([]);
  const wageDateText = computed(() =>
    wageForm.startDate && wageForm.endDate
      ? `${wageForm.startDate} è‡³ ${wageForm.endDate}`
      : "请选择日期区间"
  );
  const onWageDateConfirm = e => {
    wageForm.startDate = e[0];
    wageForm.endDate = e[e.length - 1];
    showWageCalendar.value = false;
    handleWageSearch();
  };
  // è®¾å¤‡ä¸‹æ‹‰
  const showDeviceSheet = ref(false);
  const deviceOptions = ref([]);
  const loadDevices = async () => {
    if (deviceOptions.value.length > 0) return;
    try {
      const { data } = await getDeviceLedger();
      const rows = Array.isArray(data) ? data : data?.records || [];
      deviceOptions.value = rows.map(item => ({
        name: item.deviceName || item.name || "-",
        value: item.id,
      }));
    } catch (e) {
      console.error("加载设备列表失败", e);
    }
  };
  const openDeviceSheet = () => {
    loadDevices();
    showDeviceSheet.value = true;
  };
  const selectDevice = e => {
    outputForm.deviceId = e.value;
    outputForm.deviceName = e.name;
    showDeviceSheet.value = false;
    handleOutputSearch();
  };
  // æŸ¥è¯¢ï¼šäº§é‡ç»Ÿè®¡
  const handleOutputSearch = () => {
    outputStat({
      dimension: dimension.value,
      startDate: outputForm.startDate || undefined,
      endDate: outputForm.endDate || undefined,
      deviceId: dimension.value === "device" ? outputForm.deviceId || undefined : undefined,
    })
      .then(res => {
        outputList.value = Array.isArray(res.data) ? res.data : res.data?.records || [];
      })
      .catch(() => {
        outputList.value = [];
      });
  };
  // æŸ¥è¯¢ï¼šè®¡ä»¶å·¥èµ„汇总
  const handleWageSearch = () => {
    wageSummary({
      startDate: wageForm.startDate || undefined,
      endDate: wageForm.endDate || undefined,
      userName: wageForm.userName || undefined,
    })
      .then(res => {
        wageList.value = Array.isArray(res.data) ? res.data : res.data?.records || [];
      })
      .catch(() => {
        wageList.value = [];
      });
  };
  onShow(() => {
    handleOutputSearch();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .output-statistics {
    min-height: 100vh;
    background: #f8f9fa;
  }
  .main-tabs {
    background: #fff;
    margin-bottom: 10px;
  }
  .dimension-tabs {
    margin-bottom: 16px;
  }
  .filter-bar {
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .date-input,
  .device-input {
    flex: 1;
    height: 40px;
    background: #f5f5f5;
    border-radius: 8px;
    padding: 0 12px;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .date-input .date-text,
  .device-input {
    font-size: 14px;
    color: #333;
  }
  .device-input {
    justify-content: space-between;
  }
  .placeholder {
    color: #999;
  }
  .name-input {
    flex: 1;
    height: 40px;
    background: #f5f5f5;
    border-radius: 8px;
    padding: 0 12px;
  }
  .filter-actions-top {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 12px;
  }
  .item-index {
    font-size: 12px;
    color: #999;
    background: #f5f5f5;
    padding: 2px 8px;
    border-radius: 12px;
  }
  .ledger-item.no-click:active {
    transform: none;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
</style>
src/pages/productionManagement/pieceRateConfig/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,698 @@
<template>
  <view class="sales-account">
    <PageHeader title="计件单价配置"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="filter-row">
        <up-input class="filter-input"
                  v-model="queryParams.operationName"
                  placeholder="工序名称"
                  clearable
                  @change="handleSearch" />
        <up-input class="filter-input"
                  v-model="queryParams.productModel"
                  placeholder="产品规格"
                  clearable
                  @change="handleSearch" />
        <view class="filter-status"
              @click="showStatusSheet = true">
          <text :class="{ placeholder: statusText === '' }">{{ statusText || "全部状态" }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#999"></up-icon>
        </view>
      </view>
      <view class="filter-actions-top">
        <up-button type="primary"
                   size="small"
                   text="搜索"
                   @click="handleSearch"></up-button>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view v-if="list.length > 0"
          class="ledger-list">
      <view v-for="item in list"
            :key="item.id"
            class="ledger-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="setting"
                       size="16"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-id">{{ item.productModel || "-" }}</text>
          </view>
          <up-tag :text="item.status == 1 ? '启用' : '停用'"
                  :type="item.status == 1 ? 'success' : 'info'"
                  size="mini" />
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">计件单价</text>
            <text class="detail-value highlight">Â¥{{ Number(item.unitPrice) || 0 }} / ä»¶</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工序名称</text>
            <text class="detail-value">{{ item.operationName || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark || "-" }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">创建时间</text>
            <text class="detail-value">{{ item.createTime || "-" }}</text>
          </view>
        </view>
        <view class="action-buttons">
          <up-button class="action-btn"
                     size="small"
                     :type="item.status == 1 ? 'warning' : 'success'"
                     @click="handleChangeStatus(item)">{{ item.status == 1 ? "停用" : "启用" }}</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="primary"
                     @click="handleEdit(item)">编辑</up-button>
          <up-button class="action-btn"
                     size="small"
                     type="error"
                     @click="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <up-loadmore :status="pageStatus" />
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无数据"></up-empty>
    </view>
    <!-- æ–°å¢žæŒ‰é’® -->
    <view class="fab-button"
          @click="handleAdd">
      <up-icon name="plus"
               size="28"
               color="#ffffff"></up-icon>
    </view>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <up-popup v-model:show="showForm"
              mode="bottom"
              :round="20"
              :safeAreaInsetBottom="true"
              @close="showForm = false">
      <view class="form-popup">
        <view class="popup-header">
          <text class="popup-title">{{ form.id ? "编辑计件单价" : "新增计件单价" }}</text>
        </view>
        <view class="popup-body">
          <up-form labelWidth="120">
            <up-form-item label="产品"
                          required>
              <view class="pick-field"
                    @click="showProductTree = true">
                <up-input class="pick-input"
                          v-model="form.productName"
                          placeholder="请选择产品"
                          readonly />
                <up-icon name="arrow-right"
                         color="#999999"></up-icon>
              </view>
            </up-form-item>
            <up-form-item label="规格型号"
                          required>
              <view class="pick-field"
                    @click="openModelSheet">
                <up-input class="pick-input"
                          v-model="form.model"
                          placeholder="请选择规格型号"
                          readonly />
                <up-icon name="arrow-right"
                         color="#999999"></up-icon>
              </view>
            </up-form-item>
            <up-form-item label="工序名称"
                          required>
              <up-input v-model="form.operationName"
                        placeholder="请输入工序名称"
                        clearable />
            </up-form-item>
            <up-form-item label="计件单价"
                          required>
              <up-input v-model="form.unitPrice"
                        type="number"
                        placeholder="请输入单价(元/件)" />
              <text class="unit-text">元/ä»¶</text>
            </up-form-item>
            <up-form-item label="备注">
              <up-input v-model="form.remark"
                        placeholder="请输入备注"
                        clearable />
            </up-form-item>
          </up-form>
        </view>
        <view class="popup-footer">
          <up-button text="取消"
                     type="info"
                     plain
                     :customStyle="{ flex: 1, marginRight: '12px' }"
                     @click="showForm = false" />
          <up-button text="保存"
                     type="primary"
                     :customStyle="{ flex: 1 }"
                     :loading="submitting"
                     @click="handleSubmit" />
        </view>
      </view>
    </up-popup>
    <!-- äº§å“æ ‘选择器 -->
    <up-popup v-model:show="showProductTree"
              mode="bottom"
              :round="true"
              :closeable="true"
              @close="showProductTree = false">
      <view class="tree-selector">
        <view class="tree-header">
          <text class="tree-title">选择产品</text>
        </view>
        <scroll-view scroll-y
                     class="tree-content">
          <view v-for="(node, index) in productOptions"
                :key="index"
                class="tree-node">
            <view v-if="node.children && node.children.length > 0"
                  class="tree-node-header"
                  @click="toggleNode(node)">
              <up-icon :name="node.expanded ? 'arrow-down' : 'arrow-right'"
                       class="tree-node-icon" />
              <text class="tree-node-label">{{ node.label }}</text>
            </view>
            <view v-else
                  class="tree-node-header"
                  @click="selectTreeNode(node)">
              <text class="tree-node-icon-placeholder"></text>
              <text class="tree-node-label">{{ node.label }}</text>
              <up-icon name="checkmark"
                       v-if="form.productId == node.value"
                       class="tree-node-check" />
            </view>
            <view v-if="node.children && node.children.length > 0 && node.expanded"
                  class="tree-node-children">
              <view v-for="(child, childIndex) in node.children"
                    :key="childIndex"
                    class="tree-node">
                <view v-if="child.children && child.children.length > 0"
                      class="tree-node-header"
                      @click="toggleNode(child)">
                  <up-icon :name="child.expanded ? 'arrow-down' : 'arrow-right'"
                           class="tree-node-icon" />
                  <text class="tree-node-label">{{ child.label }}</text>
                </view>
                <view v-else
                      class="tree-node-header"
                      @click="selectTreeNode(child)">
                  <text class="tree-node-icon-placeholder"></text>
                  <text class="tree-node-label">{{ child.label }}</text>
                  <up-icon name="checkmark"
                           v-if="form.productId == child.value"
                           class="tree-node-check" />
                </view>
                <view v-if="child.children && child.children.length > 0 && child.expanded"
                      class="tree-node-children">
                  <view v-for="(leaf, leafIndex) in child.children"
                        :key="leafIndex"
                        class="tree-node-header"
                        @click="selectTreeNode(leaf)">
                    <text class="tree-node-icon-placeholder"></text>
                    <text class="tree-node-label">{{ leaf.label }}</text>
                    <up-icon name="checkmark"
                             v-if="form.productId == leaf.value"
                             class="tree-node-check" />
                  </view>
                </view>
              </view>
            </view>
          </view>
        </scroll-view>
      </view>
    </up-popup>
    <!-- è§„格型号选择器 -->
    <up-action-sheet :show="showModelSheet"
                     :actions="modelOptions"
                     title="选择规格型号"
                     @select="selectModel"
                     @close="showModelSheet = false" />
    <!-- çŠ¶æ€ç­›é€‰ -->
    <up-action-sheet :show="showStatusSheet"
                     :actions="statusOptions"
                     title="选择状态"
                     @select="selectStatus"
                     @close="showStatusSheet = false" />
  </view>
</template>
<script setup>
  import { ref, reactive } from "vue";
  import { onReachBottom, onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { productTreeList, modelList } from "@/api/basicData/product.js";
  import {
    listPage,
    add,
    update,
    changeStatus,
    batchDelete,
  } from "@/api/productionManagement/pieceRateConfig.js";
  const goBack = () => {
    uni.navigateBack();
  };
  // åˆ—表与分页
  const list = ref([]);
  const pageStatus = ref("loadmore");
  const loading = ref(false);
  const page = reactive({ current: 1, size: 10, total: 0 });
  const queryParams = reactive({
    operationName: "",
    productModel: "",
    status: "",
  });
  const statusText = ref("");
  // çŠ¶æ€ç­›é€‰é€‰é¡¹
  const statusOptions = [
    { name: "全部状态", value: "" },
    { name: "启用", value: 1 },
    { name: "停用", value: 0 },
  ];
  const showStatusSheet = ref(false);
  const selectStatus = e => {
    queryParams.status = e.value;
    statusText.value = e.name === "全部状态" ? "" : e.name;
    showStatusSheet.value = false;
    handleSearch();
  };
  // å…¼å®¹ä¸åŒå±‚级的列表返回结构
  const pickList = res => {
    const rows = res?.data?.records || res?.data || res?.records || res;
    return Array.isArray(rows) ? rows : [];
  };
  const getList = () => {
    if (loading.value || pageStatus.value === "loading") return;
    loading.value = true;
    pageStatus.value = "loading";
    const params = {
      current: page.current,
      size: page.size,
      operationName: queryParams.operationName || undefined,
      productModel: queryParams.productModel || undefined,
      status: queryParams.status === "" ? undefined : queryParams.status,
    };
    listPage(params)
      .then(res => {
        const records = res?.data?.records || res?.records || [];
        const total = res?.data?.total || res?.total || 0;
        list.value =
          page.current === 1 ? records : [...list.value, ...records];
        page.total = total;
        if (list.value.length >= total || records.length < page.size) {
          pageStatus.value = "nomore";
        } else {
          pageStatus.value = "loadmore";
          page.current++;
        }
      })
      .catch(() => {
        pageStatus.value = "loadmore";
      })
      .finally(() => {
        loading.value = false;
      });
  };
  const handleSearch = () => {
    page.current = 1;
    list.value = [];
    pageStatus.value = "loadmore";
    getList();
  };
  // ============ è¡¨å• ============
  const showForm = ref(false);
  const submitting = ref(false);
  const form = ref({
    id: null,
    productId: "",
    productName: "",
    productModelId: "",
    model: "",
    operationName: "",
    unitPrice: "",
    remark: "",
  });
  // äº§å“æ ‘
  const showProductTree = ref(false);
  const productOptions = ref([]);
  const showModelSheet = ref(false);
  const modelOptions = ref([]);
  const convertIdToValue = data => {
    return (data || []).map(item => {
      const { id, children, ...rest } = item;
      const newItem = { ...rest, value: id };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  };
  const findNodeById = (nodes, id) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === id) return nodes[i].label;
      if (nodes[i].children && nodes[i].children.length > 0) {
        const found = findNodeById(nodes[i].children, id);
        if (found) return found;
      }
    }
    return null;
  };
  const getProductOptions = () => {
    if (productOptions.value.length > 0) return;
    productTreeList()
      .then(res => {
        productOptions.value = convertIdToValue(pickList(res));
      })
      .catch(() => {});
  };
  const toggleNode = node => {
    node.expanded = !node.expanded;
  };
  const selectTreeNode = node => {
    if (node.children && node.children.length > 0) return;
    form.value.productId = node.value;
    form.value.productName = node.label;
    showProductTree.value = false;
    getModels(node.value);
  };
  const getModels = productId => {
    form.value.productModelId = "";
    form.value.model = "";
    modelOptions.value = [];
    modelList({ id: productId })
      .then(res => {
        modelOptions.value = pickList(res).map(item => ({
          name: item.model,
          value: item.id,
        }));
      })
      .catch(() => {
        modelOptions.value = [];
      });
  };
  const openModelSheet = () => {
    if (!form.value.productId) {
      uni.showToast({ title: "请先选择产品", icon: "none" });
      return;
    }
    showModelSheet.value = true;
  };
  const selectModel = e => {
    form.value.productModelId = e.value;
    form.value.model = e.name;
    showModelSheet.value = false;
  };
  const handleAdd = () => {
    form.value = {
      id: null,
      productId: "",
      productName: "",
      productModelId: "",
      model: "",
      operationName: "",
      unitPrice: "",
      remark: "",
    };
    getProductOptions();
    showForm.value = true;
  };
  const handleEdit = item => {
    form.value = {
      id: item.id,
      productId: item.productId || "",
      productName: item.productName || "",
      productModelId: item.productModelId || "",
      model: item.productModel || "",
      operationName: item.operationName || "",
      unitPrice: item.unitPrice ?? "",
      remark: item.remark || "",
    };
    getProductOptions();
    if (item.productModelId) {
      modelList({ id: item.productId })
        .then(res => {
          modelOptions.value = pickList(res).map(m => ({
            name: m.model,
            value: m.id,
          }));
        })
        .catch(() => {});
    }
    showForm.value = true;
  };
  const handleSubmit = () => {
    if (!form.value.productModelId) {
      uni.showToast({ title: "请选择规格型号", icon: "none" });
      return;
    }
    if (!form.value.operationName) {
      uni.showToast({ title: "请输入工序名称", icon: "none" });
      return;
    }
    const unitPrice = Number(form.value.unitPrice);
    if (!(unitPrice > 0)) {
      uni.showToast({ title: "计件单价必须大于0", icon: "none" });
      return;
    }
    submitting.value = true;
    const payload = {
      productModelId: form.value.productModelId,
      productModel: form.value.model,
      operationName: form.value.operationName,
      unitPrice: unitPrice,
      remark: form.value.remark,
    };
    const req = form.value.id
      ? update({ ...payload, id: form.value.id })
      : add(payload);
    req
      .then(() => {
        uni.showToast({ title: form.value.id ? "修改成功" : "新增成功" });
        showForm.value = false;
        handleSearch();
      })
      .catch(() => {})
      .finally(() => {
        submitting.value = false;
      });
  };
  const handleChangeStatus = item => {
    const nextStatus = item.status == 1 ? 0 : 1;
    changeStatus({ id: item.id, status: nextStatus })
      .then(() => {
        uni.showToast({ title: nextStatus == 1 ? "已启用" : "已停用" });
        handleSearch();
      })
      .catch(() => {});
  };
  const handleDelete = item => {
    uni.showModal({
      title: "提示",
      content: "确定要删除该计件单价配置吗?",
      success: res => {
        if (res.confirm) {
          batchDelete([item.id])
            .then(() => {
              uni.showToast({ title: "删除成功" });
              handleSearch();
            })
            .catch(() => {});
        }
      },
    });
  };
  onReachBottom(() => {
    if (pageStatus.value === "nomore") return;
    if (pageStatus.value === "loading") return;
    getList();
  });
  onShow(() => {
    handleSearch();
  });
</script>
<style scoped lang="scss">
  @import "@/styles/procurement-common.scss";
  .filter-row {
    display: flex;
    align-items: center;
    gap: 12px;
    .filter-input {
      flex: 1;
      background: #f5f5f5;
      border-radius: 8px;
      padding: 0 12px;
      height: 40px;
    }
    .filter-status {
      flex: 1;
      height: 40px;
      background: #f5f5f5;
      border-radius: 8px;
      padding: 0 12px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-size: 14px;
      color: #333;
      .placeholder {
        color: #999;
      }
    }
  }
  .filter-actions-top {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 12px;
  }
  .form-popup {
    background: #fff;
    .popup-header {
      padding: 16px;
      border-bottom: 1px solid #f5f5f5;
      text-align: center;
    }
    .popup-title {
      font-size: 16px;
      font-weight: 500;
      color: #333;
    }
    .popup-body {
      padding: 8px 16px;
      max-height: 60vh;
      overflow-y: auto;
    }
    .unit-text {
      margin-left: 8px;
      color: #909399;
      font-size: 12px;
    }
    // é€‰æ‹©ç±»å­—段:readonly çš„ up-input ä¼šæŠŠåŽŸç”Ÿ input ç½®ä¸º disabled,
    // disabled å…ƒç´ ä¼šåžæŽ‰ç‚¹å‡»äº‹ä»¶å¯¼è‡´å¤–层收不到,故让 input ä¸å‚与指针事件,由 .pick-field æŽ¥ç®¡
    .pick-field {
      flex: 1;
      display: flex;
      align-items: center;
      .pick-input {
        flex: 1;
      }
      :deep(input) {
        pointer-events: none;
      }
    }
    .popup-footer {
      display: flex;
      padding: 12px 16px 20px;
    }
  }
  .tree-selector {
    padding: 16px;
    .tree-header {
      text-align: center;
      padding-bottom: 12px;
      border-bottom: 1px solid #f5f5f5;
    }
    .tree-title {
      font-size: 16px;
      font-weight: 500;
      color: #333;
    }
    .tree-content {
      max-height: 60vh;
    }
    .tree-node-header {
      display: flex;
      align-items: center;
      padding: 12px 4px;
      border-bottom: 1px solid #f7f7f7;
      .tree-node-icon {
        margin-right: 8px;
      }
      .tree-node-icon-placeholder {
        width: 16px;
        margin-right: 8px;
      }
      .tree-node-label {
        flex: 1;
        font-size: 14px;
        color: #333;
      }
    }
    .tree-node-children {
      padding-left: 24px;
    }
  }
</style>
src/pages/productionManagement/productionAccounting/index.vue
@@ -124,6 +124,18 @@
              <text class="detail-value">{{ item.process }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工资口径</text>
              <view class="detail-value">
                <up-tag :text="item.wageMode || '计时'"
                        :type="item.wageMode === '计件' ? 'success' : 'info'"
                        size="mini" />
              </view>
            </view>
            <view class="detail-row">
              <text class="detail-label">使用设备</text>
              <text class="detail-value">{{ item.deviceName || "-" }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产数量</text>
              <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
            </view>
src/pages/productionManagement/productionReport/index.vue
@@ -40,6 +40,15 @@
                   @click="openProducerPicker"
                   suffix-icon="arrow-down" />
        </u-form-item>
        <!-- ä½¿ç”¨è®¾å¤‡ -->
        <u-form-item label="使用设备"
                     prop="deviceId">
          <u-input v-model="form.deviceName"
                   placeholder="请选择使用设备(选填)"
                   readonly
                   @click="openDevicePicker"
                   suffix-icon="arrow-down" />
        </u-form-item>
        <!-- å·¥æ—¶ -->
        <u-form-item label="工时"
                     v-if="form.type == 0"
@@ -115,6 +124,12 @@
                     title="选择生产人"
                     @select="onProducerConfirm"
                     @close="showProducerPicker = false" />
    <!-- ä½¿ç”¨è®¾å¤‡é€‰æ‹©å™¨ -->
    <up-action-sheet :show="showDevicePicker"
                     :actions="deviceList"
                     title="选择使用设备"
                     @select="onDeviceConfirm"
                     @close="showDevicePicker = false" />
    <!-- å‚数选择器 -->
    <up-action-sheet :show="showParamSelect"
                     :actions="paramOptions"
@@ -142,6 +157,7 @@
    });
  };
  import { addProductMain } from "@/api/productionManagement/productionReporting";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import { getInfo } from "@/api/login";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import {
@@ -167,10 +183,39 @@
    workHour: 0,
    type: null,
    paramGroups: {},
    deviceId: "",
    deviceName: "",
  });
  const showProducerPicker = ref(false);
  const producerList = ref([]);
  // ä½¿ç”¨è®¾å¤‡é€‰æ‹©
  const showDevicePicker = ref(false);
  const deviceList = ref([]);
  const openDevicePicker = async () => {
    if (deviceList.value.length === 0) {
      try {
        const { data } = await getDeviceLedger();
        const rows = Array.isArray(data) ? data : data?.records || [];
        deviceList.value = rows.map(item => ({
          name: item.deviceName || item.name || "-",
          value: item.id,
        }));
      } catch (error) {
        console.error("加载设备列表失败:", error);
        return;
      }
    }
    showDevicePicker.value = true;
  };
  const onDeviceConfirm = e => {
    form.value.deviceId = e.value;
    form.value.deviceName = e.name;
    showDevicePicker.value = false;
  };
  const params = ref([]);
  const dictOptions = ref({});
@@ -361,6 +406,7 @@
        form.value.productionOrderRoutingOperationId,
      productionOrderId: form.value.productionOrderId,
      workHour: form.value.workHour,
      deviceId: form.value.deviceId || undefined,
      productionOperationParamList: productionOperationParamList,
    };
src/pages/productionManagement/productionReporting/ledger.vue
@@ -19,6 +19,14 @@
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- è®¾å¤‡ç­›é€‰ -->
      <view class="device-filter"
            @click="openDeviceFilter">
        <text :class="{ placeholder: !searchForm.deviceName }">{{ searchForm.deviceName || "全部设备" }}</text>
        <up-icon name="arrow-down"
                 size="14"
                 color="#999"></up-icon>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <scroll-view scroll-y
@@ -47,6 +55,10 @@
              <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>
                <text class="detail-value">{{ item.deviceName || "-" }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">工时(h)</text>
@@ -169,6 +181,12 @@
                  text="暂无参数数据" />
      </view>
    </up-modal>
    <!-- è®¾å¤‡ç­›é€‰ -->
    <up-action-sheet :show="showDeviceSheet"
                     :actions="deviceOptions"
                     title="选择设备"
                     @select="selectDevice"
                     @close="showDeviceSheet = false" />
  </view>
</template>
@@ -181,6 +199,7 @@
    productionProductInputListPage,
  } from "@/api/productionManagement/productionProductMain.js";
  import PageHeader from "@/components/PageHeader.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import modal from "@/plugins/modal";
  const tableData = ref([]);
@@ -195,7 +214,44 @@
  const searchForm = reactive({
    keyword: "",
    deviceId: "",
    deviceName: "",
  });
  // è®¾å¤‡ç­›é€‰
  const showDeviceSheet = ref(false);
  const deviceOptions = ref([
    { name: "全部设备", value: "" },
  ]);
  const loadDevices = async () => {
    if (deviceOptions.value.length > 1) return;
    try {
      const { data } = await getDeviceLedger();
      const rows = Array.isArray(data) ? data : data?.records || [];
      deviceOptions.value = [
        { name: "全部设备", value: "" },
        ...rows.map(item => ({
          name: item.deviceName || item.name || "-",
          value: item.id,
        })),
      ];
    } catch (error) {
      console.error("加载设备列表失败:", error);
    }
  };
  const openDeviceFilter = () => {
    loadDevices();
    showDeviceSheet.value = true;
  };
  const selectDevice = e => {
    searchForm.deviceId = e.value;
    searchForm.deviceName = e.value ? e.name : "";
    showDeviceSheet.value = false;
    handleQuery();
  };
  // æŠ•入详情相关
  const inputVisible = ref(false);
@@ -233,6 +289,7 @@
      current: page.current,
      size: page.size,
      workOrderNo: searchForm.keyword,
      deviceId: searchForm.deviceId || undefined,
    };
    productionProductMainListPage(params)
@@ -321,6 +378,23 @@
    height: 0;
  }
  .device-filter {
    margin-top: 12px;
    height: 40px;
    background: #f5f5f5;
    border-radius: 8px;
    padding: 0 12px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 14px;
    color: #333;
    .placeholder {
      color: #999;
    }
  }
  .ledger-item {
    .item-header {
      .item-tag {
src/pages/productionManagement/productionTraceability/index.vue
@@ -2,510 +2,887 @@
  <view class="production-traceability">
    <PageHeader title="生产追溯"
                @back="goBack" />
    <!-- æ–¹å‘页签 -->
    <view class="trace-tabs">
      <view class="tab-item"
            :class="{ active: direction === 'forward' }"
            @click="switchDirection('forward')">正向追溯</view>
      <view class="tab-item"
            :class="{ active: direction === 'reverse' }"
            @click="switchDirection('reverse')">反向追溯</view>
    </view>
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar"
            @click="openNpsNoSelector">
      <view class="search-bar">
        <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"
          <up-icon name="search"
                   size="18"
                   color="#999"></up-icon>
          <up-input class="search-text"
                    v-model="batchNo"
                    :placeholder="placeholder"
                    border="none"
                    clearable
                    @input="onBatchInput"
                    @focus="onBatchFocus"
                    @clear="onBatchClear" />
        </view>
      </view>
      <!-- æ‰¹å·å€™é€‰ä¸‹æ‹‰ -->
      <view class="option-panel"
            v-if="showOptions">
        <view class="option-item"
              v-for="(opt, index) in batchOptions"
              :key="index"
              @click="onSelectOption(opt)">
          <view class="option-top">
            <text class="option-no">{{ opt.batchNo || '-' }}</text>
            <text class="option-qty">{{ num(opt.qty) }}{{ opt.unit || '' }}</text>
          </view>
          <view class="option-bottom">
            <text class="option-name">{{ text(opt.productName) }} / {{ text(opt.productModel) }}</text>
            <text class="option-time">{{ opt.inTime || '-' }}</text>
          </view>
        </view>
        <view class="option-empty"
              v-if="!optionsLoading && batchOptions.length === 0">无匹配批号,可直接输入后查询</view>
      </view>
      <view class="search-tip">{{ direction === 'forward' ? '输入成品批号,追溯生产全链路' : '输入原料批号,定位受影响的成品范围' }}</view>
      <view class="btn-row">
        <up-button type="primary"
                   size="small"
                   text="查询"
                   @click="handleQuery"></up-button>
      </view>
    </view>
    <!-- å†…容区域 -->
    <view class="content-container"
          v-if="rowData.productionOrderDto">
      <!-- åŸºç¡€ä¿¡æ¯ -->
      <view class="info-card">
        <view class="card-title">基础信息</view>
        <view class="base-info">
          <view class="info-row">
            <text class="label">生产订单号:</text>
            <text class="value">{{ rowData.productionOrderDto?.npsNo || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">产品名称:</text>
            <text class="value">{{ rowData.productionOrderDto?.productName || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">规格型号:</text>
            <text class="value">{{ rowData.productionOrderDto?.model || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">计划数量:</text>
            <text class="value">{{ rowData.productionOrderDto?.quantity || 0 }} {{ rowData.productionOrderDto?.unit }}</text>
          </view>
          <view class="info-row">
            <text class="label">当前状态:</text>
            <view class="value">
              <up-tag :text="getStatusText(rowData.productionOrderDto?.status)"
                      :type="getStatusType(rowData.productionOrderDto?.status)"
    <!-- åŠ è½½ä¸­ -->
    <view v-if="loading"
          class="loading-box">
      <up-loading-icon text="加载中..."></up-loading-icon>
    </view>
    <template v-else>
      <!-- æœªæŸ¥è¯¢æç¤º -->
      <view v-if="!queried"
            class="no-data">
        <up-empty mode="search"
                  :text="direction === 'forward' ? '请输入成品批号查询追溯链路' : '请输入原料批号查询影响范围'"></up-empty>
      </view>
      <!-- ================= æ­£å‘追溯 ================= -->
      <view v-else-if="direction === 'forward'"
            class="result-container">
        <view v-if="forwardEmpty"
              class="no-data">
          <up-empty mode="data"
                    text="未查询到该批号的追溯记录"></up-empty>
        </view>
        <template v-else>
          <view class="result-title">生产订单链路({{ forwardData.chains.length }})</view>
          <view v-for="(chain, ci) in forwardData.chains"
                :key="ci"
                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">{{ text(chain.npsNo) }}</text>
              </view>
              <up-tag :text="orderStatusText(chain.orderStatus)"
                      :type="orderStatusType(chain.orderStatus)"
                      size="mini" />
            </view>
          </view>
          <view class="info-row">
            <text class="label">客户名称:</text>
            <text class="value">{{ rowData.productionOrderDto?.customerName || '-' }}</text>
          </view>
          <view class="info-row">
            <text class="label">开始日期:</text>
            <text class="value">{{ formatDate(rowData.productionOrderDto?.startTime) }}</text>
          </view>
          <view class="info-row">
            <text class="label">完成进度:</text>
            <view class="value progress-box">
              <up-line-progress :percentage="formatProgress(rowData.productionOrderDto?.completionStatus)"
                                :activeColor="progressColor(formatProgress(rowData.productionOrderDto?.completionStatus))"
                                :showText="true" />
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">成品</text>
                <text class="detail-value font-bold">{{ text(chain.finishedProduct) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">单位</text>
                <text class="detail-value">{{ text(chain.unit) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">计划数量</text>
                <text class="detail-value">{{ num(chain.planQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">完工数量</text>
                <text class="detail-value highlight">{{ num(chain.completeQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">订单创建时间</text>
                <text class="detail-value">{{ text(chain.orderCreateTime) }}</text>
              </view>
            </view>
            <!-- å‘½ä¸­çš„入库批次 -->
            <view class="chip-wrap"
                  v-if="chain.batches && chain.batches.length">
              <text class="chip"
                    v-for="(b, bi) in chain.batches"
                    :key="bi">{{ text(b.batchNo) }} Â· {{ recordTypeText(b.recordType) }} Â· {{ num(b.qty) }}{{ chain.unit || '' }}</text>
            </view>
            <!-- æŠ¥å·¥å·¥åº -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(ci, 'reports')">
                <text class="collapse-title">报工工序</text>
                <text class="collapse-count">{{ (chain.reports || []).length }}</text>
                <up-icon :name="isSectionOpen(ci, 'reports') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(ci, 'reports')">
                <view class="sub-item"
                      v-for="(r, ri) in chain.reports"
                      :key="ri">
                  <view class="detail-row">
                    <text class="detail-label">工单号</text>
                    <text class="detail-value font-bold">{{ text(r.workOrderNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">工序</text>
                    <text class="detail-value">{{ text(r.operationName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报工人</text>
                    <text class="detail-value">{{ text(r.userName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">设备</text>
                    <text class="detail-value">{{ text(r.deviceName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报工单号</text>
                    <text class="detail-value">{{ text(r.productNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">产出数量</text>
                    <text class="detail-value">{{ num(r.outputQty) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报废数量</text>
                    <text class="detail-value danger">{{ num(r.scrapQty) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报工时间</text>
                    <text class="detail-value">{{ text(r.reportTime) }}</text>
                  </view>
                </view>
                <view class="sub-empty"
                      v-if="!(chain.reports || []).length">暂无报工记录</view>
              </view>
            </view>
            <!-- æŠ•入物料 -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(ci, 'inputs')">
                <text class="collapse-title">投入物料</text>
                <text class="collapse-count">{{ (chain.inputs || []).length }}</text>
                <up-icon :name="isSectionOpen(ci, 'inputs') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(ci, 'inputs')">
                <view class="sub-item"
                      v-for="(m, mi) in chain.inputs"
                      :key="mi">
                  <view class="detail-row">
                    <text class="detail-label">物料</text>
                    <text class="detail-value font-bold">{{ text(m.materialName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">规格型号</text>
                    <text class="detail-value">{{ text(m.materialModel) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">投入数量</text>
                    <text class="detail-value">{{ num(m.inputQty) }} {{ text(m.unit) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">工序</text>
                    <text class="detail-value">{{ text(m.operationName) }}</text>
                  </view>
                </view>
                <view class="sub-empty"
                      v-if="!(chain.inputs || []).length">暂无投入物料</view>
              </view>
            </view>
            <!-- é¢†æ–™æ‰¹æ¬¡ -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(ci, 'picks')">
                <text class="collapse-title">领料批次</text>
                <text class="collapse-count">{{ (chain.picks || []).length }}</text>
                <up-icon :name="isSectionOpen(ci, 'picks') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(ci, 'picks')">
                <view class="sub-item"
                      v-for="(p, pi) in chain.picks"
                      :key="pi">
                  <view class="detail-row">
                    <text class="detail-label">原料批号</text>
                    <text class="detail-value link"
                          @click="jumpToReverse(p.batchNo)">{{ text(p.batchNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">物料</text>
                    <text class="detail-value">{{ text(p.materialName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">规格型号</text>
                    <text class="detail-value">{{ text(p.materialModel) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">领用数量</text>
                    <text class="detail-value">{{ num(p.quantity) }} {{ text(p.unit) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">工序</text>
                    <text class="detail-value">{{ text(p.operationName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">领料时间</text>
                    <text class="detail-value">{{ text(p.pickTime) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">供应商</text>
                    <text class="detail-value">{{ text(p.supplierName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">采购合同号</text>
                    <text class="detail-value">{{ text(p.contractNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">原料检验结论</text>
                    <text class="detail-value">{{ text(p.checkResult) }}</text>
                  </view>
                </view>
                <view class="sub-empty"
                      v-if="!(chain.picks || []).length">暂无领料记录</view>
              </view>
            </view>
            <!-- æ£€éªŒå• -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(ci, 'inspects')">
                <text class="collapse-title">检验单</text>
                <text class="collapse-count">{{ (chain.inspects || []).length }}</text>
                <up-icon :name="isSectionOpen(ci, 'inspects') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(ci, 'inspects')">
                <view class="sub-item"
                      v-for="(ins, ii) in chain.inspects"
                      :key="ii">
                  <view class="detail-row">
                    <text class="detail-label">检验类型</text>
                    <text class="detail-value font-bold">{{ text(ins.inspectTypeName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">检验员</text>
                    <text class="detail-value">{{ text(ins.checkName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">数量</text>
                    <text class="detail-value">{{ num(ins.quantity) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">合格数量</text>
                    <text class="detail-value">{{ num(ins.qualifiedQuantity) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">不合格数量</text>
                    <text class="detail-value danger">{{ num(ins.unqualifiedQuantity) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">检验结论</text>
                    <text class="detail-value">{{ text(ins.checkResult) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">检验时间</text>
                    <text class="detail-value">{{ text(ins.checkTime) }}</text>
                  </view>
                  <!-- ä¸åˆæ ¼å¤„理 -->
                  <view class="unqualified-box"
                        v-if="(ins.unqualifiedList || []).length">
                    <view class="unqualified-title">不合格处理</view>
                    <view class="inner-item"
                          v-for="(u, ui) in ins.unqualifiedList"
                          :key="ui">
                      <view class="detail-row">
                        <text class="detail-label">缺陷现象</text>
                        <text class="detail-value danger">{{ text(u.defectivePhenomena) }}</text>
                      </view>
                      <view class="detail-row">
                        <text class="detail-label">数量</text>
                        <text class="detail-value">{{ num(u.quantity) }}</text>
                      </view>
                      <view class="detail-row">
                        <text class="detail-label">处理结果</text>
                        <text class="detail-value">{{ text(u.dealResult) }}</text>
                      </view>
                      <view class="detail-row">
                        <text class="detail-label">处理人</text>
                        <text class="detail-value">{{ text(u.dealName) }}</text>
                      </view>
                      <view class="detail-row">
                        <text class="detail-label">处理时间</text>
                        <text class="detail-value">{{ text(u.dealTime) }}</text>
                      </view>
                    </view>
                  </view>
                </view>
                <view class="sub-empty"
                      v-if="!(chain.inspects || []).length">暂无检验记录</view>
              </view>
            </view>
          </view>
        </view>
          <!-- å‡ºåº“流向 -->
          <view class="result-title">出库流向({{ forwardData.outbounds.length }})</view>
          <view v-for="(o, oi) in forwardData.outbounds"
                :key="oi"
                class="ledger-item">
            <view class="item-header">
              <view class="item-left">
                <view class="document-icon out">
                  <up-icon name="arrow-upward"
                           size="16"
                           color="#ffffff"></up-icon>
                </view>
                <text class="item-id">{{ text(o.batchNo) }}</text>
              </view>
              <text class="type-tag">{{ recordTypeText(o.recordType) }}</text>
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">产品名称</text>
                <text class="detail-value font-bold">{{ text(o.productName) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">规格型号</text>
                <text class="detail-value">{{ text(o.productModel) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">出库数量</text>
                <text class="detail-value highlight">{{ num(o.qty) }} {{ text(o.unit) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">出库时间</text>
                <text class="detail-value">{{ text(o.outTime) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">备注</text>
                <text class="detail-value">{{ text(o.remark) }}</text>
              </view>
            </view>
          </view>
          <view class="sub-empty card"
                v-if="!forwardData.outbounds.length">暂无出库流向记录</view>
        </template>
      </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.operationName || '-' }}</text>
            </view>
            <view class="content-row">
              <text class="label">需求/完成:</text>
              <text class="value">{{ item.workOrder.planQuantity }} / {{ item.workOrder.completeQuantity }}</text>
            </view>
            <view class="content-row">
              <text class="label">报废数量:</text>
              <text class="value error-text">{{ item.workOrder.scrapQty || 0 }}</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>
            class="result-container">
        <view v-if="reverseEmpty"
              class="no-data">
          <up-empty mode="data"
                    text="未查询到该批号的追溯记录"></up-empty>
        </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.quantity || 0 }}</text></view>
              <view class="item-row"><text class="label">报废数量:</text><text class="value error-text">{{ report.scrapQty || 0 }}</text></view>
              <view class="item-row"><text class="label">工时(h):</text><text class="value">{{ report.workHour || 0 }}</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="quality-record">
              <view class="record-title">投入记录 {{ idx + 1 }}</view>
              <view class="info-grid">
                <view class="info-item"><text class="label">报工单号</text><text class="value">{{ item.productNo || '-' }}</text></view>
                <view class="info-item"><text class="label">投入数量</text><text class="value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text></view>
                <view class="info-item full-width"><text class="label">投入产品名称</text><text class="value">{{ item.productName || '-' }}</text></view>
                <view class="info-item full-width"><text class="label">投入产品型号</text><text class="value">{{ item.model || '-' }}</text></view>
        <template v-else>
          <!-- åŽŸæ–™æ¥æº -->
          <view class="result-title">原料来源</view>
          <view class="ledger-item"
                v-if="reverseData.material">
            <view class="item-header">
              <view class="item-left">
                <view class="document-icon material">
                  <up-icon name="cube"
                           size="16"
                           color="#ffffff"></up-icon>
                </view>
                <text class="item-id">{{ text(reverseData.material.materialName) }}</text>
              </view>
              <text class="type-tag">{{ recordTypeText(reverseData.material.recordType) }}</text>
            </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 class="info-item"><text class="label">报工单号</text><text class="value">{{ record.reportNo || '-' }}</text></view>
              <view class="info-item"><text class="label">产品名称</text><text class="value">{{ record.productName || '-' }}</text></view>
              <view class="info-item"><text class="label">规格型号</text><text class="value">{{ record.model || '-' }}</text></view>
              <view class="info-item"><text class="label">检测单位</text><text class="value">{{ record.checkCompany || '-' }}</text></view>
            </view>
            <view class="params-table">
              <view class="table-header">
                <text class="col">指标</text>
                <text class="col">单位</text>
                <text class="col">标准值</text>
                <text class="col">内控值</text>
                <text class="col">实际值</text>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">原料批号</text>
                <text class="detail-value font-bold">{{ text(reverseData.material.batchNo) }}</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.unit || '-' }}</text>
                <text class="col">{{ param.standardValue }}</text>
                <text class="col">{{ param.controlValue || '-' }}</text>
                <text class="col"
                      :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text>
              <view class="detail-row">
                <text class="detail-label">规格型号</text>
                <text class="detail-value">{{ text(reverseData.material.materialModel) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">单位</text>
                <text class="detail-value">{{ text(reverseData.material.unit) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">入库数量</text>
                <text class="detail-value">{{ num(reverseData.material.qty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">入库时间</text>
                <text class="detail-value">{{ text(reverseData.material.inTime) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">检验结论</text>
                <text class="detail-value">{{ text(reverseData.material.checkResult) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">检验数量</text>
                <text class="detail-value">{{ num(reverseData.material.inspectQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">合格数量</text>
                <text class="detail-value">{{ num(reverseData.material.qualifiedQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">不合格数量</text>
                <text class="detail-value danger">{{ num(reverseData.material.unqualifiedQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">检验时间</text>
                <text class="detail-value">{{ text(reverseData.material.checkTime) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">供应商</text>
                <text class="detail-value">{{ text(reverseData.material.supplierName) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">采购合同号</text>
                <text class="detail-value">{{ text(reverseData.material.contractNo) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">采购日期</text>
                <text class="detail-value">{{ text(reverseData.material.entryDate) }}</text>
              </view>
            </view>
          </view>
          <view v-if="!qualityRecords || qualityRecords.length === 0"
                class="no-data-minor">暂无质检记录</view>
        </scroll-view>
          <view class="ledger-item"
                v-else>
            <view class="sub-empty">该批号无入库记录</view>
          </view>
          <!-- å—影响的生产订单 -->
          <view class="result-title">受影响生产订单({{ reverseData.orders.length }})</view>
          <view v-for="(order, oi) in reverseData.orders"
                :key="oi"
                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">{{ text(order.npsNo) }}</text>
              </view>
              <up-tag :text="orderStatusText(order.orderStatus)"
                      :type="orderStatusType(order.orderStatus)"
                      size="mini" />
            </view>
            <up-divider></up-divider>
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">成品</text>
                <text class="detail-value font-bold">{{ text(order.finishedProduct) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">计划数量</text>
                <text class="detail-value">{{ num(order.planQty) }} {{ text(order.unit) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">完工数量</text>
                <text class="detail-value">{{ num(order.completeQty) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">订单创建时间</text>
                <text class="detail-value">{{ text(order.orderCreateTime) }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">本订单领用数量</text>
                <text class="detail-value highlight">{{ num(order.pickedQty) }}</text>
              </view>
            </view>
            <!-- é¢†ç”¨æ˜Žç»† -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(oi, 'rpicks')">
                <text class="collapse-title">领用明细</text>
                <text class="collapse-count">{{ (order.picks || []).length }}</text>
                <up-icon :name="isSectionOpen(oi, 'rpicks') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(oi, 'rpicks')">
                <view class="sub-item"
                      v-for="(p, pi) in order.picks"
                      :key="pi">
                  <view class="detail-row">
                    <text class="detail-label">批号</text>
                    <text class="detail-value font-bold">{{ text(p.batchNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">物料</text>
                    <text class="detail-value">{{ text(p.materialName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">规格型号</text>
                    <text class="detail-value">{{ text(p.materialModel) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">领用数量</text>
                    <text class="detail-value">{{ num(p.quantity) }} {{ text(p.unit) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">工序</text>
                    <text class="detail-value">{{ text(p.operationName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">领料时间</text>
                    <text class="detail-value">{{ text(p.pickTime) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">供应商</text>
                    <text class="detail-value">{{ text(p.supplierName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">采购合同号</text>
                    <text class="detail-value">{{ text(p.contractNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">检验结论</text>
                    <text class="detail-value">{{ text(p.checkResult) }}</text>
                  </view>
                </view>
                <view class="sub-empty"
                      v-if="!(order.picks || []).length">暂无领用记录</view>
              </view>
            </view>
            <!-- æŠ¥å·¥å·¥åº -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(oi, 'rreports')">
                <text class="collapse-title">报工工序</text>
                <text class="collapse-count">{{ (order.reports || []).length }}</text>
                <up-icon :name="isSectionOpen(oi, 'rreports') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(oi, 'rreports')">
                <view class="sub-item"
                      v-for="(r, ri) in order.reports"
                      :key="ri">
                  <view class="detail-row">
                    <text class="detail-label">工单号</text>
                    <text class="detail-value font-bold">{{ text(r.workOrderNo) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">工序</text>
                    <text class="detail-value">{{ text(r.operationName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报工人</text>
                    <text class="detail-value">{{ text(r.userName) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">产出数量</text>
                    <text class="detail-value">{{ num(r.outputQty) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报废数量</text>
                    <text class="detail-value danger">{{ num(r.scrapQty) }}</text>
                  </view>
                  <view class="detail-row">
                    <text class="detail-label">报工时间</text>
                    <text class="detail-value">{{ text(r.reportTime) }}</text>
                  </view>
                </view>
                <view class="sub-empty"
                      v-if="!(order.reports || []).length">暂无报工记录</view>
              </view>
            </view>
            <!-- äº§å‡ºæˆå“æ‰¹å· -->
            <view class="collapse-block">
              <view class="collapse-head"
                    @click="toggleSection(oi, 'batches')">
                <text class="collapse-title">产出成品批号</text>
                <text class="collapse-count">{{ (order.finishedBatches || []).length }}</text>
                <up-icon :name="isSectionOpen(oi, 'batches') ? 'arrow-up' : 'arrow-down'"
                         size="14"
                         color="#999"></up-icon>
              </view>
              <view class="collapse-body"
                    v-show="isSectionOpen(oi, 'batches')">
                <view class="chip-wrap">
                  <text class="chip link-chip"
                        v-for="(b, bi) in order.finishedBatches"
                        :key="bi"
                        @click="jumpToForward(b.batchNo)">{{ text(b.batchNo) }} Â· {{ num(b.qty) }}{{ order.unit || '' }}</text>
                </view>
                <view class="sub-empty"
                      v-if="!(order.finishedBatches || []).length">暂无成品批次</view>
              </view>
            </view>
          </view>
          <view class="sub-empty card"
                v-if="!reverseData.orders.length">该批号暂无生产订单领用记录</view>
        </template>
      </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>
    </template>
  </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";
  import {
    traceForward,
    traceReverse,
    traceBatchOptions,
  } from "@/api/qualityManagement/productionTrace.js";
  // é€‰æ‹©å™¨ç›¸å…³
  const showNpsNoSelector = ref(false);
  const npsNoQuery = ref("");
  const npsNoOptions = ref([]);
  const npsNoLoading = ref(false);
  const selectedNpsNo = ref(null);
  const selectedNpsNoLabel = ref("");
  // å½“前方向:forward æ­£å‘ / reverse åå‘
  const direction = ref("forward");
  const batchNo = ref("");
  const loading = ref(false);
  // æ˜¯å¦å·²æ‰§è¡Œè¿‡æŸ¥è¯¢
  const queried = ref(false);
  const rowData = reactive({
    productionOrderDto: null,
    productionRecords: [],
  });
  // æ‰¹å·å€™é€‰ä¸‹æ‹‰
  const batchOptions = ref([]);
  const showOptions = ref(false);
  const optionsLoading = ref(false);
  let searchTimer = null;
  // æŠ¥å·¥è¯¦æƒ…
  const reportPopupVisible = ref(false);
  const detailData = ref({ workOrder: {}, reports: [] });
  const forwardData = reactive({ batchNo: "", chains: [], outbounds: [] });
  const reverseData = reactive({ batchNo: "", material: null, orders: [] });
  // æŠ˜å é¢æ¿å±•开状态
  const openMap = reactive({});
  // æŠ•入详情
  const inputPopupVisible = ref(false);
  const inputListData = ref([]);
  const inputLoading = ref(false);
  const placeholder = computed(() =>
    direction.value === "forward" ? "请输入或选择成品批号" : "请输入或选择原料批号"
  );
  // è´¨æ£€è¯¦æƒ…
  const qualityPopupVisible = ref(false);
  const qualityRecords = ref([]);
  const forwardEmpty = computed(
    () =>
      forwardData.chains.length === 0 && forwardData.outbounds.length === 0
  );
  const reverseEmpty = computed(
    () => !reverseData.material && reverseData.orders.length === 0
  );
  // å‚数详情
  const paramModalVisible = ref(false);
  const currentParams = ref([]);
  // ç”Ÿäº§è®¢å•状态字典(与生产订单列表页保持一致)
  const ORDER_STATUS_TEXT = {
    1: "待开始",
    2: "进行中",
    3: "已完成",
    4: "已取消",
    5: "已结束",
  };
  const ORDER_STATUS_TYPE = {
    1: "primary",
    2: "warning",
    3: "success",
    4: "info",
    5: "error",
  };
  // å…¥åº“/出库类型字典
  const RECORD_TYPE_TEXT = {
    0: "自定义入库",
    1: "自定义出库",
    2: "生产报工入库",
    3: "生产报工出库",
    6: "质检合格入库",
    7: "采购入库",
    8: "销售出库",
    10: "采购入库",
    13: "销售发货出库",
  };
  const num = val => {
    const n = Number(val);
    return Number.isFinite(n) ? n : 0;
  };
  const text = val =>
    val === null || val === undefined || val === "" ? "-" : val;
  const orderStatusText = status =>
    ORDER_STATUS_TEXT[status] ?? text(status);
  const orderStatusType = status => ORDER_STATUS_TYPE[status] || "info";
  const recordTypeText = type => {
    if (type === null || type === undefined || type === "") return "-";
    return RECORD_TYPE_TEXT[String(type)] || `类型${type}`;
  };
  const goBack = () => {
    uni.navigateBack();
  };
  const openNpsNoSelector = () => {
    showNpsNoSelector.value = true;
    if (npsNoOptions.value.length === 0) {
      handleNpsNoSearch();
    }
  // æŠ˜å é¢æ¿
  const isSectionOpen = (key, name) => !!openMap[key + "-" + name];
  const toggleSection = (key, name) => {
    const k = key + "-" + name;
    openMap[k] = !openMap[k];
  };
  const handleNpsNoSearch = async () => {
    npsNoLoading.value = true;
    try {
      const res = await productOrderListPage({
        npsNo: npsNoQuery.value || "",
        pageNum: 1,
        pageSize: 50,
  // åŠ è½½æ‰¹å·å€™é€‰
  const loadBatchOptions = () => {
    optionsLoading.value = true;
    traceBatchOptions({
      type: direction.value === "forward" ? "finished" : "material",
      keyword: batchNo.value || "",
      limit: 50,
    })
      .then(res => {
        batchOptions.value = res.data || [];
      })
      .catch(() => {
        batchOptions.value = [];
      })
      .finally(() => {
        optionsLoading.value = false;
      });
      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();
    }
  const onBatchInput = () => {
    showOptions.value = true;
    if (searchTimer) clearTimeout(searchTimer);
    searchTimer = setTimeout(() => {
      loadBatchOptions();
    }, 300);
  };
  onLoad(async options => {
    if (options.npsNo) {
      uni.showLoading({ title: "加载中..." });
      try {
        const res = await productOrderListPage({
          npsNo: options.npsNo,
          pageNum: 1,
          pageSize: 10,
  const onBatchFocus = () => {
    showOptions.value = true;
    loadBatchOptions();
  };
  const onBatchClear = () => {
    batchNo.value = "";
    batchOptions.value = [];
    showOptions.value = false;
  };
  const onSelectOption = opt => {
    batchNo.value = opt.batchNo || "";
    showOptions.value = false;
    handleQuery();
  };
  // åˆ‡æ¢æ–¹å‘
  const switchDirection = dir => {
    if (direction.value === dir) return;
    direction.value = dir;
    batchNo.value = "";
    batchOptions.value = [];
    showOptions.value = false;
    queried.value = false;
    forwardData.chains = [];
    forwardData.outbounds = [];
    reverseData.material = null;
    reverseData.orders = [];
  };
  // æŸ¥è¯¢è¿½æº¯æ•°æ®
  const handleQuery = () => {
    const no = (batchNo.value || "").trim();
    if (!no) {
      uni.showToast({ title: "请输入批号", icon: "none" });
      return;
    }
    showOptions.value = false;
    loading.value = true;
    if (direction.value === "forward") {
      traceForward({ batchNo: no })
        .then(res => {
          const data = res.data || {};
          forwardData.batchNo = data.batchNo || no;
          forwardData.chains = data.chains || [];
          forwardData.outbounds = data.outbounds || [];
          queried.value = true;
        })
        .catch(() => {
          // é”™è¯¯æ–‡æ¡ˆç”± request ç»Ÿä¸€æç¤º
        })
        .finally(() => {
          loading.value = false;
        });
        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();
    } else {
      traceReverse({ batchNo: no })
        .then(res => {
          const data = res.data || {};
          reverseData.batchNo = data.batchNo || no;
          reverseData.material = data.material || null;
          reverseData.orders = data.orders || [];
          queried.value = true;
        })
        .catch(() => {
          // é”™è¯¯æ–‡æ¡ˆç”± request ç»Ÿä¸€æç¤º
        })
        .finally(() => {
          loading.value = false;
        });
    }
  };
  // è·³è½¬åå‘追溯并回填原料批号
  const jumpToReverse = no => {
    if (!no) return;
    direction.value = "reverse";
    batchNo.value = no;
    batchOptions.value = [];
    showOptions.value = false;
    queried.value = false;
    forwardData.chains = [];
    forwardData.outbounds = [];
    reverseData.material = null;
    reverseData.orders = [];
    uni.pageScrollTo({ scrollTop: 0, duration: 0 });
    handleQuery();
  };
  // è·³è½¬æ­£å‘追溯并回填成品批号
  const jumpToForward = no => {
    if (!no) return;
    direction.value = "forward";
    batchNo.value = no;
    batchOptions.value = [];
    showOptions.value = false;
    queried.value = false;
    forwardData.chains = [];
    forwardData.outbounds = [];
    reverseData.material = null;
    reverseData.orders = [];
    uni.pageScrollTo({ scrollTop: 0, duration: 0 });
    handleQuery();
  };
  onLoad(options => {
    // æ”¯æŒä»Žå…¶å®ƒé¡µé¢å¸¦æ‰¹å·è¿›å…¥
    if (options && options.batchNo) {
      try {
        batchNo.value = decodeURIComponent(options.batchNo);
      } catch (e) {
        batchNo.value = options.batchNo;
      }
      direction.value = options.type === "material" ? "reverse" : "forward";
      handleQuery();
    }
  });
  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,
        ...(r.reportOutputList ? r.reportOutputList[0] : {}),
        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,
      productName: row.workOrder?.productName || "-",
      model: row.workOrder?.model || "-",
      userName: i.reportMain?.userName || "-",
      inspectParamList: i.inspectParamList || [],
    }));
    qualityPopupVisible.value = true;
  };
  const showParams = params => {
    currentParams.value = params || [];
    paramModalVisible.value = true;
  };
</script>
<style scoped lang="scss">
@@ -513,520 +890,285 @@
  .production-traceability {
    min-height: 100vh;
    background-color: #f5f7fa;
    background: #f8f9fa;
    padding-bottom: 40rpx;
  }
  // æ–¹å‘页签
  .trace-tabs {
    display: flex;
    background: #ffffff;
    .tab-item {
      flex: 1;
      text-align: center;
      padding: 24rpx 0;
      font-size: 28rpx;
      color: #666;
      position: relative;
      &.active {
        color: #2979ff;
        font-weight: 600;
        &::after {
          content: "";
          position: absolute;
          left: 50%;
          bottom: 0;
          transform: translateX(-50%);
          width: 80rpx;
          height: 6rpx;
          background: #2979ff;
          border-radius: 4rpx;
        }
      }
    }
  }
  .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;
    position: relative;
    border-bottom: 1rpx solid #f0f0f0;
    .search-input {
      flex: 1;
      display: flex;
      align-items: center;
      .placeholder {
        font-size: 28rpx;
        color: #999;
      }
      .value {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
      }
      height: 76rpx;
    }
    .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-text {
      font-size: 28rpx;
    }
    .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;
    .search-tip {
      font-size: 22rpx;
      color: #999;
      font-size: 26rpx;
      margin-top: 12rpx;
    }
  }
  .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;
    .btn-row {
      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;
    }
  }
  .base-info {
  // æ‰¹å·å€™é€‰ä¸‹æ‹‰
  .option-panel {
    margin-top: 12rpx;
    background: #fff;
    padding: 24rpx;
    border-radius: 16rpx;
    margin-bottom: 30rpx;
    border: 1rpx solid #ebeef5;
    border-radius: 12rpx;
    max-height: 520rpx;
    overflow-y: auto;
    .info-row {
      margin-bottom: 16rpx;
      font-size: 28rpx;
      display: flex;
      align-items: center;
    .option-item {
      padding: 18rpx 20rpx;
      border-bottom: 1rpx solid #f5f5f5;
      &:last-child {
        margin-bottom: 0;
        border-bottom: none;
      }
      .label {
        color: #999;
        min-width: 180rpx;
      &:active {
        background: #f5f7fa;
      }
      .value {
        color: #333;
        flex: 1;
        font-weight: 500;
        &.progress-box {
      .option-top {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .option-no {
          font-size: 28rpx;
          color: #333;
          font-weight: 600;
          flex: 1;
          margin-right: 16rpx;
        }
        .option-qty {
          font-size: 24rpx;
          color: #2979ff;
        }
      }
      .option-bottom {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-top: 8rpx;
        .option-name {
          font-size: 24rpx;
          color: #666;
          flex: 1;
          margin-right: 16rpx;
        }
        .option-time {
          font-size: 22rpx;
          color: #999;
        }
      }
    }
    .option-empty {
      padding: 30rpx 20rpx;
      text-align: center;
      font-size: 24rpx;
      color: #999;
    }
  }
  .info-grid {
  .loading-box {
    display: flex;
    justify-content: center;
    padding-top: 120rpx;
  }
  .result-container {
    padding: 20rpx;
  }
  .result-title {
    font-size: 28rpx;
    font-weight: 600;
    color: #333;
    margin: 10rpx 0 16rpx;
    &::before {
      content: "";
      display: inline-block;
      width: 6rpx;
      height: 26rpx;
      background: #2979ff;
      border-radius: 4rpx;
      margin-right: 12rpx;
      vertical-align: middle;
    }
  }
  .type-tag {
    font-size: 22rpx;
    color: #2979ff;
    background: #ecf5ff;
    border-radius: 6rpx;
    padding: 4rpx 12rpx;
  }
  .document-icon {
    &.out {
      background: #ff9900;
    }
    &.material {
      background: #52c41a;
    }
  }
  .font-bold {
    font-weight: 500;
  }
  .detail-value.link,
  .chip.link-chip {
    color: #2979ff;
    text-decoration: underline;
  }
  // æ‰¹æ¬¡æ ‡ç­¾
  .chip-wrap {
    display: flex;
    flex-wrap: wrap;
    padding: 10rpx 0;
    gap: 12rpx;
    padding-bottom: 12rpx;
    .info-item {
      width: 50%;
      margin-bottom: 20rpx;
    .chip {
      font-size: 22rpx;
      color: #333;
      background: #f5f7fa;
      border-radius: 8rpx;
      padding: 8rpx 16rpx;
    }
  }
  // æŠ˜å é¢æ¿
  .collapse-block {
    border-top: 1rpx solid #f0f0f0;
    .collapse-head {
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 24rpx 0;
      &.full-width {
        width: 100%;
      }
      .label {
        font-size: 24rpx;
        color: #999;
        margin-bottom: 4rpx;
      }
      .value {
      .collapse-title {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
      }
    }
  }
  .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;
      .collapse-count {
        font-size: 22rpx;
        color: #2979ff;
        background: #ecf5ff;
        border-radius: 20rpx;
        padding: 2rpx 14rpx;
        margin: 0 12rpx 0 auto;
      }
    }
    .popup-scroll {
      flex: 1;
      overflow: hidden;
    .collapse-body {
      padding-bottom: 16rpx;
    }
  }
  .detail-info {
  .sub-item {
    background: #f8f9fa;
    padding: 24rpx;
    border-radius: 16rpx;
    margin-bottom: 30rpx;
    flex-direction: column;
    border-radius: 10rpx;
    padding: 16rpx;
    margin-bottom: 12rpx;
    .info-row {
    &:last-child {
      margin-bottom: 0;
    }
  }
  .inner-item {
    padding: 12rpx;
    margin-top: 10rpx;
    background: #fff5f5;
    border-radius: 8rpx;
    &:first-child {
      margin-top: 0;
    }
  }
  .unqualified-box {
    margin-top: 16rpx;
    padding-top: 16rpx;
    border-top: 1rpx dashed #e4e7ed;
    .unqualified-title {
      font-size: 24rpx;
      color: #ee0a24;
      font-weight: 500;
      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;
  .sub-empty {
    font-size: 24rpx;
    color: #999;
    font-size: 28rpx;
    text-align: center;
    padding: 24rpx 0;
    &.card {
      background: #fff;
      border-radius: 12rpx;
      margin-bottom: 16rpx;
    }
  }
  .no-data {
    padding-top: 100rpx;
  }
</style>
src/pages/qualityManagement/finalInspection/add.vue
@@ -29,10 +29,10 @@
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
                  @click="!isReadonly && (showProductTree = true)"
                  :disabled="isEdit || isReadonly" />
        <template #right>
          <up-icon @click="showProductTree = true"
          <up-icon @click="!isReadonly && (showProductTree = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -43,9 +43,9 @@
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
                  :disabled="isEdit || isReadonly" />
        <template #right>
          <up-icon @click="showModelSheet = true"
          <up-icon @click="!isReadonly && (showModelSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -55,7 +55,8 @@
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
                  readonly
                  :disabled="isReadonly" />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
@@ -75,13 +76,14 @@
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="processQuantityDisabled" />
                  :disabled="processQuantityDisabled || isReadonly" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  :disabled="isReadonly"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
@@ -91,9 +93,10 @@
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  :disabled="isReadonly"
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
          <up-icon @click="!isReadonly && (showResultSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -103,9 +106,10 @@
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  :disabled="isReadonly"
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
          <up-icon @click="!isReadonly && (showInspectorSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -115,7 +119,8 @@
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
                  readonly
                  :disabled="isReadonly" />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
@@ -155,6 +160,7 @@
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          :disabled="isReadonly"
                          clearable
                          border-bottom
                          class="step-input" />
@@ -174,14 +180,16 @@
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
        {{ isReadonly ? '返回' : '取消' }}
      </up-button>
      <up-button type="primary"
      <!-- å·²æäº¤å•据只读,隐藏保存/提交 -->
      <up-button v-if="!isReadonly"
                 type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
        ä¿å­˜
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
@@ -465,6 +473,9 @@
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // å·²æäº¤ï¼ˆinspectState=1)单据整体只读
  const isReadonly = computed(() => Number(form.value?.inspectState) === 1);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
@@ -472,11 +483,13 @@
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    if (isReadonly.value) return;
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    if (isReadonly.value) return;
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
@@ -597,7 +610,7 @@
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    if (isReadonly.value) return;
    showTestStandardSheet.value = true;
  };
@@ -674,6 +687,11 @@
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    // å·²æäº¤å•据禁止保存/提交
    if (isReadonly.value) {
      showToast("检验单已提交,不允许修改");
      return;
    }
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
@@ -713,21 +731,22 @@
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        // æ–°å¢žä»…保存草稿,正式提交在列表页「提交」按钮(会生成入库记录)
        await qualityInspectAdd(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("提交失败:", error);
    } finally {
      loading.value = false;
    }
src/pages/qualityManagement/finalInspection/detail.vue
@@ -24,7 +24,7 @@
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
              {{ detailData.inspectState ? '已提交' : '待提交' }}
            </u-tag>
          </view>
        </view>
@@ -66,6 +66,18 @@
            <text class="detail-label">检测单位</text>
            <text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合格数量</text>
            <text class="detail-value">{{ detailData.qualifiedQuantity != null ? detailData.qualifiedQuantity : '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">不合格数量</text>
            <text class="detail-value">{{ detailData.unqualifiedQuantity != null ? detailData.unqualifiedQuantity : '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检验结论</text>
            <text class="detail-value">{{ detailData.checkResult || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">检验标准</text>
            <text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
src/pages/qualityManagement/finalInspection/index.vue
@@ -26,6 +26,17 @@
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ£€éªŒçŠ¶æ€ç­›é€‰ -->
      <view class="filter-row">
        <view class="filter-item"
              @click="showStateSheet = true">
          <text class="filter-label">检验状态</text>
          <text class="filter-value">{{ stateFilterLabel }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ—¥æœŸé€‰æ‹© -->
      <!-- <view class="date-range"
            v-if="searchForm.entryDate">
@@ -85,7 +96,7 @@
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
                     class="status-tag">
                {{ item.inspectState ? '已提交' : '未提交' }}
                {{ item.inspectState ? '已提交' : '待提交' }}
              </u-tag>
            </view>
          </view>
@@ -114,6 +125,18 @@
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">合格数量</text>
              <text class="detail-value">{{ item.qualifiedQuantity != null ? item.qualifiedQuantity : '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">不合格数量</text>
              <text class="detail-value">{{ item.unqualifiedQuantity != null ? item.unqualifiedQuantity : '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检验结论</text>
              <text class="detail-value">{{ item.checkResult || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
@@ -152,6 +175,15 @@
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
          </view>
          <view class="action-buttons">
            <u-button type="error"
                      size="small"
                      class="action-btn"
                      :disabled="!(Number(item.unqualifiedQuantity) > 0)"
                      @click.stop="openUnqualified(item)">
              ä¸åˆæ ¼å¤„理
            </u-button>
          </view>
        </view>
@@ -223,6 +255,56 @@
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
    <!-- æ£€éªŒçŠ¶æ€ç­›é€‰ -->
    <up-action-sheet :show="showStateSheet"
                     :actions="stateSheetOptions"
                     @select="selectState"
                     @close="showStateSheet = false"
                     title="选择检验状态" />
    <!-- ä¸åˆæ ¼å¤„理弹窗 -->
    <up-popup v-model:show="showUnqualifiedPopup"
              mode="bottom"
              round>
      <view class="unqualified-dialog">
        <view class="dialog-header">
          <text class="dialog-title">不合格处理</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showUnqualifiedPopup = false"></up-icon>
        </view>
        <view class="dialog-content">
          <view v-if="unqualifiedLoading"
                class="unqualified-empty">加载中...</view>
          <view v-else-if="unqualifiedList.length === 0"
                class="unqualified-empty">暂无待处理不合格记录</view>
          <view v-for="(row, idx) in unqualifiedList"
                :key="idx"
                class="unqualified-item">
            <view class="unqualified-info">
              <text class="unqualified-label">不合格现象</text>
              <text class="unqualified-value">{{ row.defectivePhenomena || '-' }}</text>
            </view>
            <view class="unqualified-info">
              <text class="unqualified-label">数量</text>
              <text class="unqualified-value">{{ row.quantity != null ? row.quantity : '-' }}</text>
            </view>
            <u-button type="primary"
                      size="small"
                      class="unqualified-btn"
                      @click="openDealSheet(row)">
              å¤„理
            </u-button>
          </view>
        </view>
      </view>
    </up-popup>
    <!-- å¤„理结果选择 -->
    <up-action-sheet :show="showDealSheet"
                     :actions="dealSheetOptions"
                     @select="selectDeal"
                     @close="showDealSheet = false"
                     title="选择处理结果" />
  </view>
</template>
@@ -235,8 +317,13 @@
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
    qualityUnqualifiedListPage,
    qualityUnqualifiedDeal,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // å‡ºåŽ‚/成品检验 inspectType=2
  const currentInspectType = 2;
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
@@ -294,6 +381,121 @@
      value: item.nickName,
    }));
  });
  // æ£€éªŒçŠ¶æ€ç­›é€‰
  const showStateSheet = ref(false);
  const stateFilter = ref("");
  const stateSheetOptions = [
    { name: "全部", value: "" },
    { name: "待提交", value: 0 },
    { name: "已提交", value: 1 },
  ];
  const stateFilterLabel = computed(() => {
    const hit = stateSheetOptions.find(o => o.value === stateFilter.value);
    return hit ? hit.name : "全部";
  });
  const selectState = e => {
    stateFilter.value = e.value;
    showStateSheet.value = false;
    getList();
  };
  // ä¸åˆæ ¼å¤„理相关
  const showUnqualifiedPopup = ref(false);
  const unqualifiedLoading = ref(false);
  const unqualifiedList = ref([]);
  const currentInspectRow = ref(null);
  const showDealSheet = ref(false);
  const currentUnqualifiedRow = ref(null);
  // è¿‡ç¨‹/成品类支持 è¿”ä¿®/返工/报废/让步放行
  const dealSheetOptions = computed(() => {
    return ["返修", "返工", "报废", "让步放行"].map(name => ({
      name,
      value: name,
    }));
  });
  // æ‰“开不合格处理弹窗:按检验单id拉取待处理不合格记录
  const openUnqualified = async item => {
    currentInspectRow.value = item;
    showUnqualifiedPopup.value = true;
    unqualifiedLoading.value = true;
    unqualifiedList.value = [];
    try {
      const res = await qualityUnqualifiedListPage({
        inspectType: currentInspectType,
        inspectState: 0,
        inspectId: item.id,
        current: -1,
        size: -1,
      });
      let records = res.data?.records || res.data || [];
      // è‹¥åŽç«¯æœªæŒ‰æ£€éªŒå•过滤,前端兜底按检验单id过滤
      const hasInspectRef = records.some(
        r => r.inspectId != null || r.qualityInspectId != null
      );
      if (hasInspectRef) {
        records = records.filter(
          r => (r.inspectId ?? r.qualityInspectId) == item.id
        );
      }
      // åªä¿ç•™å¾…处理记录
      unqualifiedList.value = records.filter(
        r => Number(r.inspectState) !== 1
      );
    } catch (error) {
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("获取不合格记录失败:", error);
      unqualifiedList.value = [];
    } finally {
      unqualifiedLoading.value = false;
    }
  };
  // æ‰“开处理结果选择
  const openDealSheet = row => {
    currentUnqualifiedRow.value = row;
    showDealSheet.value = true;
  };
  // é€‰æ‹©å¤„理结果
  const selectDeal = e => {
    showDealSheet.value = false;
    if (!currentUnqualifiedRow.value) return;
    const result = e.value;
    // è¿”ä¿®/返工会自动创建返工生产订单,先二次确认
    if (result === "返修" || result === "返工") {
      uni.showModal({
        title: "提示",
        content: "将自动创建返工生产订单(FG单号),是否继续?",
        success: res => {
          if (res.confirm) {
            doDeal(currentUnqualifiedRow.value, result);
          }
        },
      });
      return;
    }
    doDeal(currentUnqualifiedRow.value, result);
  };
  // æäº¤ä¸åˆæ ¼å¤„理
  const doDeal = async (row, dealResult) => {
    try {
      await qualityUnqualifiedDeal({ id: row.id, dealResult });
      showToast("处理成功");
      if (currentInspectRow.value) {
        await openUnqualified(currentInspectRow.value);
      }
      if (unqualifiedList.value.length === 0) {
        showUnqualifiedPopup.value = false;
      }
      getList();
    } catch (error) {
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("不合格处理失败:", error);
    }
  };
  // è¿”回上一页
  const goBack = () => {
@@ -371,7 +573,9 @@
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 2 })
    params.inspectState =
      stateFilter.value === "" ? undefined : stateFilter.value;
    qualityInspectListPage({ ...params, inspectType: currentInspectType })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
@@ -430,25 +634,30 @@
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async item => {
  const submitInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    try {
      const res = await submitQualityInspect({ id: item.id });
      if (res.code === 200) {
        showToast("提交成功");
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("提交失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("提交失败:", error);
      showToast("提交失败,请重试");
    }
    uni.showModal({
      title: "提示",
      content: "提交后将生成入库记录且不可修改,是否继续?",
      success: async res => {
        if (!res.confirm) return;
        try {
          await submitQualityInspect({ id: item.id });
          showToast(
            "提交成功,合格部分已生成入库记录(待审核),不合格部分已生成处理单"
          );
          setTimeout(() => {
            getList();
          }, 1500);
        } catch (error) {
          // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
          console.error("提交失败:", error);
        }
      },
    });
  };
  // åˆ†é…æ£€éªŒå‘˜
@@ -551,6 +760,78 @@
    padding: 5px;
  }
  // æ£€éªŒçŠ¶æ€ç­›é€‰
  .filter-row {
    display: flex;
    align-items: center;
    margin-top: 10px;
  }
  .filter-item {
    display: flex;
    align-items: center;
    padding: 4px 10px;
    background: #f2f3f5;
    border-radius: 12px;
  }
  .filter-label {
    font-size: 12px;
    color: #666;
    margin-right: 4px;
  }
  .filter-value {
    font-size: 12px;
    color: #2979ff;
    margin-right: 4px;
  }
  // ä¸åˆæ ¼å¤„理弹窗
  .unqualified-dialog {
    background: #ffffff;
    border-radius: 16px 16px 0 0;
    padding: 20px;
    max-height: 70vh;
    overflow-y: auto;
  }
  .unqualified-empty {
    padding: 30px 0;
    text-align: center;
    font-size: 13px;
    color: #999;
  }
  .unqualified-item {
    display: flex;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .unqualified-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    margin-right: 10px;
  }
  .unqualified-label {
    font-size: 12px;
    color: #999;
  }
  .unqualified-value {
    font-size: 13px;
    color: #333;
    margin-top: 2px;
  }
  .unqualified-btn {
    width: 70px;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .stats-cards {
    display: flex;
src/pages/qualityManagement/materialInspection/add.vue
@@ -16,9 +16,9 @@
        <up-input v-model="form.supplier"
                  placeholder="请选择供应商"
                  readonly
                  :disabled="supplierQuantityDisabled" />
                  :disabled="supplierQuantityDisabled || isReadonly" />
        <template #right>
          <up-icon @click="showSupplierSheet = true"
          <up-icon @click="!isReadonly && (showSupplierSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -29,10 +29,10 @@
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
                  @click="!isReadonly && (showProductTree = true)"
                  :disabled="isEdit || isReadonly" />
        <template #right>
          <up-icon @click="showProductTree = true"
          <up-icon @click="!isReadonly && (showProductTree = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -43,9 +43,17 @@
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
                  :disabled="isEdit || isReadonly" />
        <template #right>
          <up-icon @click="showModelSheet = true"
          <view v-if="!isEdit"
                class="scan-entry"
                @click.stop="scanBarcode">
            <up-icon name="scan"
                     size="18"
                     color="#3c9cff" />
            <text class="scan-entry-text">扫码</text>
          </view>
          <up-icon @click="!isReadonly && (showModelSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -55,7 +63,8 @@
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
                  readonly
                  :disabled="isReadonly" />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
@@ -75,13 +84,14 @@
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="supplierQuantityDisabled" />
                  :disabled="supplierQuantityDisabled || isReadonly" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  :disabled="isReadonly"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
@@ -91,9 +101,10 @@
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  :disabled="isReadonly"
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
          <up-icon @click="!isReadonly && (showResultSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -103,9 +114,10 @@
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  :disabled="isReadonly"
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
          <up-icon @click="!isReadonly && (showInspectorSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -116,6 +128,7 @@
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly
                  :disabled="isReadonly"
                  @click="showDatePicker" />
        <!-- <template #right>
          <up-icon name="arrow-right"
@@ -163,6 +176,7 @@
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          :disabled="isReadonly"
                          clearable
                          border-bottom
                          class="step-input" />
@@ -182,14 +196,16 @@
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
        {{ isReadonly ? '返回' : '取消' }}
      </up-button>
      <up-button type="primary"
      <!-- å·²æäº¤å•据只读,隐藏保存/提交 -->
      <up-button v-if="!isReadonly"
                 type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
        ä¿å­˜
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
@@ -319,7 +335,11 @@
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import {
    modelList,
    productTreeList,
    modelByBarcode,
  } from "@/api/basicData/product.js";
  import {
    qualityInspectAdd,
    qualityInspectUpdate,
@@ -421,6 +441,7 @@
    return modelOptions.value.map(item => ({
      name: item.model,
      value: item.id,
      subname: item.barcode ? `条码:${item.barcode}` : "",
    }));
  });
@@ -477,6 +498,9 @@
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // å·²æäº¤ï¼ˆinspectState=1)单据整体只读
  const isReadonly = computed(() => Number(form.value?.inspectState) === 1);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
@@ -484,6 +508,7 @@
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    if (isReadonly.value) return;
    showDate.value = true;
  };
@@ -610,7 +635,7 @@
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    if (isReadonly.value) return;
    showTestStandardSheet.value = true;
  };
@@ -640,19 +665,51 @@
    }
  };
  // èŽ·å–åž‹å·åˆ—è¡¨
  // èŽ·å–åž‹å·åˆ—è¡¨ï¼ˆè¿”å›ž Promise,便于扫码后顺序回填)
  const getModels = value => {
    form.value.productModelId = "";
    form.value.unit = "";
    modelOptions.value = [];
    currentProductId.value = value;
    form.value.productName = findNodeById(productOptions.value, value);
    modelList({ id: value }).then(res => {
      modelOptions.value = res;
    const task = modelList({ id: value }).then(res => {
      modelOptions.value = res?.data || res || [];
    });
    if (currentProductId.value) {
      getList();
    }
    return task;
  };
  // æ‰«ç æŒ‰æ¡ç ç²¾ç¡®å›žå¡«äº§å“+规格型号
  const scanBarcode = () => {
    if (isEdit.value) return;
    uni.scanCode({
      scanType: ["barCode", "qrCode"],
      success: async res => {
        const barcode = (res.result || "").trim();
        if (!barcode) return;
        uni.showLoading({ title: "匹配中...", mask: true });
        try {
          const resp = await modelByBarcode({ barcode });
          const info = resp?.data || resp;
          if (info && info.productId) {
            // å…ˆæŒ‰ productId è”动出产品层,保证 UI çŠ¶æ€ä¸Žæ‰‹åŠ¨é€‰æ‹©ä¸€è‡´
            await getModels(info.productId);
            form.value.productId = info.productId;
            form.value.productName = info.productName || form.value.productName;
            form.value.productModelId = info.id;
            form.value.model = info.model || "";
            form.value.unit = info.unit || "";
          }
          uni.hideLoading();
          uni.showToast({ title: "已匹配规格", icon: "success" });
        } catch (e) {
          // æœªåŒ¹é…ç­‰é”™è¯¯å·²æœ‰å…¨å±€ toast,这里只恢复 loading
          uni.hideLoading();
        }
      },
    });
  };
  // èŽ·å–æŒ‡æ ‡åˆ—è¡¨
@@ -687,6 +744,11 @@
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    // å·²æäº¤å•据禁止保存/提交
    if (isReadonly.value) {
      showToast("检验单已提交,不允许修改");
      return;
    }
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
@@ -726,21 +788,22 @@
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        // æ–°å¢žä»…保存草稿,正式提交在列表页「提交」按钮(会生成入库记录)
        await qualityInspectAdd(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("提交失败:", error);
    } finally {
      loading.value = false;
    }
@@ -912,6 +975,19 @@
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  // è§„格行扫码入口
  .scan-entry {
    display: flex;
    align-items: center;
    margin-right: 8px;
    .scan-entry-text {
      font-size: 13px;
      color: #3c9cff;
      margin-left: 2px;
    }
  }
  .material-inspection-add {
    min-height: 100vh;
    background: #f8f9fa;
src/pages/qualityManagement/materialInspection/detail.vue
@@ -24,7 +24,7 @@
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
              {{ detailData.inspectState ? '已提交' : '待提交' }}
            </u-tag>
          </view>
        </view>
@@ -66,6 +66,18 @@
            <text class="detail-label">检测单位</text>
            <text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合格数量</text>
            <text class="detail-value">{{ detailData.qualifiedQuantity != null ? detailData.qualifiedQuantity : '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">不合格数量</text>
            <text class="detail-value">{{ detailData.unqualifiedQuantity != null ? detailData.unqualifiedQuantity : '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检验结论</text>
            <text class="detail-value">{{ detailData.checkResult || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">检验标准</text>
            <text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
src/pages/qualityManagement/materialInspection/index.vue
@@ -26,6 +26,17 @@
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ£€éªŒçŠ¶æ€ç­›é€‰ -->
      <view class="filter-row">
        <view class="filter-item"
              @click="showStateSheet = true">
          <text class="filter-label">检验状态</text>
          <text class="filter-value">{{ stateFilterLabel }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ—¥æœŸé€‰æ‹© -->
      <!-- <view class="date-range"
            v-if="searchForm.entryDate">
@@ -85,7 +96,7 @@
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
                     class="status-tag">
                {{ item.inspectState ? '已提交' : '未提交' }}
                {{ item.inspectState ? '已提交' : '待提交' }}
              </u-tag>
            </view>
          </view>
@@ -114,6 +125,18 @@
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">合格数量</text>
              <text class="detail-value">{{ item.qualifiedQuantity != null ? item.qualifiedQuantity : '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">不合格数量</text>
              <text class="detail-value">{{ item.unqualifiedQuantity != null ? item.unqualifiedQuantity : '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检验结论</text>
              <text class="detail-value">{{ item.checkResult || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
@@ -152,6 +175,15 @@
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
          </view>
          <view class="action-buttons">
            <u-button type="error"
                      size="small"
                      class="action-btn"
                      :disabled="!(Number(item.unqualifiedQuantity) > 0)"
                      @click.stop="openUnqualified(item)">
              ä¸åˆæ ¼å¤„理
            </u-button>
          </view>
        </view>
@@ -223,6 +255,56 @@
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
    <!-- æ£€éªŒçŠ¶æ€ç­›é€‰ -->
    <up-action-sheet :show="showStateSheet"
                     :actions="stateSheetOptions"
                     @select="selectState"
                     @close="showStateSheet = false"
                     title="选择检验状态" />
    <!-- ä¸åˆæ ¼å¤„理弹窗 -->
    <up-popup v-model:show="showUnqualifiedPopup"
              mode="bottom"
              round>
      <view class="unqualified-dialog">
        <view class="dialog-header">
          <text class="dialog-title">不合格处理</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showUnqualifiedPopup = false"></up-icon>
        </view>
        <view class="dialog-content">
          <view v-if="unqualifiedLoading"
                class="unqualified-empty">加载中...</view>
          <view v-else-if="unqualifiedList.length === 0"
                class="unqualified-empty">暂无待处理不合格记录</view>
          <view v-for="(row, idx) in unqualifiedList"
                :key="idx"
                class="unqualified-item">
            <view class="unqualified-info">
              <text class="unqualified-label">不合格现象</text>
              <text class="unqualified-value">{{ row.defectivePhenomena || '-' }}</text>
            </view>
            <view class="unqualified-info">
              <text class="unqualified-label">数量</text>
              <text class="unqualified-value">{{ row.quantity != null ? row.quantity : '-' }}</text>
            </view>
            <u-button type="primary"
                      size="small"
                      class="unqualified-btn"
                      @click="openDealSheet(row)">
              å¤„理
            </u-button>
          </view>
        </view>
      </view>
    </up-popup>
    <!-- å¤„理结果选择 -->
    <up-action-sheet :show="showDealSheet"
                     :actions="dealSheetOptions"
                     @select="selectDeal"
                     @close="showDealSheet = false"
                     title="选择处理结果" />
  </view>
</template>
@@ -235,8 +317,13 @@
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
    qualityUnqualifiedListPage,
    qualityUnqualifiedDeal,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // åŽŸæ–™æ£€éªŒ inspectType=0
  const currentInspectType = 0;
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
@@ -294,6 +381,104 @@
      value: item.nickName,
    }));
  });
  // æ£€éªŒçŠ¶æ€ç­›é€‰
  const showStateSheet = ref(false);
  const stateFilter = ref("");
  const stateSheetOptions = [
    { name: "全部", value: "" },
    { name: "待提交", value: 0 },
    { name: "已提交", value: 1 },
  ];
  const stateFilterLabel = computed(() => {
    const hit = stateSheetOptions.find(o => o.value === stateFilter.value);
    return hit ? hit.name : "全部";
  });
  const selectState = e => {
    stateFilter.value = e.value;
    showStateSheet.value = false;
    getList();
  };
  // ä¸åˆæ ¼å¤„理相关
  const showUnqualifiedPopup = ref(false);
  const unqualifiedLoading = ref(false);
  const unqualifiedList = ref([]);
  const currentInspectRow = ref(null);
  const showDealSheet = ref(false);
  const currentUnqualifiedRow = ref(null);
  // åŽŸæ–™ç±»ä»…æ”¯æŒ æŠ¥åºŸ/让步放行
  const dealSheetOptions = computed(() => {
    return ["报废", "让步放行"].map(name => ({ name, value: name }));
  });
  // æ‰“开不合格处理弹窗:按检验单id拉取待处理不合格记录
  const openUnqualified = async item => {
    currentInspectRow.value = item;
    showUnqualifiedPopup.value = true;
    unqualifiedLoading.value = true;
    unqualifiedList.value = [];
    try {
      const res = await qualityUnqualifiedListPage({
        inspectType: currentInspectType,
        inspectState: 0,
        inspectId: item.id,
        current: -1,
        size: -1,
      });
      let records = res.data?.records || res.data || [];
      // è‹¥åŽç«¯æœªæŒ‰æ£€éªŒå•过滤,前端兜底按检验单id过滤
      const hasInspectRef = records.some(
        r => r.inspectId != null || r.qualityInspectId != null
      );
      if (hasInspectRef) {
        records = records.filter(
          r => (r.inspectId ?? r.qualityInspectId) == item.id
        );
      }
      // åªä¿ç•™å¾…处理记录
      unqualifiedList.value = records.filter(
        r => Number(r.inspectState) !== 1
      );
    } catch (error) {
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("获取不合格记录失败:", error);
      unqualifiedList.value = [];
    } finally {
      unqualifiedLoading.value = false;
    }
  };
  // æ‰“开处理结果选择
  const openDealSheet = row => {
    currentUnqualifiedRow.value = row;
    showDealSheet.value = true;
  };
  // é€‰æ‹©å¤„理结果
  const selectDeal = e => {
    showDealSheet.value = false;
    if (!currentUnqualifiedRow.value) return;
    doDeal(currentUnqualifiedRow.value, e.value);
  };
  // æäº¤ä¸åˆæ ¼å¤„理
  const doDeal = async (row, dealResult) => {
    try {
      await qualityUnqualifiedDeal({ id: row.id, dealResult });
      showToast("处理成功");
      if (currentInspectRow.value) {
        await openUnqualified(currentInspectRow.value);
      }
      if (unqualifiedList.value.length === 0) {
        showUnqualifiedPopup.value = false;
      }
      getList();
    } catch (error) {
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("不合格处理失败:", error);
    }
  };
  // è¿”回上一页
  const goBack = () => {
@@ -371,7 +556,9 @@
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 0 })
    params.inspectState =
      stateFilter.value === "" ? undefined : stateFilter.value;
    qualityInspectListPage({ ...params, inspectType: currentInspectType })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
@@ -429,25 +616,30 @@
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async item => {
  const submitInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    try {
      const res = await submitQualityInspect({ id: item.id });
      if (res.code === 200) {
        showToast("提交成功");
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("提交失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("提交失败:", error);
      showToast("提交失败,请重试");
    }
    uni.showModal({
      title: "提示",
      content: "提交后将生成入库记录且不可修改,是否继续?",
      success: async res => {
        if (!res.confirm) return;
        try {
          await submitQualityInspect({ id: item.id });
          showToast(
            "提交成功,合格部分已生成入库记录(待审核),不合格部分已生成处理单"
          );
          setTimeout(() => {
            getList();
          }, 1500);
        } catch (error) {
          // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
          console.error("提交失败:", error);
        }
      },
    });
  };
  // åˆ†é…æ£€éªŒå‘˜
@@ -550,6 +742,78 @@
    padding: 5px;
  }
  // æ£€éªŒçŠ¶æ€ç­›é€‰
  .filter-row {
    display: flex;
    align-items: center;
    margin-top: 10px;
  }
  .filter-item {
    display: flex;
    align-items: center;
    padding: 4px 10px;
    background: #f2f3f5;
    border-radius: 12px;
  }
  .filter-label {
    font-size: 12px;
    color: #666;
    margin-right: 4px;
  }
  .filter-value {
    font-size: 12px;
    color: #2979ff;
    margin-right: 4px;
  }
  // ä¸åˆæ ¼å¤„理弹窗
  .unqualified-dialog {
    background: #ffffff;
    border-radius: 16px 16px 0 0;
    padding: 20px;
    max-height: 70vh;
    overflow-y: auto;
  }
  .unqualified-empty {
    padding: 30px 0;
    text-align: center;
    font-size: 13px;
    color: #999;
  }
  .unqualified-item {
    display: flex;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .unqualified-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    margin-right: 10px;
  }
  .unqualified-label {
    font-size: 12px;
    color: #999;
  }
  .unqualified-value {
    font-size: 13px;
    color: #333;
    margin-top: 2px;
  }
  .unqualified-btn {
    width: 70px;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .stats-cards {
    display: flex;
src/pages/qualityManagement/processInspection/add.vue
@@ -16,9 +16,9 @@
        <up-input v-model="form.process"
                  placeholder="请选择工序"
                  readonly
                  :disabled="processQuantityDisabled" />
                  :disabled="processQuantityDisabled || isReadonly" />
        <template #right>
          <up-icon @click="showprocessSheet = true"
          <up-icon @click="!isReadonly && (showprocessSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -29,10 +29,10 @@
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
                  @click="!isReadonly && (showProductTree = true)"
                  :disabled="isEdit || isReadonly" />
        <template #right>
          <up-icon @click="showProductTree = true"
          <up-icon @click="!isReadonly && (showProductTree = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -43,9 +43,9 @@
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
                  :disabled="isEdit || isReadonly" />
        <template #right>
          <up-icon @click="showModelSheet = true"
          <up-icon @click="!isReadonly && (showModelSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -55,7 +55,8 @@
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
                  readonly
                  :disabled="isReadonly" />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
@@ -75,13 +76,14 @@
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="processQuantityDisabled" />
                  :disabled="processQuantityDisabled || isReadonly" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  :disabled="isReadonly"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
@@ -91,9 +93,10 @@
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  :disabled="isReadonly"
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
          <up-icon @click="!isReadonly && (showResultSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -103,9 +106,10 @@
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  :disabled="isReadonly"
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
          <up-icon @click="!isReadonly && (showInspectorSheet = true)"
                   name="arrow-right" />
        </template>
      </up-form-item>
@@ -115,7 +119,8 @@
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
                  readonly
                  :disabled="isReadonly" />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
@@ -155,6 +160,7 @@
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          :disabled="isReadonly"
                          clearable
                          border-bottom
                          class="step-input" />
@@ -174,14 +180,16 @@
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
        {{ isReadonly ? '返回' : '取消' }}
      </up-button>
      <up-button type="primary"
      <!-- å·²æäº¤å•据只读,隐藏保存/提交 -->
      <up-button v-if="!isReadonly"
                 type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
        ä¿å­˜
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
@@ -469,6 +477,9 @@
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // å·²æäº¤ï¼ˆinspectState=1)单据整体只读
  const isReadonly = computed(() => Number(form.value?.inspectState) === 1);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
@@ -476,11 +487,13 @@
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    if (isReadonly.value) return;
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    if (isReadonly.value) return;
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
@@ -601,7 +614,7 @@
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    if (isReadonly.value) return;
    showTestStandardSheet.value = true;
  };
@@ -678,6 +691,11 @@
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    // å·²æäº¤å•据禁止保存/提交
    if (isReadonly.value) {
      showToast("检验单已提交,不允许修改");
      return;
    }
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
@@ -717,21 +735,22 @@
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        // æ–°å¢žä»…保存草稿,正式提交在列表页「提交」按钮
        await qualityInspectAdd(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("提交失败:", error);
    } finally {
      loading.value = false;
    }
src/pages/qualityManagement/processInspection/detail.vue
@@ -24,7 +24,7 @@
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
              {{ detailData.inspectState ? '已提交' : '待提交' }}
            </u-tag>
          </view>
        </view>
@@ -66,6 +66,18 @@
            <text class="detail-label">检测单位</text>
            <text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合格数量</text>
            <text class="detail-value">{{ detailData.qualifiedQuantity != null ? detailData.qualifiedQuantity : '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">不合格数量</text>
            <text class="detail-value">{{ detailData.unqualifiedQuantity != null ? detailData.unqualifiedQuantity : '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检验结论</text>
            <text class="detail-value">{{ detailData.checkResult || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">检验标准</text>
            <text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
src/pages/qualityManagement/processInspection/index.vue
@@ -26,6 +26,17 @@
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ£€éªŒçŠ¶æ€ç­›é€‰ -->
      <view class="filter-row">
        <view class="filter-item"
              @click="showStateSheet = true">
          <text class="filter-label">检验状态</text>
          <text class="filter-value">{{ stateFilterLabel }}</text>
          <up-icon name="arrow-down"
                   size="14"
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ—¥æœŸé€‰æ‹© -->
      <!-- <view class="date-range"
            v-if="searchForm.entryDate">
@@ -85,7 +96,7 @@
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
                     class="status-tag">
                {{ item.inspectState ? '已提交' : '未提交' }}
                {{ item.inspectState ? '已提交' : '待提交' }}
              </u-tag>
            </view>
          </view>
@@ -114,6 +125,18 @@
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">合格数量</text>
              <text class="detail-value">{{ item.qualifiedQuantity != null ? item.qualifiedQuantity : '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">不合格数量</text>
              <text class="detail-value">{{ item.unqualifiedQuantity != null ? item.unqualifiedQuantity : '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检验结论</text>
              <text class="detail-value">{{ item.checkResult || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
@@ -152,6 +175,15 @@
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
          </view>
          <view class="action-buttons">
            <u-button type="error"
                      size="small"
                      class="action-btn"
                      :disabled="!(Number(item.unqualifiedQuantity) > 0)"
                      @click.stop="openUnqualified(item)">
              ä¸åˆæ ¼å¤„理
            </u-button>
          </view>
        </view>
@@ -223,6 +255,56 @@
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
    <!-- æ£€éªŒçŠ¶æ€ç­›é€‰ -->
    <up-action-sheet :show="showStateSheet"
                     :actions="stateSheetOptions"
                     @select="selectState"
                     @close="showStateSheet = false"
                     title="选择检验状态" />
    <!-- ä¸åˆæ ¼å¤„理弹窗 -->
    <up-popup v-model:show="showUnqualifiedPopup"
              mode="bottom"
              round>
      <view class="unqualified-dialog">
        <view class="dialog-header">
          <text class="dialog-title">不合格处理</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showUnqualifiedPopup = false"></up-icon>
        </view>
        <view class="dialog-content">
          <view v-if="unqualifiedLoading"
                class="unqualified-empty">加载中...</view>
          <view v-else-if="unqualifiedList.length === 0"
                class="unqualified-empty">暂无待处理不合格记录</view>
          <view v-for="(row, idx) in unqualifiedList"
                :key="idx"
                class="unqualified-item">
            <view class="unqualified-info">
              <text class="unqualified-label">不合格现象</text>
              <text class="unqualified-value">{{ row.defectivePhenomena || '-' }}</text>
            </view>
            <view class="unqualified-info">
              <text class="unqualified-label">数量</text>
              <text class="unqualified-value">{{ row.quantity != null ? row.quantity : '-' }}</text>
            </view>
            <u-button type="primary"
                      size="small"
                      class="unqualified-btn"
                      @click="openDealSheet(row)">
              å¤„理
            </u-button>
          </view>
        </view>
      </view>
    </up-popup>
    <!-- å¤„理结果选择 -->
    <up-action-sheet :show="showDealSheet"
                     :actions="dealSheetOptions"
                     @select="selectDeal"
                     @close="showDealSheet = false"
                     title="选择处理结果" />
  </view>
</template>
@@ -235,8 +317,13 @@
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
    qualityUnqualifiedListPage,
    qualityUnqualifiedDeal,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // è¿‡ç¨‹æ£€éªŒ inspectType=1
  const currentInspectType = 1;
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
@@ -294,6 +381,121 @@
      value: item.nickName,
    }));
  });
  // æ£€éªŒçŠ¶æ€ç­›é€‰
  const showStateSheet = ref(false);
  const stateFilter = ref("");
  const stateSheetOptions = [
    { name: "全部", value: "" },
    { name: "待提交", value: 0 },
    { name: "已提交", value: 1 },
  ];
  const stateFilterLabel = computed(() => {
    const hit = stateSheetOptions.find(o => o.value === stateFilter.value);
    return hit ? hit.name : "全部";
  });
  const selectState = e => {
    stateFilter.value = e.value;
    showStateSheet.value = false;
    getList();
  };
  // ä¸åˆæ ¼å¤„理相关
  const showUnqualifiedPopup = ref(false);
  const unqualifiedLoading = ref(false);
  const unqualifiedList = ref([]);
  const currentInspectRow = ref(null);
  const showDealSheet = ref(false);
  const currentUnqualifiedRow = ref(null);
  // è¿‡ç¨‹/成品类支持 è¿”ä¿®/返工/报废/让步放行
  const dealSheetOptions = computed(() => {
    return ["返修", "返工", "报废", "让步放行"].map(name => ({
      name,
      value: name,
    }));
  });
  // æ‰“开不合格处理弹窗:按检验单id拉取待处理不合格记录
  const openUnqualified = async item => {
    currentInspectRow.value = item;
    showUnqualifiedPopup.value = true;
    unqualifiedLoading.value = true;
    unqualifiedList.value = [];
    try {
      const res = await qualityUnqualifiedListPage({
        inspectType: currentInspectType,
        inspectState: 0,
        inspectId: item.id,
        current: -1,
        size: -1,
      });
      let records = res.data?.records || res.data || [];
      // è‹¥åŽç«¯æœªæŒ‰æ£€éªŒå•过滤,前端兜底按检验单id过滤
      const hasInspectRef = records.some(
        r => r.inspectId != null || r.qualityInspectId != null
      );
      if (hasInspectRef) {
        records = records.filter(
          r => (r.inspectId ?? r.qualityInspectId) == item.id
        );
      }
      // åªä¿ç•™å¾…处理记录
      unqualifiedList.value = records.filter(
        r => Number(r.inspectState) !== 1
      );
    } catch (error) {
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("获取不合格记录失败:", error);
      unqualifiedList.value = [];
    } finally {
      unqualifiedLoading.value = false;
    }
  };
  // æ‰“开处理结果选择
  const openDealSheet = row => {
    currentUnqualifiedRow.value = row;
    showDealSheet.value = true;
  };
  // é€‰æ‹©å¤„理结果
  const selectDeal = e => {
    showDealSheet.value = false;
    if (!currentUnqualifiedRow.value) return;
    const result = e.value;
    // è¿”ä¿®/返工会自动创建返工生产订单,先二次确认
    if (result === "返修" || result === "返工") {
      uni.showModal({
        title: "提示",
        content: "将自动创建返工生产订单(FG单号),是否继续?",
        success: res => {
          if (res.confirm) {
            doDeal(currentUnqualifiedRow.value, result);
          }
        },
      });
      return;
    }
    doDeal(currentUnqualifiedRow.value, result);
  };
  // æäº¤ä¸åˆæ ¼å¤„理
  const doDeal = async (row, dealResult) => {
    try {
      await qualityUnqualifiedDeal({ id: row.id, dealResult });
      showToast("处理成功");
      if (currentInspectRow.value) {
        await openUnqualified(currentInspectRow.value);
      }
      if (unqualifiedList.value.length === 0) {
        showUnqualifiedPopup.value = false;
      }
      getList();
    } catch (error) {
      // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
      console.error("不合格处理失败:", error);
    }
  };
  // è¿”回上一页
  const goBack = () => {
@@ -371,7 +573,9 @@
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 1 })
    params.inspectState =
      stateFilter.value === "" ? undefined : stateFilter.value;
    qualityInspectListPage({ ...params, inspectType: currentInspectType })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
@@ -429,25 +633,31 @@
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async item => {
  const submitInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    try {
      const res = await submitQualityInspect({ id: item.id });
      if (res.code === 200) {
        showToast("提交成功");
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("提交失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("提交失败:", error);
      showToast("提交失败,请重试");
    }
    uni.showModal({
      title: "提示",
      content: "提交后将生成不合格处理单且不可修改,是否继续?",
      success: async res => {
        if (!res.confirm) return;
        try {
          await submitQualityInspect({ id: item.id });
          // è¿‡ç¨‹æ£€æäº¤ä¸äº§ç”Ÿå…¥åº“记录
          showToast(
            "提交成功,不合格部分已生成处理单(过程检验不产生入库记录)"
          );
          setTimeout(() => {
            getList();
          }, 1500);
        } catch (error) {
          // request.ts å·²ç»Ÿä¸€ toast åŽç«¯ msg,这里只记录
          console.error("提交失败:", error);
        }
      },
    });
  };
  // åˆ†é…æ£€éªŒå‘˜
@@ -550,6 +760,78 @@
    padding: 5px;
  }
  // æ£€éªŒçŠ¶æ€ç­›é€‰
  .filter-row {
    display: flex;
    align-items: center;
    margin-top: 10px;
  }
  .filter-item {
    display: flex;
    align-items: center;
    padding: 4px 10px;
    background: #f2f3f5;
    border-radius: 12px;
  }
  .filter-label {
    font-size: 12px;
    color: #666;
    margin-right: 4px;
  }
  .filter-value {
    font-size: 12px;
    color: #2979ff;
    margin-right: 4px;
  }
  // ä¸åˆæ ¼å¤„理弹窗
  .unqualified-dialog {
    background: #ffffff;
    border-radius: 16px 16px 0 0;
    padding: 20px;
    max-height: 70vh;
    overflow-y: auto;
  }
  .unqualified-empty {
    padding: 30px 0;
    text-align: center;
    font-size: 13px;
    color: #999;
  }
  .unqualified-item {
    display: flex;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .unqualified-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    margin-right: 10px;
  }
  .unqualified-label {
    font-size: 12px;
    color: #999;
  }
  .unqualified-value {
    font-size: 13px;
    color: #333;
    margin-top: 2px;
  }
  .unqualified-btn {
    width: 70px;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .stats-cards {
    display: flex;
src/pages/works.vue
@@ -391,6 +391,10 @@
      icon: "/static/images/icon/gongyingshangdangan.svg",
      label: "供应商档案",
    },
    {
      icon: "/static/images/icon/caigouguanli.svg",
      label: "采购需求",
    },
  ]);
  // è´¢åŠ¡ç®¡ç†åŠŸèƒ½æ•°æ®
@@ -470,6 +474,10 @@
    {
      icon: "/static/images/icon/kucunguanli.svg",
      label: "库存管理",
    },
    {
      icon: "/static/images/icon/kucunguanli.svg",
      label: "库存盘点",
    },
  ]);
@@ -647,6 +655,22 @@
      icon: "/static/images/icon/shengchanhesuan.svg",
      label: "生产核算",
    },
    {
      icon: "/static/images/icon/shengchanbaogong.svg",
      label: "计件单价配置",
    },
    {
      icon: "/static/images/icon/shengchanshikuang.svg",
      label: "产量统计",
    },
    {
      icon: "/static/images/icon/shengchanhesuan.svg",
      label: "材料消耗核算",
    },
    {
      icon: "/static/images/icon/shengchanshikuang.svg",
      label: "成本核算分析",
    },
  ]);
  // è®¾å¤‡ç®¡ç†åŠŸèƒ½æ•°æ®
@@ -774,6 +798,11 @@
      case "供应商档案":
        uni.navigateTo({
          url: "/pages/basicData/supplierManage/index",
        });
        break;
      case "采购需求":
        uni.navigateTo({
          url: "/pages/procurementManagement/procurementDemand/index",
        });
        break;
      case "公出管理":
@@ -924,6 +953,26 @@
          url: "/pages/productionManagement/productionAccounting/index",
        });
        break;
      case "计件单价配置":
        uni.navigateTo({
          url: "/pages/productionManagement/pieceRateConfig/index",
        });
        break;
      case "产量统计":
        uni.navigateTo({
          url: "/pages/productionManagement/outputStatistics/index",
        });
        break;
      case "材料消耗核算":
        uni.navigateTo({
          url: "/pages/productionManagement/materialConsumption/index",
        });
        break;
      case "成本核算分析":
        uni.navigateTo({
          url: "/pages/productionManagement/costAccounting/index",
        });
        break;
      case "生产追溯":
        uni.navigateTo({
          url: "/pages/productionManagement/productionTraceability/index",
@@ -1054,6 +1103,11 @@
          url: "/pages/inventoryManagement/stockManagement/index",
        });
        break;
      case "库存盘点":
        uni.navigateTo({
          url: "/pages/inventoryManagement/stockCheck/index",
        });
        break;
      case "反馈登记":
        uni.navigateTo({
          url: "/pages/customerService/feedbackRegistration/index",
src/utils/common.ts
@@ -1,3 +1,5 @@
import dayjs from 'dayjs'
/**
* æ˜¾ç¤ºæ¶ˆæ¯æç¤ºæ¡†
* @param content æç¤ºçš„æ ‡é¢˜
@@ -10,6 +12,19 @@
}
/**
* æœ¬å‘¨æ—¥æœŸåŒºé—´ï¼ˆå‘¨ä¸€ ~ ä»Šå¤©ï¼‰
* dayjs æœªé…ç½® locale æ—¶ä¸€å‘¨ä»Žå‘¨æ—¥ç®—起,这里显式按周一起算
*/
export function getWeekRange() {
  const today = dayjs()
  const offset = (today.day() + 6) % 7
  return {
    startDate: today.subtract(offset, 'day').format('YYYY-MM-DD'),
    endDate: today.format('YYYY-MM-DD')
  }
}
/**
* æ˜¾ç¤ºæ¨¡æ€å¼¹çª—
* @param content æç¤ºçš„æ ‡é¢˜
*/