Files
EmailBill/Web/src/components/ModernEmpty.vue

346 lines
7.6 KiB
Vue
Raw Normal View History

2026-02-09 19:25:51 +08:00
<template>
<div
class="modern-empty"
:class="[`theme-${theme}`, `size-${size}`]"
>
<div class="empty-content">
<!-- 图标容器 -->
<div
class="icon-container"
:class="{ 'with-animation': animation }"
>
<div class="icon-bg" />
<div class="icon-wrapper">
<!-- 自定义图标插槽 -->
<slot name="icon">
<svg
class="empty-icon"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<component :is="iconPath" />
</svg>
</slot>
</div>
</div>
<!-- 文字内容 -->
<div class="text-content">
<h3
v-if="title"
class="empty-title"
>
{{ title }}
</h3>
<p
v-if="description"
class="empty-description"
>
{{ description }}
</p>
</div>
<!-- 操作按钮 -->
<div
v-if="$slots.action || actionText"
class="empty-action"
>
<slot name="action">
<van-button
v-if="actionText"
type="primary"
round
size="small"
@click="$emit('action-click')"
>
{{ actionText }}
</van-button>
</slot>
</div>
</div>
</div>
</template>
<script setup>
import { computed, h } from 'vue'
const props = defineProps({
// 空状态类型search, data, inbox, calendar, finance, chart
type: {
type: String,
default: 'search'
},
// 主题色blue, green, orange, purple, gray
theme: {
type: String,
default: 'blue'
},
// 标题
title: {
type: String,
default: ''
},
// 描述文字
description: {
type: String,
default: '暂无数据'
},
// 是否显示动画
animation: {
type: Boolean,
default: true
},
// 操作按钮文字
actionText: {
type: String,
default: ''
},
// 尺寸small, medium, large
size: {
type: String,
default: 'medium'
}
})
defineEmits(['action-click'])
// 根据类型选择SVG图标路径
const iconPath = computed(() => {
const icons = {
search: () => h('g', [
h('circle', { cx: '26', cy: '26', r: '18', stroke: 'currentColor', 'stroke-width': '3', fill: 'none' }),
h('path', { d: 'M40 40L54 54', stroke: 'currentColor', 'stroke-width': '3', 'stroke-linecap': 'round' })
]),
data: () => h('g', [
h('path', { d: 'M8 48L22 32L36 40L56 16', stroke: 'currentColor', 'stroke-width': '3', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', fill: 'none' }),
h('circle', { cx: '8', cy: '48', r: '3', fill: 'currentColor' }),
h('circle', { cx: '22', cy: '32', r: '3', fill: 'currentColor' }),
h('circle', { cx: '36', cy: '40', r: '3', fill: 'currentColor' }),
h('circle', { cx: '56', cy: '16', r: '3', fill: 'currentColor' })
]),
inbox: () => h('g', [
h('path', { d: 'M8 16L32 4L56 16V52C56 54.2 54.2 56 52 56H12C9.8 56 8 54.2 8 52V16Z', stroke: 'currentColor', 'stroke-width': '3', fill: 'none' }),
h('path', { d: 'M8 32H20L24 40H40L44 32H56', stroke: 'currentColor', 'stroke-width': '3', fill: 'none' })
]),
calendar: () => h('g', [
h('rect', { x: '8', y: '12', width: '48', height: '44', rx: '4', stroke: 'currentColor', 'stroke-width': '3', fill: 'none' }),
h('path', { d: 'M8 24H56', stroke: 'currentColor', 'stroke-width': '3' }),
h('path', { d: 'M20 8V16', stroke: 'currentColor', 'stroke-width': '3', 'stroke-linecap': 'round' }),
h('path', { d: 'M44 8V16', stroke: 'currentColor', 'stroke-width': '3', 'stroke-linecap': 'round' })
]),
finance: () => h('g', [
h('circle', { cx: '32', cy: '32', r: '24', stroke: 'currentColor', 'stroke-width': '3', fill: 'none' }),
h('path', { d: 'M32 16V48', stroke: 'currentColor', 'stroke-width': '3' }),
h('path', { d: 'M24 22H36C38.2 22 40 23.8 40 26C40 28.2 38.2 30 36 30H28C25.8 30 24 31.8 24 34C24 36.2 25.8 38 28 38H40', stroke: 'currentColor', 'stroke-width': '3', fill: 'none' })
]),
chart: () => h('g', [
h('rect', { x: '12', y: '36', width: '8', height: '20', rx: '2', fill: 'currentColor' }),
h('rect', { x: '28', y: '24', width: '8', height: '32', rx: '2', fill: 'currentColor' }),
h('rect', { x: '44', y: '12', width: '8', height: '44', rx: '2', fill: 'currentColor' })
])
}
return icons[props.type] || icons.search
})
</script>
<style scoped lang="scss">
.modern-empty {
width: 100%;
padding: 40px 20px;
display: flex;
justify-content: center;
align-items: center;
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
max-width: 300px;
}
// 图标容器
.icon-container {
position: relative;
width: 120px;
height: 120px;
margin-bottom: 24px;
&.with-animation {
.icon-bg {
animation: pulse 2s ease-in-out infinite;
}
.icon-wrapper {
animation: float 3s ease-in-out infinite;
}
}
.icon-bg {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
opacity: 0.1;
}
.icon-wrapper {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.empty-icon {
width: 56px;
height: 56px;
opacity: 0.6;
}
}
}
// 文字内容
.text-content {
text-align: center;
margin-bottom: 20px;
.empty-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 8px;
line-height: 1.4;
}
.empty-description {
font-size: 14px;
color: var(--van-text-color-2);
margin: 0;
line-height: 1.6;
}
}
// 操作按钮
.empty-action {
margin-top: 4px;
}
// 主题色
&.theme-blue {
.icon-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.empty-icon {
color: #667eea;
}
}
&.theme-green {
.icon-bg {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.empty-icon {
color: #11998e;
}
}
&.theme-orange {
.icon-bg {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.empty-icon {
color: #f5576c;
}
}
&.theme-purple {
.icon-bg {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.empty-icon {
color: #4facfe;
}
}
&.theme-gray {
.icon-bg {
background: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%);
}
.empty-icon {
color: #95a5a6;
}
}
}
// 动画
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.1;
}
50% {
transform: scale(1.05);
opacity: 0.15;
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
// 尺寸变体
.modern-empty.size-small {
padding: 24px 16px;
.icon-container {
width: 80px;
height: 80px;
margin-bottom: 16px;
.empty-icon {
width: 40px;
height: 40px;
}
}
.text-content {
.empty-title {
font-size: 16px;
}
.empty-description {
font-size: 13px;
}
}
}
.modern-empty.size-large {
padding: 60px 20px;
.icon-container {
width: 160px;
height: 160px;
margin-bottom: 32px;
.empty-icon {
width: 72px;
height: 72px;
}
}
.text-content {
.empty-title {
font-size: 20px;
}
.empty-description {
font-size: 15px;
}
}
}
</style>