Files
opencatd-open/frontend/src/views/dashboard/Overview.vue
Sakurasan 15f17f4e8d frontend
2025-04-16 18:14:53 +08:00

291 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>