316 lines
8.0 KiB
Vue
316 lines
8.0 KiB
Vue
<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>
|