zhangwencui
2026-03-02 66922ca2c9360f4c06e10eea57691170eea9288a
设备保养重构\设备巡检修改
已添加1个文件
已修改5个文件
1613 ■■■■■ 文件已修改
src/api/equipmentManagement/upkeep.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/detail.vue 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/index.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/maintain.vue 1005 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/upkeep.js
@@ -97,3 +97,12 @@
  });
}
// æŸ¥è¯¢å¤‡ä»¶é€‰é¡¹
export const getSparePartsOptions = (params) => {
  return request({
    url: "/spareParts/getByIdDeviceId",
    method: "get",
    params,
  });
};
src/pages.json
@@ -541,6 +541,13 @@
      }
    },
    {
      "path": "pages/equipmentManagement/upkeep/detail",
      "style": {
        "navigationBarTitleText": "保养详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/upkeep/add",
      "style": {
        "navigationBarTitleText": "新增保养计划",
src/pages/equipmentManagement/upkeep/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,505 @@
<template>
  <view class="upkeep-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="保养详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-section"
          v-if="detailData">
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon">
            <up-icon name="file-text"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">{{ detailData.deviceName || '-' }}</text>
          <view class="status-tag">
            <u-tag v-if="detailData.status === 1"
                   type="success">完结</u-tag>
            <u-tag v-else-if="detailData.status === 0"
                   type="error">待保养</u-tag>
            <u-tag v-else-if="detailData.status === 2"
                   type="warning">失败</u-tag>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="detail-content">
          <view class="detail-row">
            <text class="detail-label">设备名称</text>
            <text class="detail-value">{{ detailData.deviceName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ detailData.deviceModel || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">计划保养日期</text>
            <text class="detail-value">{{ formatDate(detailData.maintenancePlanTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">实际保养人</text>
            <text class="detail-value">{{ detailData.maintenanceActuallyName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">实际保养日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.maintenanceActuallyTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">保养结果</text>
            <text class="detail-value">{{detailData.maintenanceResult || '-'}}
            </text>
          </view>
          <view class="detail-row">
            <text class="detail-label">录入人</text>
            <text class="detail-value">{{ detailData.createUserName || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">录入日期</text>
            <text class="detail-value">{{ formatDateTime(detailData.createTime) || '-' }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ detailData.remark || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- è®¾å¤‡å¤‡ä»¶ -->
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon secondary">
            <up-icon name="setting"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">设备备件</text>
        </view>
        <up-divider></up-divider>
        <view class="spare-parts-list"
              v-if="sparePartsList.length > 0">
          <view v-for="(item, index) in sparePartsList"
                :key="index"
                class="spare-part-item">
            <text class="spare-part-name">{{ item.sparePartName || item.name || '-' }}</text>
            <text class="spare-part-code">{{ item.sparePartCode || item.code || '-' }}</text>
          </view>
        </view>
        <view v-else
              class="empty-content">
          <text class="empty-text">暂无设备备件</text>
        </view>
      </view>
      <!-- ä¿å…»é™„ä»¶ -->
      <view class="detail-card">
        <view class="card-header">
          <view class="header-icon tertiary">
            <up-icon name="photo"
                     size="20"
                     color="#ffffff"></up-icon>
          </view>
          <text class="header-title">保养附件</text>
        </view>
        <up-divider></up-divider>
        <view class="file-list"
              v-if="fileList.length > 0">
          <view v-for="(file, index) in fileList"
                :key="index"
                class="file-item"
                @click="previewFile(file)">
            <image v-if="file.type.includes('image')"
                   :src="file.url"
                   class="file-preview"
                   mode="aspectFill" />
            <view v-else
                  class="file-placeholder">
              <up-icon name="play-circle"
                       size="32"
                       color="#999"></up-icon>
            </view>
          </view>
        </view>
        <view v-else
              class="empty-content">
          <text class="empty-text">暂无保养附件</text>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <up-icon name="info-circle"
               size="48"
               color="#ccc"></up-icon>
      <text class="no-data-text">暂无保养详情数据</text>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-btns"
          v-if="detailData">
      <u-button type="primary"
                class="action-btn"
                :disabled="detailData.status === 1"
                @click="goMaintain">
        ä¿å…»
      </u-button>
      <u-button type="error"
                plain
                class="action-btn"
                @click="deleteUpkeep">
        åˆ é™¤
      </u-button>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { delUpkeep } from "@/api/equipmentManagement/upkeep";
  import config from "@/config";
  import dayjs from "dayjs";
  // æ˜¾ç¤ºæç¤ºä¿¡æ¯
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  defineOptions({
    name: "保养详情",
  });
  // è¯¦æƒ…数据
  const detailData = ref(null);
  // è®¾å¤‡å¤‡ä»¶åˆ—表
  const sparePartsList = ref([]);
  // é™„件列表
  const fileList = ref([]);
  // é¡µé¢ID
  const pageId = ref(null);
  // æ ¼å¼åŒ–日期
  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");
  };
  // æ ¼å¼åŒ–文件URL
  const formatFileUrl = url => {
    if (!url) return "";
    if (url.startsWith("http://") || url.startsWith("https://")) {
      return url;
    }
    const uploadsIndex = url.indexOf("uploads");
    if (uploadsIndex !== -1) {
      const relativePath = url.substring(uploadsIndex);
      return `${config.baseUrl}/profile/${relativePath}`;
    }
    return `${config.baseUrl}/profile/${url}`;
  };
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
  const isImageFile = file => {
    if (file.contentType && file.contentType.startsWith("image/")) {
      return true;
    }
    if (file.type === "image") return true;
    const name = file.bucketFilename || file.originalFilename || file.name || "";
    const ext = name.split(".").pop()?.toLowerCase();
    return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
  };
  // é¢„览文件
  const previewFile = file => {
    if (file.type.includes("image")) {
      uni.previewImage({
        urls: [file.url],
        current: file.url,
      });
    } else {
      // è§†é¢‘预览
      const videoUrl = formatFileUrl(file.url || file.downloadUrl);
      uni.navigateTo({
        url: `/pages/common/videoPreview?url=${encodeURIComponent(videoUrl)}`,
      });
    }
  };
  // èŽ·å–è¯¦æƒ…æ•°æ®
  const getDetailData = () => {
    try {
      const dataStr = uni.getStorageSync("upkeepDetailData");
      if (!dataStr) {
        showToast("参数错误");
        return null;
      }
      return JSON.parse(dataStr);
    } catch (e) {
      showToast("数据解析失败");
      return null;
    }
  };
  // èŽ·å–ä¿å…»è¯¦æƒ…
  const getDetail = () => {
    const data = getDetailData();
    if (!data) {
      return;
    }
    detailData.value = data;
    pageId.value = data.id;
    // å¤„理备件数据 - æ”¯æŒå¤šç§æ•°æ®ç»“æž„
    sparePartsList.value = data.spareParts || data.sparePartList || [];
    // å¤„理附件数据 - æ”¯æŒå¤šç§æ•°æ®ç»“æž„
    console.log(data.imagesFile);
    fileList.value = data.imagesFile;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.removeStorageSync("upkeepDetailData");
    uni.navigateBack();
  };
  // è·³è½¬åˆ°ä¿å…»é¡µé¢
  const goMaintain = () => {
    if (detailData.value.status === 1) {
      showToast("该保养任务已完结");
      return;
    }
    uni.setStorageSync("repairId", pageId.value);
    uni.navigateTo({
      url: "/pages/equipmentManagement/upkeep/maintain",
    });
  };
  // åˆ é™¤ä¿å…»
  const deleteUpkeep = () => {
    uni.showModal({
      title: "警告",
      content: "确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?",
      confirmText: "确定",
      cancelText: "取消",
      success: async res => {
        if (!res.confirm) return;
        try {
          const response = await delUpkeep(pageId.value);
          if (response.code === 200) {
            showToast("删除成功");
            setTimeout(() => {
              goBack();
            }, 1500);
          } else {
            showToast(response.msg || "删除失败");
          }
        } catch (e) {
          showToast("删除失败");
        }
      },
    });
  };
  onShow(() => {
    getDetail();
  });
  onMounted(() => {
    getDetail();
  });
</script>
<style scoped lang="scss">
  .upkeep-detail {
    min-height: 100vh;
    background: #f5f5f5;
    padding-bottom: 80px;
  }
  .detail-section {
    padding: 15px;
  }
  .detail-card {
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
  }
  .header-icon {
    width: 40px;
    height: 40px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
  }
  .header-icon.secondary {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  }
  .header-icon.tertiary {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  }
  .header-title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .status-tag {
    margin-left: auto;
  }
  .detail-content {
    padding-top: 8px;
  }
  .detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .detail-row:last-child {
    border-bottom: none;
  }
  .detail-label {
    font-size: 14px;
    color: #666;
    min-width: 100px;
  }
  .detail-value {
    font-size: 14px;
    color: #333;
    text-align: right;
    flex: 1;
  }
  // å¤‡ä»¶åˆ—表
  .spare-parts-list {
    padding-top: 8px;
  }
  .spare-part-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }
  .spare-part-item:last-child {
    border-bottom: none;
  }
  .spare-part-name {
    font-size: 14px;
    color: #333;
  }
  .spare-part-code {
    font-size: 12px;
    color: #999;
  }
  // æ–‡ä»¶åˆ—表
  .file-list {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    padding-top: 8px;
  }
  .file-item {
    width: calc(33.33% - 7px);
    aspect-ratio: 1;
    border-radius: 8px;
    overflow: hidden;
    background: #f5f5f5;
  }
  .file-preview {
    width: 100%;
    height: 100%;
  }
  .file-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f0f0f0;
  }
  // ç©ºçŠ¶æ€
  .no-data {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 60px 20px;
  }
  .no-data-text {
    margin-top: 16px;
    font-size: 14px;
    color: #999;
  }
  // ç©ºå†…容提示
  .empty-content {
    padding: 30px 0;
    text-align: center;
  }
  .empty-text {
    font-size: 14px;
    color: #999;
  }
  // åº•部按钮
  .footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 12px 20px;
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
    z-index: 1000;
  }
  .action-btn {
    flex: 1;
    margin: 0 8px;
  }
</style>
src/pages/equipmentManagement/upkeep/index.vue
@@ -41,7 +41,9 @@
              <u-tag v-if="item.status === 1"
                     type="success">完结</u-tag>
              <u-tag v-if="item.status === 0"
                     type="error">待保养</u-tag>
                     type="warning">待保养</u-tag>
              <u-tag v-if="item.status === 2"
                     type="error">失败</u-tag>
            </view>
          </view>
          <up-divider></up-divider>
@@ -72,35 +74,24 @@
            </view>
            <view class="detail-row">
              <text class="detail-label">保养结果</text>
              <view class="detail-value">
                <u-tag v-if="item.maintenanceResult === 1"
                       type="success"
                       size="mini">
                  å®Œå¥½
                </u-tag>
                <u-tag v-if="item.maintenanceResult === 0"
                       type="error"
                       size="mini">
                  ç»´ä¿®
                </u-tag>
                <text v-if="item.maintenanceResult === undefined || item.maintenanceResult === null">-</text>
              </view>
              <text class="detail-value">{{item.maintenanceResult || '-'}}
              </text>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <u-button type="primary"
            <!-- <u-button type="primary"
                      size="small"
                      class="action-btn"
                      :disabled="item.status === 1"
                      @click.stop="edit(item.id)">
              ç¼–辑
            </u-button>
            </u-button> -->
            <u-button type="warning"
                      size="small"
                      class="action-btn"
                      :disabled="item.status === 1"
                      @click.stop="addMaintain(item.id)">
                      @click.stop="addMaintain(item.id, item)">
              ä¿å…»
            </u-button>
            <u-button type="error"
@@ -110,12 +101,18 @@
                      @click.stop="delUpkeepByIds(item.id)">
              åˆ é™¤
            </u-button>
            <u-button type="warning"
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click.stop="viewDetail(item)">
              è¯¦æƒ…
            </u-button>
            <!-- <u-button type="warning"
                      size="small"
                      class="action-btn"
                      @click.stop="addFile(item.id)">
              é™„ä»¶
            </u-button>
            </u-button> -->
          </view>
        </view>
      </view>
@@ -236,19 +233,34 @@
  };
  // æ–°å¢žä¿å…» - è·³è½¬åˆ°ä¿å…»é¡µé¢
  const addMaintain = id => {
  const addMaintain = (id, item) => {
    if (!id && multipleList.value.length !== 1) {
      showToast("请选择一条记录");
      return;
    }
    const targetId = id || multipleList.value[0].id;
    // ä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¼ é€’id
    const targetItem = item || multipleList.value[0];
    // ä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¼ é€’id和完整数据
    uni.setStorageSync("repairId", targetId);
    uni.setStorageSync("upkeepItemData", JSON.stringify(targetItem));
    uni.navigateTo({
      url: "/pages/equipmentManagement/upkeep/maintain",
    });
  };
  // æŸ¥çœ‹è¯¦æƒ… - è·³è½¬åˆ°è¯¦æƒ…页面
  const viewDetail = item => {
    if (!item) {
      showToast("参数错误");
      return;
    }
    // ä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¼ é€’整条数据
    uni.setStorageSync("upkeepDetailData", JSON.stringify(item));
    uni.navigateTo({
      url: "/pages/equipmentManagement/upkeep/detail",
    });
  };
  // æ–°å¢žè®¡åˆ’ - è·³è½¬åˆ°æ–°å¢žé¡µé¢
  const addPlan = () => {
    uni.navigateTo({
src/pages/equipmentManagement/upkeep/maintain.vue
@@ -1,89 +1,235 @@
<template>
    <view class="upkeep-maintain">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader title="新增保养" @back="goBack" />
    <PageHeader title="新增保养"
                @back="goBack" />
        <!-- è¡¨å•内容 -->
        <u-form ref="formRef" :model="form" :rules="formRules" label-width="110px" :error-type="['message']">
    <u-form ref="formRef"
            :model="form"
            :rules="formRules"
            label-width="110px"
            :error-type="['message']">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <u-form-item label="实际保养人" prop="maintenanceActuallyName" required border-bottom>
                <u-input
                    v-model="form.maintenanceActuallyName"
      <u-form-item label="实际保养人"
                   prop="maintenanceActuallyName"
                   required
                   border-bottom>
        <u-input v-model="form.maintenanceActuallyName"
                    placeholder="请输入实际保养人"
                    clearable
                />
                 clearable />
            </u-form-item>
            <u-form-item label="实际保养日期" prop="maintenanceActuallyTime" required border-bottom>
                <u-input
                    v-model="form.maintenanceActuallyTime"
      <u-form-item label="实际保养日期"
                   prop="maintenanceActuallyTime"
                   required
                   border-bottom>
        <u-input v-model="form.maintenanceActuallyTime"
                    placeholder="请选择实际保养日期"
                    readonly
                    @click="showDatePicker"
                    clearable
                />
                 clearable />
                <template #right>
                    <u-icon name="arrow-right" @click.stop="showDatePicker" />
          <u-icon name="arrow-right"
                  @click.stop="showDatePicker" />
                </template>
            </u-form-item>
            <u-form-item label="保养结果" prop="maintenanceResult" required border-bottom>
                <u-input
                    v-model="maintenanceResultText"
                    placeholder="请选择保养结果"
      <u-form-item label="保养结果"
                   prop="maintenanceResult"
                   required
                   border-bottom>
        <u-input v-model="form.maintenanceResult"
                 placeholder="请输入保养结果"
                 clearable />
      </u-form-item>
      <u-form-item label="保养状态"
                   prop="status"
                   required
                   border-bottom>
        <u-input v-model="maintenancestatusText"
                 placeholder="请选择保养状态"
                    readonly
                    @click="showResultPicker"
                    clearable
                />
                 clearable />
                <template #right>
                    <u-icon name="arrow-right" @click.stop="showResultPicker" />
          <u-icon name="arrow-right"
                  @click="showResultPicker" />
                </template>
            </u-form-item>
      <u-form-item label="设备备件"
                   prop="sparePartsIds"
                   border-bottom>
        <view class="spare-parts-container"
              @click="showSparePartPicker">
          <view v-if="selectedSpareParts.length > 0"
                class="spare-parts-list">
            <view v-for="(item, index) in selectedSpareParts"
                  :key="index"
                  class="spare-part-tag">
              <text>{{ item.name }}</text>
              <u-icon name="close"
                      size="12"
                      color="#fff"
                      @click="removeSparePart(index)" />
            </view>
          </view>
          <text v-else
                class="placeholder">请选择设备备件</text>
        </view>
        <template #right>
          <u-icon name="arrow-right"
                  @click="showSparePartPicker" />
        </template>
      </u-form-item>
      <!-- ä¸Šä¼ é™„ä»¶ -->
      <u-form-item v-if="form.status == '1'"
                   label="保养附件"
                   border-bottom>
        <view class="simple-upload-area">
          <view class="upload-buttons">
            <u-button type="primary"
                      @click="chooseMedia('image')"
                      :loading="uploading"
                      :disabled="uploadFiles.length >= uploadConfig.limit"
                      :customStyle="{ marginRight: '10px', flex: 1 }">
              <u-icon name="camera"
                      size="18"
                      color="#fff"
                      style="margin-right: 5px;"></u-icon>
              {{ uploading ? '上传中...' : '拍照' }}
            </u-button>
            <u-button type="success"
                      @click="chooseMedia('video')"
                      :loading="uploading"
                      :disabled="uploadFiles.length >= uploadConfig.limit"
                      :customStyle="{ flex: 1 }">
              <uni-icons type="videocam"
                         name="videocam"
                         size="18"
                         color="#fff"
                         style="margin-right: 5px;"></uni-icons>
              {{ uploading ? '上传中...' : '拍视频' }}
            </u-button>
          </view>
          <!-- ä¸Šä¼ è¿›åº¦ -->
          <view v-if="uploading"
                class="upload-progress">
            <u-line-progress :percentage="uploadProgress"
                             :showText="true"
                             activeColor="#409eff"></u-line-progress>
          </view>
          <!-- ä¸Šä¼ çš„æ–‡ä»¶åˆ—表 -->
          <view v-if="uploadFiles.length > 0"
                class="file-list">
            <view v-for="(file, index) in uploadFiles"
                  :key="index"
                  class="file-item">
              <view class="file-preview-container">
                <image v-if="file.type === 'image' || isImageFile(file)"
                       :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                       class="file-preview"
                       mode="aspectFill" />
                <view v-else-if="file.type === 'video'"
                      class="video-preview">
                  <uni-icons type="videocam"
                             name="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px;"></uni-icons>
                  <text class="video-text">视频</text>
                </view>
                <!-- åˆ é™¤æŒ‰é’® -->
                <view class="delete-btn"
                      @click="removeFile(index)">
                  <u-icon name="close"
                          size="12"
                          color="#fff"></u-icon>
                </view>
              </view>
              <view class="file-info">
                <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频')
                  }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
              </view>
            </view>
          </view>
          <view v-if="uploadFiles.length === 0"
                class="empty-state">
            <text>请选择要上传的保养图片或视频</text>
          </view>
        </view>
      </u-form-item>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <u-button class="cancel-btn" @click="goBack">取消</u-button>
                <u-button class="save-btn" @click="sendForm" :loading="loading">保存</u-button>
        <u-button class="cancel-btn"
                  @click="goBack">取消</u-button>
        <u-button class="save-btn"
                  @click="sendForm"
                  :loading="loading">保存</u-button>
            </view>
        </u-form>
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <u-popup v-model="showDate" mode="bottom" :closeable="true">
            <u-datetime-picker
                v-model="form.maintenanceActuallyTime"
    <u-popup v-model="showDate"
             mode="bottom"
             :closeable="true">
      <u-datetime-picker v-model="form.maintenanceActuallyTime"
                mode="date"
                title="选择日期"
                @confirm="onDateConfirm"
                @cancel="showDate = false"
            />
                         @cancel="showDate = false" />
        </u-popup>
        <!-- ä¿å…»ç»“果选择器 -->
        <up-action-sheet
            :show="showResult"
    <up-action-sheet :show="showResult"
            :actions="resultColumns"
            title="选择保养结果"
            @select="onResultConfirm"
            @close="showResult = false"
        />
                     @close="showResult = false" />
    <!-- è®¾å¤‡å¤‡ä»¶é€‰æ‹©å™¨ -->
    <up-popup :show="showSparePart"
              mode="bottom"
              :closeable="true"
              @close="showSparePart = false">
      <view class="spare-part-popup">
        <view class="popup-header">
          <text class="popup-title">选择设备备件</text>
          <up-button type="primary"
                     size="small"
                     @click="confirmSparePartSelection">确定</up-button>
        </view>
        <view class="spare-part-options">
          <view v-for="(item, index) in sparePartOptions"
                :key="index"
                class="spare-part-option"
                :class="{ 'selected': isSparePartSelected(item.id) }"
                @click="toggleSparePartSelection(item)">
            <text>{{ item.name }}</text>
            <u-icon v-if="isSparePartSelected(item.id)"
                    name="checkmark"
                    color="#2c7be5" />
          </view>
        </view>
      </view>
    </up-popup>
    </view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { addMaintenance } from '@/api/equipmentManagement/upkeep';
  import { ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    addMaintenance,
    getSparePartsOptions,
  } from "@/api/equipmentManagement/upkeep";
import useUserStore from "@/store/modules/user";
import dayjs from "dayjs";
import { formatDateToYMD } from '@/utils/ruoyi';
  import { formatDateToYMD } from "@/utils/ruoyi";
  import config from "@/config";
// æ˜¾ç¤ºæç¤ºä¿¡æ¯
const showToast = (message) => {
  const showToast = message => {
  uni.showToast({
    title: message,
    icon: 'none'
  })
      icon: "none",
    });
};
defineOptions({
@@ -97,28 +243,59 @@
const loading = ref(false);
const showDate = ref(false);
const showResult = ref(false);
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
  const showSparePart = ref(false);
  const currentDate = ref([
    new Date().getFullYear(),
    new Date().getMonth() + 1,
    new Date().getDate(),
  ]);
const resultPickerValue = ref([]);
const maintenanceResultText = ref('');
  const maintenancestatusText = ref("");
  const selectedSpareParts = ref([]);
  const tempSelectedSpareParts = ref([]);
  // æ–‡ä»¶ä¸Šä¼ ç›¸å…³
  const uploadFiles = ref([]);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const number = ref(0);
  // ä¸Šä¼ é…ç½®
  const uploadConfig = {
    limit: 9,
    fileType: ["jpg", "jpeg", "png", "gif", "webp", "mp4", "mov", "avi", "wmv"],
    maxVideoDuration: 60,
  };
  // ä¸Šä¼ æ–‡ä»¶URL
  const uploadFileUrl = ref(`${config.baseUrl}/file/upload`);
// ä¿å…»ç»“果选项
const resultColumns = [
    { name: '完好', value: 1 },
    { name: 'ç»´ä¿®', value: 0 }
    { name: "完结", value: 1 },
    { name: "待保养", value: 0 },
    { name: "失败", value: 2 },
];
// è¡¨å•验证规则
const formRules = {
    maintenanceActuallyName: [{ required: true, trigger: "blur", message: "请输入实际保养人" }],
    maintenanceActuallyTime: [{ required: true, trigger: "change", message: "请选择实际保养日期" }],
    maintenanceResult: [{ required: true, trigger: "change", message: "请选择保养结果" }],
    maintenanceActuallyName: [
      { required: true, trigger: "blur", message: "请输入实际保养人" },
    ],
    maintenanceActuallyTime: [
      { required: true, trigger: "change", message: "请选择实际保养日期" },
    ],
    maintenanceResult: [
      { required: true, trigger: "change", message: "请选择保养结果" },
    ],
};
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    maintenanceActuallyName: userStore.nickName || '', // é»˜è®¤ä½¿ç”¨å½“前用户昵称
    maintenanceActuallyName: userStore.nickName || "", // é»˜è®¤ä½¿ç”¨å½“前用户昵称
    maintenanceResult: undefined, // ä¿å…»ç»“æžœ
    maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // å®žé™…保养日期(只显示日期)
    sparePartsIds: undefined, // è®¾å¤‡å¤‡ä»¶ID
});
// æ¸…除表单校验状态
@@ -129,16 +306,34 @@
// é‡ç½®è¡¨å•数据和校验状态
const resetForm = () => {
    form.value = {
        maintenanceActuallyName: userStore.nickName || '',
      maintenanceActuallyName: userStore.nickName || "",
        maintenanceResult: undefined,
        maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
      sparePartsIds: [],
    };
    maintenanceResultText.value = '';
    maintenancestatusText.value = "";
    selectedSpareParts.value = [];
    tempSelectedSpareParts.value = [];
};
const resetFormAndValidate = () => {
    resetForm();
    // clearValidate(); // åˆ é™¤è¿™è¡Œï¼ŒVant4会自动处理
  };
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
  const isImageFile = file => {
    // æ£€æŸ¥contentType字段
    if (file.contentType && file.contentType.startsWith("image/")) {
      return true;
    }
    // æ£€æŸ¥åŽŸæœ‰çš„type字段
    if (file.type === "image") return true;
    // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名
    const name = file.bucketFilename || file.originalFilename || file.name || "";
    const ext = name.split(".").pop()?.toLowerCase();
    return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
};
// æäº¤è¡¨å•
@@ -146,16 +341,19 @@
    try {
        // æ‰‹åŠ¨éªŒè¯è¡¨å•
        let isValid = true;
        let errorMessage = '';
      let errorMessage = "";
        if (!form.value.maintenanceActuallyName) {
            isValid = false;
            errorMessage = '请输入实际保养人';
        errorMessage = "请输入实际保养人";
        } else if (!form.value.maintenanceActuallyTime) {
            isValid = false;
            errorMessage = '请选择实际保养日期';
        errorMessage = "请选择实际保养日期";
        } else if (form.value.maintenanceResult === undefined) {
            isValid = false;
            errorMessage = '请选择保养结果';
        errorMessage = "请选择保养结果";
      } else if (uploadFiles.value.length === 0 && form.value.status == "1") {
        isValid = false;
        errorMessage = "请上传保养照片";
        }
        if (!isValid) {
@@ -166,7 +364,7 @@
        // éªŒè¯é€šè¿‡
        submitFormData();
    } catch (e) {
        showToast('表单验证失败');
      showToast("表单验证失败");
    }
};
@@ -177,17 +375,20 @@
        const id = getPageId();
        
        if (!id) {
            showToast('参数错误');
        showToast("参数错误");
            loading.value = false;
            return;
        }
        // å‡†å¤‡æäº¤æ•°æ®ï¼ŒmaintenanceActuallyTime åŠ ä¸Šå½“å‰æ—¶åˆ†ç§’
        const submitData = { ...form.value };
      const submitData = {
        ...form.value,
        imagesFile: form.value.status == "1" ? uploadFiles.value : [],
        sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
      };
        const { code } = await addMaintenance({ id: id, ...submitData });
        
        if (code == 200) {
            showToast('新增保养成功');
        showToast("新增保养成功");
            resetFormAndValidate();
            setTimeout(() => {
                uni.navigateBack();
@@ -197,21 +398,36 @@
        }
    } catch (e) {
        loading.value = false;
        showToast('操作失败');
      showToast("操作失败");
    }
};
// è¿”回上一页
const goBack = () => {
    // æ¸…除存储的id
    uni.removeStorageSync('repairId');
    // æ¸…除存储的id和数据
    uni.removeStorageSync("repairId");
    uni.removeStorageSync("upkeepItemData");
    uni.navigateBack();
};
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    // ä»Žæœ¬åœ°å­˜å‚¨èŽ·å–id
    return uni.getStorageSync('repairId');
    return uni.getStorageSync("repairId");
  };
  // èŽ·å–è®¾å¤‡ä¿¡æ¯
  const getUpkeepItemData = () => {
    try {
      const dataStr = uni.getStorageSync("upkeepItemData");
      if (!dataStr) {
        return null;
      }
      return JSON.parse(dataStr);
    } catch (e) {
      console.error("解析设备数据失败:", e);
      return null;
    }
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
@@ -220,9 +436,11 @@
};
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = (e) => {
  const onDateConfirm = e => {
    // åªä¿å­˜å¹´æœˆæ—¥ï¼Œä¸åŒ…含时分秒
    form.value.maintenanceActuallyTime = dayjs(e.value).format('YYYY-MM-DD HH:mm:ss');
    form.value.maintenanceActuallyTime = dayjs(e.value).format(
      "YYYY-MM-DD HH:mm:ss"
    );
    showDate.value = false;
};
@@ -232,25 +450,490 @@
};
// ç¡®è®¤ä¿å…»ç»“果选择
const onResultConfirm = (selected) => {
    form.value.maintenanceResult = selected.value;
    maintenanceResultText.value = selected.name;
  const onResultConfirm = selected => {
    form.value.status = selected.value;
    maintenancestatusText.value = selected.name;
    showResult.value = false;
  };
  // æ˜¾ç¤ºè®¾å¤‡å¤‡ä»¶é€‰æ‹©å™¨
  const showSparePartPicker = () => {
    tempSelectedSpareParts.value = [...selectedSpareParts.value];
    showSparePart.value = true;
  };
  // æ£€æŸ¥å¤‡ä»¶æ˜¯å¦å·²é€‰ä¸­
  const isSparePartSelected = value => {
    return tempSelectedSpareParts.value.some(item => item.value === value);
  };
  // åˆ‡æ¢å¤‡ä»¶é€‰ä¸­çŠ¶æ€
  const toggleSparePartSelection = item => {
    const index = tempSelectedSpareParts.value.findIndex(
      selected => selected.value === item.value
    );
    if (index > -1) {
      tempSelectedSpareParts.value.splice(index, 1);
    } else {
      tempSelectedSpareParts.value.push(item);
    }
  };
  // ç¡®è®¤å¤‡ä»¶é€‰æ‹©
  const confirmSparePartSelection = () => {
    selectedSpareParts.value = [...tempSelectedSpareParts.value];
    form.value.sparePartsIds = selectedSpareParts.value.map(item => item.value);
    showSparePart.value = false;
  };
  // ç§»é™¤å·²é€‰å¤‡ä»¶
  const removeSparePart = index => {
    selectedSpareParts.value.splice(index, 1);
    form.value.sparePartsIds = selectedSpareParts.value.map(item => item.value);
};
// åˆå§‹åŒ–表单数据
const initForm = () => {
    // èŽ·å–è®¾å¤‡ä¿¡æ¯
    const itemData = getUpkeepItemData();
    // é‡ç½®é€‰æ‹©çš„备件
    selectedSpareParts.value = [];
    // é‡ç½®ä¸Šä¼ çš„æ–‡ä»¶
    uploadFiles.value = [];
    uploading.value = false;
    uploadProgress.value = 0;
    maintenancestatusText.value = "";
    // è®¾ç½®ä¿å…»äººä¸ºå½“前用户昵称
    form.value.maintenanceActuallyName = userStore.nickName || '';
    form.value.maintenanceActuallyName = userStore.nickName || "";
    // è®¾ç½®å½“前日期(只包含年月日)
    form.value.maintenanceActuallyTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
    currentDate.value = [new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()];
    form.value.maintenanceActuallyTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
    currentDate.value = [
      new Date().getFullYear(),
      new Date().getMonth() + 1,
      new Date().getDate(),
    ];
    // å¦‚果有设备信息,填充已有的保养数据
    if (itemData) {
      // å¡«å……实际保养人
      if (itemData.maintenanceActuallyName) {
        form.value.maintenanceActuallyName = itemData.maintenanceActuallyName;
      }
      // å¡«å……保养结果
      if (
        itemData.maintenanceResult !== undefined &&
        itemData.maintenanceResult !== null
      ) {
        form.value.maintenanceResult = itemData.maintenanceResult;
      }
      // å¡«å……实际保养日期
      if (itemData.maintenanceActuallyTime) {
        form.value.maintenanceActuallyTime = itemData.maintenanceActuallyTime;
        // è§£æžæ—¥æœŸè®¾ç½®åˆ°æ—¥æœŸé€‰æ‹©å™¨
        const date = dayjs(itemData.maintenanceActuallyTime);
        currentDate.value = [date.year(), date.month() + 1, date.date()];
      }
      // å¡«å……保养状态
      if (itemData.status) {
        const statusMap = {
          0: "待保养",
          1: "完结",
          2: "失败",
        };
        maintenancestatusText.value = statusMap[itemData.status] || "";
      }
      // å¡«å……备件数据
      if (itemData.spareParts && itemData.spareParts.length > 0) {
        selectedSpareParts.value = itemData.spareParts.map(sparePart => ({
          id: sparePart.id || sparePart.sparePartId || sparePart.value,
          name: sparePart.name || sparePart.sparePartName,
          code: sparePart.code || sparePart.sparePartCode,
          value: sparePart.id || sparePart.sparePartId || sparePart.value,
        }));
        // è®¾ç½®å¤‡ä»¶IDs
        form.value.sparePartsIds = selectedSpareParts.value
          .map(item => item.value)
          .join(",");
      }
      // å¡«å……附件数据
      if (itemData.files && itemData.files.length > 0) {
        uploadFiles.value = itemData.files.map(file => ({
          id: file.id,
          name: file.name || file.bucketFilename || file.originalFilename,
          url: file.url || file.downloadUrl,
          type:
            file.type ||
            (file.contentType && file.contentType.startsWith("image/")
              ? "image"
              : "video"),
          size: file.size || file.byteSize,
        }));
      } else if (itemData.uploadFiles && itemData.uploadFiles.length > 0) {
        uploadFiles.value = itemData.uploadFiles.map(file => ({
          id: file.id,
          name: file.name || file.bucketFilename || file.originalFilename,
          url: file.url || file.downloadUrl || file.tempFilePath || file.path,
          type: file.type,
          size: file.size,
        }));
      }
    }
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆå§‹åŒ–表单
    initForm();
    fetchSparePartOptions(getPageId());
});
  const sparePartOptions = ref([]);
  const fetchSparePartOptions = deviceLedgerId => {
    getSparePartsOptions({ deviceLedgerId: deviceLedgerId }).then(res => {
      if (res.code == 200) {
        sparePartOptions.value = res.data || [];
      }
    });
  };
  // æ ¼å¼åŒ–文件URL
  const formatFileUrl = url => {
    if (!url) return "";
    // å¦‚果已经是完整的URL(http或https开头),直接返回
    if (url.startsWith("http://") || url.startsWith("https://")) {
      return url;
    }
    // å¦‚果是本地路径(如 D:\\ruoyi\\prod\\uploads...),需要转换为网络URL
    // ä»Žè·¯å¾„中提取uploads后面的部分
    const uploadsIndex = url.indexOf("uploads");
    if (uploadsIndex !== -1) {
      const relativePath = url.substring(uploadsIndex);
      // ä½¿ç”¨baseUrl + /profile/ + ç›¸å¯¹è·¯å¾„
      return `http://192.168.1.35:8888/profile/${relativePath}`;
    }
    // å…¶ä»–情况,尝试直接拼接
    return `http://192.168.1.35:8888/profile/${url}`;
  };
  // æ ¼å¼åŒ–文件大小
  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 chooseMedia = type => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    const remaining = uploadConfig.limit - uploadFiles.value.length;
    // ä¼˜å…ˆï¼šchooseMedia(支持 image/video)
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"], // æ”¯æŒç›¸æœºå’Œç›¸å†Œ
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
                uid: Date.now() + Math.random() + idx,
              };
              console.log("chooseMedia æˆåŠŸèŽ·å–æ–‡ä»¶:", file);
              handleBeforeUpload(file);
            });
          } catch (e) {
            console.error("处理拍摄结果失败:", e);
            uni.showToast({ title: "处理文件失败", icon: "error" });
          }
        },
        fail: err => {
          console.error("拍摄失败:", err);
          uni.showToast({ title: "拍摄失败", icon: "error" });
        },
      });
      return;
    }
    // é™çº§ï¼šchooseImage / chooseVideo
    if (type === "video") {
      uni.chooseVideo({
        sourceType: ["camera", "album"],
        maxDuration: uploadConfig.maxVideoDuration,
        camera: "back",
        success: res => {
          try {
            if (!res.tempFilePath) {
              throw new Error("未获取到视频文件");
            }
            const file = {
              tempFilePath: res.tempFilePath,
              path: res.tempFilePath,
              type: "video",
              name: `video_${Date.now()}.mp4`,
              size: res.size || 0,
              duration: res.duration || 0,
              createTime: new Date().getTime(),
              uid: Date.now() + Math.random(),
            };
            handleBeforeUpload(file);
          } catch (error) {
            console.error("处理拍视频结果失败:", error);
            uni.showToast({ title: "处理视频失败", icon: "error" });
          }
        },
        fail: err => {
          console.error("拍视频失败:", err);
          uni.showToast({ title: "拍视频失败", icon: "error" });
        },
      });
    } else {
      uni.chooseImage({
        count: 1,
        sizeType: ["compressed", "original"],
        sourceType: ["camera", "album"],
        success: res => {
          const tempFilePath = res?.tempFilePaths?.[0];
          const tempFile = res?.tempFiles?.[0] || {};
          if (!tempFilePath) return;
          handleBeforeUpload({
            tempFilePath,
            path: tempFilePath,
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: Date.now(),
            uid: Date.now() + Math.random(),
          });
        },
      });
    }
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    uni.showModal({
      title: "确认删除",
      content: "确定要删除这个文件吗?",
      success: res => {
        if (res.confirm) {
          uploadFiles.value.splice(index, 1);
          uni.showToast({
            title: "删除成功",
            icon: "success",
          });
        }
      },
    });
  };
  // ä¸Šä¼ å‰æ ¡éªŒ
  const handleBeforeUpload = async file => {
    // æ ¡éªŒæ–‡ä»¶ç±»åž‹
    if (
      uploadConfig.fileType &&
      Array.isArray(uploadConfig.fileType) &&
      uploadConfig.fileType.length > 0
    ) {
      const fileName = file.name || "";
      const fileExtension = fileName
        ? fileName.split(".").pop().toLowerCase()
        : "";
      // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
      let expectedTypes = [];
      if (file.type === "image") {
        expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
      } else if (file.type === "video") {
        expectedTypes = ["mp4", "mov", "avi", "wmv"];
      }
      // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
      if (fileExtension && expectedTypes.length > 0) {
        const isAllowed = expectedTypes.some(
          type => uploadConfig.fileType.includes(type) && type === fileExtension
        );
        if (!isAllowed) {
          uni.showToast({
            title: `文件格式不支持,请拍摄 ${expectedTypes.join("/")} æ ¼å¼çš„æ–‡ä»¶`,
            icon: "none",
          });
          return false;
        }
      }
    }
    // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
    uploadFile(file);
    return true;
  };
  // æ–‡ä»¶ä¸Šä¼ å¤„理
  const uploadFile = async file => {
    uploading.value = true;
    uploadProgress.value = 0;
    number.value++;
    // ç¡®ä¿token存在
    const token = userStore.token;
    if (!token) {
      handleUploadError("用户未登录");
      return;
    }
    uploadWithUniUploadFile(file, file.tempFilePath || file.path || "", token);
  };
  // ä½¿ç”¨uni.uploadFile上传
  const uploadWithUniUploadFile = (file, filePath, token) => {
    if (!filePath) {
      handleUploadError("文件路径不存在");
      return;
    }
    const uploadTask = uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: filePath,
      name: "file",
      formData: {
        type: 10, // ä¿å…»é™„件类型
      },
      header: {
        Authorization: `Bearer ${token}`,
      },
      success: res => {
        try {
          if (res.statusCode === 200) {
            const response = JSON.parse(res.data);
            if (response.code === 200) {
              handleUploadSuccess(response, file);
              uni.showToast({
                title: "上传成功",
                icon: "success",
              });
            } else {
              handleUploadError(response.msg || "服务器返回错误");
            }
          } else {
            handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
          }
        } catch (e) {
          console.error("解析响应失败:", e);
          console.error("原始响应数据:", res.data);
          handleUploadError("响应数据解析失败: " + e.message);
        }
      },
      fail: err => {
        console.error("上传失败:", err.errMsg || err);
        number.value--;
        let errorMessage = "上传失败";
        if (err.errMsg) {
          if (err.errMsg.includes("statusCode: null")) {
            errorMessage = "网络连接失败,请检查网络设置";
          } else if (err.errMsg.includes("timeout")) {
            errorMessage = "上传超时,请重试";
          } else if (err.errMsg.includes("fail")) {
            errorMessage = "上传失败,请检查网络连接";
          } else {
            errorMessage = err.errMsg;
          }
        }
        handleUploadError(errorMessage);
      },
      complete: () => {
        uploading.value = false;
        uploadProgress.value = 0;
      },
    });
    // ç›‘听上传进度
    if (uploadTask && uploadTask.onProgressUpdate) {
      uploadTask.onProgressUpdate(res => {
        uploadProgress.value = res.progress;
      });
    }
  };
  // ä¸Šä¼ æˆåŠŸå¤„ç†
  const handleUploadSuccess = (res, file) => {
    console.log("上传成功响应:", res);
    // å¤„理不同的数据结构:可能是数组,也可能是单个对象
    let uploadedFile = null;
    uploadedFile = res.data;
    if (!uploadedFile) {
      console.error("无法解析上传响应数据:", res);
      number.value--;
      handleUploadError("上传响应数据格式错误", false);
      return;
    }
    console.log("上传成功文件数据:", uploadedFile, file);
    // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id和type
    const fileData = {
      name: uploadedFile.originalName,
      type: file.type,
      url: uploadedFile.tempPath,
    };
    uploadFiles.value.push(fileData);
    number.value = 0;
  };
  // ä¸Šä¼ å¤±è´¥å¤„理
  const handleUploadError = (message = "上传文件失败", showRetry = false) => {
    uploading.value = false;
    uploadProgress.value = 0;
    if (showRetry) {
      uni.showModal({
        title: "上传失败",
        content: message + ",是否重试?",
        success: res => {
          if (res.confirm) {
            // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
          }
        },
      });
    } else {
      uni.showToast({
        title: message,
        icon: "error",
      });
    }
  };
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–è¡¨å•
@@ -259,7 +942,7 @@
</script>
<style scoped lang="scss">
@import '@/static/scss/form-common.scss';
  @import "@/static/scss/form-common.scss";
.upkeep-maintain {
    min-height: 100vh;
    background: #f8f9fa;
@@ -283,9 +966,9 @@
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    color: #ffffff;
    width: 6.375rem;
    background: #C7C9CC;
    background: #c7c9cc;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
@@ -293,9 +976,9 @@
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    color: #ffffff;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
@@ -312,4 +995,164 @@
    font-size: 12px; 
    color: #888; 
}
  /* è®¾å¤‡å¤‡ä»¶å¤šé€‰æ ·å¼ */
  .spare-parts-container {
    flex: 1;
    min-height: 40px;
    display: flex;
    align-items: center;
  }
  .spare-parts-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .spare-part-tag {
    display: flex;
    align-items: center;
    gap: 4px;
    background: #2c7be5;
    color: #fff;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
  }
  .placeholder {
    color: #c0c4cc;
    font-size: 14px;
  }
  /* å¤‡ä»¶é€‰æ‹©å¼¹çª—样式 */
  .spare-part-popup {
    padding: 16px;
    max-height: 60vh;
    display: flex;
    flex-direction: column;
  }
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e8e8e8;
  }
  .popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .spare-part-options {
    flex: 1;
    overflow-y: auto;
  }
  .spare-part-option {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid #f0f0f0;
    font-size: 14px;
    color: #333;
  }
  .spare-part-option.selected {
    color: #2c7be5;
    font-weight: 500;
  }
  /* æ–‡ä»¶ä¸Šä¼ æ ·å¼ */
  .simple-upload-area {
    width: 100%;
  }
  .upload-buttons {
    display: flex;
    margin-bottom: 12px;
  }
  .upload-progress {
    margin: 12px 0;
  }
  .file-list {
    margin-top: 12px;
  }
  .file-item {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 8px;
  }
  .file-preview-container {
    position: relative;
    margin-right: 12px;
  }
  .file-preview {
    width: 80px;
    height: 80px;
    border-radius: 4px;
  }
  .video-preview {
    width: 80px;
    height: 80px;
    background: #333;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 12px;
  }
  .delete-btn {
    position: absolute;
    top: -8px;
    right: -8px;
    width: 20px;
    height: 20px;
    background: #f56c6c;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .file-info {
    flex: 1;
  }
  .file-name {
    display: block;
    font-size: 14px;
    color: #333;
    margin-bottom: 4px;
    line-height: 1.4;
  }
  .file-size {
    font-size: 12px;
    color: #999;
  }
  .empty-state {
    padding: 20px 0;
    text-align: center;
    color: #999;
    font-size: 14px;
  }
</style>
src/pages/inspectionUpload/index.vue
@@ -27,7 +27,7 @@
                fontSize: '12px',
                marginRight: '8px'
              }">
                æ‰«ç ä¸Šä¼ 
                å·¡æ£€ä¸Šä¼ 
              </u-button>
              <u-button type="success"
                        size="small"
@@ -180,6 +180,13 @@
                      :key="index"
                      class="file-item">
                  <view class="file-preview-container">
                    <!-- åˆ é™¤æŒ‰é’® -->
                    <view class="delete-btn"
                          @click="removeFile(index)">
                      <u-icon name="close"
                              size="12"
                              color="#fff"></u-icon>
                    </view>
                    <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                           :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                           class="file-preview"
@@ -192,13 +199,6 @@
                                 color="#fff"
                                 style="margin-right: 5px;"></uni-icons>
                      <text class="video-text">视频</text>
                    </view>
                    <!-- åˆ é™¤æŒ‰é’® -->
                    <view class="delete-btn"
                          @click="removeFile(index)">
                      <u-icon name="close"
                              size="12"
                              color="#fff"></u-icon>
                    </view>
                  </view>
                  <view class="file-info">
@@ -410,7 +410,7 @@
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    const baseUrl = "http://192.168.1.35:8888";
    const baseUrl = config.baseUrl;
    return baseUrl + uploadConfig.action;
  });
@@ -825,6 +825,11 @@
  // æäº¤ä¸Šä¼ 
  const submitUpload = async () => {
    // console.log("提交上传数据:", {
    //   before: beforeModelValue.value,
    //   after: afterModelValue.value,
    //   issue: issueModelValue.value,
    // });
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
      if (hasException.value === null) {
@@ -857,12 +862,15 @@
      let arr = [];
      if (beforeModelValue.value.length > 0) {
        arr.push(...beforeModelValue.value);
        infoData.value.beforeModelValue = beforeModelValue.value;
      }
      if (afterModelValue.value.length > 0) {
        arr.push(...afterModelValue.value);
        infoData.value.afterModelValue = afterModelValue.value;
      }
      if (issueModelValue.value.length > 0) {
        arr.push(...issueModelValue.value);
        infoData.value.issueModelValue = issueModelValue.value;
      }
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表(tempFileIds)
@@ -1045,7 +1053,7 @@
  };
  // æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸï¼ˆåŽç«¯è¦æ±‚前缀)
  const filePreviewBase = "http://192.168.1.35:8888";
  const filePreviewBase = config.baseUrl;
  // å°†åŽç«¯è¿”回的文件地址规范成可访问URL
  // å…¼å®¹åœºæ™¯ï¼š
@@ -1558,7 +1566,7 @@
    };
    uploadList.value.push(fileData);
    console.log("添加到分类前:", fileData);
    // ç«‹å³æ·»åŠ åˆ°å¯¹åº”çš„åˆ†ç±»ï¼Œä¸ç­‰å¾…æ‰€æœ‰æ–‡ä»¶ä¸Šä¼ å®Œæˆ
    switch (currentUploadType.value) {
      case "before":
@@ -1783,7 +1791,7 @@
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 10000;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
@@ -1922,6 +1930,7 @@
    cursor: pointer;
    box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
    transition: all 0.3s ease;
    z-index: 1000;
  }
  .delete-btn:hover {