个人中心功能概述
个人中心是用户管理自己信息和查看个人数据的重要入口。本次完善优化了个人信息管理、投稿展示、排名显示、任务追踪等多个功能模块。
完善目标
- 优化个人信息管理
- 改进投稿展示界面
- 完善排名显示逻辑
- 集成任务追踪功能
- 提升整体用户体验
个人信息管理
用户信息展示
<template>
<div class="profile-header">
<div class="avatar-section">
<el-avatar :src="userInfo.avatar" :size="100">
</el-avatar>
<el-button class="edit-avatar" circle size="small">
<el-icon><Camera /></el-icon>
</el-button>
</div>
<div class="info-section">
<h2 class="username"></h2>
<div class="user-tags">
<el-tag
v-for="role in userRoles"
:key="role"
:type="getRoleType(role)"
size="small"
>
</el-tag>
</div>
<div class="user-stats">
<div class="stat-item">
<span class="stat-value"></span>
<span class="stat-label">投稿</span>
</div>
<div class="stat-item">
<span class="stat-value"></span>
<span class="stat-label">评论</span>
</div>
<div class="stat-item">
<span class="stat-value"></span>
<span class="stat-label">排名</span>
</div>
</div>
</div>
<div class="action-section">
<el-button type="primary" @click="handleEditProfile">
<el-icon><Edit /></el-icon>
编辑资料
</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Camera, Edit } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const userInfo = computed(() => authStore.user || {})
const userRoles = computed(() => authStore.roles || [])
const stats = ref({
submissionCount: 0,
commentCount: 0,
ranking: null
})
const getRoleType = (role) => {
const types = {
admin: 'danger',
author: 'primary',
reader: 'success'
}
return types[role] || 'info'
}
const getRoleName = (role) => {
const names = {
admin: '管理员',
author: '作者',
reader: '读者'
}
return names[role] || role
}
const handleEditProfile = () => {
// 跳转到编辑页面
}
</script>
<style scoped>
.profile-header {
display: flex;
gap: 24px;
padding: 24px;
background-color: #fff;
border-radius: 8px;
margin-bottom: 20px;
}
.avatar-section {
position: relative;
}
.edit-avatar {
position: absolute;
bottom: 0;
right: 0;
background-color: var(--color-primary);
color: #fff;
}
.info-section {
flex: 1;
}
.username {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: bold;
color: #303133;
}
.user-tags {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.user-stats {
display: flex;
gap: 32px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: var(--color-primary);
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 4px;
}
.action-section {
display: flex;
align-items: flex-start;
}
/* 移动端适配 */
@media (max-width: 768px) {
.profile-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.info-section {
width: 100%;
}
.user-tags {
justify-content: center;
}
.user-stats {
justify-content: center;
}
.action-section {
width: 100%;
}
.action-section .el-button {
width: 100%;
}
}
</style>
我的投稿展示
投稿列表优化
<template>
<div class="my-submissions">
<div class="section-header">
<h3>我的投稿</h3>
<el-button type="primary" @click="handleCreateSubmission">
<el-icon><Plus /></el-icon>
新建投稿
</el-button>
</div>
<div class="submissions-grid">
<el-card
v-for="submission in submissions"
:key="submission.id"
class="submission-card"
shadow="hover"
>
<div class="card-header">
<h4 class="submission-title"></h4>
<el-tag :type="getStatusType(submission.status)" size="small">
</el-tag>
</div>
<div class="card-content">
<div class="submission-meta">
<span class="meta-item">
<el-icon><Document /></el-icon>
</span>
<span class="meta-item">
<el-icon><Clock /></el-icon>
</span>
<span class="meta-item">
<el-icon><Reading /></el-icon>
字
</span>
</div>
<!-- 作者自述预览 -->
<div v-if="submission.authorBio" class="author-bio-preview">
<div class="bio-label">作者自述:</div>
<div class="bio-content"></div>
<el-button
text
type="primary"
@click="viewAuthorBio(submission)"
>
查看完整自述
</el-button>
</div>
<!-- 统计信息 -->
<div class="submission-stats">
<div class="stat-item">
<el-icon><ChatDotRound /></el-icon>
<span> 评论</span>
</div>
<div class="stat-item">
<el-icon><Star /></el-icon>
<span> 分</span>
</div>
</div>
</div>
<div class="card-actions">
<el-button size="small" @click="viewSubmission(submission)">
查看
</el-button>
<el-button
size="small"
type="primary"
@click="editSubmission(submission)"
:disabled="submission.status === 'submitted'"
>
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="deleteSubmission(submission)"
>
删除
</el-button>
</div>
</el-card>
</div>
<!-- 空状态 -->
<el-empty
v-if="submissions.length === 0"
description="还没有投稿,快去创建吧!"
>
<el-button type="primary" @click="handleCreateSubmission">
创建投稿
</el-button>
</el-empty>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Document,
Clock,
Reading,
ChatDotRound,
Star
} from '@element-plus/icons-vue'
import { getMySubmissions, deleteSubmission as deleteSubmissionApi } from '@/api/submission'
const router = useRouter()
const submissions = ref([])
const getStatusType = (status) => {
const types = {
draft: 'info',
submitted: 'success',
approved: 'success',
rejected: 'danger'
}
return types[status] || 'info'
}
const getStatusText = (status) => {
const texts = {
draft: '草稿',
submitted: '已提交',
approved: '已通过',
rejected: '已拒绝'
}
return texts[status] || status
}
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN')
}
const handleCreateSubmission = () => {
router.push('/submissions/create')
}
const viewSubmission = (submission) => {
router.push(`/submissions/${submission.id}`)
}
const editSubmission = (submission) => {
router.push(`/submissions/${submission.id}/edit`)
}
const viewAuthorBio = (submission) => {
// 显示完整的作者自述
ElMessageBox.alert(submission.authorBio, '作者自述', {
confirmButtonText: '关闭'
})
}
const deleteSubmission = async (submission) => {
try {
await ElMessageBox.confirm(
'确定删除此投稿吗?此操作不可恢复。',
'确认删除',
{
type: 'warning'
}
)
await deleteSubmissionApi(submission.id)
ElMessage.success('删除成功')
loadSubmissions()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
const loadSubmissions = async () => {
try {
const res = await getMySubmissions()
submissions.value = res.data
} catch (error) {
ElMessage.error('加载投稿失败')
}
}
onMounted(() => {
loadSubmissions()
})
</script>
<style scoped>
.my-submissions {
padding: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-header h3 {
margin: 0;
font-size: 20px;
font-weight: bold;
}
.submissions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.submission-card {
transition: all 0.3s;
}
.submission-card:hover {
transform: translateY(-4px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.submission-title {
margin: 0;
font-size: 18px;
font-weight: bold;
color: #303133;
flex: 1;
margin-right: 12px;
}
.card-content {
margin-bottom: 16px;
}
.submission-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
font-size: 14px;
color: #909399;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.author-bio-preview {
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
margin-bottom: 12px;
}
.bio-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.bio-content {
font-size: 14px;
color: #606266;
line-height: 1.6;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
margin-bottom: 8px;
}
.submission-stats {
display: flex;
gap: 16px;
font-size: 14px;
color: #606266;
}
.submission-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.card-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
/* 移动端适配 */
@media (max-width: 768px) {
.submissions-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.section-header .el-button {
width: 100%;
}
}
</style>
排名显示逻辑
排名卡片
<template>
<el-card class="ranking-card">
<template #header>
<div class="card-header">
<span>我的排名</span>
<el-button text type="primary" @click="viewFullRanking">
查看完整排行榜
</el-button>
</div>
</template>
<div v-if="ranking" class="ranking-content">
<div class="rank-badge">
<div class="rank-number"></div>
<div class="rank-label">当前排名</div>
</div>
<div class="rank-details">
<div class="detail-item">
<span class="label">积分:</span>
<span class="value"></span>
</div>
<div class="detail-item">
<span class="label">评论数:</span>
<span class="value"></span>
</div>
<div class="detail-item">
<span class="label">平均评分:</span>
<el-rate
v-model="ranking.averageScore"
disabled
show-score
:max="5"
/>
</div>
<div class="detail-item">
<span class="label">高质量评论:</span>
<span class="value"></span>
</div>
</div>
<div class="rank-progress">
<div class="progress-label">
距离前一名还差 积分
</div>
<el-progress
:percentage="progressPercentage"
:color="progressColor"
/>
</div>
</div>
<el-empty
v-else
description="暂无排名数据"
:image-size="80"
/>
</el-card>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getMyRanking } from '@/api/ranking'
const router = useRouter()
const ranking = ref(null)
const pointsToNext = computed(() => {
if (!ranking.value || ranking.value.rank === 1) return 0
// 这里应该从后端获取前一名的积分
return 50 // 示例值
})
const progressPercentage = computed(() => {
if (!ranking.value || pointsToNext.value === 0) return 100
const current = ranking.value.points
const target = current + pointsToNext.value
return Math.round((current / target) * 100)
})
const progressColor = computed(() => {
if (progressPercentage.value >= 80) return '#67c23a'
if (progressPercentage.value >= 50) return '#e6a23c'
return '#f56c6c'
})
const viewFullRanking = () => {
router.push('/ranking')
}
const loadRanking = async () => {
try {
const res = await getMyRanking()
ranking.value = res.data
} catch (error) {
console.error('加载排名失败:', error)
}
}
onMounted(() => {
loadRanking()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.ranking-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.rank-badge {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: #fff;
}
.rank-number {
font-size: 48px;
font-weight: bold;
}
.rank-label {
font-size: 14px;
opacity: 0.9;
margin-top: 8px;
}
.rank-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
}
.detail-item .label {
font-size: 14px;
color: #909399;
}
.detail-item .value {
font-size: 16px;
font-weight: bold;
color: #303133;
}
.rank-progress {
padding: 16px;
background-color: #f5f7fa;
border-radius: 4px;
}
.progress-label {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
}
/* 移动端适配 */
@media (max-width: 768px) {
.rank-details {
grid-template-columns: 1fr;
}
}
</style>
任务追踪集成
任务概览卡片
<template>
<el-card class="task-overview-card">
<template #header>
<div class="card-header">
<span>任务追踪</span>
<el-button text type="primary" @click="viewFullTasks">
查看详情
</el-button>
</div>
</template>
<div class="task-grid">
<!-- 作者资格 -->
<div class="task-item">
<div class="task-icon author">
<el-icon><Edit /></el-icon>
</div>
<div class="task-info">
<div class="task-title">作者资格</div>
<div class="task-status" :class="authorStatus.class">
</div>
<div class="task-progress">
/
</div>
</div>
</div>
<!-- 读者资格 -->
<div class="task-item">
<div class="task-icon reader">
<el-icon><Reading /></el-icon>
</div>
<div class="task-info">
<div class="task-title">读者资格</div>
<div class="task-status" :class="readerStatus.class">
</div>
<div class="task-progress">
/
</div>
</div>
</div>
<!-- 补评任务 -->
<div class="task-item" v-if="taskStats.debtCount > 0">
<div class="task-icon debt">
<el-icon><Warning /></el-icon>
</div>
<div class="task-info">
<div class="task-title">补评任务</div>
<div class="task-status warning">
待完成
</div>
<div class="task-progress">
个待补评
</div>
</div>
</div>
</div>
</el-card>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Edit, Reading, Warning } from '@element-plus/icons-vue'
import { getTaskStats } from '@/api/task'
const router = useRouter()
const taskStats = ref({
submissionCount: 0,
commentCount: 0,
debtCount: 0
})
const rules = ref({
requiredSubmissions: 3,
requiredComments: 10
})
const authorStatus = computed(() => {
const count = taskStats.value.submissionCount
const required = rules.value.requiredSubmissions
if (count >= required) {
return { text: '已获得', class: 'success' }
} else if (count > 0) {
return { text: '进行中', class: 'warning' }
} else {
return { text: '未开始', class: 'info' }
}
})
const readerStatus = computed(() => {
const count = taskStats.value.commentCount
const required = rules.value.requiredComments
if (count >= required) {
return { text: '已获得', class: 'success' }
} else if (count > 0) {
return { text: '进行中', class: 'warning' }
} else {
return { text: '未开始', class: 'info' }
}
})
const viewFullTasks = () => {
router.push('/tasks')
}
const loadTaskStats = async () => {
try {
const res = await getTaskStats()
taskStats.value = res.data.stats
rules.value = res.data.rules
} catch (error) {
console.error('加载任务统计失败:', error)
}
}
onMounted(() => {
loadTaskStats()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.task-item {
display: flex;
gap: 12px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
transition: all 0.3s;
}
.task-item:hover {
background-color: #ecf5ff;
}
.task-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 24px;
color: #fff;
}
.task-icon.author {
background-color: #409eff;
}
.task-icon.reader {
background-color: #67c23a;
}
.task-icon.debt {
background-color: #e6a23c;
}
.task-info {
flex: 1;
}
.task-title {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.task-status {
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
}
.task-status.success {
color: #67c23a;
}
.task-status.warning {
color: #e6a23c;
}
.task-status.info {
color: #909399;
}
.task-progress {
font-size: 12px;
color: #606266;
}
</style>
总结
个人中心功能的完善显著提升了用户对自己数据的管理和查看体验。通过优化个人信息管理、改进投稿展示、完善排名显示、集成任务追踪,我们打造了一个功能完善、体验良好的个人中心。
关键改进
- 个人信息展示更加直观
- 投稿列表支持自述预览和查看
- 排名显示逻辑更加清晰
- 任务追踪集成到个人中心
- 整体界面更加美观
技术亮点
- 卡片式布局
- 响应式设计
- 数据可视化
- 交互优化
用户反馈
- 个人中心功能更加完善
- 数据展示更加清晰
- 操作更加便捷
- 整体体验显著提升
这次个人中心功能完善为用户提供了更好的个人数据管理体验,也为平台的用户粘性提升做出了贡献。