辣评个人中心功能完善(二十五)

 

个人中心功能概述

个人中心是用户管理自己信息和查看个人数据的重要入口。本次完善优化了个人信息管理、投稿展示、排名显示、任务追踪等多个功能模块。

完善目标

  • 优化个人信息管理
  • 改进投稿展示界面
  • 完善排名显示逻辑
  • 集成任务追踪功能
  • 提升整体用户体验

个人信息管理

用户信息展示

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

总结

个人中心功能的完善显著提升了用户对自己数据的管理和查看体验。通过优化个人信息管理、改进投稿展示、完善排名显示、集成任务追踪,我们打造了一个功能完善、体验良好的个人中心。

关键改进

  • 个人信息展示更加直观
  • 投稿列表支持自述预览和查看
  • 排名显示逻辑更加清晰
  • 任务追踪集成到个人中心
  • 整体界面更加美观

技术亮点

  • 卡片式布局
  • 响应式设计
  • 数据可视化
  • 交互优化

用户反馈

  • 个人中心功能更加完善
  • 数据展示更加清晰
  • 操作更加便捷
  • 整体体验显著提升

这次个人中心功能完善为用户提供了更好的个人数据管理体验,也为平台的用户粘性提升做出了贡献。

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International