refactor: 整理组件目录结构
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
- TransactionDetail, CategoryBillPopup 移入 Transaction/ - BudgetTypeTabs 移入 Budget/ - GlassBottomNav, ModernEmpty 移入 Global/ - Icon, IconSelector, ClassifySelector 等 8 个通用组件移入 Common/ - 更新所有相关引用路径
This commit is contained in:
413
Web/src/components/Global/ModernEmpty.vue
Normal file
413
Web/src/components/Global/ModernEmpty.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user