feat(results): 新增实时结果列表和详情页面,支持评委查看和评分
- 添加 results_results-list 和 results_results-detail 路由及页面 - 实现比赛列表展示,支持跳转到实时结果详情 - 在结果详情页面添加分组和题目切换功能 - 集成游戏统计 API,实时获取队伍答题数据 - 添加 TeamResultCard 和 GroupTabs 可复用组件 - 更新国际化配置和类型定义
This commit is contained in:
@ -255,6 +255,9 @@ const local: App.I18n.Schema = {
|
||||
'user_rules': 'User Rules',
|
||||
'user_groups': 'User Groups',
|
||||
'user_home': 'User Home',
|
||||
'results_results-list': 'Results List',
|
||||
'results_results-detail': 'Results Detail',
|
||||
'test': 'Test',
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
|
||||
@ -251,6 +251,9 @@ const local: App.I18n.Schema = {
|
||||
'user_rules': '用户规则',
|
||||
'user_groups': '用户组',
|
||||
'user_home': '用户首页',
|
||||
'results_results-list': '实时结果列表',
|
||||
'results_results-detail': '实时结果详情',
|
||||
'test': 'Test',
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
|
||||
@ -28,7 +28,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
"question-store": () => import("@/views/question-store/index.vue"),
|
||||
"rank_rank-detail": () => import("@/views/rank/rank-detail/index.vue"),
|
||||
"rank_rank-list": () => import("@/views/rank/rank-list/index.vue"),
|
||||
results: () => import("@/views/results/index.vue"),
|
||||
"results_results-detail-copy": () => import("@/views/results/results-detail-copy/index.vue"),
|
||||
"results_results-detail": () => import("@/views/results/results-detail/index.vue"),
|
||||
"results_results-list": () => import("@/views/results/results-list/index.vue"),
|
||||
"template_template-detail": () => import("@/views/template/template-detail/index.vue"),
|
||||
"template_template-list": () => import("@/views/template/template-list/index.vue"),
|
||||
test: () => import("@/views/test/index.vue"),
|
||||
|
||||
@ -179,13 +179,43 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
{
|
||||
name: 'results',
|
||||
path: '/results',
|
||||
component: 'layout.base$view.results',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'results',
|
||||
i18nKey: 'route.results',
|
||||
icon: 'mdi:clipboard-check-multiple',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'results_results-detail',
|
||||
path: '/results/results-detail',
|
||||
component: 'view.results_results-detail',
|
||||
meta: {
|
||||
title: 'results_results-detail',
|
||||
i18nKey: 'route.results_results-detail',
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'results_results-detail-copy',
|
||||
path: '/results/results-detail-copy',
|
||||
component: 'view.results_results-detail-copy',
|
||||
meta: {
|
||||
title: 'results_results-detail-copy',
|
||||
i18nKey: 'route.results_results-detail-copy'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'results_results-list',
|
||||
path: '/results/results-list',
|
||||
component: 'view.results_results-list',
|
||||
meta: {
|
||||
title: 'results_results-list',
|
||||
i18nKey: 'route.results_results-list'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'template',
|
||||
|
||||
@ -179,6 +179,9 @@ const routeMap: RouteMap = {
|
||||
"rank_rank-detail": "/rank/rank-detail",
|
||||
"rank_rank-list": "/rank/rank-list",
|
||||
"results": "/results",
|
||||
"results_results-detail": "/results/results-detail",
|
||||
"results_results-detail-copy": "/results/results-detail-copy",
|
||||
"results_results-list": "/results/results-list",
|
||||
"template": "/template",
|
||||
"template_template-detail": "/template/template-detail",
|
||||
"template_template-list": "/template/template-list",
|
||||
|
||||
@ -49,9 +49,9 @@ export function fetchSubmitQuestionResult(data: Api.Competition.QuestionAddParam
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
export function fetchGetGameStatistics(data: Api.Competition.QuestionAddParams) {
|
||||
export function fetchGetGameStatistics(data: { TeamGroupID: number, QuestionID: number, QuestionDetaiID: number }) {
|
||||
return request<App.Service.Response>({
|
||||
url: `/Base/ActivityMain/GetTeamQuestionUseByTeamGroupIDAndQuestionID/?RoomID=${data.RoomID}&MainID=${data.MainID}&TeamGroupID=${data.TeamGroupID}&QuestionID=${data.QuestionID}&QuestionDetaiID=${data.QuestionDetaiID}`,
|
||||
url: `/Base/ActivityMain/GetTeamQuestionUseByTeamGroupIDAndQuestionID/?TeamGroupID=${data.TeamGroupID}&QuestionID=${data.QuestionID}&QuestionDetaiID=${data.QuestionDetaiID}`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
@ -84,3 +84,14 @@ export function fetchGetQuestionTableByActivityID(ActivityID: string) {
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据 roomid查询当前正在比赛的题目 */
|
||||
export function fetchGetCurrentQuestionByRoomID(RoomID: number) {
|
||||
return request<App.Service.Response<Api.Competition.CurrentQuestionResponse>>({
|
||||
url: `/Base/ActivityMain/SelectTask?RoomID=${RoomID}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
RoomID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
10
apps/admin/src/typings/api/Competition.d.ts
vendored
10
apps/admin/src/typings/api/Competition.d.ts
vendored
@ -67,6 +67,16 @@ declare namespace Api {
|
||||
Finished = 3,
|
||||
}
|
||||
|
||||
interface CompetitionItem {
|
||||
Id: number
|
||||
Name: string
|
||||
StartTime: string
|
||||
EndTime: string
|
||||
TeamGroupNumber: number
|
||||
PublishStatus: number
|
||||
RoomID: number
|
||||
}
|
||||
|
||||
/** room */
|
||||
interface Room {
|
||||
/** room id */
|
||||
|
||||
7
apps/admin/src/typings/elegant-router.d.ts
vendored
7
apps/admin/src/typings/elegant-router.d.ts
vendored
@ -33,6 +33,9 @@ declare module "@elegant-router/types" {
|
||||
"rank_rank-detail": "/rank/rank-detail";
|
||||
"rank_rank-list": "/rank/rank-list";
|
||||
"results": "/results";
|
||||
"results_results-detail": "/results/results-detail";
|
||||
"results_results-detail-copy": "/results/results-detail-copy";
|
||||
"results_results-list": "/results/results-list";
|
||||
"template": "/template";
|
||||
"template_template-detail": "/template/template-detail";
|
||||
"template_template-list": "/template/template-list";
|
||||
@ -120,7 +123,9 @@ declare module "@elegant-router/types" {
|
||||
| "question-store"
|
||||
| "rank_rank-detail"
|
||||
| "rank_rank-list"
|
||||
| "results"
|
||||
| "results_results-detail-copy"
|
||||
| "results_results-detail"
|
||||
| "results_results-list"
|
||||
| "template_template-detail"
|
||||
| "template_template-list"
|
||||
| "test"
|
||||
|
||||
@ -150,14 +150,14 @@ async function submitForm() {
|
||||
activityID: 0,
|
||||
questionID: question.QuestionID,
|
||||
questionIndex: index,
|
||||
actitvityQuestionName: question.ActitvityQuestionName,
|
||||
actitvityQuestionName: question.ActitvityQuestionName || '',
|
||||
questionTime: question.QuestionTime,
|
||||
questionRule: question.QuestionRule,
|
||||
uiType: question.UIType,
|
||||
point: question.Point,
|
||||
templateID: question.TemplateID,
|
||||
roundType: round.roundType,
|
||||
questionSubTitle: question.ActitvityQuestionName,
|
||||
questionSubTitle: question.ActitvityQuestionName || '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
// import type { CompetitionItem } from './modules/data'
|
||||
import { NButton, NGi, NGrid, NSpace } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchDeleteActivity, fetchGetActivityList } from '@/service/api/competition'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import CompetitionAddModal from '../competition-add/index.vue'
|
||||
|
||||
import CompetitionCard from './modules/competition-card.vue'
|
||||
|
||||
export interface CompetitionItem {
|
||||
Id: number
|
||||
Name: string
|
||||
StartTime: string
|
||||
EndTime: string
|
||||
TeamGroupNumber: number
|
||||
PublishStatus: number
|
||||
}
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gap = computed(() => (appStore.isMobile ? 12 : 26))
|
||||
|
||||
const competitionListRef = ref<CompetitionItem[]>([])
|
||||
const competitionListRef = ref<Api.Competition.CompetitionItem[]>([])
|
||||
|
||||
const showAddModal = ref(false)
|
||||
|
||||
@ -27,12 +21,12 @@ function handleAdd() {
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
function handleCopy(item: CompetitionItem) {
|
||||
function handleCopy(item: Api.Competition.CompetitionItem) {
|
||||
// TODO: 实现复制逻辑
|
||||
window.$message?.success(`已复制活动:${item.Name}`)
|
||||
}
|
||||
|
||||
function handleDelete(item: CompetitionItem) {
|
||||
function handleDelete(item: Api.Competition.CompetitionItem) {
|
||||
const d = window.$dialog?.warning({
|
||||
title: '确认删除',
|
||||
content: `确定删除活动:${item.Name}吗?`,
|
||||
@ -82,6 +76,10 @@ async function fetchActivityList() {
|
||||
}
|
||||
}
|
||||
|
||||
function toDetail(item: Api.Competition.CompetitionItem) {
|
||||
routerPushByKey('competition_competition-detail', { query: { Id: item.Id.toString() } })
|
||||
}
|
||||
|
||||
onMounted(() => fetchActivityList())
|
||||
</script>
|
||||
|
||||
@ -109,7 +107,7 @@ onMounted(() => fetchActivityList())
|
||||
<!-- 卡片列表区域 -->
|
||||
<!-- 响应式布局:手机1列,平板2列,中屏3列,大屏4列 -->
|
||||
<NGrid :x-gap="gap" :y-gap="gap" responsive="screen" item-responsive>
|
||||
<NGi v-for="item in competitionListRef" :key="item.Id" span="24 s:12 m:8 l:6">
|
||||
<NGi v-for="item in competitionListRef" :key="item.Id" span="24 s:12 m:8 l:6" @click="toDetail(item)">
|
||||
<CompetitionCard :item="item" @copy="handleCopy" @delete="handleDelete" />
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
@ -1,24 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { CompetitionItem } from '../index.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { NButton, NCard, NTag, NTooltip } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useDict } from '@/hooks/business/useDict'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'copy', item: CompetitionItem): void
|
||||
(e: 'delete', item: CompetitionItem): void
|
||||
(e: 'copy', item: Api.Competition.CompetitionItem): void
|
||||
(e: 'delete', item: Api.Competition.CompetitionItem): void
|
||||
}>()
|
||||
|
||||
const { options: publishStatusOptions } = useDict('publish_status')
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
interface Props {
|
||||
item: CompetitionItem
|
||||
item: Api.Competition.CompetitionItem
|
||||
}
|
||||
|
||||
// 状态配置:颜色和文本
|
||||
@ -43,10 +40,6 @@ const statusConfig = computed(() => {
|
||||
// 处理标题换行
|
||||
// const formattedTitle = computed(() => props.item.title.replace(/\n/g, '<br/>'))
|
||||
|
||||
function toDetail(item: CompetitionItem) {
|
||||
routerPushByKey('competition_competition-detail', { query: { Id: item.Id.toString() } })
|
||||
}
|
||||
|
||||
// 随机背景色池
|
||||
const bgColors = [
|
||||
'bg-blue-50/80',
|
||||
@ -78,7 +71,7 @@ const decorationBgClass = computed(() => {
|
||||
<NCard
|
||||
:bordered="false"
|
||||
class="group relative h-full overflow-hidden border border-transparent rounded-2xl transition-all duration-300 hover:scale-[1.02] hover:border-blue-500 hover:shadow-xl hover:-translate-y-2"
|
||||
content-style="padding: 34px; display: flex; flex-direction: column; height: 100%;" @click="toDetail(item)"
|
||||
content-style="padding: 34px; display: flex; flex-direction: column; height: 100%;"
|
||||
>
|
||||
<!-- 特殊背景装饰 (所有卡片显示,随机颜色) -->
|
||||
<div
|
||||
@ -138,7 +131,7 @@ const decorationBgClass = computed(() => {
|
||||
<!-- 日期 -->
|
||||
<div class="mb-6 flex items-center text-gray-400">
|
||||
<icon-ic-baseline-calendar-month class="mr-2 text-xl" />
|
||||
<span class="text-base font-bold">{{ item.StartTime }} ~ {{ item.EndTime }}</span>
|
||||
<span class="text-base font-bold">{{ dayjs(item.StartTime).format('YYYY-MM-DD HH:mm') }} ~ {{ dayjs(item.EndTime).format('YYYY-MM-DD HH:mm') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
491
apps/admin/src/views/results/results-detail/index.vue
Normal file
491
apps/admin/src/views/results/results-detail/index.vue
Normal file
@ -0,0 +1,491 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { NButton, NCard, NImage, useMessage } from 'naive-ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { QuestionCategoryEnum } from '@/enum/business'
|
||||
import { fetchGetQuestionList } from '@/service/api/competition'
|
||||
import { fetchGetCurrentQuestionByRoomID, fetchGetGameStatistics, fetchGetGroupListByActivityID } from '@/service/api/game'
|
||||
import GroupTabs, { type GroupItem } from './modules/GroupTabs.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const activityId = computed(() => route.query?.activityId as string)
|
||||
const roomId = computed(() => route.query?.roomId as string)
|
||||
|
||||
const currentGroupId = ref<number | string>('') // 当前选中的组别 ID
|
||||
const currentQuestionId = ref<number | string>('') // 当前选中的题目 ID
|
||||
const currentQuestionType = ref<string>('') // 当前选中的题目类型
|
||||
|
||||
const groups = ref<GroupItem[]>([])
|
||||
const questionList = ref<(GroupItem & { type: string })[]>([])
|
||||
const currentQuestion = ref<any>() // 当前正在参赛的题目
|
||||
|
||||
// 计算当前题目类型对应的布局
|
||||
const currentLayout = computed(() => {
|
||||
if (!currentQuestionType.value)
|
||||
return 'list' // 默认列表
|
||||
|
||||
// 汉字听写、汉字加一加、选择题 -> Grid (多个小方块)
|
||||
const gridTypes = [
|
||||
QuestionCategoryEnum.ChineseCharacterDictation,
|
||||
QuestionCategoryEnum.CharacterRadicalAddition,
|
||||
QuestionCategoryEnum.PoetryComprehension,
|
||||
]
|
||||
|
||||
return gridTypes.includes(currentQuestionType.value as QuestionCategoryEnum) ? 'grid' : 'list'
|
||||
})
|
||||
|
||||
interface AnswerItem {
|
||||
id: number
|
||||
label?: string
|
||||
imageUrl: string
|
||||
status: 'correct' | 'wrong'
|
||||
TeamQuestionUseID: number
|
||||
Guid: string
|
||||
}
|
||||
|
||||
interface TeamAnswerData {
|
||||
Id: number
|
||||
MainId: number
|
||||
TeamGroupId: number
|
||||
TeamId: number
|
||||
QuestionId: number
|
||||
QuestionDetailId: number | null
|
||||
UserAnswerPicture: string
|
||||
UserAnswerFont: string // JSON string
|
||||
TeamName: string
|
||||
Points: number
|
||||
ResultPotins: number
|
||||
Status: number
|
||||
}
|
||||
|
||||
interface TeamResult {
|
||||
TeamName: string
|
||||
UserAnswerPicture: string
|
||||
Answers: AnswerItem[]
|
||||
Points: number
|
||||
TeamId: number
|
||||
}
|
||||
|
||||
// 当前所有队伍的解析结果列表
|
||||
const teamResults = ref<TeamResult[]>([])
|
||||
|
||||
// 切换状态逻辑:点击后 Correct <-> Wrong
|
||||
function toggleStatus(team: TeamResult, item: AnswerItem) {
|
||||
item.status = item.status === 'correct' ? 'wrong' : 'correct'
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
// “下一步”按钮的逻辑
|
||||
// 简单示例:切换到下一题
|
||||
const currentIndex = questionList.value.findIndex(q => q.id === currentQuestionId.value)
|
||||
if (currentIndex !== -1 && currentIndex < questionList.value.length - 1) {
|
||||
currentQuestionId.value = questionList.value[currentIndex + 1].id
|
||||
}
|
||||
else {
|
||||
message.info('已经是最后一题了')
|
||||
}
|
||||
}
|
||||
|
||||
function handlePublish() {
|
||||
// 发布结果逻辑
|
||||
message.success('结果已发布')
|
||||
}
|
||||
|
||||
// Mock 数据生成器
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function generateMockData() {
|
||||
// Mock 分组
|
||||
groups.value = Array.from({ length: 5 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
name: `第 ${i + 1} 组`,
|
||||
}))
|
||||
if (groups.value.length > 0) {
|
||||
currentGroupId.value = groups.value[0].id
|
||||
}
|
||||
|
||||
// Mock 题目
|
||||
// 1. 汉字听写 (Grid)
|
||||
// 2. 汉字加一加 (Grid)
|
||||
// 3. 诗词理解/选择题 (Grid)
|
||||
// 4. 词语听写 (Long)
|
||||
// 5. 成语写一写 (Long)
|
||||
const mockTypes = [
|
||||
QuestionCategoryEnum.ChineseCharacterDictation,
|
||||
QuestionCategoryEnum.CharacterRadicalAddition,
|
||||
QuestionCategoryEnum.PoetryComprehension,
|
||||
QuestionCategoryEnum.WordDictation,
|
||||
QuestionCategoryEnum.IdiomWriting,
|
||||
]
|
||||
|
||||
// 生成 10 个题目,覆盖所有类型
|
||||
questionList.value = Array.from({ length: 10 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
name: `题目 ${i + 1}`,
|
||||
type: mockTypes[i % mockTypes.length],
|
||||
}))
|
||||
|
||||
if (questionList.value.length > 0) {
|
||||
currentQuestionId.value = questionList.value[0].id
|
||||
currentQuestionType.value = questionList.value[0].type
|
||||
}
|
||||
|
||||
// Mock 答题卡数据
|
||||
teamResults.value = Array.from({ length: 4 }).map((_, i) => {
|
||||
// 根据当前题目类型决定答案数量
|
||||
// 词语听写(WordDictation)、成语(IdiomWriting) 通常是单个答案块(包含多个字),整体判分
|
||||
// 汉字听写(ChineseCharacterDictation)、汉字加一加(CharacterRadicalAddition)、诗词(PoetryComprehension) 通常是多个答案块,单独判分
|
||||
|
||||
let answerCount = 5
|
||||
if ([QuestionCategoryEnum.WordDictation, QuestionCategoryEnum.IdiomWriting].includes(currentQuestionType.value as QuestionCategoryEnum)) {
|
||||
answerCount = 1
|
||||
}
|
||||
|
||||
return {
|
||||
TeamId: i + 1,
|
||||
TeamName: `Mock 战队 ${i + 1}`,
|
||||
UserAnswerPicture: 'https://placehold.co/800x400?text=Original+Answer+Sheet',
|
||||
Points: 0,
|
||||
Answers: Array.from({ length: answerCount }).map((__, j) => ({
|
||||
id: j + 1,
|
||||
label: answerCount > 1 ? `${j + 1}.` : '', // 只有多个答案时才显示序号
|
||||
// Grid 类型用方形图,List 类型用长方形图
|
||||
imageUrl: answerCount > 1
|
||||
? `https://placehold.co/100x100?text=Answer+${j + 1}`
|
||||
: `https://placehold.co/400x100?text=Long+Answer`,
|
||||
status: Math.random() > 0.5 ? 'correct' : 'wrong',
|
||||
TeamQuestionUseID: 123,
|
||||
Guid: `mock-guid-${j}`,
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组列表
|
||||
*/
|
||||
async function fetchGroups() {
|
||||
if (!activityId.value) {
|
||||
window.$message?.error('请先选择比赛')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data, error } = await fetchGetGroupListByActivityID(activityId.value)
|
||||
if (error) {
|
||||
window.$message?.error('获取题目失败')
|
||||
return
|
||||
}
|
||||
const list = data?.data || []
|
||||
groups.value = list.map((item: any) => ({
|
||||
id: item.Id,
|
||||
name: item.Name,
|
||||
}))
|
||||
// 默认选中第一个分组
|
||||
if (groups.value.length > 0 && !currentGroupId.value) {
|
||||
currentGroupId.value = groups.value[0].id
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
window.$message?.error(error.message || '获取分组失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目列表
|
||||
*/
|
||||
async function fetchQuestions() {
|
||||
if (!activityId.value)
|
||||
return
|
||||
try {
|
||||
const { data, error } = await fetchGetQuestionList(activityId.value)
|
||||
if (error) {
|
||||
message.error(error.message)
|
||||
return
|
||||
}
|
||||
const list = data?.data || []
|
||||
questionList.value = list.map((item: any) => ({
|
||||
id: item.ID, // 注意大小写,根据接口返回调整
|
||||
name: item.Name || `题目 ${item.ID}`,
|
||||
type: item.Type || '', // 假设接口返回 Type
|
||||
}))
|
||||
// 默认选中第一个题目
|
||||
if (questionList.value.length > 0 && !currentQuestionId.value) {
|
||||
currentQuestionId.value = questionList.value[0].id
|
||||
currentQuestionType.value = questionList.value[0].type
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
message.error(error.message || '获取题目失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前正在比赛的题目
|
||||
*/
|
||||
|
||||
async function fetchCurrentQuestion() {
|
||||
if (!roomId.value)
|
||||
return
|
||||
try {
|
||||
const { data, error } = await fetchGetCurrentQuestionByRoomID(Number(roomId.value))
|
||||
if (error) {
|
||||
message.error(error.message)
|
||||
return
|
||||
}
|
||||
currentQuestion.value = data?.data || {}
|
||||
}
|
||||
catch (error: any) {
|
||||
message.error(error.message || '获取当前题目失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取答题卡数据
|
||||
*/
|
||||
async function fetchAnswerData() {
|
||||
if (!currentGroupId.value || !currentQuestionId.value)
|
||||
return
|
||||
|
||||
// 如果是 Mock 环境,重新生成一下随机答案
|
||||
if (!activityId.value) {
|
||||
// 复用 generateMockData 中的逻辑来更新 teamResults
|
||||
// 但这里只需要更新 results 部分,不需要重置 group 和 questionList
|
||||
teamResults.value = Array.from({ length: 4 }).map((_, i) => {
|
||||
let answerCount = 5
|
||||
// Grid 类型 (ChineseCharacterDictation, CharacterRadicalAddition, PoetryComprehension) -> 多图
|
||||
// List 类型 (WordDictation, IdiomWriting) -> 单图
|
||||
const isListType = [QuestionCategoryEnum.WordDictation, QuestionCategoryEnum.IdiomWriting].includes(currentQuestionType.value as QuestionCategoryEnum)
|
||||
|
||||
if (isListType) {
|
||||
answerCount = 1
|
||||
}
|
||||
|
||||
return {
|
||||
TeamId: i + 1,
|
||||
TeamName: `Mock 战队 ${i + 1}`,
|
||||
UserAnswerPicture: 'https://placehold.co/800x400?text=Original+Answer+Sheet',
|
||||
Points: 0,
|
||||
Answers: Array.from({ length: answerCount }).map((__, j) => ({
|
||||
id: j + 1,
|
||||
label: answerCount > 1 ? `${j + 1}.` : '',
|
||||
imageUrl: answerCount > 1
|
||||
? `https://placehold.co/100x100?text=Answer+${j + 1}`
|
||||
: `https://placehold.co/400x100?text=Long+Answer`,
|
||||
status: Math.random() > 0.5 ? 'correct' : 'wrong',
|
||||
TeamQuestionUseID: 123,
|
||||
Guid: `mock-guid-${j}`,
|
||||
})),
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里假设 fetchGetGameStatistics 需要的参数是 TeamGroupID, QuestionID, QuestionDetaiID
|
||||
// 由于接口定义中 QuestionDetaiID 是必须的,但根据需求描述可能只知道 QuestionId
|
||||
// 这里暂时传 0 或者从 questionList 中获取更多信息如果可用
|
||||
const params = {
|
||||
TeamGroupID: Number(currentGroupId.value),
|
||||
QuestionID: Number(currentQuestionId.value),
|
||||
QuestionDetaiID: 0, // 暂时传0,如果有详情ID需补充
|
||||
}
|
||||
|
||||
const { data, error } = await fetchGetGameStatistics(params)
|
||||
if (error) {
|
||||
message.error(error.message)
|
||||
return
|
||||
}
|
||||
|
||||
// 假设返回的数据结构是数组,取第一条或者根据逻辑取特定的一条
|
||||
// 根据需求描述,返回的是一个对象结构
|
||||
const result = data?.data as unknown as TeamAnswerData[]
|
||||
// 注意:fetchGetGameStatistics 返回类型是 App.Service.Response,data.data 类型未知,这里强制转换以便演示
|
||||
// 实际接口可能返回数组,取第一个有数据的
|
||||
|
||||
if (result && result.length > 0) {
|
||||
teamResults.value = result.map((answerData) => {
|
||||
let parsedAnswers: AnswerItem[] = []
|
||||
try {
|
||||
const parsedFonts = JSON.parse(answerData.UserAnswerFont)
|
||||
parsedAnswers = parsedFonts.map((item: any, index: number) => ({
|
||||
id: index + 1,
|
||||
label: `${index + 1}.`,
|
||||
imageUrl: item.AnswerValuePicture, // 注意去除可能存在的反斜杠转义
|
||||
status: item.Status === 1 ? 'correct' : 'wrong', // 假设 1 是正确,0 是错误,根据实际调整
|
||||
TeamQuestionUseID: item.TeamQuestionUseID,
|
||||
Guid: item.Guid,
|
||||
}))
|
||||
}
|
||||
catch (e) {
|
||||
console.error('解析 UserAnswerFont 失败', e)
|
||||
}
|
||||
|
||||
return {
|
||||
TeamId: answerData.TeamId,
|
||||
TeamName: answerData.TeamName,
|
||||
UserAnswerPicture: answerData.UserAnswerPicture,
|
||||
Points: answerData.Points,
|
||||
Answers: parsedAnswers,
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
teamResults.value = []
|
||||
message.info('暂无答题数据')
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
message.error(error.message || '获取答题数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听分组和题目的变化,重新获取答题数据
|
||||
watch([currentGroupId, currentQuestionId], () => {
|
||||
if (currentGroupId.value && currentQuestionId.value) {
|
||||
const question = questionList.value.find(q => q.id === currentQuestionId.value)
|
||||
if (question) {
|
||||
currentQuestionType.value = question.type
|
||||
}
|
||||
fetchAnswerData()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchCurrentQuestion()
|
||||
await fetchGroups()
|
||||
await fetchQuestions()
|
||||
// generateMockData()
|
||||
})
|
||||
</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
|
||||
v-model:group-id="currentGroupId" v-model:question-id="currentQuestionId" :groups="groups"
|
||||
:question-list="questionList"
|
||||
/>
|
||||
</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">
|
||||
当前进度 {{ questionList.findIndex(q => q.id === currentQuestionId) + 1 }}/{{ questionList.length }}
|
||||
</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-auto">
|
||||
<div v-if="teamResults.length === 0" class="h-full flex items-center justify-center text-gray-400">
|
||||
暂无队伍数据
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-6 pb-6">
|
||||
<NCard
|
||||
v-for="team in teamResults" :key="team.TeamId" :title="team.TeamName ? `${team.TeamName}` : '队伍信息'"
|
||||
hoverable
|
||||
>
|
||||
<div class="flex flex-row items-start gap-6">
|
||||
<!-- 学生答题原图 -->
|
||||
<div class="UserAnswerPicture flex-shrink-0">
|
||||
<NImage
|
||||
v-if="team.UserAnswerPicture" :src="team.UserAnswerPicture" width="150" object-fit="contain"
|
||||
class="block border border-gray-200 rounded-lg"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="h-[150px] w-[150px] flex items-center justify-center rounded-lg bg-gray-100 p-2 text-center text-xs text-gray-400"
|
||||
>
|
||||
暂无原图
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 解析答题里面的题目 -->
|
||||
<div class="UserAnswerFont flex-1">
|
||||
<div class="flex flex-wrap gap-x-8 gap-y-4">
|
||||
<!-- Grid Layout -->
|
||||
<div v-if="currentLayout === 'grid'" class="flex flex-wrap gap-4">
|
||||
<div
|
||||
v-for="item in team.Answers" :key="item.id"
|
||||
class="relative cursor-pointer transition-all duration-200" @click="toggleStatus(team, item)"
|
||||
>
|
||||
<!-- Image Container with Status Border -->
|
||||
<div
|
||||
class="relative overflow-hidden border-2 rounded-md border-dashed bg-white p-1" :class="[
|
||||
item.status === 'correct' ? 'border-green-500' : 'border-red-500',
|
||||
]"
|
||||
>
|
||||
<NImage :src="item.imageUrl" preview-disabled object-fit="contain" class="h-24 w-24" />
|
||||
|
||||
<!-- Status Icon Overlay -->
|
||||
<div class="absolute z-10 rounded-full bg-white -right-3 -top-3">
|
||||
<Icon
|
||||
v-if="item.status === 'correct'" icon="mdi:check-circle"
|
||||
class="text-2xl text-green-500"
|
||||
/>
|
||||
<Icon v-else icon="mdi:close-circle" class="text-2xl text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Label if exists -->
|
||||
<div v-if="item.label" class="mt-1 text-center text-sm text-gray-500">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Layout (Long Rectangle for Words/Idioms) -->
|
||||
<div v-else class="flex flex-wrap gap-x-8 gap-y-4">
|
||||
<div
|
||||
v-for="item in team.Answers" :key="item.id"
|
||||
class="relative cursor-pointer transition-all duration-200" @click="toggleStatus(team, item)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="item.label" class="w-6 text-gray-600 font-medium">{{ item.label }}</span>
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden border-2 rounded-md border-dashed bg-white px-2 py-1" :class="[
|
||||
item.status === 'correct' ? 'border-green-500' : 'border-red-500',
|
||||
]"
|
||||
>
|
||||
<NImage :src="item.imageUrl" preview-disabled object-fit="contain" height="50" />
|
||||
|
||||
<!-- Status Icon Overlay -->
|
||||
<div class="absolute z-10 rounded-full bg-white shadow-sm -right-3 -top-3">
|
||||
<Icon
|
||||
v-if="item.status === 'correct'" icon="mdi:check-circle"
|
||||
class="text-2xl text-green-500"
|
||||
/>
|
||||
<Icon v-else icon="mdi:close-circle" class="text-2xl text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-card__content) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
export interface GroupItem {
|
||||
id: number | string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface QuestionListItem {
|
||||
id: number | string
|
||||
name: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
groups: GroupItem[]
|
||||
questionList: QuestionListItem[]
|
||||
groupId: number | string // 分组ID
|
||||
questionId: number | string // 题目ID
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:groupId', id: number | string): void
|
||||
(e: 'update:questionId', id: number | string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="scrollbar-hide flex gap-3 overflow-x-auto pb-2">
|
||||
<NButton
|
||||
v-for="group in groups" :key="group.id" round size="large"
|
||||
:type="groupId === group.id ? 'primary' : 'default'" :secondary="groupId !== group.id"
|
||||
class="px-6 font-bold transition-all" :class="{ 'shadow-lg shadow-blue-500/30': groupId === group.id }"
|
||||
@click="emit('update:groupId', group.id)"
|
||||
>
|
||||
{{ group.name }}
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hide flex gap-3 overflow-x-auto pb-2">
|
||||
<NButton
|
||||
v-for="item in questionList" :key="item.id" round size="large"
|
||||
:type="questionId === item.id ? 'primary' : 'default'" :secondary="questionId !== item.id"
|
||||
class="px-6 font-bold transition-all" :class="{ 'shadow-lg shadow-blue-500/30': questionId === item.id }"
|
||||
@click="emit('update:questionId', item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { NImage } from 'naive-ui'
|
||||
|
||||
export interface AICharacter {
|
||||
id: string
|
||||
char: string
|
||||
isCorrect: boolean
|
||||
}
|
||||
|
||||
export interface TeamResult {
|
||||
id: number | string
|
||||
name: string
|
||||
correctCount: number
|
||||
score: number
|
||||
imageUrl: string
|
||||
status: 'pending' | 'analyzing' | 'done'
|
||||
aiData: AICharacter[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const props = defineProps<{
|
||||
data: TeamResult
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-correct', charId: string): void
|
||||
(e: 'update-char', charId: string, newChar: string): void
|
||||
}>()
|
||||
|
||||
// 切换正确状态
|
||||
function handleCharClick(item: AICharacter) {
|
||||
// eslint-disable-next-line vue/custom-event-name-casing
|
||||
emit('toggle-correct', item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col overflow-hidden border border-gray-100 rounded-xl bg-white shadow-sm">
|
||||
<!-- 头部信息 -->
|
||||
<div class="flex items-center justify-between border-b border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<h3 class="m-0 text-lg text-gray-800 font-bold">
|
||||
{{ data.name }}
|
||||
</h3>
|
||||
<div class="flex gap-3 text-sm">
|
||||
<span class="text-green-600 font-bold">正确 {{ data.correctCount }} 个</span>
|
||||
<span class="text-blue-600 font-bold">积分 {{ data.score }} 分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片区域 -->
|
||||
<div class="relative aspect-[3/4] w-full flex items-center justify-center overflow-hidden border-b border-gray-100 bg-gray-100">
|
||||
<!-- 图片居中 -->
|
||||
<NImage
|
||||
v-if="data.imageUrl"
|
||||
:src="data.imageUrl"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
:img-props="{ class: 'w-full h-full object-contain object-center' }"
|
||||
preview-disabled
|
||||
/>
|
||||
<div v-else class="h-full w-full flex items-center justify-center text-gray-400">
|
||||
等待上传答题卡...
|
||||
</div>
|
||||
|
||||
<!-- 加载遮罩 -->
|
||||
<!-- <div
|
||||
v-if="data.status === 'analyzing'"
|
||||
class="absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[1px]"
|
||||
>
|
||||
<NSpin size="large" />
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- AI 结果网格 -->
|
||||
<div class="min-h-[200px] flex-1 overflow-y-auto p-5">
|
||||
<div v-if="data.aiData.length === 0" class="h-full flex items-center justify-center text-sm text-gray-400">
|
||||
{{ data.status === 'pending' ? '等待识别...' : '暂无数据' }}
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-8 gap-2">
|
||||
<!--
|
||||
grid-cols-5 控制宽度 (宽度由网格列数控制)
|
||||
h-10 控制高度 (高度由固定高度类控制)
|
||||
-->
|
||||
<div
|
||||
v-for="item in data.aiData"
|
||||
:key="item.id"
|
||||
class="h-14 w-14 flex cursor-pointer select-none items-center justify-center rounded-lg text-lg font-bold transition-all active:scale-95 hover:scale-105"
|
||||
:class="[
|
||||
item.isCorrect
|
||||
? 'bg-green-100 text-green-600 border border-green-200'
|
||||
: 'bg-red-100 text-red-600 border border-red-200',
|
||||
]"
|
||||
@click="handleCharClick(item)"
|
||||
>
|
||||
{{ item.char }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Custom scrollbar for the grid area if needed */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
64
apps/admin/src/views/results/results-list/index.vue
Normal file
64
apps/admin/src/views/results/results-list/index.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { NGi, NGrid, NSpace } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetActivityList } from '@/service/api/competition'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import CompetitionCard from '@/views/competition/competition-list/modules/competition-card.vue'
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gap = computed(() => (appStore.isMobile ? 12 : 26))
|
||||
|
||||
const competitionListRef = ref<Api.Competition.CompetitionItem[]>([])
|
||||
|
||||
/** 获取活动列表 */
|
||||
async function fetchActivityList() {
|
||||
try {
|
||||
const { response, error } = await fetchGetActivityList()
|
||||
if (!error) {
|
||||
const list = response?.data?.data || []
|
||||
competitionListRef.value = list.filter((item: any) => item.PublishStatus === PublishStatus.Processing || item.PublishStatus === PublishStatus.Published)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function toDetail(item: Api.Competition.CompetitionItem) {
|
||||
routerPushByKey('results_results-detail', { query: { activityId: item.Id.toString(), roomId: item.RoomID.toString() } })
|
||||
}
|
||||
|
||||
onMounted(() => fetchActivityList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="24">
|
||||
<!-- 顶部 Header 区域 -->
|
||||
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<h2 class="m-0 text-xl text-gray-800 font-bold dark:text-white">
|
||||
比赛列表
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
评委将对正在进行的比赛进行打分
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片列表区域 -->
|
||||
<!-- 响应式布局:手机1列,平板2列,中屏3列,大屏4列 -->
|
||||
<NGrid :x-gap="gap" :y-gap="gap" responsive="screen" item-responsive>
|
||||
<NGi v-for="item in competitionListRef" :key="item.Id" span="24 s:12 m:8 l:6" @click="toDetail(item)">
|
||||
<CompetitionCard :item="item" />
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 针对不同屏幕微调间距 */
|
||||
</style>
|
||||
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { NImage } from 'naive-ui'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchCheckNextStep, fetchGetCurrentQuestion } from '@/service/api/game'
|
||||
import { fetchCheckNextStep, fetchGetCurrentQuestion, fetchGetGameStatistics } from '@/service/api/game'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
|
||||
const store = useCompetitionStore()
|
||||
@ -15,56 +16,17 @@ const groupsId = computed(() => route.query?.groupsId as string)
|
||||
const teamId = computed(() => route.query?.teamId as string)
|
||||
const teamIds = computed(() => route.query?.teamIds as string)
|
||||
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
|
||||
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||||
|
||||
const TeamGroup_QuestionID = computed(() => currentQuestionMainInfo.value?.TeamGroup_QuestionID || 0)
|
||||
const QuestionID = computed(() => currentQuestionMainInfo.value?.Activity_Question.ID || 0)
|
||||
const QuestionDetaiID = computed(() => currentQuestionDetail.value?.Id || 0)
|
||||
|
||||
const zoomedImage = ref<string | null>(null)
|
||||
const isCompleted = ref(false)
|
||||
const showNextBtn = ref(false)
|
||||
|
||||
// Mock team data
|
||||
const teams = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '第一队',
|
||||
writtenCount: 16,
|
||||
correctCount: 13,
|
||||
score: 13,
|
||||
manualScore: null as number | null,
|
||||
image: 'https://via.placeholder.com/300x400?text=Team+1+Answer',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '第二队',
|
||||
writtenCount: 15,
|
||||
correctCount: 12,
|
||||
score: 12,
|
||||
manualScore: null as number | null,
|
||||
image: 'https://via.placeholder.com/300x400?text=Team+2+Answer',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '第三队',
|
||||
writtenCount: 18,
|
||||
correctCount: 15,
|
||||
score: 15,
|
||||
manualScore: null as number | null,
|
||||
image: 'https://via.placeholder.com/300x400?text=Team+3+Answer',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '第四队',
|
||||
writtenCount: 14,
|
||||
correctCount: 10,
|
||||
score: 10,
|
||||
manualScore: null as number | null,
|
||||
image: 'https://via.placeholder.com/300x400?text=Team+4+Answer',
|
||||
},
|
||||
])
|
||||
|
||||
function handleZoom(img: string) {
|
||||
zoomedImage.value = img
|
||||
}
|
||||
const teams = ref<any[]>([])
|
||||
|
||||
function handleBack() {
|
||||
routerPushByKey('user_game', {
|
||||
@ -127,6 +89,7 @@ async function initQuestions() {
|
||||
}
|
||||
|
||||
const timer = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||||
const statisticsTimer = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||||
|
||||
/** 定时刷新 检查评委是否已经完成打分,是否可以下一步 fetchCheckNextStep */
|
||||
async function checkNextStep() {
|
||||
@ -153,12 +116,34 @@ async function checkNextStep() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取游戏现场统计结果 */
|
||||
async function fetchGetGameStatisticsData() {
|
||||
if (store.currentQuestionInfo && store.currentQuestionDetail) {
|
||||
try {
|
||||
const _params = {
|
||||
TeamGroupID: Number(groupsId.value),
|
||||
QuestionID: Number(QuestionID.value),
|
||||
QuestionDetaiID: Number(QuestionDetaiID.value),
|
||||
}
|
||||
const { data } = await fetchGetGameStatistics(_params)
|
||||
teams.value = (data?.data as any[]) || []
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('Failed to fetch game statistics', error)
|
||||
window.$message?.error(error.message || '获取游戏现场统计结果失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定时刷新 检查是否可以下一步
|
||||
timer.value = setInterval(checkNextStep, 2000)
|
||||
// 定时刷新 游戏统计数据
|
||||
statisticsTimer.value = setInterval(fetchGetGameStatisticsData, 2000)
|
||||
|
||||
/** 销毁 定时刷新 检查是否可以下一步 */
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timer.value)
|
||||
clearInterval(statisticsTimer.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -172,7 +157,7 @@ onBeforeUnmount(() => {
|
||||
<!-- Content -->
|
||||
<div class="w-full flex flex-col flex-1 overflow-hidden">
|
||||
<div class="mb-2 text-xl text-red-600 font-bold">
|
||||
备注:由AI模型评判
|
||||
备注:评委正在评分中,请等待。。。
|
||||
</div>
|
||||
|
||||
<div v-if="teams && teams.length > 0" class="grid grid-cols-4 h-full gap-6 pb-8">
|
||||
@ -180,43 +165,37 @@ onBeforeUnmount(() => {
|
||||
v-for="team in teams" :key="team.id"
|
||||
class="flex flex-col border-2 border-gray-200 rounded-xl bg-white/90 p-4 shadow-sm"
|
||||
>
|
||||
<div class="mb-2 text-xl text-gray-800 font-bold">
|
||||
{{ team.name }}
|
||||
<div
|
||||
class="mb-3 flex items-center gap-2 rounded-full from-sky-300 to-cyan-200 bg-gradient-to-r px-4 py-2 shadow-sm"
|
||||
>
|
||||
<span class="text-lg text-white font-bold drop-shadow">{{ team.TeamName }}</span>
|
||||
<span class="ml-2 rounded-full bg-white/40 px-2 py-0.5 text-sm">正确 <span class="text-md text-blue">{{
|
||||
team.correctCount }}</span> 字</span>
|
||||
<span class="text-s rounded-full bg-white/40 px-2 py-0.5">积分 <span class="text-md text-blue">{{ team.Points
|
||||
}}</span></span>
|
||||
</div>
|
||||
|
||||
<!-- Answer Image Area -->
|
||||
<div
|
||||
class="relative mb-4 flex-1 cursor-zoom-in overflow-hidden border border-gray-300 rounded bg-gray-50"
|
||||
@click="handleZoom(team.image)"
|
||||
class="relative mb-4 flex-1 overflow-hidden border border-gray-300 rounded bg-gray-50"
|
||||
>
|
||||
<!-- Grid Background Simulation -->
|
||||
<div class="pointer-events-none absolute inset-0 grid grid-cols-8 gap-px bg-gray-200 opacity-20">
|
||||
<div v-for="n in 64" :key="n" class="bg-white" />
|
||||
</div>
|
||||
<!-- Simulated Handwriting/Content -->
|
||||
<div class="absolute inset-0 flex items-center justify-center text-gray-400">
|
||||
(点击放大查看详情)
|
||||
<!-- <div v-for="n in 64" :key="n" class="bg-white" /> -->
|
||||
</div>
|
||||
<NImage
|
||||
:src="team.UserAnswerPicture"
|
||||
class="h-full w-full"
|
||||
object-fit="contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Stats & Score -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- <div class="flex flex-col gap-2">
|
||||
<div class="text-sm text-gray-700 font-medium">
|
||||
写了数量{{ team.writtenCount }},正确{{ team.correctCount }}个,积分{{ team.score }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-red-500 font-bold">评委当场评</span>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="team.manualScore" type="number"
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="留个输入框,修改正确积分"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user