Previewer
浮窗式图片预览,不遮罩、不抢焦点、不阻塞背景交互——形态类似 picture-in-picture。支持滚轮缩放、平移、多图切换、缩略图栏。
全局单例:再次 preview.show 自动关闭上一个浮窗(触发其 onClose),同一时间只能存在一个预览实例。
演示页已挂载 <Previewer />
本页脚本顶部已渲染 <Previewer />,点击下方按钮即可打开浮窗。自己接入时必须在应用 root 显式挂一次。
Anatomy
Previewer浮窗容器,在应用 root 挂一次。订阅全局单例 state 渲染当前活动预览preview命令式 helper,show/close/只读响应式 state
单图预览
preview.show(单值)
多图预览
数组 + 默认 index
多图时浮窗显示左右翻页按钮 + 底部缩略图栏。
通过点击项定位
替代手写 arr.indexOf(item):target 选项让组件在数组里查 index。
preview.show(images, { target })
点击下方按钮,浮窗将打开第 3 张图。
持有 close 句柄 + onClose
show 返回 close 闭包;旧 close 在新 show 触发后自动 noop。
保留 close 句柄
点击「打开」后,可通过「程序化关闭」主动关闭。
联动响应式 state
preview.isOpen / currentIndex / currentImage 是 Readonly<Ref<...>>。模板里使用前先在 setup 顶层赋值,Vue 3 的自动解包仅对 setup 顶层 ref 生效。
读取 isOpen / currentIndex
先打开图组,再观察下方实时变化。
false-1不要写 v-if="preview.isOpen"
对象属性访问绕过 Vue 模板的 ref 自动解包,会拿到 ref 对象本身(永远 truthy)。务必先 const isOpen = preview.isOpen 再用。
路由切换自动关闭
Previewer 不绑定路由,业务侧自行 watch:
<script setup lang="ts">
import { preview } from '@tripo3d/design';
const route = useRoute();
watch(() => route.fullPath, () => preview.close());
</script>交互行为
| 动作 | 结果 |
|---|---|
| 拖动标题栏 | 移动浮窗(按钮、图片区由 .cancel-drag class 排除,不参与窗口拖动) |
| 滚轮 | 阶跃缩放 ±0.25,范围 [0.5, 2] |
| 右下角 +/- | 同滚轮,按钮触发 |
| 拖图 | 缩放 > 1 时启用,自动 clamp 不出 viewport |
| 左右翻页 | 多图时显示,自动重置缩放/位移 |
| 缩略图 | 多图时底部显示,点击切换;当前项自动 scrollIntoView 居中 |
| 关闭按钮 | 触发 onClose,关闭浮窗 |
不响应:ESC 全局键盘、点击外部、route/store 联动。需要这些联动请在应用层自行 watch + preview.close()。
API
<Previewer /> Props
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
default-position | 'center' | { x?: number; y?: number } | 'center' | 浮窗每次打开的初始位置;x/y 缺失轴自动居中。preview.show 传 initialPosition 可覆盖 |
width | number | 448 | 浮窗宽度(px,含 padding) |
image-size | number | 424 | 主图区正方形边长(px) |
class | string | — | 透传到外层容器 |
preview 命令式 API
| 方法 | 签名 | 说明 |
|---|---|---|
preview.show | (src, options?) => () => void | 打开浮窗,返回 close 闭包;再次 show 自动关闭上一个并触发其 onClose |
preview.close | () => void | 关闭当前活动浮窗(不持有 close 句柄时使用) |
preview.show 选项
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
initialIndex | number | 0 | 默认打开的图片索引,越界 / 负数自动 clamp |
target | string | File | — | 替代手动 indexOf:传任一图,组件在数组里查 index |
onClose | () => void | — | 关闭时触发一次(含被新 show 替代的场景) |
initialPosition | { x?: number; y?: number } | — | 本次打开的初始位置,缺值用容器 default-position |
preview 只读响应式
| 字段 | 类型 | 说明 |
|---|---|---|
preview.isOpen | Readonly<Ref<boolean>> | 当前是否有活动浮窗 |
preview.currentIndex | Readonly<Ref<number>> | 当前展示的图片索引,未打开时 -1 |
preview.currentImage | Readonly<Ref<string | null>> | 当前图片 URL(File 已 resolve 为 ObjectURL) |
单例语义
const close1 = preview.show(a, { onClose: () => console.log('a closed') });
const close2 = preview.show(b); // 输出 'a closed',浮窗内容变 b
close1(); // noop(已被 close2 替代)
close2(); // 真正关闭 b旧 close 闭包内部带 token 校验,被新 show 替代后调用变 noop——不会误关后续打开的浮窗。
SSR
show / close 在 SSR 环境(globalThis.window === undefined)下是 noop:show 返回空 close,close 直接返回。<Previewer /> 用 <Teleport to="body">,仅在客户端 hydrate 后挂载,不影响 SSR 渲染。