Merge 86c9b4cc9a
into c9443e038b
commit
9c697c3b5b
|
@ -0,0 +1,101 @@
|
|||
import { defineComponent, ref, computed, watchEffect } from 'vue';
|
||||
|
||||
export interface ProgressProps {
|
||||
prefixCls: string;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
const viewSize = 100;
|
||||
const borderWidth = viewSize / 5;
|
||||
const radius = viewSize / 2 - borderWidth / 2;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
const position = 50;
|
||||
|
||||
const CustomCircle = defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
dotClassName: String,
|
||||
style: Object,
|
||||
hasCircleCls: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const cStyle = computed(() => props.style || {});
|
||||
|
||||
return () => (
|
||||
<circle
|
||||
class={[
|
||||
`${props.dotClassName}-circle`,
|
||||
{
|
||||
[`${props.dotClassName}-circle-bg`]: props.hasCircleCls,
|
||||
},
|
||||
]}
|
||||
r={radius}
|
||||
cx={position}
|
||||
cy={position}
|
||||
stroke-width={borderWidth}
|
||||
style={cStyle.value}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'Progress',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
percent: Number,
|
||||
prefixCls: String,
|
||||
},
|
||||
setup(props) {
|
||||
const dotClassName = `${props.prefixCls}-dot`;
|
||||
const holderClassName = `${dotClassName}-holder`;
|
||||
const hideClassName = `${holderClassName}-hidden`;
|
||||
|
||||
const render = ref(false);
|
||||
|
||||
// ==================== Visible =====================
|
||||
watchEffect(() => {
|
||||
if (props.percent !== 0) {
|
||||
render.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Progress ====================
|
||||
const safePtg = computed(() => Math.max(Math.min(props.percent, 100), 0));
|
||||
|
||||
const circleStyle = computed(() => ({
|
||||
strokeDashoffset: `${circumference / 4}`,
|
||||
strokeDasharray: `${(circumference * safePtg.value) / 100} ${
|
||||
(circumference * (100 - safePtg.value)) / 100
|
||||
}`,
|
||||
}));
|
||||
|
||||
// ===================== Render =====================
|
||||
return () => {
|
||||
if (!render.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
class={[holderClassName, `${dotClassName}-progress`, safePtg.value <= 0 && hideClassName]}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${viewSize} ${viewSize}`}
|
||||
{...({
|
||||
role: 'progressbar',
|
||||
'aria-valuemin': 0,
|
||||
'aria-valuemax': 100,
|
||||
'aria-valuenow': safePtg.value,
|
||||
} as any)}
|
||||
>
|
||||
<CustomCircle dotClassName={dotClassName} hasCircleCls={true} />
|
||||
<CustomCircle dotClassName={dotClassName} style={circleStyle.value} />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,11 +1,22 @@
|
|||
import type { VNode, ExtractPropTypes, PropType } from 'vue';
|
||||
import { onBeforeUnmount, cloneVNode, isVNode, defineComponent, shallowRef, watch } from 'vue';
|
||||
import {
|
||||
onBeforeUnmount,
|
||||
cloneVNode,
|
||||
isVNode,
|
||||
defineComponent,
|
||||
shallowRef,
|
||||
watch,
|
||||
computed,
|
||||
} from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { filterEmpty, getPropsSlot } from '../_util/props-util';
|
||||
import initDefaultProps from '../_util/props-util/initDefaultProps';
|
||||
import useStyle from './style';
|
||||
import useConfigInject from '../config-provider/hooks/useConfigInject';
|
||||
import useCSSVarCls from '../config-provider/hooks/useCssVarCls';
|
||||
import Progress from './Progress';
|
||||
import usePercent from './usePercent';
|
||||
|
||||
export type SpinSize = 'small' | 'default' | 'large';
|
||||
export const spinProps = () => ({
|
||||
|
@ -16,6 +27,8 @@ export const spinProps = () => ({
|
|||
tip: PropTypes.any,
|
||||
delay: Number,
|
||||
indicator: PropTypes.any,
|
||||
fullscreen: Boolean,
|
||||
percent: [Number, String] as PropType<number | 'auto'>,
|
||||
});
|
||||
|
||||
export type SpinProps = Partial<ExtractPropTypes<ReturnType<typeof spinProps>>>;
|
||||
|
@ -40,11 +53,16 @@ export default defineComponent({
|
|||
size: 'default',
|
||||
spinning: true,
|
||||
wrapperClassName: '',
|
||||
fullscreen: false,
|
||||
}),
|
||||
setup(props, { attrs, slots }) {
|
||||
const { prefixCls, size, direction } = useConfigInject('spin', props);
|
||||
const [wrapSSR, hashId] = useStyle(prefixCls);
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
const sSpinning = shallowRef(props.spinning && !shouldDelay(props.spinning, props.delay));
|
||||
|
||||
const mergedPercent = computed(() => usePercent(sSpinning.value, props.percent));
|
||||
|
||||
let updateSpinning: any;
|
||||
watch(
|
||||
[() => props.spinning, () => props.delay],
|
||||
|
@ -63,6 +81,7 @@ export default defineComponent({
|
|||
onBeforeUnmount(() => {
|
||||
updateSpinning?.cancel();
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { class: cls, ...divProps } = attrs;
|
||||
const { tip = slots.tip?.() } = props;
|
||||
|
@ -78,8 +97,11 @@ export default defineComponent({
|
|||
[cls as string]: !!cls,
|
||||
};
|
||||
|
||||
function renderIndicator(prefixCls: string) {
|
||||
function renderIndicator(prefixCls: string, percent: number) {
|
||||
const dotClassName = `${prefixCls}-dot`;
|
||||
const holderClassName = `${dotClassName}-holder`;
|
||||
const hideClassName = `${holderClassName}-hidden`;
|
||||
|
||||
let indicator = getPropsSlot(slots, props, 'indicator');
|
||||
// should not be render default indicator when indicator value is null
|
||||
if (indicator === null) {
|
||||
|
@ -89,43 +111,87 @@ export default defineComponent({
|
|||
indicator = indicator.length === 1 ? indicator[0] : indicator;
|
||||
}
|
||||
if (isVNode(indicator)) {
|
||||
return cloneVNode(indicator, { class: dotClassName });
|
||||
return cloneVNode(indicator, { class: dotClassName, percent });
|
||||
}
|
||||
|
||||
if (defaultIndicator && isVNode(defaultIndicator())) {
|
||||
return cloneVNode(defaultIndicator(), { class: dotClassName });
|
||||
return cloneVNode(defaultIndicator(), { class: dotClassName, percent });
|
||||
}
|
||||
|
||||
return (
|
||||
<span class={`${dotClassName} ${prefixCls}-dot-spin`}>
|
||||
<i class={`${prefixCls}-dot-item`} />
|
||||
<i class={`${prefixCls}-dot-item`} />
|
||||
<i class={`${prefixCls}-dot-item`} />
|
||||
<i class={`${prefixCls}-dot-item`} />
|
||||
</span>
|
||||
<>
|
||||
<span class={[holderClassName, percent > 0 && hideClassName]}>
|
||||
<span class={[dotClassName, `${prefixCls}-dot-spin`]}>
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<i class={`${prefixCls}-dot-item`} key={i} />
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
{props.percent && <Progress prefixCls={prefixCls} percent={percent} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
const spinElement = (
|
||||
<div {...divProps} class={spinClassName} aria-live="polite" aria-busy={sSpinning.value}>
|
||||
{renderIndicator(prefixCls.value)}
|
||||
{tip ? <div class={`${prefixCls.value}-text`}>{tip}</div> : null}
|
||||
<div
|
||||
{...divProps}
|
||||
key="loading"
|
||||
class={[spinClassName, rootCls.value, cssVarCls.value]}
|
||||
aria-live="polite"
|
||||
aria-busy={sSpinning.value}
|
||||
>
|
||||
{renderIndicator(prefixCls.value, mergedPercent.value.value)}
|
||||
{tip ? (
|
||||
<div class={[`${prefixCls.value}-text`, hashId.value, rootCls.value, cssVarCls.value]}>
|
||||
{tip}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
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(
|
||||
<div class={[`${prefixCls.value}-nested-loading`, props.wrapperClassName, hashId.value]}>
|
||||
{sSpinning.value && <div key="loading">{spinElement}</div>}
|
||||
return wrapCSSVar(
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls.value}-nested-loading`,
|
||||
props.wrapperClassName,
|
||||
hashId.value,
|
||||
rootCls.value,
|
||||
cssVarCls.value,
|
||||
]}
|
||||
>
|
||||
{sSpinning.value && spinElement}
|
||||
<div class={containerClassName} key="container">
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
return wrapSSR(spinElement);
|
||||
|
||||
if (props.fullscreen) {
|
||||
return wrapCSSVar(
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls.value}-fullscreen`,
|
||||
{
|
||||
[`${prefixCls.value}-fullscreen-show`]: sSpinning.value,
|
||||
},
|
||||
hashId.value,
|
||||
rootCls.value,
|
||||
cssVarCls.value,
|
||||
]}
|
||||
>
|
||||
{spinElement}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return wrapCSSVar(spinElement);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,15 +17,38 @@ Use custom loading indicator.
|
|||
</docs>
|
||||
|
||||
<template>
|
||||
<a-spin :indicator="indicator" />
|
||||
<a-flex align="center" gap="middle">
|
||||
<a-spin :indicator="smallIndicator" />
|
||||
<a-spin :indicator="indicator" />
|
||||
<a-spin :indicator="largeIndicator" />
|
||||
<a-spin :indicator="customIndicator" />
|
||||
</a-flex>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { h } from 'vue';
|
||||
const smallIndicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '16px',
|
||||
},
|
||||
spin: true,
|
||||
});
|
||||
const indicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '24px',
|
||||
},
|
||||
spin: true,
|
||||
});
|
||||
const largeIndicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '36px',
|
||||
},
|
||||
spin: true,
|
||||
});
|
||||
const customIndicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '48px',
|
||||
},
|
||||
spin: true,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<docs>
|
||||
---
|
||||
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.
|
||||
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
<a-button @click="showLoader">Show fullscreen</a-button>
|
||||
<a-spin :spinning="spinning" :percent="percent" fullscreen />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from 'vue';
|
||||
|
||||
const spinning = ref(false);
|
||||
const percent = ref(0);
|
||||
let interval = null;
|
||||
|
||||
const showLoader = () => {
|
||||
spinning.value = true;
|
||||
let ptg = -10;
|
||||
|
||||
interval = setInterval(() => {
|
||||
ptg += 5;
|
||||
percent.value = ptg;
|
||||
|
||||
if (ptg > 120) {
|
||||
if (interval) clearInterval(interval);
|
||||
spinning.value = false;
|
||||
percent.value = 0;
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
});
|
||||
</script>
|
|
@ -7,6 +7,8 @@
|
|||
<tip />
|
||||
<delay />
|
||||
<custom-indicator />
|
||||
<fullscreen />
|
||||
<percent />
|
||||
</demo-sort>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
@ -16,6 +18,8 @@ import Inside from './inside.vue';
|
|||
import Nested from './nested.vue';
|
||||
import Tip from './tip.vue';
|
||||
import Delay from './delay.vue';
|
||||
import Fullscreen from './fullscreen.vue';
|
||||
import Percent from './percent.vue';
|
||||
import CustomIndicator from './custom-indicator.vue';
|
||||
import CN from '../index.zh-CN.md';
|
||||
import US from '../index.en-US.md';
|
||||
|
@ -31,6 +35,8 @@ export default defineComponent({
|
|||
Tip,
|
||||
Delay,
|
||||
CustomIndicator,
|
||||
Fullscreen,
|
||||
Percent,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<docs>
|
||||
---
|
||||
order: 7
|
||||
title:
|
||||
zh-CN: 进度
|
||||
en-US: progress
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
展示进度,当设置 `percent="auto"` 时会预估一个永远不会停止的进度条。
|
||||
|
||||
## en-US
|
||||
|
||||
Show the progress. When `percent="auto"` is set, an indeterminate progress will be displayed.
|
||||
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
<div style="display: flex; align-items: center; gap: 16px">
|
||||
<!-- 开关组件 -->
|
||||
<a-switch
|
||||
v-model:checked="auto"
|
||||
checked-children="Auto"
|
||||
un-checked-children="Auto"
|
||||
@change="toggleAuto"
|
||||
/>
|
||||
|
||||
<!-- 加载动画组件 -->
|
||||
<a-spin :percent="mergedPercent" size="small" />
|
||||
<a-spin :percent="mergedPercent" />
|
||||
<a-spin :percent="mergedPercent" size="large" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted, onMounted, getCurrentInstance } from 'vue';
|
||||
|
||||
const auto = ref(false); // 是否启用自动模式
|
||||
const percent = ref(-50); // 当前加载百分比
|
||||
let interval = null;
|
||||
|
||||
const mergedPercent = computed(() => (auto.value ? 'auto' : percent.value));
|
||||
|
||||
const toggleAuto = checked => {
|
||||
auto.value = checked;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
interval = setInterval(() => {
|
||||
percent.value = percent.value + 5;
|
||||
if (percent.value > 150) {
|
||||
percent.value = -50;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
|
@ -1,6 +1,6 @@
|
|||
<docs>
|
||||
---
|
||||
order: 4
|
||||
order: 4
|
||||
title:
|
||||
zh-CN: 自定义描述文案
|
||||
en-US: Customized description
|
||||
|
@ -17,10 +17,23 @@ Customized description content.
|
|||
</docs>
|
||||
|
||||
<template>
|
||||
<a-spin tip="Loading...">
|
||||
<a-alert
|
||||
message="Alert message title"
|
||||
description="Further details about the context of this alert."
|
||||
></a-alert>
|
||||
</a-spin>
|
||||
<a-flex gap="middle" vertical>
|
||||
<a-flex gap="middle">
|
||||
<a-spin tip="Loading" size="small">
|
||||
<div style="padding: 50px; background: rgba(0, 0, 0, 0.05); border-radius: 4px"></div>
|
||||
</a-spin>
|
||||
<a-spin tip="Loading">
|
||||
<div style="padding: 50px; background: rgba(0, 0, 0, 0.05); border-radius: 4px"></div>
|
||||
</a-spin>
|
||||
<a-spin tip="Loading" size="large">
|
||||
<div style="padding: 50px; background: rgba(0, 0, 0, 0.05); border-radius: 4px"></div>
|
||||
</a-spin>
|
||||
</a-flex>
|
||||
<a-spin tip="Loading...">
|
||||
<a-alert
|
||||
message="Alert message title"
|
||||
description="Further details about the context of this alert."
|
||||
></a-alert>
|
||||
</a-spin>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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<SpinToken> = (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}`,
|
||||
const genSpinStyle: GenerateStyle<SpinToken> = (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}`,
|
||||
|
||||
'&-spinning': {
|
||||
position: 'static',
|
||||
display: 'inline-block',
|
||||
opacity: 1,
|
||||
},
|
||||
'&-spinning': {
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
'&-nested-loading': {
|
||||
position: 'relative',
|
||||
[`> div > ${token.componentCls}`]: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
zIndex: 4,
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: token.contentHeight,
|
||||
[`${componentCls}-text`]: {
|
||||
fontSize: token.fontSize,
|
||||
paddingTop: calc(calc(token.dotSize).sub(token.fontSize)).div(2).add(2).equal(),
|
||||
},
|
||||
|
||||
[`${token.componentCls}-dot`]: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
insetInlineStart: '50%',
|
||||
margin: -token.spinDotSize / 2,
|
||||
'&-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',
|
||||
},
|
||||
|
||||
[`${token.componentCls}-text`]: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
width: '100%',
|
||||
paddingTop: (token.spinDotSize - token.fontSize) / 2 + 2,
|
||||
textShadow: `0 1px 2px ${token.colorBgContainer}`, // FIXME: shadow
|
||||
},
|
||||
|
||||
[`&${token.componentCls}-show-text ${token.componentCls}-dot`]: {
|
||||
marginTop: -(token.spinDotSize / 2) - 10,
|
||||
},
|
||||
|
||||
'&-sm': {
|
||||
[`${token.componentCls}-dot`]: {
|
||||
margin: -token.spinDotSizeSM / 2,
|
||||
[componentCls]: {
|
||||
[`${componentCls}-dot-holder`]: {
|
||||
color: token.colorWhite,
|
||||
},
|
||||
[`${token.componentCls}-text`]: {
|
||||
paddingTop: (token.spinDotSizeSM - token.fontSize) / 2 + 2,
|
||||
},
|
||||
[`&${token.componentCls}-show-text ${token.componentCls}-dot`]: {
|
||||
marginTop: -(token.spinDotSizeSM / 2) - 10,
|
||||
},
|
||||
},
|
||||
|
||||
'&-lg': {
|
||||
[`${token.componentCls}-dot`]: {
|
||||
margin: -(token.spinDotSizeLG / 2),
|
||||
},
|
||||
[`${token.componentCls}-text`]: {
|
||||
paddingTop: (token.spinDotSizeLG - token.fontSize) / 2 + 2,
|
||||
},
|
||||
[`&${token.componentCls}-show-text ${token.componentCls}-dot`]: {
|
||||
marginTop: -(token.spinDotSizeLG / 2) - 10,
|
||||
[`${componentCls}-text`]: {
|
||||
color: token.colorTextLightSolid,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[`${token.componentCls}-container`]: {
|
||||
'&-nested-loading': {
|
||||
position: 'relative',
|
||||
transition: `opacity ${token.motionDurationSlow}`,
|
||||
|
||||
'&::after': {
|
||||
[`> ${componentCls}`]: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineEnd: 0,
|
||||
bottom: 0,
|
||||
insetInlineStart: 0,
|
||||
zIndex: 10,
|
||||
zIndex: 4,
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: token.colorBgContainer,
|
||||
opacity: 0,
|
||||
transition: `all ${token.motionDurationSlow}`,
|
||||
content: '""',
|
||||
maxHeight: token.contentHeight,
|
||||
[`${componentCls}-dot`]: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
insetInlineStart: '50%',
|
||||
margin: calc(token.dotSize).mul(-1).div(2).equal(),
|
||||
},
|
||||
|
||||
[`${componentCls}-text`]: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
width: '100%',
|
||||
// paddingTop: (token.spinDotSize - token.fontSize) / 2 + 2,
|
||||
textShadow: `0 1px 2px ${token.colorBgContainer}`, // FIXME: shadow
|
||||
},
|
||||
|
||||
[`&${componentCls}-show-text ${componentCls}-dot`]: {
|
||||
marginTop: calc(token.dotSize).div(2).mul(-1).sub(10).equal(),
|
||||
},
|
||||
|
||||
'&-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(),
|
||||
},
|
||||
},
|
||||
|
||||
'&-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(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[`${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',
|
||||
},
|
||||
},
|
||||
|
||||
[`${componentCls}-blur`]: {
|
||||
clear: 'both',
|
||||
opacity: 0.5,
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
|
||||
[`&::after`]: {
|
||||
opacity: 0.4,
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[`${token.componentCls}-blur`]: {
|
||||
clear: 'both',
|
||||
opacity: 0.5,
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
|
||||
[`&::after`]: {
|
||||
opacity: 0.4,
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
// tip
|
||||
// ------------------------------
|
||||
[`&-tip`]: {
|
||||
color: token.spinDotDefault,
|
||||
},
|
||||
},
|
||||
|
||||
// tip
|
||||
// ------------------------------
|
||||
[`&-tip`]: {
|
||||
color: token.spinDotDefault,
|
||||
},
|
||||
|
||||
// dots
|
||||
// ------------------------------
|
||||
[`${token.componentCls}-dot`]: {
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
fontSize: token.spinDotSize,
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
|
||||
'&-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)',
|
||||
// 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%',
|
||||
opacity: 0.3,
|
||||
animationName: antSpinMove,
|
||||
animationDuration: '1s',
|
||||
animationIterationCount: 'infinite',
|
||||
animationTimingFunction: 'linear',
|
||||
animationDirection: 'alternate',
|
||||
lineHeight: 1,
|
||||
color: token.colorPrimary,
|
||||
|
||||
'&:nth-child(1)': {
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
},
|
||||
|
||||
'&: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',
|
||||
'&-hidden': {
|
||||
transform: 'scale(0.3)',
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'&-spin': {
|
||||
transform: 'rotate(45deg)',
|
||||
animationName: antRotate,
|
||||
animationDuration: '1.2s',
|
||||
animationIterationCount: 'infinite',
|
||||
animationTimingFunction: 'linear',
|
||||
// progress
|
||||
// ------------------------------
|
||||
[`${componentCls}-dot-progress`]: {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
},
|
||||
|
||||
// dots
|
||||
// ------------------------------
|
||||
[`${componentCls}-dot`]: {
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
fontSize: token.dotSize,
|
||||
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',
|
||||
|
||||
'&:nth-child(1)': {
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
},
|
||||
|
||||
'&: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',
|
||||
},
|
||||
},
|
||||
|
||||
'&-spin': {
|
||||
transform: 'rotate(45deg)',
|
||||
animationName: antRotate,
|
||||
animationDuration: '1.2s',
|
||||
animationIterationCount: 'infinite',
|
||||
animationTimingFunction: 'linear',
|
||||
},
|
||||
|
||||
'&-circle': {
|
||||
strokeLinecap: 'round',
|
||||
transition: ['stroke-dashoffset', 'stroke-dasharray', 'stroke', 'stroke-width', 'opacity']
|
||||
.map(item => `${item} ${token.motionDurationSlow} ease`)
|
||||
.join(','),
|
||||
fillOpacity: 0,
|
||||
stroke: 'currentcolor',
|
||||
},
|
||||
|
||||
'&-circle-bg': {
|
||||
stroke: token.colorFillSecondary,
|
||||
},
|
||||
},
|
||||
|
||||
// Sizes
|
||||
// ------------------------------
|
||||
|
||||
[`&-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 ${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(),
|
||||
},
|
||||
},
|
||||
|
||||
[`&${componentCls}-show-text ${componentCls}-text`]: {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Sizes
|
||||
// ------------------------------
|
||||
|
||||
// small
|
||||
[`&-sm ${token.componentCls}-dot`]: {
|
||||
fontSize: token.spinDotSizeSM,
|
||||
|
||||
i: {
|
||||
width: (token.spinDotSizeSM - token.marginXXS / 2) / 2,
|
||||
height: (token.spinDotSizeSM - token.marginXXS / 2) / 2,
|
||||
},
|
||||
},
|
||||
|
||||
// large
|
||||
[`&-lg ${token.componentCls}-dot`]: {
|
||||
fontSize: token.spinDotSizeLG,
|
||||
|
||||
i: {
|
||||
width: (token.spinDotSizeLG - token.marginXXS) / 2,
|
||||
height: (token.spinDotSizeLG - token.marginXXS) / 2,
|
||||
},
|
||||
},
|
||||
|
||||
[`&${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<SpinToken>(token, {
|
||||
spinDotDefault: token.colorTextDescription,
|
||||
spinDotSize: token.controlHeightLG / 2,
|
||||
spinDotSizeSM: token.controlHeightLG * 0.35,
|
||||
spinDotSizeLG: token.controlHeight,
|
||||
});
|
||||
return [genSpinStyle(spinToken)];
|
||||
},
|
||||
{
|
||||
contentHeight: 400,
|
||||
},
|
||||
prepareComponentToken,
|
||||
);
|
||||
|
|
|
@ -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<ReturnType<typeof setInterval> | 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;
|
||||
}
|
Loading…
Reference in New Issue