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:
@@ -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" />
|
||||||
|
<view class="bubble relative h-[150rpx] w-[150rpx] flex items-center justify-center rounded-full">
|
||||||
|
<text class="bubble-icon">
|
||||||
|
<wd-icon class-prefix="uhemoji-icon" :name="statusScene.icon" size="120rpx" />
|
||||||
</text>
|
</text>
|
||||||
<view class="text-primary">
|
|
||||||
{{ props.loadingText }}
|
|
||||||
</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>
|
||||||
<uh-button @click="emit('refresh')">
|
</view>
|
||||||
|
<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>
|
|
||||||
|
|
||||||
<!-- 无数据 -->
|
|
||||||
<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>
|
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user