);
- if (children && filterEmpty(children).length) {
+ if (children && filterEmpty(children).length && !props.fullscreen) {
const containerClassName = {
[`${prefixCls.value}-container`]: true,
[`${prefixCls.value}-blur`]: sSpinning.value,
+ [rootCls.value]: true,
+ [cssVarCls.value]: true,
+ [hashId.value]: true,
};
- return wrapSSR(
-
- {sSpinning.value &&
{spinElement}
}
+ return wrapCSSVar(
+
+ {sSpinning.value && spinElement}
{children}
,
);
}
- return wrapSSR(spinElement);
+
+ if (props.fullscreen) {
+ return wrapCSSVar(
+
+ {spinElement}
+
,
+ );
+ }
+
+ return wrapCSSVar(spinElement);
};
},
});
diff --git a/components/spin/demo/custom-indicator.vue b/components/spin/demo/custom-indicator.vue
index 566b6cefa4..cb1f2697f7 100644
--- a/components/spin/demo/custom-indicator.vue
+++ b/components/spin/demo/custom-indicator.vue
@@ -17,15 +17,38 @@ Use custom loading indicator.
-
+
+
+
+
+
+
diff --git a/components/spin/demo/fullscreen.vue b/components/spin/demo/fullscreen.vue
new file mode 100644
index 0000000000..2674935348
--- /dev/null
+++ b/components/spin/demo/fullscreen.vue
@@ -0,0 +1,51 @@
+
+---
+order: 8
+title:
+ zh-CN: 全屏
+ en-US: fullscreen
+---
+
+## zh-CN
+
+`fullscreen` 属性非常适合创建流畅的页面加载器。它添加了半透明覆盖层,并在其中心放置了一个旋转加载符号。
+
+## en-US
+
+The `fullscreen` mode is perfect for creating page loaders. It adds a dimmed overlay with a centered spinner.
+
+
+
+
+ Show fullscreen
+
+
+
+
diff --git a/components/spin/demo/index.vue b/components/spin/demo/index.vue
index 2a1a556717..af8724d55a 100644
--- a/components/spin/demo/index.vue
+++ b/components/spin/demo/index.vue
@@ -7,6 +7,8 @@
+
+
+
diff --git a/components/spin/demo/tip.vue b/components/spin/demo/tip.vue
index c284e0952b..12a7acda69 100644
--- a/components/spin/demo/tip.vue
+++ b/components/spin/demo/tip.vue
@@ -1,6 +1,6 @@
---
-order: 4
+order: 4
title:
zh-CN: 自定义描述文案
en-US: Customized description
@@ -17,10 +17,23 @@ Customized description content.
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/spin/index.en-US.md b/components/spin/index.en-US.md
index 525e9bfd65..264a3106e6 100644
--- a/components/spin/index.en-US.md
+++ b/components/spin/index.en-US.md
@@ -17,7 +17,9 @@ When part of the page is waiting for asynchronous data or during a rendering pro
| Property | Description | Type | Default Value | Version |
| --- | --- | --- | --- | --- |
| delay | specifies a delay in milliseconds for loading state (prevent flush) | number (milliseconds) | - | |
+| fullscreen | Display a backdrop with the `Spin` component | boolean | false | |
| indicator | vue node of the spinning indicator | vNode \|slot | - | |
+| percent | The progress percentage, when set to `auto`, it will be an indeterminate progress | number \| 'auto' | - | |
| size | size of Spin, options: `small`, `default` and `large` | string | `default` | |
| spinning | whether Spin is visible | boolean | true | |
| tip | customize description content when Spin has children | string \| slot | - | slot 3.0 |
diff --git a/components/spin/index.zh-CN.md b/components/spin/index.zh-CN.md
index f3537f6c82..d7df71d113 100644
--- a/components/spin/index.zh-CN.md
+++ b/components/spin/index.zh-CN.md
@@ -18,7 +18,9 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*i43_ToFrL8YAAA
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| delay | 延迟显示加载效果的时间(防止闪烁) | number (毫秒) | - | |
+| fullscreen | 显示带有`Spin`组件的背景 | boolean | false | |
| indicator | 加载指示符 | vNode \| slot | - | |
+| percent | 展示进度,当设置`percent="auto"`时会预估一个永远不会停止的进度 | number \| 'auto' | - | |
| size | 组件大小,可选值为 `small` `default` `large` | string | `default` | |
| spinning | 是否为加载中状态 | boolean | true | |
| tip | 当作为包裹元素时,可以自定义描述文案 | string \| slot | - | slot 3.0 |
diff --git a/components/spin/style/index.ts b/components/spin/style/index.ts
index 2eccec181a..1aba8430f8 100644
--- a/components/spin/style/index.ts
+++ b/components/spin/style/index.ts
@@ -1,18 +1,34 @@
import type { CSSObject } from '../../_util/cssinjs';
import { Keyframes } from '../../_util/cssinjs';
-import type { FullToken, GenerateStyle } from '../../theme/internal';
-import { genComponentStyleHook, mergeToken } from '../../theme/internal';
+import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
+import { genStyleHooks, mergeToken } from '../../theme/internal';
import { resetComponent } from '../../style';
export interface ComponentToken {
- contentHeight: number;
+ /**
+ * @desc 内容区域高度
+ * @descEN Height of content area
+ */
+ contentHeight: number | string;
+ /**
+ * @desc 加载图标尺寸
+ * @descEN Loading icon size
+ */
+ dotSize: number;
+ /**
+ * @desc 小号加载图标尺寸
+ * @descEN Small loading icon size
+ */
+ dotSizeSM: number;
+ /**
+ * @desc 大号加载图标尺寸
+ * @descEN Large loading icon size
+ */
+ dotSizeLG: number;
}
interface SpinToken extends FullToken<'Spin'> {
spinDotDefault: string;
- spinDotSize: number;
- spinDotSizeSM: number;
- spinDotSizeLG: number;
}
const antSpinMove = new Keyframes('antSpinMove', {
@@ -23,219 +39,309 @@ const antRotate = new Keyframes('antRotate', {
to: { transform: 'rotate(405deg)' },
});
-const genSpinStyle: GenerateStyle
= (token: SpinToken): CSSObject => ({
- [`${token.componentCls}`]: {
- ...resetComponent(token),
- position: 'absolute',
- display: 'none',
- color: token.colorPrimary,
- textAlign: 'center',
- verticalAlign: 'middle',
- opacity: 0,
- transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOutCirc}`,
-
- '&-spinning': {
- position: 'static',
- display: 'inline-block',
- opacity: 1,
- },
+const genSpinStyle: GenerateStyle = (token: SpinToken): CSSObject => {
+ const { componentCls, calc } = token;
+ return {
+ [componentCls]: {
+ ...resetComponent(token),
+ position: 'absolute',
+ display: 'none',
+ color: token.colorPrimary,
+ fontSize: 0,
+ textAlign: 'center',
+ verticalAlign: 'middle',
+ opacity: 0,
+ transition: `transform ${token.motionDurationSlow} ${token.motionEaseInOutCirc}`,
- '&-nested-loading': {
- position: 'relative',
- [`> div > ${token.componentCls}`]: {
- position: 'absolute',
- top: 0,
- insetInlineStart: 0,
- zIndex: 4,
- display: 'block',
- width: '100%',
- height: '100%',
- maxHeight: token.contentHeight,
+ '&-spinning': {
+ position: 'relative',
+ display: 'inline-block',
+ opacity: 1,
+ },
- [`${token.componentCls}-dot`]: {
- position: 'absolute',
- top: '50%',
- insetInlineStart: '50%',
- margin: -token.spinDotSize / 2,
+ [`${componentCls}-text`]: {
+ fontSize: token.fontSize,
+ paddingTop: calc(calc(token.dotSize).sub(token.fontSize)).div(2).add(2).equal(),
+ },
+
+ '&-fullscreen': {
+ position: 'fixed',
+ width: '100vw',
+ height: '100vh',
+ backgroundColor: token.colorBgMask,
+ zIndex: token.zIndexPopupBase,
+ inset: 0,
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ opacity: 0,
+ visibility: 'hidden',
+ transition: `all ${token.motionDurationMid}`,
+ '&-show': {
+ opacity: 1,
+ visibility: 'visible',
+ },
+
+ [componentCls]: {
+ [`${componentCls}-dot-holder`]: {
+ color: token.colorWhite,
+ },
+ [`${componentCls}-text`]: {
+ color: token.colorTextLightSolid,
+ },
},
+ },
- [`${token.componentCls}-text`]: {
+ '&-nested-loading': {
+ position: 'relative',
+ [`> ${componentCls}`]: {
position: 'absolute',
- top: '50%',
+ top: 0,
+ insetInlineStart: 0,
+ zIndex: 4,
+ display: 'block',
width: '100%',
- paddingTop: (token.spinDotSize - token.fontSize) / 2 + 2,
- textShadow: `0 1px 2px ${token.colorBgContainer}`, // FIXME: shadow
- },
+ height: '100%',
+ maxHeight: token.contentHeight,
+ [`${componentCls}-dot`]: {
+ position: 'absolute',
+ top: '50%',
+ insetInlineStart: '50%',
+ margin: calc(token.dotSize).mul(-1).div(2).equal(),
+ },
- [`&${token.componentCls}-show-text ${token.componentCls}-dot`]: {
- marginTop: -(token.spinDotSize / 2) - 10,
- },
+ [`${componentCls}-text`]: {
+ position: 'absolute',
+ top: '50%',
+ width: '100%',
+ // paddingTop: (token.spinDotSize - token.fontSize) / 2 + 2,
+ textShadow: `0 1px 2px ${token.colorBgContainer}`, // FIXME: shadow
+ },
- '&-sm': {
- [`${token.componentCls}-dot`]: {
- margin: -token.spinDotSizeSM / 2,
+ [`&${componentCls}-show-text ${componentCls}-dot`]: {
+ marginTop: calc(token.dotSize).div(2).mul(-1).sub(10).equal(),
},
- [`${token.componentCls}-text`]: {
- paddingTop: (token.spinDotSizeSM - token.fontSize) / 2 + 2,
+
+ '&-sm': {
+ [`${componentCls}-dot`]: {
+ margin: calc(token.dotSizeSM).mul(-1).div(2).equal(),
+ },
+ [`${componentCls}-text`]: {
+ paddingTop: calc(calc(token.dotSizeSM).sub(token.fontSize)).div(2).add(2).equal(),
+ },
+ [`&${componentCls}-show-text ${componentCls}-dot`]: {
+ marginTop: calc(token.dotSizeSM).div(2).mul(-1).sub(10).equal(),
+ },
},
- [`&${token.componentCls}-show-text ${token.componentCls}-dot`]: {
- marginTop: -(token.spinDotSizeSM / 2) - 10,
+
+ '&-lg': {
+ [`${componentCls}-dot`]: {
+ margin: calc(token.dotSizeLG).mul(-1).div(2).equal(),
+ },
+ [`${componentCls}-text`]: {
+ paddingTop: calc(calc(token.dotSizeLG).sub(token.fontSize)).div(2).add(2).equal(),
+ },
+ [`&${componentCls}-show-text ${componentCls}-dot`]: {
+ marginTop: calc(token.dotSizeLG).div(2).mul(-1).sub(10).equal(),
+ },
},
},
- '&-lg': {
- [`${token.componentCls}-dot`]: {
- margin: -(token.spinDotSizeLG / 2),
- },
- [`${token.componentCls}-text`]: {
- paddingTop: (token.spinDotSizeLG - token.fontSize) / 2 + 2,
+ [`${componentCls}-container`]: {
+ position: 'relative',
+ transition: `opacity ${token.motionDurationSlow}`,
+
+ '&::after': {
+ position: 'absolute',
+ top: 0,
+ insetInlineEnd: 0,
+ bottom: 0,
+ insetInlineStart: 0,
+ zIndex: 10,
+ width: '100%',
+ height: '100%',
+ background: token.colorBgContainer,
+ opacity: 0,
+ transition: `all ${token.motionDurationSlow}`,
+ content: '""',
+ pointerEvents: 'none',
},
- [`&${token.componentCls}-show-text ${token.componentCls}-dot`]: {
- marginTop: -(token.spinDotSizeLG / 2) - 10,
+ },
+
+ [`${componentCls}-blur`]: {
+ clear: 'both',
+ opacity: 0.5,
+ userSelect: 'none',
+ pointerEvents: 'none',
+
+ [`&::after`]: {
+ opacity: 0.4,
+ pointerEvents: 'auto',
},
},
},
- [`${token.componentCls}-container`]: {
- position: 'relative',
- transition: `opacity ${token.motionDurationSlow}`,
+ // tip
+ // ------------------------------
+ [`&-tip`]: {
+ color: token.spinDotDefault,
+ },
- '&::after': {
- position: 'absolute',
- top: 0,
- insetInlineEnd: 0,
- bottom: 0,
- insetInlineStart: 0,
- zIndex: 10,
- width: '100%',
- height: '100%',
- background: token.colorBgContainer,
+ // holder
+ // ------------------------------
+ [`${componentCls}-dot-holder`]: {
+ width: '1em',
+ height: '1em',
+ fontSize: token.dotSize,
+ display: 'inline-block',
+ transition: `transform ${token.motionDurationSlow} ease, opacity ${token.motionDurationSlow} ease`,
+ transformOrigin: '50% 50%',
+ lineHeight: 1,
+ color: token.colorPrimary,
+
+ '&-hidden': {
+ transform: 'scale(0.3)',
opacity: 0,
- transition: `all ${token.motionDurationSlow}`,
- content: '""',
- pointerEvents: 'none',
},
},
- [`${token.componentCls}-blur`]: {
- clear: 'both',
- opacity: 0.5,
- userSelect: 'none',
- pointerEvents: 'none',
-
- [`&::after`]: {
- opacity: 0.4,
- pointerEvents: 'auto',
- },
+ // progress
+ // ------------------------------
+ [`${componentCls}-dot-progress`]: {
+ position: 'absolute',
+ inset: 0,
},
- },
- // tip
- // ------------------------------
- [`&-tip`]: {
- color: token.spinDotDefault,
- },
+ // dots
+ // ------------------------------
+ [`${componentCls}-dot`]: {
+ position: 'relative',
+ display: 'inline-block',
+ fontSize: token.dotSize,
+ width: '1em',
+ height: '1em',
- // dots
- // ------------------------------
- [`${token.componentCls}-dot`]: {
- position: 'relative',
- display: 'inline-block',
- fontSize: token.spinDotSize,
- width: '1em',
- height: '1em',
+ '&-item': {
+ position: 'absolute',
+ display: 'block',
+ width: calc(token.dotSize).sub(calc(token.marginXXS).div(2)).div(2).equal(),
+ height: calc(token.dotSize).sub(calc(token.marginXXS).div(2)).div(2).equal(),
+ backgroundColor: token.colorPrimary,
+ borderRadius: '100%',
+ transform: 'scale(0.75)',
+ transformOrigin: '50% 50%',
+ opacity: 0.3,
+ animationName: antSpinMove,
+ animationDuration: '1s',
+ animationIterationCount: 'infinite',
+ animationTimingFunction: 'linear',
+ animationDirection: 'alternate',
- '&-item': {
- position: 'absolute',
- display: 'block',
- width: (token.spinDotSize - token.marginXXS / 2) / 2,
- height: (token.spinDotSize - token.marginXXS / 2) / 2,
- backgroundColor: token.colorPrimary,
- borderRadius: '100%',
- transform: 'scale(0.75)',
- transformOrigin: '50% 50%',
- opacity: 0.3,
- animationName: antSpinMove,
- animationDuration: '1s',
- animationIterationCount: 'infinite',
- animationTimingFunction: 'linear',
- animationDirection: 'alternate',
-
- '&:nth-child(1)': {
- top: 0,
- insetInlineStart: 0,
- },
+ '&:nth-child(1)': {
+ top: 0,
+ insetInlineStart: 0,
+ },
- '&:nth-child(2)': {
- top: 0,
- insetInlineEnd: 0,
- animationDelay: '0.4s',
+ '&:nth-child(2)': {
+ top: 0,
+ insetInlineEnd: 0,
+ animationDelay: '0.4s',
+ },
+
+ '&:nth-child(3)': {
+ insetInlineEnd: 0,
+ bottom: 0,
+ animationDelay: '0.8s',
+ },
+
+ '&:nth-child(4)': {
+ bottom: 0,
+ insetInlineStart: 0,
+ animationDelay: '1.2s',
+ },
},
- '&:nth-child(3)': {
- insetInlineEnd: 0,
- bottom: 0,
- animationDelay: '0.8s',
+ '&-spin': {
+ transform: 'rotate(45deg)',
+ animationName: antRotate,
+ animationDuration: '1.2s',
+ animationIterationCount: 'infinite',
+ animationTimingFunction: 'linear',
},
- '&:nth-child(4)': {
- bottom: 0,
- insetInlineStart: 0,
- animationDelay: '1.2s',
+ '&-circle': {
+ strokeLinecap: 'round',
+ transition: ['stroke-dashoffset', 'stroke-dasharray', 'stroke', 'stroke-width', 'opacity']
+ .map(item => `${item} ${token.motionDurationSlow} ease`)
+ .join(','),
+ fillOpacity: 0,
+ stroke: 'currentcolor',
},
- },
- '&-spin': {
- transform: 'rotate(45deg)',
- animationName: antRotate,
- animationDuration: '1.2s',
- animationIterationCount: 'infinite',
- animationTimingFunction: 'linear',
+ '&-circle-bg': {
+ stroke: token.colorFillSecondary,
+ },
},
- },
- // Sizes
- // ------------------------------
+ // Sizes
+ // ------------------------------
- // small
- [`&-sm ${token.componentCls}-dot`]: {
- fontSize: token.spinDotSizeSM,
-
- i: {
- width: (token.spinDotSizeSM - token.marginXXS / 2) / 2,
- height: (token.spinDotSizeSM - token.marginXXS / 2) / 2,
+ [`&-sm ${componentCls}-dot`]: {
+ '&, &-holder': {
+ fontSize: token.dotSizeSM,
+ },
+ },
+ // small
+ [`&-sm ${componentCls}-dot-holder`]: {
+ i: {
+ width: calc(calc(token.dotSizeSM).sub(calc(token.marginXXS).div(2)))
+ .div(2)
+ .equal(),
+ height: calc(calc(token.dotSizeSM).sub(calc(token.marginXXS).div(2)))
+ .div(2)
+ .equal(),
+ },
},
- },
- // large
- [`&-lg ${token.componentCls}-dot`]: {
- fontSize: token.spinDotSizeLG,
+ // large
+ [`&-lg ${componentCls}-dot`]: {
+ '&, &-holder': {
+ fontSize: token.dotSizeLG,
+ },
+ },
+ [`&-lg ${componentCls}-dot-holder`]: {
+ i: {
+ width: calc(calc(token.dotSizeLG).sub(token.marginXXS)).div(2).equal(),
+ height: calc(calc(token.dotSizeLG).sub(token.marginXXS)).div(2).equal(),
+ },
+ },
- i: {
- width: (token.spinDotSizeLG - token.marginXXS) / 2,
- height: (token.spinDotSizeLG - token.marginXXS) / 2,
+ [`&${componentCls}-show-text ${componentCls}-text`]: {
+ display: 'block',
},
},
+ };
+};
- [`&${token.componentCls}-show-text ${token.componentCls}-text`]: {
- display: 'block',
- },
- },
-});
+export const prepareComponentToken: GetDefaultToken<'Spin'> = token => {
+ const { controlHeightLG, controlHeight } = token;
+ return {
+ contentHeight: 400,
+ dotSize: controlHeightLG / 2,
+ dotSizeSM: controlHeightLG * 0.35,
+ dotSizeLG: controlHeight,
+ };
+};
// ============================== Export ==============================
-export default genComponentStyleHook(
+export default genStyleHooks(
'Spin',
token => {
const spinToken = mergeToken(token, {
spinDotDefault: token.colorTextDescription,
- spinDotSize: token.controlHeightLG / 2,
- spinDotSizeSM: token.controlHeightLG * 0.35,
- spinDotSizeLG: token.controlHeight,
});
return [genSpinStyle(spinToken)];
},
- {
- contentHeight: 400,
- },
+ prepareComponentToken,
);
diff --git a/components/spin/usePercent.ts b/components/spin/usePercent.ts
new file mode 100644
index 0000000000..39432e8d93
--- /dev/null
+++ b/components/spin/usePercent.ts
@@ -0,0 +1,46 @@
+import { ref, computed, watchEffect } from 'vue';
+
+const AUTO_INTERVAL = 200;
+const STEP_BUCKETS: [limit: number, stepPtg: number][] = [
+ [30, 0.05],
+ [70, 0.03],
+ [96, 0.01],
+];
+
+export default function usePercent(spinning: boolean, percent?: number | 'auto') {
+ const mockPercent = ref(0);
+ const mockIntervalRef = ref | null>(null);
+
+ const isAuto = ref(percent === 'auto');
+
+ watchEffect(() => {
+ // 清除现有定时器
+ if (mockIntervalRef.value || !isAuto.value || !spinning) {
+ clearInterval(mockIntervalRef.value);
+ mockIntervalRef.value = null;
+ }
+
+ if (isAuto.value && spinning) {
+ mockPercent.value = 0;
+
+ mockIntervalRef.value = setInterval(() => {
+ mockPercent.value = calculateNextPercent(mockPercent.value);
+ }, AUTO_INTERVAL);
+ }
+ });
+
+ return computed(() => (isAuto.value ? mockPercent.value : +percent));
+}
+
+function calculateNextPercent(prev: number): number {
+ const restPTG = 100 - prev;
+
+ for (let i = 0; i < STEP_BUCKETS.length; i += 1) {
+ const [limit, stepPtg] = STEP_BUCKETS[i];
+ if (prev <= limit) {
+ return prev + restPTG * stepPtg;
+ }
+ }
+
+ return prev;
+}