| <template> | |
| <div class="home-page"> | |
| <!-- 导航栏 --> | |
| <div class="navbar"> | |
| <router-link to="/home" class="navbar-brand">PT Tracker</router-link> | |
| <div class="navbar-nav"> | |
| <router-link to="/home" class="navbar-item">首页</router-link> | |
| <router-link to="/torrents" class="navbar-item">种子</router-link> | |
| <router-link to="/forum" class="navbar-item">论坛</router-link> | |
| <el-dropdown @command="handleUserCommand"> | |
| <span class="navbar-user"> | |
| <el-avatar :size="32" :src="userAvatar"> | |
| {{ username.charAt(0).toUpperCase() }} | |
| </el-avatar> | |
| <span class="username">{{ username }}</span> | |
| <el-icon><ArrowDown /></el-icon> | |
| </span> | |
| <template #dropdown> | |
| <el-dropdown-menu> | |
| <el-dropdown-item command="profile"> | |
| <el-icon><User /></el-icon> | |
| 个人资料 | |
| </el-dropdown-item> | |
| <el-dropdown-item command="settings"> | |
| <el-icon><Setting /></el-icon> | |
| 设置 | |
| </el-dropdown-item> | |
| <el-dropdown-item divided command="logout"> | |
| <el-icon><SwitchButton /></el-icon> | |
| 退出登录 | |
| </el-dropdown-item> | |
| </el-dropdown-menu> | |
| </template> | |
| </el-dropdown> | |
| </div> | |
| </div> | |
| <!-- 主内容区 --> | |
| <div class="main-content"> | |
| <!-- 欢迎卡片 --> | |
| <div class="welcome-card"> | |
| <div class="welcome-header"> | |
| <h1>欢迎回来,{{ username }}!</h1> | |
| <p>当前时间:{{ currentTime }}</p> | |
| </div> | |
| <!-- 用户统计概览 --> | |
| <div class="stats-overview" v-if="userInfo"> | |
| <div class="stat-item"> | |
| <div class="stat-icon upload"> | |
| <el-icon size="24"><Upload /></el-icon> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ formatBytes(userInfo.user.uploaded) }}</h3> | |
| <p>总上传</p> | |
| </div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon download"> | |
| <el-icon size="24"><Download /></el-icon> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ formatBytes(userInfo.user.downloaded) }}</h3> | |
| <p>总下载</p> | |
| </div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon ratio"> | |
| <el-icon size="24"><TrendCharts /></el-icon> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ calculateRatio(userInfo.user.uploaded, userInfo.user.downloaded) }}</h3> | |
| <p>分享率</p> | |
| </div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-icon points"> | |
| <el-icon size="24"><Star /></el-icon> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ userInfo.user.karma || '0' }}</h3> | |
| <p>积分</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 功能快捷入口 --> | |
| <div class="quick-actions"> | |
| <h2>快捷操作</h2> | |
| <div class="actions-grid"> | |
| <div class="action-card" @click="$router.push('/upload')"> | |
| <el-icon size="32" color="#67c23a"><Upload /></el-icon> | |
| <h3>上传种子</h3> | |
| <p>分享你的资源</p> | |
| </div> | |
| <div class="action-card" @click="$router.push('/torrents')"> | |
| <el-icon size="32" color="#409eff"><Search /></el-icon> | |
| <h3>浏览种子</h3> | |
| <p>发现新内容</p> | |
| </div> | |
| <div class="action-card" @click="$router.push('/forum')"> | |
| <el-icon size="32" color="#e6a23c"><ChatDotRound /></el-icon> | |
| <h3>社区论坛</h3> | |
| <p>交流讨论</p> | |
| </div> | |
| <div class="action-card" @click="$router.push('/profile')"> | |
| <el-icon size="32" color="#f56c6c"><User /></el-icon> | |
| <h3>个人中心</h3> | |
| <p>管理账户</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- API连接状态测试 --> | |
| <div class="api-status-card"> | |
| <h2>API连接状态</h2> | |
| <div class="status-items"> | |
| <div class="status-item"> | |
| <el-icon :color="loginStatusColor"><CircleCheck /></el-icon> | |
| <span>登录状态:{{ loginStatusText }}</span> | |
| <el-button size="small" @click="checkLoginStatus">检查状态</el-button> | |
| </div> | |
| <div class="status-item"> | |
| <el-icon :color="userInfoStatusColor"><User /></el-icon> | |
| <span>用户信息:{{ userInfoStatusText }}</span> | |
| <el-button size="small" @click="refreshUserInfo">刷新信息</el-button> | |
| </div> | |
| </div> | |
| <!-- 用户详细信息展示 --> | |
| <div class="user-details" v-if="userInfo"> | |
| <h3>用户详细信息</h3> | |
| <div class="details-grid"> | |
| <div class="detail-item"> | |
| <label>用户ID:</label> | |
| <span>{{ userInfo.user.id }}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <label>用户名:</label> | |
| <span>{{ userInfo.user.username }}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <label>邮箱:</label> | |
| <span>{{ userInfo.user.email }}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <label>用户组:</label> | |
| <span>{{ userInfo.user.group?.displayName || '未知' }}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <label>注册时间:</label> | |
| <span>{{ formatDate(userInfo.user.createdAt) }}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <label>个性签名:</label> | |
| <span>{{ userInfo.user.signature || '这个用户很懒,还没有个性签名' }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import { ref, computed, onMounted, onUnmounted } from 'vue' | |
| import { useRouter } from 'vue-router' | |
| import { useStore } from 'vuex' | |
| import { ElMessage, ElMessageBox } from 'element-plus' | |
| import { | |
| ArrowDown, | |
| User, | |
| Setting, | |
| SwitchButton, | |
| Upload, | |
| Download, | |
| TrendCharts, | |
| Star, | |
| Search, | |
| ChatDotRound, | |
| CircleCheck | |
| } from '@element-plus/icons-vue' | |
| export default { | |
| name: 'HomeView', | |
| components: { | |
| ArrowDown, | |
| User, | |
| Setting, | |
| SwitchButton, | |
| Upload, | |
| Download, | |
| TrendCharts, | |
| Star, | |
| Search, | |
| ChatDotRound, | |
| CircleCheck | |
| }, | |
| setup() { | |
| const router = useRouter() | |
| const store = useStore() | |
| const currentTime = ref('') | |
| const timeInterval = ref(null) | |
| // 从store获取用户信息 | |
| const userInfo = computed(() => store.getters['auth/userInfo']) | |
| const username = computed(() => store.getters['auth/username']) | |
| const userAvatar = computed(() => store.getters['auth/avatar']) | |
| const isAuthenticated = computed(() => store.getters['auth/isAuthenticated']) | |
| // API状态 | |
| const loginStatusColor = computed(() => isAuthenticated.value ? '#67c23a' : '#f56c6c') | |
| const loginStatusText = computed(() => isAuthenticated.value ? '已登录' : '未登录') | |
| const userInfoStatusColor = computed(() => userInfo.value ? '#67c23a' : '#f56c6c') | |
| const userInfoStatusText = computed(() => userInfo.value ? '已获取' : '未获取') | |
| // 更新当前时间 | |
| const updateCurrentTime = () => { | |
| currentTime.value = new Date().toLocaleString('zh-CN') | |
| } | |
| // 格式化字节数 | |
| const formatBytes = (bytes) => { | |
| if (!bytes || bytes === 0) return '0 B' | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] | |
| const i = Math.floor(Math.log(bytes) / Math.log(1024)) | |
| const size = (bytes / Math.pow(1024, i)).toFixed(2) | |
| return `${size} ${sizes[i]}` | |
| } | |
| // 计算分享率 | |
| const calculateRatio = (uploaded, downloaded) => { | |
| if (!uploaded || !downloaded || downloaded === 0) { | |
| return uploaded > 0 ? '∞' : '0.00' | |
| } | |
| return (uploaded / downloaded).toFixed(2) | |
| } | |
| // 格式化日期 | |
| const formatDate = (timestamp) => { | |
| if (!timestamp) return '未知' | |
| return new Date(timestamp).toLocaleDateString('zh-CN') | |
| } | |
| // 检查登录状态 | |
| const checkLoginStatus = async () => { | |
| try { | |
| await store.dispatch('auth/checkLoginStatus') | |
| ElMessage.success('登录状态检查完成') | |
| } catch (error) { | |
| console.error('检查登录状态失败:', error) | |
| ElMessage.error('检查登录状态失败') | |
| } | |
| } | |
| // 刷新用户信息 | |
| const refreshUserInfo = async () => { | |
| try { | |
| await store.dispatch('auth/checkLoginStatus') | |
| ElMessage.success('用户信息刷新成功') | |
| } catch (error) { | |
| console.error('刷新用户信息失败:', error) | |
| ElMessage.error('刷新用户信息失败') | |
| } | |
| } | |
| // 处理用户菜单命令 | |
| const handleUserCommand = async (command) => { | |
| switch (command) { | |
| case 'profile': | |
| router.push('/profile') | |
| break | |
| case 'settings': | |
| ElMessage.info('设置功能开发中...') | |
| break | |
| case 'logout': | |
| try { | |
| await ElMessageBox.confirm('确定要退出登录吗?', '提示', { | |
| confirmButtonText: '确定', | |
| cancelButtonText: '取消', | |
| type: 'warning' | |
| }) | |
| await store.dispatch('auth/logout') | |
| router.push('/login') | |
| } catch (error) { | |
| // 用户取消操作 | |
| if (error !== 'cancel') { | |
| console.error('退出登录失败:', error) | |
| } | |
| } | |
| break | |
| } | |
| } | |
| onMounted(() => { | |
| // 开始时间更新 | |
| updateCurrentTime() | |
| timeInterval.value = setInterval(updateCurrentTime, 1000) | |
| // 检查登录状态 | |
| if (!isAuthenticated.value) { | |
| checkLoginStatus() | |
| } | |
| }) | |
| onUnmounted(() => { | |
| // 清理定时器 | |
| if (timeInterval.value) { | |
| clearInterval(timeInterval.value) | |
| } | |
| }) | |
| return { | |
| currentTime, | |
| userInfo, | |
| username, | |
| userAvatar, | |
| isAuthenticated, | |
| loginStatusColor, | |
| loginStatusText, | |
| userInfoStatusColor, | |
| userInfoStatusText, | |
| formatBytes, | |
| calculateRatio, | |
| formatDate, | |
| checkLoginStatus, | |
| refreshUserInfo, | |
| handleUserCommand | |
| } | |
| } | |
| } | |
| </script> | |
| <style lang="scss" scoped> | |
| .home-page { | |
| min-height: 100vh; | |
| background: #f5f5f5; | |
| } | |
| .navbar { | |
| background: #fff; | |
| box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); | |
| height: 60px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 24px; | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| } | |
| .navbar-brand { | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #409eff; | |
| text-decoration: none; | |
| } | |
| .navbar-nav { | |
| display: flex; | |
| align-items: center; | |
| gap: 24px; | |
| } | |
| .navbar-item { | |
| color: #606266; | |
| text-decoration: none; | |
| font-weight: 500; | |
| transition: color 0.3s; | |
| &:hover { | |
| color: #409eff; | |
| } | |
| } | |
| .navbar-user { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 6px; | |
| transition: background-color 0.3s; | |
| &:hover { | |
| background-color: #f5f7fa; | |
| } | |
| .username { | |
| font-weight: 500; | |
| color: #303133; | |
| } | |
| } | |
| .main-content { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| } | |
| .welcome-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border-radius: 12px; | |
| padding: 32px; | |
| margin-bottom: 24px; | |
| .welcome-header { | |
| text-align: center; | |
| margin-bottom: 24px; | |
| h1 { | |
| font-size: 28px; | |
| margin-bottom: 8px; | |
| } | |
| p { | |
| opacity: 0.9; | |
| font-size: 14px; | |
| } | |
| } | |
| .stats-overview { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 16px; | |
| .stat-item { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 8px; | |
| padding: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| .stat-icon { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.2); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .stat-content { | |
| h3 { | |
| font-size: 20px; | |
| font-weight: 600; | |
| margin: 0 0 4px 0; | |
| } | |
| p { | |
| font-size: 14px; | |
| opacity: 0.8; | |
| margin: 0; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| .quick-actions { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
| h2 { | |
| font-size: 20px; | |
| color: #303133; | |
| margin: 0 0 20px 0; | |
| } | |
| .actions-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 16px; | |
| .action-card { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 24px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| border: 2px solid transparent; | |
| &:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); | |
| border-color: #409eff; | |
| } | |
| h3 { | |
| font-size: 16px; | |
| color: #303133; | |
| margin: 12px 0 8px 0; | |
| } | |
| p { | |
| font-size: 14px; | |
| color: #909399; | |
| margin: 0; | |
| } | |
| } | |
| } | |
| } | |
| .api-status-card { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 24px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
| h2 { | |
| font-size: 20px; | |
| color: #303133; | |
| margin: 0 0 20px 0; | |
| } | |
| .status-items { | |
| margin-bottom: 24px; | |
| .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 12px 0; | |
| border-bottom: 1px solid #ebeef5; | |
| &:last-child { | |
| border-bottom: none; | |
| } | |
| span { | |
| flex: 1; | |
| font-size: 14px; | |
| color: #606266; | |
| } | |
| } | |
| } | |
| .user-details { | |
| border-top: 1px solid #ebeef5; | |
| padding-top: 20px; | |
| h3 { | |
| font-size: 16px; | |
| color: #303133; | |
| margin: 0 0 16px 0; | |
| } | |
| .details-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 12px; | |
| .detail-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 8px 0; | |
| label { | |
| font-weight: 500; | |
| color: #909399; | |
| min-width: 80px; | |
| font-size: 14px; | |
| } | |
| span { | |
| color: #606266; | |
| font-size: 14px; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .navbar { | |
| padding: 0 16px; | |
| .navbar-nav { | |
| gap: 16px; | |
| } | |
| .navbar-user .username { | |
| display: none; | |
| } | |
| } | |
| .main-content { | |
| padding: 16px; | |
| } | |
| .welcome-card { | |
| padding: 24px 16px; | |
| .welcome-header h1 { | |
| font-size: 24px; | |
| } | |
| .stats-overview { | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| .stat-item { | |
| padding: 16px; | |
| .stat-content h3 { | |
| font-size: 16px; | |
| } | |
| } | |
| } | |
| } | |
| .actions-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| .action-card { | |
| padding: 16px; | |
| h3 { | |
| font-size: 14px; | |
| } | |
| p { | |
| font-size: 12px; | |
| } | |
| } | |
| } | |
| .details-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .stats-overview { | |
| grid-template-columns: 1fr; | |
| } | |
| .actions-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> |