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