u
This commit is contained in:
1 parent
f572dce2f6
commit
e99a9fb274
356 files changed
+28877
-1055
No files matched your search
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, useSlots, watch } from 'vue'
|
||||
import { Loader } from '@lucide/vue'
|
||||
import { useLocale } from '../_composables/useLocale'
|
||||
import { useNamespace } from '../_composables/useNamespace'
|
||||
import type { SpinProps } from './types'
|
||||
|
||||
/**
|
||||
* 加载中 —— 两种形态:
|
||||
* - 无默认插槽:独立指示器(inline-flex),可带文案;
|
||||
* - 有默认插槽:包裹内容并叠加半透明遮罩,容器仅 position: relative,不改变内容布局。
|
||||
*/
|
||||
defineOptions({ name: 'G3Spin' })
|
||||
|
||||
const props = withDefaults(defineProps<SpinProps>(), {
|
||||
spinning: true,
|
||||
delay: 0,
|
||||
description: '',
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const { b, e, m, is } = useNamespace('spin')
|
||||
const locale = useLocale()
|
||||
|
||||
/** 实际显示状态:spinning 为 true 且 delay > 0 时,delay 毫秒后才置 true */
|
||||
const active = ref(false)
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function syncActive() {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
if (!props.spinning) {
|
||||
active.value = false
|
||||
return
|
||||
}
|
||||
if (props.delay > 0) {
|
||||
timer = setTimeout(() => {
|
||||
active.value = true
|
||||
}, props.delay)
|
||||
} else {
|
||||
active.value = true
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [props.spinning, props.delay], syncActive, { immediate: true })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearTimeout(timer)
|
||||
})
|
||||
|
||||
/** 是否有默认插槽:有则「包裹内容 + 遮罩」,否则「独立指示器」 */
|
||||
const nested = computed(() => !!slots.default && slots.default().length > 0)
|
||||
const hasDescription = computed(() => !!props.description || !!slots.description)
|
||||
|
||||
const classes = computed(() => [
|
||||
b(),
|
||||
{
|
||||
[m('nested')]: nested.value,
|
||||
[is('spinning')]: active.value,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes" :aria-busy="active" aria-live="polite">
|
||||
<!-- 形态二:内容容器,仅 position: relative,不改变内部元素位置 / 尺寸 -->
|
||||
<div v-if="nested" :class="e('container')">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<template v-if="active">
|
||||
<!-- 形态一:指示器区直接落在根节点(inline-flex) -->
|
||||
<template v-if="!nested">
|
||||
<slot name="indicator">
|
||||
<Loader :class="e('indicator')" aria-hidden="true" />
|
||||
</slot>
|
||||
<div
|
||||
v-if="hasDescription"
|
||||
:class="e('description')"
|
||||
role="status"
|
||||
:aria-label="locale.common.loading"
|
||||
>
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 形态二:绝对居中于遮罩之上 -->
|
||||
<div v-else :class="e('section')" role="status" :aria-label="locale.common.loading">
|
||||
<slot name="indicator">
|
||||
<Loader :class="e('indicator')" aria-hidden="true" />
|
||||
</slot>
|
||||
<div v-if="hasDescription" :class="e('description')">
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@g3soft/tokens/scss' as *;
|
||||
|
||||
.#{b('spin')} {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: v('spin-description-gap', 8px);
|
||||
color: v('color-primary', #18181b);
|
||||
}
|
||||
|
||||
.#{m('spin', 'nested')} {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.#{e('spin', 'container')} {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 包裹形态:内容上盖一层半透明遮罩,视觉上「进行中」 */
|
||||
.#{b('spin')}.#{m('spin', 'nested')}.#{is('spinning')}::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: color-mix(in srgb, #{v('bg-container', #fff)} 65%, transparent);
|
||||
}
|
||||
|
||||
.#{e('spin', 'section')} {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: v('spin-description-gap', 8px);
|
||||
}
|
||||
|
||||
.#{e('spin', 'indicator')} {
|
||||
/* 块级:svg 默认行内会让容器多出行高间隙(20px 的图标把盒子撑到 24px) */
|
||||
display: block;
|
||||
width: v('spin-size', 20px);
|
||||
height: v('spin-size', 20px);
|
||||
animation: #{b('spin')}-rotate 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes #{b('spin')}-rotate {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.#{e('spin', 'description')} {
|
||||
color: v('text-2', rgba(0, 0, 0, 0.65));
|
||||
font-size: v('font-size-sm', 12px);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.#{e('spin', 'indicator')} {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as G3Spin } from './Spin.vue'
|
||||
export type { SpinProps } from './types'
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface SpinProps {
|
||||
/** 是否加载中 */
|
||||
spinning?: boolean
|
||||
/** 延迟显示(毫秒),防短请求闪烁 */
|
||||
delay?: number
|
||||
/** 加载文案 */
|
||||
description?: string
|
||||
}
|
||||
Reference in new issue
Block a user