Commit e23bd2696da945291a9b652f1af39ad1936f376b

Authored by 无木
1 parent 98749ec6

feat(preview): add more features

为Preview组件添加新的属性及事件
CHANGELOG.zh_CN.md
  1 +### ✨ Features
  2 +
  3 +- **Preview** 添加新的属性及事件
  4 +
1 ### 🐛 Bug Fixes 5 ### 🐛 Bug Fixes
2 6
3 - **ApiTreeSelect** 修复未能正确监听`params`变化的问题 7 - **ApiTreeSelect** 修复未能正确监听`params`变化的问题
src/components/Preview/src/Functional.vue
@@ -38,13 +38,33 @@ @@ -38,13 +38,33 @@
38 type: Number as PropType<number>, 38 type: Number as PropType<number>,
39 default: 0, 39 default: 0,
40 }, 40 },
  41 + scaleStep: {
  42 + type: Number as PropType<number>,
  43 + },
  44 + defaultWidth: {
  45 + type: Number as PropType<number>,
  46 + },
  47 + maskClosable: {
  48 + type: Boolean as PropType<boolean>,
  49 + },
  50 + rememberState: {
  51 + type: Boolean as PropType<boolean>,
  52 + },
41 }; 53 };
42 54
43 const prefixCls = 'img-preview'; 55 const prefixCls = 'img-preview';
44 export default defineComponent({ 56 export default defineComponent({
45 name: 'ImagePreview', 57 name: 'ImagePreview',
46 props, 58 props,
47 - setup(props: Props) { 59 + emits: ['img-load', 'img-error'],
  60 + setup(props: Props, { expose, emit }) {
  61 + interface stateInfo {
  62 + scale: number;
  63 + rotate: number;
  64 + top: number;
  65 + left: number;
  66 + }
  67 + const stateMap = new Map<string, stateInfo>();
48 const imgState = reactive<ImgState>({ 68 const imgState = reactive<ImgState>({
49 currentUrl: '', 69 currentUrl: '',
50 imgScale: 1, 70 imgScale: 1,
@@ -96,6 +116,14 @@ @@ -96,6 +116,14 @@
96 }; 116 };
97 } 117 }
98 118
  119 + const getScaleStep = computed(() => {
  120 + if (props.scaleStep > 0 && props.scaleStep < 100) {
  121 + return props.scaleStep / 100;
  122 + } else {
  123 + return imgState.imgScale / 10;
  124 + }
  125 + });
  126 +
99 // 监听鼠标滚轮 127 // 监听鼠标滚轮
100 function scrollFunc(e: any) { 128 function scrollFunc(e: any) {
101 e = e || window.event; 129 e = e || window.event;
@@ -104,11 +132,11 @@ @@ -104,11 +132,11 @@
104 e.preventDefault(); 132 e.preventDefault();
105 if (e.delta > 0) { 133 if (e.delta > 0) {
106 // 滑轮向上滚动 134 // 滑轮向上滚动
107 - scaleFunc(0.015); 135 + scaleFunc(getScaleStep.value);
108 } 136 }
109 if (e.delta < 0) { 137 if (e.delta < 0) {
110 // 滑轮向下滚动 138 // 滑轮向下滚动
111 - scaleFunc(-0.015); 139 + scaleFunc(-getScaleStep.value);
112 } 140 }
113 } 141 }
114 // 缩放函数 142 // 缩放函数
@@ -134,11 +162,54 @@ @@ -134,11 +162,54 @@
134 imgState.status = StatueEnum.LOADING; 162 imgState.status = StatueEnum.LOADING;
135 const img = new Image(); 163 const img = new Image();
136 img.src = url; 164 img.src = url;
137 - img.onload = () => { 165 + img.onload = (e: Event) => {
  166 + if (imgState.currentUrl !== url) {
  167 + const ele: HTMLElement[] = e.composedPath();
  168 + if (props.rememberState) {
  169 + // 保存当前图片的缩放信息
  170 + stateMap.set(imgState.currentUrl, {
  171 + scale: imgState.imgScale,
  172 + top: imgState.imgTop,
  173 + left: imgState.imgLeft,
  174 + rotate: imgState.imgRotate,
  175 + });
  176 + // 如果之前已存储缩放信息,就应用
  177 + const stateInfo = stateMap.get(url);
  178 + if (stateInfo) {
  179 + imgState.imgScale = stateInfo.scale;
  180 + imgState.imgTop = stateInfo.top;
  181 + imgState.imgRotate = stateInfo.rotate;
  182 + imgState.imgLeft = stateInfo.left;
  183 + } else {
  184 + initState();
  185 + if (props.defaultWidth) {
  186 + imgState.imgScale = props.defaultWidth / ele[0].naturalWidth;
  187 + }
  188 + }
  189 + } else {
  190 + if (props.defaultWidth) {
  191 + imgState.imgScale = props.defaultWidth / ele[0].naturalWidth;
  192 + }
  193 + }
  194 +
  195 + ele &&
  196 + emit('img-load', {
  197 + index: imgState.currentIndex,
  198 + dom: ele[0] as HTMLImageElement,
  199 + url,
  200 + });
  201 + }
138 imgState.currentUrl = url; 202 imgState.currentUrl = url;
139 imgState.status = StatueEnum.DONE; 203 imgState.status = StatueEnum.DONE;
140 }; 204 };
141 - img.onerror = () => { 205 + img.onerror = (e: Event) => {
  206 + const ele: EventTarget[] = e.composedPath();
  207 + ele &&
  208 + emit('img-error', {
  209 + index: imgState.currentIndex,
  210 + dom: ele[0] as HTMLImageElement,
  211 + url,
  212 + });
142 imgState.status = StatueEnum.FAIL; 213 imgState.status = StatueEnum.FAIL;
143 }; 214 };
144 } 215 }
@@ -146,6 +217,10 @@ @@ -146,6 +217,10 @@
146 // 关闭 217 // 关闭
147 function handleClose(e: MouseEvent) { 218 function handleClose(e: MouseEvent) {
148 e && e.stopPropagation(); 219 e && e.stopPropagation();
  220 + close();
  221 + }
  222 +
  223 + function close() {
149 imgState.show = false; 224 imgState.show = false;
150 // 移除火狐浏览器下的鼠标滚动事件 225 // 移除火狐浏览器下的鼠标滚动事件
151 document.body.removeEventListener('DOMMouseScroll', scrollFunc); 226 document.body.removeEventListener('DOMMouseScroll', scrollFunc);
@@ -158,6 +233,19 @@ @@ -158,6 +233,19 @@
158 initState(); 233 initState();
159 } 234 }
160 235
  236 + expose({
  237 + resume,
  238 + close,
  239 + prev: handleChange.bind(null, 'left'),
  240 + next: handleChange.bind(null, 'right'),
  241 + setScale: (scale: number) => {
  242 + if (scale > 0 && scale <= 10) imgState.imgScale = scale;
  243 + },
  244 + setRotate: (rotate: number) => {
  245 + imgState.imgRotate = rotate;
  246 + },
  247 + } as PreviewActions);
  248 +
161 // 上一页下一页 249 // 上一页下一页
162 function handleChange(direction: 'left' | 'right') { 250 function handleChange(direction: 'left' | 'right') {
163 const { currentIndex } = imgState; 251 const { currentIndex } = imgState;
@@ -205,6 +293,7 @@ @@ -205,6 +293,7 @@
205 transform: `scale(${imgScale}) rotate(${imgRotate}deg)`, 293 transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,
206 marginTop: `${imgTop}px`, 294 marginTop: `${imgTop}px`,
207 marginLeft: `${imgLeft}px`, 295 marginLeft: `${imgLeft}px`,
  296 + maxWidth: props.defaultWidth ? 'unset' : '100%',
208 }; 297 };
209 }); 298 });
210 299
@@ -222,6 +311,16 @@ @@ -222,6 +311,16 @@
222 } 311 }
223 }); 312 });
224 313
  314 + const handleMaskClick = (e: MouseEvent) => {
  315 + if (
  316 + props.maskClosable &&
  317 + e.target &&
  318 + (e.target as HTMLDivElement).classList.contains(`${prefixCls}-content`)
  319 + ) {
  320 + handleClose(e);
  321 + }
  322 + };
  323 +
225 const renderClose = () => { 324 const renderClose = () => {
226 return ( 325 return (
227 <div class={`${prefixCls}__close`} onClick={handleClose}> 326 <div class={`${prefixCls}__close`} onClick={handleClose}>
@@ -246,10 +345,16 @@ @@ -246,10 +345,16 @@
246 const renderController = () => { 345 const renderController = () => {
247 return ( 346 return (
248 <div class={`${prefixCls}__controller`}> 347 <div class={`${prefixCls}__controller`}>
249 - <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(-0.15)}> 348 + <div
  349 + class={`${prefixCls}__controller-item`}
  350 + onClick={() => scaleFunc(-getScaleStep.value)}
  351 + >
250 <img src={unScaleSvg} /> 352 <img src={unScaleSvg} />
251 </div> 353 </div>
252 - <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}> 354 + <div
  355 + class={`${prefixCls}__controller-item`}
  356 + onClick={() => scaleFunc(getScaleStep.value)}
  357 + >
253 <img src={scaleSvg} /> 358 <img src={scaleSvg} />
254 </div> 359 </div>
255 <div class={`${prefixCls}__controller-item`} onClick={resume}> 360 <div class={`${prefixCls}__controller-item`} onClick={resume}>
@@ -279,7 +384,12 @@ @@ -279,7 +384,12 @@
279 return () => { 384 return () => {
280 return ( 385 return (
281 imgState.show && ( 386 imgState.show && (
282 - <div class={prefixCls} ref={wrapElRef} onMouseup={handleMouseUp}> 387 + <div
  388 + class={prefixCls}
  389 + ref={wrapElRef}
  390 + onMouseup={handleMouseUp}
  391 + onClick={handleMaskClick}
  392 + >
283 <div class={`${prefixCls}-content`}> 393 <div class={`${prefixCls}-content`}>
284 {/*<Spin*/} 394 {/*<Spin*/}
285 {/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/} 395 {/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}
src/components/Preview/src/functional.ts
@@ -6,15 +6,12 @@ import { createVNode, render } from &#39;vue&#39;; @@ -6,15 +6,12 @@ import { createVNode, render } from &#39;vue&#39;;
6 let instance: ReturnType<typeof createVNode> | null = null; 6 let instance: ReturnType<typeof createVNode> | null = null;
7 export function createImgPreview(options: Options) { 7 export function createImgPreview(options: Options) {
8 if (!isClient) return; 8 if (!isClient) return;
9 - const { imageList, show = true, index = 0 } = options;  
10 -  
11 const propsData: Partial<Props> = {}; 9 const propsData: Partial<Props> = {};
12 const container = document.createElement('div'); 10 const container = document.createElement('div');
13 - propsData.imageList = imageList;  
14 - propsData.show = show;  
15 - propsData.index = index; 11 + Object.assign(propsData, { show: true, index: 0, scaleStep: 100 }, options);
16 12
17 instance = createVNode(ImgPreview, propsData); 13 instance = createVNode(ImgPreview, propsData);
18 render(instance, container); 14 render(instance, container);
19 document.body.appendChild(container); 15 document.body.appendChild(container);
  16 + return instance.component?.exposed;
20 } 17 }
src/components/Preview/src/typing.ts
@@ -2,6 +2,12 @@ export interface Options { @@ -2,6 +2,12 @@ export interface Options {
2 show?: boolean; 2 show?: boolean;
3 imageList: string[]; 3 imageList: string[];
4 index?: number; 4 index?: number;
  5 + scaleStep?: number;
  6 + defaultWidth?: number;
  7 + maskClosable?: boolean;
  8 + rememberState?: boolean;
  9 + onImgLoad?: (img: HTMLImageElement) => void;
  10 + onImgError?: (img: HTMLImageElement) => void;
5 } 11 }
6 12
7 export interface Props { 13 export interface Props {
@@ -9,6 +15,19 @@ export interface Props { @@ -9,6 +15,19 @@ export interface Props {
9 instance: Props; 15 instance: Props;
10 imageList: string[]; 16 imageList: string[];
11 index: number; 17 index: number;
  18 + scaleStep: number;
  19 + defaultWidth: number;
  20 + maskClosable: boolean;
  21 + rememberState: boolean;
  22 +}
  23 +
  24 +export interface PreviewActions {
  25 + resume: () => void;
  26 + close: () => void;
  27 + prev: () => void;
  28 + next: () => void;
  29 + setScale: (scale: number) => void;
  30 + setRotate: (rotate: number) => void;
12 } 31 }
13 32
14 export interface ImageProps { 33 export interface ImageProps {
src/views/demo/feat/img-preview/index.vue
@@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
8 import { defineComponent } from 'vue'; 8 import { defineComponent } from 'vue';
9 import { createImgPreview, ImagePreview } from '/@/components/Preview/index'; 9 import { createImgPreview, ImagePreview } from '/@/components/Preview/index';
10 import { PageWrapper } from '/@/components/Page'; 10 import { PageWrapper } from '/@/components/Page';
  11 + // import { PreviewActions } from '/@/components/Preview/src/typing';
11 12
12 const imgList: string[] = [ 13 const imgList: string[] = [
13 'https://picsum.photos/id/66/346/216', 14 'https://picsum.photos/id/66/346/216',
@@ -18,7 +19,11 @@ @@ -18,7 +19,11 @@
18 components: { PageWrapper, ImagePreview }, 19 components: { PageWrapper, ImagePreview },
19 setup() { 20 setup() {
20 function openImg() { 21 function openImg() {
21 - createImgPreview({ imageList: imgList }); 22 + const onImgLoad = ({ index, url, dom }) => {
  23 + console.log(`第${index + 1}张图片已加载,URL为:${url}`, dom);
  24 + };
  25 + // 可以使用createImgPreview返回的 PreviewActions 来控制预览逻辑,实现类似幻灯片、自动旋转之类的骚操作
  26 + createImgPreview({ imageList: imgList, defaultWidth: 700, rememberState: true, onImgLoad });
22 } 27 }
23 return { imgList, openImg }; 28 return { imgList, openImg };
24 }, 29 },