From c777ca07a1675e3b726755b381223cfde21dcaed Mon Sep 17 00:00:00 2001 From: Dexter <52393227+dexterBo@users.noreply.github.com> Date: Tue, 11 Jun 2024 19:47:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(loading):=20loading=E6=96=B0=E5=A2=9Eattac?= =?UTF-8?q?h=E3=80=81fullscreen=E5=B1=9E=E6=80=A7=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=87=BD=E6=95=B0=E5=BC=8F=E8=B0=83=E7=94=A8=20(#1444?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(loading): loading新增attach、fullscreen属性,支持函数式调用 * feat(loading): 组件示例更新 * fix: fix lint * chore(Loading): add loadingPlugin to plugins file --------- Co-authored-by: anlyyao --- src/_common | 2 +- .../__test__/__snapshots__/demo.test.jsx.snap | 13 +++ src/loading/demos/attach.vue | 25 +++++ src/loading/demos/fullscreen.vue | 32 ++++++ src/loading/demos/mobile.vue | 16 +++ src/loading/demos/service.vue | 62 ++++++++++++ src/loading/index.ts | 3 + src/loading/loading.en-US.md | 12 +++ src/loading/loading.md | 12 +++ src/loading/loading.tsx | 46 ++++++++- src/loading/plugin.tsx | 99 +++++++++++++++++++ src/loading/props.ts | 7 ++ src/loading/type.ts | 18 +++- src/plugins.ts | 1 + 14 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 src/loading/demos/attach.vue create mode 100644 src/loading/demos/fullscreen.vue create mode 100644 src/loading/demos/service.vue create mode 100644 src/loading/plugin.tsx diff --git a/src/_common b/src/_common index 51a7e6b75..b09ab7369 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 51a7e6b75a1dfaf6338e2b8fbe57763a8a2f4999 +Subproject commit b09ab736990e8a29c26472a20039fc06fb23c1d9 diff --git a/src/loading/__test__/__snapshots__/demo.test.jsx.snap b/src/loading/__test__/__snapshots__/demo.test.jsx.snap index 834d49740..7798e8611 100644 --- a/src/loading/__test__/__snapshots__/demo.test.jsx.snap +++ b/src/loading/__test__/__snapshots__/demo.test.jsx.snap @@ -927,6 +927,19 @@ exports[`Loading > Loading mobileVue demo works fine 1`] = ` + + `; diff --git a/src/loading/demos/attach.vue b/src/loading/demos/attach.vue new file mode 100644 index 000000000..4bbdaaf37 --- /dev/null +++ b/src/loading/demos/attach.vue @@ -0,0 +1,25 @@ + + + + diff --git a/src/loading/demos/fullscreen.vue b/src/loading/demos/fullscreen.vue new file mode 100644 index 000000000..712861a4d --- /dev/null +++ b/src/loading/demos/fullscreen.vue @@ -0,0 +1,32 @@ + + + + diff --git a/src/loading/demos/mobile.vue b/src/loading/demos/mobile.vue index 9a6496dd3..69636d92f 100644 --- a/src/loading/demos/mobile.vue +++ b/src/loading/demos/mobile.vue @@ -28,6 +28,19 @@ + + @@ -38,6 +51,9 @@ import vert from './vert.vue'; import pureText from './pure-text.vue'; import speed from './speed.vue'; import size from './size.vue'; +import service from './service.vue'; +import fullscreen from './fullscreen.vue'; +import attach from './attach.vue'; diff --git a/src/loading/index.ts b/src/loading/index.ts index 75241d18b..fa710e6d4 100644 --- a/src/loading/index.ts +++ b/src/loading/index.ts @@ -5,6 +5,9 @@ import { TdLoadingProps } from './type'; import './style'; export * from './type'; +export * from './plugin'; +export { default as LoadingPlugin } from './plugin'; + export type LoadingProps = TdLoadingProps; const _Loading: WithInstallType = withInstall(Loading); diff --git a/src/loading/loading.en-US.md b/src/loading/loading.en-US.md index 36bf504e0..2fa0c6391 100644 --- a/src/loading/loading.en-US.md +++ b/src/loading/loading.en-US.md @@ -6,10 +6,12 @@ name | type | default | description | required -- | -- | -- | -- | -- +attach | String / Function | '' | Typescript:`AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N content | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N default | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N delay | Number | 0 | \- | N duration | Number | 800 | \- | N +fullscreen | Boolean | false | \- | N indicator | Boolean / Slot / Function | true | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N inheritColor | Boolean | false | \- | N layout | String | horizontal | options: horizontal/vertical | N @@ -20,6 +22,16 @@ size | String | '20px' | \- | N text | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N theme | String | circular | options: circular/spinner/dots | N +### LoadingPlugin + +同时也支持 `this.$loading`。 + +name | params | default | description +-- | -- | -- | -- +options | Function | - | required。Typescript:`boolean \| TdLoadingProps` + +插件返回值:`LoadingInstance【interface LoadingInstance { hide: () => void }】` + ### CSS Variables The component provides the following CSS variables, which can be used to customize styles. diff --git a/src/loading/loading.md b/src/loading/loading.md index 18420c4e1..ba302c553 100644 --- a/src/loading/loading.md +++ b/src/loading/loading.md @@ -6,10 +6,12 @@ 名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- +attach | String / Function | '' | 挂载元素,默认挂载到组件本身所在的位置。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body。TS 类型:`AttachNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N content | String / Slot / Function | - | 子元素。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N default | String / Slot / Function | - | 子元素,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N delay | Number | 0 | 延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 | N duration | Number | 800 | 加载动画执行完成一次的时间,单位:毫秒 | N +fullscreen | Boolean | false | 是否显示为全屏加载 | N indicator | Boolean / Slot / Function | true | 加载指示符,值为 true 显示默认指示符,值为 false 则不显示,也可以自定义指示符。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N inheritColor | Boolean | false | 是否继承父元素颜色 | N layout | String | horizontal | 对齐方式。可选项:horizontal/vertical | N @@ -20,6 +22,16 @@ size | String | '20px' | 尺寸,示例:20px | N text | String / Slot / Function | - | 加载提示文案。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N theme | String | circular | 加载组件类型。可选项:circular/spinner/dots | N +### LoadingPlugin + +同时也支持 `this.$loading`。 + +参数名称 | 参数类型 | 参数默认值 | 参数描述 +-- | -- | -- | -- +options | Function | - | 必需。TS 类型:`boolean \| TdLoadingProps` + +插件返回值:`LoadingInstance【interface LoadingInstance { hide: () => void }】` + ### CSS Variables 组件提供了下列 CSS 变量,可用于自定义样式。 diff --git a/src/loading/loading.tsx b/src/loading/loading.tsx index d0f229e9c..f0a6f9472 100644 --- a/src/loading/loading.tsx +++ b/src/loading/loading.tsx @@ -1,4 +1,4 @@ -import { defineComponent, computed, ref, watch, h, setBlockTracking } from 'vue'; +import { defineComponent, computed, ref, watch, h, setBlockTracking, Teleport, onMounted } from 'vue'; import TGradientIcon from './icon/gradient'; import SpinnerIcon from './icon/spinner'; @@ -6,6 +6,7 @@ import config from '../config'; import props from './props'; import { useContent, useTNodeJSX } from '../hooks/tnode'; import { usePrefixClass } from '../hooks/useClass'; +import { addClass, getAttach, removeClass } from '@/shared/dom'; const { prefix } = config; @@ -18,6 +19,7 @@ export default defineComponent({ const loadingClass = usePrefixClass('loading'); const delayShowLoading = ref(false); + const teleportElement = ref(); const countDelay = () => { delayShowLoading.value = false; @@ -44,6 +46,8 @@ export default defineComponent({ const rootClass = computed(() => [ loadingClass.value, { [`${loadingClass.value}--vertical`]: props.layout === 'vertical' }, + { [`${loadingClass.value}--fullscreen`]: props.fullscreen }, + { [`${loadingClass.value}--full`]: !props.fullscreen && !!props.attach }, ]); const textClass = computed(() => [ @@ -69,6 +73,20 @@ export default defineComponent({ spinner: SpinnerIcon, }; + onMounted(() => { + if (props.attach) { + const attach = getAttach(props.attach); + if (!attach) { + console.error('attach is not exist'); + } else { + teleportElement.value = attach; + } + } + if (props.fullscreen) { + teleportElement.value = getAttach('body'); + } + }); + const dotsLoading = computed(() => { setBlockTracking(-1); const node = ( @@ -119,12 +137,38 @@ export default defineComponent({ return node; }); + watch( + () => props.loading, + (isLoading) => { + if (isLoading && props.fullscreen) { + countDelay(); + addClass(document.body, `${loadingClass.value}--lock`); + } else { + removeClass(document.body, `${loadingClass.value}--lock`); + } + }, + ); + return () => { const indicator = renderTNodeJSX('indicator', { defaultNode: props.theme === 'dots' ? dotsLoading.value : defaultLoading.value, }); const text = renderTNodeJSX('text'); const TNodeContent = renderTNodeContent('default', 'content'); + + if (props.fullscreen || props.attach) { + if (!props.loading) return null; + return ( + +
+ {realLoading.value && indicator} + {text && realLoading.value && {text}} + {TNodeContent} +
+
+ ); + } + return (
{realLoading.value && indicator} diff --git a/src/loading/plugin.tsx b/src/loading/plugin.tsx new file mode 100644 index 000000000..8b7ddd1ae --- /dev/null +++ b/src/loading/plugin.tsx @@ -0,0 +1,99 @@ +import { App, Plugin, createApp, defineComponent, h, reactive } from 'vue'; +import merge from 'lodash/merge'; +import LoadingComponent from './loading'; +import { getAttach, removeClass, addClass } from '../shared/dom'; +import { TdLoadingProps, LoadingInstance, LoadingMethod } from './type'; +import { usePrefixClass } from '../hooks/useClass'; + +let fullScreenLoadingInstance: LoadingInstance = null; + +function mergeDefaultProps(props: TdLoadingProps): TdLoadingProps { + const options: TdLoadingProps = merge( + { + fullscreen: false, + attach: 'body', + loading: true, + }, + props, + ); + + return options; +} + +function createLoading(props: TdLoadingProps): LoadingInstance { + const mergedProps = mergeDefaultProps(props); + + if (mergedProps.fullscreen && fullScreenLoadingInstance) { + return fullScreenLoadingInstance; + } + + const component = defineComponent({ + setup() { + const loadingOptions = reactive(mergedProps); + return { + loadingOptions, + }; + }, + render() { + return h(LoadingComponent, { + ...this.loadingOptions, + }); + }, + }); + + const attach = getAttach(mergedProps.fullscreen ? 'body' : mergedProps.attach); + + const app = createApp(component); + app.mount(document.createElement('div')); + const parentRelativeClass = usePrefixClass('loading__parent--relative').value; + const lockClass = usePrefixClass('loading--lock').value; + + if (mergedProps.fullscreen) { + addClass(document.body, lockClass); + } + + if (attach) { + addClass(attach, parentRelativeClass); + } else { + console.error('attach is not exist'); + } + + const loadingInstance: LoadingInstance = { + hide: () => { + removeClass(attach, parentRelativeClass); + removeClass(document.body, lockClass); + app.unmount(); + }, + }; + return loadingInstance; +} + +function produceLoading(props: boolean | TdLoadingProps): LoadingInstance { + // 全屏加载 + if (props === true) { + fullScreenLoadingInstance = createLoading({ + fullscreen: true, + loading: true, + attach: 'body', + }); + return fullScreenLoadingInstance; + } + + if (props === false) { + // 销毁全屏实例 + fullScreenLoadingInstance?.hide(); + fullScreenLoadingInstance = null; + return; + } + return createLoading(props); +} + +export type LoadingPluginType = Plugin & LoadingMethod; + +export const LoadingPlugin: LoadingPluginType = produceLoading as LoadingPluginType; + +LoadingPlugin.install = (app: App) => { + app.config.globalProperties.$loading = produceLoading; +}; + +export default LoadingPlugin; diff --git a/src/loading/props.ts b/src/loading/props.ts index 4e639680e..23b93f1ab 100644 --- a/src/loading/props.ts +++ b/src/loading/props.ts @@ -8,6 +8,11 @@ import { TdLoadingProps } from './type'; import { PropType } from 'vue'; export default { + /** 挂载元素,默认挂载到组件本身所在的位置。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body */ + attach: { + type: [String, Function] as PropType, + default: '', + }, /** 子元素 */ content: { type: [String, Function] as PropType, @@ -26,6 +31,8 @@ export default { type: Number, default: 800, }, + /** 是否显示为全屏加载 */ + fullscreen: Boolean, /** 加载指示符,值为 true 显示默认指示符,值为 false 则不显示,也可以自定义指示符 */ indicator: { type: [Boolean, Function] as PropType, diff --git a/src/loading/type.ts b/src/loading/type.ts index 9f6a5caa7..2fd3d054f 100644 --- a/src/loading/type.ts +++ b/src/loading/type.ts @@ -4,9 +4,14 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode } from '../common'; +import { TNode, AttachNode } from '../common'; export interface TdLoadingProps { + /** + * 挂载元素,默认挂载到组件本身所在的位置。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body + * @default '' + */ + attach?: AttachNode; /** * 子元素 */ @@ -25,6 +30,11 @@ export interface TdLoadingProps { * @default 800 */ duration?: number; + /** + * 是否显示为全屏加载 + * @default false + */ + fullscreen?: boolean; /** * 加载指示符,值为 true 显示默认指示符,值为 false 则不显示,也可以自定义指示符 * @default true @@ -69,3 +79,9 @@ export interface TdLoadingProps { */ theme?: 'circular' | 'spinner' | 'dots'; } + +export interface LoadingInstance { + hide: () => void; +} + +export type LoadingMethod = (options: boolean | TdLoadingProps) => LoadingInstance; diff --git a/src/plugins.ts b/src/plugins.ts index 7fda2de90..17a3660d7 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -2,3 +2,4 @@ export { DialogPlugin } from './dialog'; export { MessagePlugin } from './message'; export { ToastPlugin } from './toast'; export { DrawerPlugin } from './drawer'; +export { LoadingPlugin } from './loading';