时间线说明
本文以 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);
}
}
总结
任务追踪系统的完善显著提升了用户对自己参赛状态的了解程度,通过直观的卡片展示、清晰的进度追踪和及时的数据刷新,帮助用户更好地管理自己的参赛任务。
关键改进
- 优化了页面设计和布局
- 完善了资格状态展示
- 改进了补评进度追踪
- 添加了数据刷新功能
- 提升了移动端体验
用户反馈
- 页面更加直观易懂
- 状态一目了然
- 刷新功能很实用
- 移动端体验良好