| <template> |
| <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' |
| |
| export default { |
| name: 'ProfileView', |
| 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> |