已添加2个文件
已修改18个文件
2920 ■■■■■ 文件已修改
src/api/equipmentManagement/repair.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/acceptance.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/detail.vue 360 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/index.vue 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/maintain.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/attachment.vue 704 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/components/formDia.vue 720 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/upload.vue 153 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/add.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/detail.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/finalInspection/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/add.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/detail.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/materialInspection/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/add.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/detail.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/qualityManagement/processInspection/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/repair.js
@@ -70,6 +70,19 @@
    data,
  });
};
/**
 * @desc æŠ¥ä¿®éªŒæ”¶ç¡®è®¤
 * @param {验收参数} data
 * @returns
 */
export const repairAcceptance = (data) => {
  return request({
    url: "/device/repair/acceptance",
    method: "post",
    data,
  });
};
export const getSparePartsList = (params) => {
  return request({
    url: "/spareParts/listPage",
src/pages.json
@@ -689,6 +689,20 @@
      }
    },
    {
      "path": "pages/equipmentManagement/repair/acceptance",
      "style": {
        "navigationBarTitleText": "设备报修验收",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/repair/detail",
      "style": {
        "navigationBarTitleText": "设备报修详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/upkeep/index",
      "style": {
        "navigationBarTitleText": "设备保养",
src/pages/equipmentManagement/repair/acceptance.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
<template>
  <view class="repair-acceptance">
    <PageHeader title="设备报修验收"
                @back="goBack" />
    <!-- æŠ¥ä¿®ä¿¡æ¯ -->
    <view class="section">
      <view class="section-title">报修信息</view>
      <view class="info-item">
        <text class="info-label">设备名称</text>
        <text class="info-value">{{ detail.deviceName || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">规格型号</text>
        <text class="info-value">{{ detail.deviceModel || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">报修日期</text>
        <text class="info-value">{{ formatDate(detail.repairTime) || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">报修人</text>
        <text class="info-value">{{ detail.repairName || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">维修人</text>
        <text class="info-value">{{ detail.maintenanceName || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">维修项目</text>
        <text class="info-value">{{ detail.machineryCategory || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">故障现象</text>
        <text class="info-value multi-line">{{ detail.remark || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">维修结果</text>
        <text class="info-value multi-line">{{ detail.maintenanceResult || '-' }}</text>
      </view>
      <view class="info-item">
        <text class="info-label">维修日期</text>
        <text class="info-value">{{ formatDateTime(detail.maintenanceTime) || '-' }}</text>
      </view>
    </view>
    <!-- éªŒæ”¶è¡¨å• -->
    <u-form ref="formRef"
            :model="form"
            label-width="110">
      <u-cell-group title="验收信息">
        <u-form-item label="验收人"
                     prop="acceptanceName"
                     required
                     border-bottom>
          <u-input v-model="form.acceptanceName"
                   disabled
                   placeholder="验收人" />
        </u-form-item>
        <u-form-item label="验收时间"
                     prop="acceptanceTime"
                     required
                     border-bottom>
          <u-input v-model="form.acceptanceTime"
                   placeholder="请选择验收时间"
                   readonly
                   @click="showDatePicker = true" />
          <template #right>
            <u-icon name="arrow-right"
                    @click="showDatePicker = true"></u-icon>
          </template>
        </u-form-item>
        <u-form-item label="验收备注"
                     prop="acceptanceRemark"
                     required
                     border-bottom>
          <u-textarea v-model="form.acceptanceRemark"
                      placeholder="请输入验收备注"
                      :maxlength="200"
                      count
                      :autoHeight="true" />
        </u-form-item>
      </u-cell-group>
      <view class="footer-btns">
        <u-button class="cancel-btn"
                  @click="goBack">取消</u-button>
        <u-button class="save-btn"
                  type="primary"
                  @click="handleSubmit"
                  :loading="loading">验收确认</u-button>
      </view>
    </u-form>
    <up-datetime-picker :show="showDatePicker"
                        v-model="pickerDateValue"
                        mode="datetime"
                        title="选择验收时间"
                        @confirm="onDateConfirm"
                        @cancel="showDatePicker = false" />
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { repairAcceptance, getRepairById } from "@/api/equipmentManagement/repair";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  defineOptions({ name: "repair-acceptance" });
  const showToast = message => {
    uni.showToast({ title: message, icon: "none" });
  };
  const userStore = useUserStore();
  const loading = ref(false);
  const showDatePicker = ref(false);
  const pickerDateValue = ref(Date.now());
  const repairId = ref("");
  const detail = ref({});
  const form = ref({
    id: "",
    acceptanceName: "",
    acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    acceptanceRemark: "",
  });
  const formatDate = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss");
  };
  const canAccept = item => {
    const currentName = userStore.nickName || userStore.name || "";
    return currentName && item.acceptanceName === currentName;
  };
  const loadDetail = async id => {
    try {
      const cached = uni.getStorageSync("repairDetail");
      if (cached) {
        detail.value = typeof cached === "string" ? JSON.parse(cached) : cached;
      }
      if (id && (!detail.value?.id || detail.value.id != id)) {
        const { code, data } = await getRepairById(id);
        if (code == 200) {
          detail.value = data;
        }
      }
      if (!canAccept(detail.value)) {
        showToast("仅指定验收人可进行验收");
        setTimeout(() => uni.navigateBack(), 1500);
        return;
      }
      form.value.id = detail.value.id;
      form.value.acceptanceName = detail.value.acceptanceName || "";
    } catch (e) {
      showToast("获取报修信息失败");
    }
  };
  const onDateConfirm = e => {
    form.value.acceptanceTime = dayjs(e.value).format("YYYY-MM-DD HH:mm:ss");
    pickerDateValue.value = e.value;
    showDatePicker.value = false;
  };
  const handleSubmit = async () => {
    if (!form.value.acceptanceTime?.trim()) {
      showToast("请选择验收时间");
      return;
    }
    if (!form.value.acceptanceRemark?.trim()) {
      showToast("请输入验收备注");
      return;
    }
    try {
      loading.value = true;
      const { code } = await repairAcceptance({
        id: form.value.id,
        acceptanceTime: form.value.acceptanceTime,
        acceptanceRemark: form.value.acceptanceRemark,
      });
      if (code == 200) {
        showToast("验收成功");
        uni.removeStorageSync("repairDetail");
        setTimeout(() => uni.navigateBack(), 500);
      } else {
        loading.value = false;
      }
    } catch (e) {
      loading.value = false;
      showToast("验收提交失败");
    }
  };
  const goBack = () => {
    uni.removeStorageSync("repairDetail");
    uni.navigateBack();
  };
  onLoad(options => {
    if (options.id) {
      repairId.value = options.id;
    }
  });
  onMounted(async () => {
    if (!userStore.nickName) {
      await userStore.getInfo().catch(() => {});
    }
    if (repairId.value) {
      loadDetail(repairId.value);
    }
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .repair-acceptance {
    min-height: 100vh;
    background-color: #f8f9fa;
    padding-bottom: 160rpx;
  }
  .section {
    background-color: #ffffff;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  }
  .section-title {
    font-size: 16px;
    font-weight: 600;
    color: #333333;
    padding: 16px 16px 12px;
    border-bottom: 1px solid #f0f0f0;
  }
  .info-item {
    display: flex;
    padding: 14px 16px;
    border-bottom: 1px solid #f8f8f8;
    align-items: flex-start;
  }
  .info-item:last-child {
    border-bottom: none;
  }
  .info-label {
    font-size: 14px;
    color: #666666;
    min-width: 80px;
    flex-shrink: 0;
    line-height: 22px;
  }
  .info-value {
    font-size: 14px;
    color: #333333;
    flex: 1;
    line-height: 22px;
    text-align: right;
  }
  .multi-line {
    text-align: left;
    word-break: break-all;
  }
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
  .cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #666;
    background: #f5f5f5;
    border: 1px solid #ddd;
    width: 45%;
    height: 2.5rem;
    border-radius: 2.5rem;
  }
  .save-btn {
    font-weight: 500;
    font-size: 1rem;
    color: #fff;
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    border: none;
    width: 45%;
    height: 2.5rem;
    border-radius: 2.5rem;
  }
</style>
src/pages/equipmentManagement/repair/add.vue
@@ -76,6 +76,13 @@
                   placeholder="请输入维修人"
                   clearable />
        </u-form-item>
        <u-form-item label="验收人"
                     prop="acceptanceName"
                     border-bottom>
          <u-input v-model="form.acceptanceName"
                   placeholder="请输入验收人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修项目"
                     prop="machineryCategory"
                     border-bottom>
@@ -198,6 +205,8 @@
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸ
    repairName: undefined, // æŠ¥ä¿®äºº
    maintenanceName: undefined, // ç»´ä¿®äºº
    acceptanceName: undefined, // éªŒæ”¶äºº
    status: "0", // æŠ¥ä¿®çŠ¶æ€
    machineryCategory: undefined, // ç»´ä¿®é¡¹ç›®
    remark: undefined, // æ•…障现象
    storageBlobDTOs: [], // å›¾ç‰‡é™„ä»¶
@@ -206,10 +215,11 @@
  // æŠ¥ä¿®çŠ¶æ€é€‰é¡¹
  const repairStatusOptions = ref([
    { name: "待维修", value: "0" },
    { name: "完结", value: "1" },
    { name: "失败", value: "2" },
    { name: "完成", value: "1" },
    { name: "维修失败", value: "2" },
    { name: "待验收", value: "3" },
  ]);
  const repairStatusText = ref("");
  const repairStatusText = ref("待维修");
  // æ‰“开报修状态选择器
  const openRepairStatusPicker = () => {
@@ -253,12 +263,15 @@
          form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD");
          form.value.repairName = data.repairName;
          form.value.maintenanceName = data.maintenanceName;
          form.value.acceptanceName = data.acceptanceName;
          form.value.status = String(data.status ?? "0");
          form.value.machineryCategory = data.machineryCategory;
          form.value.remark = data.remark;
          form.value.storageBlobDTOs = data.storageBlobVOs || [];
          repairStatusText.value =
            repairStatusOptions.value.find(item => item.value == data.status)
              ?.name || "";
            repairStatusOptions.value.find(
              item => item.value == String(data.status)
            )?.name || "";
          // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
          const device = deviceOptions.value.find(
            item => item.id === data.deviceLedgerId
src/pages/equipmentManagement/repair/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,360 @@
<template>
  <view class="repair-detail">
    <PageHeader title="设备报修详情"
                @back="goBack" />
    <view class="content-container">
      <!-- 1. æŠ¥ä¿®ç™»è®° -->
      <view class="section">
        <view class="section-head">
          <view class="section-bar"></view>
          <text class="section-head-text">1. æŠ¥ä¿®ç™»è®°</text>
        </view>
        <view class="desc-table">
          <view class="desc-row">
            <view class="desc-cell">
              <text class="desc-label">设备名称</text>
              <text class="desc-value">{{ detail.deviceName || '-' }}</text>
            </view>
            <view class="desc-cell">
              <text class="desc-label">规格型号</text>
              <text class="desc-value">{{ detail.deviceModel || '-' }}</text>
            </view>
          </view>
          <view class="desc-row">
            <view class="desc-cell">
              <text class="desc-label">报修日期</text>
              <text class="desc-value">{{ formatDate(detail.repairTime) || '-' }}</text>
            </view>
            <view class="desc-cell">
              <text class="desc-label">报修人</text>
              <text class="desc-value">{{ detail.repairName || '-' }}</text>
            </view>
          </view>
          <view class="desc-row">
            <view class="desc-cell">
              <text class="desc-label">验收人</text>
              <text class="desc-value">{{ detail.acceptanceName || '-' }}</text>
            </view>
            <view class="desc-cell">
              <text class="desc-label">维修人</text>
              <text class="desc-value">{{ detail.maintenanceName || '-' }}</text>
            </view>
          </view>
          <view class="desc-row full">
            <view class="desc-cell full">
              <text class="desc-label">故障现象</text>
              <text class="desc-value">{{ detail.remark || '-' }}</text>
            </view>
          </view>
          <view class="desc-row full">
            <view class="desc-cell full status-cell">
              <text class="desc-label">当前状态</text>
              <view class="desc-value">
                <u-tag :type="getStatusType(detail.status)"
                       size="mini">{{ getStatusLabel(detail.status) }}</u-tag>
              </view>
            </view>
          </view>
        </view>
        <view class="image-block">
          <text class="image-block-title">设备问题图片</text>
          <view v-if="imageList.length"
                class="image-content">
            <CommonUpload :model-value="imageList"
                          disabled />
          </view>
          <view v-else
                class="image-empty">
            <up-icon name="photo"
                     size="40"
                     color="#c0c4cc"></up-icon>
            <text class="image-empty-text">暂无设备问题图片</text>
          </view>
        </view>
      </view>
      <!-- 2. ç»´ä¿®å¤„理 -->
      <view class="section">
        <view class="section-head">
          <view class="section-bar"></view>
          <text class="section-head-text">2. ç»´ä¿®å¤„理</text>
        </view>
        <view class="desc-table">
          <view class="desc-row">
            <view class="desc-cell">
              <text class="desc-label">维修人</text>
              <text class="desc-value">{{ detail.maintenanceName || '-' }}</text>
            </view>
            <view class="desc-cell">
              <text class="desc-label">维修时间</text>
              <text class="desc-value">{{ formatDateTime(detail.maintenanceTime) || '-' }}</text>
            </view>
          </view>
          <view class="desc-row full">
            <view class="desc-cell full">
              <text class="desc-label">维修结果</text>
              <text class="desc-value">{{ detail.maintenanceResult || '-' }}</text>
            </view>
          </view>
        </view>
      </view>
      <!-- 3. éªŒæ”¶ -->
      <view class="section">
        <view class="section-head">
          <view class="section-bar"></view>
          <text class="section-head-text">3. éªŒæ”¶</text>
        </view>
        <view class="desc-table">
          <view class="desc-row">
            <view class="desc-cell">
              <text class="desc-label">验收人</text>
              <text class="desc-value">{{ detail.acceptanceName || '-' }}</text>
            </view>
            <view class="desc-cell">
              <text class="desc-label">验收时间</text>
              <text class="desc-value">{{ formatDateTime(detail.acceptanceTime) || '-' }}</text>
            </view>
          </view>
          <view class="desc-row full">
            <view class="desc-cell full">
              <text class="desc-label">验收备注</text>
              <text class="desc-value">{{ detail.acceptanceRemark || '-' }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import { getRepairById } from "@/api/equipmentManagement/repair";
  import dayjs from "dayjs";
  defineOptions({ name: "repair-detail" });
  const showToast = message => {
    uni.showToast({ title: message, icon: "none" });
  };
  const repairId = ref("");
  const detail = ref({});
  const STATUS_MAP = {
    0: { label: "待维修", type: "error" },
    1: { label: "完成", type: "success" },
    2: { label: "维修失败", type: "warning" },
    3: { label: "待验收", type: "primary" },
  };
  const getStatusLabel = status => STATUS_MAP[status]?.label || "-";
  const getStatusType = status => STATUS_MAP[status]?.type || "info";
  const imageList = computed(() => {
    return detail.value.storageBlobVOs || detail.value.storageBlobDTOs || [];
  });
  const formatDate = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD");
  };
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss");
  };
  const loadDetail = async id => {
    try {
      const cached = uni.getStorageSync("repairDetail");
      if (cached) {
        detail.value = typeof cached === "string" ? JSON.parse(cached) : cached;
      }
      if (id) {
        const { code, data } = await getRepairById(id);
        if (code == 200) {
          detail.value = { ...detail.value, ...data };
        }
      }
    } catch (e) {
      showToast("获取详情失败");
    }
  };
  const goBack = () => {
    uni.removeStorageSync("repairDetail");
    uni.navigateBack();
  };
  onLoad(options => {
    if (options.id) {
      repairId.value = options.id;
    }
  });
  onMounted(() => {
    loadDetail(repairId.value);
  });
</script>
<style scoped lang="scss">
  .repair-detail {
    min-height: 100vh;
    background-color: #f0f2f5;
    padding-bottom: 24px;
  }
  .content-container {
    padding: 12px;
  }
  .section {
    background-color: #ffffff;
    border-radius: 4px;
    margin-bottom: 12px;
    overflow: hidden;
    border: 1px solid #ebeef5;
  }
  .section-head {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid #ebeef5;
    background: #fff;
  }
  .section-bar {
    width: 4px;
    height: 16px;
    background: #409eff;
    border-radius: 2px;
    margin-right: 8px;
    flex-shrink: 0;
  }
  .section-head-text {
    font-size: 15px;
    font-weight: 600;
    color: #303133;
  }
  .desc-table {
    border-top: 1px solid #ebeef5;
  }
  .desc-row {
    display: flex;
    border-bottom: 1px solid #ebeef5;
    &:last-child {
      border-bottom: none;
    }
    &.full {
      .desc-cell.full {
        flex: 1;
        width: 100%;
      }
    }
  }
  .desc-cell {
    flex: 1;
    display: flex;
    min-width: 0;
    border-right: 1px solid #ebeef5;
    &:last-child {
      border-right: none;
    }
    &.full {
      border-right: none;
    }
  }
  .desc-label {
    width: 88px;
    flex-shrink: 0;
    padding: 10px 12px;
    font-size: 13px;
    color: #606266;
    background: #f5f7fa;
    border-right: 1px solid #ebeef5;
    line-height: 20px;
    box-sizing: border-box;
  }
  .desc-value {
    flex: 1;
    padding: 10px 12px;
    font-size: 13px;
    color: #303133;
    background: #ffffff;
    line-height: 20px;
    word-break: break-all;
    min-width: 0;
    box-sizing: border-box;
  }
  .status-cell {
    .desc-value {
      display: flex;
      align-items: center;
    }
  }
  .image-block {
    padding: 12px 16px 16px;
    border-top: 1px solid #ebeef5;
  }
  .image-block-title {
    display: block;
    font-size: 13px;
    color: #606266;
    margin-bottom: 12px;
  }
  .image-content {
    padding: 0;
  }
  .image-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 120px;
    background: #fafafa;
    border: 1px dashed #dcdfe6;
    border-radius: 4px;
    gap: 8px;
  }
  .image-empty-text {
    font-size: 13px;
    color: #c0c4cc;
  }
  @media (max-width: 400px) {
    .desc-row:not(.full) {
      flex-direction: column;
      .desc-cell {
        border-right: none;
        border-bottom: 1px solid #ebeef5;
        &:last-child {
          border-bottom: none;
        }
      }
    }
  }
</style>
src/pages/equipmentManagement/repair/index.vue
@@ -37,10 +37,8 @@
              <text class="item-id">设备名称:{{ item.deviceName }}</text>
            </view>
            <view class="status-tag">
              <u-tag v-if="item.status === 1"
                     type="success">完结</u-tag>
              <u-tag v-if="item.status === 0"
                     type="error">待维修</u-tag>
              <u-tag :type="getStatusType(item.status)"
                     size="mini">{{ getStatusLabel(item.status) }}</u-tag>
            </view>
          </view>
          <up-divider></up-divider>
@@ -62,6 +60,10 @@
              <text class="detail-value">{{ item.maintenanceName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">验收人</text>
              <text class="detail-value">{{ item.acceptanceName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修项目</text>
              <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
            </view>
@@ -75,11 +77,26 @@
            </view>
            <view class="detail-row">
              <text class="detail-label">维修日期</text>
              <text class="detail-value">{{ formatDate(item.maintenanceTime) || '-' }}</text>
              <text class="detail-value">{{ formatDateTime(item.maintenanceTime) || '-' }}</text>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <u-button type="info"
                      size="small"
                      plain
                      class="action-btn"
                      @click="goDetail(item)">
              è¯¦æƒ…
            </u-button>
            <u-button type="success"
                      size="small"
                      class="action-btn"
                      v-if="item.status === 3"
                      :disabled="!canAccept(item)"
                      @click="goAcceptance(item)">
              éªŒæ”¶
            </u-button>
            <u-button type="primary"
                      size="small"
                      class="action-btn"
@@ -135,6 +152,22 @@
  const userStore = useUserStore();
  const STATUS_MAP = {
    0: { label: "待维修", type: "error" },
    1: { label: "完成", type: "success" },
    2: { label: "维修失败", type: "warning" },
    3: { label: "待验收", type: "primary" },
  };
  const getStatusLabel = status => STATUS_MAP[status]?.label || "-";
  const getStatusType = status => STATUS_MAP[status]?.type || "info";
  const canAccept = item => {
    if (item.status !== 3) return false;
    const currentName = userStore.nickName || userStore.name || "";
    return currentName && item.acceptanceName === currentName;
  };
  // æœç´¢å…³é”®è¯
  const searchKeyword = ref("");
@@ -154,6 +187,18 @@
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  };
  const formatDateTime = dateStr => {
    if (!dateStr) return "";
    const date = new Date(dateStr);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    const hours = String(date.getHours()).padStart(2, "0");
    const minutes = String(date.getMinutes()).padStart(2, "0");
    const seconds = String(date.getSeconds()).padStart(2, "0");
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  };
  // æŸ¥è¯¢åˆ—表
@@ -194,7 +239,6 @@
      showToast("参数错误");
      return;
    }
    // ä½¿ç”¨uni.setStorageSync存储id
    uni.setStorageSync("repairId", id);
    uni.navigateTo({
      url: "/pages/equipmentManagement/repair/maintain",
@@ -208,13 +252,31 @@
    });
  };
  // ç¼–辑 - è·³è½¬åˆ°add页面,通过id区分新增还是编辑
  // ç¼–辑
  const edit = id => {
    if (!id) return;
    // ä½¿ç”¨uni.setStorageSync存储id
    // uni.setStorageSync("repairId", id);
    uni.navigateTo({
      url: "/pages/equipmentManagement/repair/add?id=" + id,
    });
  };
  // è¯¦æƒ…
  const goDetail = item => {
    uni.setStorageSync("repairDetail", JSON.stringify(item));
    uni.navigateTo({
      url: "/pages/equipmentManagement/repair/detail?id=" + item.id,
    });
  };
  // éªŒæ”¶
  const goAcceptance = item => {
    if (!canAccept(item)) {
      showToast("仅指定验收人可进行验收");
      return;
    }
    uni.setStorageSync("repairDetail", JSON.stringify(item));
    uni.navigateTo({
      url: "/pages/equipmentManagement/repair/acceptance?id=" + item.id,
    });
  };
@@ -243,6 +305,9 @@
  };
  onMounted(() => {
    if (!userStore.nickName) {
      userStore.getInfo().catch(() => {});
    }
    getList();
  });
@@ -254,9 +319,8 @@
<style scoped lang="scss">
  @import "@/styles/sales-common.scss";
  // è®¾å¤‡ç»´ä¿®ç‰¹æœ‰æ ·å¼
  .sales-account {
    padding-bottom: 80px; // ä¸ºæµ®åŠ¨æŒ‰é’®ç•™å‡ºç©ºé—´
    padding-bottom: 80px;
  }
  .status-tag {
@@ -265,6 +329,7 @@
  }
  .action-buttons {
    gap: 8px; // ä¸Žå…¬å…±æ ·å¼ä¸­çš„ 12px ä¸åŒ
    gap: 8px;
    flex-wrap: wrap;
  }
</style>
</style>
src/pages/equipmentManagement/repair/maintain.vue
@@ -12,10 +12,10 @@
      <u-cell-group title="维修信息"
                    inset>
        <u-form-item prop="maintenanceName"
                     label="报修人"
                     label="维修人"
                     required>
          <u-input v-model="form.maintenanceName"
                   placeholder="请输入报修人"
                   placeholder="请输入维修人"
                   clearable />
        </u-form-item>
        <u-form-item prop="maintenanceResult"
@@ -177,7 +177,7 @@
  // è¡¨å•验证规则
  const formRules = {
    maintenanceName: [
      { required: true, trigger: "blur", message: "请输入报修人" },
      { required: true, trigger: "blur", message: "请输入维修人" },
    ],
    maintenanceResult: [
      { required: true, trigger: "blur", message: "请输入维修结果" },
@@ -188,10 +188,11 @@
  };
  const repairStatusOptions = ref([
    { name: "待维修", value: "0" },
    { name: "完结", value: "1" },
    { name: "失败", value: "2" },
    { name: "完成", value: "1" },
    { name: "维修失败", value: "2" },
    { name: "待验收", value: "3" },
  ]);
  const repairStatusText = ref("完结");
  const repairStatusText = ref("待验收");
  // æ‰“开报修状态选择器
  const openRepairStatusPicker = () => {
    uni.showActionSheet({
@@ -207,7 +208,7 @@
    maintenanceName: userStore.nickName || "", // é»˜è®¤ä½¿ç”¨å½“前用户昵称
    maintenanceResult: undefined, // ç»´ä¿®ç»“æžœ
    maintenanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // ç»´ä¿®æ—¥æœŸï¼ˆåªæ˜¾ç¤ºæ—¥æœŸï¼‰
    status: "1",
    status: "3",
    sparePartsIds: [],
  });
@@ -225,7 +226,7 @@
      maintenanceName: userStore.nickName || "",
      maintenanceResult: undefined,
      maintenanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
      status: "1",
      status: "3",
      sparePartsIds: [],
    };
    selectedSpareParts.value = [];
@@ -411,7 +412,7 @@
  // åˆå§‹åŒ–表单数据
  const initForm = async () => {
    form.value.status = "1";
    form.value.status = "3";
    // è®¾ç½®æŠ¥ä¿®äººä¸ºå½“前用户昵称
    form.value.maintenanceName = userStore.nickName || "";
    // è®¾ç½®å½“前日期(只包含年月日)
src/pages/inspectionUpload/attachment.vue
@@ -1,53 +1,28 @@
<template>
  <view class="attachment-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader :title="`查看附件 - ${taskInfo?.taskName || ''}`" @back="goBack" />
    <PageHeader :title="`查看附件 - ${taskInfo?.taskName || ''}`"
                @back="goBack" />
    <!-- é¡µé¢å†…容 -->
    <view class="attachment-content">
      <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
      <view class="attachment-tabs">
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'before' }"
          @click="switchViewType('before')"
        >
          ç”Ÿäº§å‰ ({{ getAttachmentsByType(0).length }})
        </view>
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'after' }"
          @click="switchViewType('after')"
        >
          ç”Ÿäº§ä¸­ ({{ getAttachmentsByType(1).length }})
        </view>
        <view
          class="tab-item"
          :class="{ active: currentViewType === 'issue' }"
          @click="switchViewType('issue')"
        >
          ç”Ÿäº§åŽ ({{ getAttachmentsByType(2).length }})
        </view>
      </view>
      <!-- å½“前分类的附件列表 -->
      <view class="attachment-list-container">
        <view v-if="getCurrentViewAttachments().length > 0" class="attachment-list">
          <view
            v-for="(file, index) in getCurrentViewAttachments()"
            :key="index"
            class="attachment-item"
            @click="previewAttachment(file)"
          >
        <view v-if="attachmentList.length > 0"
              class="attachment-list">
          <view v-for="(file, index) in attachmentList"
                :key="index"
                class="attachment-item"
                @click="previewAttachment(file)">
            <view class="attachment-preview-container">
              <image
                v-if="isImageFile(file)"
                :src="file.url || file.downloadUrl"
                class="attachment-preview"
                mode="aspectFill"
              />
              <view v-else class="attachment-video-preview">
                <u-icon name="video" size="40" color="#409eff"></u-icon>
              <image v-if="isImageFile(file)"
                     :src="file.url || file.downloadUrl"
                     class="attachment-preview"
                     mode="aspectFill" />
              <view v-else
                    class="attachment-video-preview">
                <u-icon name="video"
                        size="40"
                        color="#409eff"></u-icon>
                <text class="video-text">视频</text>
              </view>
            </view>
@@ -57,31 +32,37 @@
            </view>
          </view>
        </view>
        <view v-else class="attachment-empty">
          <u-icon name="folder-open" size="60" color="#ccc"></u-icon>
        <view v-else
              class="attachment-empty">
          <u-icon name="folder-open"
                  size="60"
                  color="#ccc"></u-icon>
          <text class="empty-text">该分类暂无附件</text>
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
    <view v-if="showVideoDialog" class="video-modal-overlay" @click="closeVideoPreview">
      <view class="video-modal-container" @click.stop>
    <view v-if="showVideoDialog"
          class="video-modal-overlay"
          @click="closeVideoPreview">
      <view class="video-modal-container"
            @click.stop>
        <view class="video-modal-header">
          <text class="video-modal-title">{{ currentVideoFile?.originalFilename || '视频预览' }}</text>
          <view class="close-btn-video" @click="closeVideoPreview">
            <u-icon name="close" size="20" color="#fff"></u-icon>
          <view class="close-btn-video"
                @click="closeVideoPreview">
            <u-icon name="close"
                    size="20"
                    color="#fff"></u-icon>
          </view>
        </view>
        <view class="video-modal-body">
          <video
            v-if="currentVideoFile"
            :src="currentVideoFile.url || currentVideoFile.downloadUrl"
            class="video-player"
            controls
            autoplay
            @error="handleVideoError"
          ></video>
          <video v-if="currentVideoFile"
                 :src="currentVideoFile.url || currentVideoFile.downloadUrl"
                 class="video-player"
                 controls
                 autoplay
                 @error="handleVideoError"></video>
        </view>
      </view>
    </view>
@@ -89,372 +70,351 @@
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import config from '@/config';
  import { ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import config from "@/config";
// ä»»åŠ¡ä¿¡æ¯
const taskInfo = ref(null);
  // ä»»åŠ¡ä¿¡æ¯
  const taskInfo = ref(null);
// é™„件列表
const attachmentList = ref([]);
  // é™„件列表
  const attachmentList = ref([]);
// å½“前查看类型
const currentViewType = ref('before'); // 'before', 'after', 'issue'
  // è§†é¢‘预览相关状态
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
// è§†é¢‘预览相关状态
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
  // æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸ
  const filePreviewBase = config.fileUrl;
// æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸ
const filePreviewBase = config.fileUrl;
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.taskInfo) {
    try {
      taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
      loadAttachments();
    } catch (e) {
      console.error('解析任务信息失败:', e);
      uni.showToast({
        title: '加载失败',
        icon: 'error'
      });
  // é¡µé¢åŠ è½½
  onLoad(options => {
    if (options.taskInfo) {
      try {
        taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
        loadAttachments();
      } catch (e) {
        console.error("解析任务信息失败:", e);
        uni.showToast({
          title: "加载失败",
          icon: "error",
        });
      }
    }
  }
});
  });
// åŠ è½½é™„ä»¶æ•°æ®
const loadAttachments = () => {
  const task = taskInfo.value;
  if (!task) return;
  // åŠ è½½é™„ä»¶æ•°æ®
  const loadAttachments = () => {
    const task = taskInfo.value;
    if (!task) return;
  attachmentList.value = [];
    attachmentList.value = [];
  // åŽç«¯åæ˜¾å­—段
  const allList = Array.isArray(task?.commonFileList) ? task.commonFileList : [];
  const beforeList = Array.isArray(task?.commonFileListBefore)
    ? task.commonFileListBefore
    : allList.filter((f) => f?.type === 10);
  const afterList = Array.isArray(task?.commonFileListAfter)
    ? task.commonFileListAfter
    : allList.filter((f) => f?.type === 11);
  const issueList = Array.isArray(task?.commonFileListIssue)
    ? task.commonFileListIssue
    : allList.filter((f) => f?.type === 12);
    // èŽ·å–é™„ä»¶åˆ—è¡¨ï¼Œä¼˜å…ˆä»Ž commonFileListBeforeVO èŽ·å–
    let rawList = [];
    if (Array.isArray(task.commonFileListBeforeVO)) {
      rawList = task.commonFileListBeforeVO;
    } else if (Array.isArray(task.commonFileListBefore)) {
      rawList = task.commonFileListBefore;
    } else if (Array.isArray(task.commonFileList)) {
      // é™çº§ï¼šä»Žé€šç”¨åˆ—表过滤 type ä¸º 10 çš„
      rawList = task.commonFileList.filter(f => f?.type === 10);
    }
  const mapToViewFile = (file, viewType) => {
    const u = normalizeFileUrl(file?.url || file?.downloadUrl || '');
    return {
      ...file,
      type: viewType,
      name: file?.name || file?.originalFilename || file?.bucketFilename,
      bucketFilename: file?.bucketFilename || file?.name,
      originalFilename: file?.originalFilename || file?.name,
      url: u,
      downloadUrl: u,
      size: file?.size || file?.byteSize,
    const mapToViewFile = file => {
      // ä¼˜å…ˆä½¿ç”¨ previewURL æˆ– url
      const rawUrl =
        file?.previewURL ||
        file?.url ||
        file?.downloadUrl ||
        file?.downloadURL ||
        "";
      const u = normalizeFileUrl(rawUrl);
      return {
        ...file,
        name:
          file?.name || file?.originalFilename || file?.bucketFilename || "附件",
        bucketFilename:
          file?.bucketFilename || file?.name || file?.originalFilename,
        originalFilename: file?.originalFilename || file?.name,
        url: u,
        downloadUrl: u,
        size: file?.size || file?.byteSize || 0,
      };
    };
    attachmentList.value = rawList.map(f => mapToViewFile(f));
  };
  attachmentList.value.push(...beforeList.map((f) => mapToViewFile(f, 0)));
  attachmentList.value.push(...afterList.map((f) => mapToViewFile(f, 1)));
  attachmentList.value.push(...issueList.map((f) => mapToViewFile(f, 2)));
};
  // å°†åŽç«¯è¿”回的文件地址规范成可访问URL
  const normalizeFileUrl = rawUrl => {
    try {
      if (!rawUrl || typeof rawUrl !== "string") return "";
      const url = rawUrl.trim();
      if (!url) return "";
      if (/^https?:\/\//i.test(url)) return url;
      if (url.startsWith("/")) return `${filePreviewBase}${url}`;
// å°†åŽç«¯è¿”回的文件地址规范成可访问URL
const normalizeFileUrl = (rawUrl) => {
  try {
    if (!rawUrl || typeof rawUrl !== 'string') return '';
    const url = rawUrl.trim();
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    if (url.startsWith('/')) return `${filePreviewBase}${url}`;
    // Windows path -> web path
    if (/^[a-zA-Z]:\\/.test(url)) {
      const normalized = url.replace(/\\/g, '/');
      const idx = normalized.indexOf('/prod/');
      if (idx >= 0) {
        const relative = normalized.slice(idx + '/prod/'.length);
        return `${filePreviewBase}/${relative}`;
      // Windows path -> web path
      if (/^[a-zA-Z]:\\/.test(url)) {
        const normalized = url.replace(/\\/g, "/");
        const idx = normalized.indexOf("/prod/");
        if (idx >= 0) {
          const relative = normalized.slice(idx + "/prod/".length);
          return `${filePreviewBase}/${relative}`;
        }
        return normalized;
      }
      return normalized;
      return `${filePreviewBase}/${url.replace(/^\//, "")}`;
    } catch (e) {
      return rawUrl || "";
    }
  };
    return `${filePreviewBase}/${url.replace(/^\//, '')}`;
  } catch (e) {
    return rawUrl || '';
  }
};
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
  const isImageFile = file => {
    if (file.contentType && file.contentType.startsWith("image/")) {
      return true;
    }
    if (file.type === "image") return true;
// åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
const switchViewType = (type) => {
  currentViewType.value = type;
};
    const name = file.bucketFilename || file.originalFilename || file.name || "";
    const ext = name.split(".").pop()?.toLowerCase();
    return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
  };
// æ ¹æ®type获取对应分类的附件
const getAttachmentsByType = (typeValue) => {
  return attachmentList.value.filter((file) => file.type === typeValue) || [];
};
  // é¢„览附件
  const previewAttachment = file => {
    if (isImageFile(file)) {
      const imageUrls = attachmentList.value
        .filter(f => isImageFile(f))
        .map(f => f.url || f.downloadUrl);
// èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
const getCurrentViewAttachments = () => {
  switch (currentViewType.value) {
    case 'before':
      return getAttachmentsByType(0);
    case 'after':
      return getAttachmentsByType(1);
    case 'issue':
      return getAttachmentsByType(2);
    default:
      return [];
  }
};
      uni.previewImage({
        urls: imageUrls,
        current: file.url || file.downloadUrl,
      });
    } else {
      showVideoPreview(file);
    }
  };
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
const isImageFile = (file) => {
  if (file.contentType && file.contentType.startsWith('image/')) {
    return true;
  }
  if (file.type === 'image') return true;
  // æ˜¾ç¤ºè§†é¢‘预览
  const showVideoPreview = file => {
    currentVideoFile.value = file;
    showVideoDialog.value = true;
  };
  const name = file.bucketFilename || file.originalFilename || file.name || '';
  const ext = name.split('.').pop()?.toLowerCase();
  return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext);
};
  // å…³é—­è§†é¢‘预览
  const closeVideoPreview = () => {
    showVideoDialog.value = false;
    currentVideoFile.value = null;
  };
// é¢„览附件
const previewAttachment = (file) => {
  if (isImageFile(file)) {
    const imageUrls = getCurrentViewAttachments()
      .filter((f) => isImageFile(f))
      .map((f) => f.url || f.downloadUrl);
    uni.previewImage({
      urls: imageUrls,
      current: file.url || file.downloadUrl,
  // è§†é¢‘播放错误处理
  const handleVideoError = () => {
    uni.showToast({
      title: "视频播放失败",
      icon: "error",
    });
  } else {
    showVideoPreview(file);
  }
};
  };
// æ˜¾ç¤ºè§†é¢‘预览
const showVideoPreview = (file) => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
// å…³é—­è§†é¢‘预览
const closeVideoPreview = () => {
  showVideoDialog.value = false;
  currentVideoFile.value = null;
};
// è§†é¢‘播放错误处理
const handleVideoError = () => {
  uni.showToast({
    title: '视频播放失败',
    icon: 'error',
  });
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size) => {
  if (!size) return '';
  if (size < 1024) return size + 'B';
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB';
  return (size / (1024 * 1024)).toFixed(1) + 'MB';
};
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (!size) return "";
    if (size < 1024) return size + "B";
    if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
    return (size / (1024 * 1024)).toFixed(1) + "MB";
  };
</script>
<style scoped>
.attachment-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
  .attachment-page {
    min-height: 100vh;
    background-color: #f5f5f5;
  }
.attachment-content {
  padding: 15px;
}
  .attachment-content {
    padding: 15px;
  }
/* æ ‡ç­¾é¡µæ ·å¼ */
.attachment-tabs {
  display: flex;
  background: #fff;
  border-radius: 12px;
  margin-bottom: 15px;
  padding: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
  /* æ ‡ç­¾é¡µæ ·å¼ */
  .attachment-tabs {
    display: flex;
    background: #fff;
    border-radius: 12px;
    margin-bottom: 15px;
    padding: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
.tab-item {
  flex: 1;
  text-align: center;
  padding: 12px 8px;
  font-size: 14px;
  color: #666;
  border-radius: 8px;
  transition: all 0.3s ease;
}
  .tab-item {
    flex: 1;
    text-align: center;
    padding: 12px 8px;
    font-size: 14px;
    color: #666;
    border-radius: 8px;
    transition: all 0.3s ease;
  }
.tab-item.active {
  background: #409eff;
  color: #fff;
  font-weight: 500;
}
  .tab-item.active {
    background: #409eff;
    color: #fff;
    font-weight: 500;
  }
/* é™„件列表样式 */
.attachment-list-container {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  min-height: 400px;
}
  /* é™„件列表样式 */
  .attachment-list-container {
    background: #fff;
    border-radius: 12px;
    padding: 15px;
    min-height: 400px;
  }
.attachment-list {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}
  .attachment-list {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
  }
.attachment-item {
  width: calc(33.33% - 10px);
  background: #f8f9fa;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transition: all 0.3s ease;
}
  .attachment-item {
    width: calc(33.33% - 10px);
    background: #f8f9fa;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    transition: all 0.3s ease;
  }
.attachment-item:active {
  transform: scale(0.98);
}
  .attachment-item:active {
    transform: scale(0.98);
  }
.attachment-preview-container {
  width: 100%;
  height: 120px;
  background: #e9ecef;
  display: flex;
  align-items: center;
  justify-content: center;
}
  .attachment-preview-container {
    width: 100%;
    height: 120px;
    background: #e9ecef;
    display: flex;
    align-items: center;
    justify-content: center;
  }
.attachment-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
  .attachment-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
.attachment-video-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
  .attachment-video-preview {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
  }
.video-text {
  font-size: 12px;
  color: #666;
}
  .video-text {
    font-size: 12px;
    color: #666;
  }
.attachment-info {
  padding: 10px;
}
  .attachment-info {
    padding: 10px;
  }
.attachment-name {
  font-size: 12px;
  color: #333;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 4px;
}
  .attachment-name {
    font-size: 12px;
    color: #333;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 4px;
  }
.attachment-size {
  font-size: 10px;
  color: #999;
}
  .attachment-size {
    font-size: 10px;
    color: #999;
  }
/* ç©ºçŠ¶æ€æ ·å¼ */
.attachment-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 20px;
  color: #999;
}
  /* ç©ºçŠ¶æ€æ ·å¼ */
  .attachment-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 80px 20px;
    color: #999;
  }
.empty-text {
  margin-top: 15px;
  font-size: 14px;
}
  .empty-text {
    margin-top: 15px;
    font-size: 14px;
  }
/* è§†é¢‘弹窗样式 */
.video-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.9);
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
  /* è§†é¢‘弹窗样式 */
  .video-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.9);
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
  }
.video-modal-container {
  width: 100%;
  max-width: 800px;
  background: #000;
  border-radius: 12px;
  overflow: hidden;
}
  .video-modal-container {
    width: 100%;
    max-width: 800px;
    background: #000;
    border-radius: 12px;
    overflow: hidden;
  }
.video-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  background: #1a1a1a;
}
  .video-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    background: #1a1a1a;
  }
.video-modal-title {
  font-size: 16px;
  color: #fff;
  font-weight: 500;
}
  .video-modal-title {
    font-size: 16px;
    color: #fff;
    font-weight: 500;
  }
.close-btn-video {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 50%;
}
  .close-btn-video {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 50%;
  }
.video-modal-body {
  padding: 20px;
}
  .video-modal-body {
    padding: 20px;
  }
.video-player {
  width: 100%;
  height: 400px;
  border-radius: 8px;
}
  .video-player {
    width: 100%;
    height: 400px;
    border-radius: 8px;
  }
</style>
src/pages/inspectionUpload/components/formDia.vue
@@ -1,460 +1,398 @@
<template>
  <u-popup
    v-model="dialogVisitable"
    mode="center"
    :round="10"
    :closeable="true"
    @close="cancel"
  >
  <u-popup v-model="dialogVisitable"
           mode="center"
           :round="10"
           :closeable="true"
           @close="cancel">
    <view class="popup-content">
      <view class="popup-header">
        <text class="popup-title">巡检记录上传</text>
      </view>
      <view class="upload-container">
        <!-- å¼‚常状态选择 -->
        <view class="form-container">
          <view class="title">巡检状态</view>
          <view class="exception-section">
            <view class="exception-options">
              <view
                class="exception-option"
                :class="{ active: hasException === false }"
                @click="setExceptionStatus(false)"
              >
                <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
              <view class="exception-option"
                    :class="{ active: hasException === false }"
                    @click="setExceptionStatus(false)">
                <u-icon name="checkmark-circle"
                        size="20"
                        color="#52c41a"></u-icon>
                <text class="option-text">正常</text>
              </view>
              <view
                class="exception-option"
                :class="{ active: hasException === true }"
                @click="setExceptionStatus(true)"
              >
                <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon>
              <view class="exception-option"
                    :class="{ active: hasException === true }"
                    @click="setExceptionStatus(true)">
                <u-icon name="close-circle"
                        size="20"
                        color="#ff4d4f"></u-icon>
                <text class="option-text">存在异常</text>
              </view>
            </view>
          </view>
        </view>
        <!-- å¼‚常描述(仅在异常时显示) -->
        <view class="form-container" v-if="hasException === true">
        <view class="form-container"
              v-if="hasException === true">
          <view class="title">异常描述</view>
          <u-input
            v-model="exceptionDescription"
            type="textarea"
            :maxlength="500"
            placeholder="请描述异常情况..."
            :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }"
          />
          <u-input v-model="exceptionDescription"
                   type="textarea"
                   :maxlength="500"
                   placeholder="请描述异常情况..."
                   :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }" />
        </view>
        <!-- ä¸Šä¼ åŒºåŸŸï¼ˆä»…在异常时显示) -->
        <template v-if="hasException === true">
          <view class="form-container">
            <view class="title">生产前</view>
            <u-upload
              :fileList="beforeModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="before"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
          <view class="form-container">
            <view class="title">生产后</view>
            <u-upload
              :fileList="afterModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="after"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
          </view>
          <view class="form-container">
            <view class="title">生产问题</view>
            <u-upload
              :fileList="issueModelValue"
              @afterRead="afterRead"
              @delete="deleteFile"
              name="issue"
              multiple
              :maxCount="10"
              :maxSize="5 * 1024 * 1024"
              accept="image/*"
              :previewFullImage="true"
            ></u-upload>
            <view class="title">巡检照片</view>
            <u-upload :fileList="beforeModelValue"
                      @afterRead="afterRead"
                      @delete="deleteFile"
                      name="before"
                      multiple
                      :maxCount="10"
                      :maxSize="5 * 1024 * 1024"
                      accept="image/*"
                      :previewFullImage="true"></u-upload>
          </view>
        </template>
        <!-- æ­£å¸¸çŠ¶æ€æç¤º -->
        <view class="form-container normal-tip" v-if="hasException === false">
          <u-icon name="info-circle" size="40" color="#52c41a"></u-icon>
        <view class="form-container normal-tip"
              v-if="hasException === false">
          <u-icon name="info-circle"
                  size="40"
                  color="#52c41a"></u-icon>
          <text class="tip-text">设备运行正常,无需上传照片</text>
        </view>
      </view>
      <view class="popup-footer">
        <u-button @click="cancel" :customStyle="{ marginRight: '10px' }">取消</u-button>
        <u-button type="primary" @click="submitForm">保存</u-button>
        <u-button @click="cancel"
                  :customStyle="{ marginRight: '10px' }">取消</u-button>
        <u-button type="primary"
                  @click="submitForm">保存</u-button>
      </view>
    </view>
  </u-popup>
</template>
<script setup>
import { ref, computed } from 'vue'
import { submitInspectionRecord } from '@/api/equipmentManagement/inspection.js'
import { getToken } from '@/utils/auth'
import config from '@/config'
  import { ref, computed } from "vue";
  import { submitInspectionRecord } from "@/api/equipmentManagement/inspection.js";
  import { getToken } from "@/utils/auth";
  import config from "@/config";
const emit = defineEmits(['closeDia'])
  const emit = defineEmits(["closeDia"]);
const dialogVisitable = ref(false)
const beforeModelValue = ref([])
const afterModelValue = ref([])
const issueModelValue = ref([])
const infoData = ref(null)
  const dialogVisitable = ref(false);
  const beforeModelValue = ref([]);
  const infoData = ref(null);
// å¼‚常状态:null=未选择, false=正常, true=异常
const hasException = ref(null)
// å¼‚常描述
const exceptionDescription = ref('')
  // å¼‚常状态:null=未选择, false=正常, true=异常
  const hasException = ref(null);
  // å¼‚常描述
  const exceptionDescription = ref("");
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
  let baseUrl = '';
  if (process.env.VUE_APP_BASE_API) {
    baseUrl = process.env.VUE_APP_BASE_API;
  } else {
    baseUrl = config.baseUrl;
  }
  return baseUrl + '/file/upload';
})
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    let baseUrl = "";
const uploadSingleFile = async (fileItem, typeValue) => {
  const token = getToken()
  if (!token) throw new Error('用户未登录')
  // H5: u-upload å¯èƒ½ç»™åŽŸç”Ÿ File(fileItem.file)
  const rawFile = fileItem?.file
  if (rawFile) {
    const formData = new FormData()
    formData.append('file', rawFile, rawFile.name || 'image.jpg')
    formData.append('type', String(typeValue))
    const res = await fetch(uploadFileUrl.value, {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + token },
      body: formData
    })
    const data = await res.json()
    if (data?.code !== 200) throw new Error(data?.msg || '上传失败')
    return {
      url: data?.data?.url,
      name: rawFile.name || 'image.jpg',
      status: 'success'
    if (process.env.VUE_APP_BASE_API) {
      baseUrl = process.env.VUE_APP_BASE_API;
    } else {
      baseUrl = config.baseUrl;
    }
  }
  // éž H5 / å…¼å®¹ï¼šèµ° uni.uploadFile
  return await new Promise((resolve, reject) => {
    uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: fileItem.url,
      name: 'file',
      header: {
        'Authorization': `Bearer ${token}`
      },
      formData: {
        type: typeValue
      },
      success: (res) => {
        try {
          const data = JSON.parse(res.data)
          if (data.code === 200) {
            resolve({
              url: data.data.url,
              name: fileItem.name,
              status: 'success'
            })
          } else {
            reject(new Error(data.msg || '上传失败'))
    return baseUrl + "/file/upload";
  });
  const uploadSingleFile = async (fileItem, typeValue) => {
    const token = getToken();
    if (!token) throw new Error("用户未登录");
    // H5: u-upload å¯èƒ½ç»™åŽŸç”Ÿ File(fileItem.file)
    const rawFile = fileItem?.file;
    if (rawFile) {
      const formData = new FormData();
      formData.append("file", rawFile, rawFile.name || "image.jpg");
      formData.append("type", String(typeValue));
      const res = await fetch(uploadFileUrl.value, {
        method: "POST",
        headers: { Authorization: "Bearer " + token },
        body: formData,
      });
      const data = await res.json();
      if (data?.code !== 200) throw new Error(data?.msg || "上传失败");
      return {
        url: data?.data?.url,
        name: rawFile.name || "image.jpg",
        status: "success",
      };
    }
    // éž H5 / å…¼å®¹ï¼šèµ° uni.uploadFile
    return await new Promise((resolve, reject) => {
      uni.uploadFile({
        url: uploadFileUrl.value,
        filePath: fileItem.url,
        name: "file",
        header: {
          Authorization: `Bearer ${token}`,
        },
        formData: {
          type: typeValue,
        },
        success: res => {
          try {
            const data = JSON.parse(res.data);
            if (data.code === 200) {
              resolve({
                url: data.data.url,
                name: fileItem.name,
                status: "success",
              });
            } else {
              reject(new Error(data.msg || "上传失败"));
            }
          } catch (e) {
            reject(e);
          }
        } catch (e) {
          reject(e)
        },
        fail: err => reject(err),
      });
    });
  };
  // æ–‡ä»¶ä¸Šä¼ å¤„理
  const afterRead = event => {
    const { file } = event;
    // ä»…保留生产前(typeValue=10)
    let typeValue = 10;
    const files = Array.isArray(file) ? file : [file];
    Promise.resolve()
      .then(async () => {
        for (const f of files) {
          const uploaded = await uploadSingleFile(f, typeValue);
          beforeModelValue.value.push(uploaded);
        }
      },
      fail: (err) => reject(err)
    })
  })
}
// æ–‡ä»¶ä¸Šä¼ å¤„理
const afterRead = (event) => {
  const { name, file } = event
  // æ ¹æ®ä¸Šä¼ ç±»åž‹è®¾ç½®ä¸åŒçš„type值
  let typeValue = 10 // é»˜è®¤å€¼
  if (name === 'before') {
    typeValue = 10 // ç”Ÿäº§å‰
  } else if (name === 'after') {
    typeValue = 11 // ç”Ÿäº§ä¸­
  } else if (name === 'issue') {
    typeValue = 12 // ç”Ÿäº§åŽ
  }
  const files = Array.isArray(file) ? file : [file]
  Promise.resolve().then(async () => {
    for (const f of files) {
      const uploaded = await uploadSingleFile(f, typeValue)
      if (name === 'before') {
        beforeModelValue.value.push(uploaded)
      } else if (name === 'after') {
        afterModelValue.value.push(uploaded)
      } else if (name === 'issue') {
        issueModelValue.value.push(uploaded)
      }
    }
    uni.showToast({ title: '上传成功', icon: 'success' })
  }).catch((err) => {
    console.error('上传失败:', err)
    uni.showToast({ title: '上传失败', icon: 'error' })
  })
}
// åˆ é™¤æ–‡ä»¶
const deleteFile = (event) => {
  const { name, index } = event
  if (name === 'before') {
    beforeModelValue.value.splice(index, 1)
  } else if (name === 'after') {
    afterModelValue.value.splice(index, 1)
  } else if (name === 'issue') {
    issueModelValue.value.splice(index, 1)
  }
}
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å·¡æ£€çŠ¶æ€
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择巡检状态',
        icon: 'none'
        uni.showToast({ title: "上传成功", icon: "success" });
      })
      return
    }
      .catch(err => {
        console.error("上传失败:", err);
        uni.showToast({ title: "上传失败", icon: "error" });
      });
  };
    // å¦‚果是异常状态,检查是否有上传文件
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length
      if (totalFiles === 0) {
  // åˆ é™¤æ–‡ä»¶
  const deleteFile = event => {
    const { index } = event;
    beforeModelValue.value.splice(index, 1);
  };
  // è®¾ç½®å¼‚常状态
  const setExceptionStatus = status => {
    hasException.value = status;
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å·¡æ£€çŠ¶æ€
      if (hasException.value === null) {
        uni.showToast({
          title: '请上传异常照片',
          icon: 'none'
        })
        return
          title: "请选择巡检状态",
          icon: "none",
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
      if (!exceptionDescription.value.trim()) {
        uni.showToast({
          title: '请填写异常描述',
          icon: 'none'
        })
        return
      // å¦‚果是异常状态,检查是否有上传文件
      if (hasException.value === true) {
        if (beforeModelValue.value.length === 0) {
          uni.showToast({
            title: "请上传异常照片",
            icon: "none",
          });
          return;
        }
        // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
        if (!exceptionDescription.value.trim()) {
          uni.showToast({
            title: "请填写异常描述",
            icon: "none",
          });
          return;
        }
      }
    }
    let arr = []
    if (beforeModelValue.value.length > 0) {
      arr.push(...beforeModelValue.value.map(item => ({ ...item, statusType: 0 })))
    }
    if (afterModelValue.value.length > 0) {
      arr.push(...afterModelValue.value.map(item => ({ ...item, statusType: 1 })))
    }
    if (issueModelValue.value.length > 0) {
      arr.push(...issueModelValue.value.map(item => ({ ...item, statusType: 2 })))
    }
    // æäº¤æ•°æ®
    infoData.value.storageBlobDTO = arr
    infoData.value.hasException = hasException.value
    infoData.value.exceptionDescription = exceptionDescription.value
    await submitInspectionRecord({ ...infoData.value })
    uni.showToast({
      title: '提交成功',
      icon: 'success'
    })
    cancel()
  } catch (error) {
    console.error('提交失败:', error)
    uni.showToast({
      title: '提交失败',
      icon: 'error'
    })
  }
}
      let arr = [];
      if (beforeModelValue.value.length > 0) {
        arr.push(
          ...beforeModelValue.value.map(item => ({ ...item, statusType: 0 }))
        );
      }
// æ‰“开弹框
const openDialog = async (row) => {
  infoData.value = row
  dialogVisitable.value = true
  // æ¸…空之前的数据
  beforeModelValue.value = []
  afterModelValue.value = []
  issueModelValue.value = []
  hasException.value = null
  exceptionDescription.value = ''
}
      // æäº¤æ•°æ®
      infoData.value.storageBlobDTO = arr;
      infoData.value.hasException = hasException.value;
      infoData.value.exceptionDescription = exceptionDescription.value;
      await submitInspectionRecord({ ...infoData.value });
// å…³é—­å¼¹æ¡†
const cancel = () => {
  dialogVisitable.value = false
  emit('closeDia')
}
      uni.showToast({
        title: "提交成功",
        icon: "success",
      });
defineExpose({ openDialog })
      cancel();
    } catch (error) {
      console.error("提交失败:", error);
      uni.showToast({
        title: "提交失败",
        icon: "error",
      });
    }
  };
  // æ‰“开弹框
  const openDialog = async row => {
    infoData.value = row;
    dialogVisitable.value = true;
    // æ¸…空之前的数据
    beforeModelValue.value = [];
    hasException.value = null;
    exceptionDescription.value = "";
  };
  // å…³é—­å¼¹æ¡†
  const cancel = () => {
    dialogVisitable.value = false;
    emit("closeDia");
  };
  defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.popup-content {
  width: 90vw;
  max-width: 400px;
  background-color: #fff;
  border-radius: 10px;
  overflow: hidden;
}
.popup-header {
  padding: 20px 20px 10px;
  text-align: center;
  border-bottom: 1px solid #f0f0f0;
}
.popup-title {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.upload-container {
  padding: 20px;
  max-height: 60vh;
  overflow-y: auto;
}
.form-container {
  margin-bottom: 20px;
  &:last-child {
    margin-bottom: 0;
  .popup-content {
    width: 90vw;
    max-width: 400px;
    background-color: #fff;
    border-radius: 10px;
    overflow: hidden;
  }
}
.title {
  font-size: 14px;
  color: #1890ff;
  line-height: 20px;
  font-weight: 600;
  padding-left: 10px;
  position: relative;
  margin: 6px 0 10px;
  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 3px;
    width: 4px;
    height: 14px;
    background-color: #1890ff;
  .popup-header {
    padding: 20px 20px 10px;
    text-align: center;
    border-bottom: 1px solid #f0f0f0;
  }
}
.popup-footer {
  display: flex;
  justify-content: center;
  padding: 15px 20px;
  border-top: 1px solid #f0f0f0;
  background-color: #fafafa;
}
// å¼‚常状态选择样式
.exception-section {
  padding: 10px 0;
}
.exception-options {
  display: flex;
  gap: 15px;
}
.exception-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 15px 20px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
  background-color: #fff;
  &.active {
    border-color: #1890ff;
    background-color: #e6f7ff;
  .popup-title {
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
  &:active {
    opacity: 0.8;
  .upload-container {
    padding: 20px;
    max-height: 60vh;
    overflow-y: auto;
  }
}
.option-text {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
  .form-container {
    margin-bottom: 20px;
// æ­£å¸¸çŠ¶æ€æç¤ºæ ·å¼
.normal-tip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  background-color: #f6ffed;
  border: 1px dashed #b7eb8f;
  border-radius: 8px;
  .tip-text {
    margin-top: 15px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .title {
    font-size: 14px;
    color: #52c41a;
    color: #1890ff;
    line-height: 20px;
    font-weight: 600;
    padding-left: 10px;
    position: relative;
    margin: 6px 0 10px;
    &::before {
      content: "";
      position: absolute;
      left: 0;
      top: 3px;
      width: 4px;
      height: 14px;
      background-color: #1890ff;
    }
  }
}
  .popup-footer {
    display: flex;
    justify-content: center;
    padding: 15px 20px;
    border-top: 1px solid #f0f0f0;
    background-color: #fafafa;
  }
  // å¼‚常状态选择样式
  .exception-section {
    padding: 10px 0;
  }
  .exception-options {
    display: flex;
    gap: 15px;
  }
  .exception-option {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 15px 20px;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s;
    background-color: #fff;
    &.active {
      border-color: #1890ff;
      background-color: #e6f7ff;
    }
    &:active {
      opacity: 0.8;
    }
  }
  .option-text {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  // æ­£å¸¸çŠ¶æ€æç¤ºæ ·å¼
  .normal-tip {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 40px 20px;
    background-color: #f6ffed;
    border: 1px dashed #b7eb8f;
    border-radius: 8px;
    .tip-text {
      margin-top: 15px;
      font-size: 14px;
      color: #52c41a;
    }
  }
</style>
src/pages/inspectionUpload/index.vue
@@ -296,18 +296,22 @@
  };
  const getFileStatus = record => {
    let _beforeProduction =
      record.beforeProduction && record.beforeProduction.length;
    let _afterProduction =
      record.afterProduction && record.afterProduction.length;
    let _productionIssues =
      record.productionIssues && record.productionIssues.length;
    if (_beforeProduction && _afterProduction && _productionIssues) {
      return 2;
    } else if (_beforeProduction || _afterProduction || _productionIssues) {
      return 1;
    // æ£€æŸ¥æ˜¯å¦æœ‰å·¡æ£€ç…§ç‰‡ (commonFileListBeforeVO)
    const hasFiles =
      (record.commonFileListBeforeVO &&
        record.commonFileListBeforeVO.length > 0) ||
      (record.commonFileListAfterVO && record.commonFileListAfterVO.length > 0) ||
      (record.commonFileListVO && record.commonFileListVO.length > 0);
    if (hasFiles) {
      return 2; // å·²å®Œæˆ
    } else if (
      record.inspectionResult !== undefined &&
      record.inspectionResult !== null
    ) {
      return 1; // å·¡æ£€ä¸­ (已有结果但没照片,或者根据业务逻辑定义)
    } else {
      return 0;
      return 0; // æœªå·¡æ£€
    }
  };
@@ -400,7 +404,12 @@
  // æŸ¥çœ‹é™„ä»¶ - è·³è½¬åˆ°é™„件页面
  const viewAttachments = async task => {
    const taskData = encodeURIComponent(JSON.stringify(task));
    // ä»…传递必要的任务信息和 commonFileListBeforeVO é™„件列表
    const taskInfoToPass = {
      taskName: task.taskName,
      commonFileListBeforeVO: task.commonFileListBeforeVO || [],
    };
    const taskData = encodeURIComponent(JSON.stringify(taskInfoToPass));
    uni.navigateTo({
      url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`,
    });
src/pages/inspectionUpload/upload.vue
@@ -57,33 +57,17 @@
                  maxlength="500"
                  placeholder="请描述异常情况..." />
      </view>
      <!-- åˆ†ç±»æ ‡ç­¾é¡µï¼ˆä»…在异常时显示) -->
      <!-- ä¸Šä¼ åŒºåŸŸï¼ˆä»…在异常时显示) -->
      <view class="section-card"
            v-if="hasException === true">
        <view class="upload-tabs">
          <view class="tab-item"
                :class="{ active: currentUploadType === 'before' }"
                @click="switchUploadType('before')">
            ç”Ÿäº§å‰
          </view>
          <view class="tab-item"
                :class="{ active: currentUploadType === 'after' }"
                @click="switchUploadType('after')">
            ç”Ÿäº§ä¸­
          </view>
          <view class="tab-item"
                :class="{ active: currentUploadType === 'issue' }"
                @click="switchUploadType('issue')">
            ç”Ÿäº§åŽ
          </view>
        </view>
        <view class="section-title">巡检照片/视频</view>
        <!-- å½“前分类的上传区域 -->
        <view class="upload-area">
          <view class="upload-buttons">
            <u-button type="primary"
                      @click="chooseMedia('image')"
                      :loading="uploading"
                      :disabled="getCurrentFiles().length >= uploadConfig.limit"
                      :disabled="beforeModelValue.length >= uploadConfig.limit"
                      :customStyle="{ marginRight: '10px', flex: 1 }">
              <u-icon name="camera"
                      size="18"
@@ -94,7 +78,7 @@
            <u-button type="success"
                      @click="chooseMedia('video')"
                      :loading="uploading"
                      :disabled="getCurrentFiles().length >= uploadConfig.limit"
                      :disabled="beforeModelValue.length >= uploadConfig.limit"
                      :customStyle="{ flex: 1 }">
              <uni-icons type="videocam"
                         size="18"
@@ -111,9 +95,9 @@
                             activeColor="#409eff"></u-line-progress>
          </view>
          <!-- å½“前分类的文件列表 -->
          <view v-if="getCurrentFiles().length > 0"
          <view v-if="beforeModelValue.length > 0"
                class="file-list">
            <view v-for="(file, index) in getCurrentFiles()"
            <view v-for="(file, index) in beforeModelValue"
                  :key="index"
                  class="file-item">
              <view class="file-preview-container">
@@ -143,17 +127,15 @@
              </view>
            </view>
          </view>
          <view v-if="getCurrentFiles().length === 0"
          <view v-if="beforeModelValue.length === 0"
                class="empty-state">
            <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
            <text>请选择要上传的图片或视频</text>
          </view>
        </view>
        <!-- ç»Ÿè®¡ä¿¡æ¯ -->
        <view class="upload-summary">
          <text class="summary-text">
            ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
            ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
            ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
            å·²ä¸Šä¼ : {{ beforeModelValue.length }}个文件
          </text>
        </view>
      </view>
@@ -198,12 +180,7 @@
  const uploadProgress = ref(0);
  // ä¸‰ä¸ªåˆ†ç±»çš„上传状态
  const beforeModelValue = ref([]); // ç”Ÿäº§å‰
  const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
  const issueModelValue = ref([]); // ç”Ÿäº§åŽ
  // å½“前激活的上传类型
  const currentUploadType = ref("before"); // 'before', 'after', 'issue'
  const beforeModelValue = ref([]); // å·¡æ£€ç…§ç‰‡
  // å¼‚常状态
  const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
@@ -258,24 +235,25 @@
          });
        };
        // æ ¹æ®ç”¨æˆ·è¦æ±‚映射:AfterDTO(生产前), DTO(生产中), BeforeDTO(生产后)
        // æ ¹æ®ç”¨æˆ·è¦æ±‚映射:仅保留生产前
        if (
          info.commonFileListAfterVO &&
          Array.isArray(info.commonFileListAfterVO)
        ) {
          beforeModelValue.value = mapFiles(info.commonFileListAfterVO);
        }
        console.log(beforeModelValue.value, "beforeModelValue");
        if (info.commonFileListVO && Array.isArray(info.commonFileListVO)) {
          afterModelValue.value = mapFiles(info.commonFileListVO);
        }
        if (
        } else if (
          info.commonFileListVO &&
          Array.isArray(info.commonFileListVO)
        ) {
          beforeModelValue.value = mapFiles(info.commonFileListVO);
        } else if (
          info.commonFileListBeforeVO &&
          Array.isArray(info.commonFileListBeforeVO)
        ) {
          issueModelValue.value = mapFiles(info.commonFileListBeforeVO);
          beforeModelValue.value = mapFiles(info.commonFileListBeforeVO);
        }
        console.log(beforeModelValue.value, "beforeModelValue");
        // å¦‚果有异常描述,也恢复
        if (info.abnormalDescription) {
@@ -293,12 +271,7 @@
        }
        // è‡ªåŠ¨å…œåº•ï¼šå¦‚æžœå­˜åœ¨å·²ä¸Šä¼ æ–‡ä»¶ï¼Œåˆ™å¿…ç„¶æ˜¯å¼‚å¸¸çŠ¶æ€ï¼Œç¡®ä¿ UI æ­£å¸¸æ˜¾ç¤º
        if (
          !hasException.value &&
          (beforeModelValue.value.length > 0 ||
            afterModelValue.value.length > 0 ||
            issueModelValue.value.length > 0)
        ) {
        if (!hasException.value && beforeModelValue.value.length > 0) {
          hasException.value = true;
        }
      } catch (e) {
@@ -310,39 +283,6 @@
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // åˆ‡æ¢ä¸Šä¼ ç±»åž‹
  const switchUploadType = type => {
    currentUploadType.value = type;
  };
  // èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
  const getCurrentFiles = () => {
    switch (currentUploadType.value) {
      case "before":
        return beforeModelValue.value || [];
      case "after":
        return afterModelValue.value || [];
      case "issue":
        return issueModelValue.value || [];
      default:
        return [];
    }
  };
  // èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
  const getUploadTypeText = () => {
    switch (currentUploadType.value) {
      case "before":
        return "生产前";
      case "after":
        return "生产中";
      case "issue":
        return "生产后";
      default:
        return "";
    }
  };
  // è®¾ç½®å¼‚常状态
@@ -361,12 +301,8 @@
        hasException: hasException.value,
        inspectionResult: hasException.value ? 0 : 1, // 0-异常,1-正常
        commonFileListAfterDTO: beforeModelValue.value,
        commonFileListDTO: afterModelValue.value,
        commonFileListBeforeDTO: issueModelValue.value,
        uploadedFiles: {
          before: beforeModelValue.value,
          after: afterModelValue.value,
          issue: issueModelValue.value,
        },
      };
@@ -398,11 +334,7 @@
      // å¦‚果是异常状态,检查是否有上传文件和描述
      if (hasException.value === true) {
        const totalFiles =
          beforeModelValue.value.length +
          afterModelValue.value.length +
          issueModelValue.value.length;
        if (totalFiles === 0) {
        if (beforeModelValue.value.length === 0) {
          uni.showToast({
            title: "请上传异常照片",
            icon: "none",
@@ -426,11 +358,7 @@
      });
      // æŒ‰ç…§é€»è¾‘合并所有分类的文件用于提取ID
      const allFiles = [
        ...beforeModelValue.value,
        ...afterModelValue.value,
        ...issueModelValue.value,
      ];
      const allFiles = [...beforeModelValue.value];
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表
      let tempFileIds = [];
@@ -444,8 +372,6 @@
      const submitData = {
        ...taskInfo.value,
        commonFileListAfterDTO: beforeModelValue.value, // ç”Ÿäº§å‰
        commonFileListDTO: afterModelValue.value, // ç”Ÿäº§ä¸­
        commonFileListBeforeDTO: issueModelValue.value, // ç”Ÿäº§åŽ
        hasException: hasException.value,
        inspectionResult: hasException.value ? 0 : 1, // 0-异常,1-正常
        abnormalDescription: abnormalDescription.value,
@@ -498,7 +424,7 @@
  // æ‹ç…§/拍视频
  const chooseMedia = type => {
    if (getCurrentFiles().length >= uploadConfig.limit) {
    if (beforeModelValue.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
@@ -506,7 +432,7 @@
      return;
    }
    const remaining = uploadConfig.limit - getCurrentFiles().length;
    const remaining = uploadConfig.limit - beforeModelValue.value.length;
    // ä¼˜å…ˆä½¿ç”¨ chooseMedia
    if (typeof uni.chooseMedia === "function") {
@@ -612,7 +538,7 @@
        Authorization: `Bearer ${token}`,
      },
      formData: {
        type: getTabType(),
        type: 10,
      },
      success: res => {
        try {
@@ -638,14 +564,8 @@
              status: "success",
            };
            // æ ¹æ®å½“前类型添加到对应数组
            if (currentUploadType.value === "before") {
              beforeModelValue.value.push(uploadedFile);
            } else if (currentUploadType.value === "after") {
              afterModelValue.value.push(uploadedFile);
            } else if (currentUploadType.value === "issue") {
              issueModelValue.value.push(uploadedFile);
            }
            // ä»…添加到 beforeModelValue
            beforeModelValue.value.push(uploadedFile);
            uni.showToast({ title: "上传成功", icon: "success" });
          } else {
@@ -670,24 +590,9 @@
    });
  };
  // èŽ·å–type值
  const getTabType = () => {
    switch (currentUploadType.value) {
      case "before":
        return 10;
      case "after":
        return 11;
      case "issue":
        return 12;
      default:
        return 10;
    }
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    const files = getCurrentFiles();
    files.splice(index, 1);
    beforeModelValue.value.splice(index, 1);
  };
</script>
src/pages/qualityManagement/finalInspection/add.vue
@@ -68,14 +68,32 @@
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
      <up-form-item label="总数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  placeholder="请输入总数量"
                  :disabled="processQuantityDisabled" />
      </up-form-item>
      <up-form-item label="合格数量"
                    prop="qualifiedQuantity"
                    required
                    border-bottom>
        <up-input v-model="form.qualifiedQuantity"
                  type="number"
                  placeholder="请输入合格数量"
                  clearable />
      </up-form-item>
      <up-form-item label="不合格数量"
                    prop="unqualifiedQuantity"
                    required
                    border-bottom>
        <up-input v-model="form.unqualifiedQuantity"
                  type="number"
                  placeholder="请输入不合格数量"
                  clearable />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
@@ -83,19 +101,6 @@
        <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"
@@ -208,12 +213,6 @@
                     @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"
@@ -340,8 +339,6 @@
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
@@ -359,8 +356,9 @@
    testStandardId: "",
    unit: "",
    quantity: "",
    qualifiedQuantity: "",
    unqualifiedQuantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
@@ -380,11 +378,6 @@
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
@@ -409,13 +402,6 @@
    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,
    }));
  });
@@ -447,10 +433,9 @@
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
@@ -555,12 +540,6 @@
      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;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
@@ -685,7 +664,15 @@
      //   return;
      // }
      if (!form.value.quantity) {
        showToast("请输入数量");
        showToast("请输入总数量");
        return;
      }
      if (!form.value.qualifiedQuantity && form.value.qualifiedQuantity !== 0) {
        showToast("请输入合格数量");
        return;
      }
      if (!form.value.unqualifiedQuantity && form.value.unqualifiedQuantity !== 0) {
        showToast("请输入不合格数量");
        return;
      }
      if (!form.value.productId) {
@@ -696,11 +683,6 @@
        showToast("请选择指标");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 2;
@@ -712,6 +694,8 @@
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      data.qualifiedQuantity = Number(data.qualifiedQuantity);
      data.unqualifiedQuantity = Number(data.unqualifiedQuantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
@@ -751,7 +735,8 @@
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        qualifiedQuantity: "",
        unqualifiedQuantity: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
@@ -773,7 +758,8 @@
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        qualifiedQuantity: 0,
        unqualifiedQuantity: 0,
        productMainId: null,
        purchaseLedgerId: null,
      };
@@ -852,8 +838,9 @@
        testStandardId: "",
        unit: "",
        quantity: "",
        qualifiedQuantity: "",
        unqualifiedQuantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
src/pages/qualityManagement/finalInspection/detail.vue
@@ -15,11 +15,11 @@
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag v-if="detailData.checkResult"
                   :type="getTagType(detailData.checkResult)"
            <u-tag v-if="detailData.passRate != null && detailData.passRate !== ''"
                   type="primary"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
              åˆæ ¼çއ {{ formatPassRate(detailData.passRate) }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
@@ -59,8 +59,20 @@
            <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>
            <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.qualifiedQuantity ?? 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">不合格数量</text>
            <text class="detail-value">{{ detailData.unqualifiedQuantity ?? 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合格率</text>
            <text class="detail-value">{{ formatPassRate(detailData.passRate) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检测单位</text>
@@ -162,15 +174,12 @@
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  const formatPassRate = rate => {
    if (rate === null || rate === undefined || rate === "") return "-";
    const num = Number(rate);
    if (isNaN(num)) return rate;
    if (num <= 1) return `${(num * 100).toFixed(2)}%`;
    return `${num}%`;
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
src/pages/qualityManagement/finalInspection/index.vue
@@ -76,11 +76,11 @@
              </view>
            </view>
            <view class="status-tags">
              <u-tag v-if="item.checkResult"
                     :type="getTagType(item.checkResult)"
              <u-tag v-if="item.passRate != null && item.passRate !== ''"
                     type="primary"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
                åˆæ ¼çއ {{ formatPassRate(item.passRate) }}
              </u-tag>
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
@@ -316,11 +316,13 @@
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  // æ ¼å¼åŒ–合格率
  const formatPassRate = rate => {
    if (rate === null || rate === undefined || rate === "") return "-";
    const num = Number(rate);
    if (isNaN(num)) return rate;
    if (num <= 1) return `${(num * 100).toFixed(2)}%`;
    return `${num}%`;
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
@@ -383,9 +385,11 @@
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
        qualifiedCount.value = inspectionList.value.filter(item => {
          const rate = Number(item.passRate);
          if (isNaN(rate)) return false;
          return rate <= 1 ? rate >= 1 : rate >= 100;
        }).length;
      })
      .catch(err => {
        tableLoading.value = false;
src/pages/qualityManagement/materialInspection/add.vue
@@ -68,14 +68,32 @@
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
      <up-form-item label="总数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  placeholder="请输入总数量"
                  :disabled="supplierQuantityDisabled" />
      </up-form-item>
      <up-form-item label="合格数量"
                    prop="qualifiedQuantity"
                    required
                    border-bottom>
        <up-input v-model="form.qualifiedQuantity"
                  type="number"
                  placeholder="请输入合格数量"
                  clearable />
      </up-form-item>
      <up-form-item label="不合格数量"
                    prop="unqualifiedQuantity"
                    required
                    border-bottom>
        <up-input v-model="form.unqualifiedQuantity"
                  type="number"
                  placeholder="请输入不合格数量"
                  clearable />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
@@ -83,19 +101,6 @@
        <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"
@@ -220,12 +225,6 @@
                     @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"
@@ -352,8 +351,6 @@
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
@@ -371,8 +368,9 @@
    testStandardId: "",
    unit: "",
    quantity: "",
    qualifiedQuantity: "",
    unqualifiedQuantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
@@ -392,11 +390,6 @@
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
@@ -421,13 +414,6 @@
    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,
    }));
  });
@@ -459,10 +445,9 @@
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
@@ -568,12 +553,6 @@
      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;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
@@ -698,7 +677,15 @@
        return;
      }
      if (!form.value.quantity) {
        showToast("请输入数量");
        showToast("请输入总数量");
        return;
      }
      if (!form.value.qualifiedQuantity && form.value.qualifiedQuantity !== 0) {
        showToast("请输入合格数量");
        return;
      }
      if (!form.value.unqualifiedQuantity && form.value.unqualifiedQuantity !== 0) {
        showToast("请输入不合格数量");
        return;
      }
      if (!form.value.productId) {
@@ -709,11 +696,6 @@
        showToast("请选择指标");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 0;
@@ -725,6 +707,8 @@
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      data.qualifiedQuantity = Number(data.qualifiedQuantity);
      data.unqualifiedQuantity = Number(data.unqualifiedQuantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
@@ -764,7 +748,8 @@
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        qualifiedQuantity: "",
        unqualifiedQuantity: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
@@ -786,7 +771,8 @@
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        qualifiedQuantity: 0,
        unqualifiedQuantity: 0,
        productMainId: null,
        purchaseLedgerId: null,
      };
@@ -865,8 +851,9 @@
        testStandardId: "",
        unit: "",
        quantity: "",
        qualifiedQuantity: "",
        unqualifiedQuantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
src/pages/qualityManagement/materialInspection/detail.vue
@@ -15,11 +15,11 @@
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag v-if="detailData.checkResult"
                   :type="getTagType(detailData.checkResult)"
            <u-tag v-if="detailData.passRate != null && detailData.passRate !== ''"
                   type="primary"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult || '-' }}
              åˆæ ¼çއ {{ formatPassRate(detailData.passRate) }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
@@ -59,8 +59,20 @@
            <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>
            <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.qualifiedQuantity ?? 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">不合格数量</text>
            <text class="detail-value">{{ detailData.unqualifiedQuantity ?? 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合格率</text>
            <text class="detail-value">{{ formatPassRate(detailData.passRate) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检测单位</text>
@@ -161,16 +173,13 @@
    return dayjs(date).format("YYYY-MM-DD");
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  // æ ¼å¼åŒ–合格率
  const formatPassRate = rate => {
    if (rate === null || rate === undefined || rate === "") return "-";
    const num = Number(rate);
    if (isNaN(num)) return rate;
    if (num <= 1) return `${(num * 100).toFixed(2)}%`;
    return `${num}%`;
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
src/pages/qualityManagement/materialInspection/index.vue
@@ -76,11 +76,11 @@
              </view>
            </view>
            <view class="status-tags">
              <u-tag v-if="item.checkResult"
                     :type="getTagType(item.checkResult)"
              <u-tag v-if="item.passRate != null && item.passRate !== ''"
                     type="primary"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
                åˆæ ¼çއ {{ formatPassRate(item.passRate) }}
              </u-tag>
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
@@ -316,11 +316,13 @@
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  // æ ¼å¼åŒ–合格率
  const formatPassRate = rate => {
    if (rate === null || rate === undefined || rate === "") return "-";
    const num = Number(rate);
    if (isNaN(num)) return rate;
    if (num <= 1) return `${(num * 100).toFixed(2)}%`;
    return `${num}%`;
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
@@ -383,9 +385,11 @@
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
        qualifiedCount.value = inspectionList.value.filter(item => {
          const rate = Number(item.passRate);
          if (isNaN(rate)) return false;
          return rate <= 1 ? rate >= 1 : rate >= 100;
        }).length;
      })
      .catch(err => {
        tableLoading.value = false;
src/pages/qualityManagement/processInspection/add.vue
@@ -68,14 +68,32 @@
                  placeholder="请输入单位"
                  disabled />
      </up-form-item>
      <up-form-item label="数量"
      <up-form-item label="总数量"
                    prop="quantity"
                    required
                    border-bottom>
        <up-input v-model="form.quantity"
                  type="number"
                  placeholder="请输入数量"
                  placeholder="请输入总数量"
                  :disabled="processQuantityDisabled" />
      </up-form-item>
      <up-form-item label="合格数量"
                    prop="qualifiedQuantity"
                    required
                    border-bottom>
        <up-input v-model="form.qualifiedQuantity"
                  type="number"
                  placeholder="请输入合格数量"
                  clearable />
      </up-form-item>
      <up-form-item label="不合格数量"
                    prop="unqualifiedQuantity"
                    required
                    border-bottom>
        <up-input v-model="form.unqualifiedQuantity"
                  type="number"
                  placeholder="请输入不合格数量"
                  clearable />
      </up-form-item>
      <up-form-item label="检测单位"
                    prop="checkCompany"
@@ -83,19 +101,6 @@
        <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"
@@ -212,12 +217,6 @@
                     @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"
@@ -344,8 +343,6 @@
  const showProductTree = ref(false);
  // è§„格型号选择
  const showModelSheet = ref(false);
  // æ£€æµ‹ç»“果选择
  const showResultSheet = ref(false);
  // æ£€éªŒå‘˜é€‰æ‹©
  const showInspectorSheet = ref(false);
  // æŒ‡æ ‡é€‰æ‹©
@@ -363,8 +360,9 @@
    testStandardId: "",
    unit: "",
    quantity: "",
    qualifiedQuantity: "",
    unqualifiedQuantity: "",
    checkCompany: "",
    checkResult: "",
    productMainId: null,
    purchaseLedgerId: null,
  });
@@ -384,11 +382,6 @@
  const modelOptions = ref([]);
  // æ£€éªŒå‘˜åˆ—表
  const userList = ref([]);
  // æ£€æµ‹ç»“果选项
  const resultOptions = ref([
    { label: "合格", value: "合格" },
    { label: "不合格", value: "不合格" },
  ]);
  // æŒ‡æ ‡é€‰é¡¹
  const testStandardOptions = ref([]);
  // å½“前产品ID
@@ -413,13 +406,6 @@
    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,
    }));
  });
@@ -451,10 +437,9 @@
    ],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [
      { required: true, message: "请选择检测结果", trigger: "change" },
    ],
  };
  // æ˜¯å¦ä¸ºç¼–辑模式
@@ -559,12 +544,6 @@
      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;
  };
  // é€‰æ‹©æ£€éªŒå‘˜
@@ -689,7 +668,15 @@
        return;
      }
      if (!form.value.quantity) {
        showToast("请输入数量");
        showToast("请输入总数量");
        return;
      }
      if (!form.value.qualifiedQuantity && form.value.qualifiedQuantity !== 0) {
        showToast("请输入合格数量");
        return;
      }
      if (!form.value.unqualifiedQuantity && form.value.unqualifiedQuantity !== 0) {
        showToast("请输入不合格数量");
        return;
      }
      if (!form.value.productId) {
@@ -700,11 +687,6 @@
        showToast("请选择指标");
        return;
      }
      if (!form.value.checkResult) {
        showToast("请选择检测结果");
        return;
      }
      loading.value = true;
      form.value.inspectType = 1;
@@ -716,6 +698,8 @@
      const data = { ...form.value, qualityInspectParams: tableData.value };
      data.quantity = Number(data.quantity);
      data.qualifiedQuantity = Number(data.qualifiedQuantity);
      data.unqualifiedQuantity = Number(data.unqualifiedQuantity);
      if (isEdit.value) {
        const res = await qualityInspectUpdate(data);
        showToast("保存成功");
@@ -755,7 +739,8 @@
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
        qualifiedQuantity: "",
        unqualifiedQuantity: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
@@ -777,7 +762,8 @@
        unit: "kg",
        quantity: 1000,
        checkCompany: "第三方检测机构",
        checkResult: "合格",
        qualifiedQuantity: 0,
        unqualifiedQuantity: 0,
        productMainId: null,
        purchaseLedgerId: null,
      };
@@ -856,8 +842,9 @@
        testStandardId: "",
        unit: "",
        quantity: "",
        qualifiedQuantity: "",
        unqualifiedQuantity: "",
        checkCompany: "",
        checkResult: "",
        productMainId: null,
        purchaseLedgerId: null,
      };
src/pages/qualityManagement/processInspection/detail.vue
@@ -15,11 +15,11 @@
          </view>
          <text class="header-title">{{ detailData.productName || '-' }}</text>
          <view class="status-tags">
            <u-tag v-if="detailData.checkResult"
                   :type="getTagType(detailData.checkResult)"
            <u-tag v-if="detailData.passRate != null && detailData.passRate !== ''"
                   type="primary"
                   size="small"
                   class="status-tag">
              {{ detailData.checkResult }}
              åˆæ ¼çއ {{ formatPassRate(detailData.passRate) }}
            </u-tag>
            <u-tag :type="getStateTagType(detailData.inspectState)"
                   size="small"
@@ -59,8 +59,20 @@
            <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>
            <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.qualifiedQuantity ?? 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">不合格数量</text>
            <text class="detail-value">{{ detailData.unqualifiedQuantity ?? 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">合格率</text>
            <text class="detail-value">{{ formatPassRate(detailData.passRate) }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">检测单位</text>
@@ -162,15 +174,12 @@
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = result => {
    switch (result) {
      case "合格":
        return "success";
      case "不合格":
        return "error";
      default:
        return "info";
    }
  const formatPassRate = rate => {
    if (rate === null || rate === undefined || rate === "") return "-";
    const num = Number(rate);
    if (isNaN(num)) return rate;
    if (num <= 1) return `${(num * 100).toFixed(2)}%`;
    return `${num}%`;
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
src/pages/qualityManagement/processInspection/index.vue
@@ -76,11 +76,11 @@
              </view>
            </view>
            <view class="status-tags">
              <u-tag v-if="item.checkResult"
                     :type="getTagType(item.checkResult)"
              <u-tag v-if="item.passRate != null && item.passRate !== ''"
                     type="primary"
                     size="mini"
                     class="status-tag">
                {{ item.checkResult }}
                åˆæ ¼çއ {{ formatPassRate(item.passRate) }}
              </u-tag>
              <u-tag :type="getStateTagType(item.inspectState)"
                     size="mini"
@@ -316,11 +316,13 @@
    return inspectState ? "checkmark-circle" : "time";
  };
  // èŽ·å–æ ‡ç­¾ç±»åž‹
  const getTagType = checkResult => {
    if (checkResult === "合格") return "success";
    if (checkResult === "不合格") return "error";
    return "default";
  // æ ¼å¼åŒ–合格率
  const formatPassRate = rate => {
    if (rate === null || rate === undefined || rate === "") return "-";
    const num = Number(rate);
    if (isNaN(num)) return rate;
    if (num <= 1) return `${(num * 100).toFixed(2)}%`;
    return `${num}%`;
  };
  // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
@@ -383,9 +385,11 @@
        pendingCount.value = inspectionList.value.filter(
          item => !item.inspectState
        ).length;
        qualifiedCount.value = inspectionList.value.filter(
          item => item.checkResult === "合格"
        ).length;
        qualifiedCount.value = inspectionList.value.filter(item => {
          const rate = Number(item.passRate);
          if (isNaN(rate)) return false;
          return rate <= 1 ? rate >= 1 : rate >= 100;
        }).length;
      })
      .catch(err => {
        tableLoading.value = false;