ZN
2 天以前 077ab59c700b85efdd057265bf752ad5942395b2
feat(quality): 新增质量管理模块的API接口和移动端页面

- 新增基础数据API:工序分页查询
- 新增质量管理API:检测项维护、附件管理、检验标准绑定、检验参数、临期退货、不合格管理、原材料检验、指标维护、原材料检验完整功能
- 新增原生能力工具类:扫码、拍照、蓝牙等功能封装
- 新增移动端页面:指标绑定管理、质量看板可视化、检测项维护、原材料检验、近效期退货台账、指标维护详情
已添加32个文件
12458 ■■■■■ 文件已修改
src/api/basicData/productProcess.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/inspectItem.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/materialInspection.js 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/metricMaintenance.js 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/nearExpiryReturn.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/nonconformingManagement.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/qualityInspectFile.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/qualityInspectParam.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/qualityTestStandardBinding.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/rawMaterial.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/InspectItem/index.vue 340 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/add.vue 1128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/detail.vue 422 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/fileList.vue 566 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 776 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/add.vue 1134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 421 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/fileList.vue 566 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 775 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/metricBinding/detail.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/metricBinding/index.vue 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/metricMaintenance/detail.vue 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/metricMaintenance/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/nearExpiryReturn/index.vue 404 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/nonconformingManagement/index.vue 434 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/add.vue 1128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/detail.vue 423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/fileList.vue 566 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 775 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/rawMaterial/index.vue 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/visualization/qualityDashboard.vue 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/native.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/productProcess.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
import request from '@/utils/request'
// å·¥åºåˆ—表分页查询
export function productProcessListPage(query) {
  return request({
    url: '/productProcess/listPage',
    method: 'get',
    params: query
  })
}
src/api/qualityManagement/inspectItem.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import request from "@/utils/request";
export function qualityInspectItemListPage(data) {
  return request({
    url: "/qualityInspectItem/listPage",
    method: "post",
    data,
  });
}
export function qualityInspectItemSave(data) {
  return request({
    url: "/qualityInspectItem/save",
    method: "post",
    data,
  });
}
export function qualityInspectItemDelete(data) {
  return request({
    url: "/qualityInspectItem/delete",
    method: "post",
    data,
  });
}
src/api/qualityManagement/materialInspection.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,166 @@
import request from "@/utils/request";
// æŸ¥è¯¢åŽŸææ–™æ£€éªŒåˆ—è¡¨
export function qualityInspectListPage(query) {
    return request({
        url: '/quality/qualityInspect/listPage',
        method: 'get',
        params: query,
    })
}
// æ–°å¢žåŽŸææ–™æ£€éªŒ
export function qualityInspectAdd(data) {
    return request({
        url: '/quality/qualityInspect/add',
        method: 'post',
        data: data,
    })
}
// ä¿®æ”¹åŽŸææ–™æ£€éªŒ
export function qualityInspectUpdate(data) {
    return request({
        url: '/quality/qualityInspect/update',
        method: 'post',
        data: data,
    })
}
// åˆ é™¤åŽŸææ–™æ£€éªŒ
export function qualityInspectDel(data) {
    return request({
        url: '/quality/rawMaterialInspection/delete',
        method: 'post',
        data: data,
    })
}
// æäº¤åŽŸææ–™æ£€éªŒ
export function submitQualityInspect(data) {
    return request({
        url: '/quality/qualityInspect/submit',
        method: 'post',
        data: data,
    })
}
// ä¸‹è½½åŽŸææ–™æ£€éªŒæŠ¥å‘Š
export function downloadQualityInspect(data) {
    return request({
        url: '/quality/rawMaterialInspection/export',
        method: 'post',
        data: data,
        responseType: "blob",
    })
}
// èŽ·å–ä¾›åº”å•†åˆ—è¡¨
export function getSupplierList() {
    return request({
        url: '/basic/supplier/list',
        method: 'get',
    })
}
// èŽ·å–äº§å“åˆ—è¡¨
export function getProductList() {
    return request({
        url: '/basic/product/list',
        method: 'get',
    })
}
// èŽ·å–äº§å“åž‹å·åˆ—è¡¨
export function getProductModelList(productId) {
    return request({
        url: '/basic/productModel/list',
        method: 'get',
        params: { productId },
    })
}
// èŽ·å–æ£€éªŒå‘˜åˆ—è¡¨
export function getUserList() {
    return request({
        url: '/system/user/list',
        method: 'get',
    })
}
// æŸ¥è¯¢æ£€éªŒæŒ‡æ ‡
export function qualityInspectParamInfo(query) {
    return request({
        url: '/quality/qualityInspectParam/' + query,
        method: 'get',
        data: query,
    })
}
// æäº¤æ£€éªŒ
export function qualityInspectParamUpdate(query) {
    return request({
        url: '/quality/qualityInspectParam/update',
        method: 'post',
        data: query,
    })
}
// åˆ é™¤æ£€éªŒè®°å½•
export function qualityInspectParamDel(query) {
    return request({
        url: '/quality/qualityInspectParam/del',
        method: 'delete',
        data: query,
    })
}
// åˆ é™¤æŒ‡æ ‡åˆ—表
export function qualityInspectDetailByProductId(params) {
  return request({
    url: "/qualityTestStandard/getQualityTestStandardByProductId",
    method: "get",
    params: params,
  });
}
// æ ¹æ®æ ‡å‡†ID获取标准参数
export function getQualityTestStandardParamByTestStandardId(testStandardId) {
  return request({
    url: "/qualityTestStandard/getQualityTestStandardParamByTestStandardId",
    method: "get",
    params: { testStandardId },
  });
}
// æŸ¥è¯¢é™„件列表
export function qualityInspectFileListPage(query) {
    return request({
        url: '/quality/qualityInspectFile/listPage',
        method: 'get',
        params: query,
    })
}
// ä¿å­˜é™„件列表
export function qualityInspectFileAdd(query) {
    return request({
        url: '/quality/qualityInspectFile/add',
        method: 'post',
        data: query,
    })
}
// åˆ é™¤é™„件列表
export function qualityInspectFileDel(query) {
    return request({
        url: '/quality/qualityInspectFile/del',
        method: 'delete',
        data: query,
    })
}
// å·¥åºæŸ¥è¯¢
export function list() {
    return request({
        url: "/productProcess/list",
        method: "get",
    });
}
src/api/qualityManagement/metricMaintenance.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
import request from "@/utils/request";
// æŸ¥è¯¢æŒ‡æ ‡åˆ—表
export function qualityTestStandardListPage(query) {
  return request({
    url: "/qualityTestStandard/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæŒ‡æ ‡åˆ—表
export function qualityTestStandardAdd(query) {
  return request({
    url: "/qualityTestStandard/add",
    method: "post",
    data: query,
  });
}
// ä¿®æ”¹æŒ‡æ ‡åˆ—表
export function qualityTestStandardUpdate(query) {
  return request({
    url: "/qualityTestStandard/update",
    method: "post",
    data: query,
  });
}
// åˆ é™¤æŒ‡æ ‡åˆ—表
export function qualityTestStandardDel(query) {
  return request({
    url: "/qualityTestStandard/del",
    method: "delete",
    data: query,
  });
}
// åˆ é™¤æŒ‡æ ‡åˆ—表
export function qualityInspectDetailByProductId(params) {
  return request({
    url: "/qualityTestStandard/getQualityTestStandardByProductId",
    method: "get",
    params: params,
  });
}
// å¤åˆ¶æ ‡å‡†å‚æ•°
export function qualityTestStandardCopyParam(id) {
  return request({
    url: "/qualityTestStandard/copyParam",
    method: "post",
    data: { id },
  });
}
// æ‰¹é‡å®¡æ ¸ï¼ˆçŠ¶æ€ï¼š1=通过/批准,2=撤销)
// ä¼ å‚:[{ id, state }]
export function qualityTestStandardAudit(data) {
  return request({
    url: "/qualityTestStandard/qualityTestStandardAudit",
    method: "post",
    data,
  });
}
// æ ‡å‡†å‚数:列表(不分页)
export function qualityTestStandardParamList(query) {
  return request({
    url: "/qualityTestStandardParam/list",
    method: "get",
    params: query,
  });
}
// æ ‡å‡†å‚数:新增
export function qualityTestStandardParamAdd(data) {
  return request({
    url: "/qualityTestStandardParam/add",
    method: "post",
    data,
  });
}
// æ ‡å‡†å‚数:修改
export function qualityTestStandardParamUpdate(data) {
  return request({
    url: "/qualityTestStandardParam/update",
    method: "post",
    data,
  });
}
// æ ‡å‡†å‚数:删除(传 id æ•°ç»„)
export function qualityTestStandardParamDel(ids) {
  return request({
    url: "/qualityTestStandardParam/del",
    method: "delete",
    data: ids,
  });
}
src/api/qualityManagement/nearExpiryReturn.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
import request from '@/utils/request'
// æŸ¥è¯¢ä¸´æœŸé€€å›žå°è´¦åˆ—表
export function nearExpiryReturnListPage(query) {
    return request({
        url: '/quality/nearExpiryReturn/listPage',
        method: 'get',
        params: query,
    })
}
// æ–°å¢žä¸´æœŸé€€å›žå°è´¦
export function nearExpiryReturnAdd(data) {
    return request({
        url: '/quality/nearExpiryReturn/add',
        method: 'post',
        data: data,
    })
}
// ä¿®æ”¹ä¸´æœŸé€€å›žå°è´¦
export function nearExpiryReturnUpdate(data) {
    return request({
        url: '/quality/nearExpiryReturn/update',
        method: 'post',
        data: data,
    })
}
// åˆ é™¤ä¸´æœŸé€€å›žå°è´¦
export function nearExpiryReturnDel(ids) {
    return request({
        url: '/quality/nearExpiryReturn/del',
        method: 'delete',
        data: ids,
    })
}
// èŽ·å–ä¸´æœŸé€€å›žå°è´¦è¯¦æƒ…
export function nearExpiryReturnDetail(id) {
    return request({
        url: '/quality/nearExpiryReturn/' + id,
        method: 'get',
    })
}
src/api/qualityManagement/nonconformingManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
import request from "@/utils/request";
// æŸ¥è¯¢ä¸åˆæ ¼ç®¡ç†åˆ—表
export function qualityUnqualifiedListPage(query) {
  return request({
    url: "/quality/qualityUnqualified/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žä¸åˆæ ¼ç®¡ç†åˆ—表
export function qualityUnqualifiedAdd(query) {
  return request({
    url: "/quality/qualityUnqualified/add",
    method: "post",
    data: query,
  });
}
// ä¿®æ”¹ä¸åˆæ ¼ç®¡ç†åˆ—表
export function qualityUnqualifiedUpdate(query) {
  return request({
    url: "/quality/qualityUnqualified/update",
    method: "post",
    data: query,
  });
}
// ä¸åˆæ ¼å¤„理
export function qualityUnqualifiedDeal(query) {
  return request({
    url: "/quality/qualityUnqualified/deal",
    method: "post",
    data: query,
  });
}
// åˆ é™¤ä¸åˆæ ¼ç®¡ç†åˆ—表
export function qualityUnqualifiedDel(query) {
  return request({
    url: "/quality/qualityUnqualified/del",
    method: "delete",
    data: query,
  });
}
// æŸ¥è¯¢ä¸åˆæ ¼ç®¡ç†ä¿¡æ¯
export function getQualityUnqualifiedInfo(query) {
  return request({
    url: "/quality/qualityUnqualified/" + query,
    method: "get",
    data: query,
  });
}
src/api/qualityManagement/qualityInspectFile.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
import request from '@/utils/request'
// æŸ¥è¯¢é™„件列表
export function qualityInspectFileListPage(query) {
    return request({
        url: '/quality/qualityInspectFile/listPage',
        method: 'get',
        params: query,
    })
}
// ä¿å­˜é™„件列表
export function qualityInspectFileAdd(query) {
    return request({
        url: '/quality/qualityInspectFile/add',
        method: 'post',
        data: query,
    })
}
// åˆ é™¤é™„件列表
export function qualityInspectFileDel(query) {
    return request({
        url: '/quality/qualityInspectFile/del',
        method: 'delete',
        data: query,
    })
}
src/api/qualityManagement/qualityInspectParam.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import request from '@/utils/request'
// æŸ¥è¯¢æ£€éªŒæŒ‡æ ‡
export function qualityInspectParamInfo(query) {
    return request({
        url: '/quality/qualityInspectParam/' + query,
        method: 'get',
        data: query,
    })
}
// æäº¤æ£€éªŒ
export function qualityInspectParamUpdate(query) {
    return request({
        url: '/quality/qualityInspectParam/update',
        method: 'post',
        data: query,
    })
}
// åˆ é™¤æ£€éªŒè®°å½•
export function qualityInspectParamDel(query) {
    return request({
        url: '/quality/qualityInspectParam/del',
        method: 'delete',
        data: query,
    })
}
src/api/qualityManagement/qualityTestStandardBinding.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from "@/utils/request";
// ç»‘定列表(不分页)
export function qualityTestStandardBindingList(query) {
  return request({
    url: "/qualityTestStandardBinding/list",
    method: "get",
    params: query,
  });
}
// æ–°å¢žç»‘定(支持批量)
export function qualityTestStandardBindingAdd(data) {
  return request({
    url: "/qualityTestStandardBinding/add",
    method: "post",
    data,
  });
}
// åˆ é™¤ç»‘定(传 id æ•°ç»„)
export function qualityTestStandardBindingDel(ids) {
  return request({
    url: "/qualityTestStandardBinding/del",
    method: "delete",
    data: ids,
  });
}
src/api/qualityManagement/rawMaterial.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
import request from '@/utils/request'
// æŸ¥è¯¢åŽŸæ£€åˆ—è¡¨
export function findRawMaterialListPage(query) {
    return request({
        url: '/quality/rawMaterial/listPage',
        method: 'get',
        params: query,
    })
}
// æŸ¥è¯¢åŽŸæ£€è¯¦æƒ…
export function findRawMaterialDetail(id) {
    return request({
        url: '/quality/rawMaterial/detail/' + id,
        method: 'get',
    })
}
// æ–°å¢žåŽŸæ£€
export function createRawMaterial(data) {
    return request({
        url: '/quality/rawMaterial',
        method: 'post',
        data: data,
    })
}
// æäº¤
export function submitRawMaterial(id) {
    return request({
        url: '/quality/rawMaterial/submit/' + id,
        method: 'patch',
    })
}
// ä¿®æ”¹æ£€éªŒäºº
export function updateCheckUserName(data) {
    return request({
        url: '/quality/rawMaterial/updateCheckUserName',
        method: 'patch',
        data: data,
    })
}
// ä¿®æ”¹åŽŸæ£€
export function updateRawMaterial(data) {
    return request({
        url: '/quality/rawMaterial',
        method: 'put',
        data: data,
    })
}
// åˆ é™¤åŽŸæ£€
export function deleteRawMaterial(query) {
    return request({
        url: '/quality/rawMaterial',
        method: 'delete',
        data: query,
    })
}
export function downloadRawMaterial(data) {
    return request({
        url: '/quality/rawMaterial/down',
        method: 'post',
        data: data,
        responseType: "blob",
    })
}
src/pages/qualityManagement/InspectItem/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,340 @@
<template>
  <view class="inspect-item-page">
    <PageHeader title="检测项维护" @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <up-search
        placeholder="请输入检测项目名称"
        v-model="searchForm.name"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
      ></up-search>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-content" @click="openDialog('edit', item)">
          <view class="item-row">
            <text class="item-label">检测项目:</text>
            <text class="item-value">{{ item.name }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">单位:</text>
            <text class="item-value">{{ item.unit || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">标准值:</text>
            <text class="item-value">{{ item.standardValue || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">内控值:</text>
            <text class="item-value">{{ item.internalControl || '-' }}</text>
          </view>
        </view>
        <view class="item-actions">
          <up-button type="primary" size="mini" @click.stop="openDialog('edit', item)">编辑</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <!-- åŠ è½½æ›´å¤š/分页 -->
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button" @click="openDialog('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- æ–°å¢ž/修改弹窗 -->
    <up-popup v-model:show="dialogVisible" mode="center" round closeable @close="closeDialog">
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">{{ operationType === 'add' ? '新增检测项目' : '修改检测项目' }}</text>
        </view>
        <up-form :model="form" ref="formRef" label-width="100" label-position="top">
          <up-form-item label="检测项目" prop="name" required borderBottom>
            <up-input v-model="form.name" placeholder="请输入检测项目名称" border="surround" />
          </up-form-item>
          <up-form-item label="单位" prop="unit" required borderBottom>
            <up-input v-model="form.unit" placeholder="请输入单位" border="surround" />
          </up-form-item>
          <up-form-item label="标准值" prop="standardValue" borderBottom>
            <up-input v-model="form.standardValue" placeholder="请输入标准值" border="surround" />
          </up-form-item>
          <up-form-item label="内控值" prop="internalControl" borderBottom>
            <up-input v-model="form.internalControl" placeholder="请输入内控值" border="surround" />
          </up-form-item>
        </up-form>
        <view class="dialog-footer">
          <up-button type="primary" text="确认" @click="submitForm" :loading="submitLoading"></up-button>
          <up-button text="取消" @click="closeDialog" customStyle="margin-top: 20rpx"></up-button>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import {
  qualityInspectItemListPage,
  qualityInspectItemSave,
  qualityInspectItemDelete
} from '@/api/qualityManagement/inspectItem.js';
import { toast, showConfirm } from '@/utils/common';
const searchForm = reactive({
  name: ''
});
const tableData = ref([]);
const page = reactive({
  current: 1,
  size: 20,
  total: 0
});
const loadStatus = ref('loadmore');
const dialogVisible = ref(false);
const operationType = ref('add');
const submitLoading = ref(false);
const form = reactive({
  id: null,
  name: '',
  unit: '',
  standardValue: '',
  internalControl: ''
});
const rules = {
  name: {
    type: 'string',
    required: true,
    message: '请输入检测项目名称',
    trigger: ['blur', 'change']
  },
  unit: {
    type: 'string',
    required: true,
    message: '请输入单位',
    trigger: ['blur', 'change']
  }
};
const formRef = ref(null);
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const params = {
    name: searchForm.name || null,
    current: page.current,
    size: page.size
  };
  qualityInspectItemListPage(params).then(res => {
    const records = res?.data?.records || [];
    if (page.current === 1) {
      tableData.value = records;
    } else {
      tableData.value = [...tableData.value, ...records];
    }
    page.total = res?.data?.total || 0;
    if (tableData.value.length >= page.total) {
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
  });
};
const handleQuery = () => {
  page.current = 1;
  page.total = 0;
  tableData.value = [];
  loadStatus.value = 'loadmore';
  getList();
};
const goBack = () => {
  uni.navigateBack();
};
const openDialog = (type, row) => {
  operationType.value = type;
  if (type === 'edit' && row) {
    Object.assign(form, {
      id: row.id,
      name: row.name,
      unit: row.unit,
      standardValue: row.standardValue,
      internalControl: row.internalControl
    });
  } else {
    Object.assign(form, {
      id: null,
      name: '',
      unit: '',
      standardValue: '',
      internalControl: ''
    });
  }
  dialogVisible.value = true;
};
const closeDialog = () => {
  dialogVisible.value = false;
};
const submitForm = () => {
  formRef.value.validate().then(res => {
    submitLoading.value = true;
    qualityInspectItemSave(form).then(() => {
      toast(operationType.value === 'add' ? '新增成功' : '修改成功');
      dialogVisible.value = false;
      handleQuery();
    }).finally(() => {
      submitLoading.value = false;
    });
  }).catch(errors => {
    console.log('验证失败', errors);
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该检测项目吗?').then(res => {
    if (res.confirm) {
      qualityInspectItemDelete({ id: row.id }).then(() => {
        toast('删除成功');
        handleQuery();
      });
    }
  });
};
onMounted(() => {
  handleQuery();
});
</script>
<style lang="scss" scoped>
.inspect-item-page {
  padding-bottom: 120rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 160rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.pagination-container {
  padding: 20rpx 0;
}
.no-data {
  padding-top: 200rpx;
}
.fab-button {
  position: fixed;
  right: 40rpx;
  bottom: 60rpx;
  width: 100rpx;
  height: 100rpx;
  background-color: #3c9cff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.4);
  z-index: 99;
}
.dialog-content {
  width: 600rpx;
  padding: 40rpx;
  background-color: #ffffff;
  border-radius: 24rpx;
  overflow: hidden;
  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.dialog-header {
  margin-bottom: 30rpx;
  text-align: center;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #303133;
}
.dialog-footer {
  margin-top: 40rpx;
}
</style>
src/pages/qualityManagement/finalInspection/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1128 @@
<template>
  <view class="material-inspection-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="isEdit ? '编辑出厂检验' : '新增出厂检验'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <up-form :model="form"
             ref="formRef"
             label-width="110"
             :rules="rules">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <!-- <up-form-item label="工序"
                    prop="process"
                    required
                    border-bottom>
        <up-input v-model="form.process"
                  placeholder="请选择工序"
                  readonly
                  :disabled="processQuantityDisabled" />
        <template #right>
          <up-icon @click="showprocessSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item> -->
      <up-form-item label="产品名称"
                    prop="productId"
                    required
                    border-bottom>
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showProductTree = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="规格型号"
                    prop="productModelId"
                    required
                    border-bottom>
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showModelSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit"
                    border-bottom>
        <up-input v-model="form.unit"
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="processQuantityDisabled" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
                    prop="checkResult"
                    required
                    border-bottom>
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检验员"
                    prop="checkName"
                    border-bottom>
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检测日期"
                    prop="checkTime"
                    required
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
        </template> -->
      </up-form-item>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="inspection-items-container">
        <view class="steps-header">
          <text class="steps-title">检验项目</text>
          <text class="steps-count">共 {{ tableData.length }} ä¸ªé¡¹ç›®</text>
        </view>
        <view class="steps-list">
          <view v-for="(item, index) in tableData"
                :key="index"
                class="exec-step-item">
            <view class="step-number">
              {{ index + 1 }}
            </view>
            <view class="step-content">
              <view class="step-row">
                <text class="step-label">指标:</text>
                <text class="step-value">{{ item.parameterItem }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">单位:</text>
                <text class="step-value">{{ item.unit }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">标准值:</text>
                <text class="step-value">{{ item.standardValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">内控值:</text>
                <text class="step-value">{{ item.controlValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          clearable
                          border-bottom
                          class="step-input" />
              </view>
            </view>
          </view>
          <view v-if="tableData.length === 0"
                class="empty-data">
            <text>请先选择指标</text>
          </view>
        </view>
      </view>
    </up-form>
    <!-- åº•部按钮 -->
    <view class="bottom-buttons">
      <up-button type="default"
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
      </up-button>
      <up-button type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <!-- å·¥åºé€‰æ‹© -->
    <up-action-sheet :show="showprocessSheet"
                     :actions="processOptions"
                     @select="selectprocess"
                     @close="showprocessSheet = false"
                     title="选择工序" />
    <!-- äº§å“é€‰æ‹© -->
    <up-action-sheet :show="showProductSheet"
                     :actions="productSheetOptions"
                     @select="selectProduct"
                     @close="showProductSheet = false"
                     title="选择产品" />
    <!-- è§„格型号选择 -->
    <up-action-sheet :show="showModelSheet"
                     :actions="modelSheetOptions"
                     @select="selectModel"
                     @close="showModelSheet = false"
                     title="选择规格型号" />
    <!-- æ£€æµ‹ç»“果选择 -->
    <up-action-sheet :show="showResultSheet"
                     :actions="resultSheetOptions"
                     @select="selectResult"
                     @close="showResultSheet = false"
                     title="选择检测结果" />
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     @close="showInspectorSheet = false"
                     title="选择检验员" />
    <!-- æŒ‡æ ‡é€‰æ‹© -->
    <up-action-sheet :show="showTestStandardSheet"
                     :actions="testStandardSheetOptions"
                     @select="selectTestStandard"
                     @close="showTestStandardSheet = false"
                     title="选择指标" />
    <!-- äº§å“æ ‘形选择器 -->
    <up-popup v-model:show="showProductTree"
              position="bottom"
              :round="true"
              :closeable="true"
              @close="showProductTree = false">
      <view class="tree-selector">
        <view class="tree-header">
          <text class="tree-title">选择产品</text>
        </view>
        <view class="tree-content">
          <view class="tree-node"
                v-for="(node, index) in productOptions"
                :key="index">
            <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 class="tree-node"
                    v-for="(child, childIndex) in node.children"
                    :key="childIndex">
                <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 class="tree-node"
                        v-for="(grandchild, grandchildIndex) in child.children"
                        :key="grandchildIndex">
                    <view class="tree-node-header"
                          @click="selectTreeNode(grandchild)">
                      <text class="tree-node-icon-placeholder"></text>
                      <text class="tree-node-label">{{ grandchild.label }}</text>
                      <up-icon name="checkmark"
                               v-if="form.productId == grandchild.value"
                               class="tree-node-check" />
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted, nextTick } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  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 {
    qualityInspectAdd,
    qualityInspectUpdate,
    qualityInspectParamInfo,
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
    list,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¡¨å•引用
  const formRef = ref(null);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // å·¥åºé€‰æ‹©
  const showprocessSheet = ref(false);
  // äº§å“é€‰æ‹©
  const showProductSheet = ref(false);
  // äº§å“æ ‘形选择器
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
  const showTestStandardSheet = ref(false);
  // è¡¨å•数据
  const form = ref({
    checkTime: dayjs().format("YYYY-MM-DD"),
    process: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
  // æ˜¾ç¤ºç”¨çš„变量
  const testStandardDisplay = ref("");
  // æ£€éªŒé¡¹ç›®
  const tableData = ref([]);
  const tableLoading = ref(false);
  // å·¥åºåˆ—表
  const processList = ref([]);
  // äº§å“é€‰é¡¹
  const productOptions = ref([]);
  // åž‹å·é€‰é¡¹
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
  const currentProductId = ref(0);
  // ActionSheet选项
  const processOptions = computed(() => {
    return processList.value.map(item => ({
      name: item.name,
      value: item.name,
    }));
  });
  const productSheetOptions = computed(() => {
    return productOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const modelSheetOptions = computed(() => {
    return modelOptions.value.map(item => ({
      name: item.model,
      value: item.id,
    }));
  });
  const resultSheetOptions = computed(() => {
    return resultOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  const testStandardSheetOptions = computed(() => {
    return testStandardOptions.value.map(item => ({
      name: item.standardName || item.standardNo,
      value: item.id,
    }));
  });
  // è¡¨å•验证规则
  const rules = {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    process: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
  const isEdit = computed(() => {
    const id = getPageId();
    return !!id;
  });
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
  // é€‰æ‹©å·¥åº
  const selectprocess = e => {
    form.value.process = e.value;
    showprocessSheet.value = false;
  };
  // é€‰æ‹©äº§å“
  const selectProduct = e => {
    form.value.productId = e.value;
    form.value.productName = e.name;
    showProductSheet.value = false;
    getModels(e.value);
  };
  // åˆ‡æ¢æ ‘形节点展开/折叠
  const toggleNode = node => {
    node.expanded = !node.expanded;
  };
  // é€‰æ‹©æ ‘形节点
  const selectTreeNode = node => {
    // ç¡®ä¿åªé€‰æ‹©æœ«ç«¯èŠ‚ç‚¹
    if (!node.children || node.children.length == 0) {
      form.value.productId = node.value;
      form.value.productName = node.label;
      showProductTree.value = false;
      getModels(node.value);
    }
  };
  // è½¬æ¢äº§å“æ ‘结构
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // æ ¹æ®ID查找节点
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  // é€‰æ‹©è§„格型号
  const selectModel = e => {
    form.value.productModelId = e.value;
    showModelSheet.value = false;
    handleChangeModel(e.value);
  };
  // å¤„理型号变化
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  // é€‰æ‹©æ£€æµ‹ç»“æžœ
  const selectResult = e => {
    form.value.checkResult = e.value;
    showResultSheet.value = false;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    form.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // é€‰æ‹©æŒ‡æ ‡
  const selectTestStandard = e => {
    form.value.testStandardId = e.value;
    testStandardDisplay.value = e.name;
    showTestStandardSheet.value = false;
    handleTestStandardChange(e.value);
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    showTestStandardSheet.value = true;
  };
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getprocessList = () => {
    list().then(res => {
      processList.value = res.data;
    });
  };
  // èŽ·å–äº§å“é€‰é¡¹
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // èŽ·å–åž‹å·åˆ—è¡¨
  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;
    });
    if (currentProductId.value) {
      getList();
    }
  };
  // èŽ·å–æŒ‡æ ‡åˆ—è¡¨
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    let params = {
      productId: currentProductId.value,
      inspectType: 2,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
      testStandardDisplay.value = "";
    });
  };
  // èŽ·å–æ£€éªŒå‚æ•°åˆ—è¡¨ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
        showToast("请选择规格型号");
        return;
      }
      // if (!form.value.process) {
      //   showToast("请选择工序");
      //   return;
      // }
      if (!form.value.quantity) {
        showToast("请输入数量");
        return;
      }
      if (!form.value.productId) {
        showToast("请选择产品");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 2;
      if (isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
    } finally {
      loading.value = false;
    }
  };
  // åˆå§‹åŒ–表单
  const initForm = async () => {
    const id = getPageId();
    if (id) {
      // ç¼–辑模式,加载数据
      // å…ˆé‡ç½®è¡¨å•数据
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
      testStandardOptions.value = [];
      tableData.value = [];
      // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
      await getProductOptions();
      // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–ç¼–è¾‘æ•°æ®
      const row = uni.getStorageSync("finalInspectionEditData") || {
        id: id,
        checkTime: "2026-03-03",
        process: "上海金属材料有限公司",
        checkName: "张三",
        productName: "不锈钢板材",
        productId: 1,
        productModelId: 1,
        model: "304",
        testStandardId: "1",
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        productMainId: null,
        purchaseLedgerId: null,
      };
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      form.value = { ...row };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 2,
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“
          nextTick(() => {
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…
              const matchedOption = testStandardOptions.value.find(
                item =>
                  item.id == savedTestStandardId ||
                  String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id
                form.value.testStandardId = matchedOption.id;
                testStandardDisplay.value =
                  matchedOption.standardName || matchedOption.standardNo;
                // ç¼–辑保留原检验值,直接拉取原参数数据
                getQualityInspectParamList(row.id);
              } else {
                // å¦‚果找不到匹配项,尝试直接使用原值
                console.warn(
                  "未找到匹配的指标选项,testStandardId:",
                  savedTestStandardId
                );
                form.value.testStandardId = savedTestStandardId;
                getQualityInspectParamList(row.id);
              }
            } else {
              // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
              getQualityInspectParamList(row.id);
            }
          });
        });
      }
      // å±•开产品树到当前选中的节点
      expandProductTree(productOptions.value, row.productId);
    } else {
      // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–表单
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
    }
  };
  // å±•开产品树到指定节点
  const expandProductTree = (nodes, targetId) => {
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.value === targetId) {
        return true; // æ‰¾åˆ°ç›®æ ‡èŠ‚ç‚¹
      }
      if (node.children && node.children.length > 0) {
        const found = expandProductTree(node.children, targetId);
        if (found) {
          node.expanded = true; // å±•开父节点
          return true;
        }
      }
    }
    return false;
  };
  onMounted(() => {
    getprocessList();
    getProductOptions();
    getUserList();
    initForm();
  });
  onShow(() => {
    initForm();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .material-inspection-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  // æ£€éªŒé¡¹ç›®å®¹å™¨
  .inspection-items-container {
    padding: 20px;
    background-color: #fff;
  }
  .steps-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e4e7ed;
  }
  .steps-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .steps-count {
    font-size: 14px;
    color: #909399;
  }
  .steps-list {
    margin-bottom: 20px;
  }
  .exec-step-item {
    position: relative;
    display: flex;
    margin-bottom: 16px;
    padding: 16px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    transition: all 0.3s ease;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  }
  .exec-step-item:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-1px);
  }
  .delete-btn {
    position: absolute;
    top: -25rpx;
    right: -25rpx;
    width: 50rpx;
    height: 50rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 20px;
    border-radius: 50%;
    background-color: red;
    border: none;
    z-index: 10;
  }
  .delete-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(245, 108, 108, 0.4);
  }
  .step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-right: 16px;
    background-color: #ecf5ff;
    color: #409eff;
    font-size: 14px;
    font-weight: 600;
    border-radius: 50%;
    flex-shrink: 0;
  }
  .step-content {
    flex: 1;
    min-width: 0;
  }
  .step-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .step-row:last-child {
    margin-bottom: 0;
  }
  .step-label {
    display: inline-block;
    width: 80px;
    font-size: 14px;
    color: #606266;
    margin-right: 12px;
    flex-shrink: 0;
    line-height: 36px;
  }
  .step-input {
    flex: 1;
    min-width: 0;
  }
  .step-input input {
    font-size: 14px;
    color: #303133;
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 44px;
    line-height: 44px;
    font-size: 14px;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 8px;
  }
  .add-step-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  }
  .add-step-btn text {
    font-size: 14px;
  }
  // åº•部按钮
  .bottom-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 16px 20px;
    background: #ffffff;
    border-top: 1px solid #f0f0f0;
    gap: 16px;
  }
  .bottom-btn {
    flex: 1;
  }
  // æ ‘形选择器样式
  .tree-selector {
    width: 100%;
    max-height: 70vh;
    background: #ffffff;
    border-radius: 16px 16px 0 0;
  }
  .tree-header {
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    text-align: center;
  }
  .tree-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .tree-content {
    padding: 10px 0;
    max-height: calc(70vh - 60px);
    overflow-y: auto;
  }
  .tree-node {
    padding: 0 20px;
  }
  .tree-node-header {
    display: flex;
    align-items: center;
    padding: 12px 0;
    cursor: pointer;
  }
  .tree-node-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    color: #909399;
  }
  .tree-node-icon-placeholder {
    width: 20px;
    height: 20px;
    margin-right: 8px;
  }
  .tree-node-label {
    flex: 1;
    font-size: 14px;
    color: #303133;
  }
  .tree-node-check {
    width: 20px;
    height: 20px;
    color: #409eff;
  }
  .tree-node-children {
    margin-left: 28px;
  }
</style>
src/pages/qualityManagement/finalInspection/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,422 @@
<template>
  <view class="material-inspection-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="出厂检验详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-section"
          v-if="detailData">
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon">
            <up-icon name="file-text"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
            </u-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="detail-content">
          <view class="detail-row">
            <text class="detail-label">检测日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.checkTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">生产工单号</text>
            <text class="detail-value">{{ detailData.workOrderNo || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">工序</text>
            <text class="detail-value">{{ detailData.process || '-' }}</text>
          </view> -->
          <view class="detail-row">
            <text class="detail-label">检验员</text>
            <text class="detail-value">{{ detailData.checkName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ detailData.productName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ detailData.model || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ detailData.unit || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">数量</text>
            <text class="detail-value">{{ detailData.quantity || 0 }}</text>
          </view>
          <view class="detail-row">
            <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.testStandardName || '-' }}</text>
          </view> -->
        </view>
      </view>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="detail-card"
            v-if="inspectionItems.length > 0">
        <view class="card-header">
          <view class="header-icon secondary">
            <up-icon name="list"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">检验项目</text>
        </view>
        <up-divider></up-divider>
        <view class="inspection-items">
          <view v-for="(item, index) in inspectionItems"
                :key="index"
                class="inspection-item">
            <text class="item-name">{{ item.parameterItem || '检验项目' }}</text>
            <view class="item-details">
              <view class="item-detail-row">
                <text class="item-detail-label">单位:</text>
                <text class="item-detail-value">{{ item.unit || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">标准值:</text>
                <text class="item-detail-value">{{ item.standardValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">内控值:</text>
                <text class="item-detail-value">{{ item.controlValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">检验值:</text>
                <text class="item-detail-value result"
                      :class="getResultClass(item.testValue, item.standardValue)">
                  {{ item.testValue || '-' }}
                </text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <!-- æ“ä½œæŒ‰é’® -->
      <!-- <view class="action-buttons">
        <u-button type="primary"
                  class="action-btn"
                  @click="downloadReport">
          ä¸‹è½½æŠ¥å‘Š
        </u-button>
      </view> -->
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验详情"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¯¦æƒ…数据
  const detailData = ref(null);
  // æ£€éªŒé¡¹ç›®
  const inspectionItems = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = date => {
    if (!date) return "";
    return dayjs(date).format("YYYY-MM-DD");
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = state => {
    return state ? "success" : "warning";
  };
  // èŽ·å–ç»“æžœæ ·å¼
  const getResultClass = (testValue, standardValue) => {
    // ç®€å•的结果判断逻辑,实际项目中可能需要更复杂的判断
    if (testValue === "合格") {
      return "result-passed";
    } else if (testValue === "不合格") {
      return "result-rejected";
    }
    return "";
  };
  // ä¸‹è½½æŠ¥å‘Š
  const downloadReport = () => {
    uni.showToast({
      title: "报告下载中...",
      icon: "loading",
    });
    // æ¨¡æ‹Ÿä¸‹è½½
    setTimeout(() => {
      uni.showToast({
        title: "报告下载成功",
        icon: "success",
      });
    }, 1500);
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // èŽ·å–è¯¦æƒ…æ•°æ®
  const getDetail = () => {
    const id = getPageId();
    if (!id) {
      showToast("参数错误");
      return;
    }
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–è¯¦æƒ…æ•°æ®
    try {
      const detailDataFromStorage = uni.getStorageSync("finalInspectionEditData");
      if (detailDataFromStorage) {
        detailData.value = detailDataFromStorage;
        console.log(detailData.value, "detailData.value");
        // ä½¿ç”¨qualityInspectParamInfo获取检验项目
        qualityInspectParamInfo(id)
          .then(res => {
            if (res.data && res.data.length > 0) {
              inspectionItems.value = res.data;
            } else if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              // å¦‚果接口没有返回数据,使用本地存储中的数据
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          })
          .catch(error => {
            console.error("获取检验项目失败:", error);
            // æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¸­çš„æ•°æ®æˆ–模拟数据
            if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          });
      }
    } catch (error) {
      console.error("加载详情数据失败:", error);
      showToast("加载详情数据失败,请重试");
    }
  };
  onShow(() => {
    getDetail();
  });
  onMounted(() => {
    getDetail();
  });
</script>
<style scoped lang="scss">
  .material-inspection-detail {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 20px;
  }
  .detail-section {
    padding: 15px;
  }
  .detail-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
  }
  .header-icon {
    width: 40px;
    height: 40px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
  }
  .header-icon.secondary {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  }
  .header-icon.tertiary {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  }
  .header-title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .status-tag {
    margin-left: 20rpx;
  }
  .detail-content {
    padding-top: 8px;
  }
  .detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .detail-row:last-child {
    border-bottom: none;
  }
  .detail-label {
    font-size: 14px;
    color: #666;
    min-width: 100px;
  }
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  // æ£€éªŒé¡¹ç›®
  .inspection-items {
    padding-top: 8px;
  }
  .inspection-item {
    padding: 12px;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 10px;
  }
  .inspection-item:last-child {
    margin-bottom: 0;
  }
  .item-name {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f0f0;
  }
  .item-details {
    padding-top: 8px;
  }
  .item-detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 6px;
  }
  .item-detail-row:last-child {
    margin-bottom: 0;
  }
  .item-detail-label {
    font-size: 12px;
    color: #666;
    min-width: 60px;
  }
  .item-detail-value {
    font-size: 12px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  .item-detail-value.result {
    font-weight: 500;
  }
  .result-passed {
    color: #67c23a;
  }
  .result-rejected {
    color: #f56c6c;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
</style>
src/pages/qualityManagement/finalInspection/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,566 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    qualityInspectFileAdd,
    qualityInspectFileListPage,
    qualityInspectFileDel,
  } from "@/api/qualityManagement/materialInspection";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                inspectId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              qualityInspectFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    qualityInspectFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "qualityInspectFileId"
    );
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    qualityInspectFileListPage({
      inspectId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/qualityManagement/finalInspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,776 @@
<template>
  <view class="material-inspection-page">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="出厂检验"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <up-search
        placeholder="请输入产品名称搜索"
        v-model="searchForm.productName"
        @search="getList"
        @custom="getList"
        @clear="getList"
        :show-action="true"
        action-text="搜索"
        :animation="true"
      ></up-search>
    </view>
    <!-- ç»Ÿè®¡ä¿¡æ¯å¡ç‰‡ -->
    <!-- <view class="stats-cards">
      <view class="stat-card">
        <text class="stat-number">{{ totalCount }}</text>
        <text class="stat-label">总检验</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ submittedCount }}</text>
        <text class="stat-label">已提交</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ pendingCount }}</text>
        <text class="stat-label">待提交</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ qualifiedCount }}</text>
        <text class="stat-label">已合格</text>
      </view>
    </view> -->
    <!-- æ£€éªŒåˆ—表 -->
    <view class="inspection-list"
          v-if="inspectionList.length > 0">
      <view v-for="(item, index) in inspectionList"
            :key="index">
        <view class="inspection-item"
              @click="viewDetail(item)">
          <view class="item-header">
            <view class="item-left">
              <!-- <view class="material-icon"
                    :class="getStateClass(item.inspectState)">
                <up-icon :name="getStateIcon(item.inspectState)"
                         size="16"
                         color="#ffffff"></up-icon>
              </view> -->
              <view class="material-info">
                <text class="material-name">{{ item.productName }}</text>
                <text class="material-code">{{ item.model }}</text>
              </view>
            </view>
            <view class="status-tags">
              <u-tag :type="getTagType(item.checkResult)"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
              </u-tag>
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
                     class="status-tag">
                {{ item.inspectState ? '已提交' : '未提交' }}
              </u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">检测日期</text>
              <text class="detail-value">{{ formatDateTime(item.checkTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产工单号</text>
              <text class="detail-value">{{ item.workOrderNo || '-' }}</text>
            </view>
            <!-- <view class="detail-row">
              <text class="detail-label">工序</text>
              <text class="detail-value">{{ item.process || '-' }}</text>
            </view> -->
            <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">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <!-- <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button> -->
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button> -->
          </view>
          <view class="action-buttons">
            <!-- <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
              é™„ä»¶
            </u-button>
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button> -->
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验任务"></up-empty>
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <!-- <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              :range="true"
              @confirm="confirmDate" />
    <!-- åˆ†é…æ£€éªŒå‘˜å¼¹çª— -->
    <up-popup v-model:show="showAssignDialog"
              mode="center"
              round
              style="width: 80%">
      <view class="assign-dialog">
        <view class="dialog-header">
          <text class="dialog-title">分配检验员</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showAssignDialog = false"></up-icon>
        </view>
        <view class="dialog-content">
          <up-form-item label="检验员"
                        prop="checkName"
                        :label-width="60"
                        required>
            <up-input v-model="assignForm.checkName"
                      placeholder="请选择检验员"
                      readonly />
            <template #right>
              <up-icon @click="showInspectorSheet = true"
                       name="arrow-right" />
            </template>
          </up-form-item>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="showAssignDialog = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="submitAssign">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import {
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // æœç´¢è¡¨å•
  const searchForm = ref({
    productName: "",
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
  });
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  const showAssignDialog = ref(false);
  const showInspectorSheet = ref(false);
  const assignForm = ref({
    checkName: "",
  });
  const currentAssignRow = ref(null);
  // æ£€éªŒåˆ—表数据
  const inspectionList = ref([]);
  // åˆ†é¡µæ•°æ®
  const page = ref({
    current: -1,
    size: -1,
    total: 0,
  });
  // åŠ è½½çŠ¶æ€
  const tableLoading = ref(false);
  // ç»Ÿè®¡æ•°æ®
  const totalCount = ref(0);
  const submittedCount = ref(0);
  const pendingCount = ref(0);
  const qualifiedCount = ref(0);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // ActionSheet选项
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  // èŽ·å–çŠ¶æ€æ ·å¼
  const getStateClass = inspectState => {
    return inspectState ? "state-submitted" : "state-pending";
  };
  // èŽ·å–çŠ¶æ€å›¾æ ‡
  const getStateIcon = inspectState => {
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = inspectState => {
    return inspectState ? "success" : "warning";
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    searchForm.value.entryDate = e.value;
    searchForm.value.entryDateStart = dayjs(e.value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(e.value[1]).format("YYYY-MM-DD");
    getList();
  };
  const viewFileList = item => {
    uni.setStorageSync("qualityInspectFileId", item.id);
    uni.navigateTo({
      url: "/pages/qualityManagement/finalInspection/fileList",
    });
  };
  // æ¸…除日期范围
  const clearDateRange = () => {
    searchForm.value.entryDate = undefined;
    searchForm.value.entryDateStart = undefined;
    searchForm.value.entryDateEnd = undefined;
    getList();
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 2 })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
        page.value.total = res.data.total || 0;
        totalCount.value = res.data.total || 0;
        submittedCount.value = inspectionList.value.filter(
          item => item.inspectState
        ).length;
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        showToast("获取列表失败,请重试");
      });
  };
  // ç¼–辑检验
  const startInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    console.log(item, "item");
    // å­˜å‚¨å®Œæ•´çš„æ£€éªŒæ•°æ®
    uni.setStorageSync("finalInspectionEditData", item);
    // è·³è½¬åˆ°ç¼–辑页面
    uni.navigateTo({
      url: `/pages/qualityManagement/finalInspection/add?id=${item.id}&isEdit=true`,
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    uni.setStorageSync("finalInspectionEditData", item);
    // è·³è½¬åˆ°è¯¦æƒ…页面
    uni.navigateTo({
      url: `/pages/qualityManagement/finalInspection/detail?id=${item.id}`,
    });
  };
  // æ–°å¢žæ£€éªŒ
  const addInspection = () => {
    uni.navigateTo({
      url: "/pages/qualityManagement/finalInspection/add",
    });
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async 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("提交失败,请重试");
    }
  };
  // åˆ†é…æ£€éªŒå‘˜
  const assignInspector = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    currentAssignRow.value = item;
    getUserList();
    showAssignDialog.value = true;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    assignForm.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // æäº¤åˆ†é…
  const submitAssign = async () => {
    if (!currentAssignRow.value || !assignForm.value.checkName) {
      showToast("请选择检验员");
      return;
    }
    try {
      const data = {
        ...assignForm.value,
        id: currentAssignRow.value.id,
      };
      const res = await qualityInspectUpdate(data);
      if (res.code === 200) {
        showToast("分配成功");
        showAssignDialog.value = false;
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("分配失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("分配失败:", error);
      showToast("分配失败,请重试");
    }
  };
  // å¤„理分页
  const handlePagination = obj => {
    page.value.current = obj.current;
    page.value.size = obj.size;
    getList();
  };
  onMounted(() => {
    getList();
    getUserList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .material-inspection-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  // æœç´¢åŒºåŸŸ
  .search-section {
    padding: 20rpx 30rpx;
    background-color: #ffffff;
    position: sticky;
    top: 0;
    z-index: 10;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .stats-cards {
    display: flex;
    padding: 15px;
    gap: 10px;
    background: #fff;
    margin-bottom: 10px;
  }
  .stat-card {
    flex: 1;
    background: #2979ff;
    border-radius: 12px;
    padding: 15px;
    text-align: center;
    color: #fff;
    box-shadow: 0 2px 8px rgba(41, 121, 255, 0.2);
  }
  .stat-number {
    display: block;
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 5px;
  }
  .stat-label {
    font-size: 12px;
    opacity: 0.9;
  }
  // æ£€éªŒåˆ—表
  .inspection-list {
    padding: 20px;
  }
  .inspection-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding: 0 16px;
    &:active {
      transform: scale(0.98);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    }
  }
  .item-header {
    padding: 16px 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .item-left {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .material-icon {
    width: 24px;
    height: 24px;
    background: #2979ff;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .state-pending {
    background: #ff9900;
  }
  .state-submitted {
    background: #52c41a;
  }
  .material-info {
    flex: 1;
  }
  .material-name {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  .material-code {
    font-size: 12px;
    color: #999;
    margin-left: 8px;
  }
  .status-tags {
    display: flex;
    gap: 8px;
  }
  .status-tag {
    margin: 0;
  }
  .date-range {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 10px;
    padding: 8px 12px;
    background: #f8f9fa;
    border-radius: 8px;
  }
  .date-text {
    font-size: 12px;
    color: #666;
  }
  // è¯¦æƒ…行
  .item-details {
    padding: 16px 0;
  }
  .detail-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 8px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .detail-label {
    font-size: 12px;
    color: #777777;
    min-width: 60px;
  }
  .detail-value {
    font-size: 12px;
    color: #000000;
    text-align: right;
    flex: 1;
    margin-left: 16px;
  }
  // æ“ä½œæŒ‰é’®
  .action-buttons {
    display: flex;
    gap: 12px;
    padding: 0 0 16px 0;
    justify-content: space-between;
  }
  .action-btn {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
  // æµ®åŠ¨æŒ‰é’®
  .fab-button {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 56px;
    height: 56px;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
    z-index: 1000;
  }
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  .assign-dialog {
    padding: 24px;
    background: #ffffff;
    border-radius: 16px;
    overflow: hidden;
  }
  .dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid #f0f0f0;
  }
  .dialog-title {
    font-size: 18px;
    font-weight: 600;
    color: #303133;
  }
  .dialog-content {
    margin-bottom: 24px;
  }
  .dialog-footer {
    display: flex;
    gap: 16px;
    padding-top: 16px;
    border-top: 1px solid #f0f0f0;
  }
  .footer-btn {
    flex: 1;
    height: 44px;
    font-size: 16px;
  }
  // è¾“入框样式
  :deep(.up-input__inner) {
    border-radius: 8px;
    height: 44px;
    font-size: 14px;
  }
  // è¡¨å•项样式
  :deep(.up-form-item) {
    margin-bottom: 0;
  }
  :deep(.up-form-item__label) {
    font-size: 14px;
    color: #606266;
    margin-bottom: 8px;
  }
  // æŒ‰é’®æ ·å¼
  :deep(.up-button--primary) {
    border-radius: 8px;
  }
  :deep(.up-button--default) {
    border-radius: 8px;
  }
  // åˆ†é¡µç»„ä»¶
  .pagination {
    padding: 20px;
    background: #fff;
    margin-top: 10px;
    display: flex;
    justify-content: center;
  }
</style>
src/pages/qualityManagement/materialInspection/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1134 @@
<template>
  <view class="material-inspection-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="isEdit ? '编辑原材料检验' : '新增原材料检验'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <up-form :model="form"
             ref="formRef"
             label-width="110"
             :rules="rules">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <up-form-item label="供应商"
                    prop="supplier"
                    required
                    border-bottom>
        <up-input v-model="form.supplier"
                  placeholder="请选择供应商"
                  readonly
                  :disabled="supplierQuantityDisabled" />
        <template #right>
          <up-icon @click="showSupplierSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="产品名称"
                    prop="productId"
                    required
                    border-bottom>
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showProductTree = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="规格型号"
                    prop="productModelId"
                    required
                    border-bottom>
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showModelSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit"
                    border-bottom>
        <up-input v-model="form.unit"
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="supplierQuantityDisabled" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
                    prop="checkResult"
                    required
                    border-bottom>
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检验员"
                    prop="checkName"
                    border-bottom>
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检测日期"
                    prop="checkTime"
                    required
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
        </template> -->
      </up-form-item>
      <!-- <up-form-item label="采购订单号"
                    prop="purchaseContractNo"
                    border-bottom>
        <up-input v-model="form.purchaseContractNo"
                  placeholder="请输入采购订单号"
                  clearable />
      </up-form-item> -->
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="inspection-items-container">
        <view class="steps-header">
          <text class="steps-title">检验项目</text>
          <text class="steps-count">共 {{ tableData.length }} ä¸ªé¡¹ç›®</text>
        </view>
        <view class="steps-list">
          <view v-for="(item, index) in tableData"
                :key="index"
                class="exec-step-item">
            <view class="step-number">
              {{ index + 1 }}
            </view>
            <view class="step-content">
              <view class="step-row">
                <text class="step-label">指标:</text>
                <text class="step-value">{{ item.parameterItem }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">单位:</text>
                <text class="step-value">{{ item.unit }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">标准值:</text>
                <text class="step-value">{{ item.standardValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">内控值:</text>
                <text class="step-value">{{ item.controlValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          clearable
                          border-bottom
                          class="step-input" />
              </view>
            </view>
          </view>
          <view v-if="tableData.length === 0"
                class="empty-data">
            <text>请先选择指标</text>
          </view>
        </view>
      </view>
    </up-form>
    <!-- åº•部按钮 -->
    <view class="bottom-buttons">
      <up-button type="default"
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
      </up-button>
      <up-button type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <!-- ä¾›åº”商选择 -->
    <up-action-sheet :show="showSupplierSheet"
                     :actions="supplierOptions"
                     @select="selectSupplier"
                     @close="showSupplierSheet = false"
                     title="选择供应商" />
    <!-- äº§å“é€‰æ‹© -->
    <up-action-sheet :show="showProductSheet"
                     :actions="productSheetOptions"
                     @select="selectProduct"
                     @close="showProductSheet = false"
                     title="选择产品" />
    <!-- è§„格型号选择 -->
    <up-action-sheet :show="showModelSheet"
                     :actions="modelSheetOptions"
                     @select="selectModel"
                     @close="showModelSheet = false"
                     title="选择规格型号" />
    <!-- æ£€æµ‹ç»“果选择 -->
    <up-action-sheet :show="showResultSheet"
                     :actions="resultSheetOptions"
                     @select="selectResult"
                     @close="showResultSheet = false"
                     title="选择检测结果" />
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     @close="showInspectorSheet = false"
                     title="选择检验员" />
    <!-- æŒ‡æ ‡é€‰æ‹© -->
    <up-action-sheet :show="showTestStandardSheet"
                     :actions="testStandardSheetOptions"
                     @select="selectTestStandard"
                     @close="showTestStandardSheet = false"
                     title="选择指标" />
    <!-- äº§å“æ ‘形选择器 -->
    <up-popup v-model:show="showProductTree"
              position="bottom"
              :round="true"
              :closeable="true"
              @close="showProductTree = false">
      <view class="tree-selector">
        <view class="tree-header">
          <text class="tree-title">选择产品</text>
        </view>
        <view class="tree-content">
          <view class="tree-node"
                v-for="(node, index) in productOptions"
                :key="index">
            <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 class="tree-node"
                    v-for="(child, childIndex) in node.children"
                    :key="childIndex">
                <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 class="tree-node"
                        v-for="(grandchild, grandchildIndex) in child.children"
                        :key="grandchildIndex">
                    <view class="tree-node-header"
                          @click="selectTreeNode(grandchild)">
                      <text class="tree-node-icon-placeholder"></text>
                      <text class="tree-node-label">{{ grandchild.label }}</text>
                      <up-icon name="checkmark"
                               v-if="form.productId == grandchild.value"
                               class="tree-node-check" />
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted, nextTick } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  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 {
    qualityInspectAdd,
    qualityInspectUpdate,
    qualityInspectParamInfo,
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¡¨å•引用
  const formRef = ref(null);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // ä¾›åº”商选择
  const showSupplierSheet = ref(false);
  // äº§å“é€‰æ‹©
  const showProductSheet = ref(false);
  // äº§å“æ ‘形选择器
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
  const showTestStandardSheet = ref(false);
  // è¡¨å•数据
  const form = ref({
    checkTime: dayjs().format("YYYY-MM-DD"),
    supplier: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
  // æ˜¾ç¤ºç”¨çš„变量
  const testStandardDisplay = ref("");
  // æ£€éªŒé¡¹ç›®
  const tableData = ref([]);
  const tableLoading = ref(false);
  // ä¾›åº”商列表
  const supplierList = ref([]);
  // äº§å“é€‰é¡¹
  const productOptions = ref([]);
  // åž‹å·é€‰é¡¹
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
  const currentProductId = ref(0);
  // ActionSheet选项
  const supplierOptions = computed(() => {
    return supplierList.value.map(item => ({
      name: item.supplierName,
      value: item.supplierName,
    }));
  });
  const productSheetOptions = computed(() => {
    return productOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const modelSheetOptions = computed(() => {
    return modelOptions.value.map(item => ({
      name: item.model,
      value: item.id,
    }));
  });
  const resultSheetOptions = computed(() => {
    return resultOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  const testStandardSheetOptions = computed(() => {
    return testStandardOptions.value.map(item => ({
      name: item.standardName || item.standardNo,
      value: item.id,
    }));
  });
  // è¡¨å•验证规则
  const rules = {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    supplier: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
  const isEdit = computed(() => {
    const id = getPageId();
    return !!id;
  });
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™ä¾›åº”商、数量置灰
  const supplierQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
  // é€‰æ‹©ä¾›åº”商
  const selectSupplier = e => {
    form.value.supplier = e.value;
    showSupplierSheet.value = false;
  };
  // é€‰æ‹©äº§å“
  const selectProduct = e => {
    form.value.productId = e.value;
    form.value.productName = e.name;
    showProductSheet.value = false;
    getModels(e.value);
  };
  // åˆ‡æ¢æ ‘形节点展开/折叠
  const toggleNode = node => {
    node.expanded = !node.expanded;
  };
  // é€‰æ‹©æ ‘形节点
  const selectTreeNode = node => {
    // ç¡®ä¿åªé€‰æ‹©æœ«ç«¯èŠ‚ç‚¹
    if (!node.children || node.children.length == 0) {
      form.value.productId = node.value;
      form.value.productName = node.label;
      showProductTree.value = false;
      getModels(node.value);
    }
  };
  // è½¬æ¢äº§å“æ ‘结构
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // æ ¹æ®ID查找节点
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  // é€‰æ‹©è§„格型号
  const selectModel = e => {
    form.value.productModelId = e.value;
    showModelSheet.value = false;
    handleChangeModel(e.value);
  };
  // å¤„理型号变化
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  // é€‰æ‹©æ£€æµ‹ç»“æžœ
  const selectResult = e => {
    form.value.checkResult = e.value;
    showResultSheet.value = false;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    form.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // é€‰æ‹©æŒ‡æ ‡
  const selectTestStandard = e => {
    form.value.testStandardId = e.value;
    testStandardDisplay.value = e.name;
    showTestStandardSheet.value = false;
    handleTestStandardChange(e.value);
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    showTestStandardSheet.value = true;
  };
  // èŽ·å–ä¾›åº”å•†åˆ—è¡¨
  const getSuppliers = () => {
    getOptions().then(res => {
      supplierList.value = res.data;
    });
  };
  // èŽ·å–äº§å“é€‰é¡¹
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // èŽ·å–åž‹å·åˆ—è¡¨
  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;
    });
    if (currentProductId.value) {
      getList();
    }
  };
  // èŽ·å–æŒ‡æ ‡åˆ—è¡¨
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    let params = {
      productId: currentProductId.value,
      inspectType: 0,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
      testStandardDisplay.value = "";
    });
  };
  // èŽ·å–æ£€éªŒå‚æ•°åˆ—è¡¨ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
        showToast("请选择规格型号");
        return;
      }
      if (!form.value.supplier) {
        showToast("请选择供应商");
        return;
      }
      if (!form.value.quantity) {
        showToast("请输入数量");
        return;
      }
      if (!form.value.productId) {
        showToast("请选择产品");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 0;
      if (isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
    } finally {
      loading.value = false;
    }
  };
  // åˆå§‹åŒ–表单
  const initForm = async () => {
    const id = getPageId();
    if (id) {
      // ç¼–辑模式,加载数据
      // å…ˆé‡ç½®è¡¨å•数据
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        supplier: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
      testStandardOptions.value = [];
      tableData.value = [];
      // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
      await getProductOptions();
      // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–ç¼–è¾‘æ•°æ®
      const row = uni.getStorageSync("inspectionEditData") || {
        id: id,
        checkTime: "2026-03-03",
        supplier: "上海金属材料有限公司",
        checkName: "张三",
        productName: "不锈钢板材",
        productId: 1,
        productModelId: 1,
        model: "304",
        testStandardId: "1",
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        productMainId: null,
        purchaseLedgerId: null,
      };
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      form.value = { ...row };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 0,
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“
          nextTick(() => {
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…
              const matchedOption = testStandardOptions.value.find(
                item =>
                  item.id == savedTestStandardId ||
                  String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id
                form.value.testStandardId = matchedOption.id;
                testStandardDisplay.value =
                  matchedOption.standardName || matchedOption.standardNo;
                // ç¼–辑保留原检验值,直接拉取原参数数据
                getQualityInspectParamList(row.id);
              } else {
                // å¦‚果找不到匹配项,尝试直接使用原值
                console.warn(
                  "未找到匹配的指标选项,testStandardId:",
                  savedTestStandardId
                );
                form.value.testStandardId = savedTestStandardId;
                getQualityInspectParamList(row.id);
              }
            } else {
              // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
              getQualityInspectParamList(row.id);
            }
          });
        });
      }
      // å±•开产品树到当前选中的节点
      expandProductTree(productOptions.value, row.productId);
    } else {
      // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–表单
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        supplier: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
    }
  };
  // å±•开产品树到指定节点
  const expandProductTree = (nodes, targetId) => {
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.value === targetId) {
        return true; // æ‰¾åˆ°ç›®æ ‡èŠ‚ç‚¹
      }
      if (node.children && node.children.length > 0) {
        const found = expandProductTree(node.children, targetId);
        if (found) {
          node.expanded = true; // å±•开父节点
          return true;
        }
      }
    }
    return false;
  };
  onMounted(() => {
    getSuppliers();
    getProductOptions();
    getUserList();
    initForm();
  });
  onShow(() => {
    initForm();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .material-inspection-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  // æ£€éªŒé¡¹ç›®å®¹å™¨
  .inspection-items-container {
    padding: 20px;
    background-color: #fff;
  }
  .steps-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e4e7ed;
  }
  .steps-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .steps-count {
    font-size: 14px;
    color: #909399;
  }
  .steps-list {
    margin-bottom: 20px;
  }
  .exec-step-item {
    position: relative;
    display: flex;
    margin-bottom: 16px;
    padding: 16px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    transition: all 0.3s ease;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  }
  .exec-step-item:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-1px);
  }
  .delete-btn {
    position: absolute;
    top: -25rpx;
    right: -25rpx;
    width: 50rpx;
    height: 50rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 20px;
    border-radius: 50%;
    background-color: red;
    border: none;
    z-index: 10;
  }
  .delete-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(245, 108, 108, 0.4);
  }
  .step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-right: 16px;
    background-color: #ecf5ff;
    color: #409eff;
    font-size: 14px;
    font-weight: 600;
    border-radius: 50%;
    flex-shrink: 0;
  }
  .step-content {
    flex: 1;
    min-width: 0;
  }
  .step-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .step-row:last-child {
    margin-bottom: 0;
  }
  .step-label {
    display: inline-block;
    width: 80px;
    font-size: 14px;
    color: #606266;
    margin-right: 12px;
    flex-shrink: 0;
    line-height: 36px;
  }
  .step-input {
    flex: 1;
    min-width: 0;
  }
  .step-input input {
    font-size: 14px;
    color: #303133;
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 44px;
    line-height: 44px;
    font-size: 14px;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 8px;
  }
  .add-step-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  }
  .add-step-btn text {
    font-size: 14px;
  }
  // åº•部按钮
  .bottom-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 16px 20px;
    background: #ffffff;
    border-top: 1px solid #f0f0f0;
    gap: 16px;
  }
  .bottom-btn {
    flex: 1;
  }
  // æ ‘形选择器样式
  .tree-selector {
    width: 100%;
    max-height: 70vh;
    background: #ffffff;
    border-radius: 16px 16px 0 0;
  }
  .tree-header {
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    text-align: center;
  }
  .tree-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .tree-content {
    padding: 10px 0;
    max-height: calc(70vh - 60px);
    overflow-y: auto;
  }
  .tree-node {
    padding: 0 20px;
  }
  .tree-node-header {
    display: flex;
    align-items: center;
    padding: 12px 0;
    cursor: pointer;
  }
  .tree-node-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    color: #909399;
  }
  .tree-node-icon-placeholder {
    width: 20px;
    height: 20px;
    margin-right: 8px;
  }
  .tree-node-label {
    flex: 1;
    font-size: 14px;
    color: #303133;
  }
  .tree-node-check {
    width: 20px;
    height: 20px;
    color: #409eff;
  }
  .tree-node-children {
    margin-left: 28px;
  }
</style>
src/pages/qualityManagement/materialInspection/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,421 @@
<template>
  <view class="material-inspection-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="原材料检验详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-section"
          v-if="detailData">
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon">
            <up-icon name="file-text"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
            </u-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="detail-content">
          <view class="detail-row">
            <text class="detail-label">检测日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.checkTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">采购订单号</text>
            <text class="detail-value">{{ detailData.purchaseContractNo || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">供应商</text>
            <text class="detail-value">{{ detailData.supplier || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检验员</text>
            <text class="detail-value">{{ detailData.checkName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ detailData.productName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ detailData.model || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ detailData.unit || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">数量</text>
            <text class="detail-value">{{ detailData.quantity || 0 }}</text>
          </view>
          <view class="detail-row">
            <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.testStandardName || '-' }}</text>
          </view> -->
        </view>
      </view>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="detail-card"
            v-if="inspectionItems.length > 0">
        <view class="card-header">
          <view class="header-icon secondary">
            <up-icon name="list"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">检验项目</text>
        </view>
        <up-divider></up-divider>
        <view class="inspection-items">
          <view v-for="(item, index) in inspectionItems"
                :key="index"
                class="inspection-item">
            <text class="item-name">{{ item.parameterItem || '检验项目' }}</text>
            <view class="item-details">
              <view class="item-detail-row">
                <text class="item-detail-label">单位:</text>
                <text class="item-detail-value">{{ item.unit || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">标准值:</text>
                <text class="item-detail-value">{{ item.standardValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">内控值:</text>
                <text class="item-detail-value">{{ item.controlValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">检验值:</text>
                <text class="item-detail-value result"
                      :class="getResultClass(item.testValue, item.standardValue)">
                  {{ item.testValue || '-' }}
                </text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <!-- æ“ä½œæŒ‰é’® -->
      <!-- <view class="action-buttons">
        <u-button type="primary"
                  class="action-btn"
                  @click="downloadReport">
          ä¸‹è½½æŠ¥å‘Š
        </u-button>
      </view> -->
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验详情"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¯¦æƒ…数据
  const detailData = ref(null);
  // æ£€éªŒé¡¹ç›®
  const inspectionItems = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = date => {
    if (!date) return "";
    return dayjs(date).format("YYYY-MM-DD");
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = state => {
    return state ? "success" : "warning";
  };
  // èŽ·å–ç»“æžœæ ·å¼
  const getResultClass = (testValue, standardValue) => {
    // ç®€å•的结果判断逻辑,实际项目中可能需要更复杂的判断
    if (testValue === "合格") {
      return "result-passed";
    } else if (testValue === "不合格") {
      return "result-rejected";
    }
    return "";
  };
  // ä¸‹è½½æŠ¥å‘Š
  const downloadReport = () => {
    uni.showToast({
      title: "报告下载中...",
      icon: "loading",
    });
    // æ¨¡æ‹Ÿä¸‹è½½
    setTimeout(() => {
      uni.showToast({
        title: "报告下载成功",
        icon: "success",
      });
    }, 1500);
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // èŽ·å–è¯¦æƒ…æ•°æ®
  const getDetail = () => {
    const id = getPageId();
    if (!id) {
      showToast("参数错误");
      return;
    }
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–è¯¦æƒ…æ•°æ®
    try {
      const detailDataFromStorage = uni.getStorageSync("inspectionEditData");
      if (detailDataFromStorage) {
        detailData.value = detailDataFromStorage;
        // ä½¿ç”¨qualityInspectParamInfo获取检验项目
        qualityInspectParamInfo(id)
          .then(res => {
            if (res.data && res.data.length > 0) {
              inspectionItems.value = res.data;
            } else if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              // å¦‚果接口没有返回数据,使用本地存储中的数据
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          })
          .catch(error => {
            console.error("获取检验项目失败:", error);
            // æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¸­çš„æ•°æ®æˆ–模拟数据
            if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          });
      }
    } catch (error) {
      console.error("加载详情数据失败:", error);
      showToast("加载详情数据失败,请重试");
    }
  };
  onShow(() => {
    getDetail();
  });
  onMounted(() => {
    getDetail();
  });
</script>
<style scoped lang="scss">
  .material-inspection-detail {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 20px;
  }
  .detail-section {
    padding: 15px;
  }
  .detail-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
  }
  .header-icon {
    width: 40px;
    height: 40px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
  }
  .header-icon.secondary {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  }
  .header-icon.tertiary {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  }
  .header-title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .status-tag {
    margin-left: 20rpx;
  }
  .detail-content {
    padding-top: 8px;
  }
  .detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .detail-row:last-child {
    border-bottom: none;
  }
  .detail-label {
    font-size: 14px;
    color: #666;
    min-width: 100px;
  }
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  // æ£€éªŒé¡¹ç›®
  .inspection-items {
    padding-top: 8px;
  }
  .inspection-item {
    padding: 12px;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 10px;
  }
  .inspection-item:last-child {
    margin-bottom: 0;
  }
  .item-name {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f0f0;
  }
  .item-details {
    padding-top: 8px;
  }
  .item-detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 6px;
  }
  .item-detail-row:last-child {
    margin-bottom: 0;
  }
  .item-detail-label {
    font-size: 12px;
    color: #666;
    min-width: 60px;
  }
  .item-detail-value {
    font-size: 12px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  .item-detail-value.result {
    font-weight: 500;
  }
  .result-passed {
    color: #67c23a;
  }
  .result-rejected {
    color: #f56c6c;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
</style>
src/pages/qualityManagement/materialInspection/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,566 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    qualityInspectFileAdd,
    qualityInspectFileListPage,
    qualityInspectFileDel,
  } from "@/api/qualityManagement/materialInspection";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                inspectId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              qualityInspectFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    qualityInspectFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "qualityInspectFileId"
    );
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    qualityInspectFileListPage({
      inspectId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/qualityManagement/materialInspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,775 @@
<template>
  <view class="material-inspection-page">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="原材料检验"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <up-search
        placeholder="请输入供应商搜索"
        v-model="searchForm.supplier"
        @search="getList"
        @custom="getList"
        @clear="getList"
        :show-action="true"
        action-text="搜索"
        :animation="true"
      ></up-search>
    </view>
    <!-- ç»Ÿè®¡ä¿¡æ¯å¡ç‰‡ -->
    <!-- <view class="stats-cards">
      <view class="stat-card">
        <text class="stat-number">{{ totalCount }}</text>
        <text class="stat-label">总检验</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ submittedCount }}</text>
        <text class="stat-label">已提交</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ pendingCount }}</text>
        <text class="stat-label">待提交</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ qualifiedCount }}</text>
        <text class="stat-label">已合格</text>
      </view>
    </view> -->
    <!-- æ£€éªŒåˆ—表 -->
    <view class="inspection-list"
          v-if="inspectionList.length > 0">
      <view v-for="(item, index) in inspectionList"
            :key="index">
        <view class="inspection-item"
              @click="viewDetail(item)">
          <view class="item-header">
            <view class="item-left">
              <!-- <view class="material-icon"
                    :class="getStateClass(item.inspectState)">
                <up-icon :name="getStateIcon(item.inspectState)"
                         size="16"
                         color="#ffffff"></up-icon>
              </view> -->
              <view class="material-info">
                <text class="material-name">{{ item.productName }}</text>
                <text class="material-code">{{ item.model }}</text>
              </view>
            </view>
            <view class="status-tags">
              <u-tag :type="getTagType(item.checkResult)"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
              </u-tag>
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
                     class="status-tag">
                {{ item.inspectState ? '已提交' : '未提交' }}
              </u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">检测日期</text>
              <text class="detail-value">{{ formatDateTime(item.checkTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">采购订单号</text>
              <text class="detail-value">{{ item.purchaseContractNo || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">供应商</text>
              <text class="detail-value">{{ item.supplier || '-' }}</text>
            </view>
            <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">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <!-- <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button> -->
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button> -->
          </view>
          <view class="action-buttons">
            <!-- <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
              é™„ä»¶
            </u-button>
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button> -->
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验任务"></up-empty>
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <!-- <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              :range="true"
              @confirm="confirmDate" />
    <!-- åˆ†é…æ£€éªŒå‘˜å¼¹çª— -->
    <up-popup v-model:show="showAssignDialog"
              mode="center"
              round
              style="width: 80%">
      <view class="assign-dialog">
        <view class="dialog-header">
          <text class="dialog-title">分配检验员</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showAssignDialog = false"></up-icon>
        </view>
        <view class="dialog-content">
          <up-form-item label="检验员"
                        prop="checkName"
                        :label-width="60"
                        required>
            <up-input v-model="assignForm.checkName"
                      placeholder="请选择检验员"
                      readonly />
            <template #right>
              <up-icon @click="showInspectorSheet = true"
                       name="arrow-right" />
            </template>
          </up-form-item>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="showAssignDialog = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="submitAssign">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import {
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // æœç´¢è¡¨å•
  const searchForm = ref({
    supplier: "",
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
  });
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  const showAssignDialog = ref(false);
  const showInspectorSheet = ref(false);
  const assignForm = ref({
    checkName: "",
  });
  const currentAssignRow = ref(null);
  // æ£€éªŒåˆ—表数据
  const inspectionList = ref([]);
  // åˆ†é¡µæ•°æ®
  const page = ref({
    current: -1,
    size: -1,
    total: 0,
  });
  // åŠ è½½çŠ¶æ€
  const tableLoading = ref(false);
  // ç»Ÿè®¡æ•°æ®
  const totalCount = ref(0);
  const submittedCount = ref(0);
  const pendingCount = ref(0);
  const qualifiedCount = ref(0);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // ActionSheet选项
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  // èŽ·å–çŠ¶æ€æ ·å¼
  const getStateClass = inspectState => {
    return inspectState ? "state-submitted" : "state-pending";
  };
  // èŽ·å–çŠ¶æ€å›¾æ ‡
  const getStateIcon = inspectState => {
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = inspectState => {
    return inspectState ? "success" : "warning";
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    searchForm.value.entryDate = e.value;
    searchForm.value.entryDateStart = dayjs(e.value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(e.value[1]).format("YYYY-MM-DD");
    getList();
  };
  const viewFileList = item => {
    uni.setStorageSync("qualityInspectFileId", item.id);
    uni.navigateTo({
      url: "/pages/qualityManagement/materialInspection/fileList",
    });
  };
  // æ¸…除日期范围
  const clearDateRange = () => {
    searchForm.value.entryDate = undefined;
    searchForm.value.entryDateStart = undefined;
    searchForm.value.entryDateEnd = undefined;
    getList();
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 0 })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
        page.value.total = res.data.total || 0;
        totalCount.value = res.data.total || 0;
        submittedCount.value = inspectionList.value.filter(
          item => item.inspectState
        ).length;
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        showToast("获取列表失败,请重试");
      });
  };
  // ç¼–辑检验
  const startInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    // å­˜å‚¨å®Œæ•´çš„æ£€éªŒæ•°æ®
    uni.setStorageSync("inspectionEditData", item);
    // è·³è½¬åˆ°ç¼–辑页面
    uni.navigateTo({
      url: `/pages/qualityManagement/materialInspection/add?id=${item.id}&isEdit=true`,
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    uni.setStorageSync("inspectionEditData", item);
    // è·³è½¬åˆ°è¯¦æƒ…页面
    uni.navigateTo({
      url: `/pages/qualityManagement/materialInspection/detail?id=${item.id}`,
    });
  };
  // æ–°å¢žæ£€éªŒ
  const addInspection = () => {
    uni.navigateTo({
      url: "/pages/qualityManagement/materialInspection/add",
    });
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async 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("提交失败,请重试");
    }
  };
  // åˆ†é…æ£€éªŒå‘˜
  const assignInspector = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    currentAssignRow.value = item;
    getUserList();
    showAssignDialog.value = true;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    assignForm.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // æäº¤åˆ†é…
  const submitAssign = async () => {
    if (!currentAssignRow.value || !assignForm.value.checkName) {
      showToast("请选择检验员");
      return;
    }
    try {
      const data = {
        ...assignForm.value,
        id: currentAssignRow.value.id,
      };
      const res = await qualityInspectUpdate(data);
      if (res.code === 200) {
        showToast("分配成功");
        showAssignDialog.value = false;
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("分配失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("分配失败:", error);
      showToast("分配失败,请重试");
    }
  };
  // å¤„理分页
  const handlePagination = obj => {
    page.value.current = obj.current;
    page.value.size = obj.size;
    getList();
  };
  onMounted(() => {
    getList();
    getUserList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .material-inspection-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  // æœç´¢åŒºåŸŸ
  .search-section {
    padding: 20rpx 30rpx;
    background-color: #ffffff;
    position: sticky;
    top: 0;
    z-index: 10;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .stats-cards {
    display: flex;
    padding: 15px;
    gap: 10px;
    background: #fff;
    margin-bottom: 10px;
  }
  .stat-card {
    flex: 1;
    background: #2979ff;
    border-radius: 12px;
    padding: 15px;
    text-align: center;
    color: #fff;
    box-shadow: 0 2px 8px rgba(41, 121, 255, 0.2);
  }
  .stat-number {
    display: block;
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 5px;
  }
  .stat-label {
    font-size: 12px;
    opacity: 0.9;
  }
  // æ£€éªŒåˆ—表
  .inspection-list {
    padding: 20px;
  }
  .inspection-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding: 0 16px;
    &:active {
      transform: scale(0.98);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    }
  }
  .item-header {
    padding: 16px 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .item-left {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .material-icon {
    width: 24px;
    height: 24px;
    background: #2979ff;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .state-pending {
    background: #ff9900;
  }
  .state-submitted {
    background: #52c41a;
  }
  .material-info {
    flex: 1;
  }
  .material-name {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  .material-code {
    font-size: 12px;
    color: #999;
    margin-left: 8px;
  }
  .status-tags {
    display: flex;
    gap: 8px;
  }
  .status-tag {
    margin: 0;
  }
  .date-range {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 10px;
    padding: 8px 12px;
    background: #f8f9fa;
    border-radius: 8px;
  }
  .date-text {
    font-size: 12px;
    color: #666;
  }
  // è¯¦æƒ…行
  .item-details {
    padding: 16px 0;
  }
  .detail-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 8px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .detail-label {
    font-size: 12px;
    color: #777777;
    min-width: 60px;
  }
  .detail-value {
    font-size: 12px;
    color: #000000;
    text-align: right;
    flex: 1;
    margin-left: 16px;
  }
  // æ“ä½œæŒ‰é’®
  .action-buttons {
    display: flex;
    gap: 12px;
    padding: 0 0 16px 0;
    justify-content: space-between;
  }
  .action-btn {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
  // æµ®åŠ¨æŒ‰é’®
  .fab-button {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 56px;
    height: 56px;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
    z-index: 1000;
  }
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  .assign-dialog {
    padding: 24px;
    background: #ffffff;
    border-radius: 16px;
    overflow: hidden;
  }
  .dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid #f0f0f0;
  }
  .dialog-title {
    font-size: 18px;
    font-weight: 600;
    color: #303133;
  }
  .dialog-content {
    margin-bottom: 24px;
  }
  .dialog-footer {
    display: flex;
    gap: 16px;
    padding-top: 16px;
    border-top: 1px solid #f0f0f0;
  }
  .footer-btn {
    flex: 1;
    height: 44px;
    font-size: 16px;
  }
  // è¾“入框样式
  :deep(.up-input__inner) {
    border-radius: 8px;
    height: 44px;
    font-size: 14px;
  }
  // è¡¨å•项样式
  :deep(.up-form-item) {
    margin-bottom: 0;
  }
  :deep(.up-form-item__label) {
    font-size: 14px;
    color: #606266;
    margin-bottom: 8px;
  }
  // æŒ‰é’®æ ·å¼
  :deep(.up-button--primary) {
    border-radius: 8px;
  }
  :deep(.up-button--default) {
    border-radius: 8px;
  }
  // åˆ†é¡µç»„ä»¶
  .pagination {
    padding: 20px;
    background: #fff;
    margin-top: 10px;
    display: flex;
    justify-content: center;
  }
</style>
src/pages/qualityManagement/metricBinding/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
<template>
  <view class="binding-detail-page">
    <PageHeader :title="'绑定关系: ' + standardNo" @back="goBack" />
    <view class="detail-info-header">
      <view class="info-row">
        <text class="info-label">标准编号:</text>
        <text class="info-value">{{ standardNo }}</text>
      </view>
    </view>
    <view class="list-container" v-if="bindingList.length > 0">
      <view v-for="(item, index) in bindingList" :key="index" class="list-item">
        <view class="item-content">
          <text class="product-name">{{ item.productName }}</text>
        </view>
        <view class="item-actions">
          <up-button type="error" size="mini" @click.stop="handleUnbind(item)">解除绑定</up-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无绑定关系"></up-empty>
    </view>
    <view class="fab-button" @click="openBindingDialog">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- æ·»åŠ ç»‘å®šå¼¹çª— -->
    <up-popup v-model:show="showBindingDialog" mode="bottom" round closeable @close="showBindingDialog = false">
      <view class="binding-dialog">
        <view class="dialog-header">
          <text class="dialog-title">添加产品绑定</text>
        </view>
        <view class="search-box">
          <up-search
            placeholder="搜索产品"
            v-model="productSearch"
            @search="filterProducts"
            @custom="filterProducts"
            @clear="filterProducts"
            :show-action="true"
            action-text="筛选"
            :animation="true"
          ></up-search>
        </view>
        <scroll-view scroll-y class="product-list">
          <up-checkbox-group v-model="selectedProductIds" placement="column">
            <up-checkbox
              v-for="item in filteredProducts"
              :key="item.id"
              :label="item.productName"
              :name="item.id"
              customStyle="margin-bottom: 20rpx"
            ></up-checkbox>
          </up-checkbox-group>
        </scroll-view>
        <view class="dialog-footer">
          <up-button type="primary" text="确认绑定" @click="submitBinding" :loading="submitLoading"></up-button>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import {
  qualityTestStandardBindingList,
  qualityTestStandardBindingAdd,
  qualityTestStandardBindingDel
} from '@/api/qualityManagement/qualityTestStandardBinding.js';
import { productTreeList } from '@/api/basicData/product.js';
import { toast, showConfirm } from '@/utils/common';
const standardId = ref(null);
const standardNo = ref('');
const bindingList = ref([]);
const loading = ref(false);
const showBindingDialog = ref(false);
const productSearch = ref('');
const allProducts = ref([]);
const filteredProducts = ref([]);
const selectedProductIds = ref([]);
const submitLoading = ref(false);
const getList = () => {
  loading.value = true;
  qualityTestStandardBindingList({ standardId: standardId.value }).then(res => {
    bindingList.value = res.data || [];
  }).finally(() => {
    loading.value = false;
  });
};
const openBindingDialog = () => {
  selectedProductIds.value = [];
  productSearch.value = '';
  filteredProducts.value = allProducts.value;
  showBindingDialog.value = true;
};
const filterProducts = () => {
  if (!productSearch.value) {
    filteredProducts.value = allProducts.value;
  } else {
    filteredProducts.value = allProducts.value.filter(p =>
      p.productName.toLowerCase().includes(productSearch.value.toLowerCase())
    );
  }
};
const submitBinding = () => {
  if (selectedProductIds.value.length === 0) {
    toast('请选择要绑定的产品');
    return;
  }
  submitLoading.value = true;
  const data = selectedProductIds.value.map(productId => ({
    standardId: standardId.value,
    productId
  }));
  qualityTestStandardBindingAdd(data).then(() => {
    toast('绑定成功');
    showBindingDialog.value = false;
    getList();
  }).finally(() => {
    submitLoading.value = false;
  });
};
const handleUnbind = (row) => {
  showConfirm('确认解除该产品的绑定关系吗?').then(res => {
    if (res.confirm) {
      qualityTestStandardBindingDel([row.id]).then(() => {
        toast('解绑成功');
        getList();
      });
    }
  });
};
const goBack = () => {
  uni.navigateBack();
};
onLoad((options) => {
  standardId.value = options.id;
  standardNo.value = options.standardNo;
  getList();
  productTreeList().then(res => {
    allProducts.value = res.data || [];
    filteredProducts.value = allProducts.value;
  });
});
</script>
<style lang="scss" scoped>
.binding-detail-page {
  padding-bottom: 120rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.detail-info-header {
  padding: 30rpx;
  background-color: #ffffff;
  margin-bottom: 20rpx;
}
.info-row {
  display: flex;
  align-items: center;
}
.info-label {
  color: #909399;
  font-size: 28rpx;
  width: 160rpx;
}
.info-value {
  color: #303133;
  font-size: 28rpx;
  font-weight: bold;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.product-name {
  font-size: 30rpx;
  color: #303133;
  font-weight: 500;
}
.no-data {
  padding-top: 200rpx;
}
.fab-button {
  position: fixed;
  right: 40rpx;
  bottom: 60rpx;
  width: 100rpx;
  height: 100rpx;
  background-color: #3c9cff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.4);
  z-index: 99;
}
.binding-dialog {
  background-color: #ffffff;
  padding: 40rpx;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  border-radius: 24rpx 24rpx 0 0;
  overflow: hidden;
}
.dialog-header {
  margin-bottom: 30rpx;
  text-align: center;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: bold;
}
.search-box {
  margin-bottom: 30rpx;
}
.product-list {
  flex: 1;
  min-height: 400rpx;
}
.dialog-footer {
  margin-top: 40rpx;
  padding-bottom: env(safe-area-inset-bottom);
}
</style>
src/pages/qualityManagement/metricBinding/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,283 @@
<template>
  <view class="metric-binding-page">
    <PageHeader title="指标绑定" @back="goBack" />
    <!-- æœç´¢ä¸Žç­›é€‰ -->
    <view class="search-section">
      <up-search
        placeholder="标准编号/标准名称"
        v-model="searchForm.keyword"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
        customStyle="margin-bottom: 20rpx"
      ></up-search>
      <view class="filter-row">
        <view class="filter-item" @click="showTypeSelect = true">
          <text>{{ typeLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
        <view class="filter-item" @click="showStatusSelect = true">
          <text>{{ statusLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item" @click="viewDetail(item)">
        <view class="item-header">
          <text class="standard-no">{{ item.standardNo }}</text>
          <up-tag :text="getStatusText(item.state)" :type="getStatusType(item.state)" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">标准名称:</text>
            <text class="item-value">{{ item.standardName }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">类别:</text>
            <text class="item-value">{{ getInspectTypeText(item.inspectType) }}</text>
          </view>
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- ç±»åˆ«é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="typeActions"
      :show="showTypeSelect"
      @close="showTypeSelect = false"
      @select="selectType"
      title="请选择类别"
    ></up-action-sheet>
    <!-- çŠ¶æ€é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="statusActions"
      :show="showStatusSelect"
      @close="showStatusSelect = false"
      @select="selectStatus"
      title="请选择状态"
    ></up-action-sheet>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import {
  qualityTestStandardListPage
} from '@/api/qualityManagement/metricMaintenance.js';
const searchForm = reactive({
  keyword: '',
  inspectType: '',
  state: ''
});
const tableData = ref([]);
const page = reactive({
  current: 1,
  size: 20,
  total: 0
});
const loadStatus = ref('loadmore');
const showTypeSelect = ref(false);
const typeActions = [
  { name: '全部', value: '' },
  { name: '原材料检验', value: '0' },
  { name: '过程检验', value: '1' },
  { name: '出厂检验', value: '2' }
];
const typeLabel = computed(() => {
  const action = typeActions.find(a => a.value === searchForm.inspectType);
  return action ? action.name : '全部类别';
});
const showStatusSelect = ref(false);
const statusActions = [
  { name: '全部', value: '' },
  { name: '草稿', value: '0' },
  { name: '通过', value: '1' },
  { name: '撤销', value: '2' }
];
const statusLabel = computed(() => {
  const action = statusActions.find(a => a.value === searchForm.state);
  return action ? action.name : '全部状态';
});
const getInspectTypeText = (type) => {
  const types = { '0': '原材料检验', '1': '过程检验', '2': '出厂检验' };
  return types[type] || '-';
};
const getStatusText = (state) => {
  const states = { '0': '草稿', '1': '通过', '2': '撤销' };
  return states[state] || '未知';
};
const getStatusType = (state) => {
  const types = { '0': 'info', '1': 'success', '2': 'warning' };
  return types[state] || 'info';
};
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const params = {
    standardNo: searchForm.keyword || null,
    inspectType: searchForm.inspectType || null,
    state: searchForm.state || null,
    current: page.current,
    size: page.size
  };
  qualityTestStandardListPage(params).then(res => {
    const records = res?.data?.records || [];
    if (page.current === 1) {
      tableData.value = records;
    } else {
      tableData.value = [...tableData.value, ...records];
    }
    page.total = res?.data?.total || 0;
    if (tableData.value.length >= page.total) {
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
  });
};
const handleQuery = () => {
  page.current = 1;
  page.total = 0;
  tableData.value = [];
  loadStatus.value = 'loadmore';
  getList();
};
const selectType = (e) => {
  searchForm.inspectType = e.value;
  handleQuery();
};
const selectStatus = (e) => {
  searchForm.state = e.value;
  handleQuery();
};
const viewDetail = (item) => {
  uni.navigateTo({
    url: `/pages/qualityManagement/metricBinding/detail?id=${item.id}&standardNo=${item.standardNo}`
  });
};
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
});
</script>
<style lang="scss" scoped>
.metric-binding-page {
  padding-bottom: 20rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.filter-row {
  display: flex;
  justify-content: space-around;
  padding: 10rpx 0;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 10rpx;
  font-size: 28rpx;
  color: #606266;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.standard-no {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 10rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 160rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
}
.pagination-container {
  padding: 20rpx 0;
}
.no-data {
  padding-top: 200rpx;
}
</style>
src/pages/qualityManagement/metricMaintenance/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,335 @@
<template>
  <view class="metric-detail-page">
    <PageHeader :title="'标准参数: ' + standardNo" @back="goBack" />
    <!-- ä¿¡æ¯å¤´éƒ¨ -->
    <view class="detail-info-header">
      <view class="info-row">
        <text class="info-label">标准编号:</text>
        <text class="info-value">{{ standardNo }}</text>
      </view>
      <view class="info-row">
        <text class="info-label">状态:</text>
        <up-tag :text="getStatusText(state)" :type="getStatusType(state)" size="mini"></up-tag>
      </view>
    </view>
    <!-- å‚数列表 -->
    <view class="list-container" v-if="paramList.length > 0">
      <view v-for="(item, index) in paramList" :key="index" class="list-item">
        <view class="item-content">
          <view class="item-header">
            <text class="param-name">{{ item.parameterItem }}</text>
            <text class="param-unit">{{ item.unit || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">标准值:</text>
            <text class="item-value">{{ item.standardValue || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">内控值:</text>
            <text class="item-value">{{ item.controlValue || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">默认值:</text>
            <text class="item-value">{{ item.defaultValue || '-' }}</text>
          </view>
        </view>
        <view class="item-actions" v-if="!isReadonly">
          <up-button type="primary" size="mini" @click.stop="openParamDialog('edit', item)">编辑</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无参数"></up-empty>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button" v-if="!isReadonly" @click="openParamDialog('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- å‚数编辑弹窗 -->
    <up-popup v-model:show="paramDialogVisible" mode="center" round closeable @close="closeParamDialog">
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">{{ operationType === 'add' ? '新增参数' : '修改参数' }}</text>
        </view>
        <up-form :model="form" ref="formRef" label-width="80">
          <up-form-item label="参数项" prop="parameterItem" required>
            <up-input v-model="form.parameterItem" placeholder="请输入参数项" border="bottom" />
          </up-form-item>
          <up-form-item label="单位" prop="unit">
            <up-input v-model="form.unit" placeholder="请输入单位" border="bottom" />
          </up-form-item>
          <up-form-item label="标准值" prop="standardValue">
            <up-input v-model="form.standardValue" placeholder="请输入标准值" border="bottom" />
          </up-form-item>
          <up-form-item label="内控值" prop="controlValue">
            <up-input v-model="form.controlValue" placeholder="请输入内控值" border="bottom" />
          </up-form-item>
          <up-form-item label="默认值" prop="defaultValue">
            <up-input v-model="form.defaultValue" placeholder="请输入默认值" border="bottom" />
          </up-form-item>
        </up-form>
        <view class="dialog-footer">
          <up-button type="primary" text="确认" @click="submitForm" :loading="submitLoading"></up-button>
          <up-button text="取消" @click="closeParamDialog" customStyle="margin-top: 20rpx"></up-button>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import {
  qualityTestStandardParamList,
  qualityTestStandardParamAdd,
  qualityTestStandardParamUpdate,
  qualityTestStandardParamDel
} from '@/api/qualityManagement/metricMaintenance.js';
import { toast, showConfirm } from '@/utils/common';
const standardId = ref(null);
const standardNo = ref('');
const state = ref('0');
const paramList = ref([]);
const loading = ref(false);
const isReadonly = computed(() => state.value === '1' || state.value === 1);
const paramDialogVisible = ref(false);
const operationType = ref('add');
const submitLoading = ref(false);
const form = reactive({
  id: null,
  standardId: null,
  parameterItem: '',
  unit: '',
  standardValue: '',
  controlValue: '',
  defaultValue: ''
});
const getStatusText = (s) => {
  const states = { '0': '草稿', '1': '通过', '2': '撤销' };
  return states[s] || '未知';
};
const getStatusType = (s) => {
  const types = { '0': 'info', '1': 'success', '2': 'warning' };
  return types[s] || 'info';
};
const getList = () => {
  loading.value = true;
  qualityTestStandardParamList({ standardId: standardId.value }).then(res => {
    paramList.value = res.data || [];
  }).finally(() => {
    loading.value = false;
  });
};
const openParamDialog = (type, row) => {
  operationType.value = type;
  if (type === 'edit' && row) {
    Object.assign(form, {
      id: row.id,
      standardId: standardId.value,
      parameterItem: row.parameterItem,
      unit: row.unit,
      standardValue: row.standardValue,
      controlValue: row.controlValue,
      defaultValue: row.defaultValue
    });
  } else {
    Object.assign(form, {
      id: null,
      standardId: standardId.value,
      parameterItem: '',
      unit: '',
      standardValue: '',
      controlValue: '',
      defaultValue: ''
    });
  }
  paramDialogVisible.value = true;
};
const closeParamDialog = () => {
  paramDialogVisible.value = false;
};
const submitForm = () => {
  submitLoading.value = true;
  const api = operationType.value === 'add' ? qualityTestStandardParamAdd : qualityTestStandardParamUpdate;
  api(form).then(() => {
    toast('保存成功');
    paramDialogVisible.value = false;
    getList();
  }).finally(() => {
    submitLoading.value = false;
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该参数项吗?').then(res => {
    if (res.confirm) {
      qualityTestStandardParamDel([row.id]).then(() => {
        toast('删除成功');
        getList();
      });
    }
  });
};
const goBack = () => {
  uni.navigateBack();
};
onLoad((options) => {
  standardId.value = options.id;
  standardNo.value = options.standardNo;
  state.value = options.state;
  getList();
});
</script>
<style lang="scss" scoped>
.metric-detail-page {
  padding-bottom: 120rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.detail-info-header {
  padding: 30rpx;
  background-color: #ffffff;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.info-row {
  display: flex;
  align-items: center;
  margin-bottom: 10rpx;
}
.info-label {
  color: #909399;
  font-size: 28rpx;
  width: 160rpx;
}
.info-value {
  color: #303133;
  font-size: 28rpx;
  font-weight: bold;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.param-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.param-unit {
  font-size: 26rpx;
  color: #909399;
  background-color: #f0f2f5;
  padding: 4rpx 12rpx;
  border-radius: 4rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 160rpx;
  font-size: 26rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 26rpx;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
  margin-top: 20rpx;
}
.no-data {
  padding-top: 200rpx;
}
.fab-button {
  position: fixed;
  right: 40rpx;
  bottom: 60rpx;
  width: 100rpx;
  height: 100rpx;
  background-color: #3c9cff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.4);
  z-index: 99;
}
.dialog-content {
  width: 650rpx;
  padding: 40rpx;
  background-color: #ffffff;
  border-radius: 24rpx;
  overflow: hidden;
  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.dialog-header {
  margin-bottom: 30rpx;
  text-align: center;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #303133;
}
.dialog-footer {
  margin-top: 40rpx;
}
</style>
src/pages/qualityManagement/metricMaintenance/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <view class="metric-maintenance-page">
    <PageHeader title="指标维护" @back="goBack" />
    <!-- æœç´¢ä¸Žç­›é€‰ -->
    <view class="search-section">
      <up-search
        placeholder="标准编号/标准名称"
        v-model="searchForm.keyword"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
        customStyle="margin-bottom: 20rpx"
      ></up-search>
      <view class="filter-row">
        <view class="filter-item" @click="showTypeSelect = true">
          <text>{{ typeLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
        <view class="filter-item" @click="showStatusSelect = true">
          <text>{{ statusLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item" @click="viewDetail(item)">
        <view class="item-header">
          <text class="standard-no">{{ item.standardNo }}</text>
          <up-tag :text="getStatusText(item.state)" :type="getStatusType(item.state)" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">标准名称:</text>
            <text class="item-value">{{ item.standardName }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">类别:</text>
            <text class="item-value">{{ getInspectTypeText(item.inspectType) }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">工序:</text>
            <text class="item-value">{{ item.processName || '-' }}</text>
          </view>
        </view>
        <view class="item-actions">
          <up-button type="primary" size="mini" @click.stop="openStandardDialog('edit', item)">编辑</up-button>
          <up-button v-if="item.state !== 1" type="success" size="mini" @click.stop="handleAudit(item, 1)">批准</up-button>
          <up-button v-if="item.state === 1" type="warning" size="mini" @click.stop="handleAudit(item, 2)">撤销</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button" @click="openStandardDialog('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- ç±»åˆ«é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="typeActions"
      :show="showTypeSelect"
      @close="showTypeSelect = false"
      @select="selectType"
      title="请选择类别"
    ></up-action-sheet>
    <!-- çŠ¶æ€é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="statusActions"
      :show="showStatusSelect"
      @close="showStatusSelect = false"
      @select="selectStatus"
      title="请选择状态"
    ></up-action-sheet>
    <!-- æ ‡å‡†ç¼–辑弹窗 -->
    <up-popup v-model:show="standardDialogVisible" mode="center" round closeable @close="closeStandardDialog">
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">{{ standardOperationType === 'add' ? '新增检测标准' : '修改检测标准' }}</text>
        </view>
        <up-form :model="standardForm" ref="standardFormRef" label-width="100" label-position="top">
          <up-form-item label="标准编号" prop="standardNo" required borderBottom>
            <up-input v-model="standardForm.standardNo" placeholder="请输入标准编号" border="surround" />
          </up-form-item>
          <up-form-item label="标准名称" prop="standardName" required borderBottom>
            <up-input v-model="standardForm.standardName" placeholder="请输入标准名称" border="surround" />
          </up-form-item>
          <up-form-item label="类别" prop="inspectType" required borderBottom>
            <up-radio-group v-model="standardForm.inspectType">
              <up-radio label="原材料" name="0"></up-radio>
              <up-radio label="过程" name="1" customStyle="margin-left: 20rpx"></up-radio>
              <up-radio label="出厂" name="2" customStyle="margin-left: 20rpx"></up-radio>
            </up-radio-group>
          </up-form-item>
          <up-form-item label="工序" prop="processId" borderBottom>
            <up-input
              v-model="processName"
              placeholder="请选择工序"
              border="surround"
              readonly
              @click="showProcessSelect = true"
            />
          </up-form-item>
          <up-form-item label="备注" prop="remark" borderBottom>
            <up-textarea v-model="standardForm.remark" placeholder="请输入备注" count border="surround" />
          </up-form-item>
        </up-form>
        <view class="dialog-footer">
          <up-button type="primary" text="确认" @click="submitStandardForm" :loading="submitLoading"></up-button>
          <up-button text="取消" @click="closeStandardDialog" customStyle="margin-top: 20rpx"></up-button>
        </view>
      </view>
    </up-popup>
    <!-- å·¥åºé€‰æ‹©å™¨ -->
    <up-picker
      :show="showProcessSelect"
      :columns="[processOptions]"
      keyName="label"
      @confirm="confirmProcess"
      @cancel="showProcessSelect = false"
    ></up-picker>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import {
  qualityTestStandardListPage,
  qualityTestStandardAdd,
  qualityTestStandardUpdate,
  qualityTestStandardDel,
  qualityTestStandardAudit
} from '@/api/qualityManagement/metricMaintenance.js';
import { productProcessListPage } from '@/api/basicData/productProcess.js';
import { toast, showConfirm } from '@/utils/common';
const searchForm = reactive({
  keyword: '',
  inspectType: '',
  state: ''
});
const tableData = ref([]);
const page = reactive({
  current: 1,
  size: 20,
  total: 0
});
const loadStatus = ref('loadmore');
const showTypeSelect = ref(false);
const typeActions = [
  { name: '全部', value: '' },
  { name: '原材料检验', value: '0' },
  { name: '过程检验', value: '1' },
  { name: '出厂检验', value: '2' }
];
const typeLabel = computed(() => {
  const action = typeActions.find(a => a.value === searchForm.inspectType);
  return action ? action.name : '全部类别';
});
const showStatusSelect = ref(false);
const statusActions = [
  { name: '全部', value: '' },
  { name: '草稿', value: '0' },
  { name: '通过', value: '1' },
  { name: '撤销', value: '2' }
];
const statusLabel = computed(() => {
  const action = statusActions.find(a => a.value === searchForm.state);
  return action ? action.name : '全部状态';
});
const standardDialogVisible = ref(false);
const standardOperationType = ref('add');
const submitLoading = ref(false);
const standardForm = reactive({
  id: null,
  standardNo: '',
  standardName: '',
  inspectType: '0',
  processId: null,
  remark: '',
  state: '0'
});
const processOptions = ref([]);
const showProcessSelect = ref(false);
const processName = ref('');
const getInspectTypeText = (type) => {
  const types = { '0': '原材料检验', '1': '过程检验', '2': '出厂检验' };
  return types[type] || '-';
};
const getStatusText = (state) => {
  const states = { '0': '草稿', '1': '通过', '2': '撤销' };
  return states[state] || '未知';
};
const getStatusType = (state) => {
  const types = { '0': 'info', '1': 'success', '2': 'warning' };
  return types[state] || 'info';
};
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const params = {
    standardNo: searchForm.keyword || null,
    inspectType: searchForm.inspectType || null,
    state: searchForm.state || null,
    current: page.current,
    size: page.size
  };
  qualityTestStandardListPage(params).then(res => {
    const records = res?.data?.records || [];
    if (page.current === 1) {
      tableData.value = records;
    } else {
      tableData.value = [...tableData.value, ...records];
    }
    page.total = res?.data?.total || 0;
    if (tableData.value.length >= page.total) {
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
  });
};
const handleQuery = () => {
  page.current = 1;
  page.total = 0;
  tableData.value = [];
  loadStatus.value = 'loadmore';
  getList();
};
const selectType = (e) => {
  searchForm.inspectType = e.value;
  handleQuery();
};
const selectStatus = (e) => {
  searchForm.state = e.value;
  handleQuery();
};
const viewDetail = (item) => {
  uni.navigateTo({
    url: `/pages/qualityManagement/metricMaintenance/detail?id=${item.id}&standardNo=${item.standardNo}&state=${item.state}`
  });
};
const openStandardDialog = (type, row) => {
  standardOperationType.value = type;
  if (type === 'edit' && row) {
    Object.assign(standardForm, {
      id: row.id,
      standardNo: row.standardNo,
      standardName: row.standardName,
      inspectType: String(row.inspectType),
      processId: row.processId,
      remark: row.remark,
      state: String(row.state)
    });
    const process = processOptions.value.find(p => p.value === row.processId);
    processName.value = process ? process.label : '';
  } else {
    Object.assign(standardForm, {
      id: null,
      standardNo: '',
      standardName: '',
      inspectType: '0',
      processId: null,
      remark: '',
      state: '0'
    });
    processName.value = '';
  }
  standardDialogVisible.value = true;
};
const closeStandardDialog = () => {
  standardDialogVisible.value = false;
};
const submitStandardForm = () => {
  submitLoading.value = true;
  const api = standardOperationType.value === 'add' ? qualityTestStandardAdd : qualityTestStandardUpdate;
  api(standardForm).then(() => {
    toast('保存成功');
    standardDialogVisible.value = false;
    handleQuery();
  }).finally(() => {
    submitLoading.value = false;
  });
};
const handleAudit = (row, state) => {
  const text = state === 1 ? '批准' : '撤销';
  showConfirm(`确认${text}该检测标准吗?`).then(res => {
    if (res.confirm) {
      qualityTestStandardAudit([{ id: row.id, state }]).then(() => {
        toast(`${text}成功`);
        handleQuery();
      });
    }
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该检测标准吗?').then(res => {
    if (res.confirm) {
      qualityTestStandardDel([row.id]).then(() => {
        toast('删除成功');
        handleQuery();
      });
    }
  });
};
const confirmProcess = (e) => {
  const val = e.value[0];
  standardForm.processId = val.value;
  processName.value = val.label;
  showProcessSelect.value = false;
};
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
  productProcessListPage({ current: 1, size: 1000 }).then(res => {
    processOptions.value = (res?.data?.records || []).map(p => ({
      label: p.processName,
      value: p.id
    }));
  });
});
</script>
<style lang="scss" scoped>
.metric-maintenance-page {
  padding-bottom: 120rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.filter-row {
  display: flex;
  justify-content: space-around;
  padding: 10rpx 0;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 10rpx;
  font-size: 28rpx;
  color: #606266;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.standard-no {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 160rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.pagination-container {
  padding: 20rpx 0;
}
.no-data {
  padding-top: 200rpx;
}
.fab-button {
  position: fixed;
  right: 40rpx;
  bottom: 60rpx;
  width: 100rpx;
  height: 100rpx;
  background-color: #3c9cff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.4);
  z-index: 99;
}
.dialog-content {
  width: 650rpx;
  padding: 40rpx;
  background-color: #ffffff;
  border-radius: 24rpx;
  overflow: hidden;
  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.dialog-header {
  margin-bottom: 30rpx;
  text-align: center;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #303133;
}
.dialog-footer {
  margin-top: 40rpx;
}
</style>
src/pages/qualityManagement/nearExpiryReturn/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,404 @@
<template>
  <view class="near-expiry-return-page">
    <PageHeader title="近效期退货" @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <up-search
        placeholder="产品名称/批次号"
        v-model="searchForm.keyword"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
      ></up-search>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-header">
          <text class="product-name">{{ item.productName }}</text>
          <up-tag :text="getStatusText(item.status)" :type="getStatusType(item.status)" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">批次号:</text>
            <text class="item-value">{{ item.batchNumber }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">退回数量:</text>
            <text class="item-value">{{ item.returnQuantity }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">退回日期:</text>
            <text class="item-value">{{ item.returnDate }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">到期日期:</text>
            <text class="item-value">{{ item.expiryDate }}</text>
          </view>
        </view>
        <view class="item-actions">
          <up-button type="primary" size="mini" @click.stop="openDialog('edit', item)">编辑</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button" @click="openDialog('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <up-popup v-model:show="dialogVisible" mode="center" round closeable @close="closeDialog">
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">{{ operationType === 'add' ? '新增退货台账' : '修改退货台账' }}</text>
        </view>
        <up-form :model="form" ref="formRef" label-width="100" label-position="top">
          <up-form-item label="产品名称" prop="productName" required borderBottom>
            <up-input v-model="form.productName" placeholder="请输入产品名称" border="surround" />
          </up-form-item>
          <up-form-item label="产品规格" prop="productSpec" borderBottom>
            <up-input v-model="form.productSpec" placeholder="请输入产品规格" border="surround" />
          </up-form-item>
          <up-form-item label="批次号" prop="batchNumber" required borderBottom>
            <up-input v-model="form.batchNumber" placeholder="请输入批次号" border="surround" />
          </up-form-item>
          <up-form-item label="退回数量" prop="returnQuantity" required borderBottom>
            <up-number-box v-model="form.returnQuantity" :min="1" />
          </up-form-item>
          <up-form-item label="退回日期" prop="returnDate" required borderBottom>
            <up-input
              v-model="form.returnDate"
              placeholder="请选择退回日期"
              border="surround"
              readonly
              @click="showDatePicker('returnDate')"
            />
          </up-form-item>
          <up-form-item label="到期日期" prop="expiryDate" borderBottom>
            <up-input
              v-model="form.expiryDate"
              placeholder="请选择到期日期"
              border="surround"
              readonly
              @click="showDatePicker('expiryDate')"
            />
          </up-form-item>
          <up-form-item label="退回原因" prop="returnReason" borderBottom>
            <up-textarea v-model="form.returnReason" placeholder="请输入退回原因" count border="surround" />
          </up-form-item>
        </up-form>
        <view class="dialog-footer">
          <up-button type="primary" text="确认" @click="submitForm" :loading="submitLoading"></up-button>
          <up-button text="取消" @click="closeDialog" customStyle="margin-top: 20rpx"></up-button>
        </view>
      </view>
    </up-popup>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker
      :show="datePickerVisible"
      v-model="dateValue"
      mode="date"
      @confirm="confirmDate"
      @cancel="datePickerVisible = false"
    ></up-datetime-picker>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import {
  nearExpiryReturnListPage,
  nearExpiryReturnAdd,
  nearExpiryReturnUpdate,
  nearExpiryReturnDel
} from '@/api/qualityManagement/nearExpiryReturn.js';
import { toast, showConfirm } from '@/utils/common';
import dayjs from 'dayjs';
const searchForm = reactive({
  keyword: ''
});
const tableData = ref([]);
const page = reactive({
  current: 1,
  size: 20,
  total: 0
});
const loadStatus = ref('loadmore');
const dialogVisible = ref(false);
const operationType = ref('add');
const submitLoading = ref(false);
const form = reactive({
  id: null,
  productName: '',
  productSpec: '',
  batchNumber: '',
  returnQuantity: 1,
  returnDate: '',
  expiryDate: '',
  returnReason: '',
  status: '0'
});
const datePickerVisible = ref(false);
const dateValue = ref(Number(new Date()));
const currentDateField = ref('');
const getStatusText = (status) => {
  const states = { '0': '待处理', '1': '已处理' };
  return states[status] || '未知';
};
const getStatusType = (status) => {
  const types = { '0': 'warning', '1': 'success' };
  return types[status] || 'info';
};
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.length >= page.total)) return;
  loadStatus.value = 'loading';
  const params = {
    productName: searchForm.keyword || null,
    current: page.current,
    size: page.size
  };
  nearExpiryReturnListPage(params).then(res => {
    const records = res?.data?.records || [];
    if (page.current === 1) {
      tableData.value = records;
    } else {
      tableData.value = [...tableData.value, ...records];
    }
    page.total = res?.data?.total || 0;
    if (tableData.value.length >= page.total) {
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
  });
};
const handleQuery = () => {
  page.current = 1;
  page.total = 0;
  tableData.value = [];
  loadStatus.value = 'loadmore';
  getList();
};
const openDialog = (type, row) => {
  operationType.value = type;
  if (type === 'edit' && row) {
    Object.assign(form, {
      id: row.id,
      productName: row.productName,
      productSpec: row.productSpec,
      batchNumber: row.batchNumber,
      returnQuantity: row.returnQuantity,
      returnDate: row.returnDate,
      expiryDate: row.expiryDate,
      returnReason: row.returnReason,
      status: String(row.status)
    });
  } else {
    Object.assign(form, {
      id: null,
      productName: '',
      productSpec: '',
      batchNumber: '',
      returnQuantity: 1,
      returnDate: dayjs().format('YYYY-MM-DD'),
      expiryDate: '',
      returnReason: '',
      status: '0'
    });
  }
  dialogVisible.value = true;
};
const closeDialog = () => {
  dialogVisible.value = false;
};
const submitForm = () => {
  submitLoading.value = true;
  const api = operationType.value === 'add' ? nearExpiryReturnAdd : nearExpiryReturnUpdate;
  api(form).then(() => {
    toast('保存成功');
    dialogVisible.value = false;
    handleQuery();
  }).finally(() => {
    submitLoading.value = false;
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该退货台账吗?').then(res => {
    if (res.confirm) {
      nearExpiryReturnDel([row.id]).then(() => {
        toast('删除成功');
        handleQuery();
      });
    }
  });
};
const showDatePicker = (field) => {
  currentDateField.value = field;
  dateValue.value = form[field] ? Number(new Date(form[field])) : Number(new Date());
  datePickerVisible.value = true;
};
const confirmDate = (e) => {
  form[currentDateField.value] = dayjs(e.value).format('YYYY-MM-DD');
  datePickerVisible.value = false;
};
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
});
</script>
<style lang="scss" scoped>
.near-expiry-return-page {
  padding-bottom: 120rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.product-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 160rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.pagination-container {
  padding: 20rpx 0;
}
.no-data {
  padding-top: 200rpx;
}
.fab-button {
  position: fixed;
  right: 40rpx;
  bottom: 60rpx;
  width: 100rpx;
  height: 100rpx;
  background-color: #3c9cff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.4);
  z-index: 99;
}
.dialog-content {
  width: 650rpx;
  padding: 40rpx;
  background-color: #ffffff;
  border-radius: 24rpx;
  overflow: hidden;
  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.dialog-header {
  margin-bottom: 30rpx;
  text-align: center;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #303133;
}
.dialog-footer {
  margin-top: 40rpx;
}
</style>
src/pages/qualityManagement/nonconformingManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,434 @@
<template>
  <view class="nonconforming-management-page">
    <PageHeader title="不合格品管理" @back="goBack" />
    <!-- æœç´¢ä¸Žç­›é€‰ -->
    <view class="search-section">
      <up-search
        placeholder="请输入产品名称搜索"
        v-model="searchForm.productName"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
        customStyle="margin-bottom: 20rpx"
      ></up-search>
      <view class="filter-row">
        <view class="filter-item" @click="showTypeSelect = true">
          <text>{{ typeLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
        <view class="filter-item" @click="showStatusSelect = true">
          <text>{{ statusLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-header">
          <text class="product-name">{{ item.productName }}</text>
          <up-tag :text="getStatusText(item.inspectState)" :type="getStatusType(item.inspectState)" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">类别:</text>
            <text class="item-value">{{ getInspectTypeText(item.inspectType) }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">检测日期:</text>
            <text class="item-value">{{ item.checkTime || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">规格型号:</text>
            <text class="item-value">{{ item.model || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">不合格现象:</text>
            <text class="item-value text-error">{{ item.defectivePhenomena || '-' }}</text>
          </view>
          <view class="item-row" v-if="item.inspectState === 1">
            <text class="item-label">处理结果:</text>
            <text class="item-value text-success">{{ item.dealResult || '-' }}</text>
          </view>
        </view>
        <view class="item-actions">
          <up-button v-if="item.inspectState === 0" type="primary" size="mini" @click.stop="openDealDialog(item)">处理</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- ç±»åž‹é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="typeActions"
      :show="showTypeSelect"
      @close="showTypeSelect = false"
      @select="selectType"
      title="请选择类别"
    ></up-action-sheet>
    <!-- çŠ¶æ€é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="statusActions"
      :show="showStatusSelect"
      @close="showStatusSelect = false"
      @select="selectStatus"
      title="请选择状态"
    ></up-action-sheet>
    <!-- å¤„理弹窗 -->
    <up-popup v-model:show="dealDialogVisible" mode="center" round closeable @close="dealDialogVisible = false">
      <view class="dialog-content">
        <view class="dialog-header">
          <text class="dialog-title">不合格品处理</text>
        </view>
        <up-form :model="dealForm" ref="dealFormRef" label-width="100" label-position="top">
          <view class="info-summary">
            <text class="summary-text">产品:{{ currentItem?.productName }}</text>
            <text class="summary-text">不合格现象:{{ currentItem?.defectivePhenomena }}</text>
          </view>
          <up-form-item label="处理结果" prop="dealResult" required borderBottom>
            <up-textarea v-model="dealForm.dealResult" placeholder="请输入处理结果" count border="surround" />
          </up-form-item>
          <up-form-item label="处理日期" prop="dealTime" required borderBottom>
            <up-input
              v-model="dealForm.dealTime"
              placeholder="请选择处理日期"
              border="surround"
              readonly
              @click="showDatePicker = true"
            />
          </up-form-item>
        </up-form>
        <view class="dialog-footer">
          <up-button type="primary" text="提交处理" @click="submitDeal" :loading="submitLoading"></up-button>
          <up-button text="取消" @click="dealDialogVisible = false" customStyle="margin-top: 20rpx"></up-button>
        </view>
      </view>
    </up-popup>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker
      :show="showDatePicker"
      v-model="dateValue"
      mode="date"
      @confirm="confirmDate"
      @cancel="showDatePicker = false"
    ></up-datetime-picker>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import {
  qualityUnqualifiedListPage,
  qualityUnqualifiedDeal,
  qualityUnqualifiedDel
} from '@/api/qualityManagement/nonconformingManagement.js';
import { toast, showConfirm } from '@/utils/common';
import dayjs from 'dayjs';
const searchForm = reactive({
  productName: '',
  inspectType: '',
  inspectState: ''
});
const tableData = ref([]);
const page = reactive({
  current: 1,
  size: 20,
  total: 0
});
const loadStatus = ref('loadmore');
const showTypeSelect = ref(false);
const typeActions = [
  { name: '全部', value: '' },
  { name: '入厂检', value: '0' },
  { name: '车间检', value: '1' },
  { name: '出厂检', value: '2' }
];
const typeLabel = computed(() => {
  const action = typeActions.find(a => a.value === searchForm.inspectType);
  return action ? action.name : '全部类别';
});
const showStatusSelect = ref(false);
const statusActions = [
  { name: '全部', value: '' },
  { name: '待处理', value: '0' },
  { name: '已处理', value: '1' }
];
const statusLabel = computed(() => {
  const action = statusActions.find(a => a.value === searchForm.inspectState);
  return action ? action.name : '全部状态';
});
const dealDialogVisible = ref(false);
const submitLoading = ref(false);
const currentItem = ref(null);
const dealForm = reactive({
  id: null,
  dealResult: '',
  dealTime: dayjs().format('YYYY-MM-DD')
});
const showDatePicker = ref(false);
const dateValue = ref(Number(new Date()));
const getInspectTypeText = (type) => {
  const types = { '0': '入厂检', '1': '车间检', '2': '出厂检' };
  return types[type] || '-';
};
const getStatusText = (state) => {
  return state === 1 ? '已处理' : '待处理';
};
const getStatusType = (state) => {
  return state === 1 ? 'success' : 'warning';
};
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const params = {
    productName: searchForm.productName || null,
    inspectType: searchForm.inspectType || null,
    inspectState: searchForm.inspectState || null,
    current: page.current,
    size: page.size
  };
  qualityUnqualifiedListPage(params).then(res => {
    const records = res?.data?.records || [];
    if (page.current === 1) {
      tableData.value = records;
    } else {
      tableData.value = [...tableData.value, ...records];
    }
    page.total = res?.data?.total || 0;
    if (tableData.value.length >= page.total) {
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
  });
};
const handleQuery = () => {
  page.current = 1;
  page.total = 0;
  tableData.value = [];
  loadStatus.value = 'loadmore';
  getList();
};
const selectType = (e) => {
  searchForm.inspectType = e.value;
  handleQuery();
};
const selectStatus = (e) => {
  searchForm.inspectState = e.value;
  handleQuery();
};
const openDealDialog = (item) => {
  currentItem.value = item;
  dealForm.id = item.id;
  dealForm.dealResult = '';
  dealForm.dealTime = dayjs().format('YYYY-MM-DD');
  dealDialogVisible.value = true;
};
const submitDeal = () => {
  if (!dealForm.dealResult) {
    toast('请输入处理结果');
    return;
  }
  submitLoading.value = true;
  qualityUnqualifiedDeal(dealForm).then(() => {
    toast('处理成功');
    dealDialogVisible.value = false;
    handleQuery();
  }).finally(() => {
    submitLoading.value = false;
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该不合格记录吗?').then(res => {
    if (res.confirm) {
      qualityUnqualifiedDel([row.id]).then(() => {
        toast('删除成功');
        handleQuery();
      });
    }
  });
};
const confirmDate = (e) => {
  dealForm.dealTime = dayjs(e.value).format('YYYY-MM-DD');
  showDatePicker.value = false;
};
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
});
</script>
<style lang="scss" scoped>
.nonconforming-management-page {
  padding-bottom: 20rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.filter-row {
  display: flex;
  justify-content: space-around;
  padding: 10rpx 0;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 10rpx;
  font-size: 28rpx;
  color: #606266;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.product-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 180rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
}
.text-error {
  color: #f56c6c;
}
.text-success {
  color: #67c23a;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.no-data {
  padding-top: 200rpx;
}
.dialog-content {
  width: 650rpx;
  padding: 40rpx;
  background-color: #ffffff;
  border-radius: 24rpx;
  overflow: hidden;
  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.dialog-header {
  margin-bottom: 30rpx;
  text-align: center;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: bold;
}
.info-summary {
  background-color: #f5f7fa;
  padding: 20rpx;
  border-radius: 8rpx;
  margin-bottom: 30rpx;
}
.summary-text {
  display: block;
  font-size: 26rpx;
  color: #606266;
  margin-bottom: 10rpx;
}
.dialog-footer {
  margin-top: 40rpx;
}
</style>
src/pages/qualityManagement/processInspection/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1128 @@
<template>
  <view class="material-inspection-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="isEdit ? '编辑过程检验' : '新增过程检验'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <up-form :model="form"
             ref="formRef"
             label-width="110"
             :rules="rules">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <up-form-item label="工序"
                    prop="process"
                    required
                    border-bottom>
        <up-input v-model="form.process"
                  placeholder="请选择工序"
                  readonly
                  :disabled="processQuantityDisabled" />
        <template #right>
          <up-icon @click="showprocessSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="产品名称"
                    prop="productId"
                    required
                    border-bottom>
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showProductTree = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="规格型号"
                    prop="productModelId"
                    required
                    border-bottom>
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showModelSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit"
                    border-bottom>
        <up-input v-model="form.unit"
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="processQuantityDisabled" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
                    prop="checkResult"
                    required
                    border-bottom>
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检验员"
                    prop="checkName"
                    border-bottom>
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检测日期"
                    prop="checkTime"
                    required
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
        </template> -->
      </up-form-item>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="inspection-items-container">
        <view class="steps-header">
          <text class="steps-title">检验项目</text>
          <text class="steps-count">共 {{ tableData.length }} ä¸ªé¡¹ç›®</text>
        </view>
        <view class="steps-list">
          <view v-for="(item, index) in tableData"
                :key="index"
                class="exec-step-item">
            <view class="step-number">
              {{ index + 1 }}
            </view>
            <view class="step-content">
              <view class="step-row">
                <text class="step-label">指标:</text>
                <text class="step-value">{{ item.parameterItem }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">单位:</text>
                <text class="step-value">{{ item.unit }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">标准值:</text>
                <text class="step-value">{{ item.standardValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">内控值:</text>
                <text class="step-value">{{ item.controlValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          clearable
                          border-bottom
                          class="step-input" />
              </view>
            </view>
          </view>
          <view v-if="tableData.length === 0"
                class="empty-data">
            <text>请先选择指标</text>
          </view>
        </view>
      </view>
    </up-form>
    <!-- åº•部按钮 -->
    <view class="bottom-buttons">
      <up-button type="default"
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
      </up-button>
      <up-button type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <!-- å·¥åºé€‰æ‹© -->
    <up-action-sheet :show="showprocessSheet"
                     :actions="processOptions"
                     @select="selectprocess"
                     @close="showprocessSheet = false"
                     title="选择工序" />
    <!-- äº§å“é€‰æ‹© -->
    <up-action-sheet :show="showProductSheet"
                     :actions="productSheetOptions"
                     @select="selectProduct"
                     @close="showProductSheet = false"
                     title="选择产品" />
    <!-- è§„格型号选择 -->
    <up-action-sheet :show="showModelSheet"
                     :actions="modelSheetOptions"
                     @select="selectModel"
                     @close="showModelSheet = false"
                     title="选择规格型号" />
    <!-- æ£€æµ‹ç»“果选择 -->
    <up-action-sheet :show="showResultSheet"
                     :actions="resultSheetOptions"
                     @select="selectResult"
                     @close="showResultSheet = false"
                     title="选择检测结果" />
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     @close="showInspectorSheet = false"
                     title="选择检验员" />
    <!-- æŒ‡æ ‡é€‰æ‹© -->
    <up-action-sheet :show="showTestStandardSheet"
                     :actions="testStandardSheetOptions"
                     @select="selectTestStandard"
                     @close="showTestStandardSheet = false"
                     title="选择指标" />
    <!-- äº§å“æ ‘形选择器 -->
    <up-popup v-model:show="showProductTree"
              position="bottom"
              :round="true"
              :closeable="true"
              @close="showProductTree = false">
      <view class="tree-selector">
        <view class="tree-header">
          <text class="tree-title">选择产品</text>
        </view>
        <view class="tree-content">
          <view class="tree-node"
                v-for="(node, index) in productOptions"
                :key="index">
            <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 class="tree-node"
                    v-for="(child, childIndex) in node.children"
                    :key="childIndex">
                <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 class="tree-node"
                        v-for="(grandchild, grandchildIndex) in child.children"
                        :key="grandchildIndex">
                    <view class="tree-node-header"
                          @click="selectTreeNode(grandchild)">
                      <text class="tree-node-icon-placeholder"></text>
                      <text class="tree-node-label">{{ grandchild.label }}</text>
                      <up-icon name="checkmark"
                               v-if="form.productId == grandchild.value"
                               class="tree-node-check" />
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted, nextTick } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  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 {
    qualityInspectAdd,
    qualityInspectUpdate,
    qualityInspectParamInfo,
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
    list,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¡¨å•引用
  const formRef = ref(null);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // å·¥åºé€‰æ‹©
  const showprocessSheet = ref(false);
  // äº§å“é€‰æ‹©
  const showProductSheet = ref(false);
  // äº§å“æ ‘形选择器
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
  const showTestStandardSheet = ref(false);
  // è¡¨å•数据
  const form = ref({
    checkTime: dayjs().format("YYYY-MM-DD"),
    process: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
  // æ˜¾ç¤ºç”¨çš„变量
  const testStandardDisplay = ref("");
  // æ£€éªŒé¡¹ç›®
  const tableData = ref([]);
  const tableLoading = ref(false);
  // å·¥åºåˆ—表
  const processList = ref([]);
  // äº§å“é€‰é¡¹
  const productOptions = ref([]);
  // åž‹å·é€‰é¡¹
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
  const currentProductId = ref(0);
  // ActionSheet选项
  const processOptions = computed(() => {
    return processList.value.map(item => ({
      name: item.name,
      value: item.name,
    }));
  });
  const productSheetOptions = computed(() => {
    return productOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const modelSheetOptions = computed(() => {
    return modelOptions.value.map(item => ({
      name: item.model,
      value: item.id,
    }));
  });
  const resultSheetOptions = computed(() => {
    return resultOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  const testStandardSheetOptions = computed(() => {
    return testStandardOptions.value.map(item => ({
      name: item.standardName || item.standardNo,
      value: item.id,
    }));
  });
  // è¡¨å•验证规则
  const rules = {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    process: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
  const isEdit = computed(() => {
    const id = getPageId();
    return !!id;
  });
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
  // é€‰æ‹©å·¥åº
  const selectprocess = e => {
    form.value.process = e.value;
    showprocessSheet.value = false;
  };
  // é€‰æ‹©äº§å“
  const selectProduct = e => {
    form.value.productId = e.value;
    form.value.productName = e.name;
    showProductSheet.value = false;
    getModels(e.value);
  };
  // åˆ‡æ¢æ ‘形节点展开/折叠
  const toggleNode = node => {
    node.expanded = !node.expanded;
  };
  // é€‰æ‹©æ ‘形节点
  const selectTreeNode = node => {
    // ç¡®ä¿åªé€‰æ‹©æœ«ç«¯èŠ‚ç‚¹
    if (!node.children || node.children.length == 0) {
      form.value.productId = node.value;
      form.value.productName = node.label;
      showProductTree.value = false;
      getModels(node.value);
    }
  };
  // è½¬æ¢äº§å“æ ‘结构
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // æ ¹æ®ID查找节点
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  // é€‰æ‹©è§„格型号
  const selectModel = e => {
    form.value.productModelId = e.value;
    showModelSheet.value = false;
    handleChangeModel(e.value);
  };
  // å¤„理型号变化
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  // é€‰æ‹©æ£€æµ‹ç»“æžœ
  const selectResult = e => {
    form.value.checkResult = e.value;
    showResultSheet.value = false;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    form.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // é€‰æ‹©æŒ‡æ ‡
  const selectTestStandard = e => {
    form.value.testStandardId = e.value;
    testStandardDisplay.value = e.name;
    showTestStandardSheet.value = false;
    handleTestStandardChange(e.value);
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    showTestStandardSheet.value = true;
  };
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getprocessList = () => {
    list().then(res => {
      processList.value = res.data;
    });
  };
  // èŽ·å–äº§å“é€‰é¡¹
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // èŽ·å–åž‹å·åˆ—è¡¨
  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;
    });
    if (currentProductId.value) {
      getList();
    }
  };
  // èŽ·å–æŒ‡æ ‡åˆ—è¡¨
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    let params = {
      productId: currentProductId.value,
      inspectType: 1,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
      testStandardDisplay.value = "";
    });
  };
  // èŽ·å–æ£€éªŒå‚æ•°åˆ—è¡¨ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
        showToast("请选择规格型号");
        return;
      }
      if (!form.value.process) {
        showToast("请选择工序");
        return;
      }
      if (!form.value.quantity) {
        showToast("请输入数量");
        return;
      }
      if (!form.value.productId) {
        showToast("请选择产品");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 1;
      if (isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
    } finally {
      loading.value = false;
    }
  };
  // åˆå§‹åŒ–表单
  const initForm = async () => {
    const id = getPageId();
    if (id) {
      // ç¼–辑模式,加载数据
      // å…ˆé‡ç½®è¡¨å•数据
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
      testStandardOptions.value = [];
      tableData.value = [];
      // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
      await getProductOptions();
      // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–ç¼–è¾‘æ•°æ®
      const row = uni.getStorageSync("processInspectionEditData") || {
        id: id,
        checkTime: "2026-03-03",
        process: "上海金属材料有限公司",
        checkName: "张三",
        productName: "不锈钢板材",
        productId: 1,
        productModelId: 1,
        model: "304",
        testStandardId: "1",
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        productMainId: null,
        purchaseLedgerId: null,
      };
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      form.value = { ...row };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 1,
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“
          nextTick(() => {
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…
              const matchedOption = testStandardOptions.value.find(
                item =>
                  item.id == savedTestStandardId ||
                  String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id
                form.value.testStandardId = matchedOption.id;
                testStandardDisplay.value =
                  matchedOption.standardName || matchedOption.standardNo;
                // ç¼–辑保留原检验值,直接拉取原参数数据
                getQualityInspectParamList(row.id);
              } else {
                // å¦‚果找不到匹配项,尝试直接使用原值
                console.warn(
                  "未找到匹配的指标选项,testStandardId:",
                  savedTestStandardId
                );
                form.value.testStandardId = savedTestStandardId;
                getQualityInspectParamList(row.id);
              }
            } else {
              // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
              getQualityInspectParamList(row.id);
            }
          });
        });
      }
      // å±•开产品树到当前选中的节点
      expandProductTree(productOptions.value, row.productId);
    } else {
      // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–表单
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
    }
  };
  // å±•开产品树到指定节点
  const expandProductTree = (nodes, targetId) => {
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.value === targetId) {
        return true; // æ‰¾åˆ°ç›®æ ‡èŠ‚ç‚¹
      }
      if (node.children && node.children.length > 0) {
        const found = expandProductTree(node.children, targetId);
        if (found) {
          node.expanded = true; // å±•开父节点
          return true;
        }
      }
    }
    return false;
  };
  onMounted(() => {
    getprocessList();
    getProductOptions();
    getUserList();
    initForm();
  });
  onShow(() => {
    initForm();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .material-inspection-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  // æ£€éªŒé¡¹ç›®å®¹å™¨
  .inspection-items-container {
    padding: 20px;
    background-color: #fff;
  }
  .steps-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e4e7ed;
  }
  .steps-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .steps-count {
    font-size: 14px;
    color: #909399;
  }
  .steps-list {
    margin-bottom: 20px;
  }
  .exec-step-item {
    position: relative;
    display: flex;
    margin-bottom: 16px;
    padding: 16px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    transition: all 0.3s ease;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  }
  .exec-step-item:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-1px);
  }
  .delete-btn {
    position: absolute;
    top: -25rpx;
    right: -25rpx;
    width: 50rpx;
    height: 50rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 20px;
    border-radius: 50%;
    background-color: red;
    border: none;
    z-index: 10;
  }
  .delete-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(245, 108, 108, 0.4);
  }
  .step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-right: 16px;
    background-color: #ecf5ff;
    color: #409eff;
    font-size: 14px;
    font-weight: 600;
    border-radius: 50%;
    flex-shrink: 0;
  }
  .step-content {
    flex: 1;
    min-width: 0;
  }
  .step-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .step-row:last-child {
    margin-bottom: 0;
  }
  .step-label {
    display: inline-block;
    width: 80px;
    font-size: 14px;
    color: #606266;
    margin-right: 12px;
    flex-shrink: 0;
    line-height: 36px;
  }
  .step-input {
    flex: 1;
    min-width: 0;
  }
  .step-input input {
    font-size: 14px;
    color: #303133;
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 44px;
    line-height: 44px;
    font-size: 14px;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 8px;
  }
  .add-step-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  }
  .add-step-btn text {
    font-size: 14px;
  }
  // åº•部按钮
  .bottom-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 16px 20px;
    background: #ffffff;
    border-top: 1px solid #f0f0f0;
    gap: 16px;
  }
  .bottom-btn {
    flex: 1;
  }
  // æ ‘形选择器样式
  .tree-selector {
    width: 100%;
    max-height: 70vh;
    background: #ffffff;
    border-radius: 16px 16px 0 0;
  }
  .tree-header {
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    text-align: center;
  }
  .tree-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .tree-content {
    padding: 10px 0;
    max-height: calc(70vh - 60px);
    overflow-y: auto;
  }
  .tree-node {
    padding: 0 20px;
  }
  .tree-node-header {
    display: flex;
    align-items: center;
    padding: 12px 0;
    cursor: pointer;
  }
  .tree-node-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    color: #909399;
  }
  .tree-node-icon-placeholder {
    width: 20px;
    height: 20px;
    margin-right: 8px;
  }
  .tree-node-label {
    flex: 1;
    font-size: 14px;
    color: #303133;
  }
  .tree-node-check {
    width: 20px;
    height: 20px;
    color: #409eff;
  }
  .tree-node-children {
    margin-left: 28px;
  }
</style>
src/pages/qualityManagement/processInspection/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,423 @@
<template>
  <view class="material-inspection-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="过程检验详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-section"
          v-if="detailData">
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon">
            <up-icon name="file-text"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
            </u-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="detail-content">
          <view class="detail-row">
            <text class="detail-label">检测日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.checkTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">生产工单号</text>
            <text class="detail-value">{{ detailData.workOrderNo || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工序</text>
            <text class="detail-value">{{ detailData.process || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检验员</text>
            <text class="detail-value">{{ detailData.checkName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ detailData.productName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ detailData.model || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ detailData.unit || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">数量</text>
            <text class="detail-value">{{ detailData.quantity || 0 }}</text>
          </view>
          <view class="detail-row">
            <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.testStandardName || '-' }}</text>
          </view> -->
        </view>
      </view>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="detail-card"
            v-if="inspectionItems.length > 0">
        <view class="card-header">
          <view class="header-icon secondary">
            <up-icon name="list"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">检验项目</text>
        </view>
        <up-divider></up-divider>
        <view class="inspection-items">
          <view v-for="(item, index) in inspectionItems"
                :key="index"
                class="inspection-item">
            <text class="item-name">{{ item.parameterItem || '检验项目' }}</text>
            <view class="item-details">
              <view class="item-detail-row">
                <text class="item-detail-label">单位:</text>
                <text class="item-detail-value">{{ item.unit || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">标准值:</text>
                <text class="item-detail-value">{{ item.standardValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">内控值:</text>
                <text class="item-detail-value">{{ item.controlValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">检验值:</text>
                <text class="item-detail-value result"
                      :class="getResultClass(item.testValue, item.standardValue)">
                  {{ item.testValue || '-' }}
                </text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <!-- æ“ä½œæŒ‰é’® -->
      <!-- <view class="action-buttons">
        <u-button type="primary"
                  class="action-btn"
                  @click="downloadReport">
          ä¸‹è½½æŠ¥å‘Š
        </u-button>
      </view> -->
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验详情"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¯¦æƒ…数据
  const detailData = ref(null);
  // æ£€éªŒé¡¹ç›®
  const inspectionItems = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = date => {
    if (!date) return "";
    return dayjs(date).format("YYYY-MM-DD");
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = state => {
    return state ? "success" : "warning";
  };
  // èŽ·å–ç»“æžœæ ·å¼
  const getResultClass = (testValue, standardValue) => {
    // ç®€å•的结果判断逻辑,实际项目中可能需要更复杂的判断
    if (testValue === "合格") {
      return "result-passed";
    } else if (testValue === "不合格") {
      return "result-rejected";
    }
    return "";
  };
  // ä¸‹è½½æŠ¥å‘Š
  const downloadReport = () => {
    uni.showToast({
      title: "报告下载中...",
      icon: "loading",
    });
    // æ¨¡æ‹Ÿä¸‹è½½
    setTimeout(() => {
      uni.showToast({
        title: "报告下载成功",
        icon: "success",
      });
    }, 1500);
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // èŽ·å–è¯¦æƒ…æ•°æ®
  const getDetail = () => {
    const id = getPageId();
    if (!id) {
      showToast("参数错误");
      return;
    }
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–è¯¦æƒ…æ•°æ®
    try {
      const detailDataFromStorage = uni.getStorageSync(
        "processInspectionEditData"
      );
      if (detailDataFromStorage) {
        detailData.value = detailDataFromStorage;
        // ä½¿ç”¨qualityInspectParamInfo获取检验项目
        qualityInspectParamInfo(id)
          .then(res => {
            if (res.data && res.data.length > 0) {
              inspectionItems.value = res.data;
            } else if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              // å¦‚果接口没有返回数据,使用本地存储中的数据
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          })
          .catch(error => {
            console.error("获取检验项目失败:", error);
            // æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¸­çš„æ•°æ®æˆ–模拟数据
            if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          });
      }
    } catch (error) {
      console.error("加载详情数据失败:", error);
      showToast("加载详情数据失败,请重试");
    }
  };
  onShow(() => {
    getDetail();
  });
  onMounted(() => {
    getDetail();
  });
</script>
<style scoped lang="scss">
  .material-inspection-detail {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 20px;
  }
  .detail-section {
    padding: 15px;
  }
  .detail-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
  }
  .header-icon {
    width: 40px;
    height: 40px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
  }
  .header-icon.secondary {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  }
  .header-icon.tertiary {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  }
  .header-title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .status-tag {
    margin-left: 20rpx;
  }
  .detail-content {
    padding-top: 8px;
  }
  .detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .detail-row:last-child {
    border-bottom: none;
  }
  .detail-label {
    font-size: 14px;
    color: #666;
    min-width: 100px;
  }
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  // æ£€éªŒé¡¹ç›®
  .inspection-items {
    padding-top: 8px;
  }
  .inspection-item {
    padding: 12px;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 10px;
  }
  .inspection-item:last-child {
    margin-bottom: 0;
  }
  .item-name {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f0f0;
  }
  .item-details {
    padding-top: 8px;
  }
  .item-detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 6px;
  }
  .item-detail-row:last-child {
    margin-bottom: 0;
  }
  .item-detail-label {
    font-size: 12px;
    color: #666;
    min-width: 60px;
  }
  .item-detail-value {
    font-size: 12px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  .item-detail-value.result {
    font-weight: 500;
  }
  .result-passed {
    color: #67c23a;
  }
  .result-rejected {
    color: #f56c6c;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
</style>
src/pages/qualityManagement/processInspection/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,566 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    qualityInspectFileAdd,
    qualityInspectFileListPage,
    qualityInspectFileDel,
  } from "@/api/qualityManagement/materialInspection";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                inspectId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              qualityInspectFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    qualityInspectFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "qualityInspectFileId"
    );
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    qualityInspectFileListPage({
      inspectId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/qualityManagement/processInspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,775 @@
<template>
  <view class="material-inspection-page">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="过程检验"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <up-search
        placeholder="请输入工序搜索"
        v-model="searchForm.process"
        @search="getList"
        @custom="getList"
        @clear="getList"
        :show-action="true"
        action-text="搜索"
        :animation="true"
      ></up-search>
    </view>
    <!-- ç»Ÿè®¡ä¿¡æ¯å¡ç‰‡ -->
    <!-- <view class="stats-cards">
      <view class="stat-card">
        <text class="stat-number">{{ totalCount }}</text>
        <text class="stat-label">总检验</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ submittedCount }}</text>
        <text class="stat-label">已提交</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ pendingCount }}</text>
        <text class="stat-label">待提交</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ qualifiedCount }}</text>
        <text class="stat-label">已合格</text>
      </view>
    </view> -->
    <!-- æ£€éªŒåˆ—表 -->
    <view class="inspection-list"
          v-if="inspectionList.length > 0">
      <view v-for="(item, index) in inspectionList"
            :key="index">
        <view class="inspection-item"
              @click="viewDetail(item)">
          <view class="item-header">
            <view class="item-left">
              <!-- <view class="material-icon"
                    :class="getStateClass(item.inspectState)">
                <up-icon :name="getStateIcon(item.inspectState)"
                         size="16"
                         color="#ffffff"></up-icon>
              </view> -->
              <view class="material-info">
                <text class="material-name">{{ item.productName }}</text>
                <text class="material-code">{{ item.model }}</text>
              </view>
            </view>
            <view class="status-tags">
              <u-tag :type="getTagType(item.checkResult)"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
              </u-tag>
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
                     class="status-tag">
                {{ item.inspectState ? '已提交' : '未提交' }}
              </u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">检测日期</text>
              <text class="detail-value">{{ formatDateTime(item.checkTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">生产工单号</text>
              <text class="detail-value">{{ item.workOrderNo || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工序</text>
              <text class="detail-value">{{ item.process || '-' }}</text>
            </view>
            <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">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <!-- <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button> -->
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button> -->
          </view>
          <view class="action-buttons">
            <!-- <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
              é™„ä»¶
            </u-button>
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button> -->
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验任务"></up-empty>
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <!-- <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view> -->
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              :range="true"
              @confirm="confirmDate" />
    <!-- åˆ†é…æ£€éªŒå‘˜å¼¹çª— -->
    <up-popup v-model:show="showAssignDialog"
              mode="center"
              round
              style="width: 80%">
      <view class="assign-dialog">
        <view class="dialog-header">
          <text class="dialog-title">分配检验员</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showAssignDialog = false"></up-icon>
        </view>
        <view class="dialog-content">
          <up-form-item label="检验员"
                        prop="checkName"
                        :label-width="60"
                        required>
            <up-input v-model="assignForm.checkName"
                      placeholder="请选择检验员"
                      readonly />
            <template #right>
              <up-icon @click="showInspectorSheet = true"
                       name="arrow-right" />
            </template>
          </up-form-item>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="showAssignDialog = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="submitAssign">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import {
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // æœç´¢è¡¨å•
  const searchForm = ref({
    process: "",
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
  });
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  const showAssignDialog = ref(false);
  const showInspectorSheet = ref(false);
  const assignForm = ref({
    checkName: "",
  });
  const currentAssignRow = ref(null);
  // æ£€éªŒåˆ—表数据
  const inspectionList = ref([]);
  // åˆ†é¡µæ•°æ®
  const page = ref({
    current: -1,
    size: -1,
    total: 0,
  });
  // åŠ è½½çŠ¶æ€
  const tableLoading = ref(false);
  // ç»Ÿè®¡æ•°æ®
  const totalCount = ref(0);
  const submittedCount = ref(0);
  const pendingCount = ref(0);
  const qualifiedCount = ref(0);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // ActionSheet选项
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  // èŽ·å–çŠ¶æ€æ ·å¼
  const getStateClass = inspectState => {
    return inspectState ? "state-submitted" : "state-pending";
  };
  // èŽ·å–çŠ¶æ€å›¾æ ‡
  const getStateIcon = inspectState => {
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = inspectState => {
    return inspectState ? "success" : "warning";
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    searchForm.value.entryDate = e.value;
    searchForm.value.entryDateStart = dayjs(e.value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(e.value[1]).format("YYYY-MM-DD");
    getList();
  };
  const viewFileList = item => {
    uni.setStorageSync("qualityInspectFileId", item.id);
    uni.navigateTo({
      url: "/pages/qualityManagement/processInspection/fileList",
    });
  };
  // æ¸…除日期范围
  const clearDateRange = () => {
    searchForm.value.entryDate = undefined;
    searchForm.value.entryDateStart = undefined;
    searchForm.value.entryDateEnd = undefined;
    getList();
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 1 })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
        page.value.total = res.data.total || 0;
        totalCount.value = res.data.total || 0;
        submittedCount.value = inspectionList.value.filter(
          item => item.inspectState
        ).length;
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        showToast("获取列表失败,请重试");
      });
  };
  // ç¼–辑检验
  const startInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    // å­˜å‚¨å®Œæ•´çš„æ£€éªŒæ•°æ®
    uni.setStorageSync("processInspectionEditData", item);
    // è·³è½¬åˆ°ç¼–辑页面
    uni.navigateTo({
      url: `/pages/qualityManagement/processInspection/add?id=${item.id}&isEdit=true`,
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    uni.setStorageSync("processInspectionEditData", item);
    // è·³è½¬åˆ°è¯¦æƒ…页面
    uni.navigateTo({
      url: `/pages/qualityManagement/processInspection/detail?id=${item.id}`,
    });
  };
  // æ–°å¢žæ£€éªŒ
  const addInspection = () => {
    uni.navigateTo({
      url: "/pages/qualityManagement/processInspection/add",
    });
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async 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("提交失败,请重试");
    }
  };
  // åˆ†é…æ£€éªŒå‘˜
  const assignInspector = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    currentAssignRow.value = item;
    getUserList();
    showAssignDialog.value = true;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    assignForm.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // æäº¤åˆ†é…
  const submitAssign = async () => {
    if (!currentAssignRow.value || !assignForm.value.checkName) {
      showToast("请选择检验员");
      return;
    }
    try {
      const data = {
        ...assignForm.value,
        id: currentAssignRow.value.id,
      };
      const res = await qualityInspectUpdate(data);
      if (res.code === 200) {
        showToast("分配成功");
        showAssignDialog.value = false;
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("分配失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("分配失败:", error);
      showToast("分配失败,请重试");
    }
  };
  // å¤„理分页
  const handlePagination = obj => {
    page.value.current = obj.current;
    page.value.size = obj.size;
    getList();
  };
  onMounted(() => {
    getList();
    getUserList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .material-inspection-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  // æœç´¢åŒºåŸŸ
  .search-section {
    padding: 20rpx 30rpx;
    background-color: #ffffff;
    position: sticky;
    top: 0;
    z-index: 10;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .stats-cards {
    display: flex;
    padding: 15px;
    gap: 10px;
    background: #fff;
    margin-bottom: 10px;
  }
  .stat-card {
    flex: 1;
    background: #2979ff;
    border-radius: 12px;
    padding: 15px;
    text-align: center;
    color: #fff;
    box-shadow: 0 2px 8px rgba(41, 121, 255, 0.2);
  }
  .stat-number {
    display: block;
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 5px;
  }
  .stat-label {
    font-size: 12px;
    opacity: 0.9;
  }
  // æ£€éªŒåˆ—表
  .inspection-list {
    padding: 20px;
  }
  .inspection-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding: 0 16px;
    &:active {
      transform: scale(0.98);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    }
  }
  .item-header {
    padding: 16px 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .item-left {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .material-icon {
    width: 24px;
    height: 24px;
    background: #2979ff;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .state-pending {
    background: #ff9900;
  }
  .state-submitted {
    background: #52c41a;
  }
  .material-info {
    flex: 1;
  }
  .material-name {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  .material-code {
    font-size: 12px;
    color: #999;
    margin-left: 8px;
  }
  .status-tags {
    display: flex;
    gap: 8px;
  }
  .status-tag {
    margin: 0;
  }
  .date-range {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 10px;
    padding: 8px 12px;
    background: #f8f9fa;
    border-radius: 8px;
  }
  .date-text {
    font-size: 12px;
    color: #666;
  }
  // è¯¦æƒ…行
  .item-details {
    padding: 16px 0;
  }
  .detail-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 8px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .detail-label {
    font-size: 12px;
    color: #777777;
    min-width: 60px;
  }
  .detail-value {
    font-size: 12px;
    color: #000000;
    text-align: right;
    flex: 1;
    margin-left: 16px;
  }
  // æ“ä½œæŒ‰é’®
  .action-buttons {
    display: flex;
    gap: 12px;
    padding: 0 0 16px 0;
    justify-content: space-between;
  }
  .action-btn {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
  // æµ®åŠ¨æŒ‰é’®
  .fab-button {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 56px;
    height: 56px;
    background: #2979ff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
    z-index: 1000;
  }
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  .assign-dialog {
    padding: 24px;
    background: #ffffff;
    border-radius: 16px;
    overflow: hidden;
  }
  .dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid #f0f0f0;
  }
  .dialog-title {
    font-size: 18px;
    font-weight: 600;
    color: #303133;
  }
  .dialog-content {
    margin-bottom: 24px;
  }
  .dialog-footer {
    display: flex;
    gap: 16px;
    padding-top: 16px;
    border-top: 1px solid #f0f0f0;
  }
  .footer-btn {
    flex: 1;
    height: 44px;
    font-size: 16px;
  }
  // è¾“入框样式
  :deep(.up-input__inner) {
    border-radius: 8px;
    height: 44px;
    font-size: 14px;
  }
  // è¡¨å•项样式
  :deep(.up-form-item) {
    margin-bottom: 0;
  }
  :deep(.up-form-item__label) {
    font-size: 14px;
    color: #606266;
    margin-bottom: 8px;
  }
  // æŒ‰é’®æ ·å¼
  :deep(.up-button--primary) {
    border-radius: 8px;
  }
  :deep(.up-button--default) {
    border-radius: 8px;
  }
  // åˆ†é¡µç»„ä»¶
  .pagination {
    padding: 20px;
    background: #fff;
    margin-top: 10px;
    display: flex;
    justify-content: center;
  }
</style>
src/pages/qualityManagement/rawMaterial/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,335 @@
<template>
  <view class="raw-material-page">
    <PageHeader title="原材料" @back="goBack" />
    <!-- æœç´¢ä¸Žç­›é€‰ -->
    <view class="search-section">
      <up-search
        placeholder="请输入批号"
        v-model="searchForm.batchNo"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
      ></up-search>
      <view class="filter-row">
        <view class="filter-item" @click="showTypeSelect = true">
          <text>{{ typeLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
        <view class="filter-item" @click="showStatusSelect = true">
          <text>{{ statusLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- åˆ—表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-header">
          <text class="product-name">{{ item.productName }}</text>
          <up-tag :text="item.inspectState ? '已提交' : '未提交'" :type="item.inspectState ? 'success' : 'warning'" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">批次号:</text>
            <text class="item-value">{{ item.batchNo }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">检验类型:</text>
            <text class="item-value">{{ item.checkTypeText || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">规格型号:</text>
            <text class="item-value">{{ item.model || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">检测日期:</text>
            <text class="item-value">{{ item.checkTime || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">检测结果:</text>
            <up-tag :text="item.checkResult === 1 ? '合格' : '不合格'" :type="item.checkResult === 1 ? 'success' : 'error'" size="mini"></up-tag>
          </view>
        </view>
        <view class="item-actions">
          <up-button v-if="!item.inspectState" type="primary" size="mini" @click.stop="openForm('edit', item)">编辑</up-button>
          <up-button v-if="!item.inspectState" type="success" size="mini" @click.stop="handleConfirmSubmit(item)">提交</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button" @click="openForm('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- ç±»åž‹é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="typeActions"
      :show="showTypeSelect"
      @close="showTypeSelect = false"
      @select="selectType"
      title="请选择类别"
    ></up-action-sheet>
    <!-- çŠ¶æ€é€‰æ‹©å™¨ -->
    <up-action-sheet
      :actions="statusActions"
      :show="showStatusSelect"
      @close="showStatusSelect = false"
      @select="selectStatus"
      title="请选择提交状态"
    ></up-action-sheet>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import {
  findRawMaterialListPage,
  submitRawMaterial,
  deleteRawMaterial
} from '@/api/qualityManagement/rawMaterial.js';
import { toast, showConfirm } from '@/utils/common';
import useUserStore from '@/store/modules/user';
const userStore = useUserStore();
const searchForm = reactive({
  batchNo: '',
  checkType: '',
  inspectState: ''
});
const tableData = ref([]);
const page = reactive({
  current: 1,
  size: 20,
  total: 0
});
const loadStatus = ref('loadmore');
const showTypeSelect = ref(false);
const typeActions = [
  { name: '全部', value: '' },
  { name: '入厂检', value: '0' },
  { name: '车间检', value: '1' },
  { name: '出厂检', value: '2' }
];
const typeLabel = computed(() => {
  const action = typeActions.find(a => a.value === searchForm.checkType);
  return action ? action.name : '全部类别';
});
const showStatusSelect = ref(false);
const statusActions = [
  { name: '全部', value: '' },
  { name: '未提交', value: '0' },
  { name: '已提交', value: '1' }
];
const statusLabel = computed(() => {
  const action = statusActions.find(a => a.value === searchForm.inspectState);
  return action ? action.name : '全部状态';
});
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const params = {
    batchNo: searchForm.batchNo || null,
    checkType: searchForm.checkType || null,
    inspectState: searchForm.inspectState || null,
    current: page.current,
    size: page.size
  };
  findRawMaterialListPage(params).then(res => {
    const records = res?.data?.records || [];
    if (page.current === 1) {
      tableData.value = records;
    } else {
      tableData.value = [...tableData.value, ...records];
    }
    page.total = res?.data?.total || 0;
    if (tableData.value.length >= page.total) {
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
  });
};
const handleQuery = () => {
  page.current = 1;
  page.total = 0;
  tableData.value = [];
  loadStatus.value = 'loadmore';
  getList();
};
const selectType = (e) => {
  searchForm.checkType = e.value;
  handleQuery();
};
const selectStatus = (e) => {
  searchForm.inspectState = e.value;
  handleQuery();
};
const openForm = (type, item) => {
  // Mobile usually navigates to a new page for add/edit if complex
  // Here we'll just show a toast for now as the actual form components are many
  toast('功能开发中,请在PC端操作');
};
const handleConfirmSubmit = (row) => {
  showConfirm('确认提交该检验记录吗?').then(res => {
    if (res.confirm) {
      submitRawMaterial(row.id).then(() => {
        toast('提交成功');
        handleQuery();
      });
    }
  });
};
const handleDelete = (row) => {
  showConfirm('确认删除该记录吗?').then(res => {
    if (res.confirm) {
      deleteRawMaterial({ id: row.id }).then(() => {
        toast('删除成功');
        handleQuery();
      });
    }
  });
};
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
});
</script>
<style lang="scss" scoped>
.raw-material-page {
  padding-bottom: 20rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
}
.filter-row {
  display: flex;
  justify-content: space-around;
  padding: 10rpx 0;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 10rpx;
  font-size: 28rpx;
  color: #606266;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.product-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 180rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.no-data {
  padding-top: 200rpx;
}
.fab-button {
  position: fixed;
  right: 40rpx;
  bottom: 60rpx;
  width: 100rpx;
  height: 100rpx;
  background-color: #3c9cff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4rpx 16rpx rgba(60, 156, 255, 0.4);
  z-index: 99;
}
</style>
src/pages/qualityManagement/visualization/qualityDashboard.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,276 @@
<template>
  <view class="quality-dashboard-page">
    <PageHeader title="质量看板" @back="goBack" />
    <scroll-view scroll-y class="dashboard-scroll">
      <!-- æ ·å“çŠ¶æ€åˆ—è¡¨ -->
      <view class="dashboard-card">
        <view class="card-header">
          <text class="card-title">检测样品动态状态</text>
          <up-switch v-model="voiceEnabled" size="18" activeText="语音" inactiveText="静音"></up-switch>
        </view>
        <view class="status-list">
          <view v-for="item in sampleStatus" :key="item.id" class="status-item">
            <view class="status-left">
              <view class="status-dot" :class="item.status"></view>
              <text class="sample-name">{{ item.name }}</text>
            </view>
            <view class="status-right">
              <up-tag :text="statusLabel(item.status)" :type="statusTagType(item.status)" size="mini"></up-tag>
              <text class="sample-time">{{ item.time }}</text>
            </view>
          </view>
        </view>
      </view>
      <!-- åˆæ ¼çŽ‡åˆ†æž (仪表盘) -->
      <view class="dashboard-card">
        <view class="card-header">
          <text class="card-title">合格率分析</text>
        </view>
        <view class="chart-box">
          <qiun-data-charts
            type="gauge"
            :opts="gaugeOpts"
            :chartData="gaugeData"
          />
        </view>
        <view class="passrate-summary">
          <text>当前合格率:</text>
          <text class="highlight">{{ (passRate * 100).toFixed(1) }}%</text>
        </view>
      </view>
      <!-- ä»»åŠ¡æŽ’è¡Œ (柱状图) -->
      <view class="dashboard-card">
        <view class="card-header">
          <text class="card-title">任务排行 (Top 10)</text>
        </view>
        <view class="chart-box">
          <qiun-data-charts
            type="column"
            :opts="columnOpts"
            :chartData="columnData"
          />
        </view>
      </view>
      <!-- åŽ†å²è¶‹åŠ¿ (折线图) -->
      <view class="dashboard-card">
        <view class="card-header">
          <text class="card-title">历史趋势</text>
        </view>
        <view class="chart-box">
          <qiun-data-charts
            type="line"
            :opts="lineOpts"
            :chartData="lineData"
          />
        </view>
      </view>
    </scroll-view>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
const voiceEnabled = ref(false);
let dataTimer = null;
// 1) æ ·å“åŠ¨æ€çŠ¶æ€
const sampleStatus = ref([]);
const statusPool = ['processing', 'warning', 'error', 'success'];
const statusLabel = (s) => {
  const labels = { 'processing': '检测中', 'warning': '预警', 'error': '不合格', 'success': '合格' };
  return labels[s] || '未知';
};
const statusTagType = (s) => {
  const types = { 'processing': 'primary', 'warning': 'warning', 'error': 'error', 'success': 'success' };
  return types[s] || 'info';
};
const randomSample = () => {
  const id = Math.random().toString(36).slice(2, 8);
  const status = statusPool[Math.floor(Math.random() * statusPool.length)];
  const name = `样品-${Math.floor(Math.random() * 900 + 100)}`;
  const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
  return { id, name, status, time };
};
// 2) åˆæ ¼çŽ‡åˆ†æž (仪表盘)
const passRate = ref(0.92);
const gaugeData = ref({
  categories: [{ value: 0.92, color: "#2fc25b" }],
  series: [{ name: "合格率", data: 0.92 }]
});
const gaugeOpts = {
  title: { name: "合格率", color: "#2fc25b", fontSize: 16 },
  subtitle: { name: "92%", color: "#666666", fontSize: 12 },
  extra: { gauge: { type: "default", width: 15, labelColor: "#666666", splitLine: { fixRadius: -10 } } }
};
// 3) ä»»åŠ¡æŽ’è¡Œ (柱状图)
const columnData = ref({
  categories: ["任务1", "任务2", "任务3", "任务4", "任务5"],
  series: [{ name: "完成数", data: [35, 36, 31, 33, 13] }]
});
const columnOpts = {
  color: ["#1890FF"],
  padding: [15, 15, 0, 5],
  enableScroll: false,
  xAxis: { disableGrid: true },
  yAxis: { data: [{ min: 0 }] },
  extra: { column: { type: "group", width: 30, activeBgColor: "#000000", activeBgOpacity: 0.08 } }
};
// 4) åŽ†å²è¶‹åŠ¿ (折线图)
const lineData = ref({
  categories: ["10:00", "10:05", "10:10", "10:15", "10:20"],
  series: [
    { name: "来样数", data: [35, 8, 25, 37, 4, 20] },
    { name: "完成数", data: [70, 40, 65, 100, 44, 68] }
  ]
});
const lineOpts = {
  color: ["#1890FF", "#91CB74"],
  padding: [15, 10, 0, 15],
  enableScroll: false,
  xAxis: { disableGrid: true },
  yAxis: { gridType: "dash", dashLength: 2 },
  legend: { position: "top" },
  extra: { line: { type: "straight", width: 2 } }
};
const refreshData = () => {
  // æ¨¡æ‹Ÿæ•°æ®æ›´æ–°
  const next = randomSample();
  sampleStatus.value = [next, ...sampleStatus.value].slice(0, 5);
  const delta = (Math.random() - 0.5) * 0.02;
  passRate.value = Math.min(0.99, Math.max(0.6, passRate.value + delta));
  gaugeData.value = {
    series: [{ name: "合格率", data: passRate.value }]
  };
  gaugeOpts.subtitle.name = (passRate.value * 100).toFixed(1) + '%';
};
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  for (let i = 0; i < 5; i++) {
    sampleStatus.value.push(randomSample());
  }
  dataTimer = setInterval(refreshData, 3000);
});
onBeforeUnmount(() => {
  if (dataTimer) clearInterval(dataTimer);
});
</script>
<style lang="scss" scoped>
.quality-dashboard-page {
  background-color: #f5f7fa;
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.dashboard-scroll {
  flex: 1;
  padding: 20rpx;
}
.dashboard-card {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 30rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30rpx;
  border-left: 8rpx solid #3c9cff;
  padding-left: 20rpx;
}
.card-title {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.status-list {
  display: flex;
  flex-direction: column;
  gap: 20rpx;
}
.status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10rpx 0;
}
.status-left {
  display: flex;
  align-items: center;
  gap: 15rpx;
}
.status-dot {
  width: 12rpx;
  height: 12rpx;
  border-radius: 50%;
  &.processing { background-color: #3c9cff; }
  &.warning { background-color: #f9ae3d; }
  &.error { background-color: #f56c6c; }
  &.success { background-color: #5ac725; }
}
.sample-name {
  font-size: 28rpx;
  color: #303133;
}
.status-right {
  display: flex;
  align-items: center;
  gap: 20rpx;
}
.sample-time {
  font-size: 24rpx;
  color: #909399;
}
.chart-box {
  width: 100%;
  height: 400rpx;
}
.passrate-summary {
  text-align: center;
  margin-top: 20rpx;
  font-size: 28rpx;
  color: #606266;
  .highlight {
    font-size: 36rpx;
    font-weight: bold;
    color: #3c9cff;
    margin-left: 10rpx;
  }
}
</style>
src/utils/native.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
/**
 * åŽŸç”Ÿèƒ½åŠ›å°è£…ï¼ˆæ”¯æŒæ¡ä»¶ç¼–è¯‘ï¼‰
 */
/**
 * æ‰«ç 
 */
export function scanCode(): Promise<any> {
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS || MP-WEIXIN
    uni.scanCode({
      success: (res) => resolve(res),
      fail: (err) => reject(err)
    });
    // #endif
    // #ifdef H5
    // H5端通常需要通过微信JS-SDK或特定的扫码库
    uni.showToast({ title: 'H5端扫码需接入微信JS-SDK', icon: 'none' });
    reject(new Error('H5 scan not implemented'));
    // #endif
  });
}
/**
 * æ‹ç…§æˆ–选择图片
 */
export function chooseImage(count: number = 1): Promise<any> {
  return new Promise((resolve, reject) => {
    uni.chooseImage({
      count,
      sizeType: ['compressed'],
      sourceType: ['camera', 'album'],
      success: (res) => resolve(res),
      fail: (err) => reject(err)
    });
  });
}
/**
 * è“ç‰™åˆå§‹åŒ–(示例)
 */
export function initBluetooth(): Promise<any> {
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS || MP-WEIXIN
    uni.openBluetoothAdapter({
      success: (res) => resolve(res),
      fail: (err) => reject(err)
    });
    // #endif
    // #ifdef H5
    reject(new Error('H5 does not support bluetooth'));
    // #endif
  });
}