Commit 305630e3fd886b3f690f890a934a8a6ba224fba1

Authored by Vben
1 parent 3f6920f7

feat(preview): added createImgPreview picture preview function

CHANGELOG.zh_CN.md
@@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
9 - **CropperImage** `Cropper` 头像裁剪新增圆形裁剪功能 9 - **CropperImage** `Cropper` 头像裁剪新增圆形裁剪功能
10 - **CropperAvatar** 新增头像上传组件 10 - **CropperAvatar** 新增头像上传组件
11 - **Drawer** `useDrawer`新增`closeDrawer`函数 11 - **Drawer** `useDrawer`新增`closeDrawer`函数
  12 +- **Preview** 新增`createImgPreview`图片预览函数
12 13
13 ### 🐛 Bug Fixes 14 ### 🐛 Bug Fixes
14 15
src/components/Form/src/components/ApiSelect.vue
@@ -26,7 +26,6 @@ @@ -26,7 +26,6 @@
26 import { useRuleFormItem } from '/@/hooks/component/useFormItem'; 26 import { useRuleFormItem } from '/@/hooks/component/useFormItem';
27 import { useAttrs } from '/@/hooks/core/useAttrs'; 27 import { useAttrs } from '/@/hooks/core/useAttrs';
28 import { get, omit } from 'lodash-es'; 28 import { get, omit } from 'lodash-es';
29 -  
30 import { LoadingOutlined } from '@ant-design/icons-vue'; 29 import { LoadingOutlined } from '@ant-design/icons-vue';
31 import { useI18n } from '/@/hooks/web/useI18n'; 30 import { useI18n } from '/@/hooks/web/useI18n';
32 import { propTypes } from '/@/utils/propTypes'; 31 import { propTypes } from '/@/utils/propTypes';
src/components/Form/src/components/FormAction.vue
@@ -40,13 +40,11 @@ @@ -40,13 +40,11 @@
40 <script lang="ts"> 40 <script lang="ts">
41 import type { ColEx } from '../types/index'; 41 import type { ColEx } from '../types/index';
42 import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; 42 import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
43 -  
44 import { defineComponent, computed, PropType } from 'vue'; 43 import { defineComponent, computed, PropType } from 'vue';
45 import { Form, Col } from 'ant-design-vue'; 44 import { Form, Col } from 'ant-design-vue';
46 import { Button } from '/@/components/Button'; 45 import { Button } from '/@/components/Button';
47 import { BasicArrow } from '/@/components/Basic/index'; 46 import { BasicArrow } from '/@/components/Basic/index';
48 import { useFormContext } from '../hooks/useFormContext'; 47 import { useFormContext } from '../hooks/useFormContext';
49 -  
50 import { useI18n } from '/@/hooks/web/useI18n'; 48 import { useI18n } from '/@/hooks/web/useI18n';
51 import { propTypes } from '/@/utils/propTypes'; 49 import { propTypes } from '/@/utils/propTypes';
52 50
src/components/Form/src/components/FormItem.vue
@@ -4,17 +4,14 @@ @@ -4,17 +4,14 @@
4 import type { FormSchema } from '../types/form'; 4 import type { FormSchema } from '../types/form';
5 import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; 5 import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
6 import type { TableActionType } from '/@/components/Table'; 6 import type { TableActionType } from '/@/components/Table';
7 -  
8 import { defineComponent, computed, unref, toRefs } from 'vue'; 7 import { defineComponent, computed, unref, toRefs } from 'vue';
9 import { Form, Col } from 'ant-design-vue'; 8 import { Form, Col } from 'ant-design-vue';
10 import { componentMap } from '../componentMap'; 9 import { componentMap } from '../componentMap';
11 import { BasicHelp } from '/@/components/Basic'; 10 import { BasicHelp } from '/@/components/Basic';
12 -  
13 import { isBoolean, isFunction, isNull } from '/@/utils/is'; 11 import { isBoolean, isFunction, isNull } from '/@/utils/is';
14 import { getSlot } from '/@/utils/helper/tsxHelper'; 12 import { getSlot } from '/@/utils/helper/tsxHelper';
15 import { createPlaceholderMessage, setComponentRuleType } from '../helper'; 13 import { createPlaceholderMessage, setComponentRuleType } from '../helper';
16 import { upperFirst, cloneDeep } from 'lodash-es'; 14 import { upperFirst, cloneDeep } from 'lodash-es';
17 -  
18 import { useItemLabelWidth } from '../hooks/useLabelWidth'; 15 import { useItemLabelWidth } from '../hooks/useLabelWidth';
19 import { useI18n } from '/@/hooks/web/useI18n'; 16 import { useI18n } from '/@/hooks/web/useI18n';
20 17
@@ -91,7 +88,6 @@ @@ -91,7 +88,6 @@
91 if (isBoolean(dynamicDisabled)) { 88 if (isBoolean(dynamicDisabled)) {
92 disabled = dynamicDisabled; 89 disabled = dynamicDisabled;
93 } 90 }
94 -  
95 if (isFunction(dynamicDisabled)) { 91 if (isFunction(dynamicDisabled)) {
96 disabled = dynamicDisabled(unref(getValues)); 92 disabled = dynamicDisabled(unref(getValues));
97 } 93 }
@@ -276,7 +272,6 @@ @@ -276,7 +272,6 @@
276 : { 272 : {
277 default: () => renderComponentContent, 273 default: () => renderComponentContent,
278 }; 274 };
279 -  
280 return <Comp {...compAttr}>{compSlot}</Comp>; 275 return <Comp {...compAttr}>{compSlot}</Comp>;
281 } 276 }
282 277
@@ -317,7 +312,6 @@ @@ -317,7 +312,6 @@
317 }; 312 };
318 313
319 const showSuffix = !!suffix; 314 const showSuffix = !!suffix;
320 -  
321 const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix; 315 const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
322 316
323 return ( 317 return (
@@ -338,16 +332,18 @@ @@ -338,16 +332,18 @@
338 </Form.Item> 332 </Form.Item>
339 ); 333 );
340 } 334 }
  335 +
341 return () => { 336 return () => {
342 const { colProps = {}, colSlot, renderColContent, component } = props.schema; 337 const { colProps = {}, colSlot, renderColContent, component } = props.schema;
343 - if (!componentMap.has(component)) return null; 338 + if (!componentMap.has(component)) {
  339 + return null;
  340 + }
344 341
345 const { baseColProps = {} } = props.formProps; 342 const { baseColProps = {} } = props.formProps;
346 -  
347 const realColProps = { ...baseColProps, ...colProps }; 343 const realColProps = { ...baseColProps, ...colProps };
348 const { isIfShow, isShow } = getShow(); 344 const { isIfShow, isShow } = getShow();
349 -  
350 const values = unref(getValues); 345 const values = unref(getValues);
  346 +
351 const getContent = () => { 347 const getContent = () => {
352 return colSlot 348 return colSlot
353 ? getSlot(slots, colSlot, values) 349 ? getSlot(slots, colSlot, values)
src/components/Form/src/components/RadioButtonGroup.vue
1 <!-- 1 <!--
2 * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component 2 * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
3 --> 3 -->
4 -  
5 <template> 4 <template>
6 <RadioGroup v-bind="attrs" v-model:value="state" button-style="solid"> 5 <RadioGroup v-bind="attrs" v-model:value="state" button-style="solid">
7 <template v-for="item in getOptions" :key="`${item.value}`"> 6 <template v-for="item in getOptions" :key="`${item.value}`">
@@ -17,6 +16,7 @@ @@ -17,6 +16,7 @@
17 import { isString } from '/@/utils/is'; 16 import { isString } from '/@/utils/is';
18 import { useRuleFormItem } from '/@/hooks/component/useFormItem'; 17 import { useRuleFormItem } from '/@/hooks/component/useFormItem';
19 import { useAttrs } from '/@/hooks/core/useAttrs'; 18 import { useAttrs } from '/@/hooks/core/useAttrs';
  19 +
20 type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean }; 20 type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
21 type RadioItem = string | OptionsItem; 21 type RadioItem = string | OptionsItem;
22 22
@@ -39,6 +39,7 @@ @@ -39,6 +39,7 @@
39 const attrs = useAttrs(); 39 const attrs = useAttrs();
40 // Embedded in the form, just use the hook binding to perform form verification 40 // Embedded in the form, just use the hook binding to perform form verification
41 const [state] = useRuleFormItem(props); 41 const [state] = useRuleFormItem(props);
  42 +
42 // Processing options value 43 // Processing options value
43 const getOptions = computed((): OptionsItem[] => { 44 const getOptions = computed((): OptionsItem[] => {
44 const { options } = props; 45 const { options } = props;
src/components/Form/src/hooks/useAdvanced.ts
@@ -2,10 +2,8 @@ import type { ColEx } from &#39;../types&#39;; @@ -2,10 +2,8 @@ import type { ColEx } from &#39;../types&#39;;
2 import type { AdvanceState } from '../types/hooks'; 2 import type { AdvanceState } from '../types/hooks';
3 import type { ComputedRef, Ref } from 'vue'; 3 import type { ComputedRef, Ref } from 'vue';
4 import type { FormProps, FormSchema } from '../types/form'; 4 import type { FormProps, FormSchema } from '../types/form';
5 -  
6 import { computed, unref, watch } from 'vue'; 5 import { computed, unref, watch } from 'vue';
7 import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is'; 6 import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is';
8 -  
9 import { useBreakpoint } from '/@/hooks/event/useBreakpoint'; 7 import { useBreakpoint } from '/@/hooks/event/useBreakpoint';
10 import { useDebounceFn } from '@vueuse/core'; 8 import { useDebounceFn } from '@vueuse/core';
11 9
src/components/Form/src/hooks/useAutoFocus.ts
@@ -16,16 +16,22 @@ export async function useAutoFocus({ @@ -16,16 +16,22 @@ export async function useAutoFocus({
16 isInitedDefault, 16 isInitedDefault,
17 }: UseAutoFocusContext) { 17 }: UseAutoFocusContext) {
18 watchEffect(async () => { 18 watchEffect(async () => {
19 - if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) return; 19 + if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) {
  20 + return;
  21 + }
20 await nextTick(); 22 await nextTick();
21 const schemas = unref(getSchema); 23 const schemas = unref(getSchema);
22 const formEl = unref(formElRef); 24 const formEl = unref(formElRef);
23 const el = (formEl as any)?.$el as HTMLElement; 25 const el = (formEl as any)?.$el as HTMLElement;
24 - if (!formEl || !el || !schemas || schemas.length === 0) return; 26 + if (!formEl || !el || !schemas || schemas.length === 0) {
  27 + return;
  28 + }
25 29
26 const firstItem = schemas[0]; 30 const firstItem = schemas[0];
27 // Only open when the first form item is input type 31 // Only open when the first form item is input type
28 - if (!firstItem.component.includes('Input')) return; 32 + if (!firstItem.component.includes('Input')) {
  33 + return;
  34 + }
29 35
30 const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>; 36 const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
31 if (!inputEl) return; 37 if (!inputEl) return;
src/components/Form/src/hooks/useForm.ts
  1 +import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
  2 +import type { NamePath } from 'ant-design-vue/lib/form/interface';
  3 +import type { DynamicProps } from '/#/utils';
1 import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; 4 import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
2 -  
3 import { isProdMode } from '/@/utils/env'; 5 import { isProdMode } from '/@/utils/env';
4 import { error } from '/@/utils/log'; 6 import { error } from '/@/utils/log';
5 import { getDynamicProps } from '/@/utils'; 7 import { getDynamicProps } from '/@/utils';
6 8
7 -import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';  
8 -import type { NamePath } from 'ant-design-vue/lib/form/interface';  
9 -import type { DynamicProps } from '/#/utils';  
10 -  
11 export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>; 9 export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>;
12 10
13 type Props = Partial<DynamicProps<FormProps>>; 11 type Props = Partial<DynamicProps<FormProps>>;
src/components/Form/src/hooks/useFormEvents.ts
1 import type { ComputedRef, Ref } from 'vue'; 1 import type { ComputedRef, Ref } from 'vue';
2 import type { FormProps, FormSchema, FormActionType } from '../types/form'; 2 import type { FormProps, FormSchema, FormActionType } from '../types/form';
3 import type { NamePath } from 'ant-design-vue/lib/form/interface'; 3 import type { NamePath } from 'ant-design-vue/lib/form/interface';
4 -  
5 import { unref, toRaw } from 'vue'; 4 import { unref, toRaw } from 'vue';
6 -  
7 import { isArray, isFunction, isObject, isString } from '/@/utils/is'; 5 import { isArray, isFunction, isObject, isString } from '/@/utils/is';
8 import { deepMerge } from '/@/utils'; 6 import { deepMerge } from '/@/utils';
9 import { dateItemType, handleInputNumberValue } from '../helper'; 7 import { dateItemType, handleInputNumberValue } from '../helper';
src/components/Form/src/hooks/useFormValues.ts
1 import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is'; 1 import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is';
2 import { dateUtil } from '/@/utils/dateUtil'; 2 import { dateUtil } from '/@/utils/dateUtil';
3 -  
4 import { unref } from 'vue'; 3 import { unref } from 'vue';
5 import type { Ref, ComputedRef } from 'vue'; 4 import type { Ref, ComputedRef } from 'vue';
6 import type { FormProps, FormSchema } from '../types/form'; 5 import type { FormProps, FormSchema } from '../types/form';
7 -  
8 import { set } from 'lodash-es'; 6 import { set } from 'lodash-es';
9 7
10 interface UseFormValuesContext { 8 interface UseFormValuesContext {
src/components/Form/src/types/form.ts
1 import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface'; 1 import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
2 import type { VNode } from 'vue'; 2 import type { VNode } from 'vue';
3 import type { ButtonProps as AntdButtonProps } from 'ant-design-vue/es/button/buttonTypes'; 3 import type { ButtonProps as AntdButtonProps } from 'ant-design-vue/es/button/buttonTypes';
4 -  
5 import type { FormItem } from './formItem'; 4 import type { FormItem } from './formItem';
6 import type { ColEx, ComponentType } from './index'; 5 import type { ColEx, ComponentType } from './index';
7 import type { TableActionType } from '/@/components/Table/src/types/table'; 6 import type { TableActionType } from '/@/components/Table/src/types/table';
src/components/Form/src/types/index.ts
@@ -90,9 +90,7 @@ export type ComponentType = @@ -90,9 +90,7 @@ export type ComponentType =
90 | 'InputCountDown' 90 | 'InputCountDown'
91 | 'Select' 91 | 'Select'
92 | 'ApiSelect' 92 | 'ApiSelect'
93 - | 'SelectOptGroup'  
94 | 'TreeSelect' 93 | 'TreeSelect'
95 - | 'Transfer'  
96 | 'RadioButtonGroup' 94 | 'RadioButtonGroup'
97 | 'RadioGroup' 95 | 'RadioGroup'
98 | 'Checkbox' 96 | 'Checkbox'
src/components/Preview/src/index.tsx renamed to src/components/Preview/src/Functional.vue
1 -import './index.less';  
2 -  
3 -import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';  
4 -  
5 -// @ts-ignore  
6 -import { basicProps } from './props';  
7 -// @ts-ignore  
8 -import { Props } from './types';  
9 -  
10 -import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';  
11 -// import { Spin } from 'ant-design-vue';  
12 -  
13 -import resumeSvg from '/@/assets/svg/preview/resume.svg';  
14 -import rotateSvg from '/@/assets/svg/preview/p-rotate.svg';  
15 -import scaleSvg from '/@/assets/svg/preview/scale.svg';  
16 -import unScaleSvg from '/@/assets/svg/preview/unscale.svg';  
17 -import unRotateSvg from '/@/assets/svg/preview/unrotate.svg';  
18 -enum StatueEnum {  
19 - LOADING,  
20 - DONE,  
21 - FAIL,  
22 -}  
23 -interface ImgState {  
24 - currentUrl: string;  
25 - imgScale: number;  
26 - imgRotate: number;  
27 - imgTop: number;  
28 - imgLeft: number;  
29 - currentIndex: number;  
30 - status: StatueEnum;  
31 - moveX: number;  
32 - moveY: number;  
33 - show: boolean;  
34 -}  
35 -  
36 -const prefixCls = 'img-preview';  
37 -export default defineComponent({  
38 - name: 'ImagePreview',  
39 - props: basicProps,  
40 - setup(props: Props) {  
41 - const imgState = reactive<ImgState>({  
42 - currentUrl: '',  
43 - imgScale: 1,  
44 - imgRotate: 0,  
45 - imgTop: 0,  
46 - imgLeft: 0,  
47 - status: StatueEnum.LOADING,  
48 - currentIndex: 0,  
49 - moveX: 0,  
50 - moveY: 0,  
51 - show: props.show,  
52 - });  
53 -  
54 - const wrapElRef = ref<HTMLDivElement | null>(null);  
55 - const imgElRef = ref<HTMLImageElement | null>(null);  
56 -  
57 - // 初始化  
58 - function init() {  
59 - initMouseWheel();  
60 - const { index, imageList } = props;  
61 -  
62 - if (!imageList || !imageList.length) {  
63 - throw new Error('imageList is undefined'); 1 +<script lang="tsx">
  2 + import { defineComponent, ref, unref, computed, reactive, watchEffect } from 'vue';
  3 + import { Props } from './typing';
  4 + import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
  5 + import resumeSvg from '/@/assets/svg/preview/resume.svg';
  6 + import rotateSvg from '/@/assets/svg/preview/p-rotate.svg';
  7 + import scaleSvg from '/@/assets/svg/preview/scale.svg';
  8 + import unScaleSvg from '/@/assets/svg/preview/unscale.svg';
  9 + import unRotateSvg from '/@/assets/svg/preview/unrotate.svg';
  10 +
  11 + enum StatueEnum {
  12 + LOADING,
  13 + DONE,
  14 + FAIL,
  15 + }
  16 + interface ImgState {
  17 + currentUrl: string;
  18 + imgScale: number;
  19 + imgRotate: number;
  20 + imgTop: number;
  21 + imgLeft: number;
  22 + currentIndex: number;
  23 + status: StatueEnum;
  24 + moveX: number;
  25 + moveY: number;
  26 + show: boolean;
  27 + }
  28 + const props = {
  29 + show: {
  30 + type: Boolean as PropType<boolean>,
  31 + default: false,
  32 + },
  33 + imageList: {
  34 + type: [Array] as PropType<string[]>,
  35 + default: null,
  36 + },
  37 + index: {
  38 + type: Number as PropType<number>,
  39 + default: 0,
  40 + },
  41 + };
  42 +
  43 + const prefixCls = 'img-preview';
  44 + export default defineComponent({
  45 + name: 'ImagePreview',
  46 + props,
  47 + setup(props: Props) {
  48 + const imgState = reactive<ImgState>({
  49 + currentUrl: '',
  50 + imgScale: 1,
  51 + imgRotate: 0,
  52 + imgTop: 0,
  53 + imgLeft: 0,
  54 + status: StatueEnum.LOADING,
  55 + currentIndex: 0,
  56 + moveX: 0,
  57 + moveY: 0,
  58 + show: props.show,
  59 + });
  60 +
  61 + const wrapElRef = ref<HTMLDivElement | null>(null);
  62 + const imgElRef = ref<HTMLImageElement | null>(null);
  63 +
  64 + // 初始化
  65 + function init() {
  66 + initMouseWheel();
  67 + const { index, imageList } = props;
  68 +
  69 + if (!imageList || !imageList.length) {
  70 + throw new Error('imageList is undefined');
  71 + }
  72 + imgState.currentIndex = index;
  73 + handleIChangeImage(imageList[index]);
64 } 74 }
65 - imgState.currentIndex = index;  
66 - handleIChangeImage(imageList[index]);  
67 - }  
68 75
69 - // 重置  
70 - function initState() {  
71 - imgState.imgScale = 1;  
72 - imgState.imgRotate = 0;  
73 - imgState.imgTop = 0;  
74 - imgState.imgLeft = 0;  
75 - } 76 + // 重置
  77 + function initState() {
  78 + imgState.imgScale = 1;
  79 + imgState.imgRotate = 0;
  80 + imgState.imgTop = 0;
  81 + imgState.imgLeft = 0;
  82 + }
76 83
77 - // 初始化鼠标滚轮事件  
78 - function initMouseWheel() {  
79 - const wrapEl = unref(wrapElRef);  
80 - if (!wrapEl) {  
81 - return; 84 + // 初始化鼠标滚轮事件
  85 + function initMouseWheel() {
  86 + const wrapEl = unref(wrapElRef);
  87 + if (!wrapEl) {
  88 + return;
  89 + }
  90 + (wrapEl as any).onmousewheel = scrollFunc;
  91 + // 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替
  92 + document.body.addEventListener('DOMMouseScroll', scrollFunc);
  93 + // 禁止火狐浏览器下拖拽图片的默认事件
  94 + document.ondragstart = function () {
  95 + return false;
  96 + };
82 } 97 }
83 - (wrapEl as any).onmousewheel = scrollFunc;  
84 - // 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替  
85 - document.body.addEventListener('DOMMouseScroll', scrollFunc);  
86 - // 禁止火狐浏览器下拖拽图片的默认事件  
87 - document.ondragstart = function () {  
88 - return false;  
89 - };  
90 - }  
91 98
92 - // 监听鼠标滚轮  
93 - function scrollFunc(e: any) {  
94 - e = e || window.event;  
95 - e.delta = e.wheelDelta || -e.detail; 99 + // 监听鼠标滚轮
  100 + function scrollFunc(e: any) {
  101 + e = e || window.event;
  102 + e.delta = e.wheelDelta || -e.detail;
96 103
97 - e.preventDefault();  
98 - if (e.delta > 0) {  
99 - // 滑轮向上滚动  
100 - scaleFunc(0.015); 104 + e.preventDefault();
  105 + if (e.delta > 0) {
  106 + // 滑轮向上滚动
  107 + scaleFunc(0.015);
  108 + }
  109 + if (e.delta < 0) {
  110 + // 滑轮向下滚动
  111 + scaleFunc(-0.015);
  112 + }
101 } 113 }
102 - if (e.delta < 0) {  
103 - // 滑轮向下滚动  
104 - scaleFunc(-0.015); 114 + // 缩放函数
  115 + function scaleFunc(num: number) {
  116 + if (imgState.imgScale <= 0.2 && num < 0) return;
  117 + imgState.imgScale += num;
105 } 118 }
106 - }  
107 - // 缩放函数  
108 - function scaleFunc(num: number) {  
109 - if (imgState.imgScale <= 0.2 && num < 0) return;  
110 - imgState.imgScale += num;  
111 - }  
112 119
113 - // 旋转图片  
114 - function rotateFunc(deg: number) {  
115 - imgState.imgRotate += deg;  
116 - } 120 + // 旋转图片
  121 + function rotateFunc(deg: number) {
  122 + imgState.imgRotate += deg;
  123 + }
117 124
118 - // 鼠标事件  
119 - function handleMouseUp() {  
120 - const imgEl = unref(imgElRef);  
121 - if (!imgEl) return;  
122 - imgEl.onmousemove = null;  
123 - } 125 + // 鼠标事件
  126 + function handleMouseUp() {
  127 + const imgEl = unref(imgElRef);
  128 + if (!imgEl) return;
  129 + imgEl.onmousemove = null;
  130 + }
124 131
125 - // 更换图片  
126 - function handleIChangeImage(url: string) {  
127 - imgState.status = StatueEnum.LOADING;  
128 - const img = new Image();  
129 - img.src = url;  
130 - img.onload = () => {  
131 - imgState.currentUrl = url;  
132 - imgState.status = StatueEnum.DONE;  
133 - };  
134 - img.onerror = () => {  
135 - imgState.status = StatueEnum.FAIL;  
136 - };  
137 - } 132 + // 更换图片
  133 + function handleIChangeImage(url: string) {
  134 + imgState.status = StatueEnum.LOADING;
  135 + const img = new Image();
  136 + img.src = url;
  137 + img.onload = () => {
  138 + imgState.currentUrl = url;
  139 + imgState.status = StatueEnum.DONE;
  140 + };
  141 + img.onerror = () => {
  142 + imgState.status = StatueEnum.FAIL;
  143 + };
  144 + }
138 145
139 - // 关闭  
140 - function handleClose(e: MouseEvent) {  
141 - e && e.stopPropagation();  
142 - imgState.show = false;  
143 - // 移除火狐浏览器下的鼠标滚动事件  
144 - document.body.removeEventListener('DOMMouseScroll', scrollFunc);  
145 - // 恢复火狐及Safari浏览器下的图片拖拽  
146 - document.ondragstart = null;  
147 - } 146 + // 关闭
  147 + function handleClose(e: MouseEvent) {
  148 + e && e.stopPropagation();
  149 + imgState.show = false;
  150 + // 移除火狐浏览器下的鼠标滚动事件
  151 + document.body.removeEventListener('DOMMouseScroll', scrollFunc);
  152 + // 恢复火狐及Safari浏览器下的图片拖拽
  153 + document.ondragstart = null;
  154 + }
148 155
149 - // 图片复原  
150 - function resume() {  
151 - initState();  
152 - } 156 + // 图片复原
  157 + function resume() {
  158 + initState();
  159 + }
153 160
154 - // 上一页下一页  
155 - function handleChange(direction: 'left' | 'right') {  
156 - const { currentIndex } = imgState;  
157 - const { imageList } = props;  
158 - if (direction === 'left') {  
159 - imgState.currentIndex--;  
160 - if (currentIndex <= 0) {  
161 - imgState.currentIndex = imageList.length - 1; 161 + // 上一页下一页
  162 + function handleChange(direction: 'left' | 'right') {
  163 + const { currentIndex } = imgState;
  164 + const { imageList } = props;
  165 + if (direction === 'left') {
  166 + imgState.currentIndex--;
  167 + if (currentIndex <= 0) {
  168 + imgState.currentIndex = imageList.length - 1;
  169 + }
  170 + }
  171 + if (direction === 'right') {
  172 + imgState.currentIndex++;
  173 + if (currentIndex >= imageList.length - 1) {
  174 + imgState.currentIndex = 0;
  175 + }
162 } 176 }
  177 + handleIChangeImage(imageList[imgState.currentIndex]);
163 } 178 }
164 - if (direction === 'right') {  
165 - imgState.currentIndex++;  
166 - if (currentIndex >= imageList.length - 1) {  
167 - imgState.currentIndex = 0; 179 +
  180 + function handleAddMoveListener(e: MouseEvent) {
  181 + e = e || window.event;
  182 + imgState.moveX = e.clientX;
  183 + imgState.moveY = e.clientY;
  184 + const imgEl = unref(imgElRef);
  185 + if (imgEl) {
  186 + imgEl.onmousemove = moveFunc;
168 } 187 }
169 } 188 }
170 - handleIChangeImage(imageList[imgState.currentIndex]); 189 +
  190 + function moveFunc(e: MouseEvent) {
  191 + e = e || window.event;
  192 + e.preventDefault();
  193 + const movementX = e.clientX - imgState.moveX;
  194 + const movementY = e.clientY - imgState.moveY;
  195 + imgState.imgLeft += movementX;
  196 + imgState.imgTop += movementY;
  197 + imgState.moveX = e.clientX;
  198 + imgState.moveY = e.clientY;
  199 + }
  200 +
  201 + // 获取图片样式
  202 + const getImageStyle = computed(() => {
  203 + const { imgScale, imgRotate, imgTop, imgLeft } = imgState;
  204 + return {
  205 + transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,
  206 + marginTop: `${imgTop}px`,
  207 + marginLeft: `${imgLeft}px`,
  208 + };
  209 + });
  210 +
  211 + const getIsMultipleImage = computed(() => {
  212 + const { imageList } = props;
  213 + return imageList.length > 1;
  214 + });
  215 +
  216 + watchEffect(() => {
  217 + if (props.show) {
  218 + init();
  219 + }
  220 + if (props.imageList) {
  221 + initState();
  222 + }
  223 + });
  224 +
  225 + const renderClose = () => {
  226 + return (
  227 + <div class={`${prefixCls}__close`} onClick={handleClose}>
  228 + <CloseOutlined class={`${prefixCls}__close-icon`} />
  229 + </div>
  230 + );
  231 + };
  232 +
  233 + const renderIndex = () => {
  234 + if (!unref(getIsMultipleImage)) {
  235 + return null;
  236 + }
  237 + const { currentIndex } = imgState;
  238 + const { imageList } = props;
  239 + return (
  240 + <div class={`${prefixCls}__index`}>
  241 + {currentIndex + 1} / {imageList.length}
  242 + </div>
  243 + );
  244 + };
  245 +
  246 + const renderController = () => {
  247 + return (
  248 + <div class={`${prefixCls}__controller`}>
  249 + <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(-0.15)}>
  250 + <img src={unScaleSvg} />
  251 + </div>
  252 + <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}>
  253 + <img src={scaleSvg} />
  254 + </div>
  255 + <div class={`${prefixCls}__controller-item`} onClick={resume}>
  256 + <img src={resumeSvg} />
  257 + </div>
  258 + <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(-90)}>
  259 + <img src={unRotateSvg} />
  260 + </div>
  261 + <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(90)}>
  262 + <img src={rotateSvg} />
  263 + </div>
  264 + </div>
  265 + );
  266 + };
  267 +
  268 + const renderArrow = (direction: 'left' | 'right') => {
  269 + if (!unref(getIsMultipleImage)) {
  270 + return null;
  271 + }
  272 + return (
  273 + <div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}>
  274 + {direction === 'left' ? <LeftOutlined /> : <RightOutlined />}
  275 + </div>
  276 + );
  277 + };
  278 +
  279 + return () => {
  280 + return (
  281 + imgState.show && (
  282 + <div class={prefixCls} ref={wrapElRef} onMouseup={handleMouseUp}>
  283 + <div class={`${prefixCls}-content`}>
  284 + {/*<Spin*/}
  285 + {/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}
  286 + {/* spinning={true}*/}
  287 + {/* class={[*/}
  288 + {/* `${prefixCls}-image`,*/}
  289 + {/* {*/}
  290 + {/* hidden: imgState.status !== StatueEnum.LOADING,*/}
  291 + {/* },*/}
  292 + {/* ]}*/}
  293 + {/*/>*/}
  294 + <img
  295 + style={unref(getImageStyle)}
  296 + class={[
  297 + `${prefixCls}-image`,
  298 + imgState.status === StatueEnum.DONE ? '' : 'hidden',
  299 + ]}
  300 + ref={imgElRef}
  301 + src={imgState.currentUrl}
  302 + onMousedown={handleAddMoveListener}
  303 + />
  304 + {renderClose()}
  305 + {renderIndex()}
  306 + {renderController()}
  307 + {renderArrow('left')}
  308 + {renderArrow('right')}
  309 + </div>
  310 + </div>
  311 + )
  312 + );
  313 + };
  314 + },
  315 + });
  316 +</script>
  317 +<style lang="less">
  318 + .img-preview {
  319 + position: fixed;
  320 + top: 0;
  321 + right: 0;
  322 + bottom: 0;
  323 + left: 0;
  324 + z-index: @preview-comp-z-index;
  325 + background: rgba(0, 0, 0, 0.5);
  326 + user-select: none;
  327 +
  328 + &-content {
  329 + display: flex;
  330 + width: 100%;
  331 + height: 100%;
  332 + color: @white;
  333 + justify-content: center;
  334 + align-items: center;
  335 + }
  336 +
  337 + &-image {
  338 + cursor: pointer;
  339 + transition: transform 0.3s;
171 } 340 }
172 341
173 - function handleAddMoveListener(e: MouseEvent) {  
174 - e = e || window.event;  
175 - imgState.moveX = e.clientX;  
176 - imgState.moveY = e.clientY;  
177 - const imgEl = unref(imgElRef);  
178 - if (imgEl) {  
179 - imgEl.onmousemove = moveFunc; 342 + &__close {
  343 + position: absolute;
  344 + top: -40px;
  345 + right: -40px;
  346 + width: 80px;
  347 + height: 80px;
  348 + overflow: hidden;
  349 + color: @white;
  350 + cursor: pointer;
  351 + background-color: rgba(0, 0, 0, 0.5);
  352 + border-radius: 50%;
  353 + transition: all 0.2s;
  354 +
  355 + &-icon {
  356 + position: absolute;
  357 + top: 46px;
  358 + left: 16px;
  359 + font-size: 16px;
  360 + }
  361 +
  362 + &:hover {
  363 + background-color: rgba(0, 0, 0, 0.8);
180 } 364 }
181 } 365 }
182 366
183 - function moveFunc(e: MouseEvent) {  
184 - e = e || window.event;  
185 - e.preventDefault();  
186 - const movementX = e.clientX - imgState.moveX;  
187 - const movementY = e.clientY - imgState.moveY;  
188 - imgState.imgLeft += movementX;  
189 - imgState.imgTop += movementY;  
190 - imgState.moveX = e.clientX;  
191 - imgState.moveY = e.clientY; 367 + &__index {
  368 + position: absolute;
  369 + bottom: 5%;
  370 + left: 50%;
  371 + padding: 0 22px;
  372 + font-size: 16px;
  373 + background: rgba(109, 109, 109, 0.6);
  374 + border-radius: 15px;
  375 + transform: translateX(-50%);
192 } 376 }
193 377
194 - // 获取图片样式  
195 - const getImageStyle = computed(() => {  
196 - const { imgScale, imgRotate, imgTop, imgLeft } = imgState;  
197 - return {  
198 - transform: `scale(${imgScale}) rotate(${imgRotate}deg)`,  
199 - marginTop: `${imgTop}px`,  
200 - marginLeft: `${imgLeft}px`,  
201 - };  
202 - }); 378 + &__controller {
  379 + position: absolute;
  380 + bottom: 10%;
  381 + left: 50%;
  382 + display: flex;
  383 + width: 260px;
  384 + height: 44px;
  385 + padding: 0 22px;
  386 + margin-left: -139px;
  387 + background: rgba(109, 109, 109, 0.6);
  388 + border-radius: 22px;
  389 + justify-content: center;
  390 +
  391 + &-item {
  392 + display: flex;
  393 + height: 100%;
  394 + padding: 0 9px;
  395 + font-size: 24px;
  396 + cursor: pointer;
  397 + transition: all 0.2s;
203 398
204 - const getIsMultipleImage = computed(() => {  
205 - const { imageList } = props;  
206 - return imageList.length > 1;  
207 - }); 399 + &:hover {
  400 + transform: scale(1.2);
  401 + }
208 402
209 - watchEffect(() => {  
210 - if (props.show) {  
211 - init(); 403 + img {
  404 + width: 1em;
  405 + }
212 } 406 }
213 - if (props.imageList) {  
214 - initState(); 407 + }
  408 +
  409 + &__arrow {
  410 + position: absolute;
  411 + top: 50%;
  412 + display: flex;
  413 + align-items: center;
  414 + justify-content: center;
  415 + width: 50px;
  416 + height: 50px;
  417 + font-size: 28px;
  418 + cursor: pointer;
  419 + background-color: rgba(0, 0, 0, 0.5);
  420 + border-radius: 50%;
  421 + transition: all 0.2s;
  422 +
  423 + &:hover {
  424 + background-color: rgba(0, 0, 0, 0.8);
215 } 425 }
216 - });  
217 -  
218 - const renderClose = () => {  
219 - return (  
220 - <div class={`${prefixCls}__close`} onClick={handleClose}>  
221 - <CloseOutlined class={`${prefixCls}__close-icon`} />  
222 - </div>  
223 - );  
224 - };  
225 -  
226 - const renderIndex = () => {  
227 - if (!unref(getIsMultipleImage)) {  
228 - return null; 426 +
  427 + &.left {
  428 + left: 50px;
229 } 429 }
230 - const { currentIndex } = imgState;  
231 - const { imageList } = props;  
232 - return (  
233 - <div class={`${prefixCls}__index`}>  
234 - {currentIndex + 1} / {imageList.length}  
235 - </div>  
236 - );  
237 - };  
238 -  
239 - const renderController = () => {  
240 - return (  
241 - <div class={`${prefixCls}__controller`}>  
242 - <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(-0.15)}>  
243 - <img src={unScaleSvg} />  
244 - </div>  
245 - <div class={`${prefixCls}__controller-item`} onClick={() => scaleFunc(0.15)}>  
246 - <img src={scaleSvg} />  
247 - </div>  
248 - <div class={`${prefixCls}__controller-item`} onClick={resume}>  
249 - <img src={resumeSvg} />  
250 - </div>  
251 - <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(-90)}>  
252 - <img src={unRotateSvg} />  
253 - </div>  
254 - <div class={`${prefixCls}__controller-item`} onClick={() => rotateFunc(90)}>  
255 - <img src={rotateSvg} />  
256 - </div>  
257 - </div>  
258 - );  
259 - };  
260 430
261 - const renderArrow = (direction: 'left' | 'right') => {  
262 - if (!unref(getIsMultipleImage)) {  
263 - return null; 431 + &.right {
  432 + right: 50px;
264 } 433 }
265 - return (  
266 - <div class={[`${prefixCls}__arrow`, direction]} onClick={() => handleChange(direction)}>  
267 - {direction === 'left' ? <LeftOutlined /> : <RightOutlined />}  
268 - </div>  
269 - );  
270 - };  
271 -  
272 - return () => {  
273 - return (  
274 - imgState.show && (  
275 - <div class={prefixCls} ref={wrapElRef} onMouseup={handleMouseUp}>  
276 - <div class={`${prefixCls}-content`}>  
277 - {/*<Spin*/}  
278 - {/* indicator={<LoadingOutlined style="font-size: 24px" spin />}*/}  
279 - {/* spinning={true}*/}  
280 - {/* class={[*/}  
281 - {/* `${prefixCls}-image`,*/}  
282 - {/* {*/}  
283 - {/* hidden: imgState.status !== StatueEnum.LOADING,*/}  
284 - {/* },*/}  
285 - {/* ]}*/}  
286 - {/*/>*/}  
287 - <img  
288 - style={unref(getImageStyle)}  
289 - class={[`${prefixCls}-image`, imgState.status === StatueEnum.DONE ? '' : 'hidden']}  
290 - ref={imgElRef}  
291 - src={imgState.currentUrl}  
292 - onMousedown={handleAddMoveListener}  
293 - />  
294 - {renderClose()}  
295 - {renderIndex()}  
296 - {renderController()}  
297 - {renderArrow('left')}  
298 - {renderArrow('right')}  
299 - </div>  
300 - </div>  
301 - )  
302 - );  
303 - };  
304 - },  
305 -}); 434 + }
  435 + }
  436 +</style>
src/components/Preview/src/functional.ts
1 -import ImgPreview from './index'; 1 +import type { Options, Props } from './typing';
  2 +import ImgPreview from './Functional.vue';
2 import { isClient } from '/@/utils/is'; 3 import { isClient } from '/@/utils/is';
3 -  
4 -import type { Options, Props } from './types';  
5 -  
6 import { createVNode, render } from 'vue'; 4 import { createVNode, render } from 'vue';
7 5
8 -let instance: any = null; 6 +let instance: ReturnType<typeof createVNode> | null = null;
9 export function createImgPreview(options: Options) { 7 export function createImgPreview(options: Options) {
10 if (!isClient) return; 8 if (!isClient) return;
11 const { imageList, show = true, index = 0 } = options; 9 const { imageList, show = true, index = 0 } = options;
src/components/Preview/src/index.less deleted 100644 → 0
1 -.img-preview {  
2 - position: fixed;  
3 - top: 0;  
4 - right: 0;  
5 - bottom: 0;  
6 - left: 0;  
7 - z-index: @preview-comp-z-index;  
8 - background: rgba(0, 0, 0, 0.5);  
9 - user-select: none;  
10 -  
11 - &-content {  
12 - display: flex;  
13 - width: 100%;  
14 - height: 100%;  
15 - color: @white;  
16 - justify-content: center;  
17 - align-items: center;  
18 - }  
19 -  
20 - &-image {  
21 - cursor: pointer;  
22 - transition: transform 0.3s;  
23 - }  
24 -  
25 - &__close {  
26 - position: absolute;  
27 - top: -40px;  
28 - right: -40px;  
29 - width: 80px;  
30 - height: 80px;  
31 - overflow: hidden;  
32 - color: @white;  
33 - cursor: pointer;  
34 - background-color: rgba(0, 0, 0, 0.5);  
35 - border-radius: 50%;  
36 - transition: all 0.2s;  
37 -  
38 - &-icon {  
39 - position: absolute;  
40 - top: 46px;  
41 - left: 16px;  
42 - font-size: 16px;  
43 - }  
44 -  
45 - &:hover {  
46 - background-color: rgba(0, 0, 0, 0.8);  
47 - }  
48 - }  
49 -  
50 - &__index {  
51 - position: absolute;  
52 - bottom: 5%;  
53 - left: 50%;  
54 - padding: 0 22px;  
55 - font-size: 16px;  
56 - background: rgba(109, 109, 109, 0.6);  
57 - border-radius: 15px;  
58 - transform: translateX(-50%);  
59 - }  
60 -  
61 - &__controller {  
62 - position: absolute;  
63 - bottom: 10%;  
64 - left: 50%;  
65 - display: flex;  
66 - width: 260px;  
67 - height: 44px;  
68 - padding: 0 22px;  
69 - margin-left: -139px;  
70 - background: rgba(109, 109, 109, 0.6);  
71 - border-radius: 22px;  
72 - justify-content: center;  
73 -  
74 - &-item {  
75 - display: flex;  
76 - height: 100%;  
77 - padding: 0 9px;  
78 - font-size: 24px;  
79 - cursor: pointer;  
80 - transition: all 0.2s;  
81 -  
82 - &:hover {  
83 - transform: scale(1.2);  
84 - }  
85 -  
86 - img {  
87 - width: 1em;  
88 - }  
89 - }  
90 - }  
91 -  
92 - &__arrow {  
93 - position: absolute;  
94 - top: 50%;  
95 - display: flex;  
96 - align-items: center;  
97 - justify-content: center;  
98 - width: 50px;  
99 - height: 50px;  
100 - font-size: 28px;  
101 - cursor: pointer;  
102 - background-color: rgba(0, 0, 0, 0.5);  
103 - border-radius: 50%;  
104 - transition: all 0.2s;  
105 -  
106 - &:hover {  
107 - background-color: rgba(0, 0, 0, 0.8);  
108 - }  
109 -  
110 - &.left {  
111 - left: 50px;  
112 - }  
113 -  
114 - &.right {  
115 - right: 50px;  
116 - }  
117 - }  
118 -}  
src/components/Preview/src/props.ts deleted 100644 → 0
1 -import { PropType } from 'vue';  
2 -export const basicProps = {  
3 - show: {  
4 - type: Boolean as PropType<boolean>,  
5 - default: false,  
6 - },  
7 - imageList: {  
8 - type: [Array] as PropType<string[]>,  
9 - default: null,  
10 - },  
11 - index: {  
12 - type: Number as PropType<number>,  
13 - default: 0,  
14 - },  
15 -};  
src/components/Preview/src/types.ts renamed to src/components/Preview/src/typing.ts
src/views/demo/feat/img-preview/index.vue
1 <template> 1 <template>
2 <PageWrapper title="图片预览示例"> 2 <PageWrapper title="图片预览示例">
3 - <p @click="openImg">打开图片</p>  
4 <ImagePreview :imageList="imgList" /> 3 <ImagePreview :imageList="imgList" />
  4 + <a-button @click="openImg" type="primary">无预览图</a-button>
5 </PageWrapper> 5 </PageWrapper>
6 </template> 6 </template>
7 <script lang="ts"> 7 <script lang="ts">