Files
reader-star/apps/admin/src/views/results/index.vue
rjl b39761d58e feat(admin): 新增实时结果页面并优化竞赛创建逻辑
- 新增实时结果页面,包含分组切换、队伍卡片展示和AI识别结果交互
- 实现模拟数据轮询、人工修正状态保持和分数自动计算功能
- 修复竞赛创建时createdTime字段设置为空字符串的问题,改为使用startTime
- 修正teamGroupId字段类型,确保为数字类型
- 统一日期时间选择器格式为ISO标准格式(yyyy-MM-dd'T'HH:mm:ss)
2026-01-30 14:04:07 +08:00

227 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { useIntervalFn } from '@vueuse/core'
import { NButton } from 'naive-ui'
import { onUnmounted, ref } from 'vue'
import GroupTabs, { type GroupItem } from './modules/GroupTabs.vue'
import TeamResultCard, { type AICharacter, type TeamResult } from './modules/TeamResultCard.vue'
// --- 模拟数据和状态 ---
const groups = ref<GroupItem[]>(Array.from({ length: 8 }).map((_, i) => ({
id: i + 1,
name: `${i + 1}`,
})))
const currentGroupId = ref<number | string>(1) // 当前选中的组别 ID
const teamsData = ref<TeamResult[]>([]) // 当前组别下的队伍数据
const isPolling = ref(true) // 是否正在轮询
// 初始化某组的队伍结构
function initGroupTeams(groupId: number | string) {
return Array.from({ length: 4 }).map((_, i) => ({
id: `${groupId}-${i + 1}`,
name: `${i + 1}`,
correctCount: 0,
score: 0,
imageUrl: 'https://oss.qyzhjy.com/temp/1769653751992/a4.jpg',
status: 'pending' as const,
aiData: [],
}))
}
// 初始化数据
teamsData.value = initGroupTeams(currentGroupId.value)
// --- 轮询 / 模拟逻辑 ---
// 存储人工修正记录,以便在更新时保留
// Map<TeamID, Map<CharID, boolean>>
const manualCorrections = ref<Record<string, Record<string, boolean>>>({})
function mergeAIResults(currentData: TeamResult[], incomingData: Record<string, AICharacter[]>) {
currentData.forEach((team) => {
const newChars = incomingData[team.id] || []
if (newChars.length === 0)
return
team.status = 'analyzing'
// 合并逻辑:
// 1. 现有字符:如果需要更新,但要保留人工修正的状态?
// 目前假设 AI 返回的结果是稳定的,但可能会有更多字符。
// 2. 新字符:追加。
// 简单方法:替换列表但应用人工覆盖
const mergedChars = newChars.map((char) => {
const teamCorrections = manualCorrections.value[team.id] || {}
if (typeof teamCorrections[char.id] !== 'undefined') {
return { ...char, isCorrect: teamCorrections[char.id] }
}
return char
})
team.aiData = mergedChars
// 重新计算分数/数量
team.correctCount = mergedChars.filter(c => c.isCorrect).length
team.score = team.correctCount * 1.5 // 假设每个字符 1.5 分
// 如果数据足够(模拟结束条件)
if (team.aiData.length >= 20) {
team.status = 'done'
}
})
}
// 模拟 API 轮询
// eslint-disable-next-line unused-imports/no-unused-vars
const { pause, resume } = useIntervalFn(() => {
if (!isPolling.value)
return
// 模拟获取当前队伍的数据
// 实际中: const res = await api.getResults(currentGroupId.value)
// 模拟增量更新
const incomingMockData: Record<string, AICharacter[]> = {}
teamsData.value.forEach((team) => {
if (team.status === 'done') {
incomingMockData[team.id] = team.aiData
return
}
// 添加 1-2 个随机字符
const currentLen = team.aiData.length
const nextLen = Math.min(currentLen + Math.floor(Math.random() * 3), 25)
const newChars: AICharacter[] = []
for (let i = 0; i < nextLen; i++) {
// 如果已存在,保留字符,否则生成新的
if (i < currentLen) {
// 我们使用上一次模拟的原始字符(尚未应用人工修正,
// 实际上我们是在模拟“服务器”视图,它还不知道本地编辑)
// 实际上为了模拟简单,我们就生成“新”列表。
newChars.push({
id: `char-${i}`,
char: ['龙', '年', '大', '吉', '万', '事', '如', '意', '恭', '喜', '发', '财'][i % 12],
isCorrect: i % 5 !== 0, // 模拟一些错误
})
}
else {
newChars.push({
id: `char-${i}`,
char: ['龙', '年', '大', '吉', '万', '事', '如', '意', '恭', '喜', '发', '财'][i % 12],
isCorrect: Math.random() > 0.2, // 80% 正确率
})
}
}
incomingMockData[team.id] = newChars
})
mergeAIResults(teamsData.value, incomingMockData)
}, 2000)
// --- 事件处理 ---
function handleGroupChange(id: number | string) {
currentGroupId.value = id
// 重置新组的数据
teamsData.value = initGroupTeams(id)
// 清除还是保留人工修正?通常新一轮会清除,但如果是重新访问可能会保留?
// 用户未指定。我将保留在以 teamID 为键的 map 中groupId-teamIdx
// 如果停止了轮询,是否恢复?
// 在实际应用中,可能需要为该组重新启动轮询。
}
function handleToggleCorrect(teamId: number | string, charId: string) {
const team = teamsData.value.find(t => t.id === teamId)
if (!team)
return
const char = team.aiData.find(c => c.id === charId)
if (!char)
return
// 切换状态
char.isCorrect = !char.isCorrect
// 更新计数
team.correctCount = team.aiData.filter(c => c.isCorrect).length
team.score = team.correctCount * 1.5
// 保存到人工修正 map
if (!manualCorrections.value[team.id]) {
manualCorrections.value[team.id] = {}
}
manualCorrections.value[team.id][charId] = char.isCorrect
}
function handleNext() {
// “下一步”按钮的逻辑(也许是下一组?)
const idx = groups.value.findIndex(g => g.id === currentGroupId.value)
if (idx !== -1 && idx < groups.value.length - 1) {
handleGroupChange(groups.value[idx + 1].id)
}
}
function handlePublish() {
window.$message?.success('结果已发布')
}
// 清理
onUnmounted(() => {
pause()
})
</script>
<template>
<div class="h-full flex flex-col gap-2 overflow-hidden bg-[#F9FAFB] p-6">
<!-- Top Bar -->
<div class="flex items-center justify-between rounded-2xl bg-white px-6 py-4 shadow-sm">
<div class="flex items-center gap-8">
<h1 class="m-0 flex items-center gap-2 text-xl text-gray-800 font-bold">
<div class="i-carbon-chart-line-data text-2xl text-blue-600" />
实时结果
</h1>
<GroupTabs
:groups="groups"
:model-value="currentGroupId"
@update:model-value="handleGroupChange"
/>
</div>
<div class="flex gap-4">
<div class="flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm text-blue-600 font-bold">
当前进度 1/10
</div>
<NButton secondary strong @click="handleNext">
下一题
</NButton>
<NButton type="primary" strong class="px-6" @click="handlePublish">
发布结果
</NButton>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 overflow-hidden">
<div class="grid grid-cols-4 h-full gap-6">
<TeamResultCard
v-for="team in teamsData"
:key="team.id"
:data="team"
@toggle-correct="(charId) => handleToggleCorrect(team.id, charId)"
/>
</div>
</div>
</div>
</template>
<style scoped>
:deep(.n-card__content) {
padding: 0;
}
</style>