committed by
GitHub
12 changed files with 502 additions and 2 deletions
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 996 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@ -1 +1,2 @@ |
|||||
export { default as ImagePreview } from './src/Preview.vue'; |
export { default as ImagePreview } from './src/Preview.vue'; |
||||
|
export { createImgPreview } from './src/functional'; |
||||
|
|||||
@ -0,0 +1,22 @@ |
|||||
|
import ImgPreview from './index'; |
||||
|
import { isClient } from '/@/utils/is'; |
||||
|
|
||||
|
import type { Options, Props } from './types'; |
||||
|
|
||||
|
import { createVNode, render } from 'vue'; |
||||
|
|
||||
|
let instance: any = null; |
||||
|
export function createImgPreview(options: Options) { |
||||
|
if (!isClient) return; |
||||
|
const { imageList, show = true, index = 0 } = options; |
||||
|
|
||||
|
const propsData: Partial<Props> = {}; |
||||
|
const container = document.createElement('div'); |
||||
|
propsData.imageList = imageList; |
||||
|
propsData.show = show; |
||||
|
propsData.index = index; |
||||
|
|
||||
|
instance = createVNode(ImgPreview, propsData); |
||||
|
render(instance, container); |
||||
|
document.body.appendChild(container); |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
.img-preview { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
z-index: @preview-comp-z-index; |
||||
|
background: rgba(0, 0, 0, 0.5); |
||||
|
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: rgba(0, 0, 0, 0.5); |
||||
|
border-radius: 50%; |
||||
|
transition: all 0.2s; |
||||
|
|
||||
|
&-icon { |
||||
|
position: absolute; |
||||
|
top: 46px; |
||||
|
left: 16px; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: rgba(0, 0, 0, 0.8); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__index { |
||||
|
position: absolute; |
||||
|
bottom: 5%; |
||||
|
left: 50%; |
||||
|
padding: 0 22px; |
||||
|
font-size: 16px; |
||||
|
background: rgba(109, 109, 109, 0.6); |
||||
|
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: rgba(109, 109, 109, 0.6); |
||||
|
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: rgba(0, 0, 0, 0.5); |
||||
|
border-radius: 50%; |
||||
|
transition: all 0.2s; |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: rgba(0, 0, 0, 0.8); |
||||
|
} |
||||
|
|
||||
|
&.left { |
||||
|
left: 50px; |
||||
|
} |
||||
|
|
||||
|
&.right { |
||||
|
right: 50px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,305 @@ |
|||||
|
import './index.less'; |
||||
|
|
||||
|
import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue'; |
||||
|
|
||||
|
// @ts-ignore
|
||||
|
import { basicProps } from './props'; |
||||
|
// @ts-ignore
|
||||
|
import { Props } from './types'; |
||||
|
|
||||
|
import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue'; |
||||
|
// import { Spin } from 'ant-design-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 prefixCls = 'img-preview'; |
||||
|
export default defineComponent({ |
||||
|
name: 'ImagePreview', |
||||
|
props: basicProps, |
||||
|
setup(props: Props) { |
||||
|
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; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// 监听鼠标滚轮
|
||||
|
function scrollFunc(e: any) { |
||||
|
e = e || window.event; |
||||
|
e.delta = e.wheelDelta || -e.detail; |
||||
|
|
||||
|
e.preventDefault(); |
||||
|
if (e.delta > 0) { |
||||
|
// 滑轮向上滚动
|
||||
|
scaleFunc(0.015); |
||||
|
} |
||||
|
if (e.delta < 0) { |
||||
|
// 滑轮向下滚动
|
||||
|
scaleFunc(-0.015); |
||||
|
} |
||||
|
} |
||||
|
// 缩放函数
|
||||
|
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 = () => { |
||||
|
imgState.currentUrl = url; |
||||
|
imgState.status = StatueEnum.DONE; |
||||
|
}; |
||||
|
img.onerror = () => { |
||||
|
imgState.status = StatueEnum.FAIL; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// 关闭
|
||||
|
function handleClose(e: MouseEvent) { |
||||
|
e && e.stopPropagation(); |
||||
|
imgState.show = false; |
||||
|
// 移除火狐浏览器下的鼠标滚动事件
|
||||
|
document.body.removeEventListener('DOMMouseScroll', scrollFunc); |
||||
|
// 恢复火狐及Safari浏览器下的图片拖拽
|
||||
|
document.ondragstart = null; |
||||
|
} |
||||
|
|
||||
|
// 图片复原
|
||||
|
function resume() { |
||||
|
initState(); |
||||
|
} |
||||
|
|
||||
|
// 上一页下一页
|
||||
|
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`, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const getIsMultipleImage = computed(() => { |
||||
|
const { imageList } = props; |
||||
|
return imageList.length > 1; |
||||
|
}); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (props.show) { |
||||
|
init(); |
||||
|
} |
||||
|
if (props.imageList) { |
||||
|
initState(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
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(-0.15)}> |
||||
|
<img src={unScaleSvg} /> |
||||
|
</div> |
||||
|
<div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}> |
||||
|
<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}> |
||||
|
<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> |
||||
|
) |
||||
|
); |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { PropType } from 'vue'; |
||||
|
export const basicProps = { |
||||
|
show: { |
||||
|
type: Boolean as PropType<boolean>, |
||||
|
default: false, |
||||
|
}, |
||||
|
imageList: { |
||||
|
type: [Array] as PropType<string[]>, |
||||
|
default: null, |
||||
|
}, |
||||
|
index: { |
||||
|
type: Number as PropType<number>, |
||||
|
default: 0, |
||||
|
}, |
||||
|
}; |
||||
@ -0,0 +1,30 @@ |
|||||
|
export interface Options { |
||||
|
show?: boolean; |
||||
|
imageList: string[]; |
||||
|
index?: number; |
||||
|
} |
||||
|
|
||||
|
export interface Props { |
||||
|
show: boolean; |
||||
|
instance: Props; |
||||
|
imageList: string[]; |
||||
|
index: number; |
||||
|
} |
||||
|
|
||||
|
export interface ImageProps { |
||||
|
alt?: string; |
||||
|
fallback?: string; |
||||
|
src: string; |
||||
|
width: string | number; |
||||
|
height?: string | number; |
||||
|
placeholder?: string | boolean; |
||||
|
preview?: |
||||
|
| boolean |
||||
|
| { |
||||
|
visible?: boolean; |
||||
|
onVisibleChange?: (visible: boolean, prevVisible: boolean) => void; |
||||
|
getContainer: string | HTMLElement | (() => HTMLElement); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export type ImageItem = string | ImageProps; |
||||
Loading…
Reference in new issue