spring
昨天 6e763136fdf4469143ebbae0b717eb8e9b0ca954
src/pages/routingInspection/upload.vue
@@ -18,20 +18,25 @@
    <view class="attachment-list">
      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" />
      <view v-for="item in attachmentList" :key="item.id" class="attachment-card">
      <view v-for="(item, index) in attachmentList" :key="item.id || index" 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)"
              style="width: 100%; height: 100%"
              @error="onImageError(item, index)"
              @load="onImageLoad(item, index)"
              :show-menu-by-longpress="true"
            />
            <!-- 加载中遮罩 -->
            <view v-if="item.loading" class="loading-mask">
              <text class="loading-text">加载中...</text>
            </view>
            <!-- 图片加载失败显示默认图标 -->
            <view v-else class="file-icon-wrapper">
            <view v-if="item.loadError" class="file-icon-wrapper error-overlay">
              <wd-icon name="picture" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
@@ -40,15 +45,15 @@
          <!-- 视频预览 -->
          <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)"
              :show-center-play-btn="true"
              @error="onVideoError(item, index)"
              object-fit="cover"
            />
            <!-- 视频加载失败显示默认图标 -->
            <view v-else class="file-icon-wrapper">
            <view v-if="item.loadError" class="file-icon-wrapper error-overlay">
              <wd-icon name="video" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
@@ -73,15 +78,17 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ref, computed, watch } from "vue";
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;
let baseUrlValue = import.meta.env.VITE_APP_API_URL || "";
// #ifdef H5
baseUrl = import.meta.env.VITE_APP_BASE_API;
baseUrlValue = import.meta.env.VITE_APP_BASE_API || "";
// #endif
const baseUrl = ref(baseUrlValue); // 使用ref使其在模板中可访问
// 外部参数
const props = defineProps({
@@ -91,35 +98,100 @@
});
const toast = useToast();
const attachmentList = ref<any[]>(props.detailData.files || []);
const attachmentIds = ref<string[]>(props.detailData.attachmentId || []);
// 获取初始数据
const getInitialData = () => {
  // 处理不同的数据结构
  let data = props.detailData;
  // 如果是 ref 对象,获取其 value
  if (data && typeof data === "object" && "value" in data) {
    data = data.value;
  }
  // 如果是数组,直接返回
  if (Array.isArray(data)) {
    return data.map((item) => ({
      ...item,
      loading: false,
      loadError: false,
    }));
  }
  // 如果有 files 属性
  if (data && data.files) {
    const files = Array.isArray(data.files) ? data.files : [];
    return files.map((item) => ({
      ...item,
      loading: false,
      loadError: false,
    }));
  }
  return [];
};
const attachmentList = ref<any[]>(getInitialData());
const attachmentIds = ref<string[]>(attachmentList.value.map((item: any) => item.id) || []);
// 监听 props.detailData 变化
watch(
  () => props.detailData,
  (newVal) => {
    const newData = getInitialData();
    if (newData.length > 0) {
      attachmentList.value = newData.map((item) => ({
        ...item,
        loading: false,
        loadError: false,
      }));
      attachmentIds.value = newData.map((item: any) => item.id);
    }
  },
  { deep: true, immediate: false }
);
// 获取完整的图片/视频 URL
const getFullUrl = (url: string) => {
  if (!url) return "";
  // 如果已经是完整的 URL(http 或 https 开头),直接返回
  if (url.startsWith("http://") || url.startsWith("https://")) {
    return url;
  }
  // 检查 baseUrl 是否有效
  if (!baseUrl.value) {
    console.error("❌ baseUrl未配置,url:", url);
    return url;
  }
  // 如果是相对路径,拼接基础 URL
  return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
  const separator = url.startsWith("/") || baseUrl.value.endsWith("/") ? "" : "/";
  return `${baseUrl.value}${separator}${url}`;
};
// 图片加载成功
const onImageLoad = (item: any) => {
const onImageLoad = (item: any, index: number) => {
  item.loading = false;
  item.loadError = false;
  attachmentList.value = [...attachmentList.value];
};
// 图片加载失败
const onImageError = (item: any) => {
  console.error("图片加载失败:", item.url);
const onImageError = (item: any, index: number) => {
  console.error(`图片加载失败 [${index}]:`, item.url);
  item.loading = false;
  item.loadError = true;
  attachmentList.value = [...attachmentList.value];
};
// 视频加载失败
const onVideoError = (item: any) => {
  console.error("视频加载失败:", item.url);
const onVideoError = (item: any, index: number) => {
  console.error(`视频加载失败 [${index}]:`, item.url);
  item.loading = false;
  item.loadError = true;
  attachmentList.value = [...attachmentList.value];
};
// 新增附件
@@ -405,14 +477,23 @@
  .attachment-card {
    width: 100%;
    aspect-ratio: 1;
    position: relative;
    // 使用 padding-top 实现正方形(兼容性更好)
    &::before {
      content: "";
      display: block;
      padding-top: 100%; // 高度等于宽度
    }
  }
}
.media-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
@@ -421,6 +502,25 @@
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  .loading-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.3);
    z-index: 5;
    .loading-text {
      font-size: 12px;
      color: #fff;
    }
  }
  .file-icon-wrapper {
@@ -448,6 +548,16 @@
        color: #ff4757;
      }
    }
    &.error-overlay {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(255, 255, 255, 0.9);
      z-index: 5;
    }
  }
  .delete-btn {