提交 | 用户 | age
|
305630
|
1 |
<script lang="tsx"> |
V |
2 |
import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue'; |
|
3 |
import { Props } from './typing'; |
|
4 |
import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue'; |
|
5 |
import resumeSvg from '/@/assets/svg/preview/resume.svg'; |
|
6 |
import rotateSvg from '/@/assets/svg/preview/p-rotate.svg'; |
|
7 |
import scaleSvg from '/@/assets/svg/preview/scale.svg'; |
|
8 |
import unScaleSvg from '/@/assets/svg/preview/unscale.svg'; |
|
9 |
import unRotateSvg from '/@/assets/svg/preview/unrotate.svg'; |
|
10 |
|
|
11 |
enum StatueEnum { |
|
12 |
LOADING, |
|
13 |
DONE, |
|
14 |
FAIL, |
|
15 |
} |
|
16 |
interface ImgState { |
|
17 |
currentUrl: string; |
|
18 |
imgScale: number; |
|
19 |
imgRotate: number; |
|
20 |
imgTop: number; |
|
21 |
imgLeft: number; |
|
22 |
currentIndex: number; |
|
23 |
status: StatueEnum; |
|
24 |
moveX: number; |
|
25 |
moveY: number; |
|
26 |
show: boolean; |
|
27 |
} |
|
28 |
const props = { |
|
29 |
show: { |
|
30 |
type: Boolean as PropType<boolean>, |
|
31 |
default: false, |
|
32 |
}, |
|
33 |
imageList: { |
|
34 |
type: [Array] as PropType<string[]>, |
|
35 |
default: null, |
|
36 |
}, |
|
37 |
index: { |
|
38 |
type: Number as PropType<number>, |
|
39 |
default: 0, |
|
40 |
}, |
e23bd2
|
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 |
}, |
305630
|
53 |
}; |
V |
54 |
|
|
55 |
const prefixCls = 'img-preview'; |
|
56 |
export default defineComponent({ |
|
57 |
name: 'ImagePreview', |
|
58 |
props, |
e23bd2
|
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>(); |
305630
|
68 |
const imgState = reactive<ImgState>({ |
V |
69 |
currentUrl: '', |
|
70 |
imgScale: 1, |
|
71 |
imgRotate: 0, |
|
72 |
imgTop: 0, |
|
73 |
imgLeft: 0, |
|
74 |
status: StatueEnum.LOADING, |
|
75 |
currentIndex: 0, |
|
76 |
moveX: 0, |
|
77 |
moveY: 0, |
|
78 |
show: props.show, |
|
79 |
}); |
|
80 |
|
|
81 |
const wrapElRef = ref<HTMLDivElement | null>(null); |
|
82 |
const imgElRef = ref<HTMLImageElement | null>(null); |
|
83 |
|
|
84 |
// 初始化 |
|
85 |
function init() { |
|
86 |
initMouseWheel(); |
|
87 |
const { index, imageList } = props; |
|
88 |
|
|
89 |
if (!imageList || !imageList.length) { |
|
90 |
throw new Error('imageList is undefined'); |
|
91 |
} |
|
92 |
imgState.currentIndex = index; |
|
93 |
handleIChangeImage(imageList[index]); |
|
94 |
} |
|
95 |
|
|
96 |
// 重置 |
|
97 |
function initState() { |
|
98 |
imgState.imgScale = 1; |
|
99 |
imgState.imgRotate = 0; |
|
100 |
imgState.imgTop = 0; |
|
101 |
imgState.imgLeft = 0; |
|
102 |
} |
|
103 |
|
|
104 |
// 初始化鼠标滚轮事件 |
|
105 |
function initMouseWheel() { |
|
106 |
const wrapEl = unref(wrapElRef); |
|
107 |
if (!wrapEl) { |
|
108 |
return; |
|
109 |
} |
|
110 |
(wrapEl as any).onmousewheel = scrollFunc; |
|
111 |
// 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替 |
|
112 |
document.body.addEventListener('DOMMouseScroll', scrollFunc); |
|
113 |
// 禁止火狐浏览器下拖拽图片的默认事件 |
|
114 |
document.ondragstart = function () { |
|
115 |
return false; |
|
116 |
}; |
|
117 |
} |
|
118 |
|
e23bd2
|
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 |
|
305630
|
127 |
// 监听鼠标滚轮 |
V |
128 |
function scrollFunc(e: any) { |
|
129 |
e = e || window.event; |
|
130 |
e.delta = e.wheelDelta || -e.detail; |
|
131 |
|
|
132 |
e.preventDefault(); |
|
133 |
if (e.delta > 0) { |
|
134 |
// 滑轮向上滚动 |
e23bd2
|
135 |
scaleFunc(getScaleStep.value); |
305630
|
136 |
} |
V |
137 |
if (e.delta < 0) { |
|
138 |
// 滑轮向下滚动 |
e23bd2
|
139 |
scaleFunc(-getScaleStep.value); |
305630
|
140 |
} |
V |
141 |
} |
|
142 |
// 缩放函数 |
|
143 |
function scaleFunc(num: number) { |
|
144 |
if (imgState.imgScale <= 0.2 && num < 0) return; |
|
145 |
imgState.imgScale += num; |
|
146 |
} |
|
147 |
|
|
148 |
// 旋转图片 |
|
149 |
function rotateFunc(deg: number) { |
|
150 |
imgState.imgRotate += deg; |
|
151 |
} |
|
152 |
|
|
153 |
// 鼠标事件 |
|
154 |
function handleMouseUp() { |
|
155 |
const imgEl = unref(imgElRef); |
|
156 |
if (!imgEl) return; |
|
157 |
imgEl.onmousemove = null; |
|
158 |
} |
|
159 |
|
|
160 |
// 更换图片 |
|
161 |
function handleIChangeImage(url: string) { |
|
162 |
imgState.status = StatueEnum.LOADING; |
|
163 |
const img = new Image(); |
|
164 |
img.src = url; |
e23bd2
|
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 |
} |
305630
|
202 |
imgState.currentUrl = url; |
V |
203 |
imgState.status = StatueEnum.DONE; |
|
204 |
}; |
e23bd2
|
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 |
}); |
305630
|
213 |
imgState.status = StatueEnum.FAIL; |
V |
214 |
}; |
|
215 |
} |
|
216 |
|
|
217 |
// 关闭 |
|
218 |
function handleClose(e: MouseEvent) { |
|
219 |
e && e.stopPropagation(); |
e23bd2
|
220 |
close(); |
无 |
221 |
} |
|
222 |
|
|
223 |
function close() { |
305630
|
224 |
imgState.show = false; |
V |
225 |
// 移除火狐浏览器下的鼠标滚动事件 |
|
226 |
document.body.removeEventListener('DOMMouseScroll', scrollFunc); |
|
227 |
// 恢复火狐及Safari浏览器下的图片拖拽 |
|
228 |
document.ondragstart = null; |
|
229 |
} |
|
230 |
|
|
231 |
// 图片复原 |
|
232 |
function resume() { |
|
233 |
initState(); |
|
234 |
} |
e23bd2
|
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); |
305630
|
248 |
|
V |
249 |
// 上一页下一页 |
|
250 |
function handleChange(direction: 'left' | 'right') { |
|
251 |
const { currentIndex } = imgState; |
|
252 |
const { imageList } = props; |
|
253 |
if (direction === 'left') { |
|
254 |
imgState.currentIndex--; |
|
255 |
if (currentIndex <= 0) { |
|
256 |
imgState.currentIndex = imageList.length - 1; |
|
257 |
} |
|
258 |
} |
|
259 |
if (direction === 'right') { |
|
260 |
imgState.currentIndex++; |
|
261 |
if (currentIndex >= imageList.length - 1) { |
|
262 |
imgState.currentIndex = 0; |
|
263 |
} |
|
264 |
} |
|
265 |
handleIChangeImage(imageList[imgState.currentIndex]); |
|
266 |
} |
|
267 |
|
|
268 |
function handleAddMoveListener(e: MouseEvent) { |
|
269 |
e = e || window.event; |
|
270 |
imgState.moveX = e.clientX; |
|
271 |
imgState.moveY = e.clientY; |
|
272 |
const imgEl = unref(imgElRef); |
|
273 |
if (imgEl) { |
|
274 |
imgEl.onmousemove = moveFunc; |
|
275 |
} |
|
276 |
} |
|
277 |
|
|
278 |
function moveFunc(e: MouseEvent) { |
|
279 |
e = e || window.event; |
|
280 |
e.preventDefault(); |
|
281 |
const movementX = e.clientX - imgState.moveX; |
|
282 |
const movementY = e.clientY - imgState.moveY; |
|
283 |
imgState.imgLeft += movementX; |
|
284 |
imgState.imgTop += movementY; |
|
285 |
imgState.moveX = e.clientX; |
|
286 |
imgState.moveY = e.clientY; |
|
287 |
} |
|
288 |
|
|
289 |
// 获取图片样式 |
|
290 |
const getImageStyle = computed(() => { |
|
291 |
const { imgScale, imgRotate, imgTop, imgLeft } = imgState; |
|
292 |
return { |
|
293 |
transform: `scale(${imgScale}) rotate(${imgRotate}deg)`, |
|
294 |
marginTop: `${imgTop}px`, |
|
295 |
marginLeft: `${imgLeft}px`, |
e23bd2
|
296 |
maxWidth: props.defaultWidth ? 'unset' : '100%', |
305630
|
297 |
}; |
V |
298 |
}); |
|
299 |
|
|
300 |
const getIsMultipleImage = computed(() => { |
|
301 |
const { imageList } = props; |
|
302 |
return imageList.length > 1; |
|
303 |
}); |
|
304 |
|
|
305 |
watchEffect(() => { |
|
306 |
if (props.show) { |
|
307 |
init(); |
|
308 |
} |
|
309 |
if (props.imageList) { |
|
310 |
initState(); |
|
311 |
} |
|
312 |
}); |
e23bd2
|
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 |
}; |
305630
|
323 |
|
V |
324 |
const renderClose = () => { |
|
325 |
return ( |
|
326 |
<div class={`${prefixCls}__close`} onClick={handleClose}> |
|
327 |
<CloseOutlined class={`${prefixCls}__close-icon`} /> |
|
328 |
</div> |
|
329 |
); |
|
330 |
}; |
|
331 |
|
|
332 |
const renderIndex = () => { |
|
333 |
if (!unref(getIsMultipleImage)) { |
|
334 |
return null; |
|
335 |
} |
|
336 |
const { currentIndex } = imgState; |
|
337 |
const { imageList } = props; |
|
338 |
return ( |
|
339 |
<div class={`${prefixCls}__index`}> |
|
340 |
{currentIndex + 1} / {imageList.length} |
|
341 |
</div> |
|
342 |
); |
|
343 |
}; |
|
344 |
|
|
345 |
const renderController = () => { |
|
346 |
return ( |
|
347 |
<div class={`${prefixCls}__controller`}> |
e23bd2
|
348 |
<div |
无 |
349 |
class={`${prefixCls}__controller-item`} |
|
350 |
onClick={() => scaleFunc(-getScaleStep.value)} |
|
351 |
> |
305630
|
352 |
<img src={unScaleSvg} /> |
V |
353 |
</div> |
e23bd2
|
354 |
<div |
无 |
355 |
class={`${prefixCls}__controller-item`} |
|
356 |
onClick={() => scaleFunc(getScaleStep.value)} |
|
357 |
> |
305630
|
358 |
<img src={scaleSvg} /> |
V |
359 |
</div> |
|
360 |
<div class={`${prefixCls}__controller-item`} onClick={resume}> |
|
361 |
<img src={resumeSvg} /> |
|
362 |
</div> |
|
363 |
<div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(-90)}> |
|
364 |
<img src={unRotateSvg} /> |
|
365 |
</div> |
|
366 |
<div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(90)}> |
|
367 |
<img src={rotateSvg} /> |
|
368 |
</div> |
|
369 |
</div> |
|
370 |
); |
|
371 |
}; |
|
372 |
|
|
373 |
const renderArrow = (direction: 'left' | 'right') => { |
|
374 |
if (!unref(getIsMultipleImage)) { |
|
375 |
return null; |
|
376 |
} |
|
377 |
return ( |
|
378 |
<div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}> |
|
379 |
{direction === 'left' ? <LeftOutlined /> : <RightOutlined />} |
|
380 |
</div> |
|
381 |
); |
|
382 |
}; |
|
383 |
|
|
384 |
return () => { |
|
385 |
return ( |
|
386 |
imgState.show && ( |
e23bd2
|
387 |
<div |
无 |
388 |
class={prefixCls} |
|
389 |
ref={wrapElRef} |
|
390 |
onMouseup={handleMouseUp} |
|
391 |
onClick={handleMaskClick} |
|
392 |
> |
305630
|
393 |
<div class={`${prefixCls}-content`}> |
V |
394 |
{/*<Spin*/} |
|
395 |
{/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/} |
|
396 |
{/* spinning={true}*/} |
|
397 |
{/* class={[*/} |
|
398 |
{/* `${prefixCls}-image`,*/} |
|
399 |
{/* {*/} |
|
400 |
{/* hidden: imgState.status !== StatueEnum.LOADING,*/} |
|
401 |
{/* },*/} |
|
402 |
{/* ]}*/} |
|
403 |
{/*/>*/} |
|
404 |
<img |
|
405 |
style={unref(getImageStyle)} |
|
406 |
class={[ |
|
407 |
`${prefixCls}-image`, |
|
408 |
imgState.status === StatueEnum.DONE ? '' : 'hidden', |
|
409 |
]} |
|
410 |
ref={imgElRef} |
|
411 |
src={imgState.currentUrl} |
|
412 |
onMousedown={handleAddMoveListener} |
|
413 |
/> |
|
414 |
{renderClose()} |
|
415 |
{renderIndex()} |
|
416 |
{renderController()} |
|
417 |
{renderArrow('left')} |
|
418 |
{renderArrow('right')} |
|
419 |
</div> |
|
420 |
</div> |
|
421 |
) |
|
422 |
); |
|
423 |
}; |
|
424 |
}, |
|
425 |
}); |
|
426 |
</script> |
|
427 |
<style lang="less"> |
|
428 |
.img-preview { |
|
429 |
position: fixed; |
|
430 |
top: 0; |
|
431 |
right: 0; |
|
432 |
bottom: 0; |
|
433 |
left: 0; |
|
434 |
z-index: @preview-comp-z-index; |
|
435 |
background: rgba(0, 0, 0, 0.5); |
|
436 |
user-select: none; |
|
437 |
|
|
438 |
&-content { |
|
439 |
display: flex; |
|
440 |
width: 100%; |
|
441 |
height: 100%; |
|
442 |
color: @white; |
|
443 |
justify-content: center; |
|
444 |
align-items: center; |
|
445 |
} |
|
446 |
|
|
447 |
&-image { |
|
448 |
cursor: pointer; |
|
449 |
transition: transform 0.3s; |
|
450 |
} |
|
451 |
|
|
452 |
&__close { |
|
453 |
position: absolute; |
|
454 |
top: -40px; |
|
455 |
right: -40px; |
|
456 |
width: 80px; |
|
457 |
height: 80px; |
|
458 |
overflow: hidden; |
|
459 |
color: @white; |
|
460 |
cursor: pointer; |
|
461 |
background-color: rgba(0, 0, 0, 0.5); |
|
462 |
border-radius: 50%; |
|
463 |
transition: all 0.2s; |
|
464 |
|
|
465 |
&-icon { |
|
466 |
position: absolute; |
|
467 |
top: 46px; |
|
468 |
left: 16px; |
|
469 |
font-size: 16px; |
|
470 |
} |
|
471 |
|
|
472 |
&:hover { |
|
473 |
background-color: rgba(0, 0, 0, 0.8); |
|
474 |
} |
|
475 |
} |
|
476 |
|
|
477 |
&__index { |
|
478 |
position: absolute; |
|
479 |
bottom: 5%; |
|
480 |
left: 50%; |
|
481 |
padding: 0 22px; |
|
482 |
font-size: 16px; |
|
483 |
background: rgba(109, 109, 109, 0.6); |
|
484 |
border-radius: 15px; |
|
485 |
transform: translateX(-50%); |
|
486 |
} |
|
487 |
|
|
488 |
&__controller { |
|
489 |
position: absolute; |
|
490 |
bottom: 10%; |
|
491 |
left: 50%; |
|
492 |
display: flex; |
|
493 |
width: 260px; |
|
494 |
height: 44px; |
|
495 |
padding: 0 22px; |
|
496 |
margin-left: -139px; |
|
497 |
background: rgba(109, 109, 109, 0.6); |
|
498 |
border-radius: 22px; |
|
499 |
justify-content: center; |
|
500 |
|
|
501 |
&-item { |
|
502 |
display: flex; |
|
503 |
height: 100%; |
|
504 |
padding: 0 9px; |
|
505 |
font-size: 24px; |
|
506 |
cursor: pointer; |
|
507 |
transition: all 0.2s; |
|
508 |
|
|
509 |
&:hover { |
|
510 |
transform: scale(1.2); |
|
511 |
} |
|
512 |
|
|
513 |
img { |
|
514 |
width: 1em; |
|
515 |
} |
|
516 |
} |
|
517 |
} |
|
518 |
|
|
519 |
&__arrow { |
|
520 |
position: absolute; |
|
521 |
top: 50%; |
|
522 |
display: flex; |
|
523 |
align-items: center; |
|
524 |
justify-content: center; |
|
525 |
width: 50px; |
|
526 |
height: 50px; |
|
527 |
font-size: 28px; |
|
528 |
cursor: pointer; |
|
529 |
background-color: rgba(0, 0, 0, 0.5); |
|
530 |
border-radius: 50%; |
|
531 |
transition: all 0.2s; |
|
532 |
|
|
533 |
&:hover { |
|
534 |
background-color: rgba(0, 0, 0, 0.8); |
|
535 |
} |
|
536 |
|
|
537 |
&.left { |
|
538 |
left: 50px; |
|
539 |
} |
|
540 |
|
|
541 |
&.right { |
|
542 |
right: 50px; |
|
543 |
} |
|
544 |
} |
|
545 |
} |
|
546 |
</style> |