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 | 38 | type: Number as PropType<number>, |
39 | 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 | 55 | const prefixCls = 'img-preview'; |
44 | 56 | export default defineComponent({ |
45 | 57 | name: 'ImagePreview', |
46 | 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 | 68 | const imgState = reactive<ImgState>({ |
49 | 69 | currentUrl: '', |
50 | 70 | imgScale: 1, |
... | ... | @@ -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 | 128 | function scrollFunc(e: any) { |
101 | 129 | e = e || window.event; |
... | ... | @@ -104,11 +132,11 @@ |
104 | 132 | e.preventDefault(); |
105 | 133 | if (e.delta > 0) { |
106 | 134 | // 滑轮向上滚动 |
107 | - scaleFunc(0.015); | |
135 | + scaleFunc(getScaleStep.value); | |
108 | 136 | } |
109 | 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 | 162 | imgState.status = StatueEnum.LOADING; |
135 | 163 | const img = new Image(); |
136 | 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 | 202 | imgState.currentUrl = url; |
139 | 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 | 213 | imgState.status = StatueEnum.FAIL; |
143 | 214 | }; |
144 | 215 | } |
... | ... | @@ -146,6 +217,10 @@ |
146 | 217 | // 关闭 |
147 | 218 | function handleClose(e: MouseEvent) { |
148 | 219 | e && e.stopPropagation(); |
220 | + close(); | |
221 | + } | |
222 | + | |
223 | + function close() { | |
149 | 224 | imgState.show = false; |
150 | 225 | // 移除火狐浏览器下的鼠标滚动事件 |
151 | 226 | document.body.removeEventListener('DOMMouseScroll', scrollFunc); |
... | ... | @@ -158,6 +233,19 @@ |
158 | 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 | 250 | function handleChange(direction: 'left' | 'right') { |
163 | 251 | const { currentIndex } = imgState; |
... | ... | @@ -205,6 +293,7 @@ |
205 | 293 | transform: `scale(${imgScale}) rotate(${imgRotate}deg)`, |
206 | 294 | marginTop: `${imgTop}px`, |
207 | 295 | marginLeft: `${imgLeft}px`, |
296 | + maxWidth: props.defaultWidth ? 'unset' : '100%', | |
208 | 297 | }; |
209 | 298 | }); |
210 | 299 | |
... | ... | @@ -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 | 324 | const renderClose = () => { |
226 | 325 | return ( |
227 | 326 | <div class={`${prefixCls}__close`} onClick={handleClose}> |
... | ... | @@ -246,10 +345,16 @@ |
246 | 345 | const renderController = () => { |
247 | 346 | return ( |
248 | 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 | 352 | <img src={unScaleSvg} /> |
251 | 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 | 358 | <img src={scaleSvg} /> |
254 | 359 | </div> |
255 | 360 | <div class={`${prefixCls}__controller-item`} onClick={resume}> |
... | ... | @@ -279,7 +384,12 @@ |
279 | 384 | return () => { |
280 | 385 | return ( |
281 | 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 | 393 | <div class={`${prefixCls}-content`}> |
284 | 394 | {/*<Spin*/} |
285 | 395 | {/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/} | ... | ... |
src/components/Preview/src/functional.ts
... | ... | @@ -6,15 +6,12 @@ import { createVNode, render } from 'vue'; |
6 | 6 | let instance: ReturnType<typeof createVNode> | null = null; |
7 | 7 | export function createImgPreview(options: Options) { |
8 | 8 | if (!isClient) return; |
9 | - const { imageList, show = true, index = 0 } = options; | |
10 | - | |
11 | 9 | const propsData: Partial<Props> = {}; |
12 | 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 | 13 | instance = createVNode(ImgPreview, propsData); |
18 | 14 | render(instance, container); |
19 | 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 | 2 | show?: boolean; |
3 | 3 | imageList: string[]; |
4 | 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 | 13 | export interface Props { |
... | ... | @@ -9,6 +15,19 @@ export interface Props { |
9 | 15 | instance: Props; |
10 | 16 | imageList: string[]; |
11 | 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 | 33 | export interface ImageProps { | ... | ... |
src/views/demo/feat/img-preview/index.vue
... | ... | @@ -8,6 +8,7 @@ |
8 | 8 | import { defineComponent } from 'vue'; |
9 | 9 | import { createImgPreview, ImagePreview } from '/@/components/Preview/index'; |
10 | 10 | import { PageWrapper } from '/@/components/Page'; |
11 | + // import { PreviewActions } from '/@/components/Preview/src/typing'; | |
11 | 12 | |
12 | 13 | const imgList: string[] = [ |
13 | 14 | 'https://picsum.photos/id/66/346/216', |
... | ... | @@ -18,7 +19,11 @@ |
18 | 19 | components: { PageWrapper, ImagePreview }, |
19 | 20 | setup() { |
20 | 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 | 28 | return { imgList, openImg }; |
24 | 29 | }, | ... | ... |