辣评任务追踪系统完善(十八)

 

时间线说明

本文以 2026-02-27 的阶段优化为主,文中部分功能(如刷新按钮与后续样式细化)为 2026-03-06 的补充迭代,一并合并记录在本篇中。


任务追踪系统概述

任务追踪系统帮助用户了解自己的参赛资格状态、补评进度等信息。本次完善优化了页面设计、数据展示和交互体验。

优化目标

  • 优化页面设计和布局
  • 完善资格状态展示
  • 改进补评进度追踪
  • 添加数据刷新功能(2026-03-06 补充)
  • 提升用户体验

任务追踪页面设计

页面布局

<template>
  <div class="task-tracking-page">
    <el-card class="header-card">
      <div class="page-header">
        <h2>任务追踪</h2>
        <el-button type="primary" @click="refreshData" :loading="loading">
          <el-icon><Refresh /></el-icon>
          刷新数据
        </el-button>
      </div>
    </el-card>

    <!-- 资格状态卡片 -->
    <el-row :gutter="20" class="status-cards">
      <el-col :xs="24" :sm="12" :md="8">
        <el-card class="status-card author-card">
          <div class="card-icon">
            <el-icon><Edit /></el-icon>
          </div>
          <div class="card-content">
            <div class="card-title">作者资格</div>
            <div class="card-status" :class="authorStatus.class">
              
            </div>
            <div class="card-detail">
              已投稿: / 
            </div>
          </div>
        </el-card>
      </el-col>

      <el-col :xs="24" :sm="12" :md="8">
        <el-card class="status-card reader-card">
          <div class="card-icon">
            <el-icon><Reading /></el-icon>
          </div>
          <div class="card-content">
            <div class="card-title">读者资格</div>
            <div class="card-status" :class="readerStatus.class">
              
            </div>
            <div class="card-detail">
              已评论: / 
            </div>
          </div>
        </el-card>
      </el-col>

      <el-col :xs="24" :sm="12" :md="8">
        <el-card class="status-card debt-card">
          <div class="card-icon">
            <el-icon><Warning /></el-icon>
          </div>
          <div class="card-content">
            <div class="card-title">补评任务</div>
            <div class="card-status" :class="debtStatus.class">
              
            </div>
            <div class="card-detail">
              待补评:
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <!-- 详细信息 -->
    <el-card class="detail-card">
      <template #header>
        <span>详细信息</span>
      </template>

      <el-descriptions :column="2" border>
        <el-descriptions-item label="当前届次">
          第  届
        </el-descriptions-item>
        <el-descriptions-item label="参赛状态">
          <el-tag :type="participationTag.type">
            
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="投稿数量">
          
        </el-descriptions-item>
        <el-descriptions-item label="评论数量">
          
        </el-descriptions-item>
        <el-descriptions-item label="平均评分">
          <el-rate v-model="stats.averageScore" disabled show-score />
        </el-descriptions-item>
        <el-descriptions-item label="高质量评论">
          
        </el-descriptions-item>
      </el-descriptions>
    </el-card>

    <!-- 补评进度 -->
    <el-card v-if="stats.debtCount > 0" class="debt-card">
      <template #header>
        <span>补评进度</span>
      </template>

      <div class="debt-progress">
        <el-progress
          :percentage="debtProgress"
          :color="debtProgressColor"
          :status="debtProgressStatus"
        />
        <div class="debt-info">
          <span>已完成:</span>
          <span>待完成:</span>
          <span>截止日期:</span>
        </div>
      </div>

      <el-alert
        v-if="isDebtOverdue"
        title="补评任务已逾期"
        type="error"
        :closable="false"
        show-icon
      >
        请尽快完成补评任务,避免影响参赛资格。
      </el-alert>
    </el-card>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { Refresh, Edit, Reading, Warning } from '@element-plus/icons-vue'
import { getTaskTracking } from '@/api/task'
import { ElMessage } from 'element-plus'

const loading = ref(false)
const stats = ref({
  submissionCount: 0,
  commentCount: 0,
  debtCount: 0,
  completedDebt: 0,
  averageScore: 0,
  highQualityCount: 0,
  debtDueDate: null
})

const rules = ref({
  requiredSubmissions: 3,
  requiredComments: 10
})

const currentCompetition = ref({
  number: 1
})

// 作者资格状态
const authorStatus = computed(() => {
  const count = stats.value.submissionCount
  const required = rules.value.requiredSubmissions

  if (count >= required) {
    return { text: '已获得', class: 'status-success' }
  } else if (count > 0) {
    return { text: '进行中', class: 'status-warning' }
  } else {
    return { text: '未开始', class: 'status-info' }
  }
})

// 读者资格状态
const readerStatus = computed(() => {
  const count = stats.value.commentCount
  const required = rules.value.requiredComments

  if (count >= required) {
    return { text: '已获得', class: 'status-success' }
  } else if (count > 0) {
    return { text: '进行中', class: 'status-warning' }
  } else {
    return { text: '未开始', class: 'status-info' }
  }
})

// 补评任务状态
const debtStatus = computed(() => {
  const count = stats.value.debtCount

  if (count === 0) {
    return { text: '无任务', class: 'status-success' }
  } else if (isDebtOverdue.value) {
    return { text: '已逾期', class: 'status-danger' }
  } else {
    return { text: `${count} 个待完成`, class: 'status-warning' }
  }
})

// 补评进度
const debtProgress = computed(() => {
  const total = stats.value.debtCount + stats.value.completedDebt
  if (total === 0) return 100
  return Math.round((stats.value.completedDebt / total) * 100)
})

// 补评进度颜色
const debtProgressColor = computed(() => {
  if (isDebtOverdue.value) return '#f56c6c'
  if (debtProgress.value === 100) return '#67c23a'
  return '#e6a23c'
})

// 补评进度状态
const debtProgressStatus = computed(() => {
  if (isDebtOverdue.value) return 'exception'
  if (debtProgress.value === 100) return 'success'
  return undefined
})

// 是否逾期
const isDebtOverdue = computed(() => {
  if (!stats.value.debtDueDate) return false
  return new Date(stats.value.debtDueDate) < new Date()
})

// 参赛状态标签
const participationTag = computed(() => {
  const hasAuthor = authorStatus.value.text === '已获得'
  const hasReader = readerStatus.value.text === '已获得'

  if (hasAuthor && hasReader) {
    return { text: '双资格', type: 'success' }
  } else if (hasAuthor) {
    return { text: '作者', type: 'primary' }
  } else if (hasReader) {
    return { text: '读者', type: 'warning' }
  } else {
    return { text: '未参赛', type: 'info' }
  }
})

// 刷新数据
const refreshData = async () => {
  loading.value = true
  try {
    const res = await getTaskTracking()
    stats.value = res.data.stats
    rules.value = res.data.rules
    currentCompetition.value = res.data.competition
    ElMessage.success('数据已刷新')
  } catch (error) {
    ElMessage.error('刷新失败')
  } finally {
    loading.value = false
  }
}

// 格式化日期
const formatDate = (date) => {
  if (!date) return '-'
  return new Date(date).toLocaleDateString('zh-CN')
}

onMounted(() => {
  refreshData()
})
</script>

<style scoped>
.task-tracking-page {
  padding: 20px;
}

.header-card {
  margin-bottom: 20px;
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.status-cards {
  margin-bottom: 20px;
}

.status-card {
  height: 100%;
  transition: all 0.3s;
}

.status-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.status-card :deep(.el-card__body) {
  display: flex;
  align-items: center;
  gap: 16px;
}

.card-icon {
  font-size: 48px;
  opacity: 0.8;
}

.author-card .card-icon { color: #409eff; }
.reader-card .card-icon { color: #67c23a; }
.debt-card .card-icon { color: #e6a23c; }

.card-content {
  flex: 1;
}

.card-title {
  font-size: 14px;
  color: #909399;
  margin-bottom: 8px;
}

.card-status {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 4px;
}

.status-success { color: #67c23a; }
.status-warning { color: #e6a23c; }
.status-danger { color: #f56c6c; }
.status-info { color: #909399; }

.card-detail {
  font-size: 12px;
  color: #606266;
}

.detail-card,
.debt-card {
  margin-bottom: 20px;
}

.debt-progress {
  margin-bottom: 16px;
}

.debt-info {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 14px;
  color: #606266;
}

@media (max-width: 768px) {
  .status-cards {
    margin-bottom: 16px;
  }

  .status-card {
    margin-bottom: 16px;
  }
}
</style>

资格状态展示

状态计算逻辑

// 计算作者资格
const calculateAuthorQualification = (submissionCount, requiredSubmissions) => {
  const progress = (submissionCount / requiredSubmissions) * 100

  return {
    qualified: submissionCount >= requiredSubmissions,
    progress: Math.min(progress, 100),
    remaining: Math.max(requiredSubmissions - submissionCount, 0)
  }
}

// 计算读者资格
const calculateReaderQualification = (commentCount, requiredComments) => {
  const progress = (commentCount / requiredComments) * 100

  return {
    qualified: commentCount >= requiredComments,
    progress: Math.min(progress, 100),
    remaining: Math.max(requiredComments - commentCount, 0)
  }
}

补评进度追踪

后端接口

// GetTaskTracking 获取任务追踪信息
func (h *TaskHandler) GetTaskTracking(c *gin.Context) {
    userID := c.GetUint("userID")
    competitionID := c.Query("competition_id")

    // 获取统计数据
    stats, err := h.taskService.GetUserStats(c, userID, competitionID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    // 获取规则
    rules, err := h.ruleService.GetCompetitionRules(c, competitionID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    // 获取比赛信息
    competition, err := h.competitionService.GetCompetition(c, competitionID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, gin.H{
        "stats":       stats,
        "rules":       rules,
        "competition": competition,
    })
}

数据刷新机制

手动刷新

const refreshData = async () => {
  loading.value = true
  try {
    const res = await getTaskTracking()
    updateData(res.data)
    ElMessage.success('数据已刷新')
  } catch (error) {
    ElMessage.error('刷新失败')
  } finally {
    loading.value = false
  }
}

自动刷新(可选)

import { onMounted, onUnmounted } from 'vue'

let refreshTimer = null

onMounted(() => {
  // 每5分钟自动刷新一次
  refreshTimer = setInterval(() => {
    refreshData()
  }, 5 * 60 * 1000)
})

onUnmounted(() => {
  if (refreshTimer) {
    clearInterval(refreshTimer)
  }
})

样式与交互优化

响应式设计

/* 移动端优化 */
@media (max-width: 768px) {
  .status-cards {
    margin-bottom: 16px;
  }

  .status-card {
    margin-bottom: 16px;
  }

  .status-card :deep(.el-card__body) {
    flex-direction: column;
    text-align: center;
  }

  .card-icon {
    font-size: 36px;
  }

  .card-status {
    font-size: 20px;
  }
}

动画效果

.status-card {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.status-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.card-status {
  animation: fadeIn 0.5s ease-in;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

总结

任务追踪系统的完善显著提升了用户对自己参赛状态的了解程度,通过直观的卡片展示、清晰的进度追踪和及时的数据刷新,帮助用户更好地管理自己的参赛任务。

关键改进

  • 优化了页面设计和布局
  • 完善了资格状态展示
  • 改进了补评进度追踪
  • 添加了数据刷新功能
  • 提升了移动端体验

用户反馈

  • 页面更加直观易懂
  • 状态一目了然
  • 刷新功能很实用
  • 移动端体验良好

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