291 lines
13 KiB
Vue
291 lines
13 KiB
Vue
<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> |