WholeProcessPlatform/frontend-sjgl/src/components/previewMedia/index.vue

316 lines
8.0 KiB
Vue
Raw Normal View History

2026-07-20 16:02:51 +08:00
<template>
<!-- 媒体预览 Modal -->
<a-modal
:open="visible"
:title="videoPreviewTitle"
:footer="null"
width="900px"
@cancel="closeMediaPreview"
:zIndex="2000"
>
<div class="flex h-[60vh] gap-4">
<!-- 左侧混合列表 (图片+视频) -->
<div class="w-1/4 overflow-y-auto border-r pr-2 media-list-container">
<div
v-for="(item, index) in previewList"
:key="index"
class="mb-2 cursor-pointer list-item"
:class="{ select: index == currentMediaIndex }"
@click="currentMediaIndex = index"
>
<span class="file-name">{{ item.name }}</span>
<!-- 删除按钮 -->
<div
class="list-item-delete"
v-if="isDel"
@click.stop="handleDeleteMedia(item, index)"
>
<CloseCircleOutlined />
</div>
</div>
</div>
<!-- 右侧动态预览区域 -->
<div
class="w-3/4 flex flex-col items-center justify-center bg-gray-100 relative p-4"
>
<a-spin v-if="imageLoading" tip="图片加载中..." size="large" />
<!-- 图片预览 -->
<div
v-if="
currentMediaItem &&
currentMediaItem.url != '' &&
currentMediaItem.type === 'image'
"
>
<img
v-show="!imageLoading && !imageLoadError"
:src="currentMediaItem.url"
alt="preview"
class="max-w-full max-h-[50vh] object-contain"
@load="onImageLoad"
@error="onImageError"
/>
<!-- 3. 加载失败提示 -->
<div
v-if="!imageLoading && imageLoadError"
class="text-gray-400 flex flex-col items-center"
>
<ExclamationCircleOutlined style="font-size: 24px; margin-bottom: 8px" />
<span>图片加载失败</span>
</div>
</div>
<div
v-else-if="
currentMediaItem &&
currentMediaItem.url == '' &&
currentMediaItem.type === 'image'
"
class="text-gray-400"
>
暂无图片预览
</div>
<!-- 视频预览 -->
<video
v-else-if="
currentMediaItem &&
currentMediaItem.url != '' &&
currentMediaItem.type === 'video'
"
:src="currentMediaItem.url"
controls
autoplay
class="max-w-full max-h-[50vh]"
>
您的浏览器不支持视频播放
</video>
<div
v-else-if="
currentMediaItem &&
currentMediaItem.url == '' &&
currentMediaItem.type === 'video'
"
class="text-gray-400"
>
暂无视频预览
</div>
<!-- 底部切换控制 -->
<div class="absolute bottom-4 flex gap-4 z-10">
<a-button
shape="circle"
:icon="h(LeftOutlined)"
@click="prevMedia"
:disabled="currentMediaIndex === 0"
/>
<span class="self-center text-gray-600 bg-white px-2 rounded shadow-sm">
{{ currentMediaIndex + 1 }} / {{ previewList.length }}
</span>
<a-button
shape="circle"
:icon="h(RightOutlined)"
@click="nextMedia"
:disabled="currentMediaIndex === previewList.length - 1"
/>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, nextTick, computed, watch, h } from "vue";
import { message, Modal } from "ant-design-vue"; // 假设使用 ant-design-vue
import {
LeftOutlined,
RightOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons-vue";
import { deleteFile } from "@/api/guoYuSheShiShuJuTianBao";
// 图片加载状态
const imageLoading = ref(false);
const imageLoadError = ref(false);
interface MediaItem {
id: string;
type: "image" | "video";
name: string;
url: string;
}
const props = defineProps<{
visible: boolean;
videoPreviewTitle: string;
previewList: MediaItem[];
previewListIndex: number;
taskId?: string;
isDel?: boolean;
}>();
const emit = defineEmits(["update:visible", "deleteMedia"]);
const currentMediaIndex = ref(props.previewListIndex);
// 计算当前选中的项
const currentMediaItem = computed(() => props.previewList[currentMediaIndex.value]);
const closeMediaPreview = () => {
emit("update:visible", false);
nextTick(() => {
currentMediaIndex.value = 0;
});
};
// 【新增】统一切换逻辑
const prevMedia = () => {
if (currentMediaIndex.value > 0) {
currentMediaIndex.value--;
}
};
// 【新增】统一下一逻辑
const nextMedia = () => {
if (currentMediaIndex.value < props.previewList.length - 1) {
currentMediaIndex.value++;
}
};
// 【新增】统一删除逻辑
const handleDeleteMedia = (item: any, index: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要从预览列表中移除该项吗?",
zIndex: 2002,
onOk: async () => {
return new Promise(async (resolve, reject) => {
const params = {
id: item.id,
taskId: props.taskId,
type:
props.videoPreviewTitle == "图片预览"
? 1
: props.videoPreviewTitle == "视频预览"
? 2
: 0,
filename: item.name,
};
try {
let res: any = await deleteFile(params);
if (res && res?.code == 0) {
message.success("删除成功");
emit("deleteMedia", item, props.videoPreviewTitle, index);
if (props.previewList.length === 0) {
closeMediaPreview();
} else {
// 调整索引
if (index <= currentMediaIndex.value) {
currentMediaIndex.value = Math.max(0, currentMediaIndex.value - 1);
}
}
resolve(true);
} else {
message.error("删除失败");
reject();
}
} catch (e) {
message.error("删除失败");
reject();
}
}).catch(() => console.log("Oops errors!"));
},
});
};
// 【新增】图片加载完成回调
const onImageLoad = () => {
imageLoading.value = false;
imageLoadError.value = false;
};
// 【新增】图片加载失败回调
const onImageError = () => {
imageLoading.value = false;
imageLoadError.value = true;
};
watch(
() => props.previewListIndex,
(newIndex) => {
currentMediaIndex.value = newIndex;
}
);
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
if (props.videoPreviewTitle == "图片预览") {
imageLoading.value = true;
imageLoadError.value = false;
}
}
}
);
watch(currentMediaIndex, (newIndex) => {
const item = props.previewList[newIndex];
if (item && item.type === "image") {
imageLoading.value = true;
imageLoadError.value = false;
} else {
imageLoading.value = false;
imageLoadError.value = false;
}
});
</script>
<style scoped lang="scss">
.media-list-container {
background-color: rgb(234, 241, 251);
padding: 10px;
.list-item {
width: 100%;
height: 36px;
line-height: 36px;
padding: 0 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
&.select {
color: #ffffff;
background-color: rgb(37, 93, 138);
}
.list-item-delete {
color: red;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
margin-left: 8px;
&:hover {
transform: scale(1.1);
}
}
&:hover .list-item-delete {
opacity: 1;
}
}
}
</style>