1ed366885433dfdec1241312356535b868c39eee..2dd434830299e781cd942ef5e6e938160dd12704
2026-03-03 zhangwencui
原材料检验功能开发
2dd434 对比 | 目录
2026-03-03 zhangwencui
巡检管理更名设备巡检
57c610 对比 | 目录
已添加5个文件
已修改6个文件
7023 ■■■■ 文件已修改
src/api/inspectionManagement/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionUpload/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/materialInspection.js 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/indexItem.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 3732 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/add.vue 1134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 526 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/fileList.vue 566 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 814 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionManagement/index.js
@@ -1,4 +1,4 @@
// å·¡æ£€ç®¡ç†
// è®¾å¤‡å·¡æ£€
import request from '@/utils/request'
// å·¡æ£€ä»»åŠ¡è¡¨è¡¨æŸ¥è¯¢
@@ -35,7 +35,7 @@
}
// /inspectionTask/addOrEditInspectionTask
// å·¡æ£€ä¸Šä¼ 
// è®¾å¤‡å·¡æ£€
export function uploadInspectionTask(query) {
    return request({
        url: '/inspectionTask/addOrEditInspectionTask',
src/api/inspectionUpload/index.js
@@ -1,4 +1,4 @@
// å·¡æ£€ä¸Šä¼ 
// è®¾å¤‡å·¡æ£€
import request from '@/utils/request'
// äºŒç»´ç ç®¡ç†è¡¨æŸ¥è¯¢
src/api/qualityManagement/materialInspection.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
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,
    })
}
src/pages.json
@@ -599,7 +599,7 @@
    {
      "path": "pages/inspectionUpload/index",
      "style": {
        "navigationBarTitleText": "巡检上传",
        "navigationBarTitleText": "设备巡检",
        "navigationStyle": "custom",
        "enablePullDownRefresh": true,
        "backgroundColor": "#f8f8f8"
@@ -893,6 +893,34 @@
      }
    },
    {
      "path": "pages/qualityManagement/materialInspection/index",
      "style": {
        "navigationBarTitleText": "原材料检验",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/materialInspection/add",
      "style": {
        "navigationBarTitleText": "原材料检验添加",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/materialInspection/detail",
      "style": {
        "navigationBarTitleText": "原材料检验详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/materialInspection/fileList",
      "style": {
        "navigationBarTitleText": "原材料检验附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/message",
      "style": {
        "navigationBarTitleText": "消息中心"
src/pages/index.vue
@@ -137,6 +137,31 @@
        </up-grid>
      </view>
    </view>
    <!-- è´¨é‡ç®¡ç†æ¨¡å— -->
    <view class="common-module collaboration-module"
          v-if="hasQualityItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">质量管理</text>
        </view>
      </view>
      <view class="module-content">
        <up-grid :border="false"
                 col="4">
          <up-grid-item v-for="(item, index) in qualityItems"
                        :key="index"
                        @click="handleCommonItemClick(item)">
            <view class="icon-container"
                  :style="{ background: item.bgColor }">
              <up-icon :name="item.icon"
                       :size="58"
                       color="#ffffff"></up-icon>
            </view>
            <text class="item-label">{{item.label}}</text>
          </up-grid-item>
        </up-grid>
      </view>
    </view>
    <!-- äººåŠ›èµ„æºæ¨¡å— -->
    <view class="common-module collaboration-module"
          v-if="hasHumanResourcesItems">
@@ -314,6 +339,12 @@
      label: "合同管理",
    },
  ]);
  const qualityItems = reactive([
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "原材料检验",
    },
  ]);
  const safetyItems = reactive([
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
@@ -429,7 +460,7 @@
    },
    {
      icon: "/static/images/icon/xunjianshangchuan@2x.png",
      label: "巡检上传",
      label: "设备巡检",
    },
  ]);
@@ -645,7 +676,7 @@
          url: "/pages/equipmentManagement/upkeep/index",
        });
        break;
      case "巡检上传":
      case "设备巡检":
        uni.navigateTo({
          url: "/pages/inspectionUpload/index",
        });
@@ -723,6 +754,11 @@
      case "合同管理":
        uni.navigateTo({
          url: "/pages/humanResources/contractManagement/index",
        });
        break;
      case "原材料检验":
        uni.navigateTo({
          url: "/pages/qualityManagement/materialInspection/index",
        });
        break;
      default:
@@ -1001,6 +1037,15 @@
      ...filteredHumanResources
    );
    // è¿‡æ»¤è´¨é‡ç®¡ç†èœå•
    const originalQuality = [
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "原材料检验" },
    ];
    const filteredQuality = originalQuality.filter(item => {
      return allowedMenuTitles.has(item.label);
    });
    qualityItems.splice(0, qualityItems.length, ...filteredQuality);
    // è¿‡æ»¤ç”Ÿäº§ç®¡æŽ§èœå•
    const originalProduction = [
      {
@@ -1019,7 +1064,7 @@
      { icon: "/static/images/icon/shbeibaoxiu@2x.png", label: "运行管理" },
      { icon: "/static/images/icon/shbeibaoxiu@2x.png", label: "设备报修" },
      { icon: "/static/images/icon/shbeibaoyang@2x.png", label: "设备保养" },
      { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "巡检上传" },
      { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "设备巡检" },
    ];
    const filteredEquipment = originalEquipment.filter(item => {
      return allowedMenuTitles.has(item.label);
@@ -1032,6 +1077,7 @@
  const hasPurchaseItems = computed(() => purchaseItems.length > 0);
  const hasCollaborationItems = computed(() => collaborationItems.length > 0);
  const hasSafetyItems = computed(() => safetyItems.length > 0);
  const hasQualityItems = computed(() => qualityItems.length > 0);
  const hasHumanResourcesItems = computed(() => humanResourcesItems.length > 0);
  const hasProductionItems = computed(() => productionItems.length > 0);
  const hasEquipmentItems = computed(() => equipmentItems.length > 0);
src/pages/indexItem.vue
@@ -330,7 +330,7 @@
          url: "/pages/equipmentManagement/upkeep/index",
        });
        break;
      case "巡检上传":
      case "设备巡检":
        uni.navigateTo({
          url: "/pages/inspectionUpload/index",
        });
@@ -378,8 +378,8 @@
      // åªæå– label å‚数的值,去除可能附加的查询参数
      let labelValue = options.label;
      // å¦‚æžœ label åŒ…含 ? ç¬¦å·ï¼Œåªå– ? ä¹‹å‰çš„部分
      if (labelValue.includes('?')) {
        labelValue = labelValue.split('?')[0];
      if (labelValue.includes("?")) {
        labelValue = labelValue.split("?")[0];
      }
      operationType.value = labelValue;
      if (operationType.value === "考勤管理") {
src/pages/inspectionUpload/index.vue
@@ -1,20 +1,25 @@
<template>
  <view class="inspection-upload-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="巡检上传" @back="goBack" />
    <PageHeader title="设备巡检"
                @back="goBack" />
    <!-- æ•°æ®åˆ—表 -->
    <view class="table-section">
      <!-- ç”Ÿäº§å·¡æ£€åˆ—表 -->
      <view class="task-list">
        <view v-for="(item, index) in taskTableData" :key="index" class="task-item">
        <view v-for="(item, index) in taskTableData"
              :key="index"
              class="task-item">
          <view class="task-header">
            <view class="task-info">
              <text class="task-name">{{ item.taskName }}</text>
              <text class="task-location">{{ item.inspectionLocation }}</text>
            </view>
            <view class="task-actions">
              <u-button type="primary" size="small" @click.stop="startScanForTask(item)" :customStyle="{
              <u-button type="primary"
                        size="small"
                        @click.stop="startScanForTask(item)"
                        :customStyle="{
                borderRadius: '15px',
                height: '30px',
                fontSize: '12px',
@@ -22,7 +27,10 @@
              }">
                æ‰«ç ä¸Šä¼ 
              </u-button>
              <u-button type="success" size="small" @click.stop="viewAttachments(item)" :customStyle="{
              <u-button type="success"
                        size="small"
                        @click.stop="viewAttachments(item)"
                        :customStyle="{
                borderRadius: '15px',
                height: '30px',
                fontSize: '12px'
@@ -51,106 +59,146 @@
            <view class="detail-item">
              <text class="detail-label">巡检状态</text>
              <view class="detail-value">
                <uni-tag v-if="item.fileStatus==2" text="已完成" size="small" type="success" inverted></uni-tag>
                <uni-tag v-else-if="item.fileStatus==1" text="巡检中" size="small" type="primary" inverted></uni-tag>
                <uni-tag v-else="" text="未巡检" size="small" type="warning" inverted></uni-tag>
                <uni-tag v-if="item.fileStatus==2"
                         text="已完成"
                         size="small"
                         type="success"
                         inverted></uni-tag>
                <uni-tag v-else-if="item.fileStatus==1"
                         text="巡检中"
                         size="small"
                         type="primary"
                         inverted></uni-tag>
                <uni-tag v-else=""
                         text="未巡检"
                         size="small"
                         type="warning"
                         inverted></uni-tag>
              </view>
            </view>
          </view>
        </view>
        <uni-load-more :status="loadMoreStatus"></uni-load-more>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="taskTableData?.length === 0" class="no-data">
      <view v-if="taskTableData?.length === 0"
            class="no-data">
        <text>暂无数据</text>
      </view>
    </view>
    <!-- å›¾ç‰‡ä¸Šä¼ å¼¹çª— - åŽŸç”Ÿå®žçŽ° -->
    <view v-if="showUploadDialog" class="custom-modal-overlay" @click="closeUploadDialog">
      <view class="custom-modal-container" @click.stop>
    <view v-if="showUploadDialog"
          class="custom-modal-overlay"
          @click="closeUploadDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="upload-popup-content">
          <view class="upload-popup-header">
            <text class="upload-popup-title">上传巡检记录</text>
          </view>
          <view class="upload-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="upload-tabs">
              <view class="tab-item" :class="{ active: currentUploadType === 'before' }"
                @click="switchUploadType('before')">
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'before' }"
                    @click="switchUploadType('before')">
                ç”Ÿäº§å‰
              </view>
              <view class="tab-item" :class="{ active: currentUploadType === 'after' }"
                @click="switchUploadType('after')">
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'after' }"
                    @click="switchUploadType('after')">
                ç”Ÿäº§ä¸­
              </view>
              <view class="tab-item" :class="{ active: currentUploadType === 'issue' }"
                @click="switchUploadType('issue')">
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'issue' }"
                    @click="switchUploadType('issue')">
                ç”Ÿäº§åŽ
              </view>
            </view>
            <!-- å¼‚常状态选择 -->
            <view class="exception-section">
              <text class="section-title">是否存在异常?</text>
              <view class="exception-options">
                <view class="exception-option" :class="{ active: hasException === false }"
                  @click="setExceptionStatus(false)">
                  <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
                <view class="exception-option"
                      :class="{ active: hasException === false }"
                      @click="setExceptionStatus(false)">
                  <u-icon name="checkmark-circle"
                          size="20"
                          color="#52c41a"></u-icon>
                  <text>正常</text>
                </view>
                <view class="exception-option" :class="{ active: hasException === true }"
                  @click="setExceptionStatus(true)">
                  <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon>
                <view class="exception-option"
                      :class="{ active: hasException === true }"
                      @click="setExceptionStatus(true)">
                  <u-icon name="close-circle"
                          size="20"
                          color="#ff4d4f"></u-icon>
                  <text>存在异常</text>
                </view>
              </view>
            </view>
            <!-- å½“前分类的上传区域 -->
            <view class="simple-upload-area">
              <view class="upload-buttons">
                <u-button type="primary" @click="chooseMedia('image')" :loading="uploading"
                  :disabled="getCurrentFiles().length >= uploadConfig.limit"
                  :customStyle="{ marginRight: '10px', flex: 1 }">
                  <u-icon name="camera" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
                <u-button type="primary"
                          @click="chooseMedia('image')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ marginRight: '10px', flex: 1 }">
                  <u-icon name="camera"
                          size="18"
                          color="#fff"
                          style="margin-right: 5px;"></u-icon>
                  {{ uploading ? '上传中...' : '拍照' }}
                </u-button>
                <u-button type="success" @click="chooseMedia('video')" :loading="uploading"
                  :disabled="getCurrentFiles().length >= uploadConfig.limit" :customStyle="{ flex: 1 }">
                  <uni-icons type="videocam" name="videocam" size="18" color="#fff"
                    style="margin-right: 5px;"></uni-icons>
                <u-button type="success"
                          @click="chooseMedia('video')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ flex: 1 }">
                  <uni-icons type="videocam"
                             name="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px;"></uni-icons>
                  {{ uploading ? '上传中...' : '拍视频' }}
                </u-button>
              </view>
              <!-- ä¸Šä¼ è¿›åº¦ -->
              <view v-if="uploading" class="upload-progress">
                <u-line-progress :percentage="uploadProgress" :showText="true" activeColor="#409eff"></u-line-progress>
              <view v-if="uploading"
                    class="upload-progress">
                <u-line-progress :percentage="uploadProgress"
                                 :showText="true"
                                 activeColor="#409eff"></u-line-progress>
              </view>
              <!-- å½“前分类的文件列表 -->
              <view v-if="getCurrentFiles().length > 0" class="file-list">
                <view v-for="(file, index) in getCurrentFiles()" :key="index" class="file-item">
              <view v-if="getCurrentFiles().length > 0"
                    class="file-list">
                <view v-for="(file, index) in getCurrentFiles()"
                      :key="index"
                      class="file-item">
                  <view class="file-preview-container">
                    <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                      :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                      class="file-preview" mode="aspectFill" />
                    <view v-else-if="file.type === 'video'" class="video-preview">
                      <uni-icons type="videocam" name="videocam" size="18" color="#fff"
                        style="margin-right: 5px;"></uni-icons>
                           :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                           class="file-preview"
                           mode="aspectFill" />
                    <view v-else-if="file.type === 'video'"
                          class="video-preview">
                      <uni-icons type="videocam"
                                 name="videocam"
                                 size="18"
                                 color="#fff"
                                 style="margin-right: 5px;"></uni-icons>
                      <text class="video-text">视频</text>
                    </view>
                    <!-- åˆ é™¤æŒ‰é’® -->
                    <view class="delete-btn" @click="removeFile(index)">
                      <u-icon name="close" size="12" color="#fff"></u-icon>
                    <view class="delete-btn"
                          @click="removeFile(index)">
                      <u-icon name="close"
                              size="12"
                              color="#fff"></u-icon>
                    </view>
                  </view>
                  <view class="file-info">
                    <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频')
                      }}</text>
@@ -158,11 +206,10 @@
                  </view>
                </view>
              </view>
              <view v-if="getCurrentFiles().length === 0" class="empty-state">
              <view v-if="getCurrentFiles().length === 0"
                    class="empty-state">
                <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
              </view>
              <!-- ç»Ÿè®¡ä¿¡æ¯ -->
              <view class="upload-summary">
                <text class="summary-text">
@@ -173,59 +220,77 @@
              </view>
            </view>
          </view>
          <view class="upload-popup-footer">
            <u-button @click="closeUploadDialog" :customStyle="{ marginRight: '10px' }">取消</u-button>
            <u-button v-if="hasException === true" type="warning" @click="goToRepair"
              :customStyle="{ marginRight: '10px' }">
            <u-button @click="closeUploadDialog"
                      :customStyle="{ marginRight: '10px' }">取消</u-button>
            <u-button v-if="hasException === true"
                      type="warning"
                      @click="goToRepair"
                      :customStyle="{ marginRight: '10px' }">
              æ–°å¢žæŠ¥ä¿®
            </u-button>
            <u-button type="primary" @click="submitUpload">提交</u-button>
            <u-button type="primary"
                      @click="submitUpload">提交</u-button>
          </view>
        </view>
      </view>
    </view>
    <!-- æŸ¥çœ‹é™„件弹窗 -->
    <view v-if="showAttachmentDialog" class="custom-modal-overlay" @click="closeAttachmentDialog">
      <view class="custom-modal-container" @click.stop>
    <view v-if="showAttachmentDialog"
          class="custom-modal-overlay"
          @click="closeAttachmentDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="attachment-popup-content">
          <view class="attachment-popup-header">
            <text class="attachment-popup-title">查看附件 - {{ currentViewTask?.taskName }}</text>
            <view class="close-btn-attachment" @click="closeAttachmentDialog">
              <u-icon name="close" size="16" color="#666"></u-icon>
            <view class="close-btn-attachment"
                  @click="closeAttachmentDialog">
              <u-icon name="close"
                      size="16"
                      color="#666"></u-icon>
            </view>
          </view>
          <view class="attachment-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="attachment-tabs">
              <view class="tab-item" :class="{ active: currentViewType === 'before' }"
                @click="switchViewType('before')">
              <view class="tab-item"
                    :class="{ active: currentViewType === 'before' }"
                    @click="switchViewType('before')">
                ç”Ÿäº§å‰ ({{ getAttachmentsByType(0).length }})
              </view>
              <view class="tab-item" :class="{ active: currentViewType === 'after' }" @click="switchViewType('after')">
              <view class="tab-item"
                    :class="{ active: currentViewType === 'after' }"
                    @click="switchViewType('after')">
                ç”Ÿäº§ä¸­ ({{ getAttachmentsByType(1).length }})
              </view>
              <view class="tab-item" :class="{ active: currentViewType === 'issue' }" @click="switchViewType('issue')">
              <view class="tab-item"
                    :class="{ active: currentViewType === 'issue' }"
                    @click="switchViewType('issue')">
                ç”Ÿäº§åŽ ({{ getAttachmentsByType(2).length }})
              </view>
            </view>
            <!-- å½“前分类的附件列表 -->
            <view class="attachment-content">
              <view v-if="getCurrentViewAttachments().length > 0" class="attachment-list">
                <view v-for="(file, index) in getCurrentViewAttachments()" :key="index" class="attachment-item"
                  @click="previewAttachment(file)">
              <view v-if="getCurrentViewAttachments().length > 0"
                    class="attachment-list">
                <view v-for="(file, index) in getCurrentViewAttachments()"
                      :key="index"
                      class="attachment-item"
                      @click="previewAttachment(file)">
                  <view class="attachment-preview-container">
                    <image v-if="file.type === 'image' || isImageFile(file)" :src="file.url || file.downloadUrl"
                      class="attachment-preview" mode="aspectFill" />
                    <view v-else class="attachment-video-preview">
                      <u-icon name="video" size="24" color="#409eff"></u-icon>
                    <image v-if="file.type === 'image' || isImageFile(file)"
                           :src="file.url || file.downloadUrl"
                           class="attachment-preview"
                           mode="aspectFill" />
                    <view v-else
                          class="attachment-video-preview">
                      <u-icon name="video"
                              size="24"
                              color="#409eff"></u-icon>
                      <text class="video-text">视频</text>
                    </view>
                  </view>
                  <view class="attachment-info">
                    <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件'
                      }}</text>
@@ -233,8 +298,8 @@
                  </view>
                </view>
              </view>
              <view v-else class="attachment-empty">
              <view v-else
                    class="attachment-empty">
                <text>该分类暂无附件</text>
              </view>
            </view>
@@ -242,19 +307,28 @@
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
    <view v-if="showVideoDialog" class="video-modal-overlay" @click="closeVideoPreview">
      <view class="video-modal-container" @click.stop>
    <view v-if="showVideoDialog"
          class="video-modal-overlay"
          @click="closeVideoPreview">
      <view class="video-modal-container"
            @click.stop>
        <view class="video-modal-header">
          <text class="video-modal-title">{{ currentVideoFile?.originalFilename || '视频预览' }}</text>
          <view class="close-btn-video" @click="closeVideoPreview">
            <u-icon name="close" size="16" color="#fff"></u-icon>
          <view class="close-btn-video"
                @click="closeVideoPreview">
            <u-icon name="close"
                    size="16"
                    color="#fff"></u-icon>
          </view>
        </view>
        <view class="video-modal-body">
          <video v-if="currentVideoFile" :src="currentVideoFile.url || currentVideoFile.downloadUrl"
            class="video-player" controls autoplay @error="handleVideoError"></video>
          <video v-if="currentVideoFile"
                 :src="currentVideoFile.url || currentVideoFile.downloadUrl"
                 class="video-player"
                 controls
                 autoplay
                 @error="handleVideoError"></video>
        </view>
      </view>
    </view>
@@ -262,1826 +336,1856 @@
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick, computed, reactive } from 'vue'
import { onShow, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getLedgerById } from '@/api/equipmentManagement/ledger.js'
import { inspectionTaskList, uploadInspectionTask } from "@/api/inspectionManagement";
import { getToken } from "@/utils/auth";
import config from '@/config'
  import { onMounted, onUnmounted, ref, nextTick, computed, reactive } from "vue";
  import { onShow, onReachBottom, onPullDownRefresh } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { getLedgerById } from "@/api/equipmentManagement/ledger.js";
  import {
    inspectionTaskList,
    uploadInspectionTask,
  } from "@/api/inspectionManagement";
  import { getToken } from "@/utils/auth";
  import config from "@/config";
// ç»„件引用已移除
  // ç»„件引用已移除
// åŠ è½½æç¤ºæ–¹æ³•
const showLoadingToast = (message) => {
  uni.showLoading({
    title: message,
    mask: true
  })
}
const closeToast = () => {
  uni.hideLoading()
}
// è¡¨æ ¼æ•°æ®
const taskTableData = ref([]) // ç”Ÿäº§å·¡æ£€æ•°æ®
// å½“前扫描的任务
const currentScanningTask = ref(null)
const infoData = ref(null);
// ä¸Šä¼ ç›¸å…³çŠ¶æ€
const showUploadDialog = ref(false)
const uploadFiles = ref([]) // ä¿ç•™ç”¨äºŽå…¼å®¹æ€§
const uploadStatusType = ref(0)
const uploading = ref(false)
const uploadProgress = ref(0)
const number = ref(0)
const uploadList = ref([])
// ä¸‰ä¸ªåˆ†ç±»çš„上传状态
const beforeModelValue = ref([]) // ç”Ÿäº§å‰
const afterModelValue = ref([])  // ç”Ÿäº§ä¸­
const issueModelValue = ref([])  // ç”Ÿäº§åŽ
// å½“前激活的上传类型
const currentUploadType = ref('before') // 'before', 'after', 'issue'
// æŸ¥çœ‹é™„件相关状态
const showAttachmentDialog = ref(false)
const currentViewTask = ref(null)
const currentViewType = ref('before') // 'before', 'after', 'issue'
const attachmentList = ref([]) // å½“前查看任务的附件列表
// è§†é¢‘预览相关状态
const showVideoDialog = ref(false)
const currentVideoFile = ref(null)
// å¼‚常状态
const hasException = ref(null) // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
// ä¸Šä¼ é…ç½®
const uploadConfig = {
  action: "/file/upload",
  limit: 10,
  fileSize: 50, // MB
  fileType: ['jpg', 'jpeg', 'png', 'mp4', 'mov'],
  maxVideoDuration: 60 // ç§’
}
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
    const baseUrl = 'http://114.132.189.42:9030';
  return baseUrl + uploadConfig.action;
})
// è®¡ç®—请求头
const headers = computed(() => {
  const token = getToken();
  return token ? { Authorization: "Bearer " + token } : {};
})
// è¯·æ±‚取消标志,用于取消正在进行的请求
let isRequestCancelled = false
const pagesPames = reactive({
  size: 10,
  current:1
})
const loadMoreStatus = computed(()=>{
  if(loading.value){
    return 'loading'
  }
  if(noMore.value){
    return 'noMore'
  }
  return 'more'
})
const totalSize = ref(0)
const noMore = computed(()=>{
  return taskTableData.value.length>=totalSize.value
})
const loading = ref(false)
const reloadPage = ()=>{
  pagesPames.current = 1
  taskTableData.value = []
  getList()
}
const loadPage = ()=>{
  if(noMore.value||loading.value)return;
  pagesPames.current += 1
  getList()
}
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM已渲染
  // nextTick(() => {
  //   getList()
  // })
})
onReachBottom(() => {
  loadPage()
})
onPullDownRefresh(() => {
  reloadPage()
  uni.stopPullDownRefresh()
})
onShow(() => {
  // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°æ•°æ®
  reloadPage()
})
// ç»„件销毁时的清理
onUnmounted(() => {
  // è®¾ç½®å–消标志,阻止后续的异步操作
  isRequestCancelled = true
  // å…³é—­ä¸Šä¼ å¼¹çª—
  if (showUploadDialog.value) {
    showUploadDialog.value = false
  }
})
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  // æ˜¾ç¤ºåŠ è½½æç¤º
  // showLoadingToast('加载中...')
  // è®¾ç½®å–消标志
  isRequestCancelled = false
  loading.value = true
  inspectionTaskList({...pagesPames}).then(res => {
    // æ£€æŸ¥ç»„件是否还存在且请求未被取消
    if (!isRequestCancelled) {
      // å¤„理不同的数据结构
      let records = [];
      if (res && res.data) {
        // å°è¯•多种可能的数据结构
        totalSize.value = res.data.total
        if (Array.isArray(res.data.records)) {
          records = res.data.records;
        } else if (Array.isArray(res.data.rows)) {
          records = res.data.rows;
        } else if (Array.isArray(res.data)) {
          records = res.data;
        } else if (Array.isArray(res.data.list)) {
          records = res.data.list;
        }
      }
      if (records.length > 0) {
        taskTableData.value = [...taskTableData.value,...records.map((record)=>{
          record.fileStatus = getFileStatus(record)
          return record
        })];
      } else {
        taskTableData.value = [];
        uni.showToast({
          title: '暂无巡检任务数据',
          icon: 'none'
        });
      }
    }
    loading.value =false
    // å…³é—­åŠ è½½æç¤º
    // closeToast()
  }).catch(err => {
    // æ£€æŸ¥ç»„件是否还存在且请求未被取消
    if (!isRequestCancelled) {
      taskTableData.value = [];
      // æ·»åŠ é”™è¯¯æç¤º
      uni.showToast({
        title: '获取数据失败',
        icon: 'error'
      })
    }
    loading.value =false
    // å…³é—­åŠ è½½æç¤º
    // closeToast()
  })
}
const getFileStatus = (record)=>{
  let _beforeProduction = record.beforeProduction&&record.beforeProduction.length
  let _afterProduction = record.afterProduction&&record.afterProduction.length
  let _productionIssues = record.productionIssues&&record.productionIssues.length
  if(_beforeProduction&&_afterProduction&&_productionIssues){
    return 2
  }else if(_beforeProduction||_afterProduction||_productionIssues){
    return 1
  }else{
    return 0
  }
}
// ä¸ºæŒ‡å®šä»»åŠ¡å¼€å§‹æ‰«ç ï¼ˆçœŸæœºï¼‰
const startScanForTask = async (task) => {
  try {
    currentScanningTask.value = task
    uni.scanCode({
      success: (res) => {
        handleScanSuccess(res)
      },
      fail: (err) => {
        console.error('扫码失败:', err)
        uni.showToast({
          title: '扫码失败',
          icon: 'error'
        })
      }
    })
  } catch (e) {
    console.error('启动扫码失败:', e)
    uni.showToast({
      title: '启动扫码失败',
      icon: 'error'
    })
  }
}
// æ‰«ç æˆåŠŸå¤„ç†ï¼šæ ¡éªŒåŽæ‰“å¼€ä¸Šä¼ å¼¹çª—
const handleScanSuccess = (result) => {
  try {
    // è§£æžäºŒç»´ç æ•°æ®ï¼Œæå–deviceId
    let deviceId = ''
    if (result?.result && typeof result.result === 'string') {
      if (result.result.includes('deviceId=')) {
        const match = result.result.match(/deviceId=(\d+)/)
        if (match && match[1]) deviceId = match[1]
      } else {
        try {
          const qrData = JSON.parse(result.result)
          deviceId = qrData.deviceId || qrData.qrCodeId || ''
        } catch (e) {
          deviceId = result.result
        }
      }
    }
    if (!deviceId) {
      uni.showToast({ title: '未识别到设备ID', icon: 'error' })
      return
    }
    const currentTaskId = currentScanningTask.value?.taskId || currentScanningTask.value?.id
    if (!currentTaskId) {
      uni.showToast({ title: '任务信息缺失', icon: 'error' })
      return
    }
    if (deviceId === currentTaskId.toString()) {
      uni.showToast({ title: '识别成功', icon: 'success' })
      openUploadDialog(currentScanningTask.value)
    } else {
      uni.showToast({ title: '请扫描正确的设备', icon: 'error' })
    }
  } catch (error) {
    console.error('扫码结果处理失败:', error)
    uni.showToast({
      title: error?.message || '数据解析失败',
      icon: 'error'
    })
  }
}
// æ‰“开上传弹窗
const openUploadDialog = (task) => {
  // è®¾ç½®ä»»åŠ¡ä¿¡æ¯åˆ°infoData
  if (task) {
    infoData.value = {
      ...task,
      taskId: task.taskId || task.id,
      storageBlobDTO: [] // åˆå§‹åŒ–文件列表
    };
  }
  // è®¾ç½®ä¸Šä¼ çŠ¶æ€ç±»åž‹ï¼ˆå¯ä»¥æ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ä¸åŒçš„çŠ¶æ€ï¼‰
  uploadStatusType.value = 0 // é»˜è®¤çŠ¶æ€
  // æ¸…空之前的文件
  uploadFiles.value = []
  // æ˜¾ç¤ºä¸Šä¼ å¼¹çª—
  showUploadDialog.value = true
}
// å…³é—­ä¸Šä¼ å¼¹çª—
const closeUploadDialog = () => {
  showUploadDialog.value = false
  uploadFiles.value = []
  // æ¸…理三个分类的数据
  beforeModelValue.value = []
  afterModelValue.value = []
  issueModelValue.value = []
  currentUploadType.value = 'before'
  hasException.value = null // é‡ç½®å¼‚常状态
  infoData.value = null // æ¸…理任务数据
}
// åˆ‡æ¢ä¸Šä¼ ç±»åž‹
const switchUploadType = (type) => {
  currentUploadType.value = type
}
// èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
const getCurrentFiles = () => {
  switch (currentUploadType.value) {
    case 'before':
      return beforeModelValue.value||[]
    case 'after':
      return afterModelValue.value||[]
    case 'issue':
      return issueModelValue.value||[]
    default:
      return []
  }
}
// èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
const getUploadTypeText = () => {
  switch (currentUploadType.value) {
    case 'before':
      return '生产前'
    case 'after':
      return '生产中'
    case 'issue':
      return '生产后'
    default:
      return ''
  }
}
// å¤„理上传文件更新
const handleUploadUpdate = (files) => {
  uploadFiles.value = files
}
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status
}
// è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
const goToRepair = () => {
  try {
    // å­˜å‚¨å½“前任务信息到本地存储,供报修页面使用
    const taskInfo = {
      taskId: infoData.value?.taskId || infoData.value?.id,
      taskName: infoData.value?.taskName,
      inspectionLocation: infoData.value?.inspectionLocation,
      inspector: infoData.value?.inspector,
      // ä¼ é€’当前上传的文件信息
      uploadedFiles: {
        before: beforeModelValue.value,
        after: afterModelValue.value,
        issue: issueModelValue.value
      }
    }
    uni.setStorageSync('repairTaskInfo', JSON.stringify(taskInfo))
    // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
    uni.navigateTo({
      url: '/pages/equipmentManagement/repair/add'
    })
    // å…³é—­ä¸Šä¼ å¼¹çª—
    closeUploadDialog()
  } catch (error) {
    console.error('跳转报修页面失败:', error)
    uni.showToast({
      title: '跳转失败,请重试',
      icon: 'error'
    })
  }
}
// æäº¤ä¸Šä¼ 
const submitUpload = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择是否存在异常',
        icon: 'none'
      })
      return
    }
    // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•文件上传
    const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length;
    if (totalFiles === 0) {
      uni.showToast({
        title: '请先上传文件',
        icon: 'none'
      })
      return
    }
    // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
    showLoadingToast('提交中...')
    // æŒ‰ç…§æ‚¨çš„逻辑合并所有分类的文件
    let arr = [];
    if (beforeModelValue.value.length > 0) {
      arr.push(...beforeModelValue.value);
    }
    if (afterModelValue.value.length > 0) {
      arr.push(...afterModelValue.value);
    }
    if (issueModelValue.value.length > 0) {
      arr.push(...issueModelValue.value);
    }
    // ä¼ ç»™åŽç«¯çš„临时文件ID列表(tempFileIds)
    // å…¼å®¹ï¼šæœ‰äº›æŽ¥å£å¯èƒ½è¿”回 tempId / tempFileId / id
    let tempFileIds = []
    if (arr !== null && arr.length > 0) {
      tempFileIds = arr
        .map((item) => item?.tempId ?? item?.tempFileId ?? item?.id)
        .filter((v) => v !== undefined && v !== null && v !== '')
    }
    // æäº¤æ•°æ®
    infoData.value.storageBlobDTO = arr;
    // æ·»åŠ å¼‚å¸¸çŠ¶æ€ä¿¡æ¯
    infoData.value.hasException = hasException.value;
    infoData.value.tempFileIds = tempFileIds;
    const result = await uploadInspectionTask({ ...infoData.value });
    // æ£€æŸ¥æäº¤ç»“æžœ
    if (result && (result.code === 200 || result.success)) {
      // æäº¤æˆåŠŸ
      closeToast(); // å…³é—­åŠ è½½æç¤º
      uni.showToast({
        title: '提交成功',
        icon: 'success'
      })
      // å…³é—­å¼¹çª—
      closeUploadDialog()
      // åˆ·æ–°åˆ—表
      setTimeout(() => {
        reloadPage()
      }, 500)
    } else {
      // æäº¤å¤±è´¥
      closeToast();
      uni.showToast({
        title: result?.msg || result?.message || '提交失败',
        icon: 'error'
      })
    }
  } catch (error) {
    console.error('提交上传失败:', error)
    closeToast(); // å…³é—­åŠ è½½æç¤º
    let errorMessage = '提交失败';
    if (error.message) {
      errorMessage = error.message;
    } else if (error.msg) {
      errorMessage = error.msg;
    } else if (typeof error === 'string') {
      errorMessage = error;
    }
    uni.showToast({
      title: errorMessage,
      icon: 'error'
    })
  }
}
// æŸ¥çœ‹é™„ä»¶
const viewAttachments = async (task) => {
  try {
    currentViewTask.value = task
    currentViewType.value = 'before'
    // è§£æžæ–°çš„æ•°æ®ç»“æž„
    attachmentList.value = []
    // åŽç«¯åæ˜¾å­—段(你提供的数据结构):
    // - commonFileListBefore:生产前(通常 type=10)
    // - commonFileListAfter:生产中(通常 type=11)
    // - commonFileList:可能是全部/兜底(若包含生产后,一般 type=12)
    const allList = Array.isArray(task?.commonFileList) ? task.commonFileList : []
    const beforeList = Array.isArray(task?.commonFileListBefore)
      ? task.commonFileListBefore
      : allList.filter(f => f?.type === 10)
    const afterList = Array.isArray(task?.commonFileListAfter)
      ? task.commonFileListAfter
      : allList.filter(f => f?.type === 11)
    // å¦‚果后端后续补了 commonFileListIssue,则优先用;否则从 commonFileList é‡ŒæŒ‰ type=12 å…œåº•
    const issueList = Array.isArray(task?.commonFileListIssue)
      ? task.commonFileListIssue
      : allList.filter(f => f?.type === 12)
    const mapToViewFile = (file, viewType) => {
      const u = normalizeFileUrl(file?.url || file?.downloadUrl || '')
      return {
        ...file,
        // ç”¨äºŽä¸‰æ ‡ç­¾é¡µåˆ†ç»„:0=生产前 1=生产中 2=生产后
        type: viewType,
        name: file?.name || file?.originalFilename || file?.bucketFilename,
        bucketFilename: file?.bucketFilename || file?.name,
        originalFilename: file?.originalFilename || file?.name,
        url: u,
        downloadUrl: u,
        size: file?.size || file?.byteSize,
      }
    }
    attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0)))
    attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1)))
    attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2)))
    showAttachmentDialog.value = true
  } catch (error) {
    uni.showToast({
      title: '获取附件失败',
      icon: 'error'
    })
  }
}
// å…³é—­é™„件查看弹窗
const closeAttachmentDialog = () => {
  showAttachmentDialog.value = false
  currentViewTask.value = null
  attachmentList.value = []
  currentViewType.value = 'before'
}
// åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
const switchViewType = (type) => {
  currentViewType.value = type
}
// æ ¹æ®type获取对应分类的附件
const getAttachmentsByType = (typeValue) => {
  return attachmentList.value.filter(file => file.type === typeValue) || []
}
// èŽ·å–type值
const getTabType = () => {
  switch (currentUploadType.value) {
    case 'before':
      return 10
    case 'after':
      return 11
    case 'issue':
      return 12
    default:
      return 10
  }
}
// èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
const getCurrentViewAttachments = () => {
  switch (currentViewType.value) {
    case 'before':
      return getAttachmentsByType(0)
    case 'after':
      return getAttachmentsByType(1)
    case 'issue':
      return getAttachmentsByType(2)
    default:
      return []
  }
}
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
const isImageFile = (file) => {
  // æ£€æŸ¥contentType字段
  if (file.contentType && file.contentType.startsWith('image/')) {
    return true
  }
  // æ£€æŸ¥åŽŸæœ‰çš„type字段
  if (file.type === 'image') return true
  // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名
  const name = file.bucketFilename || file.originalFilename || file.name || ''
  const ext = name.split('.').pop()?.toLowerCase()
  return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)
}
// æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸï¼ˆåŽç«¯è¦æ±‚前缀)
const filePreviewBase = 'http://114.132.189.42:9098'
// å°†åŽç«¯è¿”回的文件地址规范成可访问URL
// å…¼å®¹åœºæ™¯ï¼š
// - å·²ç»æ˜¯ http/https:直接返回
// - ä»¥ / å¼€å¤´ï¼šæ‹¼æŽ¥ filePreviewBase
// - Windows æœ¬åœ°è·¯å¾„(如 D:\ruoyi\prod\uploads...\xx.jpg):尝试截取 prod ä¹‹åŽçš„相对路径并拼接 filePreviewBase
const normalizeFileUrl = (rawUrl) => {
  try {
    if (!rawUrl || typeof rawUrl !== 'string') return ''
    const url = rawUrl.trim()
    if (!url) return ''
    if (/^https?:\/\//i.test(url)) return url
    if (url.startsWith('/')) return `${filePreviewBase}${url}`
    // Windows path -> web path
    if (/^[a-zA-Z]:\\/.test(url)) {
      const normalized = url.replace(/\\/g, '/')
      const idx = normalized.indexOf('/prod/')
      if (idx >= 0) {
        const relative = normalized.slice(idx + '/prod/'.length)
        return `${filePreviewBase}/${relative}`
      }
      // å…œåº•:无法推断映射规则时,至少把反斜杠变成正斜杠
      return normalized
    }
    // å…¶ä»–相对路径:直接用 baseUrl æ‹¼ä¸€ä¸‹
    return `${filePreviewBase}/${url.replace(/^\//, '')}`
  } catch (e) {
    return rawUrl || ''
  }
}
// é¢„览附件
const previewAttachment = (file) => {
  if (isImageFile(file)) {
    // é¢„览图片
    const imageUrls = getCurrentViewAttachments()
      .filter(f => isImageFile(f))
      .map(f => f.url || f.downloadUrl)
    uni.previewImage({
      urls: imageUrls,
      current: file.url || file.downloadUrl
    })
  } else {
    // é¢„览视频 - æ˜¾ç¤ºè§†é¢‘播放弹窗
    showVideoPreview(file)
  }
}
// æ˜¾ç¤ºè§†é¢‘预览
const showVideoPreview = (file) => {
  currentVideoFile.value = file
  showVideoDialog.value = true
}
// å…³é—­è§†é¢‘预览
const closeVideoPreview = () => {
  showVideoDialog.value = false
  currentVideoFile.value = null
}
// è§†é¢‘播放错误处理
const handleVideoError = (error) => {
  uni.showToast({
    title: '视频播放失败',
    icon: 'error'
  })
}
// æ‹ç…§/拍视频(真机优先用 chooseMedia;不支持则降级)
const chooseMedia = (type) => {
  if (getCurrentFiles().length >= uploadConfig.limit) {
    uni.showToast({ title: `最多只能选择${uploadConfig.limit}个文件`, icon: 'none' })
    return
  }
  const remaining = uploadConfig.limit - getCurrentFiles().length
  // ä¼˜å…ˆï¼šchooseMedia(支持 image/video)
  if (typeof uni.chooseMedia === 'function') {
    uni.chooseMedia({
      count: Math.min(remaining, 1),
      mediaType: [type || 'image'],
      sizeType: ['compressed', 'original'],
      sourceType: ['camera'],
      success: (res) => {
        try {
          const files = res?.tempFiles || []
          if (!files.length) throw new Error('未获取到文件')
          files.forEach((tf, idx) => {
            const filePath = tf.tempFilePath || tf.path || ''
            const fileType = tf.fileType || type || 'image'
            const ext = fileType === 'video' ? 'mp4' : 'jpg'
            const file = {
              tempFilePath: filePath,
              path: filePath,
              type: fileType,
              name: `${fileType}_${Date.now()}_${idx}.${ext}`,
              size: tf.size || 0,
              duration: tf.duration || 0,
              createTime: Date.now(),
              uid: Date.now() + Math.random() + idx
            }
            handleBeforeUpload(file)
          })
        } catch (e) {
          console.error('处理拍摄结果失败:', e)
          uni.showToast({ title: '处理文件失败', icon: 'error' })
        }
      },
      fail: (err) => {
        console.error('拍摄失败:', err)
        uni.showToast({ title: '拍摄失败', icon: 'error' })
      }
    })
    return
  }
  // é™çº§ï¼šchooseImage / chooseVideo
  if (type === 'video') {
    chooseVideo()
  } else {
    uni.chooseImage({
      count: 1,
      sizeType: ['compressed', 'original'],
      sourceType: ['camera'],
      success: (res) => {
        const tempFilePath = res?.tempFilePaths?.[0]
        const tempFile = res?.tempFiles?.[0] || {}
        if (!tempFilePath) return
        handleBeforeUpload({
          tempFilePath,
          path: tempFilePath,
          type: 'image',
          name: `photo_${Date.now()}.jpg`,
          size: tempFile.size || 0,
          createTime: Date.now(),
          uid: Date.now() + Math.random()
        })
      }
    })
  }
}
// æ‹ç…§
const chooseImage = () => {
  if (uploadFiles.value.length >= uploadConfig.limit) {
    uni.showToast({
      title: `最多只能拍摄${uploadConfig.limit}个文件`,
      icon: 'none'
    });
    return;
  }
  uni.chooseMedia({
    count: 1,
    mediaType: ['image', 'video'],
    sizeType: ['compressed', 'original'],
    sourceType: ['camera'],
    success: (res) => {
      try {
        if (!res.tempFiles || res.tempFiles.length === 0) {
          throw new Error('未获取到图片文件');
        }
        const tempFilePath = res.tempFiles[0];
        const tempFile = res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
        const file = {
          tempFilePath: tempFilePath,
          path: tempFilePath, // ä¿æŒå…¼å®¹æ€§
          type: 'image',
          name: `photo_${Date.now()}.jpg`,
          size: tempFile.size || 0,
          createTime: new Date().getTime(),
          uid: Date.now() + Math.random()
        };
        handleBeforeUpload(file);
      } catch (error) {
        console.error('处理拍照结果失败:', error);
        uni.showToast({
          title: '处理图片失败',
          icon: 'error'
        });
      }
    },
    fail: (err) => {
      console.error('拍照失败:', err);
      uni.showToast({
        title: '拍照失败: ' + (err.errMsg || '未知错误'),
        icon: 'error'
      });
    }
  });
}
// æ‹è§†é¢‘
const chooseVideo = () => {
  if (uploadFiles.value.length >= uploadConfig.limit) {
    uni.showToast({
      title: `最多只能拍摄${uploadConfig.limit}个文件`,
      icon: 'none'
    });
    return;
  }
  uni.chooseVideo({
    sourceType: ['camera'],
    maxDuration: uploadConfig.maxVideoDuration,
    camera: 'back',
    success: (res) => {
      try {
        if (!res.tempFilePath) {
          throw new Error('未获取到视频文件');
        }
        const file = {
          tempFilePath: res.tempFilePath,
          path: res.tempFilePath, // ä¿æŒå…¼å®¹æ€§
          type: 'video',
          name: `video_${Date.now()}.mp4`,
          size: res.size || 0,
          duration: res.duration || 0,
          createTime: new Date().getTime(),
          uid: Date.now() + Math.random()
        };
        handleBeforeUpload(file);
      } catch (error) {
        console.error('处理拍视频结果失败:', error);
        uni.showToast({
          title: '处理视频失败',
          icon: 'error'
        });
      }
    },
    fail: (err) => {
      console.error('拍视频失败:', err);
      uni.showToast({
        title: '拍视频失败: ' + (err.errMsg || '未知错误'),
        icon: 'error'
      });
    }
  });
}
// åˆ é™¤æ–‡ä»¶
const removeFile = (index) => {
  uni.showModal({
    title: '确认删除',
    content: '确定要删除这个文件吗?',
    success: (res) => {
      if (res.confirm) {
        // æ ¹æ®å½“前上传类型删除对应分类的文件
        switch (currentUploadType.value) {
          case 'before':
            beforeModelValue.value.splice(index, 1);
            break;
          case 'after':
            afterModelValue.value.splice(index, 1);
            break;
          case 'issue':
            issueModelValue.value.splice(index, 1);
            break;
        }
        uni.showToast({
          title: '删除成功',
          icon: 'success'
        });
      }
    }
  });
}
// æ£€æŸ¥ç½‘络连接
const checkNetworkConnection = () => {
  return new Promise((resolve) => {
    uni.getNetworkType({
      success: (res) => {
        if (res.networkType === 'none') {
          resolve(false);
        } else {
          resolve(true);
        }
      },
      fail: () => {
        resolve(false);
      }
    });
  });
}
// ä¸Šä¼ å‰æ ¡éªŒ
const handleBeforeUpload = async (file) => {
  // æ ¡éªŒæ–‡ä»¶ç±»åž‹
  if (uploadConfig.fileType && Array.isArray(uploadConfig.fileType) && uploadConfig.fileType.length > 0) {
    const fileName = file.name || '';
    const fileExtension = fileName ? fileName.split('.').pop().toLowerCase() : '';
    // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
    let expectedTypes = [];
    if (file.type === 'image') {
      expectedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    } else if (file.type === 'video') {
      expectedTypes = ['mp4', 'mov', 'avi', 'wmv'];
    }
    // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
    if (fileExtension && expectedTypes.length > 0) {
      const isAllowed = expectedTypes.some(type =>
        uploadConfig.fileType.includes(type) && type === fileExtension
      );
      if (!isAllowed) {
        uni.showToast({
          title: `文件格式不支持,请拍摄 ${expectedTypes.join('/')} æ ¼å¼çš„æ–‡ä»¶`,
          icon: 'none'
        });
        return false;
      }
    }
  }
  // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
  uploadFile(file);
  return true;
}
// æ–‡ä»¶ä¸Šä¼ å¤„理(真机走 uni.uploadFile)
const uploadFile = async (file) => {
  uploading.value = true;
  uploadProgress.value = 0;
  number.value++; // å¢žåŠ ä¸Šä¼ è®¡æ•°
  // ç¡®ä¿token存在
  const token = getToken();
  if (!token) {
    handleUploadError('用户未登录');
    return;
  }
  const typeValue = getTabType(); // ç”Ÿäº§å‰:10, ç”Ÿäº§ä¸­:11, ç”Ÿäº§åŽ:12
  uploadWithUniUploadFile(file, file.tempFilePath || file.path || '', typeValue, token);
}
// ä½¿ç”¨uni.uploadFile上传(非H5环境或H5回退方案)
const uploadWithUniUploadFile = (file, filePath, typeValue, token) => {
  if (!filePath) {
    handleUploadError('文件路径不存在');
    return;
  }
  const uploadTask = uni.uploadFile({
    url: uploadFileUrl.value,
    filePath: filePath,
    name: 'file',
    formData: {
      type: typeValue
    },
    header: {
      'Authorization': `Bearer ${token}`
    },
    success: (res) => {
      try {
        if (res.statusCode === 200) {
          const response = JSON.parse(res.data);
          if (response.code === 200) {
            handleUploadSuccess(response, file);
            uni.showToast({
              title: '上传成功',
              icon: 'success'
            });
          } else {
            handleUploadError(response.msg || '服务器返回错误');
          }
        } else {
          handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
        }
      } catch (e) {
        console.error('解析响应失败:', e);
        console.error('原始响应数据:', res.data);
        handleUploadError('响应数据解析失败: ' + e.message);
      }
    },
    fail: (err) => {
      console.error('上传失败:', err.errMsg || err);
      number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
      let errorMessage = '上传失败';
      if (err.errMsg) {
        if (err.errMsg.includes('statusCode: null')) {
          errorMessage = '网络连接失败,请检查网络设置';
        } else if (err.errMsg.includes('timeout')) {
          errorMessage = '上传超时,请重试';
        } else if (err.errMsg.includes('fail')) {
          errorMessage = '上传失败,请检查网络连接';
        } else {
          errorMessage = err.errMsg;
        }
      }
      handleUploadError(errorMessage);
    },
    complete: () => {
      uploading.value = false;
      uploadProgress.value = 0;
    }
  });
  // ç›‘听上传进度
  if (uploadTask && uploadTask.onProgressUpdate) {
    uploadTask.onProgressUpdate((res) => {
      uploadProgress.value = res.progress;
    });
  }
}
// ä¸Šä¼ å¤±è´¥å¤„理
const handleUploadError = (message = '上传文件失败', showRetry = false) => {
  uploading.value = false;
  uploadProgress.value = 0;
  if (showRetry) {
    uni.showModal({
      title: '上传失败',
      content: message + ',是否重试?',
      success: (res) => {
        if (res.confirm) {
          // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
        }
      }
    });
  } else {
    uni.showToast({
  // åŠ è½½æç¤ºæ–¹æ³•
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      icon: 'error'
      mask: true,
    });
  }
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
const handleUploadSuccess = (res, file) => {
  console.log('上传成功响应:', res);
  // å¤„理不同的数据结构:可能是数组,也可能是单个对象
  let uploadedFile = null;
  uploadedFile = res.data;
  if (!uploadedFile) {
    console.error('无法解析上传响应数据:', res);
    number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
    handleUploadError('上传响应数据格式错误', false);
    return;
  }
  // æ ¹æ®å½“前上传类型设置type字段
  let typeValue = 0; // é»˜è®¤ä¸ºç”Ÿäº§å‰
  switch (currentUploadType.value) {
    case 'before':
      typeValue = 0;
      break;
    case 'after':
      typeValue = 1;
      break;
    case 'issue':
      typeValue = 2;
      break;
  }
  // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id和type
  const fileData = {
    ...file,
    id: uploadedFile.id, // æ·»åŠ æœåŠ¡å™¨è¿”å›žçš„id
    tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
    url: uploadedFile.url || uploadedFile.downloadUrl || file.tempFilePath || file.path,
    bucketFilename: uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name,
    downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
    size: uploadedFile.size || uploadedFile.byteSize || file.size,
    createTime: uploadedFile.createTime || new Date().getTime(),
    type: typeValue // æ·»åŠ ç±»åž‹å­—æ®µï¼š0=生产前, 1=生产中, 2=生产后
  };
  const closeToast = () => {
    uni.hideLoading();
  };
  uploadList.value.push(fileData);
  // ç«‹å³æ·»åŠ åˆ°å¯¹åº”çš„åˆ†ç±»ï¼Œä¸ç­‰å¾…æ‰€æœ‰æ–‡ä»¶ä¸Šä¼ å®Œæˆ
  switch (currentUploadType.value) {
    case 'before':
      beforeModelValue.value.push(fileData);
      break;
    case 'after':
      afterModelValue.value.push(fileData);
      break;
    case 'issue':
      issueModelValue.value.push(fileData);
      break;
  }
  // é‡ç½®ä¸Šä¼ åˆ—表(因为已经添加到对应分类了)
  uploadList.value = [];
  number.value = 0;
}
  // è¡¨æ ¼æ•°æ®
  const taskTableData = ref([]); // ç”Ÿäº§å·¡æ£€æ•°æ®
// ä¸Šä¼ ç»“束处理(已废弃,现在在handleUploadSuccess中直接处理)
const uploadedSuccessfully = () => {
  // æ­¤å‡½æ•°å·²ä¸å†ä½¿ç”¨ï¼Œæ–‡ä»¶ä¸Šä¼ æˆåŠŸåŽç«‹å³æ·»åŠ åˆ°å¯¹åº”åˆ†ç±»
}
  // å½“前扫描的任务
  const currentScanningTask = ref(null);
  const infoData = ref(null);
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size) => {
  if (!size) return '';
  if (size < 1024) return size + 'B';
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB';
  return (size / (1024 * 1024)).toFixed(1) + 'MB';
}
  // ä¸Šä¼ ç›¸å…³çŠ¶æ€
  const showUploadDialog = ref(false);
  const uploadFiles = ref([]); // ä¿ç•™ç”¨äºŽå…¼å®¹æ€§
  const uploadStatusType = ref(0);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const number = ref(0);
  const uploadList = ref([]);
  // ä¸‰ä¸ªåˆ†ç±»çš„上传状态
  const beforeModelValue = ref([]); // ç”Ÿäº§å‰
  const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
  const issueModelValue = ref([]); // ç”Ÿäº§åŽ
  // å½“前激活的上传类型
  const currentUploadType = ref("before"); // 'before', 'after', 'issue'
  // æŸ¥çœ‹é™„件相关状态
  const showAttachmentDialog = ref(false);
  const currentViewTask = ref(null);
  const currentViewType = ref("before"); // 'before', 'after', 'issue'
  const attachmentList = ref([]); // å½“前查看任务的附件列表
  // è§†é¢‘预览相关状态
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
  // å¼‚常状态
  const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
  // ä¸Šä¼ é…ç½®
  const uploadConfig = {
    action: "/file/upload",
    limit: 10,
    fileSize: 50, // MB
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
    maxVideoDuration: 60, // ç§’
  };
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    const baseUrl = "http://114.132.189.42:9030";
    return baseUrl + uploadConfig.action;
  });
  // è®¡ç®—请求头
  const headers = computed(() => {
    const token = getToken();
    return token ? { Authorization: "Bearer " + token } : {};
  });
  // è¯·æ±‚取消标志,用于取消正在进行的请求
  let isRequestCancelled = false;
  const pagesPames = reactive({
    size: 10,
    current: 1,
  });
  const loadMoreStatus = computed(() => {
    if (loading.value) {
      return "loading";
    }
    if (noMore.value) {
      return "noMore";
    }
    return "more";
  });
  const totalSize = ref(0);
  const noMore = computed(() => {
    return taskTableData.value.length >= totalSize.value;
  });
  const loading = ref(false);
  const reloadPage = () => {
    pagesPames.current = 1;
    taskTableData.value = [];
    getList();
  };
  const loadPage = () => {
    if (noMore.value || loading.value) return;
    pagesPames.current += 1;
    getList();
  };
  // ç”Ÿå‘½å‘¨æœŸ
  onMounted(() => {
    // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM已渲染
    // nextTick(() => {
    //   getList()
    // })
  });
  onReachBottom(() => {
    loadPage();
  });
  onPullDownRefresh(() => {
    reloadPage();
    uni.stopPullDownRefresh();
  });
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°æ•°æ®
    reloadPage();
  });
  // ç»„件销毁时的清理
  onUnmounted(() => {
    // è®¾ç½®å–消标志,阻止后续的异步操作
    isRequestCancelled = true;
    // å…³é—­ä¸Šä¼ å¼¹çª—
    if (showUploadDialog.value) {
      showUploadDialog.value = false;
    }
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    // æ˜¾ç¤ºåŠ è½½æç¤º
    // showLoadingToast('加载中...')
    // è®¾ç½®å–消标志
    isRequestCancelled = false;
    loading.value = true;
    inspectionTaskList({ ...pagesPames })
      .then(res => {
        // æ£€æŸ¥ç»„件是否还存在且请求未被取消
        if (!isRequestCancelled) {
          // å¤„理不同的数据结构
          let records = [];
          if (res && res.data) {
            // å°è¯•多种可能的数据结构
            totalSize.value = res.data.total;
            if (Array.isArray(res.data.records)) {
              records = res.data.records;
            } else if (Array.isArray(res.data.rows)) {
              records = res.data.rows;
            } else if (Array.isArray(res.data)) {
              records = res.data;
            } else if (Array.isArray(res.data.list)) {
              records = res.data.list;
            }
          }
          if (records.length > 0) {
            taskTableData.value = [
              ...taskTableData.value,
              ...records.map(record => {
                record.fileStatus = getFileStatus(record);
                return record;
              }),
            ];
          } else {
            taskTableData.value = [];
            uni.showToast({
              title: "暂无巡检任务数据",
              icon: "none",
            });
          }
        }
        loading.value = false;
        // å…³é—­åŠ è½½æç¤º
        // closeToast()
      })
      .catch(err => {
        // æ£€æŸ¥ç»„件是否还存在且请求未被取消
        if (!isRequestCancelled) {
          taskTableData.value = [];
          // æ·»åŠ é”™è¯¯æç¤º
          uni.showToast({
            title: "获取数据失败",
            icon: "error",
          });
        }
        loading.value = false;
        // å…³é—­åŠ è½½æç¤º
        // closeToast()
      });
  };
  const getFileStatus = record => {
    let _beforeProduction =
      record.beforeProduction && record.beforeProduction.length;
    let _afterProduction =
      record.afterProduction && record.afterProduction.length;
    let _productionIssues =
      record.productionIssues && record.productionIssues.length;
    if (_beforeProduction && _afterProduction && _productionIssues) {
      return 2;
    } else if (_beforeProduction || _afterProduction || _productionIssues) {
      return 1;
    } else {
      return 0;
    }
  };
  // ä¸ºæŒ‡å®šä»»åŠ¡å¼€å§‹æ‰«ç ï¼ˆçœŸæœºï¼‰
  const startScanForTask = async task => {
    try {
      currentScanningTask.value = task;
      uni.scanCode({
        success: res => {
          handleScanSuccess(res);
        },
        fail: err => {
          console.error("扫码失败:", err);
          uni.showToast({
            title: "扫码失败",
            icon: "error",
          });
        },
      });
    } catch (e) {
      console.error("启动扫码失败:", e);
      uni.showToast({
        title: "启动扫码失败",
        icon: "error",
      });
    }
  };
  // æ‰«ç æˆåŠŸå¤„ç†ï¼šæ ¡éªŒåŽæ‰“å¼€ä¸Šä¼ å¼¹çª—
  const handleScanSuccess = result => {
    try {
      // è§£æžäºŒç»´ç æ•°æ®ï¼Œæå–deviceId
      let deviceId = "";
      if (result?.result && typeof result.result === "string") {
        if (result.result.includes("deviceId=")) {
          const match = result.result.match(/deviceId=(\d+)/);
          if (match && match[1]) deviceId = match[1];
        } else {
          try {
            const qrData = JSON.parse(result.result);
            deviceId = qrData.deviceId || qrData.qrCodeId || "";
          } catch (e) {
            deviceId = result.result;
          }
        }
      }
      if (!deviceId) {
        uni.showToast({ title: "未识别到设备ID", icon: "error" });
        return;
      }
      const currentTaskId =
        currentScanningTask.value?.taskId || currentScanningTask.value?.id;
      if (!currentTaskId) {
        uni.showToast({ title: "任务信息缺失", icon: "error" });
        return;
      }
      if (deviceId === currentTaskId.toString()) {
        uni.showToast({ title: "识别成功", icon: "success" });
        openUploadDialog(currentScanningTask.value);
      } else {
        uni.showToast({ title: "请扫描正确的设备", icon: "error" });
      }
    } catch (error) {
      console.error("扫码结果处理失败:", error);
      uni.showToast({
        title: error?.message || "数据解析失败",
        icon: "error",
      });
    }
  };
  // æ‰“开上传弹窗
  const openUploadDialog = task => {
    // è®¾ç½®ä»»åŠ¡ä¿¡æ¯åˆ°infoData
    if (task) {
      infoData.value = {
        ...task,
        taskId: task.taskId || task.id,
        storageBlobDTO: [], // åˆå§‹åŒ–文件列表
      };
    }
    // è®¾ç½®ä¸Šä¼ çŠ¶æ€ç±»åž‹ï¼ˆå¯ä»¥æ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ä¸åŒçš„çŠ¶æ€ï¼‰
    uploadStatusType.value = 0; // é»˜è®¤çŠ¶æ€
    // æ¸…空之前的文件
    uploadFiles.value = [];
    // æ˜¾ç¤ºä¸Šä¼ å¼¹çª—
    showUploadDialog.value = true;
  };
  // å…³é—­ä¸Šä¼ å¼¹çª—
  const closeUploadDialog = () => {
    showUploadDialog.value = false;
    uploadFiles.value = [];
    // æ¸…理三个分类的数据
    beforeModelValue.value = [];
    afterModelValue.value = [];
    issueModelValue.value = [];
    currentUploadType.value = "before";
    hasException.value = null; // é‡ç½®å¼‚常状态
    infoData.value = null; // æ¸…理任务数据
  };
  // åˆ‡æ¢ä¸Šä¼ ç±»åž‹
  const switchUploadType = type => {
    currentUploadType.value = type;
  };
  // èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
  const getCurrentFiles = () => {
    switch (currentUploadType.value) {
      case "before":
        return beforeModelValue.value || [];
      case "after":
        return afterModelValue.value || [];
      case "issue":
        return issueModelValue.value || [];
      default:
        return [];
    }
  };
  // èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
  const getUploadTypeText = () => {
    switch (currentUploadType.value) {
      case "before":
        return "生产前";
      case "after":
        return "生产中";
      case "issue":
        return "生产后";
      default:
        return "";
    }
  };
  // å¤„理上传文件更新
  const handleUploadUpdate = files => {
    uploadFiles.value = files;
  };
  // è®¾ç½®å¼‚常状态
  const setExceptionStatus = status => {
    hasException.value = status;
  };
  // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
  const goToRepair = () => {
    try {
      // å­˜å‚¨å½“前任务信息到本地存储,供报修页面使用
      const taskInfo = {
        taskId: infoData.value?.taskId || infoData.value?.id,
        taskName: infoData.value?.taskName,
        inspectionLocation: infoData.value?.inspectionLocation,
        inspector: infoData.value?.inspector,
        // ä¼ é€’当前上传的文件信息
        uploadedFiles: {
          before: beforeModelValue.value,
          after: afterModelValue.value,
          issue: issueModelValue.value,
        },
      };
      uni.setStorageSync("repairTaskInfo", JSON.stringify(taskInfo));
      // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
      uni.navigateTo({
        url: "/pages/equipmentManagement/repair/add",
      });
      // å…³é—­ä¸Šä¼ å¼¹çª—
      closeUploadDialog();
    } catch (error) {
      console.error("跳转报修页面失败:", error);
      uni.showToast({
        title: "跳转失败,请重试",
        icon: "error",
      });
    }
  };
  // æäº¤ä¸Šä¼ 
  const submitUpload = async () => {
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
      if (hasException.value === null) {
        uni.showToast({
          title: "请选择是否存在异常",
          icon: "none",
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•文件上传
      const totalFiles =
        beforeModelValue.value.length +
        afterModelValue.value.length +
        issueModelValue.value.length;
      if (totalFiles === 0) {
        uni.showToast({
          title: "请先上传文件",
          icon: "none",
        });
        return;
      }
      // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
      showLoadingToast("提交中...");
      // æŒ‰ç…§æ‚¨çš„逻辑合并所有分类的文件
      let arr = [];
      if (beforeModelValue.value.length > 0) {
        arr.push(...beforeModelValue.value);
      }
      if (afterModelValue.value.length > 0) {
        arr.push(...afterModelValue.value);
      }
      if (issueModelValue.value.length > 0) {
        arr.push(...issueModelValue.value);
      }
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表(tempFileIds)
      // å…¼å®¹ï¼šæœ‰äº›æŽ¥å£å¯èƒ½è¿”回 tempId / tempFileId / id
      let tempFileIds = [];
      if (arr !== null && arr.length > 0) {
        tempFileIds = arr
          .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
          .filter(v => v !== undefined && v !== null && v !== "");
      }
      // æäº¤æ•°æ®
      infoData.value.storageBlobDTO = arr;
      // æ·»åŠ å¼‚å¸¸çŠ¶æ€ä¿¡æ¯
      infoData.value.hasException = hasException.value;
      infoData.value.tempFileIds = tempFileIds;
      const result = await uploadInspectionTask({ ...infoData.value });
      // æ£€æŸ¥æäº¤ç»“æžœ
      if (result && (result.code === 200 || result.success)) {
        // æäº¤æˆåŠŸ
        closeToast(); // å…³é—­åŠ è½½æç¤º
        uni.showToast({
          title: "提交成功",
          icon: "success",
        });
        // å…³é—­å¼¹çª—
        closeUploadDialog();
        // åˆ·æ–°åˆ—表
        setTimeout(() => {
          reloadPage();
        }, 500);
      } else {
        // æäº¤å¤±è´¥
        closeToast();
        uni.showToast({
          title: result?.msg || result?.message || "提交失败",
          icon: "error",
        });
      }
    } catch (error) {
      console.error("提交上传失败:", error);
      closeToast(); // å…³é—­åŠ è½½æç¤º
      let errorMessage = "提交失败";
      if (error.message) {
        errorMessage = error.message;
      } else if (error.msg) {
        errorMessage = error.msg;
      } else if (typeof error === "string") {
        errorMessage = error;
      }
      uni.showToast({
        title: errorMessage,
        icon: "error",
      });
    }
  };
  // æŸ¥çœ‹é™„ä»¶
  const viewAttachments = async task => {
    try {
      currentViewTask.value = task;
      currentViewType.value = "before";
      // è§£æžæ–°çš„æ•°æ®ç»“æž„
      attachmentList.value = [];
      // åŽç«¯åæ˜¾å­—段(你提供的数据结构):
      // - commonFileListBefore:生产前(通常 type=10)
      // - commonFileListAfter:生产中(通常 type=11)
      // - commonFileList:可能是全部/兜底(若包含生产后,一般 type=12)
      const allList = Array.isArray(task?.commonFileList)
        ? task.commonFileList
        : [];
      const beforeList = Array.isArray(task?.commonFileListBefore)
        ? task.commonFileListBefore
        : allList.filter(f => f?.type === 10);
      const afterList = Array.isArray(task?.commonFileListAfter)
        ? task.commonFileListAfter
        : allList.filter(f => f?.type === 11);
      // å¦‚果后端后续补了 commonFileListIssue,则优先用;否则从 commonFileList é‡ŒæŒ‰ type=12 å…œåº•
      const issueList = Array.isArray(task?.commonFileListIssue)
        ? task.commonFileListIssue
        : allList.filter(f => f?.type === 12);
      const mapToViewFile = (file, viewType) => {
        const u = normalizeFileUrl(file?.url || file?.downloadUrl || "");
        return {
          ...file,
          // ç”¨äºŽä¸‰æ ‡ç­¾é¡µåˆ†ç»„:0=生产前 1=生产中 2=生产后
          type: viewType,
          name: file?.name || file?.originalFilename || file?.bucketFilename,
          bucketFilename: file?.bucketFilename || file?.name,
          originalFilename: file?.originalFilename || file?.name,
          url: u,
          downloadUrl: u,
          size: file?.size || file?.byteSize,
        };
      };
      attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0)));
      attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1)));
      attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2)));
      showAttachmentDialog.value = true;
    } catch (error) {
      uni.showToast({
        title: "获取附件失败",
        icon: "error",
      });
    }
  };
  // å…³é—­é™„件查看弹窗
  const closeAttachmentDialog = () => {
    showAttachmentDialog.value = false;
    currentViewTask.value = null;
    attachmentList.value = [];
    currentViewType.value = "before";
  };
  // åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
  const switchViewType = type => {
    currentViewType.value = type;
  };
  // æ ¹æ®type获取对应分类的附件
  const getAttachmentsByType = typeValue => {
    return attachmentList.value.filter(file => file.type === typeValue) || [];
  };
  // èŽ·å–type值
  const getTabType = () => {
    switch (currentUploadType.value) {
      case "before":
        return 10;
      case "after":
        return 11;
      case "issue":
        return 12;
      default:
        return 10;
    }
  };
  // èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
  const getCurrentViewAttachments = () => {
    switch (currentViewType.value) {
      case "before":
        return getAttachmentsByType(0);
      case "after":
        return getAttachmentsByType(1);
      case "issue":
        return getAttachmentsByType(2);
      default:
        return [];
    }
  };
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
  const isImageFile = file => {
    // æ£€æŸ¥contentType字段
    if (file.contentType && file.contentType.startsWith("image/")) {
      return true;
    }
    // æ£€æŸ¥åŽŸæœ‰çš„type字段
    if (file.type === "image") return true;
    // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名
    const name = file.bucketFilename || file.originalFilename || file.name || "";
    const ext = name.split(".").pop()?.toLowerCase();
    return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
  };
  // æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸï¼ˆåŽç«¯è¦æ±‚前缀)
  const filePreviewBase = "http://114.132.189.42:9098";
  // å°†åŽç«¯è¿”回的文件地址规范成可访问URL
  // å…¼å®¹åœºæ™¯ï¼š
  // - å·²ç»æ˜¯ http/https:直接返回
  // - ä»¥ / å¼€å¤´ï¼šæ‹¼æŽ¥ filePreviewBase
  // - Windows æœ¬åœ°è·¯å¾„(如 D:\ruoyi\prod\uploads...\xx.jpg):尝试截取 prod ä¹‹åŽçš„相对路径并拼接 filePreviewBase
  const normalizeFileUrl = rawUrl => {
    try {
      if (!rawUrl || typeof rawUrl !== "string") return "";
      const url = rawUrl.trim();
      if (!url) return "";
      if (/^https?:\/\//i.test(url)) return url;
      if (url.startsWith("/")) return `${filePreviewBase}${url}`;
      // Windows path -> web path
      if (/^[a-zA-Z]:\\/.test(url)) {
        const normalized = url.replace(/\\/g, "/");
        const idx = normalized.indexOf("/prod/");
        if (idx >= 0) {
          const relative = normalized.slice(idx + "/prod/".length);
          return `${filePreviewBase}/${relative}`;
        }
        // å…œåº•:无法推断映射规则时,至少把反斜杠变成正斜杠
        return normalized;
      }
      // å…¶ä»–相对路径:直接用 baseUrl æ‹¼ä¸€ä¸‹
      return `${filePreviewBase}/${url.replace(/^\//, "")}`;
    } catch (e) {
      return rawUrl || "";
    }
  };
  // é¢„览附件
  const previewAttachment = file => {
    if (isImageFile(file)) {
      // é¢„览图片
      const imageUrls = getCurrentViewAttachments()
        .filter(f => isImageFile(f))
        .map(f => f.url || f.downloadUrl);
      uni.previewImage({
        urls: imageUrls,
        current: file.url || file.downloadUrl,
      });
    } else {
      // é¢„览视频 - æ˜¾ç¤ºè§†é¢‘播放弹窗
      showVideoPreview(file);
    }
  };
  // æ˜¾ç¤ºè§†é¢‘预览
  const showVideoPreview = file => {
    currentVideoFile.value = file;
    showVideoDialog.value = true;
  };
  // å…³é—­è§†é¢‘预览
  const closeVideoPreview = () => {
    showVideoDialog.value = false;
    currentVideoFile.value = null;
  };
  // è§†é¢‘播放错误处理
  const handleVideoError = error => {
    uni.showToast({
      title: "视频播放失败",
      icon: "error",
    });
  };
  // æ‹ç…§/拍视频(真机优先用 chooseMedia;不支持则降级)
  const chooseMedia = type => {
    if (getCurrentFiles().length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    const remaining = uploadConfig.limit - getCurrentFiles().length;
    // ä¼˜å…ˆï¼šchooseMedia(支持 image/video)
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
                uid: Date.now() + Math.random() + idx,
              };
              handleBeforeUpload(file);
            });
          } catch (e) {
            console.error("处理拍摄结果失败:", e);
            uni.showToast({ title: "处理文件失败", icon: "error" });
          }
        },
        fail: err => {
          console.error("拍摄失败:", err);
          uni.showToast({ title: "拍摄失败", icon: "error" });
        },
      });
      return;
    }
    // é™çº§ï¼šchooseImage / chooseVideo
    if (type === "video") {
      chooseVideo();
    } else {
      uni.chooseImage({
        count: 1,
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          const tempFilePath = res?.tempFilePaths?.[0];
          const tempFile = res?.tempFiles?.[0] || {};
          if (!tempFilePath) return;
          handleBeforeUpload({
            tempFilePath,
            path: tempFilePath,
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: Date.now(),
            uid: Date.now() + Math.random(),
          });
        },
      });
    }
  };
  // æ‹ç…§
  const chooseImage = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseMedia({
      count: 1,
      mediaType: ["image", "video"],
      sizeType: ["compressed", "original"],
      sourceType: ["camera"],
      success: res => {
        try {
          if (!res.tempFiles || res.tempFiles.length === 0) {
            throw new Error("未获取到图片文件");
          }
          const tempFilePath = res.tempFiles[0];
          const tempFile =
            res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
          const file = {
            tempFilePath: tempFilePath,
            path: tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍照结果失败:", error);
          uni.showToast({
            title: "处理图片失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍照失败:", err);
        uni.showToast({
          title: "拍照失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // æ‹è§†é¢‘
  const chooseVideo = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseVideo({
      sourceType: ["camera"],
      maxDuration: uploadConfig.maxVideoDuration,
      camera: "back",
      success: res => {
        try {
          if (!res.tempFilePath) {
            throw new Error("未获取到视频文件");
          }
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "video",
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍视频结果失败:", error);
          uni.showToast({
            title: "处理视频失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍视频失败:", err);
        uni.showToast({
          title: "拍视频失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    uni.showModal({
      title: "确认删除",
      content: "确定要删除这个文件吗?",
      success: res => {
        if (res.confirm) {
          // æ ¹æ®å½“前上传类型删除对应分类的文件
          switch (currentUploadType.value) {
            case "before":
              beforeModelValue.value.splice(index, 1);
              break;
            case "after":
              afterModelValue.value.splice(index, 1);
              break;
            case "issue":
              issueModelValue.value.splice(index, 1);
              break;
          }
          uni.showToast({
            title: "删除成功",
            icon: "success",
          });
        }
      },
    });
  };
  // æ£€æŸ¥ç½‘络连接
  const checkNetworkConnection = () => {
    return new Promise(resolve => {
      uni.getNetworkType({
        success: res => {
          if (res.networkType === "none") {
            resolve(false);
          } else {
            resolve(true);
          }
        },
        fail: () => {
          resolve(false);
        },
      });
    });
  };
  // ä¸Šä¼ å‰æ ¡éªŒ
  const handleBeforeUpload = async file => {
    // æ ¡éªŒæ–‡ä»¶ç±»åž‹
    if (
      uploadConfig.fileType &&
      Array.isArray(uploadConfig.fileType) &&
      uploadConfig.fileType.length > 0
    ) {
      const fileName = file.name || "";
      const fileExtension = fileName
        ? fileName.split(".").pop().toLowerCase()
        : "";
      // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
      let expectedTypes = [];
      if (file.type === "image") {
        expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
      } else if (file.type === "video") {
        expectedTypes = ["mp4", "mov", "avi", "wmv"];
      }
      // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
      if (fileExtension && expectedTypes.length > 0) {
        const isAllowed = expectedTypes.some(
          type => uploadConfig.fileType.includes(type) && type === fileExtension
        );
        if (!isAllowed) {
          uni.showToast({
            title: `文件格式不支持,请拍摄 ${expectedTypes.join("/")} æ ¼å¼çš„æ–‡ä»¶`,
            icon: "none",
          });
          return false;
        }
      }
    }
    // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
    uploadFile(file);
    return true;
  };
  // æ–‡ä»¶ä¸Šä¼ å¤„理(真机走 uni.uploadFile)
  const uploadFile = async file => {
    uploading.value = true;
    uploadProgress.value = 0;
    number.value++; // å¢žåŠ ä¸Šä¼ è®¡æ•°
    // ç¡®ä¿token存在
    const token = getToken();
    if (!token) {
      handleUploadError("用户未登录");
      return;
    }
    const typeValue = getTabType(); // ç”Ÿäº§å‰:10, ç”Ÿäº§ä¸­:11, ç”Ÿäº§åŽ:12
    uploadWithUniUploadFile(
      file,
      file.tempFilePath || file.path || "",
      typeValue,
      token
    );
  };
  // ä½¿ç”¨uni.uploadFile上传(非H5环境或H5回退方案)
  const uploadWithUniUploadFile = (file, filePath, typeValue, token) => {
    if (!filePath) {
      handleUploadError("文件路径不存在");
      return;
    }
    const uploadTask = uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: filePath,
      name: "file",
      formData: {
        type: typeValue,
      },
      header: {
        Authorization: `Bearer ${token}`,
      },
      success: res => {
        try {
          if (res.statusCode === 200) {
            const response = JSON.parse(res.data);
            if (response.code === 200) {
              handleUploadSuccess(response, file);
              uni.showToast({
                title: "上传成功",
                icon: "success",
              });
            } else {
              handleUploadError(response.msg || "服务器返回错误");
            }
          } else {
            handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
          }
        } catch (e) {
          console.error("解析响应失败:", e);
          console.error("原始响应数据:", res.data);
          handleUploadError("响应数据解析失败: " + e.message);
        }
      },
      fail: err => {
        console.error("上传失败:", err.errMsg || err);
        number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
        let errorMessage = "上传失败";
        if (err.errMsg) {
          if (err.errMsg.includes("statusCode: null")) {
            errorMessage = "网络连接失败,请检查网络设置";
          } else if (err.errMsg.includes("timeout")) {
            errorMessage = "上传超时,请重试";
          } else if (err.errMsg.includes("fail")) {
            errorMessage = "上传失败,请检查网络连接";
          } else {
            errorMessage = err.errMsg;
          }
        }
        handleUploadError(errorMessage);
      },
      complete: () => {
        uploading.value = false;
        uploadProgress.value = 0;
      },
    });
    // ç›‘听上传进度
    if (uploadTask && uploadTask.onProgressUpdate) {
      uploadTask.onProgressUpdate(res => {
        uploadProgress.value = res.progress;
      });
    }
  };
  // ä¸Šä¼ å¤±è´¥å¤„理
  const handleUploadError = (message = "上传文件失败", showRetry = false) => {
    uploading.value = false;
    uploadProgress.value = 0;
    if (showRetry) {
      uni.showModal({
        title: "上传失败",
        content: message + ",是否重试?",
        success: res => {
          if (res.confirm) {
            // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
          }
        },
      });
    } else {
      uni.showToast({
        title: message,
        icon: "error",
      });
    }
  };
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  const handleUploadSuccess = (res, file) => {
    console.log("上传成功响应:", res);
    // å¤„理不同的数据结构:可能是数组,也可能是单个对象
    let uploadedFile = null;
    uploadedFile = res.data;
    if (!uploadedFile) {
      console.error("无法解析上传响应数据:", res);
      number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
      handleUploadError("上传响应数据格式错误", false);
      return;
    }
    // æ ¹æ®å½“前上传类型设置type字段
    let typeValue = 0; // é»˜è®¤ä¸ºç”Ÿäº§å‰
    switch (currentUploadType.value) {
      case "before":
        typeValue = 0;
        break;
      case "after":
        typeValue = 1;
        break;
      case "issue":
        typeValue = 2;
        break;
    }
    // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id和type
    const fileData = {
      ...file,
      id: uploadedFile.id, // æ·»åŠ æœåŠ¡å™¨è¿”å›žçš„id
      tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
      url:
        uploadedFile.url ||
        uploadedFile.downloadUrl ||
        file.tempFilePath ||
        file.path,
      bucketFilename:
        uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name,
      downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
      size: uploadedFile.size || uploadedFile.byteSize || file.size,
      createTime: uploadedFile.createTime || new Date().getTime(),
      type: typeValue, // æ·»åŠ ç±»åž‹å­—æ®µï¼š0=生产前, 1=生产中, 2=生产后
    };
    uploadList.value.push(fileData);
    // ç«‹å³æ·»åŠ åˆ°å¯¹åº”çš„åˆ†ç±»ï¼Œä¸ç­‰å¾…æ‰€æœ‰æ–‡ä»¶ä¸Šä¼ å®Œæˆ
    switch (currentUploadType.value) {
      case "before":
        beforeModelValue.value.push(fileData);
        break;
      case "after":
        afterModelValue.value.push(fileData);
        break;
      case "issue":
        issueModelValue.value.push(fileData);
        break;
    }
    // é‡ç½®ä¸Šä¼ åˆ—表(因为已经添加到对应分类了)
    uploadList.value = [];
    number.value = 0;
  };
  // ä¸Šä¼ ç»“束处理(已废弃,现在在handleUploadSuccess中直接处理)
  const uploadedSuccessfully = () => {
    // æ­¤å‡½æ•°å·²ä¸å†ä½¿ç”¨ï¼Œæ–‡ä»¶ä¸Šä¼ æˆåŠŸåŽç«‹å³æ·»åŠ åˆ°å¯¹åº”åˆ†ç±»
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (!size) return "";
    if (size < 1024) return size + "B";
    if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
    return (size / (1024 * 1024)).toFixed(1) + "MB";
  };
</script>
<style scoped>
.inspection-upload-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
  .inspection-upload-page {
    min-height: 100vh;
    background-color: #f5f5f5;
  }
.table-section {
  padding: 15px;
}
  .table-section {
    padding: 15px;
  }
.task-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
  .task-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
.task-item {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
}
  .task-item {
    background: #fff;
    border-radius: 12px;
    padding: 15px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    transition: all 0.3s ease;
  }
.task-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 12px;
}
  .task-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 12px;
  }
.task-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
  .task-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
.task-name {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .task-name {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.task-location {
  font-size: 14px;
  color: #666;
}
  .task-location {
    font-size: 14px;
    color: #666;
  }
.task-actions {
  display: flex;
  gap: 8px;
}
  .task-actions {
    display: flex;
    gap: 8px;
  }
.task-details {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
  .task-details {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
.detail-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
  .detail-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
.detail-label {
  font-size: 12px;
  color: #999;
}
  .detail-label {
    font-size: 12px;
    color: #999;
  }
.detail-value {
  font-size: 12px;
  color: #666;
  font-weight: 500;
}
  .detail-value {
    font-size: 12px;
    color: #666;
    font-weight: 500;
  }
.no-data {
  text-align: center;
  padding: 40px 20px;
  color: #999;
  font-size: 14px;
}
  .no-data {
    text-align: center;
    padding: 40px 20px;
    color: #999;
    font-size: 14px;
  }
/* æ‰«ç å¼¹çª—样式 */
.qr-scan-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
}
  /* æ‰«ç å¼¹çª—样式 */
  .qr-scan-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.8);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
  }
.qr-scan-container {
  width: 90vw;
  max-width: 400px;
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  position: relative;
}
  .qr-scan-container {
    width: 90vw;
    max-width: 400px;
    background: #fff;
    border-radius: 12px;
    padding: 20px;
    position: relative;
  }
.scan-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
  .scan-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
  }
.scan-title {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
  .scan-title {
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
.qr-camera {
  width: 100%;
  height: 300px;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 15px;
}
  .qr-camera {
    width: 100%;
    height: 300px;
    border-radius: 8px;
    overflow: hidden;
    margin-bottom: 15px;
  }
.scan-frame-wrapper {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
}
  .scan-frame-wrapper {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
.scan-frame {
  width: 200px;
  height: 200px;
  border: 2px solid #409eff;
  border-radius: 8px;
  position: relative;
}
  .scan-frame {
    width: 200px;
    height: 200px;
    border: 2px solid #409eff;
    border-radius: 8px;
    position: relative;
  }
.scan-frame::before,
.scan-frame::after {
  content: '';
  position: absolute;
  width: 20px;
  height: 20px;
  border: 2px solid #409eff;
}
  .scan-frame::before,
  .scan-frame::after {
    content: "";
    position: absolute;
    width: 20px;
    height: 20px;
    border: 2px solid #409eff;
  }
.scan-frame::before {
  top: -2px;
  left: -2px;
  border-right: none;
  border-bottom: none;
}
  .scan-frame::before {
    top: -2px;
    left: -2px;
    border-right: none;
    border-bottom: none;
  }
.scan-frame::after {
  bottom: -2px;
  right: -2px;
  border-left: none;
  border-top: none;
}
  .scan-frame::after {
    bottom: -2px;
    right: -2px;
    border-left: none;
    border-top: none;
  }
.scan-tip {
  margin-top: 10px;
  font-size: 14px;
  color: #666;
  text-align: center;
}
  .scan-tip {
    margin-top: 10px;
    font-size: 14px;
    color: #666;
    text-align: center;
  }
/* è‡ªå®šä¹‰æ¨¡æ€æ¡†æ ·å¼ */
.custom-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
  /* è‡ªå®šä¹‰æ¨¡æ€æ¡†æ ·å¼ */
  .custom-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
  }
.custom-modal-container {
  width: 100%;
  max-width: 500px;
  max-height: 80vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
  .custom-modal-container {
    width: 100%;
    max-width: 500px;
    max-height: 80vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }
/* ä¸Šä¼ å¼¹çª—样式 */
.upload-popup-content {
  background: #fff;
  border-radius: 12px;
  width: 100%;
  min-height: 300px;
  max-height: 70vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
  /* ä¸Šä¼ å¼¹çª—样式 */
  .upload-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 300px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
.upload-popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  border-bottom: 1px solid #eee;
}
  .upload-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
  }
.upload-popup-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .upload-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.upload-popup-body {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}
  .upload-popup-body {
    flex: 1;
    padding: 20px;
    overflow-y: auto;
  }
.upload-popup-footer {
  display: flex;
  justify-content: flex-end;
  padding: 15px 20px;
  border-top: 1px solid #eee;
  gap: 10px;
}
  .upload-popup-footer {
    display: flex;
    justify-content: flex-end;
    padding: 15px 20px;
    border-top: 1px solid #eee;
    gap: 10px;
  }
/* ç®€åŒ–上传组件样式 */
.simple-upload-area {
  padding: 15px;
}
  /* ç®€åŒ–上传组件样式 */
  .simple-upload-area {
    padding: 15px;
  }
.upload-buttons {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}
  .upload-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
  }
.file-list {
  margin-top: 15px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
  .file-list {
    margin-top: 15px;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
.file-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: #fff;
  border-radius: 12px;
  padding: 8px;
  border: 1px solid #e9ecef;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  transition: all 0.3s ease;
  width: calc(50% - 6px);
  min-width: 120px;
}
  .file-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(50% - 6px);
    min-width: 120px;
  }
.file-item:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}
  .file-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
.file-preview-container {
  position: relative;
  margin-bottom: 8px;
}
  .file-preview-container {
    position: relative;
    margin-bottom: 8px;
  }
.file-preview {
  width: 80px;
  height: 80px;
  border-radius: 8px;
  object-fit: cover;
  border: 2px solid #f0f0f0;
}
  .file-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
.video-preview {
  width: 80px;
  height: 80px;
  background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: 2px solid #f0f0f0;
}
  .video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
.video-text {
  font-size: 12px;
  color: #666;
  margin-top: 4px;
}
  .video-text {
    font-size: 12px;
    color: #666;
    margin-top: 4px;
  }
.delete-btn {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  background: #ff4757;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
  transition: all 0.3s ease;
}
  .delete-btn {
    position: absolute;
    top: -6px;
    right: -6px;
    width: 20px;
    height: 20px;
    background: #ff4757;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
    transition: all 0.3s ease;
  }
.delete-btn:hover {
  background: #ff3742;
  transform: scale(1.1);
}
  .delete-btn:hover {
    background: #ff3742;
    transform: scale(1.1);
  }
.file-info {
  text-align: center;
  width: 100%;
}
  .file-info {
    text-align: center;
    width: 100%;
  }
.file-name {
  font-size: 12px;
  color: #333;
  font-weight: 500;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100px;
}
  .file-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100px;
  }
.file-size {
  font-size: 10px;
  color: #999;
  margin-top: 2px;
  display: block;
}
  .file-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #999;
  font-size: 14px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 2px dashed #ddd;
}
  .empty-state {
    text-align: center;
    padding: 40px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
.upload-progress {
  margin: 15px 0;
  padding: 0 10px;
}
  .upload-progress {
    margin: 15px 0;
    padding: 0 10px;
  }
/* ä¸Šä¼ æ ‡ç­¾é¡µæ ·å¼ */
.upload-tabs {
  display: flex;
  background: #f8f9fa;
  border-radius: 8px;
  margin-bottom: 15px;
  padding: 4px;
}
  /* ä¸Šä¼ æ ‡ç­¾é¡µæ ·å¼ */
  .upload-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
.tab-item {
  flex: 1;
  text-align: center;
  padding: 8px 12px;
  font-size: 14px;
  color: #666;
  border-radius: 6px;
  transition: all 0.3s ease;
  cursor: pointer;
}
  .tab-item {
    flex: 1;
    text-align: center;
    padding: 8px 12px;
    font-size: 14px;
    color: #666;
    border-radius: 6px;
    transition: all 0.3s ease;
    cursor: pointer;
  }
.tab-item.active {
  background: #409eff;
  color: #fff;
  font-weight: 500;
}
  .tab-item.active {
    background: #409eff;
    color: #fff;
    font-weight: 500;
  }
.tab-item:hover:not(.active) {
  background: #e9ecef;
  color: #333;
}
  .tab-item:hover:not(.active) {
    background: #e9ecef;
    color: #333;
  }
/* å¼‚常状态选择样式 */
.exception-section {
  margin-bottom: 20px;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e9ecef;
}
  /* å¼‚常状态选择样式 */
  .exception-section {
    margin-bottom: 20px;
    padding: 15px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #e9ecef;
  }
.section-title {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: #333;
  margin-bottom: 12px;
}
  .section-title {
    display: block;
    font-size: 14px;
    font-weight: 600;
    color: #333;
    margin-bottom: 12px;
  }
.exception-options {
  display: flex;
  gap: 12px;
}
  .exception-options {
    display: flex;
    gap: 12px;
  }
.exception-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 16px;
  background: #fff;
  border: 2px solid #e9ecef;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
  font-size: 14px;
  color: #666;
}
  .exception-option {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 16px;
    background: #fff;
    border: 2px solid #e9ecef;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-size: 14px;
    color: #666;
  }
.exception-option.active {
  border-color: #409eff;
  background: #f0f8ff;
  color: #409eff;
  font-weight: 500;
}
  .exception-option.active {
    border-color: #409eff;
    background: #f0f8ff;
    color: #409eff;
    font-weight: 500;
  }
.exception-option:hover:not(.active) {
  border-color: #d9d9d9;
  background: #fafafa;
}
  .exception-option:hover:not(.active) {
    border-color: #d9d9d9;
    background: #fafafa;
  }
/* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
.upload-summary {
  margin-top: 15px;
  padding: 10px;
  background: #f8f9fa;
  border-radius: 6px;
  border-left: 3px solid #409eff;
}
  /* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
  .upload-summary {
    margin-top: 15px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    border-left: 3px solid #409eff;
  }
.summary-text {
  font-size: 12px;
  color: #666;
  line-height: 1.4;
}
  .summary-text {
    font-size: 12px;
    color: #666;
    line-height: 1.4;
  }
/* æŸ¥çœ‹é™„件弹窗样式 */
.attachment-popup-content {
  background: #fff;
  border-radius: 12px;
  width: 100%;
  min-height: 400px;
  max-height: 70vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
  /* æŸ¥çœ‹é™„件弹窗样式 */
  .attachment-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 400px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
.attachment-popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  border-bottom: 1px solid #eee;
  background: #f8f9fa;
}
  .attachment-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
    background: #f8f9fa;
  }
.attachment-popup-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
  .attachment-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
.close-btn-attachment {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: #f5f5f5;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s ease;
}
  .close-btn-attachment {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #f5f5f5;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
  }
.close-btn-attachment:hover {
  background: #e9ecef;
  transform: scale(1.1);
}
  .close-btn-attachment:hover {
    background: #e9ecef;
    transform: scale(1.1);
  }
.attachment-popup-body {
  flex: 1;
  padding: 15px 20px;
  overflow-y: auto;
}
  .attachment-popup-body {
    flex: 1;
    padding: 15px 20px;
    overflow-y: auto;
  }
.attachment-tabs {
  display: flex;
  background: #f8f9fa;
  border-radius: 8px;
  margin-bottom: 15px;
  padding: 4px;
}
  .attachment-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
.attachment-content {
  min-height: 200px;
}
  .attachment-content {
    min-height: 200px;
  }
.attachment-list {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
  .attachment-list {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
.attachment-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: #fff;
  border-radius: 12px;
  padding: 8px;
  border: 1px solid #e9ecef;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  transition: all 0.3s ease;
  width: calc(33.33% - 8px);
  min-width: 100px;
  cursor: pointer;
}
  .attachment-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(33.33% - 8px);
    min-width: 100px;
    cursor: pointer;
  }
.attachment-item:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}
  .attachment-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
.attachment-preview-container {
  margin-bottom: 8px;
}
  .attachment-preview-container {
    margin-bottom: 8px;
  }
.attachment-preview {
  width: 80px;
  height: 80px;
  border-radius: 8px;
  object-fit: cover;
  border: 2px solid #f0f0f0;
}
  .attachment-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
.attachment-video-preview {
  width: 80px;
  height: 80px;
  background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: 2px solid #f0f0f0;
}
  .attachment-video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
.attachment-info {
  text-align: center;
  width: 100%;
}
  .attachment-info {
    text-align: center;
    width: 100%;
  }
.attachment-name {
  font-size: 12px;
  color: #333;
  font-weight: 500;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 80px;
}
  .attachment-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 80px;
  }
.attachment-size {
  font-size: 10px;
  color: #999;
  margin-top: 2px;
  display: block;
}
  .attachment-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
.attachment-empty {
  text-align: center;
  padding: 60px 20px;
  color: #999;
  font-size: 14px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 2px dashed #ddd;
}
  .attachment-empty {
    text-align: center;
    padding: 60px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
/* è§†é¢‘预览弹窗样式 */
.video-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  z-index: 10001;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
  /* è§†é¢‘预览弹窗样式 */
  .video-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.8);
    z-index: 10001;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
  }
.video-modal-container {
  width: 90%;
  max-width: 800px;
  max-height: 80vh;
  background: #000;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
  .video-modal-container {
    width: 90%;
    max-width: 800px;
    max-height: 80vh;
    background: #000;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  }
.video-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
}
  .video-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
  }
.video-modal-title {
  font-size: 16px;
  font-weight: 500;
  color: #fff;
}
  .video-modal-title {
    font-size: 16px;
    font-weight: 500;
    color: #fff;
  }
.close-btn-video {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s ease;
}
  .close-btn-video {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.2);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
  }
.close-btn-video:hover {
  background: rgba(255, 255, 255, 0.3);
  transform: scale(1.1);
}
  .close-btn-video:hover {
    background: rgba(255, 255, 255, 0.3);
    transform: scale(1.1);
  }
.video-modal-body {
  position: relative;
  background: #000;
}
  .video-modal-body {
    position: relative;
    background: #000;
  }
.video-player {
  width: 100%;
  height: auto;
  max-height: 60vh;
  display: block;
}
  .video-player {
    width: 100%;
    height: auto;
    max-height: 60vh;
    display: block;
  }
</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,526 @@
<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;
            } else {
              // æ¨¡æ‹Ÿæ£€éªŒé¡¹ç›®
              inspectionItems.value = [
                {
                  parameterItem: "厚度",
                  unit: "mm",
                  standardValue: "2.0 Â± 0.1",
                  controlValue: "2.0 Â± 0.05",
                  testValue: "2.05",
                },
                {
                  parameterItem: "硬度",
                  unit: "HB",
                  standardValue: "≥ 200",
                  controlValue: "≥ 210",
                  testValue: "220",
                },
                {
                  parameterItem: "表面质量",
                  unit: "",
                  standardValue: "无划痕、无锈蚀",
                  controlValue: "无划痕、无锈蚀",
                  testValue: "合格",
                },
              ];
            }
          })
          .catch(error => {
            console.error("获取检验项目失败:", error);
            // æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¸­çš„æ•°æ®æˆ–模拟数据
            if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          });
      } else {
        // æ¨¡æ‹Ÿæ•°æ®
        detailData.value = {
          id: id,
          checkTime: "2026-03-03",
          purchaseContractNo: "PO20260303001",
          supplier: "上海金属材料有限公司",
          checkName: "张三",
          productName: "不锈钢板材",
          model: "304",
          unit: "kg",
          quantity: 1000,
          checkCompany: "第三方检测机构",
          checkResult: "合格",
          inspectState: true,
        };
        // æ¨¡æ‹Ÿæ£€éªŒé¡¹ç›®
        inspectionItems.value = [
          {
            parameterItem: "厚度",
            unit: "mm",
            standardValue: "2.0 Â± 0.1",
            controlValue: "2.0 Â± 0.05",
            testValue: "2.05",
          },
          {
            parameterItem: "硬度",
            unit: "HB",
            standardValue: "≥ 200",
            controlValue: "≥ 210",
            testValue: "220",
          },
          {
            parameterItem: "表面质量",
            unit: "",
            standardValue: "无划痕、无锈蚀",
            controlValue: "无划痕、无锈蚀",
            testValue: "合格",
          },
        ];
      }
    } catch (error) {
      console.error("加载详情数据失败:", error);
      showToast("加载详情数据失败,请重试");
      // åŠ è½½å¤±è´¥æ—¶ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
      detailData.value = {
        id: id,
        checkTime: "2026-03-03",
        purchaseContractNo: "PO20260303001",
        supplier: "上海金属材料有限公司",
        checkName: "张三",
        productName: "不锈钢板材",
        model: "304",
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        inspectState: true,
      };
      inspectionItems.value = [
        {
          parameterItem: "厚度",
          unit: "mm",
          standardValue: "2.0 Â± 0.1",
          controlValue: "2.0 Â± 0.05",
          testValue: "2.05",
        },
        {
          parameterItem: "硬度",
          unit: "HB",
          standardValue: "≥ 200",
          controlValue: "≥ 210",
          testValue: "220",
        },
        {
          parameterItem: "表面质量",
          unit: "",
          standardValue: "无划痕、无锈蚀",
          controlValue: "无划痕、无锈蚀",
          testValue: "合格",
        },
      ];
    }
  };
  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,814 @@
<template>
  <view class="material-inspection-page">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="原材料检验"
                @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入供应商搜索"
                    v-model="searchForm.supplier"
                    @change="getList"
                    clearable />
        </view>
        <!-- <view class="filter-button"
              @click="showDatePicker">
          <up-icon name="calendar"
                   size="24"
                   color="#999"></up-icon>
        </view> -->
        <view class="filter-button"
              @click="getList">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
      <!-- æ—¥æœŸé€‰æ‹© -->
      <!-- <view class="date-range"
            v-if="searchForm.entryDate">
        <text class="date-text">{{ searchForm.entryDate[0] }} è‡³ {{ searchForm.entryDate[1] }}</text>
        <up-icon name="close"
                 size="16"
                 color="#999"
                 @click="clearDateRange"></up-icon>
      </view> -->
    </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.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: 10px 15px;
    background: #fff;
    border-bottom: 1px solid #f0f0f0;
  }
  .search-bar {
    display: flex;
    align-items: center;
    background: #f8f9fa;
    border-radius: 20px;
    padding: 0 15px;
    height: 40px;
  }
  .search-input {
    flex: 1;
  }
  .search-text {
    background: transparent;
    border: none;
  }
  .filter-button {
    margin-left: 10px;
    padding: 5px;
  }
  // ç»Ÿè®¡å¡ç‰‡
  .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: 12px;
  }
  .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>