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>
|