185 lines
4.1 KiB
Vue
185 lines
4.1 KiB
Vue
<template>
|
||
<view class="empty-state" :class="['type-' + type, { compact: compact }]" :style="wrapperStyle">
|
||
<view class="empty-illustration" :style="illustrationStyle">
|
||
<!-- 圆形光晕背景 -->
|
||
<view class="illustration-halo" :style="haloStyle"></view>
|
||
<!-- emoji 图标(微信小程序兼容方案) -->
|
||
<text class="illustration-emoji">{{ emojiIcon }}</text>
|
||
</view>
|
||
|
||
<view v-if="title" class="empty-title">{{ title }}</view>
|
||
<view v-if="desc" class="empty-desc">{{ desc }}</view>
|
||
|
||
<view v-if="actionText" class="empty-action btn btn--primary btn--pill" @tap="onAction">
|
||
<text class="action-text">{{ actionText }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import themeMixin from '@/mixins/themeMixin.js'
|
||
import { THEME_CHANGE_EVENT } from '@/utils/theme'
|
||
|
||
// 类型→emoji 映射(微信小程序不支持 inline SVG,使用 emoji 替代)
|
||
const EMOJI_MAP = {
|
||
'default': '📂',
|
||
'no-data': '📂',
|
||
'no-network': '📡',
|
||
'no-search': '🔍',
|
||
'mood': '📅',
|
||
'bill': '💰',
|
||
'todo': '📋',
|
||
'review': '✍️',
|
||
'growth': '🏆',
|
||
'error': '⚠️'
|
||
}
|
||
|
||
export default {
|
||
name: 'EmptyState',
|
||
mixins: [themeMixin],
|
||
props: {
|
||
// 缺省类型:default / no-data / no-network / no-search / mood / bill / todo / review / growth / error
|
||
type: { type: String, default: 'default' },
|
||
title: { type: String, default: '暂无数据' },
|
||
desc: { type: String, default: '' },
|
||
actionText: { type: String, default: '' },
|
||
compact: { type: Boolean, default: false }
|
||
},
|
||
computed: {
|
||
emojiIcon() {
|
||
return EMOJI_MAP[this.type] || EMOJI_MAP['default']
|
||
},
|
||
wrapperStyle() {
|
||
return {
|
||
backgroundColor: this.themeConfig.bgColor
|
||
}
|
||
},
|
||
illustrationStyle() {
|
||
return {}
|
||
},
|
||
haloStyle() {
|
||
const primary = this.themeConfig.primaryColor
|
||
return {
|
||
background: `radial-gradient(circle, ${primary}33 0%, ${primary}0D 70%, transparent 100%)`
|
||
}
|
||
}
|
||
},
|
||
created() {
|
||
// 组件级钩子:EmptyState 不是 page,themeMixin 的 onLoad/onShow 不会触发
|
||
// 这里手动监听主题变更事件,实时同步主题色
|
||
if (typeof this.syncThemeConfig === 'function') {
|
||
this.syncThemeConfig()
|
||
}
|
||
if (typeof this.watchThemeChange === 'function') {
|
||
this.watchThemeChange()
|
||
}
|
||
},
|
||
beforeUnmount() {
|
||
// 组件销毁时清理监听(避免内存泄漏)
|
||
if (this.themeChangeHandler) {
|
||
uni.$off && uni.$off(THEME_CHANGE_EVENT, this.themeChangeHandler)
|
||
}
|
||
},
|
||
methods: {
|
||
onAction() {
|
||
this.$emit('action')
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/uni.scss';
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
&.compact {
|
||
padding: 40rpx 32rpx 32rpx;
|
||
}
|
||
}
|
||
|
||
.empty-illustration {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 16rpx;
|
||
animation: emptyFloat 4s ease-in-out infinite;
|
||
position: relative;
|
||
}
|
||
|
||
/* 圆形光晕背景 */
|
||
.illustration-halo {
|
||
position: absolute;
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 50%;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
/* emoji 图标 */
|
||
.illustration-emoji {
|
||
position: relative;
|
||
z-index: 1;
|
||
font-size: 50rpx;
|
||
line-height: 1;
|
||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.compact .empty-illustration {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
margin-bottom: 8rpx;
|
||
|
||
.illustration-halo {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
}
|
||
|
||
.illustration-emoji {
|
||
font-size: 72rpx;
|
||
}
|
||
}
|
||
|
||
@keyframes emptyFloat {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-6rpx); }
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: v-bind('themeConfig.textPrimary');
|
||
margin-top: 12rpx;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
|
||
.empty-desc {
|
||
font-size: 24rpx;
|
||
color: v-bind('themeConfig.textSecondary');
|
||
margin-top: 12rpx;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
max-width: 480rpx;
|
||
}
|
||
|
||
.empty-action {
|
||
margin-top: 36rpx;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 28rpx;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
</style>
|