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

534 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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) {
// 最小缩放
const MIN_SCALE = 0.02
// 放大缩小的颗粒度
const GRA = 0.1
if (imgState.imgScale <= 0.2 && num < 0) return
imgState.imgScale += num * GRA
// scale 不能 < 0否则图片会倒置放大
if (imgState.imgScale < 0) {
imgState.imgScale = MIN_SCALE
}
}
// 旋转图片
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;
inset: 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>