spring
2 天以前 1e71cfb6ec97cff6531dec65a3fb5cb24b2c18ac
src/pages/production/wire/attachment/index.vue
@@ -17,26 +17,54 @@
    <view class="attachment-list">
      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" />
      <wd-card
        v-for="item in attachmentList"
        :key="item.id"
        type="rectangle"
        custom-class="attachment-card"
        :border="false"
      >
        <view class="attachment-item" @click="previewAttachment(item)">
          <view class="attachment-info">
            <view class="attachment-name">{{ item.bucketFileName || item.name }}</view>
            <view class="attachment-meta">
              <text class="file-type">{{ getFileType(item.bucketFileName) }}</text>
              <text class="upload-time">{{ formatTime(item.createTime) }}</text>
      <view v-for="item in attachmentList" :key="item.id" class="attachment-card">
        <view class="media-wrapper" @click="previewAttachment(item)">
          <!-- 图片预览 -->
          <template v-if="isImageType(item.url)">
            <image
              v-if="!item.loadError"
              :src="getFullUrl(item.url)"
              mode="aspectFill"
              class="media-preview"
              @error="onImageError(item)"
              @load="onImageLoad(item)"
            />
            <!-- 图片加载失败显示默认图标 -->
            <view v-else class="file-icon-wrapper">
              <wd-icon name="picture" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- 视频预览 -->
          <template v-else-if="isVideoType(item.url)">
            <video
              v-if="!item.loadError"
              :src="getFullUrl(item.url)"
              class="media-preview"
              :controls="false"
              :show-center-play-btn="false"
              @error="onVideoError(item)"
            />
            <!-- 视频加载失败显示默认图标 -->
            <view v-else class="file-icon-wrapper">
              <wd-icon name="video" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- 其他文件类型显示图标 -->
          <view v-else class="file-icon-wrapper">
            <wd-icon name="file-outline" size="48px" color="#999" />
            <text class="file-name">文件</text>
          </view>
          <view class="attachment-actions">
            <wd-icon name="delete" color="#ff4757" @click.stop="deleteAttachment(item.id)" />
          <!-- 删除按钮 -->
          <view class="delete-btn" @click.stop="deleteAttachment(item.id)">
            <wd-icon name="delete" color="#fff" size="20px" />
          </view>
        </view>
      </wd-card>
      </view>
    </view>
    <wd-toast />
@@ -48,6 +76,12 @@
import { useToast } from "wot-design-uni";
import AttachmentAPI from "@/api/product/attachment";
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径
let baseUrl = import.meta.env.VITE_APP_API_URL;
// #ifdef H5
baseUrl = import.meta.env.VITE_APP_BASE_API;
// #endif
const toast = useToast();
// 页面参数
@@ -56,6 +90,57 @@
const attachmentList = ref<any[]>([]);
const detailData = ref<any>({});
// 获取完整的图片/视频 URL
const getFullUrl = (url: string) => {
  if (!url) return "";
  // 如果已经是完整的 URL(http 或 https 开头),直接返回
  if (url.startsWith("http://") || url.startsWith("https://")) {
    return url;
  }
  // 如果是相对路径,拼接基础 URL
  return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
};
// 从 URL 或文件名中提取扩展名
const getExtension = (urlOrFileName: string) => {
  if (!urlOrFileName) return "";
  // 移除查询参数和哈希
  const cleanUrl = urlOrFileName.split("?")[0].split("#")[0];
  // 获取最后一个点后面的内容
  const extension = cleanUrl.split(".").pop()?.toLowerCase();
  return extension || "";
};
// 判断是否为图片类型
const isImageType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(extension);
};
// 判断是否为视频类型
const isVideoType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["mp4", "mov", "avi", "wmv", "flv", "mkv", "webm"].includes(extension);
};
// 图片加载成功
const onImageLoad = (item: any) => {
  item.loadError = false;
};
// 图片加载失败
const onImageError = (item: any) => {
  console.error("图片加载失败:", item.url);
  item.loadError = true;
};
// 视频加载失败
const onVideoError = (item: any) => {
  console.error("视频加载失败:", item.url);
  item.loadError = true;
};
// 获取附件列表
const getAttachmentList = async (data: any) => {
  try {
@@ -72,7 +157,8 @@
      // 直接调用通用查看接口查询附件列表
      // 使用示例中的附件ID数组 [850,851]
      const attachmentIds: number[] = detailData.value.attachmentId !== null? detailData.value.attachmentId.split(",") : []; // 使用HTTP文件中的示例数据
      const attachmentIds: number[] =
        detailData.value.attachmentId !== null ? detailData.value.attachmentId.split(",") : []; // 使用HTTP文件中的示例数据
      if (attachmentIds.length === 0) {
        return;
      }
@@ -90,51 +176,132 @@
// 新增附件
const addAttachment = () => {
  uni.chooseFile({
    count: 9, // 最多选择9个文件
    type: "all", // 所有类型文件
    success: async (res) => {
      try {
        toast.show("正在上传...");
        // 上传文件
        const filePaths = Array.isArray(res.tempFilePaths)
          ? res.tempFilePaths
          : [res.tempFilePaths];
        const uploadResults: any = await AttachmentAPI.uploadAttachmentFiles(filePaths);
        const result = uploadResults.map((it) => {
          return it.data;
        });
        console.log("result", result);
        // 更新附件列表
        const flattenedResult = result.flat();
        attachmentList.value.push(...flattenedResult);
        console.log(attachmentList.value);
        // 提取附件ID
        const attachmentId = attachmentList.value.map((item: any) => item.id).join(",");
        // 关联到报工
        if (attachmentId) {
          await AttachmentAPI.addOutputAttachments({
            id: parseInt(detailData.value.id),
            attachmentId: attachmentId,
          });
          detailData.value.attachmentId = attachmentId;
        }
        toast.show("上传成功");
        // 重新获取附件列表
        // await getAttachmentList();
      } catch (error) {
        console.error("上传失败:", error);
        toast.show("上传失败");
  // 显示选择文件类型的弹窗
  uni.showActionSheet({
    itemList: ["选择图片", "选择视频", "拍照", "录像"],
    success: (res) => {
      switch (res.tapIndex) {
        case 0: // 选择图片
          chooseImages();
          break;
        case 1: // 选择视频
          chooseVideos();
          break;
        case 2: // 拍照
          takePhoto();
          break;
        case 3: // 录像
          recordVideo();
          break;
      }
    },
    fail: (error) => {
      console.error("选择文件失败:", error);
      toast.show("选择文件失败");
      console.error("选择文件类型失败:", error);
      toast.show("选择文件类型失败");
    },
  });
};
// 选择图片
const chooseImages = () => {
  uni.chooseImage({
    count: 9,
    sizeType: ["original", "compressed"],
    sourceType: ["album"],
    success: async (res) => {
      const filePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths];
      await handleFileUpload(filePaths);
    },
    fail: (error) => {
      console.error("选择图片失败:", error);
      toast.show("选择图片失败");
    },
  });
};
// 选择视频
const chooseVideos = () => {
  uni.chooseVideo({
    sourceType: ["album"],
    maxDuration: 60,
    camera: "back",
    success: async (res) => {
      await handleFileUpload([res.tempFilePath]);
    },
    fail: (error) => {
      console.error("选择视频失败:", error);
      toast.show("选择视频失败");
    },
  });
};
// 拍照
const takePhoto = () => {
  uni.chooseImage({
    count: 1,
    sizeType: ["original", "compressed"],
    sourceType: ["camera"],
    success: async (res) => {
      const filePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths];
      await handleFileUpload(filePaths);
    },
    fail: (error) => {
      console.error("拍照失败:", error);
      toast.show("拍照失败");
    },
  });
};
// 录像
const recordVideo = () => {
  uni.chooseVideo({
    sourceType: ["camera"],
    maxDuration: 60,
    camera: "back",
    success: async (res) => {
      await handleFileUpload([res.tempFilePath]);
    },
    fail: (error) => {
      console.error("录像失败:", error);
      toast.show("录像失败");
    },
  });
};
// 处理文件上传
const handleFileUpload = async (filePaths: string[]) => {
  try {
    toast.show("正在上传...");
    // 上传文件
    const uploadResults: any = await AttachmentAPI.uploadAttachmentFiles(filePaths);
    const result = uploadResults.map((it: any) => {
      return it.data;
    });
    console.log("result", result);
    // 更新附件列表
    const flattenedResult = result.flat();
    attachmentList.value.push(...flattenedResult);
    console.log(attachmentList.value);
    // 提取附件ID
    const attachmentId = attachmentList.value.map((item: any) => item.id).join(",");
    // 关联到报工
    if (attachmentId) {
      await AttachmentAPI.addOutputAttachments({
        id: parseInt(detailData.value.id),
        attachmentId: attachmentId,
      });
      detailData.value.attachmentId = attachmentId;
    }
    toast.show("上传成功");
  } catch (error) {
    console.error("上传失败:", error);
    toast.show("上传失败");
  }
};
// 删除附件
@@ -170,24 +337,24 @@
// 预览附件
const previewAttachment = (item: any) => {
  // 根据文件类型进行预览
  const fileName = item.bucketFileName || item.name;
  const fileType = getFileType(fileName);
  const fileType = getFileType(item.url);
  const fullUrl = getFullUrl(item.url);
  if (fileType.startsWith("image")) {
    // 图片预览
    uni.previewImage({
      urls: [item.url],
      current: item.url,
      urls: [fullUrl],
      current: fullUrl,
    });
  } else {
    // 其他文件类型,可以下载或打开
    uni.downloadFile({
      url: item.url,
      url: fullUrl,
      success: (res) => {
        uni.openDocument({
          filePath: res.tempFilePath,
          success: () => {
            console.log("打开文档成功");
            // 打开文档成功
          },
          fail: (error) => {
            console.error("打开文档失败:", error);
@@ -204,9 +371,9 @@
};
// 获取文件类型
const getFileType = (fileName: string) => {
  if (!fileName) return "unknown";
  const extension = fileName.split(".").pop()?.toLowerCase();
const getFileType = (urlOrFileName: string) => {
  if (!urlOrFileName) return "unknown";
  const extension = getExtension(urlOrFileName);
  switch (extension) {
    case "jpg":
    case "jpeg":
@@ -215,6 +382,14 @@
    case "bmp":
    case "webp":
      return "image";
    case "mp4":
    case "mov":
    case "avi":
    case "wmv":
    case "flv":
    case "mkv":
    case "webm":
      return "video";
    case "pdf":
      return "pdf";
    case "doc":
@@ -275,43 +450,72 @@
}
.attachment-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  .attachment-card {
    margin-bottom: 12px;
    border-radius: 4px;
    width: 100%;
    aspect-ratio: 1;
  }
}
.attachment-item {
  display: flex;
  align-items: center;
  padding: 12px;
.media-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
  .attachment-info {
    flex: 1;
  .media-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
    .attachment-name {
      font-size: 16px;
      font-weight: 500;
      color: #333;
      margin-bottom: 4px;
      word-break: break-all;
    }
  .file-icon-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: 8px;
    text-align: center;
    .attachment-meta {
      display: flex;
      gap: 12px;
    .file-name {
      margin-top: 8px;
      font-size: 12px;
      color: #999;
      color: #666;
      word-break: break-all;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
      &.error-text {
        color: #ff4757;
      }
    }
  }
  .attachment-actions {
    margin-left: 12px;
    :deep(.wd-icon) {
      font-size: 20px;
      cursor: pointer;
    }
  .delete-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;
  }
}
</style>