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