blob: b10ad9e3e0e6ef579c94e954a832d9bd0f9de009 [file] [log] [blame] [edit]
<template>
<div class="profile-page">
<Navbar />s
<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">{{ userBase.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>加入时间:{{ userBase.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>{{ 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>{{ stats.downloaded || '-' }}</h3>
<p>下载量</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon ratio" :class="getRatioClass(calcRatio)">
<el-icon size="32"><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<h3>{{ calcRatio }}</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>{{ 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="stats">
<div class="stats-section" v-if="stats">
<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">{{ stats.uploaded || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">真实上传:</span>
<span class="value">{{ stats.realUploaded || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">上传带宽:</span>
<span class="value">{{ stats.uploadBandwidth || '-' }}</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">{{ stats.downloaded || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">真实下载:</span>
<span class="value">{{ stats.realDownloaded || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">下载带宽:</span>
<span class="value">{{ stats.downloadBandwidth || '-' }}</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">{{ stats.seeding || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">做种时间:</span>
<span class="value">{{ stats.seedingTime || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">做种排名:</span>
<span class="value">{{ stats.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">{{ stats.points || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">邀请名额:</span>
<span class="value">{{ stats.inviteSlot || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">ISP:</span>
<span class="value">{{ stats.isp || '-' }}</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
:current-page="torrentsPage"
:page-size="10"
:total="userTorrents.length"
layout="prev, pager, next"
small
@current-change="(page) => torrentsPage = page"
/>
</div>
</div>
</el-tab-pane>
<!-- &lt;!&ndash; 活动记录 &ndash;&gt;-->
<!-- <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-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 { userApi } from '@/api/user'
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: '',
customTitle: '',
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)
})
const stats = reactive({
uploaded: '-',
realUploaded: '-',
uploadBandwidth: '-',
downloaded: '-',
realDownloaded: '-',
downloadBandwidth: '-',
seeding: '-',
seedingTime: '-',
seedingRank: '-',
points: '-',
inviteSlot: '-',
isp: '-'
})
const calcRatio = computed(() => {
if (!stats.uploaded || !stats.downloaded || stats.downloaded === '-' || stats.uploaded === '-') return '-'
const parseGB = (str) => {
if (typeof str !== 'string') return 0
if (str.endsWith('TB')) return parseFloat(str) * 1024
if (str.endsWith('GB')) return parseFloat(str)
if (str.endsWith('MB')) return parseFloat(str) / 1024
if (str.endsWith('KB')) return parseFloat(str) / 1024 / 1024
if (str.endsWith('B')) return parseFloat(str) / 1024 / 1024 / 1024
return parseFloat(str)
}
const up = parseGB(stats.uploaded)
const down = parseGB(stats.downloaded)
if (!down) return up > 0 ? '∞' : '0.00'
return (up / down).toFixed(2)
})
const userBase = reactive({
username: '-',
joinDate: '-',
})
onMounted(async () => {
try {
const res = await userApi.getCurrentUser()
if (res.user && res.user.user) {
const u = res.user.user
stats.uploaded = formatBytes(u.uploaded)
stats.realUploaded = formatBytes(u.realUploaded)
stats.uploadBandwidth = u.uploadBandwidth || '-'
stats.downloaded = formatBytes(u.downloaded)
stats.realDownloaded = formatBytes(u.realDownloaded)
stats.downloadBandwidth = u.downloadBandwidth || '-'
stats.seeding = u.seeding || '-'
stats.seedingTime = formatSeedingTime(u.seedingTime)
stats.seedingRank = u.seedingRank || '-'
stats.points = u.karma || '-'
stats.inviteSlot = u.inviteSlot || '-'
stats.isp = u.isp || '-'
userBase.username = u.username || '-'
userBase.joinDate = u.createdAt ? formatDate(u.createdAt) : '-'
}
} catch (e) {
ElMessage.error('获取数据失败')
}
})
const getUserTitleByLevel = (level) => {
if (level == null) return '新手'
if (level >= 8) return '管理员'
if (level >= 6) return '资深会员'
if (level >= 4) return '正式会员'
if (level >= 2) return '初级会员'
return '新手'
}
const formatBytes = (bytes) => {
if (!bytes || isNaN(bytes)) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const formatSeedingTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0 小时'
const hours = Math.floor(seconds / 3600)
const days = Math.floor(hours / 24)
if (days > 0) {
return `${days} ${hours % 24} 小时`
} else {
return `${hours} 小时`
}
}
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 getLanguageName = (language) => {
const languages = {
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'en-US': 'English',
'ja-JP': '日本語',
'ko-KR': '한국어'
}
return languages[language] || language
}
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 {
const payload = {
username: editProfile.username,
email: editProfile.email
}
await userApi.updateProfile(payload)
ElMessage.success('保存成功')
} catch (error) {
ElMessage.error('保存失败')
}
}
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,
stats,
calcRatio,
userBase
}
}
}
</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;
}
}
}
}
.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>