zhangwencui
2026-03-04 e9ecd350234f21349eaea8901f64ff239f76c65c
出厂检验和过程检验模块开发
已添加8个文件
已修改5个文件
6055 ■■■■■ 文件已修改
src/api/qualityManagement/materialInspection.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/add.vue 1128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/detail.vue 422 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/fileList.vue 566 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 816 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/add.vue 1128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/detail.vue 423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/fileList.vue 566 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 815 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/materialInspection.js
@@ -154,4 +154,13 @@
        method: 'delete',
        data: query,
    })
}
// å·¥åºæŸ¥è¯¢
export function list() {
    return request({
        url: "/productProcess/list",
        method: "get",
    });
}
src/pages.json
@@ -921,6 +921,62 @@
      }
    },
    {
      "path": "pages/qualityManagement/processInspection/index",
      "style": {
        "navigationBarTitleText": "过程检验",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/processInspection/add",
      "style": {
        "navigationBarTitleText": "过程检验添加",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/processInspection/detail",
      "style": {
        "navigationBarTitleText": "过程检验详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/processInspection/fileList",
      "style": {
        "navigationBarTitleText": "过程检验附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/finalInspection/index",
      "style": {
        "navigationBarTitleText": "出厂检验",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/finalInspection/add",
      "style": {
        "navigationBarTitleText": "出厂检验添加",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/finalInspection/detail",
      "style": {
        "navigationBarTitleText": "出厂检验详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/qualityManagement/finalInspection/fileList",
      "style": {
        "navigationBarTitleText": "出厂检验附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/message",
      "style": {
        "navigationBarTitleText": "消息中心"
src/pages/index.vue
@@ -344,6 +344,14 @@
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "原材料检验",
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "过程检验",
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "出厂检验",
    },
  ]);
  const safetyItems = reactive([
    {
@@ -761,6 +769,16 @@
          url: "/pages/qualityManagement/materialInspection/index",
        });
        break;
      case "过程检验":
        uni.navigateTo({
          url: "/pages/qualityManagement/processInspection/index",
        });
        break;
      case "出厂检验":
        uni.navigateTo({
          url: "/pages/qualityManagement/finalInspection/index",
        });
        break;
      default:
        uni.showToast({
          title: `点击了${item.label}`,
@@ -1040,6 +1058,8 @@
    // è¿‡æ»¤è´¨é‡ç®¡ç†èœå•
    const originalQuality = [
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "原材料检验" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "过程检验" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "出厂检验" },
    ];
    const filteredQuality = originalQuality.filter(item => {
      return allowedMenuTitles.has(item.label);
src/pages/qualityManagement/finalInspection/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1128 @@
<template>
  <view class="material-inspection-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="isEdit ? '编辑出厂检验' : '新增出厂检验'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <up-form :model="form"
             ref="formRef"
             label-width="110"
             :rules="rules">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <!-- <up-form-item label="工序"
                    prop="process"
                    required
                    border-bottom>
        <up-input v-model="form.process"
                  placeholder="请选择工序"
                  readonly
                  :disabled="processQuantityDisabled" />
        <template #right>
          <up-icon @click="showprocessSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item> -->
      <up-form-item label="产品名称"
                    prop="productId"
                    required
                    border-bottom>
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showProductTree = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="规格型号"
                    prop="productModelId"
                    required
                    border-bottom>
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showModelSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit"
                    border-bottom>
        <up-input v-model="form.unit"
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="processQuantityDisabled" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
                    prop="checkResult"
                    required
                    border-bottom>
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检验员"
                    prop="checkName"
                    border-bottom>
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检测日期"
                    prop="checkTime"
                    required
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
        </template> -->
      </up-form-item>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="inspection-items-container">
        <view class="steps-header">
          <text class="steps-title">检验项目</text>
          <text class="steps-count">共 {{ tableData.length }} ä¸ªé¡¹ç›®</text>
        </view>
        <view class="steps-list">
          <view v-for="(item, index) in tableData"
                :key="index"
                class="exec-step-item">
            <view class="step-number">
              {{ index + 1 }}
            </view>
            <view class="step-content">
              <view class="step-row">
                <text class="step-label">指标:</text>
                <text class="step-value">{{ item.parameterItem }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">单位:</text>
                <text class="step-value">{{ item.unit }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">标准值:</text>
                <text class="step-value">{{ item.standardValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">内控值:</text>
                <text class="step-value">{{ item.controlValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          clearable
                          border-bottom
                          class="step-input" />
              </view>
            </view>
          </view>
          <view v-if="tableData.length === 0"
                class="empty-data">
            <text>请先选择指标</text>
          </view>
        </view>
      </view>
    </up-form>
    <!-- åº•部按钮 -->
    <view class="bottom-buttons">
      <up-button type="default"
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
      </up-button>
      <up-button type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <!-- å·¥åºé€‰æ‹© -->
    <up-action-sheet :show="showprocessSheet"
                     :actions="processOptions"
                     @select="selectprocess"
                     @close="showprocessSheet = false"
                     title="选择工序" />
    <!-- äº§å“é€‰æ‹© -->
    <up-action-sheet :show="showProductSheet"
                     :actions="productSheetOptions"
                     @select="selectProduct"
                     @close="showProductSheet = false"
                     title="选择产品" />
    <!-- è§„格型号选择 -->
    <up-action-sheet :show="showModelSheet"
                     :actions="modelSheetOptions"
                     @select="selectModel"
                     @close="showModelSheet = false"
                     title="选择规格型号" />
    <!-- æ£€æµ‹ç»“果选择 -->
    <up-action-sheet :show="showResultSheet"
                     :actions="resultSheetOptions"
                     @select="selectResult"
                     @close="showResultSheet = false"
                     title="选择检测结果" />
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     @close="showInspectorSheet = false"
                     title="选择检验员" />
    <!-- æŒ‡æ ‡é€‰æ‹© -->
    <up-action-sheet :show="showTestStandardSheet"
                     :actions="testStandardSheetOptions"
                     @select="selectTestStandard"
                     @close="showTestStandardSheet = false"
                     title="选择指标" />
    <!-- äº§å“æ ‘形选择器 -->
    <up-popup v-model:show="showProductTree"
              position="bottom"
              :round="true"
              :closeable="true"
              @close="showProductTree = false">
      <view class="tree-selector">
        <view class="tree-header">
          <text class="tree-title">选择产品</text>
        </view>
        <view class="tree-content">
          <view class="tree-node"
                v-for="(node, index) in productOptions"
                :key="index">
            <view v-if="node.children && node.children.length > 0"
                  class="tree-node-header"
                  @click="toggleNode(node)">
              <up-icon :name="node.expanded ? 'arrow-down' : 'arrow-right'"
                       class="tree-node-icon" />
              <text class="tree-node-label">{{ node.label }}</text>
            </view>
            <view v-else
                  class="tree-node-header"
                  @click="selectTreeNode(node)">
              <text class="tree-node-icon-placeholder"></text>
              <text class="tree-node-label">{{ node.label }}</text>
              <up-icon name="checkmark"
                       v-if="form.productId == node.value"
                       class="tree-node-check" />
            </view>
            <view v-if="node.children && node.children.length > 0 && node.expanded"
                  class="tree-node-children">
              <view class="tree-node"
                    v-for="(child, childIndex) in node.children"
                    :key="childIndex">
                <view v-if="child.children && child.children.length > 0"
                      class="tree-node-header"
                      @click="toggleNode(child)">
                  <up-icon :name="child.expanded ? 'arrow-down' : 'arrow-right'"
                           class="tree-node-icon" />
                  <text class="tree-node-label">{{ child.label }}</text>
                </view>
                <view v-else
                      class="tree-node-header"
                      @click="selectTreeNode(child)">
                  <text class="tree-node-icon-placeholder"></text>
                  <text class="tree-node-label">{{ child.label }}</text>
                  <up-icon name="checkmark"
                           v-if="form.productId == child.value"
                           class="tree-node-check" />
                </view>
                <view v-if="child.children && child.children.length > 0 && child.expanded"
                      class="tree-node-children">
                  <view class="tree-node"
                        v-for="(grandchild, grandchildIndex) in child.children"
                        :key="grandchildIndex">
                    <view class="tree-node-header"
                          @click="selectTreeNode(grandchild)">
                      <text class="tree-node-icon-placeholder"></text>
                      <text class="tree-node-label">{{ grandchild.label }}</text>
                      <up-icon name="checkmark"
                               v-if="form.productId == grandchild.value"
                               class="tree-node-check" />
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted, nextTick } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import {
    qualityInspectAdd,
    qualityInspectUpdate,
    qualityInspectParamInfo,
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
    list,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¡¨å•引用
  const formRef = ref(null);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // å·¥åºé€‰æ‹©
  const showprocessSheet = ref(false);
  // äº§å“é€‰æ‹©
  const showProductSheet = ref(false);
  // äº§å“æ ‘形选择器
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
  const showTestStandardSheet = ref(false);
  // è¡¨å•数据
  const form = ref({
    checkTime: dayjs().format("YYYY-MM-DD"),
    process: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
  // æ˜¾ç¤ºç”¨çš„变量
  const testStandardDisplay = ref("");
  // æ£€éªŒé¡¹ç›®
  const tableData = ref([]);
  const tableLoading = ref(false);
  // å·¥åºåˆ—表
  const processList = ref([]);
  // äº§å“é€‰é¡¹
  const productOptions = ref([]);
  // åž‹å·é€‰é¡¹
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
  const currentProductId = ref(0);
  // ActionSheet选项
  const processOptions = computed(() => {
    return processList.value.map(item => ({
      name: item.name,
      value: item.name,
    }));
  });
  const productSheetOptions = computed(() => {
    return productOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const modelSheetOptions = computed(() => {
    return modelOptions.value.map(item => ({
      name: item.model,
      value: item.id,
    }));
  });
  const resultSheetOptions = computed(() => {
    return resultOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  const testStandardSheetOptions = computed(() => {
    return testStandardOptions.value.map(item => ({
      name: item.standardName || item.standardNo,
      value: item.id,
    }));
  });
  // è¡¨å•验证规则
  const rules = {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    process: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
  const isEdit = computed(() => {
    const id = getPageId();
    return !!id;
  });
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
  // é€‰æ‹©å·¥åº
  const selectprocess = e => {
    form.value.process = e.value;
    showprocessSheet.value = false;
  };
  // é€‰æ‹©äº§å“
  const selectProduct = e => {
    form.value.productId = e.value;
    form.value.productName = e.name;
    showProductSheet.value = false;
    getModels(e.value);
  };
  // åˆ‡æ¢æ ‘形节点展开/折叠
  const toggleNode = node => {
    node.expanded = !node.expanded;
  };
  // é€‰æ‹©æ ‘形节点
  const selectTreeNode = node => {
    // ç¡®ä¿åªé€‰æ‹©æœ«ç«¯èŠ‚ç‚¹
    if (!node.children || node.children.length == 0) {
      form.value.productId = node.value;
      form.value.productName = node.label;
      showProductTree.value = false;
      getModels(node.value);
    }
  };
  // è½¬æ¢äº§å“æ ‘结构
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // æ ¹æ®ID查找节点
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  // é€‰æ‹©è§„格型号
  const selectModel = e => {
    form.value.productModelId = e.value;
    showModelSheet.value = false;
    handleChangeModel(e.value);
  };
  // å¤„理型号变化
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  // é€‰æ‹©æ£€æµ‹ç»“æžœ
  const selectResult = e => {
    form.value.checkResult = e.value;
    showResultSheet.value = false;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    form.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // é€‰æ‹©æŒ‡æ ‡
  const selectTestStandard = e => {
    form.value.testStandardId = e.value;
    testStandardDisplay.value = e.name;
    showTestStandardSheet.value = false;
    handleTestStandardChange(e.value);
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    showTestStandardSheet.value = true;
  };
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getprocessList = () => {
    list().then(res => {
      processList.value = res.data;
    });
  };
  // èŽ·å–äº§å“é€‰é¡¹
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // èŽ·å–åž‹å·åˆ—è¡¨
  const getModels = value => {
    form.value.productModelId = "";
    form.value.unit = "";
    modelOptions.value = [];
    currentProductId.value = value;
    form.value.productName = findNodeById(productOptions.value, value);
    modelList({ id: value }).then(res => {
      modelOptions.value = res;
    });
    if (currentProductId.value) {
      getList();
    }
  };
  // èŽ·å–æŒ‡æ ‡åˆ—è¡¨
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    let params = {
      productId: currentProductId.value,
      inspectType: 2,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
      testStandardDisplay.value = "";
    });
  };
  // èŽ·å–æ£€éªŒå‚æ•°åˆ—è¡¨ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
        showToast("请选择规格型号");
        return;
      }
      // if (!form.value.process) {
      //   showToast("请选择工序");
      //   return;
      // }
      if (!form.value.quantity) {
        showToast("请输入数量");
        return;
      }
      if (!form.value.productId) {
        showToast("请选择产品");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 2;
      if (isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
    } finally {
      loading.value = false;
    }
  };
  // åˆå§‹åŒ–表单
  const initForm = async () => {
    const id = getPageId();
    if (id) {
      // ç¼–辑模式,加载数据
      // å…ˆé‡ç½®è¡¨å•数据
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
      testStandardOptions.value = [];
      tableData.value = [];
      // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
      await getProductOptions();
      // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–ç¼–è¾‘æ•°æ®
      const row = uni.getStorageSync("finalInspectionEditData") || {
        id: id,
        checkTime: "2026-03-03",
        process: "上海金属材料有限公司",
        checkName: "张三",
        productName: "不锈钢板材",
        productId: 1,
        productModelId: 1,
        model: "304",
        testStandardId: "1",
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        productMainId: null,
        purchaseLedgerId: null,
      };
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      form.value = { ...row };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 2,
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“
          nextTick(() => {
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…
              const matchedOption = testStandardOptions.value.find(
                item =>
                  item.id == savedTestStandardId ||
                  String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id
                form.value.testStandardId = matchedOption.id;
                testStandardDisplay.value =
                  matchedOption.standardName || matchedOption.standardNo;
                // ç¼–辑保留原检验值,直接拉取原参数数据
                getQualityInspectParamList(row.id);
              } else {
                // å¦‚果找不到匹配项,尝试直接使用原值
                console.warn(
                  "未找到匹配的指标选项,testStandardId:",
                  savedTestStandardId
                );
                form.value.testStandardId = savedTestStandardId;
                getQualityInspectParamList(row.id);
              }
            } else {
              // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
              getQualityInspectParamList(row.id);
            }
          });
        });
      }
      // å±•开产品树到当前选中的节点
      expandProductTree(productOptions.value, row.productId);
    } else {
      // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–表单
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
    }
  };
  // å±•开产品树到指定节点
  const expandProductTree = (nodes, targetId) => {
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.value === targetId) {
        return true; // æ‰¾åˆ°ç›®æ ‡èŠ‚ç‚¹
      }
      if (node.children && node.children.length > 0) {
        const found = expandProductTree(node.children, targetId);
        if (found) {
          node.expanded = true; // å±•开父节点
          return true;
        }
      }
    }
    return false;
  };
  onMounted(() => {
    getprocessList();
    getProductOptions();
    getUserList();
    initForm();
  });
  onShow(() => {
    initForm();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .material-inspection-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  // æ£€éªŒé¡¹ç›®å®¹å™¨
  .inspection-items-container {
    padding: 20px;
    background-color: #fff;
  }
  .steps-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e4e7ed;
  }
  .steps-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .steps-count {
    font-size: 14px;
    color: #909399;
  }
  .steps-list {
    margin-bottom: 20px;
  }
  .exec-step-item {
    position: relative;
    display: flex;
    margin-bottom: 16px;
    padding: 16px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    transition: all 0.3s ease;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  }
  .exec-step-item:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-1px);
  }
  .delete-btn {
    position: absolute;
    top: -25rpx;
    right: -25rpx;
    width: 50rpx;
    height: 50rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 20px;
    border-radius: 50%;
    background-color: red;
    border: none;
    z-index: 10;
  }
  .delete-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(245, 108, 108, 0.4);
  }
  .step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-right: 16px;
    background-color: #ecf5ff;
    color: #409eff;
    font-size: 14px;
    font-weight: 600;
    border-radius: 50%;
    flex-shrink: 0;
  }
  .step-content {
    flex: 1;
    min-width: 0;
  }
  .step-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .step-row:last-child {
    margin-bottom: 0;
  }
  .step-label {
    display: inline-block;
    width: 80px;
    font-size: 14px;
    color: #606266;
    margin-right: 12px;
    flex-shrink: 0;
    line-height: 36px;
  }
  .step-input {
    flex: 1;
    min-width: 0;
  }
  .step-input input {
    font-size: 14px;
    color: #303133;
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 44px;
    line-height: 44px;
    font-size: 14px;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 8px;
  }
  .add-step-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  }
  .add-step-btn text {
    font-size: 14px;
  }
  // åº•部按钮
  .bottom-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 16px 20px;
    background: #ffffff;
    border-top: 1px solid #f0f0f0;
    gap: 16px;
  }
  .bottom-btn {
    flex: 1;
  }
  // æ ‘形选择器样式
  .tree-selector {
    width: 100%;
    max-height: 70vh;
    background: #ffffff;
    border-radius: 16px 16px 0 0;
  }
  .tree-header {
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    text-align: center;
  }
  .tree-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .tree-content {
    padding: 10px 0;
    max-height: calc(70vh - 60px);
    overflow-y: auto;
  }
  .tree-node {
    padding: 0 20px;
  }
  .tree-node-header {
    display: flex;
    align-items: center;
    padding: 12px 0;
    cursor: pointer;
  }
  .tree-node-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    color: #909399;
  }
  .tree-node-icon-placeholder {
    width: 20px;
    height: 20px;
    margin-right: 8px;
  }
  .tree-node-label {
    flex: 1;
    font-size: 14px;
    color: #303133;
  }
  .tree-node-check {
    width: 20px;
    height: 20px;
    color: #409eff;
  }
  .tree-node-children {
    margin-left: 28px;
  }
</style>
src/pages/qualityManagement/finalInspection/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,422 @@
<template>
  <view class="material-inspection-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="出厂检验详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-section"
          v-if="detailData">
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon">
            <up-icon name="file-text"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
            </u-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="detail-content">
          <view class="detail-row">
            <text class="detail-label">检测日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.checkTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">生产工单号</text>
            <text class="detail-value">{{ detailData.workOrderNo || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">工序</text>
            <text class="detail-value">{{ detailData.process || '-' }}</text>
          </view> -->
          <view class="detail-row">
            <text class="detail-label">检验员</text>
            <text class="detail-value">{{ detailData.checkName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ detailData.productName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ detailData.model || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ detailData.unit || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">数量</text>
            <text class="detail-value">{{ detailData.quantity || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检测单位</text>
            <text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">检验标准</text>
            <text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
          </view> -->
        </view>
      </view>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="detail-card"
            v-if="inspectionItems.length > 0">
        <view class="card-header">
          <view class="header-icon secondary">
            <up-icon name="list"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">检验项目</text>
        </view>
        <up-divider></up-divider>
        <view class="inspection-items">
          <view v-for="(item, index) in inspectionItems"
                :key="index"
                class="inspection-item">
            <text class="item-name">{{ item.parameterItem || '检验项目' }}</text>
            <view class="item-details">
              <view class="item-detail-row">
                <text class="item-detail-label">单位:</text>
                <text class="item-detail-value">{{ item.unit || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">标准值:</text>
                <text class="item-detail-value">{{ item.standardValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">内控值:</text>
                <text class="item-detail-value">{{ item.controlValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">检验值:</text>
                <text class="item-detail-value result"
                      :class="getResultClass(item.testValue, item.standardValue)">
                  {{ item.testValue || '-' }}
                </text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <!-- æ“ä½œæŒ‰é’® -->
      <!-- <view class="action-buttons">
        <u-button type="primary"
                  class="action-btn"
                  @click="downloadReport">
          ä¸‹è½½æŠ¥å‘Š
        </u-button>
      </view> -->
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验详情"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¯¦æƒ…数据
  const detailData = ref(null);
  // æ£€éªŒé¡¹ç›®
  const inspectionItems = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = date => {
    if (!date) return "";
    return dayjs(date).format("YYYY-MM-DD");
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = state => {
    return state ? "success" : "warning";
  };
  // èŽ·å–ç»“æžœæ ·å¼
  const getResultClass = (testValue, standardValue) => {
    // ç®€å•的结果判断逻辑,实际项目中可能需要更复杂的判断
    if (testValue === "合格") {
      return "result-passed";
    } else if (testValue === "不合格") {
      return "result-rejected";
    }
    return "";
  };
  // ä¸‹è½½æŠ¥å‘Š
  const downloadReport = () => {
    uni.showToast({
      title: "报告下载中...",
      icon: "loading",
    });
    // æ¨¡æ‹Ÿä¸‹è½½
    setTimeout(() => {
      uni.showToast({
        title: "报告下载成功",
        icon: "success",
      });
    }, 1500);
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // èŽ·å–è¯¦æƒ…æ•°æ®
  const getDetail = () => {
    const id = getPageId();
    if (!id) {
      showToast("参数错误");
      return;
    }
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–è¯¦æƒ…æ•°æ®
    try {
      const detailDataFromStorage = uni.getStorageSync("finalInspectionEditData");
      if (detailDataFromStorage) {
        detailData.value = detailDataFromStorage;
        console.log(detailData.value, "detailData.value");
        // ä½¿ç”¨qualityInspectParamInfo获取检验项目
        qualityInspectParamInfo(id)
          .then(res => {
            if (res.data && res.data.length > 0) {
              inspectionItems.value = res.data;
            } else if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              // å¦‚果接口没有返回数据,使用本地存储中的数据
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          })
          .catch(error => {
            console.error("获取检验项目失败:", error);
            // æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¸­çš„æ•°æ®æˆ–模拟数据
            if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          });
      }
    } catch (error) {
      console.error("加载详情数据失败:", error);
      showToast("加载详情数据失败,请重试");
    }
  };
  onShow(() => {
    getDetail();
  });
  onMounted(() => {
    getDetail();
  });
</script>
<style scoped lang="scss">
  .material-inspection-detail {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 20px;
  }
  .detail-section {
    padding: 15px;
  }
  .detail-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
  }
  .header-icon {
    width: 40px;
    height: 40px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
  }
  .header-icon.secondary {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  }
  .header-icon.tertiary {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  }
  .header-title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .status-tag {
    margin-left: 20rpx;
  }
  .detail-content {
    padding-top: 8px;
  }
  .detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .detail-row:last-child {
    border-bottom: none;
  }
  .detail-label {
    font-size: 14px;
    color: #666;
    min-width: 100px;
  }
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  // æ£€éªŒé¡¹ç›®
  .inspection-items {
    padding-top: 8px;
  }
  .inspection-item {
    padding: 12px;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 10px;
  }
  .inspection-item:last-child {
    margin-bottom: 0;
  }
  .item-name {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f0f0;
  }
  .item-details {
    padding-top: 8px;
  }
  .item-detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 6px;
  }
  .item-detail-row:last-child {
    margin-bottom: 0;
  }
  .item-detail-label {
    font-size: 12px;
    color: #666;
    min-width: 60px;
  }
  .item-detail-value {
    font-size: 12px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  .item-detail-value.result {
    font-weight: 500;
  }
  .result-passed {
    color: #67c23a;
  }
  .result-rejected {
    color: #f56c6c;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
</style>
src/pages/qualityManagement/finalInspection/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,566 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    qualityInspectFileAdd,
    qualityInspectFileListPage,
    qualityInspectFileDel,
  } from "@/api/qualityManagement/materialInspection";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                inspectId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              qualityInspectFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    qualityInspectFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "qualityInspectFileId"
    );
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    qualityInspectFileListPage({
      inspectId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/qualityManagement/finalInspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,816 @@
<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.productName"
                    @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.workOrderNo || '-' }}</text>
            </view>
            <!-- <view class="detail-row">
              <text class="detail-label">工序</text>
              <text class="detail-value">{{ item.process || '-' }}</text>
            </view> -->
            <view class="detail-row">
              <text class="detail-label">检验员</text>
              <text class="detail-value">{{ item.checkName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">数量</text>
              <text class="detail-value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button>
          </view>
          <view class="action-buttons">
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
              é™„ä»¶
            </u-button>
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验任务"></up-empty>
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              :range="true"
              @confirm="confirmDate" />
    <!-- åˆ†é…æ£€éªŒå‘˜å¼¹çª— -->
    <up-popup v-model:show="showAssignDialog"
              mode="center"
              round
              style="width: 80%">
      <view class="assign-dialog">
        <view class="dialog-header">
          <text class="dialog-title">分配检验员</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showAssignDialog = false"></up-icon>
        </view>
        <view class="dialog-content">
          <up-form-item label="检验员"
                        prop="checkName"
                        :label-width="60"
                        required>
            <up-input v-model="assignForm.checkName"
                      placeholder="请选择检验员"
                      readonly />
            <template #right>
              <up-icon @click="showInspectorSheet = true"
                       name="arrow-right" />
            </template>
          </up-form-item>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="showAssignDialog = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="submitAssign">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import {
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // æœç´¢è¡¨å•
  const searchForm = ref({
    productName: "",
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
  });
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  const showAssignDialog = ref(false);
  const showInspectorSheet = ref(false);
  const assignForm = ref({
    checkName: "",
  });
  const currentAssignRow = ref(null);
  // æ£€éªŒåˆ—表数据
  const inspectionList = ref([]);
  // åˆ†é¡µæ•°æ®
  const page = ref({
    current: -1,
    size: -1,
    total: 0,
  });
  // åŠ è½½çŠ¶æ€
  const tableLoading = ref(false);
  // ç»Ÿè®¡æ•°æ®
  const totalCount = ref(0);
  const submittedCount = ref(0);
  const pendingCount = ref(0);
  const qualifiedCount = ref(0);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // ActionSheet选项
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  // èŽ·å–çŠ¶æ€æ ·å¼
  const getStateClass = inspectState => {
    return inspectState ? "state-submitted" : "state-pending";
  };
  // èŽ·å–çŠ¶æ€å›¾æ ‡
  const getStateIcon = inspectState => {
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = inspectState => {
    return inspectState ? "success" : "warning";
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    searchForm.value.entryDate = e.value;
    searchForm.value.entryDateStart = dayjs(e.value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(e.value[1]).format("YYYY-MM-DD");
    getList();
  };
  const viewFileList = item => {
    uni.setStorageSync("qualityInspectFileId", item.id);
    uni.navigateTo({
      url: "/pages/qualityManagement/finalInspection/fileList",
    });
  };
  // æ¸…除日期范围
  const clearDateRange = () => {
    searchForm.value.entryDate = undefined;
    searchForm.value.entryDateStart = undefined;
    searchForm.value.entryDateEnd = undefined;
    getList();
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 2 })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
        page.value.total = res.data.total || 0;
        totalCount.value = res.data.total || 0;
        submittedCount.value = inspectionList.value.filter(
          item => item.inspectState
        ).length;
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        showToast("获取列表失败,请重试");
      });
  };
  // ç¼–辑检验
  const startInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    console.log(item, "item");
    // å­˜å‚¨å®Œæ•´çš„æ£€éªŒæ•°æ®
    uni.setStorageSync("finalInspectionEditData", item);
    // è·³è½¬åˆ°ç¼–辑页面
    uni.navigateTo({
      url: `/pages/qualityManagement/finalInspection/add?id=${item.id}&isEdit=true`,
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    uni.setStorageSync("finalInspectionEditData", item);
    // è·³è½¬åˆ°è¯¦æƒ…页面
    uni.navigateTo({
      url: `/pages/qualityManagement/finalInspection/detail?id=${item.id}`,
    });
  };
  // æ–°å¢žæ£€éªŒ
  const addInspection = () => {
    uni.navigateTo({
      url: "/pages/qualityManagement/finalInspection/add",
    });
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    try {
      const res = await submitQualityInspect({ id: item.id });
      if (res.code === 200) {
        showToast("提交成功");
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("提交失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("提交失败:", error);
      showToast("提交失败,请重试");
    }
  };
  // åˆ†é…æ£€éªŒå‘˜
  const assignInspector = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    currentAssignRow.value = item;
    getUserList();
    showAssignDialog.value = true;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    assignForm.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // æäº¤åˆ†é…
  const submitAssign = async () => {
    if (!currentAssignRow.value || !assignForm.value.checkName) {
      showToast("请选择检验员");
      return;
    }
    try {
      const data = {
        ...assignForm.value,
        id: currentAssignRow.value.id,
      };
      const res = await qualityInspectUpdate(data);
      if (res.code === 200) {
        showToast("分配成功");
        showAssignDialog.value = false;
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("分配失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("分配失败:", error);
      showToast("分配失败,请重试");
    }
  };
  // å¤„理分页
  const handlePagination = obj => {
    page.value.current = obj.current;
    page.value.size = obj.size;
    getList();
  };
  onMounted(() => {
    getList();
    getUserList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .material-inspection-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  // æœç´¢åŒºåŸŸ
  .search-section {
    padding: 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>
src/pages/qualityManagement/materialInspection/detail.vue
@@ -235,31 +235,6 @@
            ) {
              // å¦‚果接口没有返回数据,使用本地存储中的数据
              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 => {
@@ -272,90 +247,10 @@
              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: "合格",
        },
      ];
    }
  };
src/pages/qualityManagement/materialInspection/index.vue
@@ -413,6 +413,7 @@
      showToast("参数错误");
      return;
    }
    uni.setStorageSync("inspectionEditData", item);
    // è·³è½¬åˆ°è¯¦æƒ…页面
    uni.navigateTo({
      url: `/pages/qualityManagement/materialInspection/detail?id=${item.id}`,
src/pages/qualityManagement/processInspection/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1128 @@
<template>
  <view class="material-inspection-add">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="isEdit ? '编辑过程检验' : '新增过程检验'"
                @back="goBack" />
    <!-- è¡¨å•内容 -->
    <up-form :model="form"
             ref="formRef"
             label-width="110"
             :rules="rules">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <up-form-item label="工序"
                    prop="process"
                    required
                    border-bottom>
        <up-input v-model="form.process"
                  placeholder="请选择工序"
                  readonly
                  :disabled="processQuantityDisabled" />
        <template #right>
          <up-icon @click="showprocessSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="产品名称"
                    prop="productId"
                    required
                    border-bottom>
        <up-input v-model="form.productName"
                  placeholder="请选择产品"
                  readonly
                  @click="showProductTree = true"
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showProductTree = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="规格型号"
                    prop="productModelId"
                    required
                    border-bottom>
        <up-input v-model="form.model"
                  placeholder="请选择规格型号"
                  readonly
                  :disabled="isEdit" />
        <template #right>
          <up-icon @click="showModelSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="指标选择"
                    prop="testStandardId"
                    border-bottom>
        <up-input v-model="testStandardDisplay"
                  placeholder="请选择指标"
                  readonly />
        <template #right>
          <up-icon @click="openTestStandardSheet"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="单位"
                    prop="unit"
                    border-bottom>
        <up-input v-model="form.unit"
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  :disabled="processQuantityDisabled" />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
                    border-bottom>
        <up-input v-model="form.checkCompany"
                  placeholder="请输入检测单位"
                  clearable />
      </up-form-item>
      <up-form-item label="检测结果"
                    prop="checkResult"
                    required
                    border-bottom>
        <up-input v-model="form.checkResult"
                  placeholder="请选择检测结果"
                  readonly
                  @click="showResultSheet" />
        <template #right>
          <up-icon @click="showResultSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检验员"
                    prop="checkName"
                    border-bottom>
        <up-input v-model="form.checkName"
                  placeholder="请选择检验员"
                  readonly
                  @click="showInspectorSheet" />
        <template #right>
          <up-icon @click="showInspectorSheet = true"
                   name="arrow-right" />
        </template>
      </up-form-item>
      <up-form-item label="检测日期"
                    prop="checkTime"
                    required
                    border-bottom>
        <up-input v-model="form.checkTime"
                  placeholder="请选择检测日期"
                  readonly />
        <!-- <template #right>
          <up-icon name="calendar"
                   @click="showDatePicker"></up-icon>
        </template> -->
      </up-form-item>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="inspection-items-container">
        <view class="steps-header">
          <text class="steps-title">检验项目</text>
          <text class="steps-count">共 {{ tableData.length }} ä¸ªé¡¹ç›®</text>
        </view>
        <view class="steps-list">
          <view v-for="(item, index) in tableData"
                :key="index"
                class="exec-step-item">
            <view class="step-number">
              {{ index + 1 }}
            </view>
            <view class="step-content">
              <view class="step-row">
                <text class="step-label">指标:</text>
                <text class="step-value">{{ item.parameterItem }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">单位:</text>
                <text class="step-value">{{ item.unit }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">标准值:</text>
                <text class="step-value">{{ item.standardValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">内控值:</text>
                <text class="step-value">{{ item.controlValue }}</text>
              </view>
              <view class="step-row">
                <text class="step-label">检验值:</text>
                <up-input v-model="item.testValue"
                          placeholder="请输入检验值"
                          clearable
                          border-bottom
                          class="step-input" />
              </view>
            </view>
          </view>
          <view v-if="tableData.length === 0"
                class="empty-data">
            <text>请先选择指标</text>
          </view>
        </view>
      </view>
    </up-form>
    <!-- åº•部按钮 -->
    <view class="bottom-buttons">
      <up-button type="default"
                 size="default"
                 @click="goBack"
                 class="bottom-btn">
        å–消
      </up-button>
      <up-button type="primary"
                 size="default"
                 @click="submitForm"
                 :loading="loading"
                 class="bottom-btn">
        {{ isEdit ? '保存' : '提交' }}
      </up-button>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              @confirm="confirmDate" />
    <!-- å·¥åºé€‰æ‹© -->
    <up-action-sheet :show="showprocessSheet"
                     :actions="processOptions"
                     @select="selectprocess"
                     @close="showprocessSheet = false"
                     title="选择工序" />
    <!-- äº§å“é€‰æ‹© -->
    <up-action-sheet :show="showProductSheet"
                     :actions="productSheetOptions"
                     @select="selectProduct"
                     @close="showProductSheet = false"
                     title="选择产品" />
    <!-- è§„格型号选择 -->
    <up-action-sheet :show="showModelSheet"
                     :actions="modelSheetOptions"
                     @select="selectModel"
                     @close="showModelSheet = false"
                     title="选择规格型号" />
    <!-- æ£€æµ‹ç»“果选择 -->
    <up-action-sheet :show="showResultSheet"
                     :actions="resultSheetOptions"
                     @select="selectResult"
                     @close="showResultSheet = false"
                     title="选择检测结果" />
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     @close="showInspectorSheet = false"
                     title="选择检验员" />
    <!-- æŒ‡æ ‡é€‰æ‹© -->
    <up-action-sheet :show="showTestStandardSheet"
                     :actions="testStandardSheetOptions"
                     @select="selectTestStandard"
                     @close="showTestStandardSheet = false"
                     title="选择指标" />
    <!-- äº§å“æ ‘形选择器 -->
    <up-popup v-model:show="showProductTree"
              position="bottom"
              :round="true"
              :closeable="true"
              @close="showProductTree = false">
      <view class="tree-selector">
        <view class="tree-header">
          <text class="tree-title">选择产品</text>
        </view>
        <view class="tree-content">
          <view class="tree-node"
                v-for="(node, index) in productOptions"
                :key="index">
            <view v-if="node.children && node.children.length > 0"
                  class="tree-node-header"
                  @click="toggleNode(node)">
              <up-icon :name="node.expanded ? 'arrow-down' : 'arrow-right'"
                       class="tree-node-icon" />
              <text class="tree-node-label">{{ node.label }}</text>
            </view>
            <view v-else
                  class="tree-node-header"
                  @click="selectTreeNode(node)">
              <text class="tree-node-icon-placeholder"></text>
              <text class="tree-node-label">{{ node.label }}</text>
              <up-icon name="checkmark"
                       v-if="form.productId == node.value"
                       class="tree-node-check" />
            </view>
            <view v-if="node.children && node.children.length > 0 && node.expanded"
                  class="tree-node-children">
              <view class="tree-node"
                    v-for="(child, childIndex) in node.children"
                    :key="childIndex">
                <view v-if="child.children && child.children.length > 0"
                      class="tree-node-header"
                      @click="toggleNode(child)">
                  <up-icon :name="child.expanded ? 'arrow-down' : 'arrow-right'"
                           class="tree-node-icon" />
                  <text class="tree-node-label">{{ child.label }}</text>
                </view>
                <view v-else
                      class="tree-node-header"
                      @click="selectTreeNode(child)">
                  <text class="tree-node-icon-placeholder"></text>
                  <text class="tree-node-label">{{ child.label }}</text>
                  <up-icon name="checkmark"
                           v-if="form.productId == child.value"
                           class="tree-node-check" />
                </view>
                <view v-if="child.children && child.children.length > 0 && child.expanded"
                      class="tree-node-children">
                  <view class="tree-node"
                        v-for="(grandchild, grandchildIndex) in child.children"
                        :key="grandchildIndex">
                    <view class="tree-node-header"
                          @click="selectTreeNode(grandchild)">
                      <text class="tree-node-icon-placeholder"></text>
                      <text class="tree-node-label">{{ grandchild.label }}</text>
                      <up-icon name="checkmark"
                               v-if="form.productId == grandchild.value"
                               class="tree-node-check" />
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>
      </view>
    </up-popup>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted, nextTick } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import {
    qualityInspectAdd,
    qualityInspectUpdate,
    qualityInspectParamInfo,
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
    list,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¡¨å•引用
  const formRef = ref(null);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // å·¥åºé€‰æ‹©
  const showprocessSheet = ref(false);
  // äº§å“é€‰æ‹©
  const showProductSheet = ref(false);
  // äº§å“æ ‘形选择器
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
  const showTestStandardSheet = ref(false);
  // è¡¨å•数据
  const form = ref({
    checkTime: dayjs().format("YYYY-MM-DD"),
    process: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
  // æ˜¾ç¤ºç”¨çš„变量
  const testStandardDisplay = ref("");
  // æ£€éªŒé¡¹ç›®
  const tableData = ref([]);
  const tableLoading = ref(false);
  // å·¥åºåˆ—表
  const processList = ref([]);
  // äº§å“é€‰é¡¹
  const productOptions = ref([]);
  // åž‹å·é€‰é¡¹
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
  const currentProductId = ref(0);
  // ActionSheet选项
  const processOptions = computed(() => {
    return processList.value.map(item => ({
      name: item.name,
      value: item.name,
    }));
  });
  const productSheetOptions = computed(() => {
    return productOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const modelSheetOptions = computed(() => {
    return modelOptions.value.map(item => ({
      name: item.model,
      value: item.id,
    }));
  });
  const resultSheetOptions = computed(() => {
    return resultOptions.value.map(item => ({
      name: item.label,
      value: item.value,
    }));
  });
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  const testStandardSheetOptions = computed(() => {
    return testStandardOptions.value.map(item => ({
      name: item.standardName || item.standardNo,
      value: item.id,
    }));
  });
  // è¡¨å•验证规则
  const rules = {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    process: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [
      { required: true, message: "请选择产品型号", trigger: "change" },
    ],
    testStandardId: [
      { required: false, message: "请选择指标", trigger: "change" },
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
  const isEdit = computed(() => {
    const id = getPageId();
    return !!id;
  });
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
  };
  // é€‰æ‹©å·¥åº
  const selectprocess = e => {
    form.value.process = e.value;
    showprocessSheet.value = false;
  };
  // é€‰æ‹©äº§å“
  const selectProduct = e => {
    form.value.productId = e.value;
    form.value.productName = e.name;
    showProductSheet.value = false;
    getModels(e.value);
  };
  // åˆ‡æ¢æ ‘形节点展开/折叠
  const toggleNode = node => {
    node.expanded = !node.expanded;
  };
  // é€‰æ‹©æ ‘形节点
  const selectTreeNode = node => {
    // ç¡®ä¿åªé€‰æ‹©æœ«ç«¯èŠ‚ç‚¹
    if (!node.children || node.children.length == 0) {
      form.value.productId = node.value;
      form.value.productName = node.label;
      showProductTree.value = false;
      getModels(node.value);
    }
  };
  // è½¬æ¢äº§å“æ ‘结构
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // æ ¹æ®ID查找节点
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  // é€‰æ‹©è§„格型号
  const selectModel = e => {
    form.value.productModelId = e.value;
    showModelSheet.value = false;
    handleChangeModel(e.value);
  };
  // å¤„理型号变化
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  // é€‰æ‹©æ£€æµ‹ç»“æžœ
  const selectResult = e => {
    form.value.checkResult = e.value;
    showResultSheet.value = false;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    form.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // é€‰æ‹©æŒ‡æ ‡
  const selectTestStandard = e => {
    form.value.testStandardId = e.value;
    testStandardDisplay.value = e.name;
    showTestStandardSheet.value = false;
    handleTestStandardChange(e.value);
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const openTestStandardSheet = () => {
    console.log("openTestStandardSheet");
    showTestStandardSheet.value = true;
  };
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getprocessList = () => {
    list().then(res => {
      processList.value = res.data;
    });
  };
  // èŽ·å–äº§å“é€‰é¡¹
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // èŽ·å–åž‹å·åˆ—è¡¨
  const getModels = value => {
    form.value.productModelId = "";
    form.value.unit = "";
    modelOptions.value = [];
    currentProductId.value = value;
    form.value.productName = findNodeById(productOptions.value, value);
    modelList({ id: value }).then(res => {
      modelOptions.value = res;
    });
    if (currentProductId.value) {
      getList();
    }
  };
  // èŽ·å–æŒ‡æ ‡åˆ—è¡¨
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    let params = {
      productId: currentProductId.value,
      inspectType: 1,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
      testStandardDisplay.value = "";
    });
  };
  // èŽ·å–æ£€éªŒå‚æ•°åˆ—è¡¨ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    console.log("submitForm", form.value, tableData.value);
    try {
      // await formRef.value.validate();
      if (!form.value.productModelId) {
        showToast("请选择规格型号");
        return;
      }
      if (!form.value.process) {
        showToast("请选择工序");
        return;
      }
      if (!form.value.quantity) {
        showToast("请输入数量");
        return;
      }
      if (!form.value.productId) {
        showToast("请选择产品");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 1;
      if (isEdit.value) {
        tableData.value.forEach(item => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      } else {
        const res = await qualityInspectAdd(data);
        showToast("提交成功");
        setTimeout(() => {
          uni.navigateBack();
        }, 1500);
      }
    } catch (error) {
      console.error("表单验证失败:", error);
      showToast("提交失败,请重试");
    } finally {
      loading.value = false;
    }
  };
  // åˆå§‹åŒ–表单
  const initForm = async () => {
    const id = getPageId();
    if (id) {
      // ç¼–辑模式,加载数据
      // å…ˆé‡ç½®è¡¨å•数据
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
      testStandardOptions.value = [];
      tableData.value = [];
      // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
      await getProductOptions();
      // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–ç¼–è¾‘æ•°æ®
      const row = uni.getStorageSync("processInspectionEditData") || {
        id: id,
        checkTime: "2026-03-03",
        process: "上海金属材料有限公司",
        checkName: "张三",
        productName: "不锈钢板材",
        productId: 1,
        productModelId: 1,
        model: "304",
        testStandardId: "1",
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        productMainId: null,
        purchaseLedgerId: null,
      };
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      form.value = { ...row };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 1,
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“
          nextTick(() => {
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…
              const matchedOption = testStandardOptions.value.find(
                item =>
                  item.id == savedTestStandardId ||
                  String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id
                form.value.testStandardId = matchedOption.id;
                testStandardDisplay.value =
                  matchedOption.standardName || matchedOption.standardNo;
                // ç¼–辑保留原检验值,直接拉取原参数数据
                getQualityInspectParamList(row.id);
              } else {
                // å¦‚果找不到匹配项,尝试直接使用原值
                console.warn(
                  "未找到匹配的指标选项,testStandardId:",
                  savedTestStandardId
                );
                form.value.testStandardId = savedTestStandardId;
                getQualityInspectParamList(row.id);
              }
            } else {
              // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
              getQualityInspectParamList(row.id);
            }
          });
        });
      }
      // å±•开产品树到当前选中的节点
      expandProductTree(productOptions.value, row.productId);
    } else {
      // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–表单
      form.value = {
        checkTime: dayjs().format("YYYY-MM-DD"),
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
    }
  };
  // å±•开产品树到指定节点
  const expandProductTree = (nodes, targetId) => {
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.value === targetId) {
        return true; // æ‰¾åˆ°ç›®æ ‡èŠ‚ç‚¹
      }
      if (node.children && node.children.length > 0) {
        const found = expandProductTree(node.children, targetId);
        if (found) {
          node.expanded = true; // å±•开父节点
          return true;
        }
      }
    }
    return false;
  };
  onMounted(() => {
    getprocessList();
    getProductOptions();
    getUserList();
    initForm();
  });
  onShow(() => {
    initForm();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .material-inspection-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  // æ£€éªŒé¡¹ç›®å®¹å™¨
  .inspection-items-container {
    padding: 20px;
    background-color: #fff;
  }
  .steps-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e4e7ed;
  }
  .steps-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .steps-count {
    font-size: 14px;
    color: #909399;
  }
  .steps-list {
    margin-bottom: 20px;
  }
  .exec-step-item {
    position: relative;
    display: flex;
    margin-bottom: 16px;
    padding: 16px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    transition: all 0.3s ease;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  }
  .exec-step-item:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-1px);
  }
  .delete-btn {
    position: absolute;
    top: -25rpx;
    right: -25rpx;
    width: 50rpx;
    height: 50rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 20px;
    border-radius: 50%;
    background-color: red;
    border: none;
    z-index: 10;
  }
  .delete-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(245, 108, 108, 0.4);
  }
  .step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-right: 16px;
    background-color: #ecf5ff;
    color: #409eff;
    font-size: 14px;
    font-weight: 600;
    border-radius: 50%;
    flex-shrink: 0;
  }
  .step-content {
    flex: 1;
    min-width: 0;
  }
  .step-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .step-row:last-child {
    margin-bottom: 0;
  }
  .step-label {
    display: inline-block;
    width: 80px;
    font-size: 14px;
    color: #606266;
    margin-right: 12px;
    flex-shrink: 0;
    line-height: 36px;
  }
  .step-input {
    flex: 1;
    min-width: 0;
  }
  .step-input input {
    font-size: 14px;
    color: #303133;
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 44px;
    line-height: 44px;
    font-size: 14px;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 8px;
  }
  .add-step-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  }
  .add-step-btn text {
    font-size: 14px;
  }
  // åº•部按钮
  .bottom-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 16px 20px;
    background: #ffffff;
    border-top: 1px solid #f0f0f0;
    gap: 16px;
  }
  .bottom-btn {
    flex: 1;
  }
  // æ ‘形选择器样式
  .tree-selector {
    width: 100%;
    max-height: 70vh;
    background: #ffffff;
    border-radius: 16px 16px 0 0;
  }
  .tree-header {
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    text-align: center;
  }
  .tree-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .tree-content {
    padding: 10px 0;
    max-height: calc(70vh - 60px);
    overflow-y: auto;
  }
  .tree-node {
    padding: 0 20px;
  }
  .tree-node-header {
    display: flex;
    align-items: center;
    padding: 12px 0;
    cursor: pointer;
  }
  .tree-node-icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    color: #909399;
  }
  .tree-node-icon-placeholder {
    width: 20px;
    height: 20px;
    margin-right: 8px;
  }
  .tree-node-label {
    flex: 1;
    font-size: 14px;
    color: #303133;
  }
  .tree-node-check {
    width: 20px;
    height: 20px;
    color: #409eff;
  }
  .tree-node-children {
    margin-left: 28px;
  }
</style>
src/pages/qualityManagement/processInspection/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,423 @@
<template>
  <view class="material-inspection-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="过程检验详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-section"
          v-if="detailData">
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon">
            <up-icon name="file-text"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag :type="getTagType(detailData.checkResult)"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
                   class="status-tag">
              {{ detailData.inspectState ? '已提交' : '未提交' }}
            </u-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="detail-content">
          <view class="detail-row">
            <text class="detail-label">检测日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.checkTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">生产工单号</text>
            <text class="detail-value">{{ detailData.workOrderNo || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">工序</text>
            <text class="detail-value">{{ detailData.process || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检验员</text>
            <text class="detail-value">{{ detailData.checkName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品名称</text>
            <text class="detail-value">{{ detailData.productName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ detailData.model || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">单位</text>
            <text class="detail-value">{{ detailData.unit || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">数量</text>
            <text class="detail-value">{{ detailData.quantity || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检测单位</text>
            <text class="detail-value">{{ detailData.checkCompany || '-' }}</text>
          </view>
          <!-- <view class="detail-row">
            <text class="detail-label">检验标准</text>
            <text class="detail-value">{{ detailData.testStandardName || '-' }}</text>
          </view> -->
        </view>
      </view>
      <!-- æ£€éªŒé¡¹ç›® -->
      <view class="detail-card"
            v-if="inspectionItems.length > 0">
        <view class="card-header">
          <view class="header-icon secondary">
            <up-icon name="list"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">检验项目</text>
        </view>
        <up-divider></up-divider>
        <view class="inspection-items">
          <view v-for="(item, index) in inspectionItems"
                :key="index"
                class="inspection-item">
            <text class="item-name">{{ item.parameterItem || '检验项目' }}</text>
            <view class="item-details">
              <view class="item-detail-row">
                <text class="item-detail-label">单位:</text>
                <text class="item-detail-value">{{ item.unit || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">标准值:</text>
                <text class="item-detail-value">{{ item.standardValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">内控值:</text>
                <text class="item-detail-value">{{ item.controlValue || '-' }}</text>
              </view>
              <view class="item-detail-row">
                <text class="item-detail-label">检验值:</text>
                <text class="item-detail-value result"
                      :class="getResultClass(item.testValue, item.standardValue)">
                  {{ item.testValue || '-' }}
                </text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <!-- æ“ä½œæŒ‰é’® -->
      <!-- <view class="action-buttons">
        <u-button type="primary"
                  class="action-btn"
                  @click="downloadReport">
          ä¸‹è½½æŠ¥å‘Š
        </u-button>
      </view> -->
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验详情"></up-empty>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¯¦æƒ…数据
  const detailData = ref(null);
  // æ£€éªŒé¡¹ç›®
  const inspectionItems = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = date => {
    if (!date) return "";
    return dayjs(date).format("YYYY-MM-DD");
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = state => {
    return state ? "success" : "warning";
  };
  // èŽ·å–ç»“æžœæ ·å¼
  const getResultClass = (testValue, standardValue) => {
    // ç®€å•的结果判断逻辑,实际项目中可能需要更复杂的判断
    if (testValue === "合格") {
      return "result-passed";
    } else if (testValue === "不合格") {
      return "result-rejected";
    }
    return "";
  };
  // ä¸‹è½½æŠ¥å‘Š
  const downloadReport = () => {
    uni.showToast({
      title: "报告下载中...",
      icon: "loading",
    });
    // æ¨¡æ‹Ÿä¸‹è½½
    setTimeout(() => {
      uni.showToast({
        title: "报告下载成功",
        icon: "success",
      });
    }, 1500);
  };
  // èŽ·å–é¡µé¢ID
  const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    return currentPage.options.id;
  };
  // èŽ·å–è¯¦æƒ…æ•°æ®
  const getDetail = () => {
    const id = getPageId();
    if (!id) {
      showToast("参数错误");
      return;
    }
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–è¯¦æƒ…æ•°æ®
    try {
      const detailDataFromStorage = uni.getStorageSync(
        "processInspectionEditData"
      );
      if (detailDataFromStorage) {
        detailData.value = detailDataFromStorage;
        // ä½¿ç”¨qualityInspectParamInfo获取检验项目
        qualityInspectParamInfo(id)
          .then(res => {
            if (res.data && res.data.length > 0) {
              inspectionItems.value = res.data;
            } else if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              // å¦‚果接口没有返回数据,使用本地存储中的数据
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          })
          .catch(error => {
            console.error("获取检验项目失败:", error);
            // æŽ¥å£è°ƒç”¨å¤±è´¥æ—¶ï¼Œä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¸­çš„æ•°æ®æˆ–模拟数据
            if (
              detailDataFromStorage.qualityInspectParams &&
              detailDataFromStorage.qualityInspectParams.length > 0
            ) {
              inspectionItems.value = detailDataFromStorage.qualityInspectParams;
            }
          });
      }
    } catch (error) {
      console.error("加载详情数据失败:", error);
      showToast("加载详情数据失败,请重试");
    }
  };
  onShow(() => {
    getDetail();
  });
  onMounted(() => {
    getDetail();
  });
</script>
<style scoped lang="scss">
  .material-inspection-detail {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 20px;
  }
  .detail-section {
    padding: 15px;
  }
  .detail-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
  }
  .header-icon {
    width: 40px;
    height: 40px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
  }
  .header-icon.secondary {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  }
  .header-icon.tertiary {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  }
  .header-title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .status-tag {
    margin-left: 20rpx;
  }
  .detail-content {
    padding-top: 8px;
  }
  .detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .detail-row:last-child {
    border-bottom: none;
  }
  .detail-label {
    font-size: 14px;
    color: #666;
    min-width: 100px;
  }
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  // æ£€éªŒé¡¹ç›®
  .inspection-items {
    padding-top: 8px;
  }
  .inspection-item {
    padding: 12px;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 10px;
  }
  .inspection-item:last-child {
    margin-bottom: 0;
  }
  .item-name {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid #f0f0f0;
  }
  .item-details {
    padding-top: 8px;
  }
  .item-detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 6px;
  }
  .item-detail-row:last-child {
    margin-bottom: 0;
  }
  .item-detail-label {
    font-size: 12px;
    color: #666;
    min-width: 60px;
  }
  .item-detail-value {
    font-size: 12px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  .item-detail-value.result {
    font-weight: 500;
  }
  .result-passed {
    color: #67c23a;
  }
  .result-rejected {
    color: #f56c6c;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    padding: 60px 20px;
    text-align: center;
  }
</style>
src/pages/qualityManagement/processInspection/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,566 @@
<template>
  <view class="file-list-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="附件管理"
                @back="goBack" />
    <!-- é™„件列表 -->
    <view class="file-list-container">
      <view v-if="fileList.length > 0"
            class="file-list">
        <view v-for="(file, index) in fileList"
              :key="file.id || index"
              class="file-item">
          <!-- æ–‡ä»¶å›¾æ ‡ -->
          <!-- <view class="file-icon"
                :class="getFileIconClass(file.fileType)">
            <up-icon :name="getFileIcon(file.fileType)"
                     size="24"
                     color="#ffffff" />
          </view> -->
          <!-- æ–‡ä»¶ä¿¡æ¯ -->
          <view class="file-info">
            <text class="file-name">{{ file.name }}</text>
            <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} Â· {{ file.uploadTime || file.createTime }}</text> -->
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="file-actions">
            <!-- <u-button size="small"
                      type="primary"
                      plain
                      @click="previewFile(file)">预览</u-button> -->
            <u-button size="small"
                      type="info"
                      plain
                      @click="downloadFile(file)">下载并预览</u-button>
            <u-button size="small"
                      type="error"
                      plain
                      @click="confirmDelete(file, index)">删除</u-button>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-else
            class="empty-state">
        <up-icon name="document"
                 size="64"
                 color="#c0c4cc" />
        <text class="empty-text">暂无附件</text>
      </view>
    </view>
    <!-- <a rel="nofollow"
       id="downloadLink"
       href="#"
       style="display:none;">下载文本文件</a> -->
    <!-- ä¸Šä¼ æŒ‰é’® -->
    <view class="upload-button"
          @click="chooseFile">
      <up-icon name="plus"
               size="24"
               color="#ffffff" />
      <text class="upload-text">上传附件</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
  import { getToken } from "@/utils/auth";
  // import { saveAs } from "file-saver";
  import {
    listRuleFiles,
    delRuleFile,
  } from "@/api/managementMeetings/rulesRegulationsManagement";
  import {
    qualityInspectFileAdd,
    qualityInspectFileListPage,
    qualityInspectFileDel,
  } from "@/api/qualityManagement/materialInspection";
  import { blobValidate } from "@/utils/ruoyi";
  // é™„件列表
  const fileList = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // const request = axios.create({
  //   baseURL: "URL.com",
  //   adapter: axiosAdapterUniapp,
  // });
  // èŽ·å–æ–‡ä»¶å›¾æ ‡
  const getFileIcon = fileType => {
    const iconMap = {
      doc: "document",
      docx: "document",
      xls: "grid",
      xlsx: "grid",
      pdf: "document",
      ppt: "copy",
      pptx: "copy",
      txt: "document",
      jpg: "image",
      jpeg: "image",
      png: "image",
      gif: "image",
      zip: "folder",
      rar: "folder",
    };
    return iconMap[fileType.toLowerCase()] || "document";
  };
  // èŽ·å–æ–‡ä»¶å›¾æ ‡æ ·å¼ç±»
  const getFileIconClass = fileType => {
    const colorMap = {
      doc: "blue",
      docx: "blue",
      xls: "green",
      xlsx: "green",
      pdf: "red",
      ppt: "orange",
      pptx: "orange",
      txt: "gray",
      jpg: "purple",
      jpeg: "purple",
      png: "purple",
      gif: "purple",
      zip: "yellow",
      rar: "yellow",
    };
    return colorMap[fileType.toLowerCase()] || "gray";
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = bytes => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };
  // é€‰æ‹©æ–‡ä»¶
  const chooseFile = () => {
    uni.chooseImage({
      count: 9,
      sizeType: ["original", "compressed"],
      sourceType: ["album", "camera"],
      success: res => {
        console.log(res, "选择图片成功");
        uploadFiles(res.tempFiles);
      },
      fail: err => {
        console.error("选择图片失败:", err);
        showToast("选择文件失败");
      },
    });
    // uni.chooseFile({
    //   count: 9,
    //   extension: [
    //     ".doc",
    //     ".docx",
    //     ".xls",
    //     ".xlsx",
    //     ".pdf",
    //     ".ppt",
    //     ".pptx",
    //     ".txt",
    //     ".jpg",
    //     ".jpeg",
    //     ".png",
    //     ".gif",
    //     ".zip",
    //     ".rar",
    //   ],
    //   success: res => {
    //     console.log(res, "选择文件成功");
    //     uploadFiles(res.tempFiles);
    //   },
    //   fail: err => {
    //     showToast("选择文件失败");
    //   },
    // });
  };
  // ä¸Šä¼ æ–‡ä»¶
  const uploadFiles = tempFiles => {
    console.log(tempFiles, "上传文件1");
    tempFiles.forEach((tempFile, index) => {
      // æ˜¾ç¤ºä¸Šä¼ ä¸­æç¤º
      uni.showLoading({
        title: "上传中...",
        mask: true,
      });
      console.log(tempFile, "上传文件2");
      // 1. ç›´æŽ¥ä½¿ç”¨ uni.uploadFile ä¸Šä¼ æ–‡ä»¶
      uni.uploadFile({
        url: config.baseUrl + "/file/upload",
        filePath: tempFile.path,
        name: "file",
        header: {
          Authorization: "Bearer " + getToken(),
        },
        success: uploadRes => {
          uni.hideLoading();
          console.log(uploadRes, "上传文件3");
          try {
            const res = JSON.parse(uploadRes.data);
            console.log(res, "上传文件4");
            if (res.code === 200) {
              // 2. æå–文件信息
              const fileName = tempFile.name
                ? tempFile.name
                : tempFile.path.split("/").pop();
              // const fileType = fileName.split(".").pop();
              // 3. æž„造保存文件信息的参数
              const saveData = {
                name: fileName,
                inspectId: rulesRegulationsManagementId.value,
                url: res.data.tempPath || "",
              };
              console.log(saveData, "保存文件信息参数");
              // 4. è°ƒç”¨ addRuleFile æŽ¥å£ä¿å­˜æ–‡ä»¶ä¿¡æ¯
              qualityInspectFileAdd(saveData)
                .then(addRes => {
                  if (addRes.code === 200) {
                    // 5. æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨
                    const newFile = {
                      ...addRes.data,
                      uploadTime: new Date().toLocaleString(),
                    };
                    // fileList.value.push(newFile);
                    getFileList();
                    showToast("上传成功");
                  } else {
                    showToast("保存文件信息失败");
                  }
                })
                .catch(err => {
                  console.error("保存文件信息失败:", err);
                  showToast("保存文件信息失败");
                });
            } else {
              showToast("文件上传失败");
            }
          } catch (e) {
            console.error("解析上传结果失败:", e);
            showToast("上传失败");
          }
        },
        fail: err => {
          uni.hideLoading();
          console.error("上传失败:", err);
          showToast("上传失败");
        },
      });
    });
  };
  // ä¸‹è½½æ–‡ä»¶
  const downloadFile = file => {
    var url =
      config.baseUrl +
      "/common/download?fileName=" +
      encodeURIComponent(file.url) +
      "&delete=true";
    console.log(url, "url");
    uni
      .downloadFile({
        url: url,
        responseType: "blob",
        header: { Authorization: "Bearer " + getToken() },
      })
      .then(res => {
        let osType = uni.getStorageSync("deviceInfo").osName;
        let filePath = res.tempFilePath;
        if (osType === "ios") {
          uni.openDocument({
            filePath: filePath,
            showMenu: true,
            success: res => {
              resolve(res);
            },
            fail: err => {
              console.log("uni.openDocument--fail");
              reject(err);
            },
          });
        } else {
          uni.saveFile({
            tempFilePath: filePath,
            success: fileRes => {
              uni.showToast({
                icon: "none",
                mask: true,
                title:
                  "文件已保存:Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
                  fileRes.savedFilePath, //保存路径
                duration: 3000,
              });
              setTimeout(() => {
                //打开文档查看
                uni.openDocument({
                  filePath: fileRes.savedFilePath,
                  success: function (res) {
                    resolve(fileRes);
                  },
                });
              }, 3000);
            },
            fail: err => {
              console.log("uni.save--fail");
              reject(err);
            },
          });
        }
        // const isBlob = blobValidate(res.data);
        // if (isBlob) {
        //   const blob = new Blob([res.data], { type: "text/plain" });
        //   const url = URL.createObjectURL(blob);
        //   const downloadLink = document.getElementById("downloadLink");
        //   downloadLink.href = url;
        //   downloadLink.download = file.name;
        //   downloadLink.click();
        //   showToast("下载成功");
        // } else {
        //   showToast("下载失败");
        // }
      })
      .catch(err => {
        console.error("下载失败:", err);
        showToast("下载失败");
      });
  };
  // ç¡®è®¤åˆ é™¤
  const confirmDelete = (file, index) => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除附件 "${file.name}" å—?`,
      success: res => {
        if (res.confirm) {
          deleteFile(file.id, index);
        }
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = (fileId, index) => {
    uni.showLoading({
      title: "删除中...",
      mask: true,
    });
    qualityInspectFileDel([fileId])
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          // fileList.value.splice(index, 1);
          getFileList();
          showToast("删除成功");
        } else {
          showToast("删除失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("删除失败");
      });
  };
  // æ˜¾ç¤ºæç¤º
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const rulesRegulationsManagementId = ref("");
  // é¡µé¢åŠ è½½æ—¶
  onMounted(() => {
    rulesRegulationsManagementId.value = uni.getStorageSync(
      "qualityInspectFileId"
    );
    // ä»Ž API èŽ·å–é™„ä»¶åˆ—è¡¨
    getFileList();
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å– rulesRegulationsManagementId
  });
  // èŽ·å–é™„ä»¶åˆ—è¡¨
  const getFileList = () => {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    qualityInspectFileListPage({
      inspectId: rulesRegulationsManagementId.value,
      current: -1,
      size: -1,
    })
      .then(res => {
        uni.hideLoading();
        if (res.code === 200) {
          fileList.value = res.data.records || [];
        } else {
          showToast("获取附件列表失败");
        }
      })
      .catch(err => {
        uni.hideLoading();
        showToast("获取附件列表失败");
      });
  };
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .file-list-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100rpx;
  }
  .file-list-container {
    padding: 20rpx;
  }
  .file-list {
    background: #ffffff;
    border-radius: 8rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
    &:last-child {
      border-bottom: none;
    }
  }
  .file-icon {
    width: 56rpx;
    height: 56rpx;
    border-radius: 8rpx;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 20rpx;
    &.blue {
      background: #409eff;
    }
    &.green {
      background: #67c23a;
    }
    &.red {
      background: #f56c6c;
    }
    &.orange {
      background: #e6a23c;
    }
    &.gray {
      background: #909399;
    }
    &.purple {
      background: #909399;
    }
    &.yellow {
      background: #e6a23c;
    }
  }
  .file-info {
    flex: 1;
    min-width: 0;
  }
  .file-name {
    display: block;
    font-size: 16px;
    color: #303133;
    margin-bottom: 8rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-meta {
    display: block;
    font-size: 12px;
    color: #909399;
  }
  .file-actions {
    display: flex;
    gap: 12rpx;
  }
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
    background: #ffffff;
    border-radius: 8rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  }
  .empty-text {
    font-size: 14px;
    color: #909399;
    margin-top: 20rpx;
  }
  .upload-button {
    position: fixed;
    bottom: 40rpx;
    right: 40rpx;
    width: 130rpx;
    height: 130rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    z-index: 1000;
  }
  .upload-text {
    font-size: 10px;
    color: #ffffff;
    margin-top: 4rpx;
  }
  .upload-progress {
    padding: 40rpx 0;
  }
  .upload-progress-text {
    display: block;
    text-align: center;
    margin-top: 20rpx;
    font-size: 14px;
    color: #606266;
  }
</style>
src/pages/qualityManagement/processInspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,815 @@
<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.process"
                    @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.workOrderNo || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">工序</text>
              <text class="detail-value">{{ item.process || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检验员</text>
              <text class="detail-value">{{ item.checkName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">数量</text>
              <text class="detail-value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">检测单位</text>
              <text class="detail-value">{{ item.checkCompany || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="startInspection(item)">
              ç¼–辑
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState"
                      @click.stop="submitInspection(item)">
              æäº¤
            </u-button>
          </view>
          <view class="action-buttons">
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewFileList(item)">
              é™„ä»¶
            </u-button>
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.inspectState || item.checkName !== ''"
                      @click.stop="assignInspector(item)">
              åˆ†é…æ£€éªŒå‘˜
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-empty mode="data"
                text="暂无检验任务"></up-empty>
    </view>
    <!-- åˆ†é¡µç»„ä»¶ -->
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button"
          @click="addInspection">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-popup v-model:show="showDate"
              mode="date"
              :start-year="2020"
              :end-year="2030"
              :range="true"
              @confirm="confirmDate" />
    <!-- åˆ†é…æ£€éªŒå‘˜å¼¹çª— -->
    <up-popup v-model:show="showAssignDialog"
              mode="center"
              round
              style="width: 80%">
      <view class="assign-dialog">
        <view class="dialog-header">
          <text class="dialog-title">分配检验员</text>
          <up-icon name="close"
                   size="20"
                   color="#999"
                   @click="showAssignDialog = false"></up-icon>
        </view>
        <view class="dialog-content">
          <up-form-item label="检验员"
                        prop="checkName"
                        :label-width="60"
                        required>
            <up-input v-model="assignForm.checkName"
                      placeholder="请选择检验员"
                      readonly />
            <template #right>
              <up-icon @click="showInspectorSheet = true"
                       name="arrow-right" />
            </template>
          </up-form-item>
        </view>
        <view class="dialog-footer">
          <u-button type="default"
                    class="footer-btn"
                    @click="showAssignDialog = false">
            å–消
          </u-button>
          <u-button type="primary"
                    class="footer-btn"
                    @click="submitAssign">
            ç¡®å®š
          </u-button>
        </view>
      </view>
    </up-popup>
    <!-- æ£€éªŒå‘˜é€‰æ‹© -->
    <up-action-sheet :show="showInspectorSheet"
                     :actions="userSheetOptions"
                     @select="selectInspector"
                     title="选择检验员" />
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  import {
    submitQualityInspect,
    qualityInspectUpdate,
    qualityInspectListPage,
  } from "@/api/qualityManagement/materialInspection.js";
  import { userListNoPage } from "@/api/system/user.js";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // æœç´¢è¡¨å•
  const searchForm = ref({
    process: "",
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
  });
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDate = ref(false);
  // åˆ†é…æ£€éªŒå‘˜å¼¹çª—
  const showAssignDialog = ref(false);
  const showInspectorSheet = ref(false);
  const assignForm = ref({
    checkName: "",
  });
  const currentAssignRow = ref(null);
  // æ£€éªŒåˆ—表数据
  const inspectionList = ref([]);
  // åˆ†é¡µæ•°æ®
  const page = ref({
    current: -1,
    size: -1,
    total: 0,
  });
  // åŠ è½½çŠ¶æ€
  const tableLoading = ref(false);
  // ç»Ÿè®¡æ•°æ®
  const totalCount = ref(0);
  const submittedCount = ref(0);
  const pendingCount = ref(0);
  const qualifiedCount = ref(0);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // ActionSheet选项
  const userSheetOptions = computed(() => {
    return userList.value.map(item => ({
      name: item.nickName,
      value: item.nickName,
    }));
  });
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ ¼å¼åŒ–日期时间
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  // èŽ·å–çŠ¶æ€æ ·å¼
  const getStateClass = inspectState => {
    return inspectState ? "state-submitted" : "state-pending";
  };
  // èŽ·å–çŠ¶æ€å›¾æ ‡
  const getStateIcon = inspectState => {
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStateTagType = inspectState => {
    return inspectState ? "success" : "warning";
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = () => {
    showDate.value = true;
  };
  // ç¡®è®¤æ—¥æœŸé€‰æ‹©
  const confirmDate = e => {
    searchForm.value.entryDate = e.value;
    searchForm.value.entryDateStart = dayjs(e.value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(e.value[1]).format("YYYY-MM-DD");
    getList();
  };
  const viewFileList = item => {
    uni.setStorageSync("qualityInspectFileId", item.id);
    uni.navigateTo({
      url: "/pages/qualityManagement/processInspection/fileList",
    });
  };
  // æ¸…除日期范围
  const clearDateRange = () => {
    searchForm.value.entryDate = undefined;
    searchForm.value.entryDateStart = undefined;
    searchForm.value.entryDateEnd = undefined;
    getList();
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = async () => {
    try {
      const userRes = await userListNoPage();
      userList.value = userRes.data || [];
    } catch (e) {
      console.error("加载检验员列表失败", e);
      userList.value = [];
    }
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page.value };
    params.entryDate = undefined;
    qualityInspectListPage({ ...params, inspectType: 1 })
      .then(res => {
        tableLoading.value = false;
        inspectionList.value = res.data.records || [];
        page.value.total = res.data.total || 0;
        totalCount.value = res.data.total || 0;
        submittedCount.value = inspectionList.value.filter(
          item => item.inspectState
        ).length;
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        showToast("获取列表失败,请重试");
      });
  };
  // ç¼–辑检验
  const startInspection = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    // å­˜å‚¨å®Œæ•´çš„æ£€éªŒæ•°æ®
    uni.setStorageSync("processInspectionEditData", item);
    // è·³è½¬åˆ°ç¼–辑页面
    uni.navigateTo({
      url: `/pages/qualityManagement/processInspection/add?id=${item.id}&isEdit=true`,
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    uni.setStorageSync("processInspectionEditData", item);
    // è·³è½¬åˆ°è¯¦æƒ…页面
    uni.navigateTo({
      url: `/pages/qualityManagement/processInspection/detail?id=${item.id}`,
    });
  };
  // æ–°å¢žæ£€éªŒ
  const addInspection = () => {
    uni.navigateTo({
      url: "/pages/qualityManagement/processInspection/add",
    });
  };
  // æäº¤æ£€éªŒ
  const submitInspection = async item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    try {
      const res = await submitQualityInspect({ id: item.id });
      if (res.code === 200) {
        showToast("提交成功");
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("提交失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("提交失败:", error);
      showToast("提交失败,请重试");
    }
  };
  // åˆ†é…æ£€éªŒå‘˜
  const assignInspector = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    currentAssignRow.value = item;
    getUserList();
    showAssignDialog.value = true;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
  const selectInspector = e => {
    assignForm.value.checkName = e.value;
    showInspectorSheet.value = false;
  };
  // æäº¤åˆ†é…
  const submitAssign = async () => {
    if (!currentAssignRow.value || !assignForm.value.checkName) {
      showToast("请选择检验员");
      return;
    }
    try {
      const data = {
        ...assignForm.value,
        id: currentAssignRow.value.id,
      };
      const res = await qualityInspectUpdate(data);
      if (res.code === 200) {
        showToast("分配成功");
        showAssignDialog.value = false;
        setTimeout(() => {
          getList();
        }, 1000);
      } else {
        showToast("分配失败:" + (res.msg || "未知错误"));
      }
    } catch (error) {
      console.error("分配失败:", error);
      showToast("分配失败,请重试");
    }
  };
  // å¤„理分页
  const handlePagination = obj => {
    page.value.current = obj.current;
    page.value.size = obj.size;
    getList();
  };
  onMounted(() => {
    getList();
    getUserList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .material-inspection-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  // æœç´¢åŒºåŸŸ
  .search-section {
    padding: 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>