feat: 优化捐赠留言显示

This commit is contained in:
alger
2025-05-07 00:15:45 +08:00
parent 2f07550316
commit 3ca7e9a271
3 changed files with 195 additions and 230 deletions
+3 -1
View File
@@ -3,5 +3,7 @@ export default {
'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.', 'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.',
message: 'You can leave your email or github name when leaving a message.', message: 'You can leave your email or github name when leaving a message.',
refresh: 'Refresh List', refresh: 'Refresh List',
toDonateList: 'Buy me a coffee' toDonateList: 'Buy me a coffee',
title: 'Donation List',
noMessage: 'No Message'
}; };
+3 -1
View File
@@ -2,5 +2,7 @@ export default {
description: '您的捐赠将用于支持开发和维护工作,包括但不限于服务器维护、域名续费等。', description: '您的捐赠将用于支持开发和维护工作,包括但不限于服务器维护、域名续费等。',
message: '留言时可留下您的邮箱或 github名称。', message: '留言时可留下您的邮箱或 github名称。',
refresh: '刷新列表', refresh: '刷新列表',
toDonateList: '请我喝咖啡' toDonateList: '请我喝咖啡',
noMessage: '暂无留言',
title: '捐赠列表'
}; };
+189 -228
View File
@@ -1,6 +1,44 @@
<template> <template>
<div class="donation-container"> <div class="donation-container">
<div class="refresh-container"> <div class="qrcode-container">
<div class="description">
<p>{{ t('donation.description') }}</p>
<p>{{ t('donation.message') }}</p>
</div>
<div class="qrcode-grid">
<div class="qrcode-item">
<n-image
:src="alipay"
:alt="t('common.alipay')"
class="qrcode-image"
preview-disabled
/>
<span class="qrcode-label">{{ t('common.alipay') }}</span>
</div>
<div class="donate-button">
<n-button type="primary" @click="toDonateList">
<template #icon>
<i class="ri-cup-line"></i>
</template>
{{ t('donation.toDonateList') }}
</n-button>
</div>
<div class="qrcode-item">
<n-image
:src="wechat"
:alt="t('common.wechat')"
class="qrcode-image"
preview-disabled
/>
<span class="qrcode-label">{{ t('common.wechat') }}</span>
</div>
</div>
</div>
<div class="header-container">
<h3 class="section-title">{{ t('donation.title') }}</h3>
<n-button secondary round size="small" :loading="isLoading" @click="fetchDonors"> <n-button secondary round size="small" :loading="isLoading" @click="fetchDonors">
<template #icon> <template #icon>
<i class="ri-refresh-line"></i> <i class="ri-refresh-line"></i>
@@ -8,15 +46,13 @@
{{ t('donation.refresh') }} {{ t('donation.refresh') }}
</n-button> </n-button>
</div> </div>
<div class="donation-grid" :class="{ 'grid-expanded': isExpanded }"> <div class="donation-grid" :class="{ 'grid-expanded': isExpanded }">
<div <div
v-for="(donor, index) in displayDonors" v-for="donor in displayDonors"
:key="donor.id" :key="donor.id"
class="donation-card animate__animated" class="donation-card"
:class="getAnimationClass(index)" :class="{ 'no-message': !donor.message }"
:style="{ animationDelay: `${index * 0.1}s` }"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
> >
<div class="card-content"> <div class="card-content">
<div class="donor-avatar"> <div class="donor-avatar">
@@ -24,46 +60,45 @@
:src="donor.avatar" :src="donor.avatar"
:fallback-src="defaultAvatar" :fallback-src="defaultAvatar"
round round
size="large" class="avatar-img"
class="animate__animated animate__pulse animate__infinite avatar-img"
/> />
<div class="donor-badge" :class="getBadgeClass(donor.badge)">
{{ donor.badge }}
</div>
</div> </div>
<div class="donor-info"> <div class="donor-info">
<div class="donor-name">{{ donor.name }}</div> <div class="donor-meta">
<div class="donation-meta"> <div class="donor-name">{{ donor.name }}</div>
<n-tag <div class="price-tag">{{ donor.amount }}</div>
:type="getAmountTagType(donor.amount)"
size="small"
class="donation-amount animate__animated"
round
bordered
>
{{ donor.amount }}
</n-tag>
<span class="donation-date">{{ donor.date }}</span>
</div> </div>
<div class="donation-date">{{ donor.date }}</div>
</div> </div>
</div> </div>
<div v-if="donor.message" class="donation-message">
<n-popover trigger="hover" placement="bottom"> <!-- 有留言的情况 -->
<template #trigger> <n-popover
<div class="message-content"> v-if="donor.message"
<i class="ri-double-quotes-l quote-icon"></i> trigger="hover"
<div class="message-text">{{ donor.message }}</div> placement="bottom"
<i class="ri-double-quotes-r quote-icon"></i> :show-arrow="true"
</div> :width="240"
</template> >
<div class="message-popup"> <template #trigger>
<div class="donation-message">
<i class="ri-double-quotes-l quote-icon"></i> <i class="ri-double-quotes-l quote-icon"></i>
{{ donor.message }} <span class="message-text">{{ donor.message }}</span>
<i class="ri-double-quotes-r quote-icon"></i> <i class="ri-double-quotes-r quote-icon"></i>
</div> </div>
</n-popover> </template>
<div class="message-popover">
<i class="ri-double-quotes-l quote-icon"></i>
<span>{{ donor.message }}</span>
<i class="ri-double-quotes-r quote-icon"></i>
</div>
</n-popover>
<!-- 没有留言的情况显示占位符 -->
<div v-else class="donation-message-placeholder">
<i class="ri-emotion-line"></i>
<span>{{ t('donation.noMessage') }}</span>
</div> </div>
<div class="card-sparkles"></div>
</div> </div>
</div> </div>
@@ -75,40 +110,6 @@
{{ isExpanded ? t('common.collapse') : t('common.expand') }} {{ isExpanded ? t('common.collapse') : t('common.expand') }}
</n-button> </n-button>
</div> </div>
<div class="p-6 rounded-lg shadow-lg">
<div class="description text-center text-sm text-gray-700 dark:text-gray-200">
<p>{{ t('donation.description') }}</p>
<p>{{ t('donation.message') }}</p>
</div>
<div class="flex justify-between mt-6">
<div class="flex flex-col items-center gap-2">
<n-image
:src="alipay"
:alt="t('common.alipay')"
class="w-60 h-60 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.alipay') }}</span>
</div>
<n-button type="primary" @click="toDonateList">
<template #icon>
<i class="ri-cup-line"></i>
</template>
{{ t('donation.toDonateList') }}
<i class="ri-arrow-right-s-line"></i>
</n-button>
<div class="flex flex-col items-center gap-2">
<n-image
:src="wechat"
:alt="t('common.wechat')"
class="w-60 h-60 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.wechat') }}</span>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -152,72 +153,9 @@ onActivated(() => {
fetchDonors(); fetchDonors();
}); });
// 动画类名列表 // 只按金额排序的捐赠列表
const animationClasses = [
'animate__fadeInUp',
'animate__fadeInLeft',
'animate__fadeInRight',
'animate__zoomIn'
];
// 获取随机动画类名
const getAnimationClass = (index: number) => {
return animationClasses[index % animationClasses.length];
};
// 根据金额获取标签类型
const getAmountTagType = (amount: number): 'success' | 'warning' | 'error' | 'info' => {
if (amount >= 5) return 'warning';
if (amount >= 2) return 'success';
return 'info';
};
// 获取徽章样式类名
const getBadgeClass = (badge: string): string => {
if (badge.includes('金牌')) return 'badge-gold';
if (badge.includes('银牌')) return 'badge-silver';
return 'badge-bronze';
};
// 鼠标悬停效果
const handleMouseEnter = (event: MouseEvent) => {
const card = event.currentTarget as HTMLElement;
card.style.transform = 'translateY(-2px)';
card.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.12)';
// 添加金额标签动画
const amountTag = card.querySelector('.donation-amount');
if (amountTag) {
amountTag.classList.add('animate__tada');
}
};
const handleMouseLeave = (event: MouseEvent) => {
const card = event.currentTarget as HTMLElement;
card.style.transform = 'translateY(0)';
card.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.08)';
// 移除金额标签动画
const amountTag = card.querySelector('.donation-amount');
if (amountTag) {
amountTag.classList.remove('animate__tada');
}
};
// 按金额和留言排序的捐赠列表
const sortedDonors = computed(() => { const sortedDonors = computed(() => {
return [...donors.value].sort((a, b) => { return [...donors.value].sort((a, b) => b.amount - a.amount);
// 如果一个有留言一个没有,有留言的排在前面
if (a.message && !b.message) return -1;
if (!a.message && b.message) return 1;
// 都有留言或都没有留言时,按金额从大到小排序
const amountDiff = b.amount - a.amount;
if (amountDiff !== 0) return amountDiff;
// 金额相同时,按日期从新到旧排序
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
}); });
const isExpanded = ref(false); const isExpanded = ref(false);
@@ -240,13 +178,21 @@ const toDonateList = () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.donation-container { .donation-container {
@apply w-full overflow-hidden; @apply w-full overflow-hidden flex flex-col gap-4;
}
.header-container {
@apply flex justify-between items-center px-4 py-2;
.section-title {
@apply text-lg font-medium text-gray-700 dark:text-gray-200;
}
} }
.donation-grid { .donation-grid {
@apply grid gap-3 px-2 py-3 transition-all duration-300 overflow-hidden; @apply grid gap-3 transition-all duration-300 overflow-hidden;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
max-height: 280px; max-height: 320px;
@media (min-width: 768px) { @media (min-width: 768px) {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -262,127 +208,142 @@ const toDonateList = () => {
} }
.donation-card { .donation-card {
@apply relative rounded-lg p-3 min-w-0 w-full transition-all duration-500 shadow-md backdrop-blur-md; @apply rounded-lg p-2.5 transition-all duration-200 hover:shadow-md;
@apply bg-gradient-to-br from-white/[0.03] to-white/[0.08] border border-white/[0.08]; @apply bg-light-100 dark:bg-gray-800/5 backdrop-blur-sm;
@apply hover:shadow-lg; @apply border border-gray-200 dark:border-gray-700/10;
@apply flex flex-col justify-between;
min-height: 100px;
&.no-message {
@apply justify-between;
}
.card-content { .card-content {
@apply relative z-10 flex items-start gap-3; @apply flex items-start gap-2 mb-2;
} }
}
.donor-avatar { .donor-avatar {
@apply relative flex-shrink-0 w-10 h-9 transition-transform duration-300; @apply relative flex-shrink-0;
.avatar-img { .avatar-img {
@apply border border-white/20 dark:border-gray-800/50 shadow-sm; @apply border border-gray-200 dark:border-gray-700/10 shadow-sm;
@apply w-10 h-9; @apply w-9 h-9;
}
} }
}
.donor-badge { .donor-info {
@apply absolute -bottom-2 -right-1 px-1.5 py-0.5 text-xs font-medium text-white/90 rounded-full whitespace-nowrap; @apply flex-1 min-w-0 flex flex-col justify-center;
@apply bg-gradient-to-r from-pink-400 to-pink-500 shadow-sm opacity-90 scale-90;
@apply transition-all duration-300;
}
.donor-info {
@apply flex-1 min-w-0;
.donor-meta {
@apply flex justify-between items-center mb-0.5;
.donor-name { .donor-name {
@apply text-sm font-medium mb-0.5 truncate; @apply text-sm font-medium truncate flex-1 mr-1;
} }
.donation-meta { .price-tag {
@apply flex items-center gap-2 mb-1; @apply text-xs text-gray-400/80 dark:text-gray-500/80 whitespace-nowrap;
.donation-date {
@apply text-xs text-gray-400/80 dark:text-gray-500/80;
}
} }
} }
.donation-message { .donation-date {
@apply text-sm text-gray-600 dark:text-gray-300 leading-relaxed mt-3 w-full; @apply text-xs text-gray-400/60 dark:text-gray-500/60;
}
}
.message-content { .donation-message {
@apply relative p-2 rounded-lg transition-all duration-300 cursor-pointer; @apply text-xs text-gray-500 dark:text-gray-400 italic mt-1 px-2 py-1.5;
@apply bg-white/[0.02] hover:bg-[var(--n-color)]; @apply bg-gray-100/10 dark:bg-dark-300 rounded;
@apply flex items-start;
.message-text { @apply cursor-pointer transition-all duration-200;
@apply px-6 italic line-clamp-2;
} .quote-icon {
@apply text-gray-300 dark:text-gray-600 flex-shrink-0 opacity-60;
.quote-icon {
@apply absolute text-gray-400/60 dark:text-gray-500/60 text-sm; &:first-child {
@apply mr-1 self-start;
&:first-child {
@apply left-0.5 top-2;
}
&:last-child {
@apply right-0.5 bottom-2;
}
}
} }
&:last-child {
@apply ml-1 self-end;
}
}
.message-text {
@apply flex-1 line-clamp-2;
} }
&:hover { &:hover {
.donor-avatar { @apply bg-gray-100/40 dark:bg-dark-200;
@apply scale-105 rotate-3;
}
.donor-badge {
@apply scale-95 -translate-y-0.5;
}
.card-sparkles {
@apply opacity-60 scale-110;
}
} }
} }
.card-sparkles { .donation-message-placeholder {
@apply absolute inset-0 pointer-events-none opacity-0 transition-all duration-500; @apply text-xs text-gray-400 dark:text-gray-500 mt-1 px-2 py-1.5;
background-image: radial-gradient(2px 2px at 20px 30px, rgba(255, 255, 255, 0.95), transparent), @apply bg-gray-100/5 dark:bg-dark-300 rounded;
radial-gradient(2px 2px at 40px 70px, rgba(255, 255, 255, 0.95), transparent), @apply flex items-center justify-center gap-1 italic;
radial-gradient(2.5px 2.5px at 50px 160px, rgba(255, 255, 255, 0.95), transparent), @apply border border-transparent;
radial-gradient(2px 2px at 90px 40px, rgba(255, 255, 255, 0.95), transparent),
radial-gradient(2.5px 2.5px at 130px 80px, rgba(255, 255, 255, 0.95), transparent); i {
background-size: 200% 200%; @apply text-gray-300 dark:text-gray-600;
animation: sparkle 8s ease infinite;
}
@keyframes sparkle {
0%,
100% {
@apply bg-[0%_0%] opacity-40 scale-100;
}
50% {
@apply bg-[100%_100%] opacity-80 scale-110;
} }
} }
.refresh-container { .message-popover {
@apply flex justify-end px-2 py-2; @apply text-sm text-gray-700 dark:text-gray-200 italic p-2;
@apply flex items-start;
.quote-icon {
@apply text-gray-400 dark:text-gray-500 flex-shrink-0;
&:first-child {
@apply mr-1.5 self-start;
}
&:last-child {
@apply ml-1.5 self-end;
}
}
} }
.expand-button { .expand-button {
@apply flex justify-center items-center py-2; @apply flex justify-center items-center py-2;
:deep(.n-button) { :deep(.n-button) {
@apply transition-all duration-300 hover:-translate-y-0.5; @apply transition-all duration-200 hover:-translate-y-0.5;
} }
} }
.message-popup { .qrcode-container {
@apply relative px-4 py-2 text-sm; @apply p-5 rounded-lg shadow-sm bg-light-100 dark:bg-gray-800/5 backdrop-blur-sm border border-gray-200 dark:border-gray-700/10;
max-width: 300px;
line-height: 1.6; .description {
font-style: italic; @apply text-center text-sm text-gray-600 dark:text-gray-300 mb-4;
.quote-icon { p {
@apply text-gray-400/60 dark:text-gray-500/60; @apply mb-2;
font-size: 0.9rem; }
}
.qrcode-grid {
@apply flex justify-between items-center gap-4;
.qrcode-item {
@apply flex flex-col items-center gap-2;
.qrcode-image {
@apply w-36 h-36 rounded-lg border border-gray-200 dark:border-gray-700/10 shadow-sm transition-transform duration-200 hover:scale-105;
}
.qrcode-label {
@apply text-sm text-gray-600 dark:text-gray-300;
}
}
.donate-button {
@apply flex flex-col items-center justify-center;
}
} }
} }
</style> </style>