gaoluyang
2026-05-15 944be786cefe66d7db1bb84904822ac559713841
进销存app
1.设备保养、巡检、报修添加字段
已添加2个文件
已修改7个文件
3033 ■■■■ 文件已修改
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/add.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/attachment.vue 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/components/formDia.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 1468 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/upload.vue 885 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json
@@ -761,6 +761,20 @@
      }
    },
    {
      "path": "pages/inspectionUpload/upload",
      "style": {
        "navigationBarTitleText": "上传巡检记录",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inspectionUpload/attachment",
      "style": {
        "navigationBarTitleText": "查看附件",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/faultAnalysis/index",
      "style": {
        "navigationBarTitleText": "故障分析追溯",
src/pages/equipmentManagement/repair/add.vue
@@ -69,6 +69,20 @@
                   placeholder="请输入报修人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修人"
                     prop="maintenanceName"
                     border-bottom>
          <u-input v-model="form.maintenanceName"
                   placeholder="请输入维修人"
                   clearable />
        </u-form-item>
        <u-form-item label="维修项目"
                     prop="maintenanceProject"
                     border-bottom>
          <u-input v-model="form.maintenanceProject"
                   placeholder="请输入维修项目"
                   clearable />
        </u-form-item>
        <u-form-item label="故障现象"
                     prop="remark"
                     required
@@ -169,6 +183,8 @@
    deviceModel: undefined, // è§„格型号
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸ
    repairName: undefined, // æŠ¥ä¿®äºº
    maintenanceName: undefined, // ç»´ä¿®äºº
    maintenanceProject: undefined, // ç»´ä¿®é¡¹ç›®
    remark: undefined, // æ•…障现象
  });
@@ -221,6 +237,8 @@
          form.value.deviceModel = data.deviceModel;
          form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD");
          form.value.repairName = data.repairName;
          form.value.maintenanceName = data.maintenanceName;
          form.value.maintenanceProject = data.maintenanceProject;
          form.value.remark = data.remark;
          repairStatusText.value =
            repairStatusOptions.value.find(item => item.value == data.status)
src/pages/equipmentManagement/repair/index.vue
@@ -58,14 +58,18 @@
              <text class="detail-value">{{ item.repairName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修人</text>
              <text class="detail-value">{{ item.maintenanceName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修项目</text>
              <text class="detail-value">{{ item.maintenanceProject || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修结果</text>
              <text class="detail-value">{{ item.maintenanceResult || '-' }}</text>
            </view>
src/pages/equipmentManagement/upkeep/add.vue
@@ -41,6 +41,22 @@
                </template>
            </u-form-item>
            
            <u-form-item label="保养人" prop="maintenancePerson" border-bottom>
                <u-input
                    v-model="form.maintenancePerson"
                    placeholder="请输入保养人"
                    clearable
                />
            </u-form-item>
            <u-form-item label="保养项目" prop="maintenanceProject" border-bottom>
                <u-input
                    v-model="form.maintenanceProject"
                    placeholder="请输入保养项目"
                    clearable
                />
            </u-form-item>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <u-button class="cancel-btn" @click="goBack">取消</u-button>
@@ -122,6 +138,8 @@
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // è®¡åˆ’保养日期
    maintenancePerson: undefined, // ä¿å…»äºº
    maintenanceProject: undefined, // ä¿å…»é¡¹ç›®
});
// åŠ è½½è®¾å¤‡åˆ—è¡¨
@@ -144,6 +162,8 @@
                form.value.deviceLedgerId = data.deviceLedgerId;
                form.value.deviceModel = data.deviceModel;
                form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
            form.value.maintenancePerson = data.maintenancePerson;
            form.value.maintenanceProject = data.maintenanceProject;
                // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
                const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
                if (device) {
src/pages/equipmentManagement/upkeep/index.vue
@@ -63,6 +63,14 @@
              <text class="detail-value">{{ formatDateTime(item.createTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养人</text>
              <text class="detail-value">{{ item.maintenancePerson || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养项目</text>
              <text class="detail-value">{{ item.maintenanceProject || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">实际保养人</text>
              <text class="detail-value">{{ item.maintenanceActuallyName || '-' }}</text>
            </view>
src/pages/inspectionUpload/attachment.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,460 @@
<template>
  <view class="attachment-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <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 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>
                <text class="video-text">视频</text>
              </view>
            </view>
            <view class="attachment-info">
              <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件' }}</text>
              <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
            </view>
          </view>
        </view>
        <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 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>
        </view>
        <view class="video-modal-body">
          <video
            v-if="currentVideoFile"
            :src="currentVideoFile.url || currentVideoFile.downloadUrl"
            class="video-player"
            controls
            autoplay
            @error="handleVideoError"
          ></video>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
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 attachmentList = ref([]);
// å½“前查看类型
const currentViewType = ref('before'); // 'before', 'after', 'issue'
// è§†é¢‘预览相关状态
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
// æ–‡ä»¶è®¿é—®åŸºç¡€åŸŸ
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'
      });
    }
  }
});
// åŠ è½½é™„ä»¶æ•°æ®
const loadAttachments = () => {
  const task = taskInfo.value;
  if (!task) return;
  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);
  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,
    };
  };
  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}`;
    // 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 `${filePreviewBase}/${url.replace(/^\//, '')}`;
  } catch (e) {
    return rawUrl || '';
  }
};
// è¿”回上一页
const goBack = () => {
  uni.navigateBack();
};
// åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
const switchViewType = (type) => {
  currentViewType.value = type;
};
// æ ¹æ®type获取对应分类的附件
const getAttachmentsByType = (typeValue) => {
  return attachmentList.value.filter((file) => file.type === typeValue) || [];
};
// èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
const getCurrentViewAttachments = () => {
  switch (currentViewType.value) {
    case 'before':
      return getAttachmentsByType(0);
    case 'after':
      return getAttachmentsByType(1);
    case 'issue':
      return getAttachmentsByType(2);
    default:
      return [];
  }
};
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
const isImageFile = (file) => {
  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 previewAttachment = (file) => {
  if (isImageFile(file)) {
    const imageUrls = getCurrentViewAttachments()
      .filter((f) => isImageFile(f))
      .map((f) => f.url || f.downloadUrl);
    uni.previewImage({
      urls: imageUrls,
      current: file.url || file.downloadUrl,
    });
  } else {
    showVideoPreview(file);
  }
};
// æ˜¾ç¤ºè§†é¢‘预览
const showVideoPreview = (file) => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
// å…³é—­è§†é¢‘预览
const closeVideoPreview = () => {
  showVideoDialog.value = false;
  currentVideoFile.value = null;
};
// è§†é¢‘播放错误处理
const handleVideoError = () => {
  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';
};
</script>
<style scoped>
.attachment-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
.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);
}
.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;
}
/* é™„件列表样式 */
.attachment-list-container {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  min-height: 400px;
}
.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:active {
  transform: scale(0.98);
}
.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-video-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.video-text {
  font-size: 12px;
  color: #666;
}
.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-size {
  font-size: 10px;
  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;
}
/* è§†é¢‘弹窗样式 */
.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-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;
}
.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-player {
  width: 100%;
  height: 400px;
  border-radius: 8px;
}
</style>
src/pages/inspectionUpload/components/formDia.vue
@@ -8,10 +8,49 @@
  >
    <view class="popup-content">
      <view class="popup-header">
        <text class="popup-title">上传</text>
        <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>
                <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>
                <text class="option-text">存在异常</text>
              </view>
            </view>
          </view>
        </view>
        <!-- å¼‚常描述(仅在异常时显示) -->
        <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' }"
          />
        </view>
        <!-- ä¸Šä¼ åŒºåŸŸï¼ˆä»…在异常时显示) -->
        <template v-if="hasException === true">
        <view class="form-container">
          <view class="title">生产前</view>
          <u-upload
@@ -56,6 +95,13 @@
            :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>
          <text class="tip-text">设备运行正常,无需上传照片</text>
        </view>
      </view>
      
      <view class="popup-footer">
@@ -79,6 +125,11 @@
const afterModelValue = ref([])
const issueModelValue = ref([])
const infoData = ref(null)
// å¼‚常状态:null=未选择, false=正常, true=异常
const hasException = ref(null)
// å¼‚常描述
const exceptionDescription = ref('')
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
@@ -196,9 +247,43 @@
  }
}
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å·¡æ£€çŠ¶æ€
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择巡检状态',
        icon: 'none'
      })
      return
    }
    // å¦‚果是异常状态,检查是否有上传文件
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length
      if (totalFiles === 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 })))
@@ -212,6 +297,8 @@
    
    // æäº¤æ•°æ®
    infoData.value.storageBlobDTO = arr
    infoData.value.hasException = hasException.value
    infoData.value.exceptionDescription = exceptionDescription.value
    await submitInspectionRecord({ ...infoData.value })
    
    uni.showToast({
@@ -238,6 +325,8 @@
  beforeModelValue.value = []
  afterModelValue.value = []
  issueModelValue.value = []
  hasException.value = null
  exceptionDescription.value = ''
}
// å…³é—­å¼¹æ¡†
@@ -311,4 +400,61 @@
  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
@@ -56,6 +56,10 @@
              <text class="detail-value">{{ item.taskId || item.id }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">巡检项目</text>
              <text class="detail-value">{{ item.inspectionProject || '无' }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remarks || '无' }}</text>
            </view>
@@ -95,227 +99,6 @@
      <view v-if="taskTableData?.length === 0"
            class="no-data">
        <text>暂无数据</text>
      </view>
    </view>
    <!-- å›¾ç‰‡ä¸Šä¼ å¼¹çª— - åŽŸç”Ÿå®žçŽ° -->
    <view v-if="showUploadDialog"
          class="custom-modal-overlay"
          @click="closeUploadDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="upload-popup-content">
          <view class="upload-popup-header">
            <text class="upload-popup-title">上传巡检记录</text>
          </view>
          <view class="upload-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <view class="upload-tabs">
              <view class="tab-item"
                    :class="{ active: currentUploadType === 'before' }"
                    @click="switchUploadType('before')">
                ç”Ÿäº§å‰
              </view>
              <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="exception-section">
              <text class="section-title">是否存在异常?</text>
              <view class="exception-options">
                <view class="exception-option"
                      :class="{ active: hasException === false }"
                      @click="setExceptionStatus(false)">
                  <u-icon name="checkmark-circle"
                          size="20"
                          color="#52c41a"></u-icon>
                  <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>
                  <text>存在异常</text>
                </view>
              </view>
            </view>
            <!-- å½“前分类的上传区域 -->
            <view class="simple-upload-area">
              <view class="upload-buttons">
                <u-button type="primary"
                          @click="chooseMedia('image')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ marginRight: '10px', flex: 1 }">
                  <u-icon name="camera"
                          size="18"
                          color="#fff"
                          style="margin-right: 5px;"></u-icon>
                  {{ uploading ? '上传中...' : '拍照' }}
                </u-button>
                <u-button type="success"
                          @click="chooseMedia('video')"
                          :loading="uploading"
                          :disabled="getCurrentFiles().length >= uploadConfig.limit"
                          :customStyle="{ flex: 1 }">
                  <uni-icons type="videocam"
                             name="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px;"></uni-icons>
                  {{ 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="getCurrentFiles().length > 0"
                    class="file-list">
                <view v-for="(file, index) in getCurrentFiles()"
                      :key="index"
                      class="file-item">
                  <view class="file-preview-container">
                    <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                           :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                           class="file-preview"
                           mode="aspectFill" />
                    <view v-else-if="file.type === 'video'"
                          class="video-preview">
                      <uni-icons type="videocam"
                                 name="videocam"
                                 size="18"
                                 color="#fff"
                                 style="margin-right: 5px;"></uni-icons>
                      <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="getCurrentFiles().length === 0"
                    class="empty-state">
                <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
              </view>
              <!-- ç»Ÿè®¡ä¿¡æ¯ -->
              <view class="upload-summary">
                <text class="summary-text">
                  ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
                  ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
                  ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
                </text>
              </view>
            </view>
          </view>
          <view class="upload-popup-footer">
            <u-button @click="closeUploadDialog"
                      :customStyle="{ marginRight: '10px' }">取消</u-button>
            <u-button v-if="hasException === true"
                      type="warning"
                      @click="goToRepair"
                      :customStyle="{ marginRight: '10px' }">
              æ–°å¢žæŠ¥ä¿®
            </u-button>
            <u-button type="primary"
                      @click="submitUpload">提交</u-button>
          </view>
        </view>
      </view>
    </view>
    <!-- æŸ¥çœ‹é™„件弹窗 -->
    <view v-if="showAttachmentDialog"
          class="custom-modal-overlay"
          @click="closeAttachmentDialog">
      <view class="custom-modal-container"
            @click.stop>
        <view class="attachment-popup-content">
          <view class="attachment-popup-header">
            <text class="attachment-popup-title">查看附件 - {{ currentViewTask?.taskName }}</text>
            <view class="close-btn-attachment"
                  @click="closeAttachmentDialog">
              <u-icon name="close"
                      size="16"
                      color="#666"></u-icon>
            </view>
          </view>
          <view class="attachment-popup-body">
            <!-- åˆ†ç±»æ ‡ç­¾é¡µ -->
            <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-content">
              <view v-if="getCurrentViewAttachments().length > 0"
                    class="attachment-list">
                <view v-for="(file, index) in getCurrentViewAttachments()"
                      :key="index"
                      class="attachment-item"
                      @click="previewAttachment(file)">
                  <view class="attachment-preview-container">
                    <image v-if="file.type === 'image' || isImageFile(file)"
                           :src="file.url || file.downloadUrl"
                           class="attachment-preview"
                           mode="aspectFill" />
                    <view v-else
                          class="attachment-video-preview">
                      <u-icon name="video"
                              size="24"
                              color="#409eff"></u-icon>
                      <text class="video-text">视频</text>
                    </view>
                  </view>
                  <view class="attachment-info">
                    <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '附件'
                      }}</text>
                    <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
                  </view>
                </view>
              </view>
              <view v-else
                    class="attachment-empty">
                <text>该分类暂无附件</text>
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- è§†é¢‘预览弹窗 -->
@@ -378,57 +161,9 @@
  const currentScanningTask = ref(null);
  const infoData = ref(null);
  // ä¸Šä¼ ç›¸å…³çŠ¶æ€
  const showUploadDialog = ref(false);
  const uploadFiles = ref([]); // ä¿ç•™ç”¨äºŽå…¼å®¹æ€§
  const uploadStatusType = ref(0);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  const number = ref(0);
  const uploadList = ref([]);
  // ä¸‰ä¸ªåˆ†ç±»çš„上传状态
  const beforeModelValue = ref([]); // ç”Ÿäº§å‰
  const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
  const issueModelValue = ref([]); // ç”Ÿäº§åŽ
  // å½“前激活的上传类型
  const currentUploadType = ref("before"); // 'before', 'after', 'issue'
  // æŸ¥çœ‹é™„件相关状态
  const showAttachmentDialog = ref(false);
  const currentViewTask = ref(null);
  const currentViewType = ref("before"); // 'before', 'after', 'issue'
  const attachmentList = ref([]); // å½“前查看任务的附件列表
  // è§†é¢‘预览相关状态
  const showVideoDialog = ref(false);
  const currentVideoFile = ref(null);
  // å¼‚常状态
  const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
  // ä¸Šä¼ é…ç½®
  const uploadConfig = {
    action: "/file/upload",
    limit: 10,
    fileSize: 50, // MB
    fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
    maxVideoDuration: 60, // ç§’
  };
  // è®¡ç®—上传URL
  const uploadFileUrl = computed(() => {
    const baseUrl = config.baseUrl;
    return baseUrl + uploadConfig.action;
  });
  // è®¡ç®—请求头
  const headers = computed(() => {
    const token = getToken();
    return token ? { Authorization: "Bearer " + token } : {};
  });
  // è¯·æ±‚取消标志,用于取消正在进行的请求
  let isRequestCancelled = false;
@@ -489,11 +224,6 @@
  onUnmounted(() => {
    // è®¾ç½®å–消标志,阻止后续的异步操作
    isRequestCancelled = true;
    // å…³é—­ä¸Šä¼ å¼¹çª—
    if (showUploadDialog.value) {
      showUploadDialog.value = false;
    }
  });
  // è¿”回上一页
@@ -653,322 +383,27 @@
    }
  };
  // æ‰“开上传弹窗
  // æ‰“开上传页面
  const openUploadDialog = task => {
    // è®¾ç½®ä»»åŠ¡ä¿¡æ¯åˆ°infoData
    if (task) {
      infoData.value = {
        ...task,
        taskId: task.taskId || task.id,
        storageBlobDTO: [], // åˆå§‹åŒ–文件列表
      };
    }
    // è®¾ç½®ä¸Šä¼ çŠ¶æ€ç±»åž‹ï¼ˆå¯ä»¥æ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ä¸åŒçš„çŠ¶æ€ï¼‰
    uploadStatusType.value = 0; // é»˜è®¤çŠ¶æ€
    // æ¸…空之前的文件
    uploadFiles.value = [];
    // æ˜¾ç¤ºä¸Šä¼ å¼¹çª—
    showUploadDialog.value = true;
  };
  // å…³é—­ä¸Šä¼ å¼¹çª—
  const closeUploadDialog = () => {
    showUploadDialog.value = false;
    uploadFiles.value = [];
    // æ¸…理三个分类的数据
    beforeModelValue.value = [];
    afterModelValue.value = [];
    issueModelValue.value = [];
    currentUploadType.value = "before";
    hasException.value = null; // é‡ç½®å¼‚常状态
    infoData.value = null; // æ¸…理任务数据
  };
  // åˆ‡æ¢ä¸Šä¼ ç±»åž‹
  const switchUploadType = type => {
    currentUploadType.value = type;
  };
  // èŽ·å–å½“å‰åˆ†ç±»çš„æ–‡ä»¶åˆ—è¡¨
  const getCurrentFiles = () => {
    switch (currentUploadType.value) {
      case "before":
        return beforeModelValue.value || [];
      case "after":
        return afterModelValue.value || [];
      case "issue":
        return issueModelValue.value || [];
      default:
        return [];
    }
  };
  // èŽ·å–ä¸Šä¼ ç±»åž‹æ–‡æœ¬
  const getUploadTypeText = () => {
    switch (currentUploadType.value) {
      case "before":
        return "生产前";
      case "after":
        return "生产中";
      case "issue":
        return "生产后";
      default:
        return "";
    }
  };
  // å¤„理上传文件更新
  const handleUploadUpdate = files => {
    uploadFiles.value = files;
  };
  // è®¾ç½®å¼‚常状态
  const setExceptionStatus = status => {
    hasException.value = status;
  };
  // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
  const goToRepair = () => {
    try {
      // å­˜å‚¨å½“前任务信息到本地存储,供报修页面使用
      const taskInfo = {
        taskId: infoData.value?.taskId || infoData.value?.id,
        taskName: infoData.value?.taskName,
        inspectionLocation: infoData.value?.inspectionLocation,
        inspector: infoData.value?.inspector,
        // ä¼ é€’当前上传的文件信息
        uploadedFiles: {
          before: beforeModelValue.value,
          after: afterModelValue.value,
          issue: issueModelValue.value,
        },
      };
      uni.setStorageSync("repairTaskInfo", JSON.stringify(taskInfo));
      // è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
    // å°†ä»»åŠ¡ä¿¡æ¯ä¼ é€’åˆ°ä¸Šä¼ é¡µé¢
    const taskData = encodeURIComponent(JSON.stringify(task));
      uni.navigateTo({
        url: "/pages/equipmentManagement/repair/add",
      url: `/pages/inspectionUpload/upload?taskInfo=${taskData}`,
      });
      // å…³é—­ä¸Šä¼ å¼¹çª—
      closeUploadDialog();
    } catch (error) {
      console.error("跳转报修页面失败:", error);
      uni.showToast({
        title: "跳转失败,请重试",
        icon: "error",
      });
    }
  };
  // æäº¤ä¸Šä¼ 
  const submitUpload = async () => {
    try {
      // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
      if (hasException.value === null) {
        uni.showToast({
          title: "请选择是否存在异常",
          icon: "none",
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•文件上传
      const totalFiles =
        beforeModelValue.value.length +
        afterModelValue.value.length +
        issueModelValue.value.length;
      if (totalFiles === 0) {
        uni.showToast({
          title: "请先上传文件",
          icon: "none",
        });
        return;
      }
      // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
      showLoadingToast("提交中...");
      // æŒ‰ç…§æ‚¨çš„逻辑合并所有分类的文件
      let arr = [];
      if (beforeModelValue.value.length > 0) {
        arr.push(...beforeModelValue.value);
      }
      if (afterModelValue.value.length > 0) {
        arr.push(...afterModelValue.value);
      }
      if (issueModelValue.value.length > 0) {
        arr.push(...issueModelValue.value);
      }
      // ä¼ ç»™åŽç«¯çš„临时文件ID列表(tempFileIds)
      // å…¼å®¹ï¼šæœ‰äº›æŽ¥å£å¯èƒ½è¿”回 tempId / tempFileId / id
      let tempFileIds = [];
      if (arr !== null && arr.length > 0) {
        tempFileIds = arr
          .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
          .filter(v => v !== undefined && v !== null && v !== "");
      }
      // æäº¤æ•°æ®
      infoData.value.storageBlobDTO = arr;
      // æ·»åŠ å¼‚å¸¸çŠ¶æ€ä¿¡æ¯
      infoData.value.hasException = hasException.value;
      infoData.value.tempFileIds = tempFileIds;
      const result = await uploadInspectionTask({ ...infoData.value });
      // æ£€æŸ¥æäº¤ç»“æžœ
      if (result && (result.code === 200 || result.success)) {
        // æäº¤æˆåŠŸ
        closeToast(); // å…³é—­åŠ è½½æç¤º
        uni.showToast({
          title: "提交成功",
          icon: "success",
        });
        // å…³é—­å¼¹çª—
        closeUploadDialog();
        // åˆ·æ–°åˆ—表
        setTimeout(() => {
          reloadPage();
        }, 500);
      } else {
        // æäº¤å¤±è´¥
        closeToast();
        uni.showToast({
          title: result?.msg || result?.message || "提交失败",
          icon: "error",
        });
      }
    } catch (error) {
      console.error("提交上传失败:", error);
      closeToast(); // å…³é—­åŠ è½½æç¤º
      let errorMessage = "提交失败";
      if (error.message) {
        errorMessage = error.message;
      } else if (error.msg) {
        errorMessage = error.msg;
      } else if (typeof error === "string") {
        errorMessage = error;
      }
      uni.showToast({
        title: errorMessage,
        icon: "error",
      });
    }
  };
  // å›¾ç‰‡ä¸Šä¼ (可选择图片上传或者是相机拍照)
  const startUploadForTask = async (task, type) => {
    // ç›´æŽ¥æ‰“开上传弹窗
    // æ‰“开上传页面
    openUploadDialog(task);
  };
  // æŸ¥çœ‹é™„ä»¶
  // æŸ¥çœ‹é™„ä»¶ - è·³è½¬åˆ°é™„件页面
  const viewAttachments = async task => {
    try {
      currentViewTask.value = task;
      currentViewType.value = "before";
      // è§£æžæ–°çš„æ•°æ®ç»“æž„
      attachmentList.value = [];
      // åŽç«¯åæ˜¾å­—段(你提供的数据结构):
      // - commonFileListBefore:生产前(通常 type=10)
      // - commonFileListAfter:生产中(通常 type=11)
      // - commonFileList:可能是全部/兜底(若包含生产后,一般 type=12)
      const allList = Array.isArray(task?.commonFileList)
        ? task.commonFileList
        : [];
      const beforeList = Array.isArray(task?.commonFileListBefore)
        ? task.commonFileListBefore
        : allList.filter(f => f?.type === 10);
      const afterList = Array.isArray(task?.commonFileListAfter)
        ? task.commonFileListAfter
        : allList.filter(f => f?.type === 11);
      // å¦‚果后端后续补了 commonFileListIssue,则优先用;否则从 commonFileList é‡ŒæŒ‰ type=12 å…œåº•
      const issueList = Array.isArray(task?.commonFileListIssue)
        ? task.commonFileListIssue
        : allList.filter(f => f?.type === 12);
      const mapToViewFile = (file, viewType) => {
        const u = normalizeFileUrl(file?.url || file?.downloadUrl || "");
        return {
          ...file,
          // ç”¨äºŽä¸‰æ ‡ç­¾é¡µåˆ†ç»„:0=生产前 1=生产中 2=生产后
          type: viewType,
          name: file?.name || file?.originalFilename || file?.bucketFilename,
          bucketFilename: file?.bucketFilename || file?.name,
          originalFilename: file?.originalFilename || file?.name,
          url: u,
          downloadUrl: u,
          size: file?.size || file?.byteSize,
        };
      };
      attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0)));
      attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1)));
      attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2)));
      showAttachmentDialog.value = true;
    } catch (error) {
      uni.showToast({
        title: "获取附件失败",
        icon: "error",
    const taskData = encodeURIComponent(JSON.stringify(task));
    uni.navigateTo({
      url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`,
      });
    }
  };
  // å…³é—­é™„件查看弹窗
  const closeAttachmentDialog = () => {
    showAttachmentDialog.value = false;
    currentViewTask.value = null;
    attachmentList.value = [];
    currentViewType.value = "before";
  };
  // åˆ‡æ¢æŸ¥çœ‹ç±»åž‹
  const switchViewType = type => {
    currentViewType.value = type;
  };
  // æ ¹æ®type获取对应分类的附件
  const getAttachmentsByType = typeValue => {
    return attachmentList.value.filter(file => file.type === typeValue) || [];
  };
  // èŽ·å–type值
  const getTabType = () => {
    switch (currentUploadType.value) {
      case "before":
        return 10;
      case "after":
        return 11;
      case "issue":
        return 12;
      default:
        return 10;
    }
  };
  // èŽ·å–å½“å‰æŸ¥çœ‹ç±»åž‹çš„é™„ä»¶
  const getCurrentViewAttachments = () => {
    switch (currentViewType.value) {
      case "before":
        return getAttachmentsByType(0);
      case "after":
        return getAttachmentsByType(1);
      case "issue":
        return getAttachmentsByType(2);
      default:
        return [];
    }
  };
  // åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡æ–‡ä»¶
@@ -1060,474 +495,7 @@
    });
  };
  // æ‹ç…§/拍视频(真机优先用 chooseMedia;不支持则降级)
  const chooseMedia = type => {
    if (getCurrentFiles().length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能选择${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    const remaining = uploadConfig.limit - getCurrentFiles().length;
    // ä¼˜å…ˆï¼šchooseMedia(支持 image/video)
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
                uid: Date.now() + Math.random() + idx,
              };
              handleBeforeUpload(file);
            });
          } catch (e) {
            console.error("处理拍摄结果失败:", e);
            uni.showToast({ title: "处理文件失败", icon: "error" });
          }
        },
        fail: err => {
          console.error("拍摄失败:", err);
          uni.showToast({ title: "拍摄失败", icon: "error" });
        },
      });
      return;
    }
    // é™çº§ï¼šchooseImage / chooseVideo
    if (type === "video") {
      chooseVideo();
    } else {
      uni.chooseImage({
        count: 1,
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          const tempFilePath = res?.tempFilePaths?.[0];
          const tempFile = res?.tempFiles?.[0] || {};
          if (!tempFilePath) return;
          handleBeforeUpload({
            tempFilePath,
            path: tempFilePath,
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: Date.now(),
            uid: Date.now() + Math.random(),
          });
        },
      });
    }
  };
  // æ‹ç…§
  const chooseImage = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseMedia({
      count: 1,
      mediaType: ["image", "video"],
      sizeType: ["compressed", "original"],
      sourceType: ["camera"],
      success: res => {
        try {
          if (!res.tempFiles || res.tempFiles.length === 0) {
            throw new Error("未获取到图片文件");
          }
          const tempFilePath = res.tempFiles[0];
          const tempFile =
            res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
          const file = {
            tempFilePath: tempFilePath,
            path: tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "image",
            name: `photo_${Date.now()}.jpg`,
            size: tempFile.size || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍照结果失败:", error);
          uni.showToast({
            title: "处理图片失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍照失败:", err);
        uni.showToast({
          title: "拍照失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // æ‹è§†é¢‘
  const chooseVideo = () => {
    if (uploadFiles.value.length >= uploadConfig.limit) {
      uni.showToast({
        title: `最多只能拍摄${uploadConfig.limit}个文件`,
        icon: "none",
      });
      return;
    }
    uni.chooseVideo({
      sourceType: ["camera"],
      maxDuration: uploadConfig.maxVideoDuration,
      camera: "back",
      success: res => {
        try {
          if (!res.tempFilePath) {
            throw new Error("未获取到视频文件");
          }
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath, // ä¿æŒå…¼å®¹æ€§
            type: "video",
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: new Date().getTime(),
            uid: Date.now() + Math.random(),
          };
          handleBeforeUpload(file);
        } catch (error) {
          console.error("处理拍视频结果失败:", error);
          uni.showToast({
            title: "处理视频失败",
            icon: "error",
          });
        }
      },
      fail: err => {
        console.error("拍视频失败:", err);
        uni.showToast({
          title: "拍视频失败: " + (err.errMsg || "未知错误"),
          icon: "error",
        });
      },
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    uni.showModal({
      title: "确认删除",
      content: "确定要删除这个文件吗?",
      success: res => {
        if (res.confirm) {
          // æ ¹æ®å½“前上传类型删除对应分类的文件
          switch (currentUploadType.value) {
            case "before":
              beforeModelValue.value.splice(index, 1);
              break;
            case "after":
              afterModelValue.value.splice(index, 1);
              break;
            case "issue":
              issueModelValue.value.splice(index, 1);
              break;
          }
          uni.showToast({
            title: "删除成功",
            icon: "success",
          });
        }
      },
    });
  };
  // æ£€æŸ¥ç½‘络连接
  const checkNetworkConnection = () => {
    return new Promise(resolve => {
      uni.getNetworkType({
        success: res => {
          if (res.networkType === "none") {
            resolve(false);
          } else {
            resolve(true);
          }
        },
        fail: () => {
          resolve(false);
        },
      });
    });
  };
  // ä¸Šä¼ å‰æ ¡éªŒ
  const handleBeforeUpload = async file => {
    // æ ¡éªŒæ–‡ä»¶ç±»åž‹
    if (
      uploadConfig.fileType &&
      Array.isArray(uploadConfig.fileType) &&
      uploadConfig.fileType.length > 0
    ) {
      const fileName = file.name || "";
      const fileExtension = fileName
        ? fileName.split(".").pop().toLowerCase()
        : "";
      // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
      let expectedTypes = [];
      if (file.type === "image") {
        expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
      } else if (file.type === "video") {
        expectedTypes = ["mp4", "mov", "avi", "wmv"];
      }
      // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
      if (fileExtension && expectedTypes.length > 0) {
        const isAllowed = expectedTypes.some(
          type => uploadConfig.fileType.includes(type) && type === fileExtension
        );
        if (!isAllowed) {
          uni.showToast({
            title: `文件格式不支持,请拍摄 ${expectedTypes.join("/")} æ ¼å¼çš„æ–‡ä»¶`,
            icon: "none",
          });
          return false;
        }
      }
    }
    // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
    uploadFile(file);
    return true;
  };
  // æ–‡ä»¶ä¸Šä¼ å¤„理(真机走 uni.uploadFile)
  const uploadFile = async file => {
    uploading.value = true;
    uploadProgress.value = 0;
    number.value++; // å¢žåŠ ä¸Šä¼ è®¡æ•°
    // ç¡®ä¿token存在
    const token = getToken();
    if (!token) {
      handleUploadError("用户未登录");
      return;
    }
    const typeValue = getTabType(); // ç”Ÿäº§å‰:10, ç”Ÿäº§ä¸­:11, ç”Ÿäº§åŽ:12
    uploadWithUniUploadFile(
      file,
      file.tempFilePath || file.path || "",
      typeValue,
      token
    );
  };
  // ä½¿ç”¨uni.uploadFile上传(非H5环境或H5回退方案)
  const uploadWithUniUploadFile = (file, filePath, typeValue, token) => {
    if (!filePath) {
      handleUploadError("文件路径不存在");
      return;
    }
    const uploadTask = uni.uploadFile({
      url: uploadFileUrl.value,
      filePath: filePath,
      name: "file",
      formData: {
        type: typeValue,
      },
      header: {
        Authorization: `Bearer ${token}`,
      },
      success: res => {
        try {
          if (res.statusCode === 200) {
            const response = JSON.parse(res.data);
            if (response.code === 200) {
              handleUploadSuccess(response, file);
              uni.showToast({
                title: "上传成功",
                icon: "success",
              });
            } else {
              handleUploadError(response.msg || "服务器返回错误");
            }
          } else {
            handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
          }
        } catch (e) {
          console.error("解析响应失败:", e);
          console.error("原始响应数据:", res.data);
          handleUploadError("响应数据解析失败: " + e.message);
        }
      },
      fail: err => {
        console.error("上传失败:", err.errMsg || err);
        number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
        let errorMessage = "上传失败";
        if (err.errMsg) {
          if (err.errMsg.includes("statusCode: null")) {
            errorMessage = "网络连接失败,请检查网络设置";
          } else if (err.errMsg.includes("timeout")) {
            errorMessage = "上传超时,请重试";
          } else if (err.errMsg.includes("fail")) {
            errorMessage = "上传失败,请检查网络连接";
          } else {
            errorMessage = err.errMsg;
          }
        }
        handleUploadError(errorMessage);
      },
      complete: () => {
        uploading.value = false;
        uploadProgress.value = 0;
      },
    });
    // ç›‘听上传进度
    if (uploadTask && uploadTask.onProgressUpdate) {
      uploadTask.onProgressUpdate(res => {
        uploadProgress.value = res.progress;
      });
    }
  };
  // ä¸Šä¼ å¤±è´¥å¤„理
  const handleUploadError = (message = "上传文件失败", showRetry = false) => {
    uploading.value = false;
    uploadProgress.value = 0;
    if (showRetry) {
      uni.showModal({
        title: "上传失败",
        content: message + ",是否重试?",
        success: res => {
          if (res.confirm) {
            // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
          }
        },
      });
    } else {
      uni.showToast({
        title: message,
        icon: "error",
      });
    }
  };
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  const handleUploadSuccess = (res, file) => {
    console.log("上传成功响应:", res);
    // å¤„理不同的数据结构:可能是数组,也可能是单个对象
    let uploadedFile = null;
    uploadedFile = res.data;
    if (!uploadedFile) {
      console.error("无法解析上传响应数据:", res);
      number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
      handleUploadError("上传响应数据格式错误", false);
      return;
    }
    // æ ¹æ®å½“前上传类型设置type字段
    let typeValue = 0; // é»˜è®¤ä¸ºç”Ÿäº§å‰
    switch (currentUploadType.value) {
      case "before":
        typeValue = 0;
        break;
      case "after":
        typeValue = 1;
        break;
      case "issue":
        typeValue = 2;
        break;
    }
    // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id和type
    const fileData = {
      ...file,
      id: uploadedFile.id, // æ·»åŠ æœåŠ¡å™¨è¿”å›žçš„id
      tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
      url:
        uploadedFile.url ||
        uploadedFile.downloadUrl ||
        file.tempFilePath ||
        file.path,
      bucketFilename:
        uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name,
      downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
      size: uploadedFile.size || uploadedFile.byteSize || file.size,
      createTime: uploadedFile.createTime || new Date().getTime(),
      type: typeValue, // æ·»åŠ ç±»åž‹å­—æ®µï¼š0=生产前, 1=生产中, 2=生产后
    };
    uploadList.value.push(fileData);
    // ç«‹å³æ·»åŠ åˆ°å¯¹åº”çš„åˆ†ç±»ï¼Œä¸ç­‰å¾…æ‰€æœ‰æ–‡ä»¶ä¸Šä¼ å®Œæˆ
    switch (currentUploadType.value) {
      case "before":
        beforeModelValue.value.push(fileData);
        break;
      case "after":
        afterModelValue.value.push(fileData);
        break;
      case "issue":
        issueModelValue.value.push(fileData);
        break;
    }
    // é‡ç½®ä¸Šä¼ åˆ—表(因为已经添加到对应分类了)
    uploadList.value = [];
    number.value = 0;
  };
  // ä¸Šä¼ ç»“束处理(已废弃,现在在handleUploadSuccess中直接处理)
  const uploadedSuccessfully = () => {
    // æ­¤å‡½æ•°å·²ä¸å†ä½¿ç”¨ï¼Œæ–‡ä»¶ä¸Šä¼ æˆåŠŸåŽç«‹å³æ·»åŠ åˆ°å¯¹åº”åˆ†ç±»
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (!size) return "";
    if (size < 1024) return size + "B";
    if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
    return (size / (1024 * 1024)).toFixed(1) + "MB";
  };
</script>
<style scoped>
@@ -1725,416 +693,6 @@
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* ä¸Šä¼ å¼¹çª—样式 */
  .upload-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 300px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
  .upload-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
  }
  .upload-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .upload-popup-body {
    flex: 1;
    padding: 20px;
    overflow-y: auto;
  }
  .upload-popup-footer {
    display: flex;
    justify-content: flex-end;
    padding: 15px 20px;
    border-top: 1px solid #eee;
    gap: 10px;
  }
  /* ç®€åŒ–上传组件样式 */
  .simple-upload-area {
    padding: 15px;
  }
  .upload-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
  }
  .file-list {
    margin-top: 15px;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .file-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(50% - 6px);
    min-width: 120px;
  }
  .file-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .file-preview-container {
    position: relative;
    margin-bottom: 8px;
  }
  .file-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
  .video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
  .video-text {
    font-size: 12px;
    color: #666;
    margin-top: 4px;
  }
  .delete-btn {
    position: absolute;
    top: -6px;
    right: -6px;
    width: 20px;
    height: 20px;
    background: #ff4757;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
    transition: all 0.3s ease;
  }
  .delete-btn:hover {
    background: #ff3742;
    transform: scale(1.1);
  }
  .file-info {
    text-align: center;
    width: 100%;
  }
  .file-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100px;
  }
  .file-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
  .empty-state {
    text-align: center;
    padding: 40px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
  .upload-progress {
    margin: 15px 0;
    padding: 0 10px;
  }
  /* ä¸Šä¼ æ ‡ç­¾é¡µæ ·å¼ */
  .upload-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
  .tab-item {
    flex: 1;
    text-align: center;
    padding: 8px 12px;
    font-size: 14px;
    color: #666;
    border-radius: 6px;
    transition: all 0.3s ease;
    cursor: pointer;
  }
  .tab-item.active {
    background: #409eff;
    color: #fff;
    font-weight: 500;
  }
  .tab-item:hover:not(.active) {
    background: #e9ecef;
    color: #333;
  }
  /* å¼‚常状态选择样式 */
  .exception-section {
    margin-bottom: 20px;
    padding: 15px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #e9ecef;
  }
  .section-title {
    display: block;
    font-size: 14px;
    font-weight: 600;
    color: #333;
    margin-bottom: 12px;
  }
  .exception-options {
    display: flex;
    gap: 12px;
  }
  .exception-option {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 16px;
    background: #fff;
    border: 2px solid #e9ecef;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-size: 14px;
    color: #666;
  }
  .exception-option.active {
    border-color: #409eff;
    background: #f0f8ff;
    color: #409eff;
    font-weight: 500;
  }
  .exception-option:hover:not(.active) {
    border-color: #d9d9d9;
    background: #fafafa;
  }
  /* ç»Ÿè®¡ä¿¡æ¯æ ·å¼ */
  .upload-summary {
    margin-top: 15px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    border-left: 3px solid #409eff;
  }
  .summary-text {
    font-size: 12px;
    color: #666;
    line-height: 1.4;
  }
  /* æŸ¥çœ‹é™„件弹窗样式 */
  .attachment-popup-content {
    background: #fff;
    border-radius: 12px;
    width: 100%;
    min-height: 400px;
    max-height: 70vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  }
  .attachment-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid #eee;
    background: #f8f9fa;
  }
  .attachment-popup-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .close-btn-attachment {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #f5f5f5;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
  }
  .close-btn-attachment:hover {
    background: #e9ecef;
    transform: scale(1.1);
  }
  .attachment-popup-body {
    flex: 1;
    padding: 15px 20px;
    overflow-y: auto;
  }
  .attachment-tabs {
    display: flex;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 15px;
    padding: 4px;
  }
  .attachment-content {
    min-height: 200px;
  }
  .attachment-list {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .attachment-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    border-radius: 12px;
    padding: 8px;
    border: 1px solid #e9ecef;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    width: calc(33.33% - 8px);
    min-width: 100px;
    cursor: pointer;
  }
  .attachment-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .attachment-preview-container {
    margin-bottom: 8px;
  }
  .attachment-preview {
    width: 80px;
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid #f0f0f0;
  }
  .attachment-video-preview {
    width: 80px;
    height: 80px;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border-radius: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 2px solid #f0f0f0;
  }
  .attachment-info {
    text-align: center;
    width: 100%;
  }
  .attachment-name {
    font-size: 12px;
    color: #333;
    font-weight: 500;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 80px;
  }
  .attachment-size {
    font-size: 10px;
    color: #999;
    margin-top: 2px;
    display: block;
  }
  .attachment-empty {
    text-align: center;
    padding: 60px 20px;
    color: #999;
    font-size: 14px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 2px dashed #ddd;
  }
  /* è§†é¢‘预览弹窗样式 */
src/pages/inspectionUpload/upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,885 @@
<template>
  <view class="inspection-upload-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="上传巡检记录" @back="goBack" />
    <!-- é¡µé¢å†…容 -->
    <view class="upload-content">
      <!-- ä»»åŠ¡ä¿¡æ¯å¡ç‰‡ -->
      <view class="task-info-card" v-if="taskInfo">
        <view class="task-info-header">
          <text class="task-name">{{ taskInfo.taskName }}</text>
        </view>
        <view class="task-info-body">
          <view class="info-item">
            <text class="info-label">任务ID</text>
            <text class="info-value">{{ taskInfo.taskId || taskInfo.id }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">巡检位置</text>
            <text class="info-value">{{ taskInfo.inspectionLocation || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">执行人</text>
            <text class="info-value">{{ taskInfo.inspector || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- å¼‚常状态选择 -->
      <view class="section-card">
        <view class="section-title">巡检状态</view>
        <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>
            <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>
            <text class="option-text">存在异常</text>
          </view>
        </view>
      </view>
      <!-- å¼‚常描述(仅在异常时显示) -->
      <view class="section-card" v-if="hasException === true">
        <view class="section-title">异常描述</view>
        <textarea
          v-model="abnormalDescription"
          class="exception-textarea"
          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="upload-area">
          <view class="upload-buttons">
            <u-button
              type="primary"
              @click="chooseMedia('image')"
              :loading="uploading"
              :disabled="getCurrentFiles().length >= uploadConfig.limit"
              :customStyle="{ marginRight: '10px', flex: 1 }"
            >
              <u-icon name="camera" size="18" color="#fff" style="margin-right: 5px"></u-icon>
              {{ uploading ? '上传中...' : '拍照' }}
            </u-button>
            <u-button
              type="success"
              @click="chooseMedia('video')"
              :loading="uploading"
              :disabled="getCurrentFiles().length >= uploadConfig.limit"
              :customStyle="{ flex: 1 }"
            >
              <uni-icons type="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="getCurrentFiles().length > 0" class="file-list">
            <view v-for="(file, index) in getCurrentFiles()" :key="index" class="file-item">
              <view class="file-preview-container">
                <image
                  v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                  :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                  class="file-preview"
                  mode="aspectFill"
                />
                <view v-else-if="file.type === 'video'" class="video-preview">
                  <uni-icons type="videocam" 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="getCurrentFiles().length === 0" class="empty-state">
            <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text>
          </view>
        </view>
        <!-- ç»Ÿè®¡ä¿¡æ¯ -->
        <view class="upload-summary">
          <text class="summary-text">
            ç”Ÿäº§å‰: {{ beforeModelValue.length }}个文件 |
            ç”Ÿäº§ä¸­: {{ afterModelValue.length }}个文件 |
            ç”Ÿäº§åŽ: {{ issueModelValue.length }}个文件
          </text>
        </view>
      </view>
      <!-- æ­£å¸¸çŠ¶æ€æç¤º -->
      <view class="normal-tip-card" v-if="hasException === false">
        <u-icon name="info-circle" size="60" color="#52c41a"></u-icon>
        <text class="tip-text">设备运行正常,无需上传照片</text>
      </view>
    </view>
    <!-- åº•部按钮 -->
    <view class="footer-buttons">
      <u-button @click="goBack" :customStyle="{ marginRight: '10px' }">取消</u-button>
      <u-button v-if="hasException === true" type="warning" @click="goToRepair" :customStyle="{ marginRight: '10px' }">
        æ–°å¢žæŠ¥ä¿®
      </u-button>
      <u-button type="primary" @click="submitUpload">提交</u-button>
    </view>
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { uploadInspectionTask } from '@/api/inspectionManagement';
import { getToken } from '@/utils/auth';
import config from '@/config';
// ä»»åŠ¡ä¿¡æ¯
const taskInfo = ref(null);
// ä¸Šä¼ ç›¸å…³çŠ¶æ€
const uploading = ref(false);
const uploadProgress = ref(0);
// ä¸‰ä¸ªåˆ†ç±»çš„上传状态
const beforeModelValue = ref([]); // ç”Ÿäº§å‰
const afterModelValue = ref([]); // ç”Ÿäº§ä¸­
const issueModelValue = ref([]); // ç”Ÿäº§åŽ
// å½“前激活的上传类型
const currentUploadType = ref('before'); // 'before', 'after', 'issue'
// å¼‚常状态
const hasException = ref(null); // null: æœªé€‰æ‹©, true: å­˜åœ¨å¼‚常, false: æ­£å¸¸
// å¼‚常描述
const abnormalDescription = ref('');
// ä¸Šä¼ é…ç½®
const uploadConfig = {
  action: '/file/upload',
  limit: 10,
  fileSize: 50, // MB
  fileType: ['jpg', 'jpeg', 'png', 'mp4', 'mov'],
  maxVideoDuration: 60, // ç§’
};
// è®¡ç®—上传URL
const uploadFileUrl = computed(() => {
  const baseUrl = config.baseUrl;
  return baseUrl + uploadConfig.action;
});
// é¡µé¢åŠ è½½
onLoad((options) => {
  if (options.taskInfo) {
    try {
      taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
    } catch (e) {
      console.error('解析任务信息失败:', e);
    }
  }
});
// è¿”回上一页
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 '';
  }
};
// è®¾ç½®å¼‚常状态
const setExceptionStatus = (status) => {
  hasException.value = status;
};
// è·³è½¬åˆ°æ–°å¢žæŠ¥ä¿®é¡µé¢
const goToRepair = () => {
  try {
    const taskData = {
      taskId: taskInfo.value?.taskId || taskInfo.value?.id,
      taskName: taskInfo.value?.taskName,
      inspectionLocation: taskInfo.value?.inspectionLocation,
      inspector: taskInfo.value?.inspector,
      uploadedFiles: {
        before: beforeModelValue.value,
        after: afterModelValue.value,
        issue: issueModelValue.value,
      },
    };
    uni.setStorageSync('repairTaskInfo', JSON.stringify(taskData));
    uni.navigateTo({
      url: '/pages/equipmentManagement/repair/add',
    });
  } catch (error) {
    console.error('跳转报修页面失败:', error);
    uni.showToast({
      title: '跳转失败,请重试',
      icon: 'error',
    });
  }
};
// æäº¤ä¸Šä¼ 
const submitUpload = async () => {
  try {
    // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†å¼‚常状态
    if (hasException.value === null) {
      uni.showToast({
        title: '请选择巡检状态',
        icon: 'none',
      });
      return;
    }
    // å¦‚果是异常状态,检查是否有上传文件和描述
    if (hasException.value === true) {
      const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length;
      if (totalFiles === 0) {
        uni.showToast({
          title: '请上传异常照片',
          icon: 'none',
        });
        return;
      }
      // æ£€æŸ¥æ˜¯å¦å¡«å†™äº†å¼‚常描述
      if (!abnormalDescription.value.trim()) {
        uni.showToast({
          title: '请填写异常描述',
          icon: 'none',
        });
        return;
      }
    }
    // æ˜¾ç¤ºæäº¤ä¸­çš„加载提示
    uni.showLoading({
      title: '提交中...',
      mask: true,
    });
    // æŒ‰ç…§é€»è¾‘合并所有分类的文件
    let arr = [];
    if (beforeModelValue.value.length > 0) {
      arr.push(...beforeModelValue.value);
    }
    if (afterModelValue.value.length > 0) {
      arr.push(...afterModelValue.value);
    }
    if (issueModelValue.value.length > 0) {
      arr.push(...issueModelValue.value);
    }
    // ä¼ ç»™åŽç«¯çš„临时文件ID列表
    let tempFileIds = [];
    if (arr !== null && arr.length > 0) {
      tempFileIds = arr
        .map((item) => item?.tempId ?? item?.tempFileId ?? item?.id)
        .filter((v) => v !== undefined && v !== null && v !== '');
    }
    // æäº¤æ•°æ®
    const submitData = {
      ...taskInfo.value,
      storageBlobDTO: arr,
      hasException: hasException.value,
      abnormalDescription: abnormalDescription.value,
      tempFileIds: tempFileIds,
    };
    const result = await uploadInspectionTask(submitData);
    // æ£€æŸ¥æäº¤ç»“æžœ
    if (result && (result.code === 200 || result.success)) {
      uni.hideLoading();
      uni.showToast({
        title: '提交成功',
        icon: 'success',
      });
      // è¿”回列表页并刷新
      setTimeout(() => {
        uni.navigateBack();
      }, 500);
    } else {
      uni.hideLoading();
      uni.showToast({
        title: result?.msg || result?.message || '提交失败',
        icon: 'error',
      });
    }
  } catch (error) {
    console.error('提交上传失败:', error);
    uni.hideLoading();
    uni.showToast({
      title: error?.message || '提交失败',
      icon: 'error',
    });
  }
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size) => {
  if (!size) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB'];
  let index = 0;
  let fileSize = size;
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024;
    index++;
  }
  return `${fileSize.toFixed(2)} ${units[index]}`;
};
// æ‹ç…§/拍视频
const chooseMedia = (type) => {
  if (getCurrentFiles().length >= uploadConfig.limit) {
    uni.showToast({
      title: `最多只能选择${uploadConfig.limit}个文件`,
      icon: 'none',
    });
    return;
  }
  const remaining = uploadConfig.limit - getCurrentFiles().length;
  // ä¼˜å…ˆä½¿ç”¨ chooseMedia
  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(),
            };
            uploadFile(file);
          });
        } catch (err) {
          uni.showToast({ title: err.message || '处理文件失败', icon: 'none' });
        }
      },
      fail: (err) => {
        console.error('选择媒体失败:', err);
        uni.showToast({ title: '选择失败', icon: 'none' });
      },
    });
  } else {
    // é™çº§æ–¹æ¡ˆ
    if (type === 'video') {
      uni.chooseVideo({
        sourceType: ['camera'],
        success: (res) => {
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath,
            type: 'video',
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: Date.now(),
          };
          uploadFile(file);
        },
        fail: () => {
          uni.showToast({ title: '选择视频失败', icon: 'none' });
        },
      });
    } else {
      uni.chooseImage({
        count: Math.min(remaining, 9),
        sizeType: ['compressed'],
        sourceType: ['camera'],
        success: (res) => {
          const list = res.tempFilePaths || res.tempFiles || [];
          list.forEach((src, idx) => {
            const path = typeof src === 'string' ? src : src.path;
            const file = {
              tempFilePath: path,
              path: path,
              type: 'image',
              name: `image_${Date.now()}_${idx}.jpg`,
              size: 0,
              createTime: Date.now(),
            };
            uploadFile(file);
          });
        },
        fail: () => {
          uni.showToast({ title: '选择图片失败', icon: 'none' });
        },
      });
    }
  }
};
// ä¸Šä¼ å•个文件
const uploadFile = (file) => {
  const token = getToken();
  if (!token) {
    uni.showToast({ title: '用户未登录', icon: 'none' });
    return;
  }
  uploading.value = true;
  uploadProgress.value = 0;
  const uploadTask = uni.uploadFile({
    url: uploadFileUrl.value,
    filePath: file.tempFilePath,
    name: 'file',
    header: {
      Authorization: `Bearer ${token}`,
    },
    formData: {
      type: getTabType(),
    },
    success: (res) => {
      try {
        const data = JSON.parse(res.data);
        if (data.code === 200) {
          const uploadedFile = {
            ...file,
            url: data.data.url,
            tempId: data.data.tempId || data.data.id,
            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);
          }
          uni.showToast({ title: '上传成功', icon: 'success' });
        } else {
          uni.showToast({ title: data.msg || '上传失败', icon: 'none' });
        }
      } catch (e) {
        uni.showToast({ title: '解析响应失败', icon: 'none' });
      }
    },
    fail: (err) => {
      console.error('上传失败:', err);
      uni.showToast({ title: '上传失败', icon: 'none' });
    },
    complete: () => {
      uploading.value = false;
    },
  });
  // ç›‘听上传进度
  uploadTask.onProgressUpdate((res) => {
    uploadProgress.value = res.progress;
  });
};
// èŽ·å–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);
};
</script>
<style scoped>
.inspection-upload-page {
  min-height: 100vh;
  background-color: #f5f5f5;
  padding-bottom: 80px;
}
.upload-content {
  padding: 15px;
}
/* ä»»åŠ¡ä¿¡æ¯å¡ç‰‡ */
.task-info-card {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 15px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.task-info-header {
  margin-bottom: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid #f0f0f0;
}
.task-name {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.task-info-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.info-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.info-label {
  font-size: 13px;
  color: #999;
}
.info-value {
  font-size: 13px;
  color: #666;
}
/* é€šç”¨å¡ç‰‡æ ·å¼ */
.section-card {
  background: #fff;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 15px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.section-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
  margin-bottom: 12px;
}
/* å¼‚常状态选择 */
.exception-options {
  display: flex;
  gap: 12px;
}
.exception-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 16px;
  background: #f8f9fa;
  border: 2px solid #e9ecef;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
}
.exception-option.active {
  border-color: #409eff;
  background: #f0f8ff;
}
.option-text {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
/* å¼‚常描述 */
.exception-textarea {
  width: 100%;
  min-height: 100px;
  padding: 12px;
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 8px;
  font-size: 14px;
  color: #333;
  resize: none;
  box-sizing: border-box;
}
.exception-textarea:focus {
  outline: none;
  border-color: #409eff;
  background: #fff;
}
/* åˆ†ç±»æ ‡ç­¾é¡µ */
.upload-tabs {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}
.tab-item {
  flex: 1;
  padding: 10px;
  text-align: center;
  background: #f5f5f5;
  border-radius: 6px;
  font-size: 13px;
  color: #666;
  cursor: pointer;
  transition: all 0.3s;
}
.tab-item.active {
  background: #409eff;
  color: #fff;
}
/* ä¸Šä¼ åŒºåŸŸ */
.upload-area {
  padding: 10px 0;
}
.upload-buttons {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}
.upload-progress {
  margin-bottom: 15px;
}
/* æ–‡ä»¶åˆ—表 */
.file-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.file-item {
  width: calc(33.33% - 7px);
}
.file-preview-container {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
}
.file-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.video-preview {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #333;
}
.video-text {
  font-size: 12px;
  color: #fff;
  margin-top: 5px;
}
.delete-btn {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 22px;
  height: 22px;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.file-info {
  margin-top: 5px;
}
.file-name {
  display: block;
  font-size: 11px;
  color: #666;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-size {
  display: block;
  font-size: 10px;
  color: #999;
  margin-top: 2px;
}
.empty-state {
  text-align: center;
  padding: 30px;
  color: #999;
  font-size: 13px;
}
/* ç»Ÿè®¡ä¿¡æ¯ */
.upload-summary {
  margin-top: 15px;
  padding: 10px;
  background: #f8f9fa;
  border-radius: 6px;
  border-left: 3px solid #409eff;
}
.summary-text {
  font-size: 12px;
  color: #666;
}
/* æ­£å¸¸çŠ¶æ€æç¤º */
.normal-tip-card {
  background: #f6ffed;
  border: 2px dashed #b7eb8f;
  border-radius: 12px;
  padding: 50px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin-bottom: 15px;
}
.normal-tip-card .tip-text {
  margin-top: 15px;
  font-size: 16px;
  color: #52c41a;
  font-weight: 500;
}
/* åº•部按钮 */
.footer-buttons {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  padding: 15px;
  background: #fff;
  border-top: 1px solid #f0f0f0;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
</style>