|
@@ -38,13 +38,33 @@
|
|
|
type: Number as PropType<number>,
|
|
|
default: 0,
|
|
|
},
|
|
|
+ scaleStep: {
|
|
|
+ type: Number as PropType<number>,
|
|
|
+ },
|
|
|
+ defaultWidth: {
|
|
|
+ type: Number as PropType<number>,
|
|
|
+ },
|
|
|
+ maskClosable: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ },
|
|
|
+ rememberState: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ },
|
|
|
};
|
|
|
|
|
|
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<string, stateInfo>();
|
|
|
const imgState = reactive<ImgState>({
|
|
|
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 (
|
|
|
<div class={`${prefixCls}__close`} onClick={handleClose}>
|
|
@@ -246,10 +345,16 @@
|
|
|
const renderController = () => {
|
|
|
return (
|
|
|
<div class={`${prefixCls}__controller`}>
|
|
|
- <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(-0.15)}>
|
|
|
+ <div
|
|
|
+ class={`${prefixCls}__controller-item`}
|
|
|
+ onClick={() => scaleFunc(-getScaleStep.value)}
|
|
|
+ >
|
|
|
<img src={unScaleSvg} />
|
|
|
</div>
|
|
|
- <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}>
|
|
|
+ <div
|
|
|
+ class={`${prefixCls}__controller-item`}
|
|
|
+ onClick={() => scaleFunc(getScaleStep.value)}
|
|
|
+ >
|
|
|
<img src={scaleSvg} />
|
|
|
</div>
|
|
|
<div class={`${prefixCls}__controller-item`} onClick={resume}>
|
|
@@ -279,7 +384,12 @@
|
|
|
return () => {
|
|
|
return (
|
|
|
imgState.show && (
|
|
|
- <div class={prefixCls} ref={wrapElRef} onMouseup={handleMouseUp}>
|
|
|
+ <div
|
|
|
+ class={prefixCls}
|
|
|
+ ref={wrapElRef}
|
|
|
+ onMouseup={handleMouseUp}
|
|
|
+ onClick={handleMaskClick}
|
|
|
+ >
|
|
|
<div class={`${prefixCls}-content`}>
|
|
|
{/*<Spin*/}
|
|
|
{/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}
|