This commit is contained in:
itcsx committed 2026-09-22 23:09:11 +08:00
1 parent 4adae1bd7d
commit adcf94df4a
17 files changed
+274 -97

No files matched your search

+100 -15
View File
@@ -1,9 +1,10 @@
<template>
<div class="psp">
<!-- 图片舞台 -->
<div class="psp-stage" ref="stageEl" :class="{ 'is-paused': !isPlaying && !!current }"
<div class="psp-stage" ref="stageEl"
:class="{ 'is-paused': !isPlaying && !!current, 'is-immersive': !controlsVisible }"
:style="{ '--psp-kb': interval + 0.8 + 's', '--psp-out-span': outPlan.span + 's' }"
:title="isPlaying ? '单击暂停' : '单击看大图'" @click="onStageClick">
:title="stageTitle" @click="onStageClick">
<!-- 组合类:多图同台 + 相框 / 气泡 / 花瓣 / 彩纸 / 聚光 / 灯柱 / 舞台 / 荧光棒(演唱会等 5 套) -->
<!-- out-in:上一屏彻底走完才让下一屏进场,两屏绝不重叠(逐张离场也等最后一张飞完) -->
<transition v-if="isShowcase" :name="'psp-fx-' + fxName" :duration="fxDuration" mode="out-in">
@@ -71,12 +72,25 @@
<PictureOutlined />
<p>暂无图片</p>
</div>
<!-- 控制区收起后画面上留一个恢复入口:整页用法没有弹窗右上角按钮,
否则收起后就再也点不回来了 -->
<button v-if="!controlsVisible" class="psp-restore" type="button" title="显示控制区"
@click.stop="toggleControls">
<EyeOutlined />
</button>
</div>
<!-- 控制条 -->
<footer class="psp-bar">
<!-- 控制条(收起后舞台吃掉这部分空间,图片最大化) -->
<footer v-if="controlsVisible" class="psp-bar">
<!-- 功能图标行 -->
<div class="psp-actions">
<!-- 隐藏/显示控制区:整页用法时父组件会顺带藏掉 layout 顶栏,做到整页全屏 -->
<button class="psp-act" type="button" :class="{ 'is-on': !controlsVisible }"
:title="controlsVisible ? '隐藏控制区' : '显示控制区'" @click.stop="toggleControls">
<EyeInvisibleOutlined v-if="controlsVisible" />
<EyeOutlined v-else />
</button>
<button class="psp-act" type="button" title="下载当前图片" @click.stop="onDownload">
<DownloadOutlined />
<span class="psp-badge" :class="{ 'is-on': downloaded }">{{ downloaded ? "" : "" }}</span>
@@ -177,8 +191,9 @@
</transition>
<!-- 精看:暂停时单击图片放大看。复用 ant-design 图片预览(自带缩放 / 旋转 / 翻转),
图片本体藏起来,预览层由组件自己渲染,不受舞台裁切影响 -->
<AImage :src="current?.url || ''" :style="{ display: 'none' }"
图片本体藏起来,预览层由组件自己渲染,不受舞台裁切影响。
这里用原图(full),舞台轮播放的是缩略图,精看要看清楚 -->
<AImage :src="currentFull" :style="{ display: 'none' }"
:preview="{ visible: picVisible, onVisibleChange: setPicVisible }" />
</div>
</template>
@@ -187,20 +202,24 @@
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { Image as AImage, message } from "ant-design-vue";
import { request } from "@/utils/request";
import { useVModel } from "@vueuse/core";
import {
DownOutlined, DownloadOutlined, CloseOutlined, PictureOutlined, EllipsisOutlined,
ThunderboltOutlined, RetweetOutlined, SwapOutlined, StepBackwardFilled,
StepForwardFilled, CaretRightFilled, PauseOutlined, UnorderedListOutlined,
EyeOutlined, EyeInvisibleOutlined,
} from "@ant-design/icons-vue";
/** 一张图片 */
export interface PhotoSlideItem {
/** 原图地址 */
/** 展示地址(舞台幻灯片 + 列表缩略图):一般给缩略图,轮播加载更快 */
url: string;
/** 缩略图地址(列表用,缺省用原图) */
/** 缩略图地址(列表用,缺省用 url) */
thumb?: string;
/** 名称(下载文件名) */
name?: string;
/** 原图地址:精看与下载用它,缺省退回 url */
full?: string;
}
const props = withDefaults(
@@ -208,10 +227,15 @@ const props = withDefaults(
items?: PhotoSlideItem[];
/** 每张停留秒数 */
interval?: number;
/** 底部控制区是否显示(页面可用它做「整页全屏」) */
controls?: boolean;
}>(),
{ items: () => [], interval: 5 }
{ items: () => [], interval: 5, controls: true }
);
const emit = defineEmits<{ (e: "close"): void }>();
const emit = defineEmits<{
(e: "close"): void;
(e: "update:controls", visible: boolean): void;
}>();
/* ---------------- 基础状态 ---------------- */
const items = computed(() => props.items || []);
@@ -225,12 +249,30 @@ const showList = ref(false);
const menu = ref<"" | "effect" | "more">("");
const downloaded = ref(false);
/**
* 底部控制区是否显示:由功能图标行的眼睛图标切换。
* 收起后舞台吃掉这部分空间,图片最大化;父组件可用 v-model:controls 拿到这个状态
* 去做整页全屏(例如 Preview.vue 藏掉 layout 顶部的两条栏)。
*
* passive: true 是关键:不传的话 useVModel 直接读写 props,
* 父组件没绑 v-model:controls 时内部改了值只会 emit 出去、props 不变,
* 按钮就"点了没反应"(与 VideoPlayer 一致)。
*/
const controlsVisible = useVModel(props, "controls", emit, { passive: true });
const toggleControls = () => {
menu.value = ""; // 顺手收起可能打开的弹层(特效 / 更多)
controlsVisible.value = !controlsVisible.value;
};
/** 舞台元素与像素尺寸:圆形格子的直径按舞台短边换算,避免非正方舞台被压成椭圆 */
const stageEl = ref<HTMLElement | null>(null);
const stageSize = ref({ w: 0, h: 0 });
let stageRo: ResizeObserver | null = null;
const current = computed<PhotoSlideItem | null>(() => items.value[index.value] ?? null);
/** 精看与下载用原图(full),没单独给就退回展示地址 */
const currentFull = computed(() => current.value?.full || current.value?.url || "");
const modeTitle = computed(
() => ({ list: "顺序循环", random: "随机播放", once: "播完停止" }[mode.value])
);
@@ -789,15 +831,30 @@ const picVisible = ref(false);
const setPicVisible = (v: boolean) => {
picVisible.value = v;
};
/** 单击舞台:播放中先暂停(原来的行为);已经暂停则打开精看,效果同 DmsFileEnquiry 的单图精看 */
/**
* 单击舞台:播放中先暂停(原来的行为);已经暂停则打开精看,效果同 DmsFileEnquiry 的单图精看。
*
* 页内全屏(控制区已收起)时只切暂停/播放:那会儿没有控制条,
* 再叠一层大图预览会盖住整屏、也没有恢复入口,容易把人"困住"。
*/
const onStageClick = () => {
if (!items.value.length) return;
if (!controlsVisible.value) {
toggle();
return;
}
if (isPlaying.value) {
toggle();
return;
}
setPicVisible(true);
};
/** 舞台提示:页内全屏时没有"看大图",文案跟着变,免得点了跟说好的不一样 */
const stageTitle = computed(() => {
if (!controlsVisible.value) return isPlaying.value ? "单击暂停" : "单击播放";
return isPlaying.value ? "单击暂停" : "单击看大图";
});
const playAt = (i: number) => {
index.value = i;
elapsed.value = 0;
@@ -848,13 +905,15 @@ const onDownload = async () => {
if (!it || downloading.value) return;
downloading.value = true;
try {
// 下载要原图,不是舞台上那张缩略图
const url = currentFull.value;
const res = await request<ArrayBuffer>({
url: it.url,
url,
method: "get",
responseType: "arraybuffer",
});
const blobUrl = window.URL.createObjectURL(new Blob([res]));
const ext = (it.url.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1] || "jpg";
const ext = (url.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1] || "jpg";
const a = document.createElement("a");
a.href = blobUrl;
a.download = `${it.name || "image"}.${ext}`;
@@ -963,11 +1022,37 @@ onBeforeUnmount(() => {
cursor: pointer;
}
/* 暂停后单击就是精看,光标跟着变 */
.psp-stage.is-paused {
/* 暂停后单击就是精看,光标跟着变;
页内全屏(.is-immersive)时单击是继续播放,不给放大镜光标,免得误导 */
.psp-stage.is-paused:not(.is-immersive) {
cursor: zoom-in;
}
/* 控制区收起后的恢复入口(整页用法没有弹窗右上角按钮) */
.psp-restore {
position: absolute;
top: 14px;
right: 14px;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
font-size: 18px;
color: #fff;
background: rgba(0, 0, 0, .38);
border: 0;
border-radius: 10px;
cursor: pointer;
opacity: .55;
transition: opacity .2s;
}
.psp-restore:hover {
opacity: 1;
}
.psp-img {
position: absolute;
inset: 0;