WXL
昨天 c80bc467a41daa6cbae4e5515a300a8ca98cfeaa
components/attachment/index.vue
@@ -23,7 +23,6 @@
      @change="onPopupChange"
    >
      <view class="attachment-popup">
        <!-- 弹层标题 -->
        <view class="popup-header">
          <text class="title">附件管理</text>
          <uni-icons
@@ -36,18 +35,18 @@
          <uni-icons type="close" size="24" color="#999" @click="closePopup" />
        </view>
        <!-- 标签页切换 -->
        <!-- 标签页 -->
        <view v-if="showGradeSlip" class="attachment-tabs">
          <view
            class="tab-item"
            :class="{ active: currentTab === 'base' }"
          <view
            class="tab-item"
            :class="{ active: currentTab === 'base' }"
            @click="currentTab = 'base'"
          >
            <text>基础附件</text>
          </view>
          <view
            class="tab-item"
            :class="{ active: currentTab === 'grade' }"
          <view
            class="tab-item"
            :class="{ active: currentTab === 'grade' }"
            @click="currentTab = 'grade'"
          >
            <text>成绩单附件</text>
@@ -57,7 +56,6 @@
        <!-- 附件列表 -->
        <scroll-view scroll-y class="file-list">
          <!-- 基础附件列表 -->
          <template v-if="currentTab === 'base' || !showGradeSlip">
            <view
              class="file-item"
@@ -72,10 +70,18 @@
                />
              </view>
              <view class="file-info" @click="previewFile(file)">
                <text class="file-name">{{ file.originalFilename || file.name }}</text>
                <text class="file-name">{{
                  file.originalFilename || file.name
                }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
                <text class="file-status" v-if="file.status === 'uploading'">上传中...</text>
                <text class="file-status error" v-else-if="file.status === 'error'">上传失败</text>
                <text class="file-status" v-if="file.status === 'uploading'"
                  >上传中...</text
                >
                <text
                  class="file-status error"
                  v-else-if="file.status === 'error'"
                  >上传失败</text
                >
              </view>
              <uni-icons
                v-if="!readonly"
@@ -87,7 +93,6 @@
            </view>
          </template>
          <!-- 成绩单附件列表 -->
          <template v-if="currentTab === 'grade' && showGradeSlip">
            <view
              class="file-item"
@@ -102,10 +107,18 @@
                />
              </view>
              <view class="file-info" @click="previewFile(file)">
                <text class="file-name">{{ file.originalFilename || file.name }}</text>
                <text class="file-name">{{
                  file.originalFilename || file.name
                }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
                <text class="file-status" v-if="file.status === 'uploading'">上传中...</text>
                <text class="file-status error" v-else-if="file.status === 'error'">上传失败</text>
                <text class="file-status" v-if="file.status === 'uploading'"
                  >上传中...</text
                >
                <text
                  class="file-status error"
                  v-else-if="file.status === 'error'"
                  >上传失败</text
                >
              </view>
              <uni-icons
                v-if="!readonly"
@@ -117,138 +130,154 @@
            </view>
          </template>
          <!-- 空状态 -->
          <view class="empty" v-if="currentFileList.length === 0">
            <uni-icons type="info" size="24" color="#999" />
            <text v-if="currentTab === 'base' || !showGradeSlip">暂无基础附件</text>
            <text v-if="currentTab === 'base' || !showGradeSlip">暂无附件</text>
            <text v-else-if="currentTab === 'grade'">暂无成绩单附件</text>
          </view>
        </scroll-view>
        <!-- 操作按钮 -->
        <view class="popup-footer" v-if="!readonly">
          <button class="btn" @click="chooseFile">添加{{ currentTab === 'grade' ? '成绩单' : '基础' }}附件</button>
          <button class="btn" @click="chooseFile">添加</button>
          <button class="btn primary" @click="confirmUpload">确认上传</button>
        </view>
      </view>
    </uni-popup>
    <!-- 文件选择器 -->
    <uni-file-picker
      ref="filePicker"
      v-if="!readonly"
      :auto-upload="false"
      file-mediatype="all"
      :limit="maxCount - currentFileList.length"
      :image-styles="imageStyles"
      @select="onFileSelect"
      @delete="onFileDelete"
      style="display: none"
    />
  </view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { ref, computed, watch, onBeforeUnmount } from "vue";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const props = defineProps({
  files: {
    type: Array,
    default: () => [],
  },
  gradesFiles: { // 新增属性:成绩单附件列表
    type: Array,
    default: () => [],
  },
  readonly: {
    type: Boolean,
    default: false,
  },
  files: { type: Array, default: () => [] },
  gradesFiles: { type: Array, default: () => [] },
  readonly: { type: Boolean, default: false },
  position: {
    type: Object,
    default: () => ({
      right: "30rpx",
      bottom: "200rpx",
    }),
    default: () => ({ right: "30rpx", bottom: "200rpx" }),
  },
  bgColor: {
    type: String,
    default: "#67AFAB",
  },
  maxCount: {
    type: Number,
    default: 9,
  },
  showGradeSlip: {
    type: Boolean,
    default: false
  },
  isGradeRequired: {
    type: Boolean,
    default: false
  }
  bgColor: { type: String, default: "#67AFAB" },
  maxCount: { type: Number, default: 9 },
  showGradeSlip: { type: Boolean, default: false },
  isGradeRequired: { type: Boolean, default: false },
});
const emit = defineEmits([
  "update:files",
  "update:gradesFiles", // 新增事件:更新成绩单附件
  "upload",
  "update:files",
  "update:gradesFiles",
  "upload",
  "preview",
  "upload-grade", // 新增事件:上传成绩单附件
  "upload-base" // 新增事件:上传基础附件
  "upload-grade",
  "upload-base",
]);
const popup = ref(null);
const filePicker = ref(null);
const baseFiles = ref([]);
const gradeFiles = ref([]);
const showButton = ref(true);
const mainColor = ref("#67AFAB");
const baseUrlHt = userStore.baseUrlHt;
const currentTab = ref('base');
const currentTab = ref("base");
const blobUrls = ref([]);
// 计算当前显示的文件列表
const currentFileList = computed(() => {
  if (!props.showGradeSlip) {
    return baseFiles.value;
const getBaseUrlHt = () => {
  // #ifdef H5
  try {
    const href = window.location.href;
    // 规则 1:192.168.120.27:8091 → 120.27 + :9095
    if (href.startsWith("http://192.168.120.27:8091")) {
      return "http://192.168.120.27:9095";
    }
    // 规则 2:192.168.76.25:8091 → 76.25 + :9095
    if (href.startsWith("http://192.168.76.25:8091")) {
      return "http://192.168.76.25:9095";
    }
    // 规则 3:其余全部走生产
    return "https://opo.qduh.cn";
  } catch {
    return "https://opo.qduh.cn";
  }
  return currentTab.value === 'base' ? baseFiles.value : gradeFiles.value;
  // #endif
  // #ifdef MP-WEIXIN
  return userStore.baseUrlHt;
  // #endif
};
const baseUrlHt = getBaseUrlHt();
const currentFileList = computed(() => {
  if (!props.showGradeSlip) return baseFiles.value;
  return currentTab.value === "base" ? baseFiles.value : gradeFiles.value;
});
const totalFileCount = computed(
  () => baseFiles.value.length + gradeFiles.value.length,
);
// 计算文件总数
const totalFileCount = computed(() => {
  return baseFiles.value.length + gradeFiles.value.length;
});
watch(
  () => props.files,
  (newFiles) => {
    baseFiles.value = [...newFiles];
  },
  { immediate: true },
);
watch(
  () => props.gradesFiles,
  (newFiles) => {
    gradeFiles.value = [...newFiles];
  },
  { immediate: true },
);
// 监听外部传入的files,初始化文件列表
watch(() => props.files, (newFiles) => {
  baseFiles.value = [...newFiles];
}, { immediate: true });
// 监听外部传入的gradesFiles,初始化成绩单附件列表
watch(() => props.gradesFiles, (newFiles) => {
  gradeFiles.value = [...newFiles];
}, { immediate: true });
// 获取完整URL
// 获取完整URL - 平台差异化处理
const getFullUrl = (path) => {
  if (!path) return '';
  if (path.startsWith('http://') || path.startsWith('https://')) {
  if (!path) return "";
  // 如果是 blob 或 file 协议的本地路径,直接返回
  if (path.startsWith("blob:") || path.startsWith("file://")) {
    return path;
  }
  return `${baseUrlHt}${path.startsWith('/') ? '' : '/'}${path}`;
  // 如果是完整的 http/https 链接
  if (path.startsWith("http://") || path.startsWith("https://")) {
    // #ifdef H5
    // H5 环境:使用 new URL 解析
    try {
      const url = new URL(path);
      return `${baseUrlHt}${url.pathname}${url.search}${url.hash}`;
    } catch (error) {
      console.error("URL解析失败:", error);
      // 降级处理:手动截取路径
      const pathIndex = path.indexOf("/", 8); // 跳过 http:// 或 https://
      if (pathIndex !== -1) {
        return `${baseUrlHt}${path.substring(pathIndex)}`;
      }
      return path;
    }
    // #endif
    // #ifdef MP-WEIXIN
    // 小程序环境:不使用 new URL,手动处理
    const protocolEnd = path.indexOf("://") + 3;
    const pathStart = path.indexOf("/", protocolEnd);
    if (pathStart !== -1) {
      // 提取路径部分(包括路径、查询参数和hash)
      return `${baseUrlHt}${path.substring(pathStart)}`;
    }
    return baseUrlHt;
    // #endif
  }
  // 相对路径处理
  return `${baseUrlHt}${path.startsWith("/") ? "" : "/"}${path}`;
};
// 文件选择器样式
const imageStyles = {
  width: 120,
  height: 120,
  border: false,
};
// 支持的图片格式
const supportedImageTypes = [
  "image/jpeg",
  "image/png",
@@ -258,12 +287,9 @@
  "image/svg+xml",
];
// 弹层状态变化
const onPopupChange = (e) => {
  showButton.value = !e.show;
};
// 获取文件图标
const getFileIcon = (type) => {
  if (type && supportedImageTypes.includes(type)) return "image";
  if (type && type.includes("pdf")) return "paperclip";
@@ -272,8 +298,6 @@
  if (type && type.includes("powerpoint")) return "file-ppt";
  return "file";
};
// 获取文件颜色
const getFileColor = (type) => {
  if (type && supportedImageTypes.includes(type)) return mainColor.value;
  if (type && type.includes("pdf")) return "#ff4d4f";
@@ -282,85 +306,32 @@
  if (type && type.includes("powerpoint")) return "#b7472a";
  return "#666";
};
// 格式化文件大小
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 togglePopup = () => {
  if (popup.value) {
    popup.value.open();
  }
  popup.value?.open();
};
// 关闭弹层
const closePopup = () => {
  if (popup.value) {
    popup.value.close();
  }
  popup.value?.close();
};
// 选择文件
const chooseFile = () => {
  filePicker.value?.choose();
};
// 从文件名获取类型
const getFileTypeFromName = (filename) => {
  if (!filename) return "application/octet-stream";
  const ext = filename.split(".").pop().toLowerCase();
  const typeMap = {
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    png: "image/png",
    gif: "image/gif",
    webp: "image/webp",
    bmp: "image/bmp",
    SVG: "image/svg+xml",
    pdf: "application/pdf",
  };
  return typeMap[ext] || "application/octet-stream";
};
// 文件选中回调
const onFileSelect = (e) => {
  // 明确确定当前是基础附件还是成绩单附件
  const isGradeTab = props.showGradeSlip && currentTab.value === 'grade';
const addFilesToCurrentTab = (newFiles) => {
  const isGradeTab = props.showGradeSlip && currentTab.value === "grade";
  const targetFiles = isGradeTab ? gradeFiles.value : baseFiles.value;
  const newFiles = e.tempFiles
    .filter((file) => {
      const fileExt = file.name ? file.name.split(".").pop().toLowerCase() : "";
      const isImage = supportedImageTypes.includes(file.type) ||
        ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(fileExt);
      const isPDF = (file.type && file.type.includes("pdf")) || fileExt === "pdf";
      return isImage || isPDF;
    })
    .map((file) => ({
      name: file.name,
      url: file.path || file.url,
      type: file.type || getFileTypeFromName(file.name),
      size: file.size,
      file: file,
      status: 'pending',
      // 明确标记附件类型
      attachmentType: isGradeTab ? 'grade' : 'base'
    }));
  if (targetFiles.length + newFiles.length > props.maxCount) {
    uni.showToast({
      title: `最多只能上传${props.maxCount}个文件`,
      icon: "none",
    });
    newFiles.forEach((f) => {
      if (f.url && f.url.startsWith("blob:")) URL.revokeObjectURL(f.url);
    });
    return;
  }
  // 严格区分存储位置
  if (isGradeTab) {
    gradeFiles.value = [...gradeFiles.value, ...newFiles];
    emit("update:gradesFiles", gradeFiles.value);
@@ -370,18 +341,115 @@
  }
};
// 获取所有文件
const getAllFiles = () => {
  return [...baseFiles.value, ...gradeFiles.value];
// 选择文件(平台差异)
const chooseFile = () => {
  // #ifdef H5
  const input = document.createElement("input");
  input.type = "file";
  input.accept = "image/*,application/pdf";
  input.multiple = true;
  input.onchange = (e) => {
    const files = Array.from(e.target.files);
    if (files.length === 0) return;
    const newFiles = files
      .map((file) => {
        const ext = file.name.split(".").pop().toLowerCase();
        const isImage =
          supportedImageTypes.includes(file.type) ||
          ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(ext);
        const isPDF = file.type.includes("pdf") || ext === "pdf";
        if (!isImage && !isPDF) {
          uni.showToast({
            title: `文件 ${file.name} 格式不支持`,
            icon: "none",
          });
          return null;
        }
        const blobUrl = URL.createObjectURL(file);
        blobUrls.value.push(blobUrl);
        return {
          name: file.name,
          path: blobUrl,
          url: blobUrl,
          uploadPath: blobUrl,
          type: file.type,
          size: file.size,
          raw: file,
          status: "pending",
        };
      })
      .filter((f) => f);
    addFilesToCurrentTab(newFiles);
    input.remove();
  };
  input.click();
  // #endif
  // #ifdef MP-WEIXIN
  uni.showActionSheet({
    itemList: ["拍照/相册", "从聊天记录选择文件"],
    success: (res) => {
      const remain = props.maxCount - currentFileList.value.length;
      if (remain <= 0) {
        uni.showToast({
          title: `最多上传${props.maxCount}个文件`,
          icon: "none",
        });
        return;
      }
      if (res.tapIndex === 0) {
        uni.chooseImage({
          count: remain,
          sizeType: ["original", "compressed"],
          sourceType: ["album", "camera"],
          success: (chooseRes) => {
            const files = chooseRes.tempFiles.map((file) => ({
              name: file.name || "image.jpg",
              path: file.path,
              url: file.path,
              uploadPath: file.path,
              type: file.type || "image/jpeg",
              size: file.size,
              raw: file,
              status: "pending",
            }));
            addFilesToCurrentTab(files);
          },
        });
      } else if (res.tapIndex === 1) {
        uni.chooseMessageFile({
          count: remain,
          type: "all",
          success: (chooseRes) => {
            const files = chooseRes.tempFiles.map((file) => ({
              name: file.name,
              path: file.path,
              url: file.path,
              uploadPath: file.path,
              type: file.type,
              size: file.size,
              raw: file,
              status: "pending",
            }));
            addFilesToCurrentTab(files);
          },
        });
      }
    },
  });
  // #endif
};
const removeFile = (type, index, event) => {
  if (event) {
    event.stopPropagation();
  if (event) event.stopPropagation();
  const file =
    type === "grade" ? gradeFiles.value[index] : baseFiles.value[index];
  if (file.url && file.url.startsWith("blob:")) {
    URL.revokeObjectURL(file.url);
    const idx = blobUrls.value.indexOf(file.url);
    if (idx !== -1) blobUrls.value.splice(idx, 1);
  }
  // 严格按类型删除
  if (type === 'grade') {
  if (type === "grade") {
    gradeFiles.value.splice(index, 1);
    emit("update:gradesFiles", gradeFiles.value);
  } else {
@@ -392,199 +460,169 @@
// 预览文件
const previewFile = (file) => {
  const fullUrl = getFullUrl(file.url);
  let previewUrl = file.url;
  if (
    previewUrl &&
    !previewUrl.startsWith("blob:") &&
    !previewUrl.startsWith("file://") &&
    !previewUrl.startsWith("/")
  ) {
    previewUrl = getFullUrl(previewUrl);
  }
  if (file.type && supportedImageTypes.includes(file.type)) {
    // 预览图片
    const allFiles = getAllFiles();
    uni.previewImage({
      urls: allFiles
        .filter(f => f.type && supportedImageTypes.includes(f.type))
        .map(f => getFullUrl(f.url)),
      current: fullUrl,
    });
    const allFiles = [...baseFiles.value, ...gradeFiles.value];
    const imageUrls = allFiles
      .filter((f) => f.type && supportedImageTypes.includes(f.type))
      .map((f) =>
        f.url.startsWith("blob:") ||
        f.url.startsWith("file://") ||
        f.url.startsWith("/")
          ? f.url
          : getFullUrl(f.url),
      );
    uni.previewImage({ urls: imageUrls, current: previewUrl });
  } else if (file.type && file.type.includes("pdf")) {
    // 预览PDF
    uni.downloadFile({
      url: fullUrl,
      url: previewUrl,
      success: (res) => {
        const filePath = res.tempFilePath;
        uni.openDocument({
          filePath: filePath,
          fileType: "pdf",
          success: () => console.log("打开PDF成功"),
          fail: (err) => {
            console.error("打开PDF失败", err);
            uni.showToast({
              title: "打开PDF失败",
              icon: "none",
            });
          },
        });
        uni.openDocument({ filePath: res.tempFilePath, fileType: "pdf" });
      },
      fail: (err) => {
        console.error("下载PDF失败", err);
        uni.showToast({
          title: "下载PDF失败",
          icon: "none",
        });
      },
      fail: () => uni.showToast({ title: "打开PDF失败", icon: "none" }),
    });
  } else {
    // 其他文件类型触发预览事件
    emit("preview", file);
  }
};
// 文件上传方法
const uploadFile = (file, type) => {
  return new Promise((resolve, reject) => {
    const token = uni.getStorageSync('token');
    const filePath = file.uploadPath || file.url || file.path;
    if (!filePath || typeof filePath !== 'string') {
      reject(new Error('无效的文件路径'));
      return;
    }
    // #ifdef H5
    const uploadUrl = '/api/common/upload';
    // #endif
    // #ifdef MP-WEIXIN
    const uploadUrl = `${userStore.baseUrlHt}/common/upload`;
    // #endif
    uni.uploadFile({
      url: '/api/common/upload',
      filePath: file.path || file.url,
      url: uploadUrl,
      filePath,
      name: 'file',
      header: {
        'Authorization': `Bearer ${token}`
        Authorization: `Bearer ${token}`
      },
      success: (res) => {
        if (res.statusCode === 200) {
          const data = JSON.parse(res.data);
          console.log(data,'文件');
          if (data.code === 200) {
            resolve({
              ...data,
              fileName: data.fileName
            });
          } else {
            reject(new Error(data.msg || '上传失败'));
          try {
            const data = JSON.parse(res.data);
            if (data.code === 200) {
              resolve({ ...data, fileName: data.fileName });
            } else {
              reject(new Error(data.msg || '上传失败'));
            }
          } catch {
            reject(new Error('响应解析失败'));
          }
        } else {
          reject(new Error(`上传失败,状态码: ${res.statusCode}`));
        }
      },
      fail: (err) => {
        reject(err);
      }
      fail: reject
    });
  });
};
// 确认上传
const confirmUpload = async () => {
  // 检查成绩单附件是否必填
  if (props.showGradeSlip && props.isGradeRequired && gradeFiles.value.length === 0) {
    uni.showToast({
      title: "请上传成绩单附件",
      icon: "none",
    });
    currentTab.value = 'grade';
  if (
    props.showGradeSlip &&
    props.isGradeRequired &&
    gradeFiles.value.length === 0
  ) {
    uni.showToast({ title: "请上传成绩单附件", icon: "none" });
    currentTab.value = "grade";
    return;
  }
  const allFiles = getAllFiles();
  if (allFiles.length === 0) {
    uni.showToast({
      title: "请先添加附件",
      icon: "none",
    });
  const totalPending = [...baseFiles.value, ...gradeFiles.value].filter(
    (f) => !f.url || f.status === "pending",
  ).length;
  if (totalPending === 0) {
    uni.showToast({ title: "没有待上传的文件", icon: "none" });
    return;
  }
  uni.showLoading({
    title: '上传中',
    mask: true
  });
  uni.showLoading({ title: "上传中", mask: true });
  try {
    // 分别处理基础附件和成绩单附件的上传
    const pendingBaseFiles = baseFiles.value.filter(file =>
      !file.url || file.status === 'pending');
    const pendingGradeFiles = gradeFiles.value.filter(file =>
      !file.url || file.status === 'pending');
    // 上传基础附件
    for (const file of pendingBaseFiles) {
    const pendingBase = baseFiles.value.filter(
      (f) => !f.url || f.status === "pending",
    );
    const pendingGrade = gradeFiles.value.filter(
      (f) => !f.url || f.status === "pending",
    );
    for (const file of pendingBase) {
      try {
        file.status = 'uploading';
        const res = await uploadFile(file.file, 'base');
        file.status = "uploading";
        const res = await uploadFile(file, "base");
        Object.assign(file, {
          url: res.url,
          fileName: res.name,
          newFileName: res.newFileName,
          originalFilename: res.originalFilename,
          status: 'success',
          size:res.size
          status: "success",
          size: res.size,
        });
        emit("upload-base", file);
      } catch (error) {
        console.error('基础附件上传失败:', error);
        file.status = 'error';
        uni.showToast({
          title: `文件 ${file.name} 上传失败`,
          icon: 'none'
        });
      } catch (err) {
        file.status = "error";
        uni.showToast({ title: `文件 ${file.name} 上传失败`, icon: "none" });
      }
    }
    // 上传成绩单附件
    for (const file of pendingGradeFiles) {
    for (const file of pendingGrade) {
      try {
        file.status = 'uploading';
        const res = await uploadFile(file.file, 'grade');
        file.status = "uploading";
        const res = await uploadFile(file, "grade");
        Object.assign(file, {
          url: res.fileName,
          fileName: res.fileName,
          newFileName: res.newFileName,
          originalFilename: res.originalFilename,
          status: 'success'
          status: "success",
        });
        emit("upload-grade", file);
      } catch (error) {
        console.error('成绩单附件上传失败:', error);
        file.status = 'error';
        uni.showToast({
          title: `文件 ${file.name} 上传失败`,
          icon: 'none'
        });
      } catch (err) {
        file.status = "error";
        uni.showToast({ title: `文件 ${file.name} 上传失败`, icon: "none" });
      }
    }
    console.log(baseFiles.value,'1');
    console.log(gradeFiles.value,'2');
    // 更新文件列表
    emit("update:files", baseFiles.value);
    emit("update:gradesFiles", gradeFiles.value);
    // emit("upload", allFiles);
    uni.showToast({
      title: '上传完成',
      icon: 'success'
    });
    uni.showToast({ title: "上传完成", icon: "success" });
    closePopup();
  } catch (error) {
    console.error('上传出错:', error);
    uni.showToast({
      title: '上传出错',
      icon: 'none'
    });
    console.error("上传出错:", error);
    uni.showToast({ title: "上传出错", icon: "none" });
  } finally {
    uni.hideLoading();
  }
};
// 获取特定类型的文件(供父组件调用)
const getFilesByType = (type) => {
  return type === 'grade' ? gradeFiles.value : baseFiles.value;
};
const getFilesByType = (type) =>
  type === "grade" ? gradeFiles.value : baseFiles.value;
const getAllFiles = () => [...baseFiles.value, ...gradeFiles.value];
// 暴露方法给父组件
defineExpose({
  getFilesByType,
  getAllFiles
onBeforeUnmount(() => {
  blobUrls.value.forEach((url) => URL.revokeObjectURL(url));
  blobUrls.value = [];
});
defineExpose({ getFilesByType, getAllFiles });
</script>
<style lang="scss">
@@ -593,21 +631,17 @@
    display: flex;
    border-bottom: 1px solid #eee;
    margin-bottom: 20rpx;
    .tab-item {
      flex: 1;
      text-align: center;
      padding: 20rpx 0;
      position: relative;
      font-size: 28rpx;
      color: #666;
      &.active {
        color: #67AFAB;
        color: #67afab;
        font-weight: bold;
        border-bottom: 4rpx solid #67AFAB;
        border-bottom: 4rpx solid #67afab;
      }
      .required-mark {
        color: red;
        position: absolute;
@@ -617,8 +651,6 @@
      }
    }
  }
  // 其他样式保持不变
  .attachment-btn {
    position: fixed;
    width: 160rpx;
@@ -633,12 +665,10 @@
    font-size: 28rpx;
    font-weight: bold;
    transition: all 0.3s;
    &:active {
      opacity: 0.8;
      transform: scale(0.95);
    }
    .badge {
      position: absolute;
      top: -10rpx;
@@ -654,13 +684,11 @@
      justify-content: center;
    }
  }
  .attachment-popup {
    background-color: #fff;
    border-radius: 24rpx 24rpx 0 0;
    padding: 30rpx;
    max-height: 70vh;
    .popup-header {
      display: flex;
      justify-content: space-between;
@@ -668,13 +696,11 @@
      margin-bottom: 30rpx;
      padding-bottom: 20rpx;
      border-bottom: 1rpx solid #f5f5f5;
      .title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .uni-icons {
        padding: 10rpx;
        &:active {
@@ -682,30 +708,23 @@
        }
      }
    }
    .file-list {
      max-height: 50vh;
      margin-bottom: 30rpx;
      .file-item {
        display: flex;
        align-items: center;
        padding: 20rpx 0;
        border-bottom: 1rpx solid #f5f5f5;
        transition: all 0.2s;
        &:active {
          background-color: #f9f9f9;
        }
        .file-icon {
          margin-right: 20rpx;
        }
        .file-info {
          flex: 1;
          overflow: hidden;
          .file-name {
            font-size: 28rpx;
            color: #333;
@@ -715,24 +734,20 @@
            overflow: hidden;
            text-overflow: ellipsis;
          }
          .file-size {
            font-size: 24rpx;
            color: #999;
            display: block;
          }
          .file-status {
            font-size: 24rpx;
            color: #666;
            display: block;
            &.error {
              color: #ff4d4f;
            }
          }
        }
        .uni-icons {
          padding: 10rpx;
          &:active {
@@ -740,7 +755,6 @@
          }
        }
      }
      .empty {
        display: flex;
        flex-direction: column;
@@ -749,18 +763,14 @@
        padding: 60rpx 0;
        color: #999;
        font-size: 28rpx;
        .uni-icons {
          margin-bottom: 20rpx;
        }
      }
    }
    .popup-footer {
      display: flex;
      justify-content: space-between;
      gap: 20rpx;
      .btn {
        flex: 1;
        height: 80rpx;
@@ -771,12 +781,9 @@
        background-color: #f5f5f5;
        color: #666;
        border: none;
        transition: all 0.2s;
        &:active {
          opacity: 0.8;
        }
        &.primary {
          background-color: #67afab;
          color: #fff;
@@ -785,4 +792,4 @@
    }
  }
}
</style>
</style>