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