| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- <template>
- <div class="case-detail">
- <div class="header">
- <el-button @click="goBack"> 返回 </el-button>
- <h2 class="title">案例详情</h2>
- </div>
- <el-card v-loading="loading">
- <template v-if="detail">
- <div class="detail-list">
- <div class="detail-item">
- <div class="detail-label">所属活动 : {{ detail.activityName || detail.activityTitle || "-" }}</div>
- </div>
- <div class="detail-item">
- <div class="detail-label">用户昵称 : {{ detail.nickName || detail.nickname || "-" }}</div>
- </div>
- <div class="detail-item">
- <div class="detail-label">姓名 : {{ detail.userName || "-" }}</div>
- </div>
- <div class="detail-item">
- <div class="detail-label">联系方式 : {{ detail.phone || "-" }}</div>
- </div>
- <div class="detail-item">
- <div class="detail-label">报名时间 : {{ formatTime(detail.createdTime) }}</div>
- </div>
- </div>
- <div v-if="detail.achievementList">
- <div class="result-section" v-for="(item, index) in detail.achievementList" :key="index">
- <div class="result-title">成果展示</div>
- <div class="detail-list">
- <div class="detail-item">
- <div class="detail-label">更新时间 : {{ formatTime(item.updatedTime) }}</div>
- </div>
- <div class="detail-item">
- <div class="detail-label">成果描述 : {{ item.achievementDesc || "-" }}</div>
- </div>
- <div class="detail-item" v-if="mediaList.length > 0">
- <div class="detail-label">图片与视频 :</div>
- <div class="media-grid">
- <template v-for="(item, index) in mediaList" :key="index">
- <div v-if="item.type === 'video'" class="media-item video-item" @click="playVideo(item.url)">
- <el-image v-if="item.coverUrl" :src="item.coverUrl" 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>
- <!-- 视频播放图标覆盖层 -->
- <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.url, index)">
- <el-image
- :src="item.url"
- fit="cover"
- class="media-image"
- :preview-src-list="imageList"
- :initial-index="getImageIndex(item.url)"
- >
- <template #error>
- <div class="image-slot">
- <el-icon>
- <Picture />
- </el-icon>
- </div>
- </template>
- </el-image>
- </div>
- </template>
- </div>
- </div>
- <div class="detail-item" v-else>
- <div class="detail-label">暂无成果展示</div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <el-empty v-else-if="!loading" description="暂无数据" />
- </el-card>
- <el-dialog v-model="videoDialogVisible" title="视频预览" width="640px" destroy-on-close @close="previewVideo = ''">
- <video v-if="previewVideo" :src="previewVideo" controls autoplay class="dialog-video" />
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts" name="caseDetail">
- import { ref, onMounted, computed } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import { Picture, VideoPlay } from "@element-plus/icons-vue";
- import { getPersonCaseDetail } from "@/api/modules/operationManagement";
- const route = useRoute();
- const router = useRouter();
- const loading = ref(false);
- const detail = ref<any>(null);
- const videoDialogVisible = ref(false);
- const previewVideo = ref("");
- const activityId = computed(() => route.query.activityId as string);
- const userId = computed(() => route.query.userId as string);
- const goBack = () => {
- router.push({ path: "/operationManagement/cases" });
- };
- const formatTime = (time: string | null | undefined) => {
- if (!time) return "-";
- try {
- const date = new Date(time);
- const y = date.getFullYear();
- const m = String(date.getMonth() + 1).padStart(2, "0");
- const d = String(date.getDate()).padStart(2, "0");
- const h = String(date.getHours()).padStart(2, "0");
- const min = String(date.getMinutes()).padStart(2, "0");
- const s = String(date.getSeconds()).padStart(2, "0");
- return `${y}/${m}/${d} ${h}:${min}:${s}`;
- } catch {
- return time;
- }
- };
- 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));
- const getImageIndex = (url: string) => {
- return imageList.value.indexOf(url);
- };
- const previewImage = (url: string, index: any) => {
- // el-image 的 preview-src-list 会自动处理预览
- };
- const playVideo = (url: string) => {
- previewVideo.value = url;
- videoDialogVisible.value = true;
- };
- onMounted(async () => {
- if (!activityId.value || !userId.value) return;
- loading.value = true;
- try {
- const res = await getPersonCaseDetail({
- activityId: Number(activityId.value),
- userId: Number(userId.value)
- });
- detail.value = res?.data ?? res ?? null;
- } catch {
- detail.value = null;
- } finally {
- loading.value = false;
- }
- });
- </script>
- <style scoped lang="scss">
- .case-detail {
- min-height: 100%;
- padding: 16px;
- background: #ffffff;
- }
- .header {
- display: flex;
- gap: 16px;
- align-items: center;
- margin-bottom: 16px;
- }
- .title {
- margin: 0;
- font-size: 18px;
- font-weight: 600;
- }
- .detail-list {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .detail-item {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .detail-label {
- font-size: 14px;
- font-weight: 500;
- color: #606266;
- }
- .detail-value {
- font-size: 14px;
- color: #303133;
- word-break: break-all;
- }
- .result-section {
- margin-top: 24px;
- }
- .result-title {
- margin-bottom: 12px;
- font-size: 16px;
- font-weight: 600;
- }
- .result-desc {
- font-size: 14px;
- line-height: 1.6;
- color: #303133;
- word-break: break-all;
- white-space: pre-wrap;
- }
- .media-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 12px;
- margin-top: 8px;
- }
- .media-item {
- position: relative;
- width: 200px;
- height: 200px;
- overflow: hidden;
- cursor: pointer;
- background: #f5f7fa;
- border-radius: 8px;
- }
- .image-item {
- width: 200px;
- height: 200px;
- }
- .media-image {
- display: block;
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .video-item {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 200px;
- height: 200px;
- background: #000000;
- }
- .media-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- color: #909399;
- }
- .play-icon {
- font-size: 40px;
- }
- .video-overlay {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- pointer-events: none;
- background: rgb(0 0 0 / 30%);
- border-radius: 8px;
- }
- .play-icon-overlay {
- font-size: 48px;
- color: #ffffff;
- opacity: 0.9;
- }
- .image-slot {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- color: #909399;
- background: #f5f7fa;
- }
- .dialog-video {
- display: block;
- width: 100%;
- max-height: 70vh;
- }
- </style>
|