From e23bd2696da945291a9b652f1af39ad1936f376b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=A0=E6=9C=A8?= Date: Mon, 26 Jul 2021 20:58:18 +0800 Subject: [PATCH] feat(preview): add more features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为Preview组件添加新的属性及事件 --- CHANGELOG.zh_CN.md | 4 + src/components/Preview/src/Functional.vue | 126 ++++++++++++++++++++-- src/components/Preview/src/functional.ts | 7 +- src/components/Preview/src/typing.ts | 19 ++++ src/views/demo/feat/img-preview/index.vue | 7 +- 5 files changed, 149 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.zh_CN.md b/CHANGELOG.zh_CN.md index 62753c64..7f3245da 100644 --- a/CHANGELOG.zh_CN.md +++ b/CHANGELOG.zh_CN.md @@ -1,3 +1,7 @@ +### ✨ Features + +- **Preview** 添加新的属性及事件 + ### 🐛 Bug Fixes - **ApiTreeSelect** 修复未能正确监听`params`变化的问题 diff --git a/src/components/Preview/src/Functional.vue b/src/components/Preview/src/Functional.vue index 6642e329..4a5e821b 100644 --- a/src/components/Preview/src/Functional.vue +++ b/src/components/Preview/src/Functional.vue @@ -38,13 +38,33 @@ type: Number as PropType, default: 0, }, + scaleStep: { + type: Number as PropType, + }, + defaultWidth: { + type: Number as PropType, + }, + maskClosable: { + type: Boolean as PropType, + }, + rememberState: { + type: Boolean as PropType, + }, }; const prefixCls = 'img-preview'; export default defineComponent({ name: 'ImagePreview', props, - setup(props: Props) { + emits: ['img-load', 'img-error'], + setup(props: Props, { expose, emit }) { + interface stateInfo { + scale: number; + rotate: number; + top: number; + left: number; + } + const stateMap = new Map(); const imgState = reactive({ currentUrl: '', imgScale: 1, @@ -96,6 +116,14 @@ }; } + const getScaleStep = computed(() => { + if (props.scaleStep > 0 && props.scaleStep < 100) { + return props.scaleStep / 100; + } else { + return imgState.imgScale / 10; + } + }); + // 监听鼠标滚轮 function scrollFunc(e: any) { e = e || window.event; @@ -104,11 +132,11 @@ e.preventDefault(); if (e.delta > 0) { // 滑轮向上滚动 - scaleFunc(0.015); + scaleFunc(getScaleStep.value); } if (e.delta < 0) { // 滑轮向下滚动 - scaleFunc(-0.015); + scaleFunc(-getScaleStep.value); } } // 缩放函数 @@ -134,11 +162,54 @@ imgState.status = StatueEnum.LOADING; const img = new Image(); img.src = url; - img.onload = () => { + img.onload = (e: Event) => { + if (imgState.currentUrl !== url) { + const ele: HTMLElement[] = e.composedPath(); + if (props.rememberState) { + // 保存当前图片的缩放信息 + stateMap.set(imgState.currentUrl, { + scale: imgState.imgScale, + top: imgState.imgTop, + left: imgState.imgLeft, + rotate: imgState.imgRotate, + }); + // 如果之前已存储缩放信息,就应用 + const stateInfo = stateMap.get(url); + if (stateInfo) { + imgState.imgScale = stateInfo.scale; + imgState.imgTop = stateInfo.top; + imgState.imgRotate = stateInfo.rotate; + imgState.imgLeft = stateInfo.left; + } else { + initState(); + if (props.defaultWidth) { + imgState.imgScale = props.defaultWidth / ele[0].naturalWidth; + } + } + } else { + if (props.defaultWidth) { + imgState.imgScale = props.defaultWidth / ele[0].naturalWidth; + } + } + + ele && + emit('img-load', { + index: imgState.currentIndex, + dom: ele[0] as HTMLImageElement, + url, + }); + } imgState.currentUrl = url; imgState.status = StatueEnum.DONE; }; - img.onerror = () => { + img.onerror = (e: Event) => { + const ele: EventTarget[] = e.composedPath(); + ele && + emit('img-error', { + index: imgState.currentIndex, + dom: ele[0] as HTMLImageElement, + url, + }); imgState.status = StatueEnum.FAIL; }; } @@ -146,6 +217,10 @@ // 关闭 function handleClose(e: MouseEvent) { e && e.stopPropagation(); + close(); + } + + function close() { imgState.show = false; // 移除火狐浏览器下的鼠标滚动事件 document.body.removeEventListener('DOMMouseScroll', scrollFunc); @@ -158,6 +233,19 @@ initState(); } + expose({ + resume, + close, + prev: handleChange.bind(null, 'left'), + next: handleChange.bind(null, 'right'), + setScale: (scale: number) => { + if (scale > 0 && scale <= 10) imgState.imgScale = scale; + }, + setRotate: (rotate: number) => { + imgState.imgRotate = rotate; + }, + } as PreviewActions); + // 上一页下一页 function handleChange(direction: 'left' | 'right') { const { currentIndex } = imgState; @@ -205,6 +293,7 @@ transform: `scale(${imgScale}) rotate(${imgRotate}deg)`, marginTop: `${imgTop}px`, marginLeft: `${imgLeft}px`, + maxWidth: props.defaultWidth ? 'unset' : '100%', }; }); @@ -222,6 +311,16 @@ } }); + const handleMaskClick = (e: MouseEvent) => { + if ( + props.maskClosable && + e.target && + (e.target as HTMLDivElement).classList.contains(`${prefixCls}-content`) + ) { + handleClose(e); + } + }; + const renderClose = () => { return (
@@ -246,10 +345,16 @@ const renderController = () => { return (
-
scaleFunc(-0.15)}> +
scaleFunc(-getScaleStep.value)} + >
-
scaleFunc(0.15)}> +
scaleFunc(getScaleStep.value)} + >
@@ -279,7 +384,12 @@ return () => { return ( imgState.show && ( -
+
{/*}*/} diff --git a/src/components/Preview/src/functional.ts b/src/components/Preview/src/functional.ts index 1f4ba671..74073a20 100644 --- a/src/components/Preview/src/functional.ts +++ b/src/components/Preview/src/functional.ts @@ -6,15 +6,12 @@ import { createVNode, render } from 'vue'; let instance: ReturnType | null = null; export function createImgPreview(options: Options) { if (!isClient) return; - const { imageList, show = true, index = 0 } = options; - const propsData: Partial = {}; const container = document.createElement('div'); - propsData.imageList = imageList; - propsData.show = show; - propsData.index = index; + Object.assign(propsData, { show: true, index: 0, scaleStep: 100 }, options); instance = createVNode(ImgPreview, propsData); render(instance, container); document.body.appendChild(container); + return instance.component?.exposed; } diff --git a/src/components/Preview/src/typing.ts b/src/components/Preview/src/typing.ts index 844b2541..ab0d6c75 100644 --- a/src/components/Preview/src/typing.ts +++ b/src/components/Preview/src/typing.ts @@ -2,6 +2,12 @@ export interface Options { show?: boolean; imageList: string[]; index?: number; + scaleStep?: number; + defaultWidth?: number; + maskClosable?: boolean; + rememberState?: boolean; + onImgLoad?: (img: HTMLImageElement) => void; + onImgError?: (img: HTMLImageElement) => void; } export interface Props { @@ -9,6 +15,19 @@ export interface Props { instance: Props; imageList: string[]; index: number; + scaleStep: number; + defaultWidth: number; + maskClosable: boolean; + rememberState: boolean; +} + +export interface PreviewActions { + resume: () => void; + close: () => void; + prev: () => void; + next: () => void; + setScale: (scale: number) => void; + setRotate: (rotate: number) => void; } export interface ImageProps { diff --git a/src/views/demo/feat/img-preview/index.vue b/src/views/demo/feat/img-preview/index.vue index 79a81c32..b5ece2ea 100644 --- a/src/views/demo/feat/img-preview/index.vue +++ b/src/views/demo/feat/img-preview/index.vue @@ -8,6 +8,7 @@ import { defineComponent } from 'vue'; import { createImgPreview, ImagePreview } from '/@/components/Preview/index'; import { PageWrapper } from '/@/components/Page'; + // import { PreviewActions } from '/@/components/Preview/src/typing'; const imgList: string[] = [ 'https://picsum.photos/id/66/346/216', @@ -18,7 +19,11 @@ components: { PageWrapper, ImagePreview }, setup() { function openImg() { - createImgPreview({ imageList: imgList }); + const onImgLoad = ({ index, url, dom }) => { + console.log(`第${index + 1}张图片已加载,URL为:${url}`, dom); + }; + // 可以使用createImgPreview返回的 PreviewActions 来控制预览逻辑,实现类似幻灯片、自动旋转之类的骚操作 + createImgPreview({ imageList: imgList, defaultWidth: 700, rememberState: true, onImgLoad }); } return { imgList, openImg }; }, -- GitLab