提交 305630e3 作者: Vben

feat(preview): added createImgPreview picture preview function

上级 3f6920f7
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
- **CropperImage** `Cropper` 头像裁剪新增圆形裁剪功能 - **CropperImage** `Cropper` 头像裁剪新增圆形裁剪功能
- **CropperAvatar** 新增头像上传组件 - **CropperAvatar** 新增头像上传组件
- **Drawer** `useDrawer`新增`closeDrawer`函数 - **Drawer** `useDrawer`新增`closeDrawer`函数
- **Preview** 新增`createImgPreview`图片预览函数
### 🐛 Bug Fixes ### 🐛 Bug Fixes
......
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
import { get, omit } from 'lodash-es'; import { get, omit } from 'lodash-es';
import { LoadingOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
......
...@@ -40,13 +40,11 @@ ...@@ -40,13 +40,11 @@
<script lang="ts"> <script lang="ts">
import type { ColEx } from '../types/index'; import type { ColEx } from '../types/index';
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import { defineComponent, computed, PropType } from 'vue'; import { defineComponent, computed, PropType } from 'vue';
import { Form, Col } from 'ant-design-vue'; import { Form, Col } from 'ant-design-vue';
import { Button } from '/@/components/Button'; import { Button } from '/@/components/Button';
import { BasicArrow } from '/@/components/Basic/index'; import { BasicArrow } from '/@/components/Basic/index';
import { useFormContext } from '../hooks/useFormContext'; import { useFormContext } from '../hooks/useFormContext';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
......
...@@ -4,17 +4,14 @@ ...@@ -4,17 +4,14 @@
import type { FormSchema } from '../types/form'; import type { FormSchema } from '../types/form';
import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { TableActionType } from '/@/components/Table'; import type { TableActionType } from '/@/components/Table';
import { defineComponent, computed, unref, toRefs } from 'vue'; import { defineComponent, computed, unref, toRefs } from 'vue';
import { Form, Col } from 'ant-design-vue'; import { Form, Col } from 'ant-design-vue';
import { componentMap } from '../componentMap'; import { componentMap } from '../componentMap';
import { BasicHelp } from '/@/components/Basic'; import { BasicHelp } from '/@/components/Basic';
import { isBoolean, isFunction, isNull } from '/@/utils/is'; import { isBoolean, isFunction, isNull } from '/@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { getSlot } from '/@/utils/helper/tsxHelper';
import { createPlaceholderMessage, setComponentRuleType } from '../helper'; import { createPlaceholderMessage, setComponentRuleType } from '../helper';
import { upperFirst, cloneDeep } from 'lodash-es'; import { upperFirst, cloneDeep } from 'lodash-es';
import { useItemLabelWidth } from '../hooks/useLabelWidth'; import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
...@@ -91,7 +88,6 @@ ...@@ -91,7 +88,6 @@
if (isBoolean(dynamicDisabled)) { if (isBoolean(dynamicDisabled)) {
disabled = dynamicDisabled; disabled = dynamicDisabled;
} }
if (isFunction(dynamicDisabled)) { if (isFunction(dynamicDisabled)) {
disabled = dynamicDisabled(unref(getValues)); disabled = dynamicDisabled(unref(getValues));
} }
...@@ -276,7 +272,6 @@ ...@@ -276,7 +272,6 @@
: { : {
default: () => renderComponentContent, default: () => renderComponentContent,
}; };
return <Comp {...compAttr}>{compSlot}</Comp>; return <Comp {...compAttr}>{compSlot}</Comp>;
} }
...@@ -317,7 +312,6 @@ ...@@ -317,7 +312,6 @@
}; };
const showSuffix = !!suffix; const showSuffix = !!suffix;
const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix; const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
return ( return (
...@@ -338,16 +332,18 @@ ...@@ -338,16 +332,18 @@
</Form.Item> </Form.Item>
); );
} }
return () => { return () => {
const { colProps = {}, colSlot, renderColContent, component } = props.schema; const { colProps = {}, colSlot, renderColContent, component } = props.schema;
if (!componentMap.has(component)) return null; if (!componentMap.has(component)) {
return null;
}
const { baseColProps = {} } = props.formProps; const { baseColProps = {} } = props.formProps;
const realColProps = { ...baseColProps, ...colProps }; const realColProps = { ...baseColProps, ...colProps };
const { isIfShow, isShow } = getShow(); const { isIfShow, isShow } = getShow();
const values = unref(getValues); const values = unref(getValues);
const getContent = () => { const getContent = () => {
return colSlot return colSlot
? getSlot(slots, colSlot, values) ? getSlot(slots, colSlot, values)
......
<!-- <!--
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
--> -->
<template> <template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid"> <RadioGroup v-bind="attrs" v-model:value="state" button-style="solid">
<template v-for="item in getOptions" :key="`${item.value}`"> <template v-for="item in getOptions" :key="`${item.value}`">
...@@ -17,6 +16,7 @@ ...@@ -17,6 +16,7 @@
import { isString } from '/@/utils/is'; import { isString } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean }; type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
type RadioItem = string | OptionsItem; type RadioItem = string | OptionsItem;
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
const attrs = useAttrs(); const attrs = useAttrs();
// Embedded in the form, just use the hook binding to perform form verification // Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props); const [state] = useRuleFormItem(props);
// Processing options value // Processing options value
const getOptions = computed((): OptionsItem[] => { const getOptions = computed((): OptionsItem[] => {
const { options } = props; const { options } = props;
......
...@@ -2,10 +2,8 @@ import type { ColEx } from '../types'; ...@@ -2,10 +2,8 @@ import type { ColEx } from '../types';
import type { AdvanceState } from '../types/hooks'; import type { AdvanceState } from '../types/hooks';
import type { ComputedRef, Ref } from 'vue'; import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema } from '../types/form'; import type { FormProps, FormSchema } from '../types/form';
import { computed, unref, watch } from 'vue'; import { computed, unref, watch } from 'vue';
import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is'; import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is';
import { useBreakpoint } from '/@/hooks/event/useBreakpoint'; import { useBreakpoint } from '/@/hooks/event/useBreakpoint';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
......
...@@ -16,16 +16,22 @@ export async function useAutoFocus({ ...@@ -16,16 +16,22 @@ export async function useAutoFocus({
isInitedDefault, isInitedDefault,
}: UseAutoFocusContext) { }: UseAutoFocusContext) {
watchEffect(async () => { watchEffect(async () => {
if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) return; if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) {
return;
}
await nextTick(); await nextTick();
const schemas = unref(getSchema); const schemas = unref(getSchema);
const formEl = unref(formElRef); const formEl = unref(formElRef);
const el = (formEl as any)?.$el as HTMLElement; const el = (formEl as any)?.$el as HTMLElement;
if (!formEl || !el || !schemas || schemas.length === 0) return; if (!formEl || !el || !schemas || schemas.length === 0) {
return;
}
const firstItem = schemas[0]; const firstItem = schemas[0];
// Only open when the first form item is input type // Only open when the first form item is input type
if (!firstItem.component.includes('Input')) return; if (!firstItem.component.includes('Input')) {
return;
}
const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>; const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
if (!inputEl) return; if (!inputEl) return;
......
import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { DynamicProps } from '/#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
import { isProdMode } from '/@/utils/env'; import { isProdMode } from '/@/utils/env';
import { error } from '/@/utils/log'; import { error } from '/@/utils/log';
import { getDynamicProps } from '/@/utils'; import { getDynamicProps } from '/@/utils';
import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { DynamicProps } from '/#/utils';
export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>; export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>;
type Props = Partial<DynamicProps<FormProps>>; type Props = Partial<DynamicProps<FormProps>>;
......
import type { ComputedRef, Ref } from 'vue'; import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form'; import type { FormProps, FormSchema, FormActionType } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface'; import type { NamePath } from 'ant-design-vue/lib/form/interface';
import { unref, toRaw } from 'vue'; import { unref, toRaw } from 'vue';
import { isArray, isFunction, isObject, isString } from '/@/utils/is'; import { isArray, isFunction, isObject, isString } from '/@/utils/is';
import { deepMerge } from '/@/utils'; import { deepMerge } from '/@/utils';
import { dateItemType, handleInputNumberValue } from '../helper'; import { dateItemType, handleInputNumberValue } from '../helper';
......
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is'; import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is';
import { dateUtil } from '/@/utils/dateUtil'; import { dateUtil } from '/@/utils/dateUtil';
import { unref } from 'vue'; import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue'; import type { Ref, ComputedRef } from 'vue';
import type { FormProps, FormSchema } from '../types/form'; import type { FormProps, FormSchema } from '../types/form';
import { set } from 'lodash-es'; import { set } from 'lodash-es';
interface UseFormValuesContext { interface UseFormValuesContext {
......
import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface'; import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
import type { VNode } from 'vue'; import type { VNode } from 'vue';
import type { ButtonProps as AntdButtonProps } from 'ant-design-vue/es/button/buttonTypes'; import type { ButtonProps as AntdButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import type { FormItem } from './formItem'; import type { FormItem } from './formItem';
import type { ColEx, ComponentType } from './index'; import type { ColEx, ComponentType } from './index';
import type { TableActionType } from '/@/components/Table/src/types/table'; import type { TableActionType } from '/@/components/Table/src/types/table';
......
...@@ -90,9 +90,7 @@ export type ComponentType = ...@@ -90,9 +90,7 @@ export type ComponentType =
| 'InputCountDown' | 'InputCountDown'
| 'Select' | 'Select'
| 'ApiSelect' | 'ApiSelect'
| 'SelectOptGroup'
| 'TreeSelect' | 'TreeSelect'
| 'Transfer'
| 'RadioButtonGroup' | 'RadioButtonGroup'
| 'RadioGroup' | 'RadioGroup'
| 'Checkbox' | 'Checkbox'
......
import './index.less'; <script lang="tsx">
import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';
import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue'; import { Props } from './typing';
import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
// @ts-ignore import resumeSvg from '/@/assets/svg/preview/resume.svg';
import { basicProps } from './props'; import rotateSvg from '/@/assets/svg/preview/p-rotate.svg';
// @ts-ignore import scaleSvg from '/@/assets/svg/preview/scale.svg';
import { Props } from './types'; import unScaleSvg from '/@/assets/svg/preview/unscale.svg';
import unRotateSvg from '/@/assets/svg/preview/unrotate.svg';
import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
// import { Spin } from 'ant-design-vue'; enum StatueEnum {
LOADING,
import resumeSvg from '/@/assets/svg/preview/resume.svg'; DONE,
import rotateSvg from '/@/assets/svg/preview/p-rotate.svg'; FAIL,
import scaleSvg from '/@/assets/svg/preview/scale.svg'; }
import unScaleSvg from '/@/assets/svg/preview/unscale.svg'; interface ImgState {
import unRotateSvg from '/@/assets/svg/preview/unrotate.svg'; currentUrl: string;
enum StatueEnum { imgScale: number;
LOADING, imgRotate: number;
DONE, imgTop: number;
FAIL, imgLeft: number;
} currentIndex: number;
interface ImgState { status: StatueEnum;
currentUrl: string; moveX: number;
imgScale: number; moveY: number;
imgRotate: number; show: boolean;
imgTop: number; }
imgLeft: number; const props = {
currentIndex: number; show: {
status: StatueEnum; type: Boolean as PropType<boolean>,
moveX: number; default: false,
moveY: number; },
show: boolean; imageList: {
} type: [Array] as PropType<string[]>,
default: null,
const prefixCls = 'img-preview'; },
export default defineComponent({ index: {
name: 'ImagePreview', type: Number as PropType<number>,
props: basicProps, default: 0,
setup(props: Props) { },
const imgState = reactive<ImgState>({ };
currentUrl: '',
imgScale: 1, const prefixCls = 'img-preview';
imgRotate: 0, export default defineComponent({
imgTop: 0, name: 'ImagePreview',
imgLeft: 0, props,
status: StatueEnum.LOADING, setup(props: Props) {
currentIndex: 0, const imgState = reactive<ImgState>({
moveX: 0, currentUrl: '',
moveY: 0, imgScale: 1,
show: props.show, imgRotate: 0,
}); imgTop: 0,
imgLeft: 0,
const wrapElRef = ref<HTMLDivElement | null>(null); status: StatueEnum.LOADING,
const imgElRef = ref<HTMLImageElement | null>(null); currentIndex: 0,
moveX: 0,
// 初始化 moveY: 0,
function init() { show: props.show,
initMouseWheel(); });
const { index, imageList } = props;
const wrapElRef = ref<HTMLDivElement | null>(null);
if (!imageList || !imageList.length) { const imgElRef = ref<HTMLImageElement | null>(null);
throw new Error('imageList is undefined');
// 初始化
function init() {
initMouseWheel();
const { index, imageList } = props;
if (!imageList || !imageList.length) {
throw new Error('imageList is undefined');
}
imgState.currentIndex = index;
handleIChangeImage(imageList[index]);
} }
imgState.currentIndex = index;
handleIChangeImage(imageList[index]);
}
// 重置 // 重置
function initState() { function initState() {
imgState.imgScale = 1; imgState.imgScale = 1;
imgState.imgRotate = 0; imgState.imgRotate = 0;
imgState.imgTop = 0; imgState.imgTop = 0;
imgState.imgLeft = 0; imgState.imgLeft = 0;
} }
// 初始化鼠标滚轮事件 // 初始化鼠标滚轮事件
function initMouseWheel() { function initMouseWheel() {
const wrapEl = unref(wrapElRef); const wrapEl = unref(wrapElRef);
if (!wrapEl) { if (!wrapEl) {
return; return;
}
(wrapEl as any).onmousewheel = scrollFunc;
// 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替
document.body.addEventListener('DOMMouseScroll', scrollFunc);
// 禁止火狐浏览器下拖拽图片的默认事件
document.ondragstart = function () {
return false;
};
} }
(wrapEl as any).onmousewheel = scrollFunc;
// 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替
document.body.addEventListener('DOMMouseScroll', scrollFunc);
// 禁止火狐浏览器下拖拽图片的默认事件
document.ondragstart = function () {
return false;
};
}
// 监听鼠标滚轮 // 监听鼠标滚轮
function scrollFunc(e: any) { function scrollFunc(e: any) {
e = e || window.event; e = e || window.event;
e.delta = e.wheelDelta || -e.detail; e.delta = e.wheelDelta || -e.detail;
e.preventDefault(); e.preventDefault();
if (e.delta > 0) { if (e.delta > 0) {
// 滑轮向上滚动 // 滑轮向上滚动
scaleFunc(0.015); scaleFunc(0.015);
}
if (e.delta < 0) {
// 滑轮向下滚动
scaleFunc(-0.015);
}
} }
if (e.delta < 0) { // 缩放函数
// 滑轮向下滚动 function scaleFunc(num: number) {
scaleFunc(-0.015); if (imgState.imgScale <= 0.2 && num < 0) return;
imgState.imgScale += num;
} }
}
// 缩放函数
function scaleFunc(num: number) {
if (imgState.imgScale <= 0.2 && num < 0) return;
imgState.imgScale += num;
}
// 旋转图片 // 旋转图片
function rotateFunc(deg: number) { function rotateFunc(deg: number) {
imgState.imgRotate += deg; imgState.imgRotate += deg;
} }
// 鼠标事件 // 鼠标事件
function handleMouseUp() { function handleMouseUp() {
const imgEl = unref(imgElRef); const imgEl = unref(imgElRef);
if (!imgEl) return; if (!imgEl) return;
imgEl.onmousemove = null; imgEl.onmousemove = null;
} }
// 更换图片 // 更换图片
function handleIChangeImage(url: string) { function handleIChangeImage(url: string) {
imgState.status = StatueEnum.LOADING; imgState.status = StatueEnum.LOADING;
const img = new Image(); const img = new Image();
img.src = url; img.src = url;
img.onload = () => { img.onload = () => {
imgState.currentUrl = url; imgState.currentUrl = url;
imgState.status = StatueEnum.DONE; imgState.status = StatueEnum.DONE;
}; };
img.onerror = () => { img.onerror = () => {
imgState.status = StatueEnum.FAIL; imgState.status = StatueEnum.FAIL;
}; };
} }
// 关闭 // 关闭
function handleClose(e: MouseEvent) { function handleClose(e: MouseEvent) {
e && e.stopPropagation(); e && e.stopPropagation();
imgState.show = false; imgState.show = false;
// 移除火狐浏览器下的鼠标滚动事件 // 移除火狐浏览器下的鼠标滚动事件
document.body.removeEventListener('DOMMouseScroll', scrollFunc); document.body.removeEventListener('DOMMouseScroll', scrollFunc);
// 恢复火狐及Safari浏览器下的图片拖拽 // 恢复火狐及Safari浏览器下的图片拖拽
document.ondragstart = null; document.ondragstart = null;
} }
// 图片复原 // 图片复原
function resume() { function resume() {
initState(); initState();
} }
// 上一页下一页 // 上一页下一页
function handleChange(direction: 'left' | 'right') { function handleChange(direction: 'left' | 'right') {
const { currentIndex } = imgState; const { currentIndex } = imgState;
const { imageList } = props; const { imageList } = props;
if (direction === 'left') { if (direction === 'left') {
imgState.currentIndex--; imgState.currentIndex--;
if (currentIndex <= 0) { if (currentIndex <= 0) {
imgState.currentIndex = imageList.length - 1; imgState.currentIndex = imageList.length - 1;
}
}
if (direction === 'right') {
imgState.currentIndex++;
if (currentIndex >= imageList.length - 1) {
imgState.currentIndex = 0;
}
} }
handleIChangeImage(imageList[imgState.currentIndex]);
} }
if (direction === 'right') {
imgState.currentIndex++; function handleAddMoveListener(e: MouseEvent) {
if (currentIndex >= imageList.length - 1) { e = e || window.event;
imgState.currentIndex = 0; imgState.moveX = e.clientX;
imgState.moveY = e.clientY;
const imgEl = unref(imgElRef);
if (imgEl) {
imgEl.onmousemove = moveFunc;
} }
} }
handleIChangeImage(imageList[imgState.currentIndex]);
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>
)
);
};
},
});
</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;
} }
function handleAddMoveListener(e: MouseEvent) { &__close {
e = e || window.event; position: absolute;
imgState.moveX = e.clientX; top: -40px;
imgState.moveY = e.clientY; right: -40px;
const imgEl = unref(imgElRef); width: 80px;
if (imgEl) { height: 80px;
imgEl.onmousemove = moveFunc; 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);
} }
} }
function moveFunc(e: MouseEvent) { &__index {
e = e || window.event; position: absolute;
e.preventDefault(); bottom: 5%;
const movementX = e.clientX - imgState.moveX; left: 50%;
const movementY = e.clientY - imgState.moveY; padding: 0 22px;
imgState.imgLeft += movementX; font-size: 16px;
imgState.imgTop += movementY; background: rgba(109, 109, 109, 0.6);
imgState.moveX = e.clientX; border-radius: 15px;
imgState.moveY = e.clientY; transform: translateX(-50%);
} }
// 获取图片样式 &__controller {
const getImageStyle = computed(() => { position: absolute;
const { imgScale, imgRotate, imgTop, imgLeft } = imgState; bottom: 10%;
return { left: 50%;
transform: `scale(${imgScale}) rotate(${imgRotate}deg)`, display: flex;
marginTop: `${imgTop}px`, width: 260px;
marginLeft: `${imgLeft}px`, 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;
const getIsMultipleImage = computed(() => { &:hover {
const { imageList } = props; transform: scale(1.2);
return imageList.length > 1; }
});
watchEffect(() => { img {
if (props.show) { width: 1em;
init(); }
} }
if (props.imageList) { }
initState();
&__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 {
const renderClose = () => { left: 50px;
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') => { &.right {
if (!unref(getIsMultipleImage)) { right: 50px;
return null;
} }
return ( }
<div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}> }
{direction === 'left' ? <LeftOutlined /> : <RightOutlined />} </style>
</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>
)
);
};
},
});
import ImgPreview from './index'; import type { Options, Props } from './typing';
import ImgPreview from './Functional.vue';
import { isClient } from '/@/utils/is'; import { isClient } from '/@/utils/is';
import type { Options, Props } from './types';
import { createVNode, render } from 'vue'; import { createVNode, render } from 'vue';
let instance: any = null; let instance: ReturnType<typeof createVNode> | null = null;
export function createImgPreview(options: Options) { export function createImgPreview(options: Options) {
if (!isClient) return; if (!isClient) return;
const { imageList, show = true, index = 0 } = options; const { imageList, show = true, index = 0 } = options;
......
.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;
}
}
}
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,
},
};
<template> <template>
<PageWrapper title="图片预览示例"> <PageWrapper title="图片预览示例">
<p @click="openImg">打开图片</p>
<ImagePreview :imageList="imgList" /> <ImagePreview :imageList="imgList" />
<a-button @click="openImg" type="primary">无预览图</a-button>
</PageWrapper> </PageWrapper>
</template> </template>
<script lang="ts"> <script lang="ts">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论