前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View File

@ -0,0 +1,2 @@
export { default as ImagePreview } from './src/Preview.vue';
export { createImgPreview } from './src/functional';

View File

@ -0,0 +1,528 @@
<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: 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;
}
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div :class="prefixCls">
<PreviewGroup>
<slot v-if="!imageList || $slots.default"></slot>
<template v-else>
<template v-for="item in getImageList" :key="item.src">
<Image v-bind="item">
<template #placeholder v-if="item.placeholder">
<Image v-bind="item" :src="item.placeholder" :preview="false" />
</template>
</Image>
</template>
</template>
</PreviewGroup>
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent, computed } from 'vue';
import { Image } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
import { isString } from '/@/utils/is';
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);
};
}
type ImageItem = string | ImageProps;
export default defineComponent({
name: 'ImagePreview',
components: {
Image,
PreviewGroup: Image.PreviewGroup,
},
props: {
functional: propTypes.bool,
imageList: {
type: Array as PropType<ImageItem[]>,
},
},
setup(props) {
const { prefixCls } = useDesign('image-preview');
const getImageList = computed((): any[] => {
const { imageList } = props;
if (!imageList) {
return [];
}
return imageList.map((item) => {
if (isString(item)) {
return {
src: item,
placeholder: false,
};
}
return item;
});
});
return {
prefixCls,
getImageList,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-image-preview';
.@{prefix-cls} {
.ant-image {
margin-right: 10px;
}
.ant-image-preview-operations {
background-color: rgba(0, 0, 0, 0.4);
}
}
</style>

View File

@ -0,0 +1,18 @@
import type { Options, Props } from './typing';
import ImgPreview from './Functional.vue';
import { isClient } from '/@/utils/is';
import { createVNode, render } from 'vue';
let instance: ReturnType<typeof createVNode> | null = null;
export function createImgPreview(options: Options) {
if (!isClient) return;
const propsData: Partial<Props> = {};
const container = document.createElement('div');
Object.assign(propsData, { show: true, index: 0, scaleStep: 100 }, options);
instance = createVNode(ImgPreview, propsData);
render(instance, container);
document.body.appendChild(container);
return instance.component?.exposed;
}

View File

@ -0,0 +1,49 @@
export interface Options {
show?: boolean;
imageList: string[];
index?: number;
scaleStep?: number;
defaultWidth?: number;
maskClosable?: boolean;
rememberState?: boolean;
onImgLoad?: ({ index: number, url: string, dom: HTMLImageElement }) => void;
onImgError?: ({ index: number, url: string, dom: HTMLImageElement }) => void;
}
export interface Props {
show: boolean;
instance: Props;
imageList: string[];
index: number;
scaleStep: number;
defaultWidth: number;
maskClosable: boolean;
rememberState: boolean;
}
export interface PreviewActions {
resume: () => void;
close: () => void;
prev: () => void;
next: () => void;
setScale: (scale: number) => void;
setRotate: (rotate: number) => void;
}
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;