- 新增实时结果页面,包含分组切换、队伍卡片展示和AI识别结果交互 - 实现模拟数据轮询、人工修正状态保持和分数自动计算功能 - 修复竞赛创建时createdTime字段设置为空字符串的问题,改为使用startTime - 修正teamGroupId字段类型,确保为数字类型 - 统一日期时间选择器格式为ISO标准格式(yyyy-MM-dd'T'HH:mm:ss)
227 lines
6.9 KiB
Vue
227 lines
6.9 KiB
Vue
<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>
|