Pārlūkot izejas kodu

fix: 案例详情不显示的问题

sgc 18 stundas atpakaļ
vecāks
revīzija
519288f14d
1 mainītis faili ar 99 papildinājumiem un 178 dzēšanām
  1. 99 178
      src/views/operationManagement/caseDetail.vue

+ 99 - 178
src/views/operationManagement/caseDetail.vue

@@ -33,42 +33,20 @@
               <div class="detail-item">
                 <div class="detail-label">成果描述 : {{ items.achievementDesc || "-" }}</div>
               </div>
-              <div class="detail-item" v-if="items.mediaUrlList && items.mediaUrlList.length > 0">
+              <div class="detail-item" v-if="parseAchievementMedia(items).length > 0">
                 <div class="detail-label">图片与视频 :</div>
                 <div class="media-grid">
-                  <template v-for="(item, mediaIndex) in items.mediaUrlList" :key="mediaIndex">
-                    <!-- 视频:包含 | 分隔符 -->
-                    <div v-if="isVideoUrl(item)" class="media-item video-item" @click="playVideo(getVideoUrl(item))">
-                      <el-image v-if="getVideoCoverUrl(item)" :src="getVideoCoverUrl(item)" fit="cover" class="media-image">
-                        <template #error>
-                          <div class="media-placeholder">
-                            <el-icon class="play-icon">
-                              <VideoPlay />
-                            </el-icon>
-                          </div>
-                        </template>
-                      </el-image>
-                      <div v-else class="media-placeholder">
-                        <el-icon class="play-icon">
-                          <VideoPlay />
-                        </el-icon>
-                      </div>
-                      <!-- 视频播放图标覆盖层 -->
+                  <template v-for="(m, mediaIndex) in parseAchievementMedia(items)" :key="mediaIndex + m.url">
+                    <div v-if="m.type === 'video'" class="media-item video-item" @click="playVideo(m.url)">
+                      <video class="video-thumb" :src="thumbVideoSrc(m.url)" muted playsinline preload="metadata" />
                       <div class="video-overlay">
                         <el-icon class="play-icon-overlay">
                           <VideoPlay />
                         </el-icon>
                       </div>
                     </div>
-                    <!-- 图片:不包含 | 分隔符 -->
-                    <div v-else class="media-item image-item" @click="previewImage(item, items.mediaUrlList, mediaIndex)">
-                      <el-image
-                        :src="item"
-                        fit="cover"
-                        class="media-image"
-                        :preview-src-list="getImageListForItem(items.mediaUrlList)"
-                        :initial-index="getImageIndex(item, items.mediaUrlList)"
-                      >
+                    <div v-else class="media-item image-item" @click.stop="openImagePreview(items, m.url)">
+                      <el-image :src="m.url" fit="cover" class="media-image">
                         <template #error>
                           <div class="image-slot">
                             <el-icon>
@@ -98,6 +76,12 @@
       :autoplay="true"
       @closed="previewVideo = ''"
     />
+
+    <PcImagePreviewViewer
+      v-model:visible="imagePreviewVisible"
+      :url-list="imagePreviewUrlList"
+      :initial-index="imagePreviewInitialIndex"
+    />
   </div>
 </template>
 
@@ -107,6 +91,75 @@ import { useRoute, useRouter } from "vue-router";
 import { Picture, VideoPlay } from "@element-plus/icons-vue";
 import { getPersonCaseDetail } from "@/api/modules/operationManagement";
 import PcVideoPreviewDialog from "@/components/pcMediaPreview/PcVideoPreviewDialog.vue";
+import PcImagePreviewViewer from "@/components/pcMediaPreview/PcImagePreviewViewer.vue";
+
+/** 与 query 无关,仅看路径扩展名(忽略 ? 后参数) */
+function pathLooksVideo(url: string): boolean {
+  const path = String(url || "")
+    .split("?")[0]
+    .split("#")[0]
+    .toLowerCase();
+  return /\.(mp4|m3u8|mov|webm|mkv|mpeg|mpg|avi|3gp)$/i.test(path);
+}
+
+/**
+ * 列表里视频缩略用:尽量用视频自身首帧,不用下一张独立图片当 poster(避免与右侧图片格长得完全一样)。
+ * 对常见 MP4 等追加 `#t=0.001` 促使解码第一帧;HLS(m3u8)或已有 hash 的地址不改动。
+ */
+function thumbVideoSrc(url: string): string {
+  const s = String(url || "").trim();
+  if (!s || s.includes("#")) return s;
+  const pathOnly = s.split("?")[0].toLowerCase();
+  if (/\.m3u8$/i.test(pathOnly)) return s;
+  return `${s}#t=0.001`;
+}
+
+type ParsedMedia = { type: "image" | "video"; url: string };
+
+/**
+ * 成果媒体:仅按英文分号 `;` 切割多条 URL(与后端 mediaUrls / mediaUrlList 一致)。
+ * 每条按扩展名区分视频 / 图片,列表项一一对应网格。
+ */
+function parseAchievementMedia(row: Record<string, unknown>): ParsedMedia[] {
+  const chunks: string[] = [];
+  const pushSplit = (raw: unknown) => {
+    if (raw == null || raw === "") return;
+    if (typeof raw === "string") {
+      raw
+        .split(";")
+        .map(s => s.trim())
+        .filter(Boolean)
+        .forEach(s => chunks.push(s));
+    }
+  };
+
+  pushSplit(row.mediaUrls);
+  const list = row.mediaUrlList;
+  const arr = Array.isArray(list) ? list : list != null ? [list] : [];
+  for (const it of arr) {
+    if (typeof it === "string") pushSplit(it);
+    else if (it && typeof it === "object" && "url" in it) pushSplit((it as { url?: string }).url);
+  }
+
+  const seen = new Set<string>();
+  const tokens = chunks.filter(u => (seen.has(u) ? false : (seen.add(u), true)));
+
+  return tokens.map(u => ({
+    type: pathLooksVideo(u) ? ("video" as const) : ("image" as const),
+    url: u
+  }));
+}
+
+function getImagePreviewList(row: Record<string, unknown>): string[] {
+  return parseAchievementMedia(row)
+    .filter(m => m.type === "image")
+    .map(m => m.url);
+}
+
+function getImagePreviewIndex(url: string, row: Record<string, unknown>): number {
+  const imgs = getImagePreviewList(row);
+  return Math.max(0, imgs.indexOf(url));
+}
 
 const route = useRoute();
 const router = useRouter();
@@ -114,6 +167,9 @@ const loading = ref(false);
 const detail = ref<any>(null);
 const videoDialogVisible = ref(false);
 const previewVideo = ref("");
+const imagePreviewVisible = ref(false);
+const imagePreviewUrlList = ref<string[]>([]);
+const imagePreviewInitialIndex = ref(0);
 
 const activityId = computed(() => route.query.activityId as string);
 const userId = computed(() => route.query.userId as string);
@@ -138,147 +194,13 @@ const formatTime = (time: string | null | undefined) => {
   }
 };
 
-type MediaItem = { type: "image" | "video"; url: string; coverUrl?: string };
-
-const mediaList = computed<MediaItem[]>(() => {
-  if (!detail.value) return [];
-  const list: MediaItem[] = [];
-
-  // 优先处理 achievementList[0].mediaUrlList
-  if (detail.value.achievementList && detail.value.achievementList[0]?.mediaUrlList) {
-    const mediaUrlList = detail.value.achievementList[0].mediaUrlList;
-    const arr = Array.isArray(mediaUrlList) ? mediaUrlList : [mediaUrlList];
-    for (const it of arr) {
-      if (typeof it === "string") {
-        // 检查是否包含 | 分隔符(视频格式:xxx.mp4 | XXX.jpg)
-        if (it.includes("|")) {
-          const parts = it
-            .split("|")
-            .map(s => s.trim())
-            .filter(s => s);
-          if (parts.length >= 2) {
-            // 第一部分是视频URL,第二部分是封面URL
-            list.push({ type: "video", url: parts[0], coverUrl: parts[1] });
-          } else if (parts.length === 1) {
-            // 只有视频URL,没有封面
-            list.push({ type: "video", url: parts[0] });
-          }
-        } else {
-          // 如果是字符串,根据文件扩展名判断类型
-          const isVideo = /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it);
-          list.push({ type: isVideo ? "video" : "image", url: it });
-        }
-      } else if (it?.url) {
-        // 如果是对象,使用 type 字段或根据 url 判断
-        const isVideo = it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.url);
-        list.push({ type: isVideo ? "video" : "image", url: it.url, coverUrl: it.coverUrl });
-      } else if (it?.mediaUrl) {
-        // 处理 mediaUrl 字段
-        const isVideo = it.mediaType === "video" || it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.mediaUrl);
-        list.push({ type: isVideo ? "video" : "image", url: it.mediaUrl, coverUrl: it.coverUrl });
-      }
-    }
-  }
-
-  // 处理 mediaList 字段(可能是数组或对象数组)
-  const raw = detail.value.mediaList ?? detail.value.images ?? detail.value.videos ?? [];
-  const arr = Array.isArray(raw) ? raw : [raw];
-  for (const it of arr) {
-    if (typeof it === "string") {
-      // 检查是否包含 | 分隔符(视频格式:xxx.mp4 | XXX.jpg)
-      if (it.includes("|")) {
-        const parts = it
-          .split("|")
-          .map(s => s.trim())
-          .filter(s => s);
-        if (parts.length >= 2) {
-          // 第一部分是视频URL,第二部分是封面URL
-          list.push({ type: "video", url: parts[0], coverUrl: parts[1] });
-        } else if (parts.length === 1) {
-          // 只有视频URL,没有封面
-          list.push({ type: "video", url: parts[0] });
-        }
-      } else {
-        const isVideo = /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it);
-        list.push({ type: isVideo ? "video" : "image", url: it });
-      }
-    } else if (it?.url) {
-      const isVideo = it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.url);
-      list.push({ type: isVideo ? "video" : "image", url: it.url, coverUrl: it.coverUrl });
-    } else if (it?.mediaUrl) {
-      // 处理 mediaUrl 字段
-      const isVideo = it.mediaType === "video" || it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.mediaUrl);
-      list.push({ type: isVideo ? "video" : "image", url: it.mediaUrl, coverUrl: it.coverUrl });
-    }
-  }
-
-  // 处理单独的图片和视频数组
-  if (detail.value.resultImages && Array.isArray(detail.value.resultImages))
-    detail.value.resultImages.forEach((u: string) => list.push({ type: "image", url: u }));
-  if (detail.value.resultVideos && Array.isArray(detail.value.resultVideos))
-    detail.value.resultVideos.forEach((u: string) => list.push({ type: "video", url: u }));
-
-  return list.filter(Boolean);
-});
-
-const imageList = computed(() => mediaList.value.filter(m => m.type === "image").map(m => m.url));
-
-// 判断是否为视频URL(包含 | 分隔符)
-const isVideoUrl = (url: string) => {
-  if (typeof url !== "string") return false;
-  return url.includes("|");
-};
-
-// 获取视频URL(从 "视频链接|封面链接" 中提取视频链接)
-const getVideoUrl = (url: string) => {
-  if (typeof url !== "string") return "";
-  if (url.includes("|")) {
-    return url.split("|")[0].trim();
-  }
-  return url;
-};
-
-// 获取视频封面URL(从 "视频链接|封面链接" 中提取封面链接)
-const getVideoCoverUrl = (url: string) => {
-  if (typeof url !== "string") return "";
-  if (url.includes("|")) {
-    const parts = url.split("|");
-    if (parts.length >= 2) {
-      return parts[1].trim();
-    }
-  }
-  return "";
-};
-
-// 获取指定 mediaUrlList 中的图片列表(用于预览)
-const getImageListForItem = (mediaUrlList: any[]) => {
-  if (!mediaUrlList || !Array.isArray(mediaUrlList)) return [];
-  return mediaUrlList
-    .filter(item => {
-      // 过滤出图片类型(不包含 | 分隔符的)
-      if (typeof item === "string") {
-        return !item.includes("|");
-      }
-      return false;
-    })
-    .map(item => {
-      if (typeof item === "string") {
-        return item.trim();
-      }
-      return "";
-    })
-    .filter(url => url);
-};
-
-// 获取图片在指定 mediaUrlList 中的索引
-const getImageIndex = (url: string, mediaUrlList: any[]) => {
-  const imageList = getImageListForItem(mediaUrlList);
-  return imageList.indexOf(url);
-};
-
-const previewImage = (url: string, mediaUrlList: any[], index: any) => {
-  // el-image 的 preview-src-list 会自动处理预览
-};
+function openImagePreview(row: Record<string, unknown>, url: string) {
+  const list = getImagePreviewList(row);
+  if (!list.length) return;
+  imagePreviewUrlList.value = list;
+  imagePreviewInitialIndex.value = getImagePreviewIndex(url, row);
+  imagePreviewVisible.value = true;
+}
 
 const playVideo = (url: string) => {
   previewVideo.value = url;
@@ -386,18 +308,17 @@ onMounted(async () => {
   justify-content: center;
   width: 200px;
   height: 200px;
-  background: #000000;
+  background: #1a1a1a;
 }
-.media-placeholder {
-  display: flex;
-  align-items: center;
-  justify-content: center;
+.video-thumb {
+  position: absolute;
+  inset: 0;
+  display: block;
   width: 100%;
   height: 100%;
-  color: #909399;
-}
-.play-icon {
-  font-size: 40px;
+  pointer-events: none;
+  object-fit: cover;
+  background: #1a1a1a;
 }
 .video-overlay {
   position: absolute;