Commit e23bd2696da945291a9b652f1af39ad1936f376b
1 parent
98749ec6
feat(preview): add more features
为Preview组件添加新的属性及事件
Showing
5 changed files
with
149 additions
and
14 deletions
CHANGELOG.zh_CN.md
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 'vue'; | @@ -6,15 +6,12 @@ import { createVNode, render } from 'vue'; | ||
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 | }, |