Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 1 | <template>
|
| 2 | <div class="profile-page">
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 3 | <Navbar />
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 4 | <div class="page-container">
|
| 5 | <!-- 个人信息卡片 -->
|
| 6 | <div class="profile-header">
|
| 7 | <div class="user-avatar-section">
|
| 8 | <div class="avatar-container">
|
| 9 | <el-avatar :size="120" :src="userProfile.avatar">
|
| 10 | {{ userProfile.username.charAt(0).toUpperCase() }}
|
| 11 | </el-avatar>
|
| 12 | <el-button
|
| 13 | type="primary"
|
| 14 | size="small"
|
| 15 | class="change-avatar-btn"
|
| 16 | @click="showAvatarDialog = true"
|
| 17 | >
|
| 18 | 更换头像
|
| 19 | </el-button>
|
| 20 | </div>
|
| 21 |
|
| 22 | <div class="user-basic-info">
|
| 23 | <h1 class="username">{{ userProfile.username }}</h1>
|
| 24 | <div class="user-title">
|
| 25 | <el-tag :type="getUserTitleType(userProfile.userLevel)" size="large">
|
| 26 | {{ userProfile.userTitle }}
|
| 27 | </el-tag>
|
| 28 | </div>
|
| 29 | <div class="join-info">
|
| 30 | <el-icon><Calendar /></el-icon>
|
| 31 | <span>加入时间:{{ formatDate(userProfile.joinDate) }}</span>
|
| 32 | </div>
|
| 33 | <div class="last-login">
|
| 34 | <el-icon><Clock /></el-icon>
|
| 35 | <span>最后登录:{{ formatTime(userProfile.lastLogin) }}</span>
|
| 36 | </div>
|
| 37 | </div>
|
| 38 | </div>
|
| 39 |
|
| 40 | <div class="user-stats-overview">
|
| 41 | <div class="stats-grid">
|
| 42 | <div class="stat-card">
|
| 43 | <div class="stat-icon upload">
|
| 44 | <el-icon size="32"><Upload /></el-icon>
|
| 45 | </div>
|
| 46 | <div class="stat-info">
|
| 47 | <h3>{{ userProfile.stats.uploaded }}</h3>
|
| 48 | <p>上传量</p>
|
| 49 | </div>
|
| 50 | </div>
|
| 51 |
|
| 52 | <div class="stat-card">
|
| 53 | <div class="stat-icon download">
|
| 54 | <el-icon size="32"><Download /></el-icon>
|
| 55 | </div>
|
| 56 | <div class="stat-info">
|
| 57 | <h3>{{ userProfile.stats.downloaded }}</h3>
|
| 58 | <p>下载量</p>
|
| 59 | </div>
|
| 60 | </div>
|
| 61 |
|
| 62 | <div class="stat-card">
|
| 63 | <div class="stat-icon ratio" :class="getRatioClass(userProfile.stats.ratio)">
|
| 64 | <el-icon size="32"><TrendCharts /></el-icon>
|
| 65 | </div>
|
| 66 | <div class="stat-info">
|
| 67 | <h3>{{ userProfile.stats.ratio }}</h3>
|
| 68 | <p>分享率</p>
|
| 69 | </div>
|
| 70 | </div>
|
| 71 |
|
| 72 | <div class="stat-card">
|
| 73 | <div class="stat-icon points">
|
| 74 | <el-icon size="32"><Star /></el-icon>
|
| 75 | </div>
|
| 76 | <div class="stat-info">
|
| 77 | <h3>{{ userProfile.stats.points }}</h3>
|
| 78 | <p>积分</p>
|
| 79 | </div>
|
| 80 | </div>
|
| 81 | </div>
|
| 82 | </div>
|
| 83 | </div>
|
| 84 |
|
| 85 | <!-- 详细信息选项卡 -->
|
| 86 | <div class="profile-content">
|
| 87 | <el-tabs v-model="activeTab" type="border-card">
|
| 88 | <!-- 个人信息 -->
|
| 89 | <el-tab-pane label="个人信息" name="info">
|
| 90 | <div class="info-section">
|
| 91 | <el-form
|
| 92 | ref="profileFormRef"
|
| 93 | :model="editProfile"
|
| 94 | :rules="profileRules"
|
| 95 | label-width="120px"
|
| 96 | size="large"
|
| 97 | >
|
| 98 | <div class="form-section">
|
| 99 | <h3>基本信息</h3>
|
| 100 | <el-form-item label="用户名">
|
| 101 | <el-input v-model="editProfile.username" disabled>
|
| 102 | <template #suffix>
|
| 103 | <el-tooltip content="用户名不可修改">
|
| 104 | <el-icon><QuestionFilled /></el-icon>
|
| 105 | </el-tooltip>
|
| 106 | </template>
|
| 107 | </el-input>
|
| 108 | </el-form-item>
|
| 109 |
|
| 110 | <el-form-item label="邮箱地址" prop="email">
|
| 111 | <el-input v-model="editProfile.email" type="email" />
|
| 112 | </el-form-item>
|
| 113 |
|
| 114 | <el-form-item label="真实姓名" prop="realName">
|
| 115 | <el-input v-model="editProfile.realName" placeholder="可选填写" />
|
| 116 | </el-form-item>
|
| 117 |
|
| 118 | <el-form-item label="所在地区">
|
| 119 | <el-cascader
|
| 120 | v-model="editProfile.location"
|
| 121 | :options="locationOptions"
|
| 122 | placeholder="请选择地区"
|
| 123 | clearable
|
| 124 | />
|
| 125 | </el-form-item>
|
| 126 | </div>
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 127 | </el-form>
|
| 128 | </div>
|
| 129 | </el-tab-pane>
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 130 | </el-tabs>
|
| 131 | </div>
|
| 132 | </div>
|
| 133 |
|
| 134 | <!-- 更换头像对话框 -->
|
| 135 | <el-dialog v-model="showAvatarDialog" title="更换头像" width="400px">
|
| 136 | <div class="avatar-upload">
|
| 137 | <el-upload
|
| 138 | ref="avatarUploadRef"
|
| 139 | :auto-upload="false"
|
| 140 | :limit="1"
|
| 141 | accept="image/*"
|
| 142 | :on-change="handleAvatarChange"
|
| 143 | list-type="picture-card"
|
| 144 | class="avatar-uploader"
|
| 145 | >
|
| 146 | <el-icon><Plus /></el-icon>
|
| 147 | </el-upload>
|
| 148 | <div class="upload-tips">
|
| 149 | <p>支持 JPG、PNG 格式</p>
|
| 150 | <p>建议尺寸 200x200 像素</p>
|
| 151 | <p>文件大小不超过 2MB</p>
|
| 152 | </div>
|
| 153 | </div>
|
| 154 |
|
| 155 | <template #footer>
|
| 156 | <el-button @click="showAvatarDialog = false">取消</el-button>
|
| 157 | <el-button type="primary" @click="uploadAvatar" :loading="uploadingAvatar">
|
| 158 | 上传头像
|
| 159 | </el-button>
|
| 160 | </template>
|
| 161 | </el-dialog>
|
| 162 | </div>
|
| 163 | </template>
|
| 164 |
|
| 165 | <script>
|
| 166 | import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 167 | import { ElMessage } from 'element-plus'
|
| 168 | import {
|
| 169 | Calendar,
|
| 170 | Clock,
|
| 171 | Upload,
|
| 172 | Download,
|
| 173 | TrendCharts,
|
| 174 | Star,
|
| 175 | QuestionFilled,
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 176 | Plus
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 177 | } from '@element-plus/icons-vue'
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 178 | import Navbar from '@/components/Navbar.vue'
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 179 |
|
| 180 | export default {
|
| 181 | name: 'ProfileView',
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 182 | components: {
|
| 183 | Navbar
|
| 184 | },
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 185 | setup() {
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 186 | const activeTab = ref('info')
|
| 187 | const showAvatarDialog = ref(false)
|
| 188 | const saving = ref(false)
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 189 | const uploadingAvatar = ref(false)
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 190 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 191 | const userProfile = ref({
|
| 192 | username: 'MovieExpert',
|
| 193 | email: 'movieexpert@example.com',
|
| 194 | realName: '',
|
| 195 | avatar: '',
|
| 196 | userLevel: 5,
|
| 197 | userTitle: '资深会员',
|
| 198 | joinDate: '2023-01-15T10:00:00',
|
| 199 | lastLogin: '2025-06-03T14:30:00',
|
| 200 | location: ['北京市', '朝阳区'],
|
| 201 | signature: '热爱电影,分享快乐!',
|
| 202 | website: 'https://movieblog.com',
|
| 203 | interests: ['电影', '音乐', '科技', '摄影'],
|
| 204 | emailPublic: false,
|
| 205 | statsPublic: true,
|
| 206 | activityPublic: true,
|
| 207 | stats: {
|
| 208 | uploaded: '256.8 GB',
|
| 209 | downloaded: '89.6 GB',
|
| 210 | ratio: '2.87',
|
| 211 | points: '15,680'
|
| 212 | },
|
| 213 | detailedStats: {
|
| 214 | totalUploaded: '256.8 GB',
|
| 215 | uploadedTorrents: 45,
|
| 216 | avgUploadSize: '5.7 GB',
|
| 217 | totalDownloaded: '89.6 GB',
|
| 218 | downloadedTorrents: 123,
|
| 219 | completedTorrents: 118,
|
| 220 | seeding: 32,
|
| 221 | seedingTime: '1,245 小时',
|
| 222 | seedingRank: 86,
|
| 223 | totalEarnedPoints: '28,940',
|
| 224 | totalSpentPoints: '13,260'
|
| 225 | }
|
| 226 | })
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 227 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 228 | const editProfile = reactive({
|
| 229 | username: '',
|
| 230 | email: '',
|
| 231 | realName: '',
|
| 232 | location: [],
|
| 233 | signature: '',
|
| 234 | website: '',
|
| 235 | interests: [],
|
| 236 | emailPublic: false,
|
| 237 | statsPublic: true,
|
| 238 | activityPublic: true
|
| 239 | })
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 240 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 241 | const profileRules = {
|
| 242 | email: [
|
| 243 | { required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
| 244 | { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
| 245 | ]
|
| 246 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 247 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 248 | const locationOptions = [
|
| 249 | {
|
| 250 | value: '北京市',
|
| 251 | label: '北京市',
|
| 252 | children: [
|
| 253 | { value: '朝阳区', label: '朝阳区' },
|
| 254 | { value: '海淀区', label: '海淀区' },
|
| 255 | { value: '丰台区', label: '丰台区' }
|
| 256 | ]
|
| 257 | },
|
| 258 | {
|
| 259 | value: '上海市',
|
| 260 | label: '上海市',
|
| 261 | children: [
|
| 262 | { value: '浦东新区', label: '浦东新区' },
|
| 263 | { value: '黄浦区', label: '黄浦区' },
|
| 264 | { value: '静安区', label: '静安区' }
|
| 265 | ]
|
| 266 | }
|
| 267 | ]
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 268 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 269 | onMounted(() => {
|
| 270 | loadUserProfile()
|
| 271 | })
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 272 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 273 | const loadUserProfile = () => {
|
| 274 | // 加载用户资料到编辑表单
|
| 275 | Object.assign(editProfile, {
|
| 276 | username: userProfile.value.username,
|
| 277 | email: userProfile.value.email,
|
| 278 | realName: userProfile.value.realName,
|
| 279 | location: userProfile.value.location,
|
| 280 | signature: userProfile.value.signature,
|
| 281 | website: userProfile.value.website,
|
| 282 | interests: [...userProfile.value.interests],
|
| 283 | emailPublic: userProfile.value.emailPublic,
|
| 284 | statsPublic: userProfile.value.statsPublic,
|
| 285 | activityPublic: userProfile.value.activityPublic
|
| 286 | })
|
| 287 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 288 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 289 | const formatDate = (dateString) => {
|
| 290 | const date = new Date(dateString)
|
| 291 | return date.toLocaleDateString('zh-CN')
|
| 292 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 293 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 294 | const formatTime = (timeString) => {
|
| 295 | const date = new Date(timeString)
|
| 296 | const now = new Date()
|
| 297 | const diff = now - date
|
| 298 | const hours = Math.floor(diff / (1000 * 60 * 60))
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 299 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 300 | if (hours < 1) return '刚刚'
|
| 301 | if (hours < 24) return `${hours}小时前`
|
| 302 | const days = Math.floor(hours / 24)
|
| 303 | if (days < 7) return `${days}天前`
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 304 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 305 | return date.toLocaleDateString('zh-CN')
|
| 306 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 307 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 308 | const getUserTitleType = (level) => {
|
| 309 | if (level >= 8) return 'danger' // 管理员
|
| 310 | if (level >= 6) return 'warning' // 资深会员
|
| 311 | if (level >= 4) return 'success' // 正式会员
|
| 312 | if (level >= 2) return 'info' // 初级会员
|
| 313 | return 'default' // 新手
|
| 314 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 315 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 316 | const getRatioClass = (ratio) => {
|
| 317 | const r = parseFloat(ratio)
|
| 318 | if (r >= 2) return 'excellent'
|
| 319 | if (r >= 1) return 'good'
|
| 320 | return 'warning'
|
| 321 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 322 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 323 | const saveProfile = async () => {
|
| 324 | try {
|
| 325 | await profileFormRef.value?.validate()
|
| 326 |
|
| 327 | saving.value = true
|
| 328 |
|
| 329 | // 模拟保存过程
|
| 330 | await new Promise(resolve => setTimeout(resolve, 1500))
|
| 331 |
|
| 332 | // 更新用户资料
|
| 333 | Object.assign(userProfile.value, editProfile)
|
| 334 |
|
| 335 | ElMessage.success('个人资料保存成功')
|
| 336 |
|
| 337 | } catch (error) {
|
| 338 | console.error('表单验证失败:', error)
|
| 339 | } finally {
|
| 340 | saving.value = false
|
| 341 | }
|
| 342 | }
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 343 |
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 344 | return {
|
| 345 | activeTab,
|
| 346 | showAvatarDialog,
|
| 347 | saving,
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 348 | userProfile,
|
| 349 | editProfile,
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 350 | profileRules,
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 351 | locationOptions,
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 352 | formatDate,
|
| 353 | formatTime,
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 354 | getUserTitleType,
|
| 355 | getRatioClass,
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 356 | saveProfile
|
Xing Jinwen | ff16b1e | 2025-06-05 00:29:26 +0800 | [diff] [blame] | 357 | }
|
| 358 | }
|
| 359 | }
|
| 360 | </script>
|
| 361 |
|
Xing Jinwen | f711e2e | 2025-06-07 21:57:01 +0800 | [diff] [blame] | 362 | <style scoped>
|
| 363 | /* 添加相关样式 */
|
| 364 | </style>
|