blob: 640f8930ee02fe96db86cf10e58225dfdd41a098 [file] [log] [blame]
208159515458d95702025-06-09 14:46:58 +08001<template>
2 <div class="profile-page">
3 <div class="page-container">
4 <!-- 个人信息卡片 -->
5 <div class="profile-header">
6 <div class="user-avatar-section">
7 <div class="avatar-container">
8 <el-avatar :size="120" :src="userProfile.avatar">
9 {{ userProfile.username.charAt(0).toUpperCase() }}
10 </el-avatar>
11 <el-button
12 type="primary"
13 size="small"
14 class="change-avatar-btn"
15 @click="showAvatarDialog = true"
16 >
17 更换头像
18 </el-button>
19 </div>
20
21 <div class="user-basic-info">
22 <h1 class="username">{{ userBase.username }}</h1>
23 <div class="user-title">
24 <el-tag :type="getUserTitleType(userProfile.userLevel)" size="large">
25 {{ userProfile.userTitle }}
26 </el-tag>
27 </div>
28 <div class="join-info">
29 <el-icon><Calendar /></el-icon>
30 <span>加入时间:{{ userBase.joinDate }}</span>
31 </div>
32 <!-- <div class="last-login">
33 <el-icon><Clock /></el-icon>
34 <span>最后登录:{{ formatTime(userProfile.lastLogin) }}</span>
35 </div> -->
36 </div>
37 </div>
38
39 <div class="user-stats-overview">
40 <div class="stats-grid">
41 <div class="stat-card">
42 <div class="stat-icon upload">
43 <el-icon size="32"><Upload /></el-icon>
44 </div>
45 <div class="stat-info">
46 <h3>{{ stats.uploaded || '-' }}</h3>
47 <p>上传量</p>
48 </div>
49 </div>
50
51 <div class="stat-card">
52 <div class="stat-icon download">
53 <el-icon size="32"><Download /></el-icon>
54 </div>
55 <div class="stat-info">
56 <h3>{{ stats.downloaded || '-' }}</h3>
57 <p>下载量</p>
58 </div>
59 </div>
60
61 <div class="stat-card">
62 <div class="stat-icon ratio" :class="getRatioClass(calcRatio)">
63 <el-icon size="32"><TrendCharts /></el-icon>
64 </div>
65 <div class="stat-info">
66 <h3>{{ calcRatio }}</h3>
67 <p>分享率</p>
68 </div>
69 </div>
70
71 <div class="stat-card">
72 <div class="stat-icon points">
73 <el-icon size="32"><Star /></el-icon>
74 </div>
75 <div class="stat-info">
76 <h3>{{ stats.points || '-' }}</h3>
77 <p>积分</p>
78 </div>
79 </div>
80 </div>
81 </div>
82 </div>
83
84 <!-- 详细信息选项卡 -->
85 <div class="profile-content">
86 <el-tabs v-model="activeTab" type="border-card">
87 <!-- 数据统计 -->
88 <el-tab-pane label="数据统计" name="stats">
89 <div class="stats-section" v-if="stats">
90 <div class="stats-overview">
91 <div class="overview-grid">
92 <div class="overview-card">
93 <h3>上传统计</h3>
94 <div class="stat-details">
95 <div class="detail-item">
96 <span class="label">总上传量:</span>
97 <span class="value">{{ stats.uploaded || '-' }}</span>
98 </div>
99 <div class="detail-item">
100 <span class="label">真实上传:</span>
101 <span class="value">{{ stats.realUploaded || '-' }}</span>
102 </div>
103 <div class="detail-item">
104 <span class="label">上传带宽:</span>
105 <span class="value">{{ stats.uploadBandwidth || '-' }}</span>
106 </div>
107 </div>
108 </div>
109
110 <div class="overview-card">
111 <h3>下载统计</h3>
112 <div class="stat-details">
113 <div class="detail-item">
114 <span class="label">总下载量:</span>
115 <span class="value">{{ stats.downloaded || '-' }}</span>
116 </div>
117 <div class="detail-item">
118 <span class="label">真实下载:</span>
119 <span class="value">{{ stats.realDownloaded || '-' }}</span>
120 </div>
121 <div class="detail-item">
122 <span class="label">下载带宽:</span>
123 <span class="value">{{ stats.downloadBandwidth || '-' }}</span>
124 </div>
125 </div>
126 </div>
127
128 <div class="overview-card">
129 <h3>做种统计</h3>
130 <div class="stat-details">
131 <div class="detail-item">
132 <span class="label">正在做种:</span>
133 <span class="value">{{ stats.seeding || '-' }}</span>
134 </div>
135 <div class="detail-item">
136 <span class="label">做种时间:</span>
137 <span class="value">{{ stats.seedingTime || '-' }}</span>
138 </div>
139 <div class="detail-item">
140 <span class="label">做种排名:</span>
141 <span class="value">{{ stats.seedingRank || '-' }}</span>
142 </div>
143 </div>
144 </div>
145
146 <div class="overview-card">
147 <h3>积分记录</h3>
148 <div class="stat-details">
149 <div class="detail-item">
150 <span class="label">当前积分:</span>
151 <span class="value">{{ stats.points || '-' }}</span>
152 </div>
153 <div class="detail-item">
154 <span class="label">邀请名额:</span>
155 <span class="value">{{ stats.inviteSlot || '-' }}</span>
156 </div>
157 <div class="detail-item">
158 <span class="label">ISP:</span>
159 <span class="value">{{ stats.isp || '-' }}</span>
160 </div>
161 </div>
162 </div>
163 </div>
164 </div>
165
166 <!-- 数据图表 -->
167 <div class="charts-section">
168 <div class="chart-card">
169 <h3>上传下载趋势</h3>
170 <div class="chart-placeholder">
171 <el-icon size="48" color="#e4e7ed"><TrendCharts /></el-icon>
172 <p>图表功能开发中...</p>
173 </div>
174 </div>
175 </div>
176 </div>
177 </el-tab-pane>
178
179 <!-- 我的种子 -->
180 <el-tab-pane label="我的种子" name="torrents">
181 <div class="torrents-section">
182 <div class="section-header">
183 <h3>我上传的种子</h3>
184 <el-button type="primary" :icon="Upload" @click="$router.push('/upload')">
185 上传新种子
186 </el-button>
187 </div>
188
189 <el-table :data="userTorrents" stripe>
190 <el-table-column label="种子名称" min-width="300">
191 <template #default="{ row }">
192 <div class="torrent-info">
193 <el-tag :type="getCategoryType(row.category)" size="small">
194 {{ getCategoryName(row.category) }}
195 </el-tag>
196 <span class="torrent-title">{{ row.title }}</span>
197 </div>
198 </template>
199 </el-table-column>
200
201 <el-table-column label="大小" prop="size" width="100" />
202 <el-table-column label="做种" prop="seeders" width="80" align="center" />
203 <el-table-column label="下载" prop="leechers" width="80" align="center" />
204 <el-table-column label="完成" prop="downloads" width="80" align="center" />
205 <el-table-column label="上传时间" width="120">
206 <template #default="{ row }">
207 {{ formatDate(row.uploadTime) }}
208 </template>
209 </el-table-column>
210
211 <el-table-column label="操作" width="120" align="center">
212 <template #default="{ row }">
213 <el-button
214 type="primary"
215 size="small"
216 @click="$router.push(`/torrent/${row.id}`)"
217 >
218 查看
219 </el-button>
220 </template>
221 </el-table-column>
222 </el-table>
223
224 <div class="pagination-wrapper">
225 <el-pagination
226 :current-page="torrentsPage"
227 :page-size="10"
228 :total="userTorrents.length"
229 layout="prev, pager, next"
230 small
231 @current-change="(page) => torrentsPage = page"
232 />
233 </div>
234 </div>
235 </el-tab-pane>
236
237 <!-- 活动记录 -->
238 <el-tab-pane label="活动记录" name="activity">
239 <div class="activity-section">
240 <div class="activity-filters">
241 <el-select v-model="activityFilter" placeholder="活动类型">
242 <el-option label="全部活动" value="" />
243 <el-option label="上传种子" value="upload" />
244 <el-option label="下载种子" value="download" />
245 <el-option label="论坛发帖" value="post" />
246 <el-option label="积分变动" value="points" />
247 </el-select>
248 </div>
249
250 <div class="activity-timeline">
251 <el-timeline>
252 <el-timeline-item
253 v-for="activity in filteredActivities"
254 :key="activity.id"
255 :timestamp="formatTime(activity.time)"
256 :type="getActivityType(activity.type)"
257 >
258 <div class="activity-content">
259 <div class="activity-header">
260 <el-icon>
261 <component :is="getActivityIcon(activity.type)" />
262 </el-icon>
263 <span class="activity-title">{{ activity.title }}</span>
264 </div>
265 <div class="activity-description">{{ activity.description }}</div>
266 </div>
267 </el-timeline-item>
268 </el-timeline>
269 </div>
270 </div>
271 </el-tab-pane>
272 </el-tabs>
273 </div>
274 </div>
275
276 <!-- 更换头像对话框 -->
277 <el-dialog v-model="showAvatarDialog" title="更换头像" width="400px">
278 <div class="avatar-upload">
279 <el-upload
280 ref="avatarUploadRef"
281 :auto-upload="false"
282 :limit="1"
283 accept="image/*"
284 :on-change="handleAvatarChange"
285 list-type="picture-card"
286 class="avatar-uploader"
287 >
288 <el-icon><Plus /></el-icon>
289 </el-upload>
290 <div class="upload-tips">
291 <p>支持 JPG、PNG 格式</p>
292 <p>建议尺寸 200x200 像素</p>
293 <p>文件大小不超过 2MB</p>
294 </div>
295 </div>
296
297 <template #footer>
298 <el-button @click="showAvatarDialog = false">取消</el-button>
299 <el-button type="primary" @click="uploadAvatar" :loading="uploadingAvatar">
300 上传头像
301 </el-button>
302 </template>
303 </el-dialog>
304 </div>
305</template>
306
307<script>
308import { ref, reactive, computed, onMounted, nextTick } from 'vue'
309import { useRouter } from 'vue-router'
310import { ElMessage } from 'element-plus'
311import {
312 Calendar,
313 Clock,
314 Upload,
315 Download,
316 TrendCharts,
317 Star,
318 QuestionFilled,
319 Plus,
320 ChatDotRound,
321 Flag,
322 Coin
323} from '@element-plus/icons-vue'
324import { userApi } from '@/api/user'
325
326export default {
327 name: 'ProfileView',
328 setup() {
329 const router = useRouter()
330 const profileFormRef = ref(null)
331 const passwordFormRef = ref(null)
332 const avatarUploadRef = ref(null)
333 const interestInputRef = ref(null)
334
335 const activeTab = ref('info')
336 const showAvatarDialog = ref(false)
337 const saving = ref(false)
338 const changingPassword = ref(false)
339 const uploadingAvatar = ref(false)
340 const interestInputVisible = ref(false)
341 const interestInputValue = ref('')
342 const activityFilter = ref('')
343 const torrentsPage = ref(1)
344
345 const userProfile = ref({
346 username: 'MovieExpert',
347 email: 'movieexpert@example.com',
348 realName: '',
349 avatar: '',
350 userLevel: 5,
351 userTitle: '资深会员',
352 joinDate: '2023-01-15T10:00:00',
353 lastLogin: '2025-06-03T14:30:00',
354 location: ['北京市', '朝阳区'],
355 signature: '热爱电影,分享快乐!',
356 website: 'https://movieblog.com',
357 interests: ['电影', '音乐', '科技', '摄影'],
358 emailPublic: false,
359 statsPublic: true,
360 activityPublic: true,
361 stats: {
362 uploaded: '256.8 GB',
363 downloaded: '89.6 GB',
364 ratio: '2.87',
365 points: '15,680'
366 },
367 detailedStats: {
368 totalUploaded: '256.8 GB',
369 uploadedTorrents: 45,
370 avgUploadSize: '5.7 GB',
371 totalDownloaded: '89.6 GB',
372 downloadedTorrents: 123,
373 completedTorrents: 118,
374 seeding: 32,
375 seedingTime: '1,245 小时',
376 seedingRank: 86,
377 totalEarnedPoints: '28,940',
378 totalSpentPoints: '13,260'
379 }
380 })
381
382 const editProfile = reactive({
383 username: '',
384 email: '',
385 customTitle: '',
386 realName: '',
387 location: [],
388 signature: '',
389 website: '',
390 interests: [],
391 emailPublic: false,
392 statsPublic: true,
393 activityPublic: true
394 })
395
396 const passwordForm = reactive({
397 currentPassword: '',
398 newPassword: '',
399 confirmPassword: ''
400 })
401
402 const profileRules = {
403 email: [
404 { required: true, message: '请输入邮箱地址', trigger: 'blur' },
405 { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
406 ]
407 }
408
409 const passwordRules = {
410 currentPassword: [
411 { required: true, message: '请输入当前密码', trigger: 'blur' }
412 ],
413 newPassword: [
414 { required: true, message: '请输入新密码', trigger: 'blur' },
415 { min: 6, message: '密码长度至少6个字符', trigger: 'blur' }
416 ],
417 confirmPassword: [
418 { required: true, message: '请确认新密码', trigger: 'blur' },
419 {
420 validator: (rule, value, callback) => {
421 if (value !== passwordForm.newPassword) {
422 callback(new Error('两次输入的密码不一致'))
423 } else {
424 callback()
425 }
426 },
427 trigger: 'blur'
428 }
429 ]
430 }
431
432 const locationOptions = [
433 {
434 value: '北京市',
435 label: '北京市',
436 children: [
437 { value: '朝阳区', label: '朝阳区' },
438 { value: '海淀区', label: '海淀区' },
439 { value: '丰台区', label: '丰台区' }
440 ]
441 },
442 {
443 value: '上海市',
444 label: '上海市',
445 children: [
446 { value: '浦东新区', label: '浦东新区' },
447 { value: '黄浦区', label: '黄浦区' },
448 { value: '静安区', label: '静安区' }
449 ]
450 }
451 ]
452
453 const userTorrents = ref([
454 {
455 id: 1,
456 title: '[4K蓝光原盘] 阿凡达:水之道',
457 category: 'movie',
458 size: '85.6 GB',
459 seeders: 45,
460 leechers: 12,
461 downloads: 234,
462 uploadTime: '2025-05-15T10:00:00'
463 },
464 {
465 id: 2,
466 title: '[FLAC] 古典音乐合集',
467 category: 'music',
468 size: '2.3 GB',
469 seeders: 23,
470 leechers: 5,
471 downloads: 89,
472 uploadTime: '2025-04-20T15:30:00'
473 }
474 ])
475
476 const activities = ref([
477 {
478 id: 1,
479 type: 'upload',
480 title: '上传种子',
481 description: '上传了《阿凡达:水之道》4K蓝光原盘',
482 time: '2025-06-03T10:30:00'
483 },
484 {
485 id: 2,
486 type: 'download',
487 title: '下载种子',
488 description: '下载了《星际穿越》IMAX版本',
489 time: '2025-06-02T14:20:00'
490 },
491 {
492 id: 3,
493 type: 'post',
494 title: '发布主题',
495 description: '在电影讨论区发布了新主题',
496 time: '2025-06-01T16:45:00'
497 },
498 {
499 id: 4,
500 type: 'points',
501 title: '积分变动',
502 description: '做种奖励获得 +50 积分',
503 time: '2025-05-31T09:15:00'
504 }
505 ])
506
507 const loginHistory = ref([
508 {
509 time: '2025-06-03T14:30:00',
510 ip: '192.168.1.100',
511 device: 'Windows 11 / Chrome 120',
512 success: true
513 },
514 {
515 time: '2025-06-02T09:15:00',
516 ip: '192.168.1.100',
517 device: 'Windows 11 / Chrome 120',
518 success: true
519 },
520 {
521 time: '2025-06-01T22:30:00',
522 ip: '192.168.1.100',
523 device: 'Android / Chrome Mobile',
524 success: true
525 }
526 ])
527
528 const filteredActivities = computed(() => {
529 if (!activityFilter.value) return activities.value
530 return activities.value.filter(activity => activity.type === activityFilter.value)
531 })
532
533 const stats = reactive({
534 uploaded: '-',
535 realUploaded: '-',
536 uploadBandwidth: '-',
537 downloaded: '-',
538 realDownloaded: '-',
539 downloadBandwidth: '-',
540 seeding: '-',
541 seedingTime: '-',
542 seedingRank: '-',
543 points: '-',
544 inviteSlot: '-',
545 isp: '-'
546 })
547
548 const calcRatio = computed(() => {
549 if (!stats.uploaded || !stats.downloaded || stats.downloaded === '-' || stats.uploaded === '-') return '-'
550 const parseGB = (str) => {
551 if (typeof str !== 'string') return 0
552 if (str.endsWith('TB')) return parseFloat(str) * 1024
553 if (str.endsWith('GB')) return parseFloat(str)
554 if (str.endsWith('MB')) return parseFloat(str) / 1024
555 if (str.endsWith('KB')) return parseFloat(str) / 1024 / 1024
556 if (str.endsWith('B')) return parseFloat(str) / 1024 / 1024 / 1024
557 return parseFloat(str)
558 }
559 const up = parseGB(stats.uploaded)
560 const down = parseGB(stats.downloaded)
561 if (!down) return up > 0 ? '∞' : '0.00'
562 return (up / down).toFixed(2)
563 })
564
565 const userBase = reactive({
566 username: '-',
567 joinDate: '-',
568 })
569
570 onMounted(async () => {
571 try {
572 const res = await userApi.getCurrentUser()
573 if (res.user && res.user.user) {
574 const u = res.user.user
575 stats.uploaded = formatBytes(u.uploaded)
576 stats.realUploaded = formatBytes(u.realUploaded)
577 stats.uploadBandwidth = u.uploadBandwidth || '-'
578 stats.downloaded = formatBytes(u.downloaded)
579 stats.realDownloaded = formatBytes(u.realDownloaded)
580 stats.downloadBandwidth = u.downloadBandwidth || '-'
581 stats.seeding = u.seeding || '-'
582 stats.seedingTime = formatSeedingTime(u.seedingTime)
583 stats.seedingRank = u.seedingRank || '-'
584 stats.points = u.karma || '-'
585 stats.inviteSlot = u.inviteSlot || '-'
586 stats.isp = u.isp || '-'
587 userBase.username = u.username || '-'
588 userBase.joinDate = u.createdAt ? formatDate(u.createdAt) : '-'
589 }
590 } catch (e) {
591 ElMessage.error('获取数据失败')
592 }
593 })
594
595 const getUserTitleByLevel = (level) => {
596 if (level == null) return '新手'
597 if (level >= 8) return '管理员'
598 if (level >= 6) return '资深会员'
599 if (level >= 4) return '正式会员'
600 if (level >= 2) return '初级会员'
601 return '新手'
602 }
603
604 const formatBytes = (bytes) => {
605 if (!bytes || isNaN(bytes)) return '0 B'
606 const k = 1024
607 const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
608 const i = Math.floor(Math.log(bytes) / Math.log(k))
609 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
610 }
611
612 const formatSeedingTime = (seconds) => {
613 if (!seconds || isNaN(seconds)) return '0 小时'
614 const hours = Math.floor(seconds / 3600)
615 const days = Math.floor(hours / 24)
616 if (days > 0) {
617 return `${days} ${hours % 24} 小时`
618 } else {
619 return `${hours} 小时`
620 }
621 }
622
623 const formatDate = (dateString) => {
624 const date = new Date(dateString)
625 return date.toLocaleDateString('zh-CN')
626 }
627
628 const formatTime = (timeString) => {
629 const date = new Date(timeString)
630 const now = new Date()
631 const diff = now - date
632 const hours = Math.floor(diff / (1000 * 60 * 60))
633
634 if (hours < 1) return '刚刚'
635 if (hours < 24) return `${hours}小时前`
636 const days = Math.floor(hours / 24)
637 if (days < 7) return `${days}天前`
638
639 return date.toLocaleDateString('zh-CN')
640 }
641
642 const formatDateTime = (dateString) => {
643 const date = new Date(dateString)
644 return date.toLocaleString('zh-CN')
645 }
646
647 const getUserTitleType = (level) => {
648 if (level >= 8) return 'danger' // 管理员
649 if (level >= 6) return 'warning' // 资深会员
650 if (level >= 4) return 'success' // 正式会员
651 if (level >= 2) return 'info' // 初级会员
652 return 'default' // 新手
653 }
654
655 const getRatioClass = (ratio) => {
656 const r = parseFloat(ratio)
657 if (r >= 2) return 'excellent'
658 if (r >= 1) return 'good'
659 return 'warning'
660 }
661
662 const getCategoryType = (category) => {
663 const types = {
664 'movie': 'primary',
665 'tv': 'info',
666 'music': 'success',
667 'software': 'warning',
668 'game': 'danger'
669 }
670 return types[category] || 'default'
671 }
672
673 const getCategoryName = (category) => {
674 const names = {
675 'movie': '电影',
676 'tv': '电视剧',
677 'music': '音乐',
678 'software': '软件',
679 'game': '游戏'
680 }
681 return names[category] || category
682 }
683
684 const getLanguageName = (language) => {
685 const languages = {
686 'zh-CN': '简体中文',
687 'zh-TW': '繁体中文',
688 'en-US': 'English',
689 'ja-JP': '日本語',
690 'ko-KR': '한국어'
691 }
692 return languages[language] || language
693 }
694
695 const getActivityType = (type) => {
696 const types = {
697 'upload': 'success',
698 'download': 'primary',
699 'post': 'warning',
700 'points': 'info'
701 }
702 return types[type] || 'primary'
703 }
704
705 const getActivityIcon = (type) => {
706 const icons = {
707 'upload': 'Upload',
708 'download': 'Download',
709 'post': 'ChatDotRound',
710 'points': 'Coin'
711 }
712 return icons[type] || 'Star'
713 }
714
715 const showInterestInput = () => {
716 interestInputVisible.value = true
717 nextTick(() => {
718 interestInputRef.value?.focus()
719 })
720 }
721
722 const addInterest = () => {
723 const interest = interestInputValue.value.trim()
724 if (interest && !editProfile.interests.includes(interest)) {
725 editProfile.interests.push(interest)
726 }
727 interestInputVisible.value = false
728 interestInputValue.value = ''
729 }
730
731 const removeInterest = (interest) => {
732 const index = editProfile.interests.indexOf(interest)
733 if (index > -1) {
734 editProfile.interests.splice(index, 1)
735 }
736 }
737
738 const saveProfile = async () => {
739 try {
740 const payload = {
741 username: editProfile.username,
742 email: editProfile.email
743 }
744 await userApi.updateProfile(payload)
745 ElMessage.success('保存成功')
746 } catch (error) {
747 ElMessage.error('保存失败')
748 }
749 }
750
751 const resetProfile = () => {
752 loadUserProfile()
753 ElMessage.info('已重置为原始数据')
754 }
755
756 const changePassword = async () => {
757 try {
758 await passwordFormRef.value?.validate()
759
760 changingPassword.value = true
761
762 // 模拟密码修改过程
763 await new Promise(resolve => setTimeout(resolve, 1500))
764
765 // 重置表单
766 passwordFormRef.value?.resetFields()
767 Object.assign(passwordForm, {
768 currentPassword: '',
769 newPassword: '',
770 confirmPassword: ''
771 })
772
773 ElMessage.success('密码修改成功')
774
775 } catch (error) {
776 console.error('表单验证失败:', error)
777 } finally {
778 changingPassword.value = false
779 }
780 }
781
782 const handleAvatarChange = (file) => {
783 const isImage = file.raw.type.startsWith('image/')
784 const isLt2M = file.raw.size / 1024 / 1024 < 2
785
786 if (!isImage) {
787 ElMessage.error('只能上传图片文件!')
788 return false
789 }
790 if (!isLt2M) {
791 ElMessage.error('图片大小不能超过 2MB!')
792 return false
793 }
794
795 return true
796 }
797
798 const uploadAvatar = async () => {
799 const files = avatarUploadRef.value?.uploadFiles
800 if (!files || files.length === 0) {
801 ElMessage.warning('请选择头像文件')
802 return
803 }
804
805 uploadingAvatar.value = true
806 try {
807 // 模拟上传过程
808 await new Promise(resolve => setTimeout(resolve, 2000))
809
810 // 更新头像URL
811 userProfile.value.avatar = URL.createObjectURL(files[0].raw)
812
813 ElMessage.success('头像上传成功')
814 showAvatarDialog.value = false
815 avatarUploadRef.value?.clearFiles()
816
817 } catch (error) {
818 ElMessage.error('头像上传失败')
819 } finally {
820 uploadingAvatar.value = false
821 }
822 }
823
824 return {
825 activeTab,
826 showAvatarDialog,
827 saving,
828 changingPassword,
829 uploadingAvatar,
830 interestInputVisible,
831 interestInputValue,
832 activityFilter,
833 torrentsPage,
834 userProfile,
835 editProfile,
836 passwordForm,
837 profileRules,
838 passwordRules,
839 locationOptions,
840 userTorrents,
841 filteredActivities,
842 loginHistory,
843 profileFormRef,
844 passwordFormRef,
845 avatarUploadRef,
846 interestInputRef,
847 formatDate,
848 formatTime,
849 formatDateTime,
850 getUserTitleType,
851 getRatioClass,
852 getCategoryType,
853 getCategoryName,
854 getActivityType,
855 getActivityIcon,
856 showInterestInput,
857 addInterest,
858 removeInterest,
859 saveProfile,
860 resetProfile,
861 changePassword,
862 handleAvatarChange,
863 uploadAvatar,
864 Calendar,
865 Clock,
866 Upload,
867 Download,
868 TrendCharts,
869 Star,
870 QuestionFilled,
871 Plus,
872 ChatDotRound,
873 Flag,
874 Coin,
875 stats,
876 calcRatio,
877 userBase
878 }
879 }
880}
881</script>
882
883<style lang="scss" scoped>
884.profile-page {
885 max-width: 1200px;
886 margin: 0 auto;
887 padding: 24px;
888 background: #f5f5f5;
889 min-height: 100vh;
890}
891
892.profile-header {
893 background: #fff;
894 border-radius: 12px;
895 padding: 32px;
896 margin-bottom: 24px;
897 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
898
899 display: grid;
900 grid-template-columns: 1fr 1fr;
901 gap: 32px;
902
903 .user-avatar-section {
904 display: flex;
905 gap: 24px;
906
907 .avatar-container {
908 text-align: center;
909
910 .change-avatar-btn {
911 margin-top: 12px;
912 }
913 }
914
915 .user-basic-info {
916 flex: 1;
917
918 .username {
919 font-size: 28px;
920 font-weight: 600;
921 color: #2c3e50;
922 margin: 0 0 12px 0;
923 }
924
925 .user-title {
926 margin-bottom: 16px;
927 }
928
929 .join-info, .last-login {
930 display: flex;
931 align-items: center;
932 gap: 8px;
933 font-size: 14px;
934 color: #7f8c8d;
935 margin-bottom: 8px;
936 }
937 }
938 }
939
940 .user-stats-overview {
941 .stats-grid {
942 display: grid;
943 grid-template-columns: repeat(2, 1fr);
944 gap: 16px;
945
946 .stat-card {
947 background: #f8f9fa;
948 border-radius: 8px;
949 padding: 20px;
950 display: flex;
951 align-items: center;
952 gap: 16px;
953
954 .stat-icon {
955 width: 48px;
956 height: 48px;
957 border-radius: 50%;
958 display: flex;
959 align-items: center;
960 justify-content: center;
961
962 &.upload { background: rgba(103, 194, 58, 0.1); color: #67c23a; }
963 &.download { background: rgba(64, 158, 255, 0.1); color: #409eff; }
964 &.ratio {
965 &.excellent { background: rgba(103, 194, 58, 0.1); color: #67c23a; }
966 &.good { background: rgba(230, 162, 60, 0.1); color: #e6a23c; }
967 &.warning { background: rgba(245, 108, 108, 0.1); color: #f56c6c; }
968 }
969 &.points { background: rgba(245, 108, 108, 0.1); color: #f56c6c; }
970 }
971
972 .stat-info {
973 h3 {
974 font-size: 20px;
975 font-weight: 600;
976 color: #2c3e50;
977 margin: 0 0 4px 0;
978 }
979
980 p {
981 font-size: 14px;
982 color: #7f8c8d;
983 margin: 0;
984 }
985 }
986 }
987 }
988 }
989}
990
991.profile-content {
992 background: #fff;
993 border-radius: 12px;
994 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
995
996 :deep(.el-tabs__content) {
997 padding: 24px;
998 }
999}
1000
1001.info-section {
1002 .form-section {
1003 margin-bottom: 32px;
1004
1005 h3 {
1006 font-size: 18px;
1007 font-weight: 600;
1008 color: #2c3e50;
1009 margin: 0 0 20px 0;
1010 padding-bottom: 8px;
1011 border-bottom: 2px solid #f0f0f0;
1012 }
1013 }
1014
1015 .interests-input {
1016 display: flex;
1017 flex-wrap: wrap;
1018 gap: 8px;
1019 align-items: center;
1020
1021 .interest-tag {
1022 margin: 0;
1023 }
1024 }
1025
1026 .setting-tip {
1027 margin-left: 12px;
1028 font-size: 12px;
1029 color: #909399;
1030 }
1031
1032 .form-actions {
1033 text-align: center;
1034 margin-top: 32px;
1035
1036 .el-button {
1037 margin: 0 8px;
1038 min-width: 100px;
1039 }
1040 }
1041}
1042
1043.stats-section {
1044 .stats-overview {
1045 margin-bottom: 32px;
1046
1047 .overview-grid {
1048 display: grid;
1049 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1050 gap: 20px;
1051
1052 .overview-card {
1053 background: #f8f9fa;
1054 border-radius: 8px;
1055 padding: 24px;
1056
1057 h3 {
1058 font-size: 16px;
1059 font-weight: 600;
1060 color: #2c3e50;
1061 margin: 0 0 16px 0;
1062 }
1063
1064 .stat-details {
1065 .detail-item {
1066 display: flex;
1067 justify-content: space-between;
1068 align-items: center;
1069 margin-bottom: 12px;
1070
1071 .label {
1072 font-size: 14px;
1073 color: #7f8c8d;
1074 }
1075
1076 .value {
1077 font-size: 14px;
1078 font-weight: 600;
1079 color: #2c3e50;
1080 }
1081 }
1082 }
1083 }
1084 }
1085 }
1086
1087 .charts-section {
1088 .chart-card {
1089 background: #f8f9fa;
1090 border-radius: 8px;
1091 padding: 24px;
1092
1093 h3 {
1094 font-size: 16px;
1095 font-weight: 600;
1096 color: #2c3e50;
1097 margin: 0 0 20px 0;
1098 }
1099
1100 .chart-placeholder {
1101 text-align: center;
1102 padding: 60px 0;
1103 color: #909399;
1104
1105 p {
1106 margin: 12px 0 0 0;
1107 }
1108 }
1109 }
1110 }
1111}
1112
1113.torrents-section {
1114 .section-header {
1115 display: flex;
1116 justify-content: space-between;
1117 align-items: center;
1118 margin-bottom: 20px;
1119
1120 h3 {
1121 font-size: 18px;
1122 font-weight: 600;
1123 color: #2c3e50;
1124 margin: 0;
1125 }
1126 }
1127
1128 .torrent-info {
1129 display: flex;
1130 align-items: center;
1131 gap: 12px;
1132
1133 .torrent-title {
1134 font-weight: 500;
1135 }
1136 }
1137
1138 .pagination-wrapper {
1139 margin-top: 16px;
1140 text-align: center;
1141 }
1142}
1143
1144.activity-section {
1145 .activity-filters {
1146 margin-bottom: 24px;
1147
1148 .el-select {
1149 width: 150px;
1150 }
1151 }
1152
1153 .activity-timeline {
1154 .activity-content {
1155 .activity-header {
1156 display: flex;
1157 align-items: center;
1158 gap: 8px;
1159 margin-bottom: 8px;
1160
1161 .activity-title {
1162 font-weight: 600;
1163 color: #2c3e50;
1164 }
1165 }
1166
1167 .activity-description {
1168 font-size: 14px;
1169 color: #7f8c8d;
1170 line-height: 1.5;
1171 }
1172 }
1173 }
1174}
1175
1176.avatar-upload {
1177 text-align: center;
1178
1179 .avatar-uploader {
1180 margin-bottom: 16px;
1181 }
1182
1183 .upload-tips {
1184 font-size: 12px;
1185 color: #909399;
1186
1187 p {
1188 margin: 4px 0;
1189 }
1190 }
1191}
1192
1193@media (max-width: 768px) {
1194 .profile-page {
1195 padding: 16px;
1196 }
1197
1198 .profile-header {
1199 grid-template-columns: 1fr;
1200 gap: 24px;
1201
1202 .user-avatar-section {
1203 flex-direction: column;
1204 text-align: center;
1205 }
1206
1207 .user-stats-overview .stats-grid {
1208 grid-template-columns: 1fr;
1209 }
1210 }
1211
1212 .stats-overview .overview-grid {
1213 grid-template-columns: 1fr;
1214 }
1215
1216 .torrents-section .section-header {
1217 flex-direction: column;
1218 gap: 16px;
1219 align-items: flex-start;
1220 }
1221}
1222</style>