| <template> |
| <div class="profile-page"> |
| <div class="page-container"> |
| <!-- 个人信息卡片 --> |
| <div class="profile-header"> |
| <div class="user-avatar-section"> |
| <div class="avatar-container"> |
| <el-avatar :size="120" :src="userProfile.avatar"> |
| {{ userProfile.username.charAt(0).toUpperCase() }} |
| </el-avatar> |
| <el-button |
| type="primary" |
| size="small" |
| class="change-avatar-btn" |
| @click="showAvatarDialog = true" |
| > |
| 更换头像 |
| </el-button> |
| </div> |
| |
| <div class="user-basic-info"> |
| <h1 class="username">{{ userBase.username }}</h1> |
| <div class="user-title"> |
| <el-tag :type="getUserTitleType(userProfile.userLevel)" size="large"> |
| {{ userProfile.userTitle }} |
| </el-tag> |
| </div> |
| <div class="join-info"> |
| <el-icon><Calendar /></el-icon> |
| <span>加入时间:{{ userBase.joinDate }}</span> |
| </div> |
| <!-- <div class="last-login"> |
| <el-icon><Clock /></el-icon> |
| <span>最后登录:{{ formatTime(userProfile.lastLogin) }}</span> |
| </div> --> |
| </div> |
| </div> |
| |
| <div class="user-stats-overview"> |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-icon upload"> |
| <el-icon size="32"><Upload /></el-icon> |
| </div> |
| <div class="stat-info"> |
| <h3>{{ stats.uploaded || '-' }}</h3> |
| <p>上传量</p> |
| </div> |
| </div> |
| |
| <div class="stat-card"> |
| <div class="stat-icon download"> |
| <el-icon size="32"><Download /></el-icon> |
| </div> |
| <div class="stat-info"> |
| <h3>{{ stats.downloaded || '-' }}</h3> |
| <p>下载量</p> |
| </div> |
| </div> |
| |
| <div class="stat-card"> |
| <div class="stat-icon ratio" :class="getRatioClass(calcRatio)"> |
| <el-icon size="32"><TrendCharts /></el-icon> |
| </div> |
| <div class="stat-info"> |
| <h3>{{ calcRatio }}</h3> |
| <p>分享率</p> |
| </div> |
| </div> |
| |
| <div class="stat-card"> |
| <div class="stat-icon points"> |
| <el-icon size="32"><Star /></el-icon> |
| </div> |
| <div class="stat-info"> |
| <h3>{{ stats.points || '-' }}</h3> |
| <p>积分</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- 详细信息选项卡 --> |
| <div class="profile-content"> |
| <el-tabs v-model="activeTab" type="border-card"> |
| <!-- 数据统计 --> |
| <el-tab-pane label="数据统计" name="stats"> |
| <div class="stats-section" v-if="stats"> |
| <div class="stats-overview"> |
| <div class="overview-grid"> |
| <div class="overview-card"> |
| <h3>上传统计</h3> |
| <div class="stat-details"> |
| <div class="detail-item"> |
| <span class="label">总上传量:</span> |
| <span class="value">{{ stats.uploaded || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">真实上传:</span> |
| <span class="value">{{ stats.realUploaded || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">上传带宽:</span> |
| <span class="value">{{ stats.uploadBandwidth || '-' }}</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="overview-card"> |
| <h3>下载统计</h3> |
| <div class="stat-details"> |
| <div class="detail-item"> |
| <span class="label">总下载量:</span> |
| <span class="value">{{ stats.downloaded || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">真实下载:</span> |
| <span class="value">{{ stats.realDownloaded || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">下载带宽:</span> |
| <span class="value">{{ stats.downloadBandwidth || '-' }}</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="overview-card"> |
| <h3>做种统计</h3> |
| <div class="stat-details"> |
| <div class="detail-item"> |
| <span class="label">正在做种:</span> |
| <span class="value">{{ stats.seeding || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">做种时间:</span> |
| <span class="value">{{ stats.seedingTime || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">做种排名:</span> |
| <span class="value">{{ stats.seedingRank || '-' }}</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="overview-card"> |
| <h3>积分记录</h3> |
| <div class="stat-details"> |
| <div class="detail-item"> |
| <span class="label">当前积分:</span> |
| <span class="value">{{ stats.points || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">邀请名额:</span> |
| <span class="value">{{ stats.inviteSlot || '-' }}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="label">ISP:</span> |
| <span class="value">{{ stats.isp || '-' }}</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- 数据图表 --> |
| <div class="charts-section"> |
| <div class="chart-card"> |
| <h3>上传下载趋势</h3> |
| <div class="chart-placeholder"> |
| <el-icon size="48" color="#e4e7ed"><TrendCharts /></el-icon> |
| <p>图表功能开发中...</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </el-tab-pane> |
| |
| <!-- 我的种子 --> |
| <el-tab-pane label="我的种子" name="torrents"> |
| <div class="torrents-section"> |
| <div class="section-header"> |
| <h3>我上传的种子</h3> |
| <el-button type="primary" :icon="Upload" @click="$router.push('/upload')"> |
| 上传新种子 |
| </el-button> |
| </div> |
| |
| <el-table :data="userTorrents" stripe> |
| <el-table-column label="种子名称" min-width="300"> |
| <template #default="{ row }"> |
| <div class="torrent-info"> |
| <el-tag :type="getCategoryType(row.category)" size="small"> |
| {{ getCategoryName(row.category) }} |
| </el-tag> |
| <span class="torrent-title">{{ row.title }}</span> |
| </div> |
| </template> |
| </el-table-column> |
| |
| <el-table-column label="大小" prop="size" width="100" /> |
| <el-table-column label="做种" prop="seeders" width="80" align="center" /> |
| <el-table-column label="下载" prop="leechers" width="80" align="center" /> |
| <el-table-column label="完成" prop="downloads" width="80" align="center" /> |
| <el-table-column label="上传时间" width="120"> |
| <template #default="{ row }"> |
| {{ formatDate(row.uploadTime) }} |
| </template> |
| </el-table-column> |
| |
| <el-table-column label="操作" width="120" align="center"> |
| <template #default="{ row }"> |
| <el-button |
| type="primary" |
| size="small" |
| @click="$router.push(`/torrent/${row.id}`)" |
| > |
| 查看 |
| </el-button> |
| </template> |
| </el-table-column> |
| </el-table> |
| |
| <div class="pagination-wrapper"> |
| <el-pagination |
| :current-page="torrentsPage" |
| :page-size="10" |
| :total="userTorrents.length" |
| layout="prev, pager, next" |
| small |
| @current-change="(page) => torrentsPage = page" |
| /> |
| </div> |
| </div> |
| </el-tab-pane> |
| |
| <!-- 活动记录 --> |
| <el-tab-pane label="活动记录" name="activity"> |
| <div class="activity-section"> |
| <div class="activity-filters"> |
| <el-select v-model="activityFilter" placeholder="活动类型"> |
| <el-option label="全部活动" value="" /> |
| <el-option label="上传种子" value="upload" /> |
| <el-option label="下载种子" value="download" /> |
| <el-option label="论坛发帖" value="post" /> |
| <el-option label="积分变动" value="points" /> |
| </el-select> |
| </div> |
| |
| <div class="activity-timeline"> |
| <el-timeline> |
| <el-timeline-item |
| v-for="activity in filteredActivities" |
| :key="activity.id" |
| :timestamp="formatTime(activity.time)" |
| :type="getActivityType(activity.type)" |
| > |
| <div class="activity-content"> |
| <div class="activity-header"> |
| <el-icon> |
| <component :is="getActivityIcon(activity.type)" /> |
| </el-icon> |
| <span class="activity-title">{{ activity.title }}</span> |
| </div> |
| <div class="activity-description">{{ activity.description }}</div> |
| </div> |
| </el-timeline-item> |
| </el-timeline> |
| </div> |
| </div> |
| </el-tab-pane> |
| </el-tabs> |
| </div> |
| </div> |
| |
| <!-- 更换头像对话框 --> |
| <el-dialog v-model="showAvatarDialog" title="更换头像" width="400px"> |
| <div class="avatar-upload"> |
| <el-upload |
| ref="avatarUploadRef" |
| :auto-upload="false" |
| :limit="1" |
| accept="image/*" |
| :on-change="handleAvatarChange" |
| list-type="picture-card" |
| class="avatar-uploader" |
| > |
| <el-icon><Plus /></el-icon> |
| </el-upload> |
| <div class="upload-tips"> |
| <p>支持 JPG、PNG 格式</p> |
| <p>建议尺寸 200x200 像素</p> |
| <p>文件大小不超过 2MB</p> |
| </div> |
| </div> |
| |
| <template #footer> |
| <el-button @click="showAvatarDialog = false">取消</el-button> |
| <el-button type="primary" @click="uploadAvatar" :loading="uploadingAvatar"> |
| 上传头像 |
| </el-button> |
| </template> |
| </el-dialog> |
| </div> |
| </template> |
| |
| <script> |
| import { ref, reactive, computed, onMounted, nextTick } from 'vue' |
| import { useRouter } from 'vue-router' |
| import { ElMessage } from 'element-plus' |
| import { |
| Calendar, |
| Clock, |
| Upload, |
| Download, |
| TrendCharts, |
| Star, |
| QuestionFilled, |
| Plus, |
| ChatDotRound, |
| Flag, |
| Coin |
| } from '@element-plus/icons-vue' |
| import { userApi } from '@/api/user' |
| |
| export default { |
| name: 'ProfileView', |
| setup() { |
| const router = useRouter() |
| const profileFormRef = ref(null) |
| const passwordFormRef = ref(null) |
| const avatarUploadRef = ref(null) |
| const interestInputRef = ref(null) |
| |
| const activeTab = ref('info') |
| const showAvatarDialog = ref(false) |
| const saving = ref(false) |
| const changingPassword = ref(false) |
| const uploadingAvatar = ref(false) |
| const interestInputVisible = ref(false) |
| const interestInputValue = ref('') |
| const activityFilter = ref('') |
| const torrentsPage = ref(1) |
| |
| const userProfile = ref({ |
| username: 'MovieExpert', |
| email: 'movieexpert@example.com', |
| realName: '', |
| avatar: '', |
| userLevel: 5, |
| userTitle: '资深会员', |
| joinDate: '2023-01-15T10:00:00', |
| lastLogin: '2025-06-03T14:30:00', |
| location: ['北京市', '朝阳区'], |
| signature: '热爱电影,分享快乐!', |
| website: 'https://movieblog.com', |
| interests: ['电影', '音乐', '科技', '摄影'], |
| emailPublic: false, |
| statsPublic: true, |
| activityPublic: true, |
| stats: { |
| uploaded: '256.8 GB', |
| downloaded: '89.6 GB', |
| ratio: '2.87', |
| points: '15,680' |
| }, |
| detailedStats: { |
| totalUploaded: '256.8 GB', |
| uploadedTorrents: 45, |
| avgUploadSize: '5.7 GB', |
| totalDownloaded: '89.6 GB', |
| downloadedTorrents: 123, |
| completedTorrents: 118, |
| seeding: 32, |
| seedingTime: '1,245 小时', |
| seedingRank: 86, |
| totalEarnedPoints: '28,940', |
| totalSpentPoints: '13,260' |
| } |
| }) |
| |
| const editProfile = reactive({ |
| username: '', |
| email: '', |
| customTitle: '', |
| realName: '', |
| location: [], |
| signature: '', |
| website: '', |
| interests: [], |
| emailPublic: false, |
| statsPublic: true, |
| activityPublic: true |
| }) |
| |
| const passwordForm = reactive({ |
| currentPassword: '', |
| newPassword: '', |
| confirmPassword: '' |
| }) |
| |
| const profileRules = { |
| email: [ |
| { required: true, message: '请输入邮箱地址', trigger: 'blur' }, |
| { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' } |
| ] |
| } |
| |
| const passwordRules = { |
| currentPassword: [ |
| { required: true, message: '请输入当前密码', trigger: 'blur' } |
| ], |
| newPassword: [ |
| { required: true, message: '请输入新密码', trigger: 'blur' }, |
| { min: 6, message: '密码长度至少6个字符', trigger: 'blur' } |
| ], |
| confirmPassword: [ |
| { required: true, message: '请确认新密码', trigger: 'blur' }, |
| { |
| validator: (rule, value, callback) => { |
| if (value !== passwordForm.newPassword) { |
| callback(new Error('两次输入的密码不一致')) |
| } else { |
| callback() |
| } |
| }, |
| trigger: 'blur' |
| } |
| ] |
| } |
| |
| const locationOptions = [ |
| { |
| value: '北京市', |
| label: '北京市', |
| children: [ |
| { value: '朝阳区', label: '朝阳区' }, |
| { value: '海淀区', label: '海淀区' }, |
| { value: '丰台区', label: '丰台区' } |
| ] |
| }, |
| { |
| value: '上海市', |
| label: '上海市', |
| children: [ |
| { value: '浦东新区', label: '浦东新区' }, |
| { value: '黄浦区', label: '黄浦区' }, |
| { value: '静安区', label: '静安区' } |
| ] |
| } |
| ] |
| |
| const userTorrents = ref([ |
| { |
| id: 1, |
| title: '[4K蓝光原盘] 阿凡达:水之道', |
| category: 'movie', |
| size: '85.6 GB', |
| seeders: 45, |
| leechers: 12, |
| downloads: 234, |
| uploadTime: '2025-05-15T10:00:00' |
| }, |
| { |
| id: 2, |
| title: '[FLAC] 古典音乐合集', |
| category: 'music', |
| size: '2.3 GB', |
| seeders: 23, |
| leechers: 5, |
| downloads: 89, |
| uploadTime: '2025-04-20T15:30:00' |
| } |
| ]) |
| |
| const activities = ref([ |
| { |
| id: 1, |
| type: 'upload', |
| title: '上传种子', |
| description: '上传了《阿凡达:水之道》4K蓝光原盘', |
| time: '2025-06-03T10:30:00' |
| }, |
| { |
| id: 2, |
| type: 'download', |
| title: '下载种子', |
| description: '下载了《星际穿越》IMAX版本', |
| time: '2025-06-02T14:20:00' |
| }, |
| { |
| id: 3, |
| type: 'post', |
| title: '发布主题', |
| description: '在电影讨论区发布了新主题', |
| time: '2025-06-01T16:45:00' |
| }, |
| { |
| id: 4, |
| type: 'points', |
| title: '积分变动', |
| description: '做种奖励获得 +50 积分', |
| time: '2025-05-31T09:15:00' |
| } |
| ]) |
| |
| const loginHistory = ref([ |
| { |
| time: '2025-06-03T14:30:00', |
| ip: '192.168.1.100', |
| device: 'Windows 11 / Chrome 120', |
| success: true |
| }, |
| { |
| time: '2025-06-02T09:15:00', |
| ip: '192.168.1.100', |
| device: 'Windows 11 / Chrome 120', |
| success: true |
| }, |
| { |
| time: '2025-06-01T22:30:00', |
| ip: '192.168.1.100', |
| device: 'Android / Chrome Mobile', |
| success: true |
| } |
| ]) |
| |
| const filteredActivities = computed(() => { |
| if (!activityFilter.value) return activities.value |
| return activities.value.filter(activity => activity.type === activityFilter.value) |
| }) |
| |
| const stats = reactive({ |
| uploaded: '-', |
| realUploaded: '-', |
| uploadBandwidth: '-', |
| downloaded: '-', |
| realDownloaded: '-', |
| downloadBandwidth: '-', |
| seeding: '-', |
| seedingTime: '-', |
| seedingRank: '-', |
| points: '-', |
| inviteSlot: '-', |
| isp: '-' |
| }) |
| |
| const calcRatio = computed(() => { |
| if (!stats.uploaded || !stats.downloaded || stats.downloaded === '-' || stats.uploaded === '-') return '-' |
| const parseGB = (str) => { |
| if (typeof str !== 'string') return 0 |
| if (str.endsWith('TB')) return parseFloat(str) * 1024 |
| if (str.endsWith('GB')) return parseFloat(str) |
| if (str.endsWith('MB')) return parseFloat(str) / 1024 |
| if (str.endsWith('KB')) return parseFloat(str) / 1024 / 1024 |
| if (str.endsWith('B')) return parseFloat(str) / 1024 / 1024 / 1024 |
| return parseFloat(str) |
| } |
| const up = parseGB(stats.uploaded) |
| const down = parseGB(stats.downloaded) |
| if (!down) return up > 0 ? '∞' : '0.00' |
| return (up / down).toFixed(2) |
| }) |
| |
| const userBase = reactive({ |
| username: '-', |
| joinDate: '-', |
| }) |
| |
| onMounted(async () => { |
| try { |
| const res = await userApi.getCurrentUser() |
| if (res.user && res.user.user) { |
| const u = res.user.user |
| stats.uploaded = formatBytes(u.uploaded) |
| stats.realUploaded = formatBytes(u.realUploaded) |
| stats.uploadBandwidth = u.uploadBandwidth || '-' |
| stats.downloaded = formatBytes(u.downloaded) |
| stats.realDownloaded = formatBytes(u.realDownloaded) |
| stats.downloadBandwidth = u.downloadBandwidth || '-' |
| stats.seeding = u.seeding || '-' |
| stats.seedingTime = formatSeedingTime(u.seedingTime) |
| stats.seedingRank = u.seedingRank || '-' |
| stats.points = u.karma || '-' |
| stats.inviteSlot = u.inviteSlot || '-' |
| stats.isp = u.isp || '-' |
| userBase.username = u.username || '-' |
| userBase.joinDate = u.createdAt ? formatDate(u.createdAt) : '-' |
| } |
| } catch (e) { |
| ElMessage.error('获取数据失败') |
| } |
| }) |
| |
| const getUserTitleByLevel = (level) => { |
| if (level == null) return '新手' |
| if (level >= 8) return '管理员' |
| if (level >= 6) return '资深会员' |
| if (level >= 4) return '正式会员' |
| if (level >= 2) return '初级会员' |
| return '新手' |
| } |
| |
| const formatBytes = (bytes) => { |
| if (!bytes || isNaN(bytes)) return '0 B' |
| const k = 1024 |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] |
| const i = Math.floor(Math.log(bytes) / Math.log(k)) |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] |
| } |
| |
| const formatSeedingTime = (seconds) => { |
| if (!seconds || isNaN(seconds)) return '0 小时' |
| const hours = Math.floor(seconds / 3600) |
| const days = Math.floor(hours / 24) |
| if (days > 0) { |
| return `${days} 天 ${hours % 24} 小时` |
| } else { |
| return `${hours} 小时` |
| } |
| } |
| |
| const formatDate = (dateString) => { |
| const date = new Date(dateString) |
| return date.toLocaleDateString('zh-CN') |
| } |
| |
| const formatTime = (timeString) => { |
| const date = new Date(timeString) |
| const now = new Date() |
| const diff = now - date |
| const hours = Math.floor(diff / (1000 * 60 * 60)) |
| |
| if (hours < 1) return '刚刚' |
| if (hours < 24) return `${hours}小时前` |
| const days = Math.floor(hours / 24) |
| if (days < 7) return `${days}天前` |
| |
| return date.toLocaleDateString('zh-CN') |
| } |
| |
| const formatDateTime = (dateString) => { |
| const date = new Date(dateString) |
| return date.toLocaleString('zh-CN') |
| } |
| |
| const getUserTitleType = (level) => { |
| if (level >= 8) return 'danger' // 管理员 |
| if (level >= 6) return 'warning' // 资深会员 |
| if (level >= 4) return 'success' // 正式会员 |
| if (level >= 2) return 'info' // 初级会员 |
| return 'default' // 新手 |
| } |
| |
| const getRatioClass = (ratio) => { |
| const r = parseFloat(ratio) |
| if (r >= 2) return 'excellent' |
| if (r >= 1) return 'good' |
| return 'warning' |
| } |
| |
| const getCategoryType = (category) => { |
| const types = { |
| 'movie': 'primary', |
| 'tv': 'info', |
| 'music': 'success', |
| 'software': 'warning', |
| 'game': 'danger' |
| } |
| return types[category] || 'default' |
| } |
| |
| const getCategoryName = (category) => { |
| const names = { |
| 'movie': '电影', |
| 'tv': '电视剧', |
| 'music': '音乐', |
| 'software': '软件', |
| 'game': '游戏' |
| } |
| return names[category] || category |
| } |
| |
| const getLanguageName = (language) => { |
| const languages = { |
| 'zh-CN': '简体中文', |
| 'zh-TW': '繁体中文', |
| 'en-US': 'English', |
| 'ja-JP': '日本語', |
| 'ko-KR': '한국어' |
| } |
| return languages[language] || language |
| } |
| |
| const getActivityType = (type) => { |
| const types = { |
| 'upload': 'success', |
| 'download': 'primary', |
| 'post': 'warning', |
| 'points': 'info' |
| } |
| return types[type] || 'primary' |
| } |
| |
| const getActivityIcon = (type) => { |
| const icons = { |
| 'upload': 'Upload', |
| 'download': 'Download', |
| 'post': 'ChatDotRound', |
| 'points': 'Coin' |
| } |
| return icons[type] || 'Star' |
| } |
| |
| const showInterestInput = () => { |
| interestInputVisible.value = true |
| nextTick(() => { |
| interestInputRef.value?.focus() |
| }) |
| } |
| |
| const addInterest = () => { |
| const interest = interestInputValue.value.trim() |
| if (interest && !editProfile.interests.includes(interest)) { |
| editProfile.interests.push(interest) |
| } |
| interestInputVisible.value = false |
| interestInputValue.value = '' |
| } |
| |
| const removeInterest = (interest) => { |
| const index = editProfile.interests.indexOf(interest) |
| if (index > -1) { |
| editProfile.interests.splice(index, 1) |
| } |
| } |
| |
| const saveProfile = async () => { |
| try { |
| const payload = { |
| username: editProfile.username, |
| email: editProfile.email |
| } |
| await userApi.updateProfile(payload) |
| ElMessage.success('保存成功') |
| } catch (error) { |
| ElMessage.error('保存失败') |
| } |
| } |
| |
| const resetProfile = () => { |
| loadUserProfile() |
| ElMessage.info('已重置为原始数据') |
| } |
| |
| const changePassword = async () => { |
| try { |
| await passwordFormRef.value?.validate() |
| |
| changingPassword.value = true |
| |
| // 模拟密码修改过程 |
| await new Promise(resolve => setTimeout(resolve, 1500)) |
| |
| // 重置表单 |
| passwordFormRef.value?.resetFields() |
| Object.assign(passwordForm, { |
| currentPassword: '', |
| newPassword: '', |
| confirmPassword: '' |
| }) |
| |
| ElMessage.success('密码修改成功') |
| |
| } catch (error) { |
| console.error('表单验证失败:', error) |
| } finally { |
| changingPassword.value = false |
| } |
| } |
| |
| const handleAvatarChange = (file) => { |
| const isImage = file.raw.type.startsWith('image/') |
| const isLt2M = file.raw.size / 1024 / 1024 < 2 |
| |
| if (!isImage) { |
| ElMessage.error('只能上传图片文件!') |
| return false |
| } |
| if (!isLt2M) { |
| ElMessage.error('图片大小不能超过 2MB!') |
| return false |
| } |
| |
| return true |
| } |
| |
| const uploadAvatar = async () => { |
| const files = avatarUploadRef.value?.uploadFiles |
| if (!files || files.length === 0) { |
| ElMessage.warning('请选择头像文件') |
| return |
| } |
| |
| uploadingAvatar.value = true |
| try { |
| // 模拟上传过程 |
| await new Promise(resolve => setTimeout(resolve, 2000)) |
| |
| // 更新头像URL |
| userProfile.value.avatar = URL.createObjectURL(files[0].raw) |
| |
| ElMessage.success('头像上传成功') |
| showAvatarDialog.value = false |
| avatarUploadRef.value?.clearFiles() |
| |
| } catch (error) { |
| ElMessage.error('头像上传失败') |
| } finally { |
| uploadingAvatar.value = false |
| } |
| } |
| |
| return { |
| activeTab, |
| showAvatarDialog, |
| saving, |
| changingPassword, |
| uploadingAvatar, |
| interestInputVisible, |
| interestInputValue, |
| activityFilter, |
| torrentsPage, |
| userProfile, |
| editProfile, |
| passwordForm, |
| profileRules, |
| passwordRules, |
| locationOptions, |
| userTorrents, |
| filteredActivities, |
| loginHistory, |
| profileFormRef, |
| passwordFormRef, |
| avatarUploadRef, |
| interestInputRef, |
| formatDate, |
| formatTime, |
| formatDateTime, |
| getUserTitleType, |
| getRatioClass, |
| getCategoryType, |
| getCategoryName, |
| getActivityType, |
| getActivityIcon, |
| showInterestInput, |
| addInterest, |
| removeInterest, |
| saveProfile, |
| resetProfile, |
| changePassword, |
| handleAvatarChange, |
| uploadAvatar, |
| Calendar, |
| Clock, |
| Upload, |
| Download, |
| TrendCharts, |
| Star, |
| QuestionFilled, |
| Plus, |
| ChatDotRound, |
| Flag, |
| Coin, |
| stats, |
| calcRatio, |
| userBase |
| } |
| } |
| } |
| </script> |
| |
| <style lang="scss" scoped> |
| .profile-page { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 24px; |
| background: #f5f5f5; |
| min-height: 100vh; |
| } |
| |
| .profile-header { |
| background: #fff; |
| border-radius: 12px; |
| padding: 32px; |
| margin-bottom: 24px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
| |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 32px; |
| |
| .user-avatar-section { |
| display: flex; |
| gap: 24px; |
| |
| .avatar-container { |
| text-align: center; |
| |
| .change-avatar-btn { |
| margin-top: 12px; |
| } |
| } |
| |
| .user-basic-info { |
| flex: 1; |
| |
| .username { |
| font-size: 28px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0 0 12px 0; |
| } |
| |
| .user-title { |
| margin-bottom: 16px; |
| } |
| |
| .join-info, .last-login { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 14px; |
| color: #7f8c8d; |
| margin-bottom: 8px; |
| } |
| } |
| } |
| |
| .user-stats-overview { |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 16px; |
| |
| .stat-card { |
| background: #f8f9fa; |
| border-radius: 8px; |
| padding: 20px; |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| |
| .stat-icon { |
| width: 48px; |
| height: 48px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| |
| &.upload { background: rgba(103, 194, 58, 0.1); color: #67c23a; } |
| &.download { background: rgba(64, 158, 255, 0.1); color: #409eff; } |
| &.ratio { |
| &.excellent { background: rgba(103, 194, 58, 0.1); color: #67c23a; } |
| &.good { background: rgba(230, 162, 60, 0.1); color: #e6a23c; } |
| &.warning { background: rgba(245, 108, 108, 0.1); color: #f56c6c; } |
| } |
| &.points { background: rgba(245, 108, 108, 0.1); color: #f56c6c; } |
| } |
| |
| .stat-info { |
| h3 { |
| font-size: 20px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0 0 4px 0; |
| } |
| |
| p { |
| font-size: 14px; |
| color: #7f8c8d; |
| margin: 0; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| .profile-content { |
| background: #fff; |
| border-radius: 12px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
| |
| :deep(.el-tabs__content) { |
| padding: 24px; |
| } |
| } |
| |
| .info-section { |
| .form-section { |
| margin-bottom: 32px; |
| |
| h3 { |
| font-size: 18px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0 0 20px 0; |
| padding-bottom: 8px; |
| border-bottom: 2px solid #f0f0f0; |
| } |
| } |
| |
| .interests-input { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| align-items: center; |
| |
| .interest-tag { |
| margin: 0; |
| } |
| } |
| |
| .setting-tip { |
| margin-left: 12px; |
| font-size: 12px; |
| color: #909399; |
| } |
| |
| .form-actions { |
| text-align: center; |
| margin-top: 32px; |
| |
| .el-button { |
| margin: 0 8px; |
| min-width: 100px; |
| } |
| } |
| } |
| |
| .stats-section { |
| .stats-overview { |
| margin-bottom: 32px; |
| |
| .overview-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
| gap: 20px; |
| |
| .overview-card { |
| background: #f8f9fa; |
| border-radius: 8px; |
| padding: 24px; |
| |
| h3 { |
| font-size: 16px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0 0 16px 0; |
| } |
| |
| .stat-details { |
| .detail-item { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 12px; |
| |
| .label { |
| font-size: 14px; |
| color: #7f8c8d; |
| } |
| |
| .value { |
| font-size: 14px; |
| font-weight: 600; |
| color: #2c3e50; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| .charts-section { |
| .chart-card { |
| background: #f8f9fa; |
| border-radius: 8px; |
| padding: 24px; |
| |
| h3 { |
| font-size: 16px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0 0 20px 0; |
| } |
| |
| .chart-placeholder { |
| text-align: center; |
| padding: 60px 0; |
| color: #909399; |
| |
| p { |
| margin: 12px 0 0 0; |
| } |
| } |
| } |
| } |
| } |
| |
| .torrents-section { |
| .section-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 20px; |
| |
| h3 { |
| font-size: 18px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0; |
| } |
| } |
| |
| .torrent-info { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| |
| .torrent-title { |
| font-weight: 500; |
| } |
| } |
| |
| .pagination-wrapper { |
| margin-top: 16px; |
| text-align: center; |
| } |
| } |
| |
| .activity-section { |
| .activity-filters { |
| margin-bottom: 24px; |
| |
| .el-select { |
| width: 150px; |
| } |
| } |
| |
| .activity-timeline { |
| .activity-content { |
| .activity-header { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| margin-bottom: 8px; |
| |
| .activity-title { |
| font-weight: 600; |
| color: #2c3e50; |
| } |
| } |
| |
| .activity-description { |
| font-size: 14px; |
| color: #7f8c8d; |
| line-height: 1.5; |
| } |
| } |
| } |
| } |
| |
| .avatar-upload { |
| text-align: center; |
| |
| .avatar-uploader { |
| margin-bottom: 16px; |
| } |
| |
| .upload-tips { |
| font-size: 12px; |
| color: #909399; |
| |
| p { |
| margin: 4px 0; |
| } |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .profile-page { |
| padding: 16px; |
| } |
| |
| .profile-header { |
| grid-template-columns: 1fr; |
| gap: 24px; |
| |
| .user-avatar-section { |
| flex-direction: column; |
| text-align: center; |
| } |
| |
| .user-stats-overview .stats-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| .stats-overview .overview-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .torrents-section .section-header { |
| flex-direction: column; |
| gap: 16px; |
| align-items: flex-start; |
| } |
| } |
| </style> |