| <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> |