vue-vben-admin/src/components/Preview/src/Functional.vue

547 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="tsx">
import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';
import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import resumeSvg from '/@/assets/svg/preview/resume.svg';
import rotateSvg from '/@/assets/svg/preview/p-rotate.svg';
import scaleSvg from '/@/assets/svg/preview/scale.svg';
import unScaleSvg from '/@/assets/svg/preview/unscale.svg';
import unRotateSvg from '/@/assets/svg/preview/unrotate.svg';
enum StatueEnum {
LOADING,
DONE,
FAIL,
}
interface ImgState {
currentUrl: string;
imgScale: number;
imgRotate: number;
imgTop: number;
imgLeft: number;
currentIndex: number;
status: StatueEnum;
moveX: number;
moveY: number;
show: boolean;
}
const props = {
show: {
type: Boolean as PropType<boolean>,
default: false,
},
imageList: {
type: [Array] as PropType<string[]>,
default: null,
},
index: {
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,
emits: ['img-load', 'img-error'],
setup(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,
imgRotate: 0,
imgTop: 0,
imgLeft: 0,
status: StatueEnum.LOADING,
currentIndex: 0,
moveX: 0,
moveY: 0,
show: props.show,
});
const wrapElRef = ref<HTMLDivElement | null>(null);
const imgElRef = ref<HTMLImageElement | null>(null);
// 初始化
function init() {
initMouseWheel();
const { index, imageList } = props;
if (!imageList || !imageList.length) {
throw new Error('imageList is undefined');
}
imgState.currentIndex = index;
handleIChangeImage(imageList[index]);
}
// 重置
function initState() {
imgState.imgScale = 1;
imgState.imgRotate = 0;
imgState.imgTop = 0;
imgState.imgLeft = 0;
}
// 初始化鼠标滚轮事件
function initMouseWheel() {
const wrapEl = unref(wrapElRef);
if (!wrapEl) {
return;
}
(wrapEl as any).onmousewheel = scrollFunc;
// 火狐浏览器没有onmousewheel事件用DOMMouseScroll代替
document.body.addEventListener('DOMMouseScroll', scrollFunc);
// 禁止火狐浏览器下拖拽图片的默认事件
document.ondragstart = function () {
return false;
};
}
const getScaleStep = computed(() => {
const scaleStep = props?.scaleStep ?? 0;
if (scaleStep ?? (0 > 0 && scaleStep < 100)) {
return scaleStep / 100;
} else {
return imgState.imgScale / 10;
}
});
// 监听鼠标滚轮
function scrollFunc(e: any) {
e = e || window.event;
e.delta = e.wheelDelta || -e.detail;
e.preventDefault();
if (e.delta > 0) {
// 滑轮向上滚动
scaleFunc(getScaleStep.value);
}
if (e.delta < 0) {
// 滑轮向下滚动
scaleFunc(-getScaleStep.value);
}
}
// 缩放函数
function scaleFunc(num: number) {
if (imgState.imgScale <= 0.2 && num < 0) return;
imgState.imgScale += num;
}
// 旋转图片
function rotateFunc(deg: number) {
imgState.imgRotate += deg;
}
// 鼠标事件
function handleMouseUp() {
const imgEl = unref(imgElRef);
if (!imgEl) return;
imgEl.onmousemove = null;
}
// 更换图片
function handleIChangeImage(url: string) {
imgState.status = StatueEnum.LOADING;
const img = new Image();
img.src = url;
img.onload = (e: Event) => {
if (imgState.currentUrl !== url) {
const ele: any[] = 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 = (e: Event) => {
const ele: EventTarget[] = e.composedPath();
ele &&
emit('img-error', {
index: imgState.currentIndex,
dom: ele[0] as HTMLImageElement,
url,
});
imgState.status = StatueEnum.FAIL;
};
}
// 关闭
function handleClose(e: MouseEvent) {
e && e.stopPropagation();
close();
}
function close() {
imgState.show = false;
// 移除火狐浏览器下的鼠标滚动事件
document.body.removeEventListener('DOMMouseScroll', scrollFunc);
// 恢复火狐及Safari浏览器下的图片拖拽
document.ondragstart = null;
}
// 图片复原
function resume() {
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;
},
});
// 上一页下一页
function handleChange(direction: 'left' | 'right') {
const { currentIndex } = imgState;
const { imageList } = props;
if (direction === 'left') {
imgState.currentIndex--;
if (currentIndex <= 0) {
imgState.currentIndex = imageList.length - 1;
}
}
if (direction === 'right') {
imgState.currentIndex++;
if (currentIndex >= imageList.length - 1) {
imgState.currentIndex = 0;
}
}
handleIChangeImage(imageList[imgState.currentIndex]);
}
function handleAddMoveListener(e: MouseEvent) {
e = e || window.event;
imgState.moveX = e.clientX;
imgState.moveY = e.clientY;
const imgEl = unref(imgElRef);
if (imgEl) {
imgEl.onmousemove = moveFunc;
}
}
function moveFunc(e: MouseEvent) {
e = e || window.event;
e.preventDefault();
const movementX = e.clientX - imgState.moveX;
const movementY = e.clientY - imgState.moveY;
imgState.imgLeft += movementX;
imgState.imgTop += movementY;
imgState.moveX = e.clientX;
imgState.moveY = e.clientY;
}
// 获取图片样式
const getImageStyle = computed(() => {
const { imgScale, imgRotate, imgTop, imgLeft } = imgState;
return {
transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,
marginTop: `${imgTop}px`,
marginLeft: `${imgLeft}px`,
maxWidth: props.defaultWidth ? 'unset' : '100%',
};
});
const getIsMultipleImage = computed(() => {
const { imageList } = props;
return imageList.length > 1;
});
watchEffect(() => {
if (props.show) {
init();
}
if (props.imageList) {
initState();
}
});
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}>
<CloseOutlined class={`${prefixCls}__close-icon`} />
</div>
);
};
const renderIndex = () => {
if (!unref(getIsMultipleImage)) {
return null;
}
const { currentIndex } = imgState;
const { imageList } = props;
return (
<div class={`${prefixCls}__index`}>
{currentIndex + 1} / {imageList.length}
</div>
);
};
const renderController = () => {
return (
<div class={`${prefixCls}__controller`}>
<div
class={`${prefixCls}__controller-item`}
onClick={() => scaleFunc(-getScaleStep.value)}
>
<img src={unScaleSvg} />
</div>
<div
class={`${prefixCls}__controller-item`}
onClick={() => scaleFunc(getScaleStep.value)}
>
<img src={scaleSvg} />
</div>
<div class={`${prefixCls}__controller-item`} onClick={resume}>
<img src={resumeSvg} />
</div>
<div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(-90)}>
<img src={unRotateSvg} />
</div>
<div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(90)}>
<img src={rotateSvg} />
</div>
</div>
);
};
const renderArrow = (direction: 'left' | 'right') => {
if (!unref(getIsMultipleImage)) {
return null;
}
return (
<div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}>
{direction === 'left' ? <LeftOutlined /> : <RightOutlined />}
</div>
);
};
return () => {
return (
imgState.show && (
<div
class={prefixCls}
ref={wrapElRef}
onMouseup={handleMouseUp}
onClick={handleMaskClick}
>
<div class={`${prefixCls}-content`}>
{/*<Spin*/}
{/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}
{/* spinning={true}*/}
{/* class={[*/}
{/* `${prefixCls}-image`,*/}
{/* {*/}
{/* hidden: imgState.status !== StatueEnum.LOADING,*/}
{/* },*/}
{/* ]}*/}
{/*/>*/}
<img
style={unref(getImageStyle)}
class={[
`${prefixCls}-image`,
imgState.status === StatueEnum.DONE ? '' : 'hidden',
]}
ref={imgElRef}
src={imgState.currentUrl}
onMousedown={handleAddMoveListener}
/>
{renderClose()}
{renderIndex()}
{renderController()}
{renderArrow('left')}
{renderArrow('right')}
</div>
</div>
)
);
};
},
});
</script>
<style lang="less">
.img-preview {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: @preview-comp-z-index;
background: rgb(0 0 0 / 50%);
user-select: none;
&-content {
display: flex;
width: 100%;
height: 100%;
color: @white;
justify-content: center;
align-items: center;
}
&-image {
cursor: pointer;
transition: transform 0.3s;
}
&__close {
position: absolute;
top: -40px;
right: -40px;
width: 80px;
height: 80px;
overflow: hidden;
color: @white;
cursor: pointer;
background-color: rgb(0 0 0 / 50%);
border-radius: 50%;
transition: all 0.2s;
&-icon {
position: absolute;
top: 46px;
left: 16px;
font-size: 16px;
}
&:hover {
background-color: rgb(0 0 0 / 80%);
}
}
&__index {
position: absolute;
bottom: 5%;
left: 50%;
padding: 0 22px;
font-size: 16px;
background: rgb(109 109 109 / 60%);
border-radius: 15px;
transform: translateX(-50%);
}
&__controller {
position: absolute;
bottom: 10%;
left: 50%;
display: flex;
width: 260px;
height: 44px;
padding: 0 22px;
margin-left: -139px;
background: rgb(109 109 109 / 60%);
border-radius: 22px;
justify-content: center;
&-item {
display: flex;
height: 100%;
padding: 0 9px;
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
&:hover {
transform: scale(1.2);
}
img {
width: 1em;
}
}
}
&__arrow {
position: absolute;
top: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
font-size: 28px;
cursor: pointer;
background-color: rgb(0 0 0 / 50%);
border-radius: 50%;
transition: all 0.2s;
&:hover {
background-color: rgb(0 0 0 / 80%);
}
&.left {
left: 50px;
}
&.right {
right: 50px;
}
}
}
</style>