1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

refactor(uh-data-loading): 重构数据加载组件,新增多状态优化与样式升级

1. 新增副文案支持与默认文案优化
2. 重构状态渲染逻辑,统一三态UI结构
3. 添加氛围动效:光晕呼吸、装饰点漂浮、差异化表情动画
4. 调整测试页与首页默认配置,关闭开发模式
This commit is contained in:
小莫唐尼
2026-09-04 03:11:54 +08:00
parent 5f22f4fa01
commit 3c84661d0e
3 changed files with 226 additions and 40 deletions
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import type { DataLoadingStatus } from '@/hooks/useDataLoading' import type { DataLoadingStatus } from '@/hooks/useDataLoading'
interface IProps { interface IProps {
@@ -9,76 +10,261 @@ interface IProps {
loadingText?: string loadingText?: string
errorText?: string errorText?: string
emptyText?: string emptyText?: string
/** 各态副文案(留空则不显示副行) */
loadingSubText?: string
errorSubText?: string
emptySubText?: string
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
loadingStatus: 'loading', loadingStatus: 'loading',
minHeight: '60vh', minHeight: '60vh',
loadingText: '稍等,正在努力加载中哦...', loadingText: '稍等,正在加载中哦...',
errorText: '哎呀,加载失败了呢~', errorText: '哎呀,加载失败了呢~',
emptyText: '啊偶,暂时没有数据呢~', emptyText: '啊偶,暂时没有数据呢~',
loadingSubText: '',
errorSubText: '请检查网络连接,或稍后再试',
emptySubText: '稍后再来看看吧~',
}) })
const emit = defineEmits<{ (e: 'refresh'): void }>() const emit = defineEmits<{ (e: 'refresh'): void }>()
const isLoading = computed(() => props.loadingStatus === 'loading')
const statusScene = computed(() => {
switch (props.loadingStatus) {
case 'error':
return {
icon: '-injury',
stageClass: 'stage-error',
mainTextClass: 'text-red-400',
mainText: props.errorText,
subText: props.errorSubText,
}
case 'empty':
return {
icon: '-confused',
stageClass: 'stage-empty',
mainTextClass: 'text-gray-600',
mainText: props.emptyText,
subText: props.emptySubText,
}
default:
return {
icon: '-happy-1',
stageClass: 'stage-loading',
mainTextClass: 'text-primary',
mainText: props.loadingText,
subText: props.loadingSubText,
}
}
})
</script> </script>
<template> <template>
<view <view
class="w-full flex flex-col items-center justify-center gap-y-4 text-sm" class="w-full flex flex-col items-center justify-center gap-y-8 text-sm"
:style="{ minHeight: props.minHeight }" :style="{ minHeight: props.minHeight }"
> >
<!-- 加载中 --> <!-- 状态舞台:光晕 + 漂浮装饰点 + 毛玻璃表情珠 -->
<template v-if="props.loadingStatus === 'loading'"> <view class="scene relative h-[250rpx] w-[250rpx] flex items-center justify-center" :class="statusScene.stageClass">
<!-- 表情上下漂浮动画 --> <view class="glow absolute inset-0 m-auto h-[220rpx] w-[220rpx] rounded-full" />
<text class="bob-icon"> <view class="deco-dot dot-a absolute rounded-full" />
<wd-icon class-prefix="uhemoji-icon" name="-happy-1" size="100rpx" /> <view class="deco-dot dot-b absolute rounded-full" />
</text> <view class="bubble relative h-[150rpx] w-[150rpx] flex items-center justify-center rounded-full">
<view class="text-primary"> <text class="bubble-icon">
{{ props.loadingText }} <wd-icon class-prefix="uhemoji-icon" :name="statusScene.icon" size="120rpx" />
</text>
</view> </view>
</template> </view>
<!-- 加载错误(可重试) --> <!-- 文案区 -->
<template v-else-if="props.loadingStatus === 'error'"> <view class="flex flex-col items-center">
<!-- 表情上下漂浮动画 --> <view class="flex items-center justify-center text-[28rpx] font-bold" :class="statusScene.mainTextClass">
<text class="bob-icon"> <text>{{ statusScene.mainText }}</text>
<wd-icon class-prefix="uhemoji-icon" name="-injury" size="100rpx" /> <!-- 加载中三点跳动 -->
</text> <view v-if="isLoading" class="ml-3 flex items-end gap-1">
<view class="text-red-400"> <view v-for="n in 3" :key="n" class="typing-dot" />
{{ props.errorText }} </view>
</view> </view>
<uh-button @click="emit('refresh')"> <text v-if="statusScene.subText" class="mt-3 text-[24rpx] text-gray-400">
{{ statusScene.subText }}
</text>
<uh-button class="mt-5" @click="emit('refresh')">
刷新试试 刷新试试
</uh-button> </uh-button>
</template> </view>
<!-- 无数据 -->
<template v-else>
<!-- 表情上下漂浮动画 -->
<text class="bob-icon">
<wd-icon class-prefix="uhemoji-icon" name="-confused" size="120rpx" />
</text>
<view class="text-gray-900">
{{ props.emptyText }}
</view>
</template>
</view> </view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/* 状态表情上下漂浮动画:@keyframes 无法用 UnoCSS / wot-ui 原子类表达保留 scoped 样式 */ /* 氛围化状态占位:keyframes/状态配色无法用原子类表达,保留 scoped 样式 */
@keyframes bob-icon-float {
/* —— 毛玻璃表情珠(三态共用,缓慢上下漂浮) —— */
.bubble {
animation: bubble-float 2s ease-in-out infinite;
}
.bubble-icon {
display: inline-block;
}
/* —— 光晕(状态色,缓慢呼吸) —— */
.glow {
animation: glow-pulse 2.4s ease-in-out infinite;
}
.stage-loading .glow {
background: rgba(185, 228, 36, 0.32);
}
.stage-error .glow {
background: rgba(248, 113, 113, 0.24);
}
.stage-empty .glow {
background: rgba(217, 249, 157, 0.5);
}
/* —— 漂浮装饰点(品牌双色,错峰漂浮) —— */
.deco-dot {
animation: deco-float 2s ease-in-out infinite;
}
.dot-a {
top: 16rpx;
left: 10rpx;
width: 22rpx;
height: 22rpx;
background: rgba(163, 230, 53, 0.9);
}
.dot-b {
top: 4rpx;
right: 14rpx;
width: 14rpx;
height: 14rpx;
background: rgba(217, 249, 157, 0.95);
animation-delay: -0.7s;
}
/* —— 三态表情差异化动效(均无限循环,柔和不抢眼) —— */
.stage-loading .bubble-icon {
animation: sway 1.6s ease-in-out infinite;
}
.stage-error .bubble-icon {
animation: head-shake 2.8s ease-in-out infinite;
transform-origin: 50% 85%;
}
.stage-empty .bubble-icon {
animation: sigh 3s ease-in-out infinite;
transform-origin: 50% 85%;
}
/* —— 加载中三点跳动 —— */
.typing-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: rgba(163, 230, 53, 0.95);
animation: dot-jump 1s ease-in-out infinite;
&:nth-child(2) {
animation-delay: 0.15s;
}
&:nth-child(3) {
animation-delay: 0.3s;
}
}
/* —— keyframes —— */
@keyframes bubble-float {
0%, 0%,
100% { 100% {
transform: translateY(0); transform: translateY(0);
} }
50% { 50% {
transform: translateY(-18rpx); transform: translateY(-14rpx);
} }
} }
.bob-icon { @keyframes glow-pulse {
display: inline-block; 0%,
animation: bob-icon-float 2s ease-in-out infinite; 100% {
transform: scale(1);
opacity: 0.55;
}
50% {
transform: scale(1.1);
opacity: 0.9;
}
}
@keyframes deco-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-12rpx);
}
}
@keyframes dot-jump {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8rpx);
}
}
/* 歪头左右打量(loading) */
@keyframes sway {
0%,
100% {
transform: rotate(-4deg);
}
50% {
transform: rotate(4deg);
}
}
/* 缓慢左右摇头(error) */
@keyframes head-shake {
0%,
100% {
transform: rotate(0);
}
25% {
transform: rotate(6deg);
}
75% {
transform: rotate(-6deg);
}
}
/* 轻轻叹气缩肩(empty) */
@keyframes sigh {
0%,
100% {
transform: scaleY(1);
}
30%,
70% {
transform: scaleY(0.92);
}
} }
</style> </style>
+1 -1
View File
@@ -9,6 +9,6 @@
<template> <template>
<view class="w-full h-screen"> <view class="w-full h-screen">
<uh-data-loading :loading-status="DataLoadingStatusEnum.Empty"></uh-data-loading> <uh-data-loading :loading-status="DataLoadingStatusEnum.Loading"></uh-data-loading>
</view> </view>
</template> </template>
+1 -1
View File
@@ -26,7 +26,7 @@ const homePagePath = '/pages/tabbar/home/home'
const articleDetailPath = '/pages-blog/article-detail/article-detail' const articleDetailPath = '/pages-blog/article-detail/article-detail'
// 本地开发快速跳转页面,发布请置为 false // 本地开发快速跳转页面,发布请置为 false
const DEV_MODE = true const DEV_MODE = false
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar' const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
const DEV_TO_PATH = `/pages-blog/test/test` const DEV_TO_PATH = `/pages-blog/test/test`