Commit 81baf1d5c4606aab83c0e65397ce4b090c2e4e08
1 parent
819127e8
perf: perf modal and drawer
Showing
25 changed files
with
572 additions
and
497 deletions
CHANGELOG.zh_CN.md
... | ... | @@ -18,12 +18,18 @@ |
18 | 18 | |
19 | 19 | - 缓存可以配置是否加密,默认生产环境开启 Aes 加密 |
20 | 20 | - 新增标签页拖拽排序 |
21 | +- 新增 LayoutFooter.默认显示,可以在配置内关闭 | |
22 | + | |
23 | +### ⚡ Performance Improvements | |
24 | + | |
25 | +- 优化`Modal`组件全屏动画不流畅问题 | |
21 | 26 | |
22 | 27 | ### 🐛 Bug Fixes |
23 | 28 | |
24 | 29 | - 修复 tree 文本超出挡住操作按钮问题 |
25 | 30 | - 修复通过 useRedo 刷新页面参数丢失问题 |
26 | 31 | - 修复表单校验先设置在校验及控制台错误信息问题 |
32 | +- 修复`modal`与`drawer`组件传递数组参数问题 | |
27 | 33 | |
28 | 34 | ### 🎫 Chores |
29 | 35 | ... | ... |
src/components/Drawer/index.ts
1 | -export { default as BasicDrawer } from './src/BasicDrawer'; | |
1 | +import BasicDrawerLib from './src/BasicDrawer'; | |
2 | +import { withInstall } from '../util'; | |
2 | 3 | |
3 | -export { useDrawer, useDrawerInner } from './src/useDrawer'; | |
4 | 4 | export * from './src/types'; |
5 | +export { useDrawer, useDrawerInner } from './src/useDrawer'; | |
6 | +export const BasicDrawer = withInstall(BasicDrawerLib); | ... | ... |
src/components/Drawer/src/BasicDrawer.tsx
1 | 1 | import './index.less'; |
2 | 2 | |
3 | 3 | import type { DrawerInstance, DrawerProps } from './types'; |
4 | +import type { CSSProperties } from 'vue'; | |
4 | 5 | |
5 | 6 | import { defineComponent, ref, computed, watchEffect, watch, unref, nextTick, toRaw } from 'vue'; |
6 | 7 | import { Drawer, Row, Col, Button } from 'ant-design-vue'; |
... | ... | @@ -9,53 +10,96 @@ import { BasicTitle } from '/@/components/Basic'; |
9 | 10 | import { FullLoading } from '/@/components/Loading/index'; |
10 | 11 | import { LeftOutlined } from '@ant-design/icons-vue'; |
11 | 12 | |
12 | -import { basicProps } from './props'; | |
13 | +import { useI18n } from '/@/hooks/web/useI18n'; | |
13 | 14 | |
14 | 15 | import { getSlot } from '/@/utils/helper/tsxHelper'; |
15 | 16 | import { isFunction, isNumber } from '/@/utils/is'; |
16 | -import { buildUUID } from '/@/utils/uuid'; | |
17 | 17 | import { deepMerge } from '/@/utils'; |
18 | -import { useI18n } from '/@/hooks/web/useI18n'; | |
18 | +import { tryTsxEmit } from '/@/utils/helper/vueHelper'; | |
19 | + | |
20 | +import { basicProps } from './props'; | |
19 | 21 | |
20 | 22 | const prefixCls = 'basic-drawer'; |
21 | 23 | export default defineComponent({ |
22 | - // inheritAttrs: false, | |
24 | + inheritAttrs: false, | |
23 | 25 | props: basicProps, |
24 | 26 | emits: ['visible-change', 'ok', 'close', 'register'], |
25 | 27 | setup(props, { slots, emit, attrs }) { |
26 | 28 | const scrollRef = ref<ElRef>(null); |
27 | - | |
28 | 29 | const visibleRef = ref(false); |
29 | - const propsRef = ref<Partial<DrawerProps> | null>(null); | |
30 | + const propsRef = ref<Partial<Nullable<DrawerProps>>>(null); | |
30 | 31 | |
31 | 32 | const { t } = useI18n('component.drawer'); |
32 | 33 | |
33 | - const getMergeProps = computed((): any => { | |
34 | - return deepMerge(toRaw(props), unref(propsRef)); | |
35 | - }); | |
36 | - | |
37 | - const getProps = computed(() => { | |
38 | - const opt: any = { | |
39 | - placement: 'right', | |
40 | - ...attrs, | |
41 | - ...props, | |
42 | - ...(unref(propsRef) as any), | |
43 | - visible: unref(visibleRef), | |
44 | - }; | |
45 | - opt.title = undefined; | |
34 | + const getMergeProps = computed( | |
35 | + (): DrawerProps => { | |
36 | + return deepMerge(toRaw(props), unref(propsRef)); | |
37 | + } | |
38 | + ); | |
46 | 39 | |
47 | - if (opt.isDetail) { | |
48 | - if (!opt.width) { | |
49 | - opt.width = '100%'; | |
50 | - } | |
51 | - opt.wrapClassName = opt.wrapClassName | |
52 | - ? `${opt.wrapClassName} ${prefixCls}__detail` | |
53 | - : `${prefixCls}__detail`; | |
54 | - if (!opt.getContainer) { | |
55 | - opt.getContainer = '.layout-content'; | |
40 | + const getProps = computed( | |
41 | + (): DrawerProps => { | |
42 | + const opt = { | |
43 | + placement: 'right', | |
44 | + ...attrs, | |
45 | + ...unref(getMergeProps), | |
46 | + visible: unref(visibleRef), | |
47 | + }; | |
48 | + opt.title = undefined; | |
49 | + const { isDetail, width, wrapClassName, getContainer } = opt; | |
50 | + if (isDetail) { | |
51 | + if (!width) { | |
52 | + opt.width = '100%'; | |
53 | + } | |
54 | + const detailCls = `${prefixCls}__detail`; | |
55 | + | |
56 | + opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls; | |
57 | + | |
58 | + if (!getContainer) { | |
59 | + // TODO type error? | |
60 | + opt.getContainer = '.layout-content' as any; | |
61 | + } | |
56 | 62 | } |
63 | + return opt as DrawerProps; | |
57 | 64 | } |
58 | - return opt; | |
65 | + ); | |
66 | + | |
67 | + const getBindValues = computed( | |
68 | + (): DrawerProps => { | |
69 | + return { | |
70 | + ...attrs, | |
71 | + ...unref(getProps), | |
72 | + }; | |
73 | + } | |
74 | + ); | |
75 | + | |
76 | + // Custom implementation of the bottom button, | |
77 | + const getFooterHeight = computed(() => { | |
78 | + const { footerHeight, showFooter } = unref(getProps); | |
79 | + | |
80 | + if (showFooter && footerHeight) { | |
81 | + return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`; | |
82 | + } | |
83 | + return `0px`; | |
84 | + }); | |
85 | + | |
86 | + const getScrollContentStyle = computed( | |
87 | + (): CSSProperties => { | |
88 | + const footerHeight = unref(getFooterHeight); | |
89 | + return { | |
90 | + position: 'relative', | |
91 | + height: `calc(100% - ${footerHeight})`, | |
92 | + overflow: 'auto', | |
93 | + padding: '16px', | |
94 | + paddingBottom: '30px', | |
95 | + }; | |
96 | + } | |
97 | + ); | |
98 | + | |
99 | + const getLoading = computed(() => { | |
100 | + return { | |
101 | + hidden: !unref(getProps).loading, | |
102 | + }; | |
59 | 103 | }); |
60 | 104 | |
61 | 105 | watchEffect(() => { |
... | ... | @@ -74,22 +118,13 @@ export default defineComponent({ |
74 | 118 | } |
75 | 119 | ); |
76 | 120 | |
77 | - // Custom implementation of the bottom button, | |
78 | - const getFooterHeight = computed(() => { | |
79 | - const { footerHeight, showFooter }: DrawerProps = unref(getProps); | |
80 | - if (showFooter && footerHeight) { | |
81 | - return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`; | |
82 | - } | |
83 | - return `0px`; | |
84 | - }); | |
85 | - | |
86 | 121 | // Cancel event |
87 | - async function onClose(e: any) { | |
122 | + async function onClose(e: ChangeEvent) { | |
88 | 123 | const { closeFunc } = unref(getProps); |
89 | 124 | emit('close', e); |
90 | 125 | if (closeFunc && isFunction(closeFunc)) { |
91 | 126 | const res = await closeFunc(); |
92 | - res && (visibleRef.value = false); | |
127 | + visibleRef.value = !res; | |
93 | 128 | return; |
94 | 129 | } |
95 | 130 | visibleRef.value = false; |
... | ... | @@ -98,12 +133,16 @@ export default defineComponent({ |
98 | 133 | function setDrawerProps(props: Partial<DrawerProps>): void { |
99 | 134 | // Keep the last setDrawerProps |
100 | 135 | propsRef.value = deepMerge(unref(propsRef) || {}, props); |
136 | + | |
101 | 137 | if (Reflect.has(props, 'visible')) { |
102 | 138 | visibleRef.value = !!props.visible; |
103 | 139 | } |
104 | 140 | } |
105 | 141 | |
106 | 142 | function renderFooter() { |
143 | + if (slots?.footer) { | |
144 | + return getSlot(slots, 'footer'); | |
145 | + } | |
107 | 146 | const { |
108 | 147 | showCancelBtn, |
109 | 148 | cancelButtonProps, |
... | ... | @@ -114,65 +153,64 @@ export default defineComponent({ |
114 | 153 | okButtonProps, |
115 | 154 | confirmLoading, |
116 | 155 | showFooter, |
117 | - }: DrawerProps = unref(getProps); | |
156 | + } = unref(getProps); | |
157 | + if (!showFooter) { | |
158 | + return null; | |
159 | + } | |
118 | 160 | |
119 | 161 | return ( |
120 | - getSlot(slots, 'footer') || | |
121 | - (showFooter && ( | |
122 | - <div class={`${prefixCls}__footer`}> | |
123 | - {getSlot(slots, 'insertFooter')} | |
124 | - | |
125 | - {showCancelBtn && ( | |
126 | - <Button {...cancelButtonProps} onClick={onClose} class="mr-2"> | |
127 | - {() => cancelText} | |
128 | - </Button> | |
129 | - )} | |
130 | - {getSlot(slots, 'centerFooter')} | |
131 | - {showOkBtn && ( | |
132 | - <Button | |
133 | - type={okType} | |
134 | - onClick={() => { | |
135 | - emit('ok'); | |
136 | - }} | |
137 | - {...okButtonProps} | |
138 | - loading={confirmLoading} | |
139 | - > | |
140 | - {() => okText} | |
141 | - </Button> | |
142 | - )} | |
143 | - | |
144 | - {getSlot(slots, 'appendFooter')} | |
145 | - </div> | |
146 | - )) | |
162 | + <div class={`${prefixCls}__footer`}> | |
163 | + {getSlot(slots, 'insertFooter')} | |
164 | + {showCancelBtn && ( | |
165 | + <Button {...cancelButtonProps} onClick={onClose} class="mr-2"> | |
166 | + {() => cancelText} | |
167 | + </Button> | |
168 | + )} | |
169 | + {getSlot(slots, 'centerFooter')} | |
170 | + {showOkBtn && ( | |
171 | + <Button | |
172 | + type={okType} | |
173 | + onClick={() => { | |
174 | + emit('ok'); | |
175 | + }} | |
176 | + {...okButtonProps} | |
177 | + loading={confirmLoading} | |
178 | + > | |
179 | + {() => okText} | |
180 | + </Button> | |
181 | + )} | |
182 | + {getSlot(slots, 'appendFooter')} | |
183 | + </div> | |
147 | 184 | ); |
148 | 185 | } |
149 | 186 | |
150 | 187 | function renderHeader() { |
188 | + if (slots?.title) { | |
189 | + return getSlot(slots, 'title'); | |
190 | + } | |
151 | 191 | const { title } = unref(getMergeProps); |
152 | - return props.isDetail ? ( | |
153 | - getSlot(slots, 'title') || ( | |
154 | - <Row type="flex" align="middle" class={`${prefixCls}__detail-header`}> | |
155 | - {() => ( | |
156 | - <> | |
157 | - {props.showDetailBack && ( | |
158 | - <Button size="small" type="link" onClick={onClose}> | |
159 | - {() => <LeftOutlined />} | |
160 | - </Button> | |
161 | - )} | |
162 | - | |
163 | - {title && ( | |
164 | - <Col style="flex:1" class={[`${prefixCls}__detail-title`, 'ellipsis', 'px-2']}> | |
165 | - {() => title} | |
166 | - </Col> | |
167 | - )} | |
168 | - | |
169 | - {getSlot(slots, 'titleToolbar')} | |
170 | - </> | |
171 | - )} | |
172 | - </Row> | |
173 | - ) | |
174 | - ) : ( | |
175 | - <BasicTitle>{() => title || getSlot(slots, 'title')}</BasicTitle> | |
192 | + | |
193 | + if (!props.isDetail) { | |
194 | + return <BasicTitle>{() => title || getSlot(slots, 'title')}</BasicTitle>; | |
195 | + } | |
196 | + return ( | |
197 | + <Row type="flex" align="middle" class={`${prefixCls}__detail-header`}> | |
198 | + {() => ( | |
199 | + <> | |
200 | + {props.showDetailBack && ( | |
201 | + <Button size="small" type="link" onClick={onClose}> | |
202 | + {() => <LeftOutlined />} | |
203 | + </Button> | |
204 | + )} | |
205 | + {title && ( | |
206 | + <Col style="flex:1" class={[`${prefixCls}__detail-title`, 'ellipsis', 'px-2']}> | |
207 | + {() => title} | |
208 | + </Col> | |
209 | + )} | |
210 | + {getSlot(slots, 'titleToolbar')} | |
211 | + </> | |
212 | + )} | |
213 | + </Row> | |
176 | 214 | ); |
177 | 215 | } |
178 | 216 | |
... | ... | @@ -180,41 +218,20 @@ export default defineComponent({ |
180 | 218 | setDrawerProps: setDrawerProps, |
181 | 219 | }; |
182 | 220 | |
183 | - const uuid = buildUUID(); | |
184 | - emit('register', drawerInstance, uuid); | |
221 | + tryTsxEmit((instance) => { | |
222 | + emit('register', drawerInstance, instance.uid); | |
223 | + }); | |
185 | 224 | |
186 | 225 | return () => { |
187 | - const footerHeight = unref(getFooterHeight); | |
188 | 226 | return ( |
189 | - <Drawer | |
190 | - class={prefixCls} | |
191 | - onClose={onClose} | |
192 | - {...{ | |
193 | - ...attrs, | |
194 | - ...unref(getProps), | |
195 | - }} | |
196 | - > | |
227 | + <Drawer class={prefixCls} onClose={onClose} {...unref(getBindValues)}> | |
197 | 228 | {{ |
198 | 229 | title: () => renderHeader(), |
199 | 230 | default: () => ( |
200 | 231 | <> |
201 | - <div | |
202 | - ref={scrollRef} | |
203 | - {...attrs} | |
204 | - style={{ | |
205 | - position: 'relative', | |
206 | - height: `calc(100% - ${footerHeight})`, | |
207 | - overflow: 'auto', | |
208 | - padding: '16px', | |
209 | - paddingBottom: '30px', | |
210 | - }} | |
211 | - > | |
212 | - <FullLoading | |
213 | - absolute | |
214 | - tip={t('loadingText')} | |
215 | - class={[!unref(getProps).loading ? 'hidden' : '']} | |
216 | - /> | |
217 | - {getSlot(slots, 'default')} | |
232 | + <div ref={scrollRef} style={unref(getScrollContentStyle)}> | |
233 | + <FullLoading absolute tip={t('loadingText')} class={unref(getLoading)} /> | |
234 | + {getSlot(slots)} | |
218 | 235 | </div> |
219 | 236 | {renderFooter()} |
220 | 237 | </> | ... | ... |
src/components/Drawer/src/props.ts
1 | 1 | import type { PropType } from 'vue'; |
2 | 2 | |
3 | 3 | import { useI18n } from '/@/hooks/web/useI18n'; |
4 | +import { propTypes } from '/@/utils/propTypes'; | |
4 | 5 | const { t } = useI18n('component.drawer'); |
5 | 6 | |
6 | 7 | export const footerProps = { |
7 | - confirmLoading: Boolean as PropType<boolean>, | |
8 | + confirmLoading: propTypes.bool, | |
8 | 9 | /** |
9 | 10 | * @description: Show close button |
10 | 11 | */ |
11 | - showCancelBtn: { | |
12 | - type: Boolean as PropType<boolean>, | |
13 | - default: true, | |
14 | - }, | |
12 | + showCancelBtn: propTypes.bool.def(true), | |
15 | 13 | cancelButtonProps: Object as PropType<any>, |
16 | - cancelText: { | |
17 | - type: String as PropType<string>, | |
18 | - default: t('cancelText'), | |
19 | - }, | |
14 | + cancelText: propTypes.string.def(t('cancelText')), | |
20 | 15 | /** |
21 | 16 | * @description: Show confirmation button |
22 | 17 | */ |
23 | - showOkBtn: { | |
24 | - type: Boolean as PropType<boolean>, | |
25 | - default: true, | |
26 | - }, | |
27 | - okButtonProps: Object as PropType<any>, | |
28 | - okText: { | |
29 | - type: String as PropType<string>, | |
30 | - default: t('okText'), | |
31 | - }, | |
32 | - okType: { | |
33 | - type: String as PropType<string>, | |
34 | - default: 'primary', | |
35 | - }, | |
36 | - showFooter: { | |
37 | - type: Boolean as PropType<boolean>, | |
38 | - default: false, | |
39 | - }, | |
18 | + showOkBtn: propTypes.bool.def(true), | |
19 | + okButtonProps: propTypes.any, | |
20 | + okText: propTypes.string.def(t('okText')), | |
21 | + okType: propTypes.string.def('primary'), | |
22 | + showFooter: propTypes.bool, | |
40 | 23 | footerHeight: { |
41 | 24 | type: [String, Number] as PropType<string | number>, |
42 | 25 | default: 60, |
43 | 26 | }, |
44 | 27 | }; |
45 | 28 | export const basicProps = { |
46 | - isDetail: { | |
47 | - type: Boolean as PropType<boolean>, | |
48 | - default: false, | |
49 | - }, | |
50 | - title: { | |
51 | - type: String as PropType<string>, | |
52 | - default: '', | |
53 | - }, | |
54 | - showDetailBack: { | |
55 | - type: Boolean as PropType<boolean>, | |
56 | - default: true, | |
57 | - }, | |
58 | - visible: { | |
59 | - type: Boolean as PropType<boolean>, | |
60 | - default: false, | |
61 | - }, | |
62 | - loading: { | |
63 | - type: Boolean as PropType<boolean>, | |
64 | - default: false, | |
65 | - }, | |
66 | - maskClosable: { | |
67 | - type: Boolean as PropType<boolean>, | |
68 | - default: true, | |
69 | - }, | |
29 | + isDetail: propTypes.bool, | |
30 | + title: propTypes.string.def(''), | |
31 | + showDetailBack: propTypes.bool.def(true), | |
32 | + visible: propTypes.bool, | |
33 | + loading: propTypes.bool, | |
34 | + maskClosable: propTypes.bool.def(true), | |
70 | 35 | getContainer: { |
71 | 36 | type: [Object, String] as PropType<any>, |
72 | 37 | }, |
... | ... | @@ -78,10 +43,7 @@ export const basicProps = { |
78 | 43 | type: [Function, Object] as PropType<any>, |
79 | 44 | default: null, |
80 | 45 | }, |
81 | - triggerWindowResize: { | |
82 | - type: Boolean as PropType<boolean>, | |
83 | - default: false, | |
84 | - }, | |
85 | - destroyOnClose: Boolean as PropType<boolean>, | |
46 | + triggerWindowResize: propTypes.bool, | |
47 | + destroyOnClose: propTypes.bool, | |
86 | 48 | ...footerProps, |
87 | 49 | }; | ... | ... |
src/components/Drawer/src/types.ts
... | ... | @@ -75,7 +75,7 @@ export interface DrawerProps extends DrawerFooterProps { |
75 | 75 | * @type ScrollContainerOptions |
76 | 76 | */ |
77 | 77 | scrollOptions?: ScrollContainerOptions; |
78 | - closeFunc?: () => Promise<void>; | |
78 | + closeFunc?: () => Promise<any>; | |
79 | 79 | triggerWindowResize?: boolean; |
80 | 80 | /** |
81 | 81 | * Whether a close (x) button is visible on top right of the Drawer dialog or not. | ... | ... |
src/components/Drawer/src/useDrawer.ts
... | ... | @@ -6,12 +6,15 @@ import type { |
6 | 6 | UseDrawerInnerReturnType, |
7 | 7 | } from './types'; |
8 | 8 | |
9 | -import { ref, getCurrentInstance, onUnmounted, unref, reactive, watchEffect, nextTick } from 'vue'; | |
9 | +import { ref, getCurrentInstance, unref, reactive, watchEffect, nextTick, toRaw } from 'vue'; | |
10 | 10 | |
11 | 11 | import { isProdMode } from '/@/utils/env'; |
12 | 12 | import { isFunction } from '/@/utils/is'; |
13 | +import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; | |
14 | +import { isEqual } from 'lodash-es'; | |
13 | 15 | |
14 | 16 | const dataTransferRef = reactive<any>({}); |
17 | + | |
15 | 18 | /** |
16 | 19 | * @description: Applicable to separate drawer and call outside |
17 | 20 | */ |
... | ... | @@ -19,21 +22,23 @@ export function useDrawer(): UseDrawerReturnType { |
19 | 22 | if (!getCurrentInstance()) { |
20 | 23 | throw new Error('Please put useDrawer function in the setup function!'); |
21 | 24 | } |
25 | + | |
22 | 26 | const drawerRef = ref<DrawerInstance | null>(null); |
23 | - const loadedRef = ref<boolean | null>(false); | |
27 | + const loadedRef = ref<Nullable<boolean>>(false); | |
24 | 28 | const uidRef = ref<string>(''); |
25 | 29 | |
26 | - function getDrawer(drawerInstance: DrawerInstance, uuid: string) { | |
27 | - uidRef.value = uuid; | |
30 | + function register(drawerInstance: DrawerInstance, uuid: string) { | |
28 | 31 | isProdMode() && |
29 | - onUnmounted(() => { | |
32 | + tryOnUnmounted(() => { | |
30 | 33 | drawerRef.value = null; |
31 | 34 | loadedRef.value = null; |
32 | 35 | dataTransferRef[unref(uidRef)] = null; |
33 | 36 | }); |
37 | + | |
34 | 38 | if (unref(loadedRef) && isProdMode() && drawerInstance === unref(drawerRef)) { |
35 | 39 | return; |
36 | 40 | } |
41 | + uidRef.value = uuid; | |
37 | 42 | drawerRef.value = drawerInstance; |
38 | 43 | loadedRef.value = true; |
39 | 44 | } |
... | ... | @@ -55,37 +60,46 @@ export function useDrawer(): UseDrawerReturnType { |
55 | 60 | getInstance().setDrawerProps({ |
56 | 61 | visible: visible, |
57 | 62 | }); |
58 | - if (data) { | |
59 | - dataTransferRef[unref(uidRef)] = openOnSet | |
60 | - ? { | |
61 | - ...data, | |
62 | - __t__: Date.now(), | |
63 | - } | |
64 | - : data; | |
63 | + if (!data) return; | |
64 | + | |
65 | + if (openOnSet) { | |
66 | + dataTransferRef[unref(uidRef)] = null; | |
67 | + dataTransferRef[unref(uidRef)] = data; | |
68 | + return; | |
69 | + } | |
70 | + const equal = isEqual(toRaw(dataTransferRef[unref(uidRef)]), data); | |
71 | + if (!equal) { | |
72 | + dataTransferRef[unref(uidRef)] = data; | |
65 | 73 | } |
66 | 74 | }, |
67 | 75 | }; |
68 | 76 | |
69 | - return [getDrawer, methods]; | |
77 | + return [register, methods]; | |
70 | 78 | } |
79 | + | |
71 | 80 | export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => { |
72 | - const drawerInstanceRef = ref<DrawerInstance | null>(null); | |
81 | + const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null); | |
73 | 82 | const currentInstall = getCurrentInstance(); |
74 | 83 | const uidRef = ref<string>(''); |
75 | 84 | |
76 | 85 | if (!currentInstall) { |
77 | - throw new Error('instance is undefined!'); | |
86 | + throw new Error('useDrawerInner instance is undefined!'); | |
78 | 87 | } |
79 | 88 | |
80 | 89 | const getInstance = () => { |
81 | 90 | const instance = unref(drawerInstanceRef); |
82 | 91 | if (!instance) { |
83 | - throw new Error('instance is undefined!'); | |
92 | + throw new Error('useDrawerInner instance is undefined!'); | |
84 | 93 | } |
85 | 94 | return instance; |
86 | 95 | }; |
87 | 96 | |
88 | 97 | const register = (modalInstance: DrawerInstance, uuid: string) => { |
98 | + isProdMode() && | |
99 | + tryOnUnmounted(() => { | |
100 | + drawerInstanceRef.value = null; | |
101 | + }); | |
102 | + | |
89 | 103 | uidRef.value = uuid; |
90 | 104 | drawerInstanceRef.value = modalInstance; |
91 | 105 | currentInstall.emit('register', modalInstance); | ... | ... |
src/components/Modal/index.ts
1 | 1 | import './src/index.less'; |
2 | -export { default as BasicModal } from './src/BasicModal'; | |
3 | -export { default as Modal } from './src/Modal'; | |
2 | +import BasicModalLib from './src/BasicModal'; | |
3 | +import { withInstall } from '../util'; | |
4 | + | |
5 | +export { useModalContext } from './src/useModalContext'; | |
4 | 6 | export { useModal, useModalInner } from './src/useModal'; |
5 | 7 | export * from './src/types'; |
8 | +export const BasicModal = withInstall(BasicModalLib); | ... | ... |
src/components/Modal/src/BasicModal.tsx
1 | 1 | import type { ModalProps, ModalMethods } from './types'; |
2 | 2 | |
3 | -import { defineComponent, computed, ref, watch, unref, watchEffect } from 'vue'; | |
3 | +import { defineComponent, computed, ref, watch, unref, watchEffect, toRef } from 'vue'; | |
4 | 4 | |
5 | 5 | import Modal from './Modal'; |
6 | 6 | import { Button } from '/@/components/Button'; |
... | ... | @@ -11,10 +11,10 @@ import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant- |
11 | 11 | import { getSlot, extendSlots } from '/@/utils/helper/tsxHelper'; |
12 | 12 | import { isFunction } from '/@/utils/is'; |
13 | 13 | import { deepMerge } from '/@/utils'; |
14 | -import { buildUUID } from '/@/utils/uuid'; | |
14 | +import { tryTsxEmit } from '/@/utils/helper/vueHelper'; | |
15 | 15 | |
16 | 16 | import { basicProps } from './props'; |
17 | -// import { triggerWindowResize } from '@/utils/event/triggerWindowResizeEvent'; | |
17 | +import { useFullScreen } from './useFullScreen'; | |
18 | 18 | export default defineComponent({ |
19 | 19 | name: 'BasicModal', |
20 | 20 | props: basicProps, |
... | ... | @@ -26,31 +26,41 @@ export default defineComponent({ |
26 | 26 | // modal Bottom and top height |
27 | 27 | const extHeightRef = ref(0); |
28 | 28 | // Unexpanded height of the popup |
29 | - const formerHeightRef = ref(0); | |
30 | - const fullScreenRef = ref(false); | |
31 | 29 | |
32 | 30 | // Custom title component: get title |
33 | - const getMergeProps = computed(() => { | |
34 | - return { | |
35 | - ...props, | |
36 | - ...(unref(propsRef) as any), | |
37 | - }; | |
31 | + const getMergeProps = computed( | |
32 | + (): ModalProps => { | |
33 | + return { | |
34 | + ...props, | |
35 | + ...(unref(propsRef) as any), | |
36 | + }; | |
37 | + } | |
38 | + ); | |
39 | + | |
40 | + const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({ | |
41 | + modalWrapperRef, | |
42 | + extHeightRef, | |
43 | + wrapClassName: toRef(getMergeProps.value, 'wrapClassName'), | |
38 | 44 | }); |
39 | 45 | |
40 | 46 | // modal component does not need title |
41 | - const getProps = computed((): any => { | |
42 | - const opt = { | |
43 | - ...props, | |
44 | - ...((unref(propsRef) || {}) as any), | |
45 | - visible: unref(visibleRef), | |
46 | - title: undefined, | |
47 | - }; | |
48 | - const { wrapClassName = '' } = opt; | |
49 | - const className = unref(fullScreenRef) ? `${wrapClassName} fullscreen-modal` : wrapClassName; | |
50 | - return { | |
51 | - ...opt, | |
52 | - wrapClassName: className, | |
53 | - }; | |
47 | + const getProps = computed( | |
48 | + (): ModalProps => { | |
49 | + const opt = { | |
50 | + ...unref(getMergeProps), | |
51 | + visible: unref(visibleRef), | |
52 | + title: undefined, | |
53 | + }; | |
54 | + | |
55 | + return { | |
56 | + ...opt, | |
57 | + wrapClassName: unref(getWrapClassName), | |
58 | + }; | |
59 | + } | |
60 | + ); | |
61 | + | |
62 | + const getModalBindValue = computed((): any => { | |
63 | + return { ...attrs, ...unref(getProps) }; | |
54 | 64 | }); |
55 | 65 | |
56 | 66 | watchEffect(() => { |
... | ... | @@ -80,7 +90,35 @@ export default defineComponent({ |
80 | 90 | ); |
81 | 91 | } |
82 | 92 | |
93 | + // 取消事件 | |
94 | + async function handleCancel(e: Event) { | |
95 | + e?.stopPropagation(); | |
96 | + | |
97 | + if (props.closeFunc && isFunction(props.closeFunc)) { | |
98 | + const isClose: boolean = await props.closeFunc(); | |
99 | + visibleRef.value = !isClose; | |
100 | + return; | |
101 | + } | |
102 | + | |
103 | + visibleRef.value = false; | |
104 | + emit('cancel'); | |
105 | + } | |
106 | + | |
107 | + /** | |
108 | + * @description: 设置modal参数 | |
109 | + */ | |
110 | + function setModalProps(props: Partial<ModalProps>): void { | |
111 | + // Keep the last setModalProps | |
112 | + propsRef.value = deepMerge(unref(propsRef) || {}, props); | |
113 | + if (!Reflect.has(props, 'visible')) return; | |
114 | + visibleRef.value = !!props.visible; | |
115 | + } | |
116 | + | |
83 | 117 | function renderContent() { |
118 | + type OmitWrapperType = Omit< | |
119 | + ModalProps, | |
120 | + 'fullScreen' | 'modalFooterHeight' | 'visible' | 'loading' | |
121 | + >; | |
84 | 122 | const { useWrapper, loading, wrapperProps } = unref(getProps); |
85 | 123 | if (!useWrapper) return getSlot(slots); |
86 | 124 | |
... | ... | @@ -93,7 +131,7 @@ export default defineComponent({ |
93 | 131 | loading={loading} |
94 | 132 | visible={unref(visibleRef)} |
95 | 133 | modalFooterHeight={showFooter} |
96 | - {...wrapperProps} | |
134 | + {...((wrapperProps as unknown) as OmitWrapperType)} | |
97 | 135 | onGetExtHeight={(height: number) => { |
98 | 136 | extHeightRef.value = height; |
99 | 137 | }} |
... | ... | @@ -106,18 +144,6 @@ export default defineComponent({ |
106 | 144 | ); |
107 | 145 | } |
108 | 146 | |
109 | - // 取消事件 | |
110 | - async function handleCancel(e: Event) { | |
111 | - e && e.stopPropagation(); | |
112 | - if (props.closeFunc && isFunction(props.closeFunc)) { | |
113 | - const isClose: boolean = await props.closeFunc(); | |
114 | - visibleRef.value = !isClose; | |
115 | - return; | |
116 | - } | |
117 | - visibleRef.value = false; | |
118 | - emit('cancel'); | |
119 | - } | |
120 | - | |
121 | 147 | // 底部按钮自定义实现, |
122 | 148 | function renderFooter() { |
123 | 149 | const { |
... | ... | @@ -162,64 +188,37 @@ export default defineComponent({ |
162 | 188 | */ |
163 | 189 | function renderClose() { |
164 | 190 | const { canFullscreen } = unref(getProps); |
165 | - if (!canFullscreen) { | |
166 | - return null; | |
167 | - } | |
191 | + | |
192 | + const fullScreen = unref(fullScreenRef) ? ( | |
193 | + <FullscreenExitOutlined role="full" onClick={handleFullScreen} /> | |
194 | + ) : ( | |
195 | + <FullscreenOutlined role="close" onClick={handleFullScreen} /> | |
196 | + ); | |
197 | + | |
198 | + const cls = [ | |
199 | + 'custom-close-icon', | |
200 | + { | |
201 | + 'can-full': canFullscreen, | |
202 | + }, | |
203 | + ]; | |
204 | + | |
168 | 205 | return ( |
169 | - <div class="custom-close-icon"> | |
170 | - {unref(fullScreenRef) ? ( | |
171 | - <FullscreenExitOutlined role="full" onClick={handleFullScreen} /> | |
172 | - ) : ( | |
173 | - <FullscreenOutlined role="close" onClick={handleFullScreen} /> | |
174 | - )} | |
206 | + <div class={cls}> | |
207 | + {canFullscreen && fullScreen} | |
175 | 208 | <CloseOutlined onClick={handleCancel} /> |
176 | 209 | </div> |
177 | 210 | ); |
178 | 211 | } |
179 | 212 | |
180 | - function handleFullScreen(e: Event) { | |
181 | - e && e.stopPropagation(); | |
182 | - fullScreenRef.value = !unref(fullScreenRef); | |
183 | - | |
184 | - const modalWrapper = unref(modalWrapperRef); | |
185 | - if (!modalWrapper) return; | |
186 | - | |
187 | - const wrapperEl = modalWrapper.$el as HTMLElement; | |
188 | - if (!wrapperEl) return; | |
189 | - | |
190 | - const modalWrapSpinEl = wrapperEl.querySelector('.ant-spin-nested-loading') as HTMLElement; | |
191 | - if (!modalWrapSpinEl) return; | |
192 | - | |
193 | - if (!unref(formerHeightRef) && unref(fullScreenRef)) { | |
194 | - formerHeightRef.value = modalWrapSpinEl.offsetHeight; | |
195 | - } | |
196 | - | |
197 | - if (unref(fullScreenRef)) { | |
198 | - modalWrapSpinEl.style.height = `${window.innerHeight - unref(extHeightRef)}px`; | |
199 | - } else { | |
200 | - modalWrapSpinEl.style.height = `${unref(formerHeightRef)}px`; | |
201 | - } | |
202 | - } | |
203 | - | |
204 | - /** | |
205 | - * @description: 设置modal参数 | |
206 | - */ | |
207 | - function setModalProps(props: Partial<ModalProps>): void { | |
208 | - // Keep the last setModalProps | |
209 | - propsRef.value = deepMerge(unref(propsRef) || {}, props); | |
210 | - if (!Reflect.has(props, 'visible')) return; | |
211 | - visibleRef.value = !!props.visible; | |
212 | - } | |
213 | - | |
214 | 213 | const modalMethods: ModalMethods = { |
215 | 214 | setModalProps, |
216 | 215 | }; |
217 | 216 | |
218 | - const uuid = buildUUID(); | |
219 | - emit('register', modalMethods, uuid); | |
220 | - | |
217 | + tryTsxEmit((instance) => { | |
218 | + emit('register', modalMethods, instance.uid); | |
219 | + }); | |
221 | 220 | return () => ( |
222 | - <Modal onCancel={handleCancel} {...{ ...attrs, ...props, ...unref(getProps) }}> | |
221 | + <Modal onCancel={handleCancel} {...unref(getModalBindValue)}> | |
223 | 222 | {{ |
224 | 223 | footer: () => renderFooter(), |
225 | 224 | closeIcon: () => renderClose(), | ... | ... |
src/components/Modal/src/Modal.tsx
1 | 1 | import { Modal } from 'ant-design-vue'; |
2 | -import { defineComponent, watchEffect } from 'vue'; | |
2 | +import { defineComponent, toRefs } from 'vue'; | |
3 | 3 | import { basicProps } from './props'; |
4 | -import { useTimeoutFn } from '/@/hooks/core/useTimeout'; | |
4 | +import { useModalDragMove } from './useModalDrag'; | |
5 | 5 | import { extendSlots } from '/@/utils/helper/tsxHelper'; |
6 | 6 | |
7 | 7 | export default defineComponent({ |
... | ... | @@ -9,99 +9,12 @@ export default defineComponent({ |
9 | 9 | inheritAttrs: false, |
10 | 10 | props: basicProps, |
11 | 11 | setup(props, { attrs, slots }) { |
12 | - const getStyle = (dom: any, attr: any) => { | |
13 | - return getComputedStyle(dom)[attr]; | |
14 | - }; | |
15 | - const drag = (wrap: any) => { | |
16 | - if (!wrap) return; | |
17 | - wrap.setAttribute('data-drag', props.draggable); | |
18 | - const dialogHeaderEl = wrap.querySelector('.ant-modal-header'); | |
19 | - const dragDom = wrap.querySelector('.ant-modal'); | |
20 | - | |
21 | - if (!dialogHeaderEl || !dragDom || !props.draggable) return; | |
22 | - | |
23 | - dialogHeaderEl.style.cursor = 'move'; | |
24 | - | |
25 | - dialogHeaderEl.onmousedown = (e: any) => { | |
26 | - if (!e) return; | |
27 | - // 鼠标按下,计算当前元素距离可视区的距离 | |
28 | - const disX = e.clientX; | |
29 | - const disY = e.clientY; | |
30 | - const screenWidth = document.body.clientWidth; // body当前宽度 | |
31 | - const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取) | |
32 | - | |
33 | - const dragDomWidth = dragDom.offsetWidth; // 对话框宽度 | |
34 | - const dragDomheight = dragDom.offsetHeight; // 对话框高度 | |
35 | - | |
36 | - const minDragDomLeft = dragDom.offsetLeft; | |
37 | - | |
38 | - const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth; | |
39 | - const minDragDomTop = dragDom.offsetTop; | |
40 | - const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight; | |
41 | - // 获取到的值带px 正则匹配替换 | |
42 | - const domLeft = getStyle(dragDom, 'left'); | |
43 | - const domTop = getStyle(dragDom, 'top'); | |
44 | - let styL = +domLeft; | |
45 | - let styT = +domTop; | |
46 | - | |
47 | - // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px | |
48 | - if (domLeft.includes('%')) { | |
49 | - styL = +document.body.clientWidth * (+domLeft.replace(/%/g, '') / 100); | |
50 | - styT = +document.body.clientHeight * (+domTop.replace(/%/g, '') / 100); | |
51 | - } else { | |
52 | - styL = +domLeft.replace(/px/g, ''); | |
53 | - styT = +domTop.replace(/px/g, ''); | |
54 | - } | |
55 | - | |
56 | - document.onmousemove = function (e) { | |
57 | - // 通过事件委托,计算移动的距离 | |
58 | - let left = e.clientX - disX; | |
59 | - let top = e.clientY - disY; | |
60 | - | |
61 | - // 边界处理 | |
62 | - if (-left > minDragDomLeft) { | |
63 | - left = -minDragDomLeft; | |
64 | - } else if (left > maxDragDomLeft) { | |
65 | - left = maxDragDomLeft; | |
66 | - } | |
67 | - | |
68 | - if (-top > minDragDomTop) { | |
69 | - top = -minDragDomTop; | |
70 | - } else if (top > maxDragDomTop) { | |
71 | - top = maxDragDomTop; | |
72 | - } | |
73 | - | |
74 | - // 移动当前元素 | |
75 | - dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`; | |
76 | - }; | |
77 | - | |
78 | - document.onmouseup = () => { | |
79 | - document.onmousemove = null; | |
80 | - document.onmouseup = null; | |
81 | - }; | |
82 | - }; | |
83 | - }; | |
84 | - | |
85 | - const handleDrag = () => { | |
86 | - const dragWraps = document.querySelectorAll('.ant-modal-wrap'); | |
87 | - for (const wrap of dragWraps as any) { | |
88 | - if (!wrap) continue; | |
89 | - const display = getStyle(wrap, 'display'); | |
90 | - const draggable = wrap.getAttribute('data-drag'); | |
91 | - if (display !== 'none') { | |
92 | - // 拖拽位置 | |
93 | - (draggable === null || props.destroyOnClose) && drag(wrap); | |
94 | - } | |
95 | - } | |
96 | - }; | |
12 | + const { visible, draggable, destroyOnClose } = toRefs(props); | |
97 | 13 | |
98 | - watchEffect(() => { | |
99 | - if (!props.visible) { | |
100 | - return; | |
101 | - } | |
102 | - useTimeoutFn(() => { | |
103 | - handleDrag(); | |
104 | - }, 30); | |
14 | + useModalDragMove({ | |
15 | + visible, | |
16 | + destroyOnClose, | |
17 | + draggable, | |
105 | 18 | }); |
106 | 19 | |
107 | 20 | return () => { | ... | ... |
src/components/Modal/src/ModalWrapper.tsx
1 | -import type { PropType } from 'vue'; | |
2 | 1 | import type { ModalWrapperProps } from './types'; |
2 | +import type { CSSProperties } from 'vue'; | |
3 | 3 | |
4 | 4 | import { |
5 | 5 | defineComponent, |
... | ... | @@ -18,59 +18,44 @@ import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; |
18 | 18 | |
19 | 19 | import { getSlot } from '/@/utils/helper/tsxHelper'; |
20 | 20 | import { useElResize } from '/@/hooks/event/useElResize'; |
21 | -import { provideModal } from './provideModal'; | |
21 | +import { propTypes } from '/@/utils/propTypes'; | |
22 | +import { createModalContext } from './useModalContext'; | |
22 | 23 | |
23 | 24 | export default defineComponent({ |
24 | 25 | name: 'ModalWrapper', |
25 | 26 | props: { |
26 | - loading: { | |
27 | - type: Boolean as PropType<boolean>, | |
28 | - default: false, | |
29 | - }, | |
30 | - modalHeaderHeight: { | |
31 | - type: Number as PropType<number>, | |
32 | - default: 50, | |
33 | - }, | |
34 | - modalFooterHeight: { | |
35 | - type: Number as PropType<number>, | |
36 | - default: 70, | |
37 | - }, | |
38 | - minHeight: { | |
39 | - type: Number as PropType<number>, | |
40 | - default: 200, | |
41 | - }, | |
42 | - footerOffset: { | |
43 | - type: Number as PropType<number>, | |
44 | - default: 0, | |
45 | - }, | |
46 | - visible: { | |
47 | - type: Boolean as PropType<boolean>, | |
48 | - default: false, | |
49 | - }, | |
50 | - fullScreen: { | |
51 | - type: Boolean as PropType<boolean>, | |
52 | - default: false, | |
53 | - }, | |
27 | + loading: propTypes.bool, | |
28 | + modalHeaderHeight: propTypes.number.def(50), | |
29 | + modalFooterHeight: propTypes.number.def(54), | |
30 | + minHeight: propTypes.number.def(200), | |
31 | + footerOffset: propTypes.number.def(0), | |
32 | + visible: propTypes.bool, | |
33 | + fullScreen: propTypes.bool, | |
54 | 34 | }, |
55 | 35 | emits: ['heightChange', 'getExtHeight'], |
56 | 36 | setup(props: ModalWrapperProps, { slots, emit }) { |
57 | - const wrapperRef = ref<HTMLElement | null>(null); | |
37 | + const wrapperRef = ref<ElRef>(null); | |
58 | 38 | const spinRef = ref<ComponentRef>(null); |
59 | 39 | const realHeightRef = ref(0); |
60 | - // 重试次数 | |
61 | - // let tryCount = 0; | |
40 | + | |
62 | 41 | let stopElResizeFn: Fn = () => {}; |
63 | 42 | |
64 | - provideModal(setModalHeight); | |
43 | + useWindowSizeFn(setModalHeight); | |
65 | 44 | |
66 | - const wrapStyle = computed(() => { | |
67 | - return { | |
68 | - minHeight: `${props.minHeight}px`, | |
69 | - height: `${unref(realHeightRef)}px`, | |
70 | - overflow: 'auto', | |
71 | - }; | |
45 | + createModalContext({ | |
46 | + redoModalHeight: setModalHeight, | |
72 | 47 | }); |
73 | 48 | |
49 | + const wrapStyle = computed( | |
50 | + (): CSSProperties => { | |
51 | + return { | |
52 | + minHeight: `${props.minHeight}px`, | |
53 | + height: `${unref(realHeightRef)}px`, | |
54 | + overflow: 'auto', | |
55 | + }; | |
56 | + } | |
57 | + ); | |
58 | + | |
74 | 59 | watchEffect(() => { |
75 | 60 | setModalHeight(); |
76 | 61 | }); |
... | ... | @@ -92,8 +77,6 @@ export default defineComponent({ |
92 | 77 | stopElResizeFn && stopElResizeFn(); |
93 | 78 | }); |
94 | 79 | |
95 | - useWindowSizeFn(setModalHeight); | |
96 | - | |
97 | 80 | async function setModalHeight() { |
98 | 81 | // 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度 |
99 | 82 | // 加上这个,就必须在使用的时候传递父级的visible |
... | ... | @@ -107,9 +90,8 @@ export default defineComponent({ |
107 | 90 | |
108 | 91 | try { |
109 | 92 | const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement; |
110 | - if (!modalDom) { | |
111 | - return; | |
112 | - } | |
93 | + if (!modalDom) return; | |
94 | + | |
113 | 95 | const modalRect = getComputedStyle(modalDom).top; |
114 | 96 | const modalTop = Number.parseInt(modalRect); |
115 | 97 | let maxHeight = |
... | ... | @@ -135,11 +117,12 @@ export default defineComponent({ |
135 | 117 | |
136 | 118 | if (props.fullScreen) { |
137 | 119 | realHeightRef.value = |
138 | - window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight - 6; | |
120 | + window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight; | |
139 | 121 | } else { |
140 | 122 | realHeightRef.value = realHeight > maxHeight ? maxHeight : realHeight + 16 + 30; |
141 | 123 | } |
142 | 124 | emit('heightChange', unref(realHeightRef)); |
125 | + | |
143 | 126 | nextTick(() => { |
144 | 127 | const el = spinEl.$el; |
145 | 128 | if (el) { |
... | ... | @@ -154,8 +137,10 @@ export default defineComponent({ |
154 | 137 | function listenElResize() { |
155 | 138 | const wrapper = unref(wrapperRef); |
156 | 139 | if (!wrapper) return; |
140 | + | |
157 | 141 | const container = wrapper.querySelector('.ant-spin-container'); |
158 | 142 | if (!container) return; |
143 | + | |
159 | 144 | const [start, stop] = useElResize(container, () => { |
160 | 145 | setModalHeight(); |
161 | 146 | }); | ... | ... |
src/components/Modal/src/index.less
... | ... | @@ -9,6 +9,11 @@ |
9 | 9 | bottom: 0 !important; |
10 | 10 | left: 0 !important; |
11 | 11 | width: 100% !important; |
12 | + height: 100%; | |
13 | + | |
14 | + &-content { | |
15 | + height: 100%; | |
16 | + } | |
12 | 17 | } |
13 | 18 | } |
14 | 19 | |
... | ... | @@ -35,8 +40,23 @@ |
35 | 40 | height: 95%; |
36 | 41 | align-items: center; |
37 | 42 | |
38 | - > * { | |
39 | - margin-left: 12px; | |
43 | + > span { | |
44 | + margin-left: 48px; | |
45 | + font-size: 16px; | |
46 | + } | |
47 | + | |
48 | + &.can-full { | |
49 | + > span { | |
50 | + margin-left: 12px; | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + &:not(.can-full) { | |
55 | + > span:nth-child(1) { | |
56 | + &:hover { | |
57 | + font-weight: 700; | |
58 | + } | |
59 | + } | |
40 | 60 | } |
41 | 61 | |
42 | 62 | & span:nth-child(1) { |
... | ... | @@ -76,7 +96,7 @@ |
76 | 96 | } |
77 | 97 | |
78 | 98 | &-footer { |
79 | - padding: 10px 26px 26px 16px; | |
99 | + // padding: 10px 26px 26px 16px; | |
80 | 100 | |
81 | 101 | button + button { |
82 | 102 | margin-left: 10px; | ... | ... |
src/components/Modal/src/props.ts
... | ... | @@ -2,66 +2,38 @@ import type { PropType } from 'vue'; |
2 | 2 | import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; |
3 | 3 | |
4 | 4 | import { useI18n } from '/@/hooks/web/useI18n'; |
5 | +import { propTypes } from '/@/utils/propTypes'; | |
5 | 6 | const { t } = useI18n('component.modal'); |
6 | 7 | |
7 | 8 | export const modalProps = { |
8 | - visible: Boolean as PropType<boolean>, | |
9 | + visible: propTypes.bool, | |
9 | 10 | // open drag |
10 | - draggable: { | |
11 | - type: Boolean as PropType<boolean>, | |
12 | - default: true, | |
13 | - }, | |
14 | - centered: { | |
15 | - type: Boolean as PropType<boolean>, | |
16 | - default: false, | |
17 | - }, | |
18 | - cancelText: { | |
19 | - type: String as PropType<string>, | |
20 | - default: t('cancelText'), | |
21 | - }, | |
22 | - okText: { | |
23 | - type: String as PropType<string>, | |
24 | - default: t('okText'), | |
25 | - }, | |
11 | + draggable: propTypes.bool.def(true), | |
12 | + centered: propTypes.bool, | |
13 | + cancelText: propTypes.string.def(t('cancelText')), | |
14 | + okText: propTypes.string.def(t('okText')), | |
15 | + | |
26 | 16 | closeFunc: Function as PropType<() => Promise<boolean>>, |
27 | 17 | }; |
28 | 18 | |
29 | 19 | export const basicProps = Object.assign({}, modalProps, { |
30 | 20 | // Can it be full screen |
31 | - canFullscreen: { | |
32 | - type: Boolean as PropType<boolean>, | |
33 | - default: true, | |
34 | - }, | |
21 | + canFullscreen: propTypes.bool.def(true), | |
35 | 22 | // After enabling the wrapper, the bottom can be increased in height |
36 | - wrapperFooterOffset: { | |
37 | - type: Number as PropType<number>, | |
38 | - default: 0, | |
39 | - }, | |
23 | + wrapperFooterOffset: propTypes.number.def(0), | |
40 | 24 | // Warm reminder message |
41 | 25 | helpMessage: [String, Array] as PropType<string | string[]>, |
42 | 26 | // Whether to setting wrapper |
43 | - useWrapper: { | |
44 | - type: Boolean as PropType<boolean>, | |
45 | - default: true, | |
46 | - }, | |
47 | - loading: { | |
48 | - type: Boolean as PropType<boolean>, | |
49 | - default: false, | |
50 | - }, | |
27 | + useWrapper: propTypes.bool.def(true), | |
28 | + loading: propTypes.bool, | |
51 | 29 | /** |
52 | 30 | * @description: Show close button |
53 | 31 | */ |
54 | - showCancelBtn: { | |
55 | - type: Boolean as PropType<boolean>, | |
56 | - default: true, | |
57 | - }, | |
32 | + showCancelBtn: propTypes.bool.def(true), | |
58 | 33 | /** |
59 | 34 | * @description: Show confirmation button |
60 | 35 | */ |
61 | - showOkBtn: { | |
62 | - type: Boolean as PropType<boolean>, | |
63 | - default: true, | |
64 | - }, | |
36 | + showOkBtn: propTypes.bool.def(true), | |
65 | 37 | |
66 | 38 | wrapperProps: Object as PropType<any>, |
67 | 39 | ... | ... |
src/components/Modal/src/provideModal.ts deleted
100644 → 0
src/components/Modal/src/types.ts
... | ... | @@ -8,9 +8,11 @@ export interface ModalMethods { |
8 | 8 | } |
9 | 9 | |
10 | 10 | export type RegisterFn = (modalMethods: ModalMethods, uuid?: string) => void; |
11 | + | |
11 | 12 | export interface ReturnMethods extends ModalMethods { |
12 | 13 | openModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void; |
13 | 14 | } |
15 | + | |
14 | 16 | export type UseModalReturnType = [RegisterFn, ReturnMethods]; |
15 | 17 | |
16 | 18 | export interface ReturnInnerMethods extends ModalMethods { |
... | ... | @@ -18,6 +20,7 @@ export interface ReturnInnerMethods extends ModalMethods { |
18 | 20 | changeLoading: (loading: boolean) => void; |
19 | 21 | changeOkLoading: (loading: boolean) => void; |
20 | 22 | } |
23 | + | |
21 | 24 | export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods]; |
22 | 25 | |
23 | 26 | export interface ModalProps { | ... | ... |
src/components/Modal/src/useFullScreen.ts
0 → 100644
1 | +import { computed, Ref, ref, unref } from 'vue'; | |
2 | + | |
3 | +export interface UseFullScreenContext { | |
4 | + wrapClassName: Ref<string | undefined>; | |
5 | + modalWrapperRef: Ref<ComponentRef>; | |
6 | + extHeightRef: Ref<number>; | |
7 | +} | |
8 | + | |
9 | +export function useFullScreen(context: UseFullScreenContext) { | |
10 | + const formerHeightRef = ref(0); | |
11 | + const fullScreenRef = ref(false); | |
12 | + | |
13 | + const getWrapClassName = computed(() => { | |
14 | + const clsName = unref(context.wrapClassName) || ''; | |
15 | + | |
16 | + return unref(fullScreenRef) ? `fullscreen-modal ${clsName} ` : unref(clsName); | |
17 | + }); | |
18 | + | |
19 | + function handleFullScreen(e: Event) { | |
20 | + e && e.stopPropagation(); | |
21 | + fullScreenRef.value = !unref(fullScreenRef); | |
22 | + | |
23 | + const modalWrapper = unref(context.modalWrapperRef); | |
24 | + | |
25 | + if (!modalWrapper) return; | |
26 | + | |
27 | + const wrapperEl = modalWrapper.$el as HTMLElement; | |
28 | + if (!wrapperEl) return; | |
29 | + const modalWrapSpinEl = wrapperEl.querySelector('.ant-spin-nested-loading') as HTMLElement; | |
30 | + | |
31 | + if (!modalWrapSpinEl) return; | |
32 | + | |
33 | + if (!unref(formerHeightRef) && unref(fullScreenRef)) { | |
34 | + formerHeightRef.value = modalWrapSpinEl.offsetHeight; | |
35 | + } | |
36 | + | |
37 | + if (unref(fullScreenRef)) { | |
38 | + modalWrapSpinEl.style.height = `${window.innerHeight - unref(context.extHeightRef)}px`; | |
39 | + } else { | |
40 | + modalWrapSpinEl.style.height = `${unref(formerHeightRef)}px`; | |
41 | + } | |
42 | + } | |
43 | + return { getWrapClassName, handleFullScreen, fullScreenRef }; | |
44 | +} | ... | ... |
src/components/Modal/src/useModal.ts
... | ... | @@ -5,9 +5,21 @@ import type { |
5 | 5 | ReturnMethods, |
6 | 6 | UseModalInnerReturnType, |
7 | 7 | } from './types'; |
8 | -import { ref, onUnmounted, unref, getCurrentInstance, reactive, watchEffect, nextTick } from 'vue'; | |
8 | + | |
9 | +import { | |
10 | + ref, | |
11 | + onUnmounted, | |
12 | + unref, | |
13 | + getCurrentInstance, | |
14 | + reactive, | |
15 | + watchEffect, | |
16 | + nextTick, | |
17 | + toRaw, | |
18 | +} from 'vue'; | |
9 | 19 | import { isProdMode } from '/@/utils/env'; |
10 | 20 | import { isFunction } from '/@/utils/is'; |
21 | +import { isEqual } from 'lodash-es'; | |
22 | +import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; | |
11 | 23 | const dataTransferRef = reactive<any>({}); |
12 | 24 | |
13 | 25 | /** |
... | ... | @@ -20,6 +32,7 @@ export function useModal(): UseModalReturnType { |
20 | 32 | const modalRef = ref<Nullable<ModalMethods>>(null); |
21 | 33 | const loadedRef = ref<Nullable<boolean>>(false); |
22 | 34 | const uidRef = ref<string>(''); |
35 | + | |
23 | 36 | function register(modalMethod: ModalMethods, uuid: string) { |
24 | 37 | uidRef.value = uuid; |
25 | 38 | |
... | ... | @@ -52,13 +65,16 @@ export function useModal(): UseModalReturnType { |
52 | 65 | visible: visible, |
53 | 66 | }); |
54 | 67 | |
55 | - if (data) { | |
56 | - dataTransferRef[unref(uidRef)] = openOnSet | |
57 | - ? { | |
58 | - ...data, | |
59 | - __t__: Date.now(), | |
60 | - } | |
61 | - : data; | |
68 | + if (!data) return; | |
69 | + | |
70 | + if (openOnSet) { | |
71 | + dataTransferRef[unref(uidRef)] = null; | |
72 | + dataTransferRef[unref(uidRef)] = data; | |
73 | + return; | |
74 | + } | |
75 | + const equal = isEqual(toRaw(dataTransferRef[unref(uidRef)]), data); | |
76 | + if (!equal) { | |
77 | + dataTransferRef[unref(uidRef)] = data; | |
62 | 78 | } |
63 | 79 | }, |
64 | 80 | }; |
... | ... | @@ -66,7 +82,7 @@ export function useModal(): UseModalReturnType { |
66 | 82 | } |
67 | 83 | |
68 | 84 | export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => { |
69 | - const modalInstanceRef = ref<ModalMethods | null>(null); | |
85 | + const modalInstanceRef = ref<Nullable<ModalMethods>>(null); | |
70 | 86 | const currentInstall = getCurrentInstance(); |
71 | 87 | const uidRef = ref<string>(''); |
72 | 88 | |
... | ... | @@ -83,6 +99,11 @@ export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => { |
83 | 99 | }; |
84 | 100 | |
85 | 101 | const register = (modalInstance: ModalMethods, uuid: string) => { |
102 | + isProdMode() && | |
103 | + tryOnUnmounted(() => { | |
104 | + modalInstanceRef.value = null; | |
105 | + }); | |
106 | + | |
86 | 107 | uidRef.value = uuid; |
87 | 108 | modalInstanceRef.value = modalInstance; |
88 | 109 | currentInstall.emit('register', modalInstance); | ... | ... |
src/components/Modal/src/useModalContext.ts
0 → 100644
1 | +import { InjectionKey } from 'vue'; | |
2 | +import { createContext, useContext } from '/@/hooks/core/useContext'; | |
3 | + | |
4 | +export interface ModalContextProps { | |
5 | + redoModalHeight: () => void; | |
6 | +} | |
7 | + | |
8 | +const modalContextInjectKey: InjectionKey<ModalContextProps> = Symbol(); | |
9 | + | |
10 | +export function createModalContext(context: ModalContextProps) { | |
11 | + return createContext<ModalContextProps>(context, modalContextInjectKey); | |
12 | +} | |
13 | + | |
14 | +export function useModalContext() { | |
15 | + return useContext<ModalContextProps>(modalContextInjectKey); | |
16 | +} | ... | ... |
src/components/Modal/src/useModalDrag.ts
0 → 100644
1 | +import { Ref, unref, watchEffect } from 'vue'; | |
2 | +import { useTimeoutFn } from '/@/hooks/core/useTimeout'; | |
3 | + | |
4 | +export interface UseModalDragMoveContext { | |
5 | + draggable: Ref<boolean>; | |
6 | + destroyOnClose: Ref<boolean | undefined> | undefined; | |
7 | + visible: Ref<boolean>; | |
8 | +} | |
9 | + | |
10 | +export function useModalDragMove(context: UseModalDragMoveContext) { | |
11 | + const getStyle = (dom: any, attr: any) => { | |
12 | + return getComputedStyle(dom)[attr]; | |
13 | + }; | |
14 | + const drag = (wrap: any) => { | |
15 | + if (!wrap) return; | |
16 | + wrap.setAttribute('data-drag', unref(context.draggable)); | |
17 | + const dialogHeaderEl = wrap.querySelector('.ant-modal-header'); | |
18 | + const dragDom = wrap.querySelector('.ant-modal'); | |
19 | + | |
20 | + if (!dialogHeaderEl || !dragDom || !unref(context.draggable)) return; | |
21 | + | |
22 | + dialogHeaderEl.style.cursor = 'move'; | |
23 | + | |
24 | + dialogHeaderEl.onmousedown = (e: any) => { | |
25 | + if (!e) return; | |
26 | + // 鼠标按下,计算当前元素距离可视区的距离 | |
27 | + const disX = e.clientX; | |
28 | + const disY = e.clientY; | |
29 | + const screenWidth = document.body.clientWidth; // body当前宽度 | |
30 | + const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取) | |
31 | + | |
32 | + const dragDomWidth = dragDom.offsetWidth; // 对话框宽度 | |
33 | + const dragDomheight = dragDom.offsetHeight; // 对话框高度 | |
34 | + | |
35 | + const minDragDomLeft = dragDom.offsetLeft; | |
36 | + | |
37 | + const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth; | |
38 | + const minDragDomTop = dragDom.offsetTop; | |
39 | + const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight; | |
40 | + // 获取到的值带px 正则匹配替换 | |
41 | + const domLeft = getStyle(dragDom, 'left'); | |
42 | + const domTop = getStyle(dragDom, 'top'); | |
43 | + let styL = +domLeft; | |
44 | + let styT = +domTop; | |
45 | + | |
46 | + // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px | |
47 | + if (domLeft.includes('%')) { | |
48 | + styL = +document.body.clientWidth * (+domLeft.replace(/%/g, '') / 100); | |
49 | + styT = +document.body.clientHeight * (+domTop.replace(/%/g, '') / 100); | |
50 | + } else { | |
51 | + styL = +domLeft.replace(/px/g, ''); | |
52 | + styT = +domTop.replace(/px/g, ''); | |
53 | + } | |
54 | + | |
55 | + document.onmousemove = function (e) { | |
56 | + // 通过事件委托,计算移动的距离 | |
57 | + let left = e.clientX - disX; | |
58 | + let top = e.clientY - disY; | |
59 | + | |
60 | + // 边界处理 | |
61 | + if (-left > minDragDomLeft) { | |
62 | + left = -minDragDomLeft; | |
63 | + } else if (left > maxDragDomLeft) { | |
64 | + left = maxDragDomLeft; | |
65 | + } | |
66 | + | |
67 | + if (-top > minDragDomTop) { | |
68 | + top = -minDragDomTop; | |
69 | + } else if (top > maxDragDomTop) { | |
70 | + top = maxDragDomTop; | |
71 | + } | |
72 | + | |
73 | + // 移动当前元素 | |
74 | + dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`; | |
75 | + }; | |
76 | + | |
77 | + document.onmouseup = () => { | |
78 | + document.onmousemove = null; | |
79 | + document.onmouseup = null; | |
80 | + }; | |
81 | + }; | |
82 | + }; | |
83 | + | |
84 | + const handleDrag = () => { | |
85 | + const dragWraps = document.querySelectorAll('.ant-modal-wrap'); | |
86 | + for (const wrap of Array.from(dragWraps)) { | |
87 | + if (!wrap) continue; | |
88 | + const display = getStyle(wrap, 'display'); | |
89 | + const draggable = wrap.getAttribute('data-drag'); | |
90 | + if (display !== 'none') { | |
91 | + // 拖拽位置 | |
92 | + if (draggable === null || unref(context.destroyOnClose)) { | |
93 | + drag(wrap); | |
94 | + } | |
95 | + } | |
96 | + } | |
97 | + }; | |
98 | + | |
99 | + watchEffect(() => { | |
100 | + if (!unref(context.visible) || !unref(context.draggable)) { | |
101 | + return; | |
102 | + } | |
103 | + useTimeoutFn(() => { | |
104 | + handleDrag(); | |
105 | + }, 30); | |
106 | + }); | |
107 | +} | ... | ... |
src/components/Scrollbar/src/Scrollbar.tsx
src/components/Table/src/hooks/useTableScroll.ts
1 | 1 | import type { BasicTableProps } from '../types/table'; |
2 | 2 | import { computed, Ref, onMounted, unref, ref, nextTick, ComputedRef, watch } from 'vue'; |
3 | 3 | |
4 | -import { injectModal } from '/@/components/Modal/src/provideModal'; | |
5 | - | |
6 | 4 | import { getViewportOffset } from '/@/utils/domUtils'; |
7 | 5 | import { isBoolean } from '/@/utils/is'; |
8 | 6 | |
9 | 7 | import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; |
10 | 8 | import { useProps } from './useProps'; |
9 | +import { useModalContext } from '/@/components/Modal'; | |
11 | 10 | |
12 | 11 | export function useTableScroll(refProps: ComputedRef<BasicTableProps>, tableElRef: Ref<any>) { |
13 | 12 | const { propsRef } = useProps(refProps); |
14 | 13 | |
15 | 14 | const tableHeightRef: Ref<number | null> = ref(null); |
16 | 15 | |
17 | - const redoModalHeight = injectModal(); | |
16 | + const modalFn = useModalContext(); | |
18 | 17 | |
19 | 18 | watch( |
20 | 19 | () => unref(propsRef).canResize, |
... | ... | @@ -93,7 +92,7 @@ export function useTableScroll(refProps: ComputedRef<BasicTableProps>, tableElRe |
93 | 92 | tableHeightRef.value = |
94 | 93 | tableHeightRef.value! > maxHeight! ? (maxHeight as number) : tableHeightRef.value; |
95 | 94 | // 解决表格放modal内的时候,modal自适应高度计算问题 |
96 | - redoModalHeight && redoModalHeight(); | |
95 | + modalFn?.redoModalHeight?.(); | |
97 | 96 | }, 16); |
98 | 97 | } |
99 | 98 | ... | ... |
src/components/Tree/src/BasicTree.tsx
1 | 1 | import './index.less'; |
2 | 2 | |
3 | -import type { ReplaceFields, TreeItem, Keys, CheckKeys } from './types'; | |
3 | +import type { ReplaceFields, TreeItem, Keys, CheckKeys, TreeActionType } from './types'; | |
4 | 4 | |
5 | 5 | import { defineComponent, reactive, computed, unref, ref, watchEffect, CSSProperties } from 'vue'; |
6 | 6 | import { Tree } from 'ant-design-vue'; |
... | ... | @@ -124,7 +124,6 @@ export default defineComponent({ |
124 | 124 | title: () => ( |
125 | 125 | <span class={`${prefixCls}-title`}> |
126 | 126 | <span class={`${prefixCls}__content`} style={unref(getContentStyle)}> |
127 | - {' '} | |
128 | 127 | {titleField && anyItem[titleField]} |
129 | 128 | </span> |
130 | 129 | <span class={`${prefixCls}__actions`}> {renderAction(item)}</span> |
... | ... | @@ -183,7 +182,7 @@ export default defineComponent({ |
183 | 182 | state.checkedKeys = props.checkedKeys; |
184 | 183 | }); |
185 | 184 | |
186 | - tryTsxEmit((currentInstance) => { | |
185 | + tryTsxEmit<TreeActionType>((currentInstance) => { | |
187 | 186 | currentInstance.setExpandedKeys = setExpandedKeys; |
188 | 187 | currentInstance.getExpandedKeys = getExpandedKeys; |
189 | 188 | currentInstance.setSelectedKeys = setSelectedKeys; | ... | ... |
src/components/Tree/src/useTree.ts
... | ... | @@ -10,7 +10,7 @@ export function useTree( |
10 | 10 | getReplaceFields: ComputedRef<ReplaceFields> |
11 | 11 | ) { |
12 | 12 | // 更新节点 |
13 | - function updateNodeByKey(key: string, node: TreeItem, list: TreeItem[]) { | |
13 | + function updateNodeByKey(key: string, node: TreeItem, list?: TreeItem[]) { | |
14 | 14 | if (!key) return; |
15 | 15 | const treeData = list || unref(treeDataRef); |
16 | 16 | const { key: keyField, children: childrenField } = unref(getReplaceFields); |
... | ... | @@ -75,7 +75,7 @@ export function useTree( |
75 | 75 | } |
76 | 76 | |
77 | 77 | // 删除节点 |
78 | - function deleteNodeByKey(key: string, list: TreeItem[]) { | |
78 | + function deleteNodeByKey(key: string, list?: TreeItem[]) { | |
79 | 79 | if (!key) return; |
80 | 80 | const treeData = list || unref(treeDataRef); |
81 | 81 | const { key: keyField, children: childrenField } = unref(getReplaceFields); | ... | ... |
src/components/Verify/src/DragVerify.tsx
... | ... | @@ -6,6 +6,7 @@ import { getSlot } from '/@/utils/helper/tsxHelper'; |
6 | 6 | import './DragVerify.less'; |
7 | 7 | import { CheckOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
8 | 8 | import { tryTsxEmit } from '/@/utils/helper/vueHelper'; |
9 | +import type { DragVerifyActionType } from './types'; | |
9 | 10 | export default defineComponent({ |
10 | 11 | name: 'BaseDargVerify', |
11 | 12 | props: basicProps, |
... | ... | @@ -210,7 +211,7 @@ export default defineComponent({ |
210 | 211 | contentEl.style.width = unref(getContentStyleRef).width; |
211 | 212 | } |
212 | 213 | |
213 | - tryTsxEmit((instance) => { | |
214 | + tryTsxEmit<DragVerifyActionType>((instance) => { | |
214 | 215 | instance.resume = resume; |
215 | 216 | }); |
216 | 217 | ... | ... |
src/hooks/setting/useRootSetting.ts
... | ... | @@ -46,7 +46,7 @@ export function useRootSetting() { |
46 | 46 | unref(getRootSetting).contentMode === ContentEnum.FULL ? ContentEnum.FULL : ContentEnum.FIXED |
47 | 47 | ); |
48 | 48 | |
49 | - function setRootSetting(setting: RootSetting) { | |
49 | + function setRootSetting(setting: Partial<RootSetting>) { | |
50 | 50 | appStore.commitProjectConfigState(setting); |
51 | 51 | } |
52 | 52 | ... | ... |
src/utils/helper/vueHelper.ts
... | ... | @@ -7,6 +7,7 @@ import { |
7 | 7 | onUnmounted, |
8 | 8 | nextTick, |
9 | 9 | reactive, |
10 | + ComponentInternalInstance, | |
10 | 11 | } from 'vue'; |
11 | 12 | |
12 | 13 | export function explicitComputed<T, S>(source: WatchSource<S>, fn: () => T) { |
... | ... | @@ -29,8 +30,10 @@ export function tryOnUnmounted(fn: () => Promise<void> | void) { |
29 | 30 | getCurrentInstance() && onUnmounted(fn); |
30 | 31 | } |
31 | 32 | |
32 | -export function tryTsxEmit(fn: (_instance: any) => Promise<void> | void) { | |
33 | - const instance = getCurrentInstance(); | |
33 | +export function tryTsxEmit<T extends any = ComponentInternalInstance>( | |
34 | + fn: (_instance: T) => Promise<void> | void | |
35 | +) { | |
36 | + const instance = getCurrentInstance() as any; | |
34 | 37 | instance && fn.call(null, instance); |
35 | 38 | } |
36 | 39 | ... | ... |