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