2025-01-11 18:22:14 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="donation-container">
|
2025-01-12 01:25:39 +08:00
|
|
|
<div class="refresh-container">
|
|
|
|
|
<n-button secondary round size="small" :loading="isLoading" @click="fetchDonors">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<i class="ri-refresh-line"></i>
|
|
|
|
|
</template>
|
2025-02-19 01:01:43 +08:00
|
|
|
{{ t('donation.refresh') }}
|
2025-01-12 01:25:39 +08:00
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="donation-grid" :class="{ 'grid-expanded': isExpanded }">
|
2025-01-11 18:22:14 +08:00
|
|
|
<div
|
2025-01-12 01:25:39 +08:00
|
|
|
v-for="(donor, index) in displayDonors"
|
2025-01-11 18:22:14 +08:00
|
|
|
:key="donor.id"
|
|
|
|
|
class="donation-card animate__animated"
|
|
|
|
|
:class="getAnimationClass(index)"
|
|
|
|
|
:style="{ animationDelay: `${index * 0.1}s` }"
|
|
|
|
|
@mouseenter="handleMouseEnter"
|
|
|
|
|
@mouseleave="handleMouseLeave"
|
|
|
|
|
>
|
|
|
|
|
<div class="card-content">
|
|
|
|
|
<div class="donor-avatar">
|
|
|
|
|
<n-avatar
|
|
|
|
|
:src="donor.avatar"
|
|
|
|
|
:fallback-src="defaultAvatar"
|
|
|
|
|
round
|
|
|
|
|
size="large"
|
|
|
|
|
class="animate__animated animate__pulse animate__infinite avatar-img"
|
|
|
|
|
/>
|
|
|
|
|
<div class="donor-badge" :class="getBadgeClass(donor.badge)">
|
|
|
|
|
{{ donor.badge }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="donor-info">
|
|
|
|
|
<div class="donor-name">{{ donor.name }}</div>
|
|
|
|
|
<div class="donation-meta">
|
|
|
|
|
<n-tag
|
|
|
|
|
: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>
|
2025-01-12 12:38:51 +08:00
|
|
|
<div v-if="donor.message" class="donation-message">
|
|
|
|
|
<n-popover trigger="hover" placement="bottom">
|
|
|
|
|
<template #trigger>
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
<i class="ri-double-quotes-l quote-icon"></i>
|
|
|
|
|
<div class="message-text">{{ donor.message }}</div>
|
|
|
|
|
<i class="ri-double-quotes-r quote-icon"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="message-popup">
|
|
|
|
|
<i class="ri-double-quotes-l quote-icon"></i>
|
|
|
|
|
{{ donor.message }}
|
|
|
|
|
<i class="ri-double-quotes-r quote-icon"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</n-popover>
|
|
|
|
|
</div>
|
2025-01-11 18:22:14 +08:00
|
|
|
<div class="card-sparkles"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
<div v-if="sortedDonors.length > 8" class="expand-button">
|
|
|
|
|
<n-button text @click="toggleExpand">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<i :class="isExpanded ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'"></i>
|
|
|
|
|
</template>
|
2025-02-19 01:01:43 +08:00
|
|
|
{{ isExpanded ? t('common.collapse') : t('common.expand') }}
|
2025-01-12 01:25:39 +08:00
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-01-11 18:22:14 +08:00
|
|
|
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
|
2025-01-13 23:08:47 +08:00
|
|
|
<div class="description text-center text-sm text-gray-700 dark:text-gray-200">
|
2025-02-19 01:01:43 +08:00
|
|
|
<p>{{ t('donation.description') }}</p>
|
|
|
|
|
<p>{{ t('donation.message') }}</p>
|
2025-01-13 23:08:47 +08:00
|
|
|
</div>
|
2025-01-11 18:22:14 +08:00
|
|
|
<div class="flex justify-between">
|
|
|
|
|
<div class="flex flex-col items-center gap-2">
|
|
|
|
|
<n-image
|
|
|
|
|
:src="alipay"
|
2025-02-19 01:01:43 +08:00
|
|
|
:alt="t('common.alipay')"
|
2025-01-11 18:22:14 +08:00
|
|
|
class="w-60 h-60 rounded-lg cursor-none"
|
|
|
|
|
preview-disabled
|
|
|
|
|
/>
|
2025-02-19 01:01:43 +08:00
|
|
|
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.alipay') }}</span>
|
2025-01-11 18:22:14 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="flex flex-col items-center gap-2">
|
|
|
|
|
<n-image
|
|
|
|
|
:src="wechat"
|
2025-02-19 01:01:43 +08:00
|
|
|
:alt="t('common.wechat')"
|
2025-01-11 18:22:14 +08:00
|
|
|
class="w-60 h-60 rounded-lg cursor-none"
|
|
|
|
|
preview-disabled
|
|
|
|
|
/>
|
2025-02-19 01:01:43 +08:00
|
|
|
<span class="text-sm text-gray-700 dark:text-gray-200">{{ t('common.wechat') }}</span>
|
2025-01-11 18:22:14 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-01-12 01:25:39 +08:00
|
|
|
import { computed, onActivated, onMounted, ref } from 'vue';
|
2025-02-19 01:01:43 +08:00
|
|
|
import { useI18n } from 'vue-i18n';
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-02-12 10:12:20 +08:00
|
|
|
import type { Donor } from '@/api/donation';
|
|
|
|
|
import { getDonationList } from '@/api/donation';
|
2025-01-11 18:22:14 +08:00
|
|
|
import alipay from '@/assets/alipay.png';
|
|
|
|
|
import wechat from '@/assets/wechat.png';
|
|
|
|
|
|
2025-02-19 01:01:43 +08:00
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
2025-01-11 18:22:14 +08:00
|
|
|
// 默认头像
|
|
|
|
|
const defaultAvatar = 'https://avatars.githubusercontent.com/u/0?v=4';
|
|
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
const donors = ref<Donor[]>([]);
|
|
|
|
|
const isLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
const fetchDonors = async () => {
|
|
|
|
|
isLoading.value = true;
|
2025-01-11 18:22:14 +08:00
|
|
|
try {
|
2025-02-12 10:12:20 +08:00
|
|
|
const data = await getDonationList();
|
|
|
|
|
donors.value = data.map((donor, index) => ({
|
2025-01-11 18:22:14 +08:00
|
|
|
...donor,
|
2025-01-12 12:38:51 +08:00
|
|
|
avatar: `https://api.dicebear.com/7.x/micah/svg?seed=${index}`
|
2025-01-11 18:22:14 +08:00
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to fetch donors:', error);
|
2025-01-12 01:25:39 +08:00
|
|
|
} finally {
|
|
|
|
|
isLoading.value = false;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
2025-01-12 01:25:39 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchDonors();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onActivated(() => {
|
|
|
|
|
fetchDonors();
|
2025-01-11 18:22:14 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 动画类名列表
|
|
|
|
|
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;
|
2025-01-12 01:25:39 +08:00
|
|
|
card.style.transform = 'translateY(-2px)';
|
|
|
|
|
card.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.12)';
|
2025-01-11 18:22:14 +08:00
|
|
|
|
|
|
|
|
// 添加金额标签动画
|
|
|
|
|
const amountTag = card.querySelector('.donation-amount');
|
|
|
|
|
if (amountTag) {
|
|
|
|
|
amountTag.classList.add('animate__tada');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMouseLeave = (event: MouseEvent) => {
|
|
|
|
|
const card = event.currentTarget as HTMLElement;
|
2025-01-12 01:25:39 +08:00
|
|
|
card.style.transform = 'translateY(0)';
|
|
|
|
|
card.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.08)';
|
2025-01-11 18:22:14 +08:00
|
|
|
|
|
|
|
|
// 移除金额标签动画
|
|
|
|
|
const amountTag = card.querySelector('.donation-amount');
|
|
|
|
|
if (amountTag) {
|
|
|
|
|
amountTag.classList.remove('animate__tada');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-12 12:38:51 +08:00
|
|
|
// 按金额和留言排序的捐赠列表
|
2025-01-11 18:22:14 +08:00
|
|
|
const sortedDonors = computed(() => {
|
|
|
|
|
return [...donors.value].sort((a, b) => {
|
2025-01-12 12:38:51 +08:00
|
|
|
// 如果一个有留言一个没有,有留言的排在前面
|
|
|
|
|
if (a.message && !b.message) return -1;
|
|
|
|
|
if (!a.message && b.message) return 1;
|
|
|
|
|
|
|
|
|
|
// 都有留言或都没有留言时,按金额从大到小排序
|
2025-01-11 18:22:14 +08:00
|
|
|
const amountDiff = b.amount - a.amount;
|
|
|
|
|
if (amountDiff !== 0) return amountDiff;
|
|
|
|
|
|
|
|
|
|
// 金额相同时,按日期从新到旧排序
|
|
|
|
|
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-01-12 01:25:39 +08:00
|
|
|
|
|
|
|
|
const isExpanded = ref(false);
|
|
|
|
|
|
|
|
|
|
const displayDonors = computed(() => {
|
|
|
|
|
if (isExpanded.value) {
|
|
|
|
|
return sortedDonors.value;
|
|
|
|
|
}
|
|
|
|
|
return sortedDonors.value.slice(0, 8);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const toggleExpand = () => {
|
|
|
|
|
isExpanded.value = !isExpanded.value;
|
|
|
|
|
};
|
2025-01-11 18:22:14 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.donation-container {
|
|
|
|
|
@apply w-full overflow-hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.donation-grid {
|
2025-01-12 01:25:39 +08:00
|
|
|
@apply grid gap-3 px-2 py-3 transition-all duration-300 overflow-hidden;
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
max-height: 280px;
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (min-width: 1024px) {
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.grid-expanded {
|
|
|
|
|
@apply max-h-none;
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.donation-card {
|
2025-01-12 01:25:39 +08:00
|
|
|
@apply relative rounded-lg p-3 min-w-0 w-full transition-all duration-500 shadow-md backdrop-blur-md;
|
|
|
|
|
@apply bg-gradient-to-br from-white/[0.03] to-white/[0.08] border border-white/[0.08];
|
|
|
|
|
@apply hover:shadow-lg;
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.card-content {
|
|
|
|
|
@apply relative z-10 flex items-start gap-3;
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donor-avatar {
|
|
|
|
|
@apply relative flex-shrink-0 w-10 h-9 transition-transform duration-300;
|
|
|
|
|
|
|
|
|
|
.avatar-img {
|
|
|
|
|
@apply border border-white/20 dark:border-gray-800/50 shadow-sm;
|
|
|
|
|
@apply w-10 h-9;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donor-badge {
|
|
|
|
|
@apply absolute -bottom-2 -right-1 px-1.5 py-0.5 text-xs font-medium text-white/90 rounded-full whitespace-nowrap;
|
|
|
|
|
@apply bg-gradient-to-r from-pink-400 to-pink-500 shadow-sm opacity-90 scale-90;
|
|
|
|
|
@apply transition-all duration-300;
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donor-info {
|
|
|
|
|
@apply flex-1 min-w-0;
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donor-name {
|
|
|
|
|
@apply text-sm font-medium mb-0.5 truncate;
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donation-meta {
|
|
|
|
|
@apply flex items-center gap-2 mb-1;
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donation-date {
|
|
|
|
|
@apply text-xs text-gray-400/80 dark:text-gray-500/80;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donation-message {
|
2025-01-12 12:38:51 +08:00
|
|
|
@apply text-sm text-gray-600 dark:text-gray-300 leading-relaxed mt-3 w-full;
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.message-content {
|
|
|
|
|
@apply relative p-2 rounded-lg transition-all duration-300 cursor-pointer;
|
|
|
|
|
@apply bg-white/[0.02] hover:bg-[var(--n-color)];
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.message-text {
|
|
|
|
|
@apply px-6 italic line-clamp-2;
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.quote-icon {
|
|
|
|
|
@apply absolute text-gray-400/60 dark:text-gray-500/60 text-sm;
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
&:first-child {
|
|
|
|
|
@apply left-0.5 top-2;
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
&:last-child {
|
|
|
|
|
@apply right-0.5 bottom-2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-11 18:22:14 +08:00
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
&:hover {
|
|
|
|
|
.donor-avatar {
|
|
|
|
|
@apply scale-105 rotate-3;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
|
2025-01-12 01:25:39 +08:00
|
|
|
.donor-badge {
|
|
|
|
|
@apply scale-95 -translate-y-0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-sparkles {
|
|
|
|
|
@apply opacity-60 scale-110;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-sparkles {
|
2025-01-12 01:25:39 +08:00
|
|
|
@apply absolute inset-0 pointer-events-none opacity-0 transition-all duration-500;
|
|
|
|
|
background-image: radial-gradient(2px 2px at 20px 30px, rgba(255, 255, 255, 0.95), transparent),
|
|
|
|
|
radial-gradient(2px 2px at 40px 70px, rgba(255, 255, 255, 0.95), transparent),
|
|
|
|
|
radial-gradient(2.5px 2.5px at 50px 160px, rgba(255, 255, 255, 0.95), 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);
|
2025-01-11 18:22:14 +08:00
|
|
|
background-size: 200% 200%;
|
2025-01-12 01:25:39 +08:00
|
|
|
animation: sparkle 8s ease infinite;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes sparkle {
|
|
|
|
|
0%,
|
|
|
|
|
100% {
|
2025-01-12 01:25:39 +08:00
|
|
|
@apply bg-[0%_0%] opacity-40 scale-100;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
50% {
|
2025-01-12 01:25:39 +08:00
|
|
|
@apply bg-[100%_100%] opacity-80 scale-110;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.refresh-container {
|
|
|
|
|
@apply flex justify-end px-2 py-2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.expand-button {
|
|
|
|
|
@apply flex justify-center items-center py-2;
|
|
|
|
|
|
|
|
|
|
:deep(.n-button) {
|
|
|
|
|
@apply transition-all duration-300 hover:-translate-y-0.5;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-popup {
|
|
|
|
|
@apply relative px-4 py-2 text-sm;
|
|
|
|
|
max-width: 300px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
|
|
|
|
.quote-icon {
|
|
|
|
|
@apply text-gray-400/60 dark:text-gray-500/60;
|
|
|
|
|
font-size: 0.9rem;
|
2025-01-11 18:22:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|