This commit is contained in:
Sakurasan
2025-04-16 18:14:53 +08:00
parent ffb4496fd8
commit 15f17f4e8d
55 changed files with 7654 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
<template>
<div class="min-h-screen bg-base-100 p-3 md:p-6">
<BreadcrumbHeader />
<div class="card shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl mb-6" >
<div :class="['card-body text-white', getGradientClass()]">
<div class="flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
<div class="avatar">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full ring ring-white ring-offset-base-100 ring-offset-2"
v-if="!user?.avatar_url">
<div
class="bg-white text-primary-content font-bold flex items-center justify-center w-full h-full">
<span class="text-2xl sm:text-3xl">{{ user?.username?.[0]?.toUpperCase() }}</span>
</div>
</div>
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full ring ring-white ring-offset-base-100 ring-offset-2"
v-else>
<img :src="user?.avatar_url" :alt="user?.name" />
</div>
</div>
<div class="text-center sm:text-left">
<h1 class="text-2xl sm:text-4xl font-extrabold tracking-tight">
<span class="mr-2">👋 </span>{{ getTimeOfDay() }}{{ user?.name || user?.username }}
</h1>
<p class="text-white/80 text-base sm:text-lg mt-1 font-medium">欢迎回到您的个人仪表盘</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 lg:gap-8">
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 border border-base-200">
<div class="card-body p-4 md:p-6">
<h2 class="card-title text-lg md:text-xl font-bold flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6 text-base-content"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
基本信息
</h2>
<div class="divider my-1"></div>
<div class="space-y-3">
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">用户名</span>
<span class="badge">{{ user?.username }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">显示名称</span>
<span class="badge">{{ user?.name || '-' }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">邮箱</span>
<span class="badge truncate max-w-full">{{ user?.email || '-' }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">角色</span>
<span class="badge" :class="user?.role > 0 ? 'badge-warning' : 'badge-ghost'">{{
getRoleName(user?.role || 0) }}</span>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 border border-base-200">
<div class="card-body p-4 md:p-6">
<h2 class="card-title text-lg md:text-xl font-bold flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6 text-base-content"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
账户状态
</h2>
<div class="divider my-1"></div>
<div class="space-y-3">
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">状态</span>
<span :class="[
'badge badge-outline',
user?.active ? 'badge-success' : 'badge-error'
]">
{{ user?.active ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">时区</span>
<span class="badge ">{{ user?.timezone || 'UTC' }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">语言</span>
<span class="badge">{{ user?.language || 'en' }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-base-content/70 w-20">最后活动</span>
<span class="badge">{{ getLastActive() }}</span>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 border border-base-200">
<div class="card-body p-4 md:p-6">
<h2 class="card-title text-lg md:text-xl font-bold flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6 text-base-content"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
配额信息
</h2>
<div class="divider my-1"></div>
<div class="space-y-4">
<div v-if="user?.unlimited_quota">
<div class="flex flex-wrap justify-between mb-2">
<span class="font-semibold text-base-content/70">使用量</span>
<span class="badge badge-lg text-success">无限制</span>
</div>
</div>
<div v-else>
<div class="flex flex-wrap justify-between mb-2">
<span class="font-semibold text-base-content/70">使用量</span>
<span :class="getQuotaColor">
{{ formatQuota(user?.used_quota || 0, user?.quota||0) }}
</span>
</div>
<progress class="progress w-full"
:class="getQuotaColorClass"
:value="quotaPercentage"
max="100"></progress>
</div>
<div class="stats stats-vertical shadow w-full bg-base-100">
<div class="stat p-2 md:p-4">
<div class="stat-title text-xs md:text-sm">创建时间</div>
<div class="stat-value text-sm md:text-base">{{ formatDateTime(user?.created_at) }}
</div>
</div>
<div class="stat p-2 md:p-4">
<div class="stat-title text-xs md:text-sm">更新时间</div>
<div class="stat-value text-sm md:text-base">{{ formatDateTime(user?.updated_at) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
const router = useRouter();
const authStore = useAuthStore();
const user = computed(() => authStore.user);
onMounted(async () => {
if (!authStore.isLoggedIn) {
router.push('/login');
};
await authStore.refreshProfile();
});
const getTimeOfDay = () => {
const hour = new Date().getHours();
if (hour < 12) return '早上好';
if (hour < 18) return '下午好';
return '晚上好';
};
const formatQuota = (used, total) => {
if (total === 0) return '无限制';
// 格式化金额
const formatCurrency = (amount) => {
if (amount === 0) return '$0';
return `$${amount.toFixed(2)}`;
};
const percentage = (used / total) * 100;
return `${formatCurrency(used)} / ${formatCurrency(total)} (${percentage.toFixed(1)}%)`;
};
const getRoleName = (role) => {
switch (role) {
case 20: return 'Root';
case 10: return 'Admin';
default: return 'User';
}
};
// 格式化日期时间
function formatDateTime(unixTimestamp) {
// 如果时间戳不存在或为0返回'未知'
if (!unixTimestamp) return '未知';
// 将Unix时间戳转换为毫秒
const date = new Date(unixTimestamp * 1000);
// 获取日期和时间的各个部分
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
// 返回格式化的日期时间字符串
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
// 获取背景渐变类
const getGradientClass = () => {
const hour = new Date().getHours();
if (hour < 6) return 'bg-gradient-to-r from-[#e0f2f1] to-[#1a1a1a] bg-opacity-50 backdrop-blur-lg'; // 深夜到黎明:柔和的薄荷绿渐变到微黑
if (hour < 12) return 'bg-gradient-to-r from-[#8cc7f1] to-[#cf6f26] bg-opacity-50 backdrop-blur-lg'; // 早晨:温暖的杏仁色渐变到深灰
if (hour < 18) return 'bg-gradient-to-r from-[#e3f2fd] to-[#ad4212] bg-opacity-50 backdrop-blur-lg'; // 下午:清新的天空蓝渐变到近黑
return 'bg-gradient-to-r from-[#000000] to-[#434343] bg-opacity-50 backdrop-blur-lg'; // 夜晚:纯黑到深灰
};
// 计算配额百分比
const quotaPercentage = computed(() => {
if (!user.value || user.value.unlimited_quota || !user.value.quota) return 0;
return (user.value.used_quota / user.value.quota) * 100;
});
// 获取配额颜色
const getQuotaColor = computed(() => {
const percentage = quotaPercentage.value;
if (percentage < 50) return 'text-success';
if (percentage < 80) return 'text-warning';
return 'text-error';
});
// 获取配额颜色类
const getQuotaColorClass = computed(() => {
const percentage = quotaPercentage.value;
if (percentage < 50) return 'progress-success';
if (percentage < 80) return 'progress-warning';
return 'progress-error';
});
// 用户最后活动时间
const getLastActive = () => {
if (!user.value || !user.value?.updated_at) return '未知';
const lastActive = new Date(user.value.updated_at * 1000);
const now = new Date();
const diff = now - lastActive;
// 转换为天/小时/分钟
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days > 0) return `${days}天前`;
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours > 0) return `${hours}小时前`;
const minutes = Math.floor(diff / (1000 * 60));
if (minutes > 0) return `${minutes}分钟前`;
return '刚刚';
};
</script>
<style scoped>
/* 添加一些过渡动画 */
.card {
transform-origin: center;
backface-visibility: hidden;
}
.card:hover {
transform: translateY(-4px);
}
/* 确保响应式设计中文本不会溢出 */
.badge {
white-space: normal;
height: auto;
min-height: 1.6rem;
line-height: 1.2;
}
</style>