feat(admin): 支持模板页码和题目类型细分,优化评分界面
- 在模板详情页添加页码(PageNo)字段,支持模板按页管理 - 将汉字听写和成语题型细分为多个子类型,提升题目分类精度 - 重构结果详情页的评分界面,区分客观题(网格布局)和主观题(分屏布局) - 添加AI评分推荐和人工校准功能,支持分数手动调整和快捷打分 - 实现活动复制功能,优化题目排序逻辑和界面间距 - 新增评委打分和状态更新接口,完善评分数据持久化机制
This commit is contained in:
@ -16,9 +16,11 @@ export enum QuestionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum QuestionCategoryEnum {
|
export enum QuestionCategoryEnum {
|
||||||
ChineseCharacterDictation = 'ChineseCharacterDictation', // 汉字听写-提示 (jiu)
|
ChineseCharacterDictation1 = 'ChineseCharacterDictation1', // 汉字听写-提示 (jiu表示小鸟的叫声)
|
||||||
|
ChineseCharacterDictation2 = 'ChineseCharacterDictation2', // 根据拼音写汉字-提示 (tang)
|
||||||
CharacterRadicalAddition = 'CharacterRadicalAddition', // 汉字加一加 (车)
|
CharacterRadicalAddition = 'CharacterRadicalAddition', // 汉字加一加 (车)
|
||||||
WordDictation = 'WordDictation', // 词语听写 (zhi re)
|
WordDictation = 'WordDictation', // 词语听写 (zhi re)
|
||||||
IdiomWriting = 'IdiomWriting', // 成语-文字要求 (反义字)
|
IdiomWriting1 = 'IdiomWriting1', // 成语-文字要求 (反义字)
|
||||||
|
IdiomWriting2 = 'IdiomWriting2', // 成语-文字要求 (看图写成语)
|
||||||
PoetryComprehension = 'PoetryComprehension', // 诗词理解-选择题
|
PoetryComprehension = 'PoetryComprehension', // 诗词理解-选择题
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export function fetchCreateQuestion(data: Api.Competition.CreateQuestionRequest[
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 根据活动ID获取活动题目列表 */
|
/** 根据活动ID获取活动题目列表 */
|
||||||
export function fetchGetQuestionList(id: string) {
|
export function fetchGetQuestionList(id: number) {
|
||||||
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
return request<App.Service.Response<Api.Competition.QuestionListRecord[]>>({
|
||||||
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}`,
|
url: `/Base/ActivityMain/GetActivity_QuestionByActivityID?ID=${id}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -140,3 +140,11 @@ export function fetchUpdatePublishStatus(id: number, status: number) {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 复制活动 */
|
||||||
|
export function fetchCopyActivity(ActivityID: number) {
|
||||||
|
return request<App.Service.Response<Api.Common.CommonResponse>>({
|
||||||
|
url: `/Base/ActivityMain/CopyActivity?ActivityID=${ActivityID}`,
|
||||||
|
method: 'post',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -8,3 +8,21 @@ export function updateCurrentQuestionUse(TeamGroupID: number) {
|
|||||||
data: { TeamGroupID },
|
data: { TeamGroupID },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 评委对其进行打分 */
|
||||||
|
export function updateScore(Id: number, ResultPotins: number) {
|
||||||
|
return request<App.Service.Response>({
|
||||||
|
url: `/Base/ActivityMain/UpdateTeamQuestionUseOne?Id=${Id}&ResultPotins=${ResultPotins}`,
|
||||||
|
method: 'post',
|
||||||
|
data: { Id, ResultPotins },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 评委更新状态 (Correct/Wrong) */
|
||||||
|
export function updateScoreStatus(Id: number, Status: number) {
|
||||||
|
return request<App.Service.Response>({
|
||||||
|
url: `/Base/ActivityMain/UpdateTeamQuestionUseOneStatus?Id=${Id}&Status=${Status}`,
|
||||||
|
method: 'post',
|
||||||
|
data: { Id, Status },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
16
apps/admin/src/typings/api/Competition.d.ts
vendored
16
apps/admin/src/typings/api/Competition.d.ts
vendored
@ -38,13 +38,15 @@ declare namespace Api {
|
|||||||
type QuestionTimeType = 5 | 10 | 15 | 20 | 30 | 45 | 60 | 90 /** 5s | 10s | 15s | 20s | 30s | 45s | 60s | 90s */
|
type QuestionTimeType = 5 | 10 | 15 | 20 | 30 | 45 | 60 | 90 /** 5s | 10s | 15s | 20s | 30s | 45s | 60s | 90s */
|
||||||
|
|
||||||
/** question template id */
|
/** question template id */
|
||||||
type QuestionCategoryName =
|
type QuestionCategoryName =
|
||||||
| 'ChineseCharacterDictation' // 汉字听写-提示 (jiu)
|
| 'ChineseCharacterDictation1' // 汉字听写-提示 (jiu表示小鸟的叫声)
|
||||||
| 'CharacterRadicalAddition' // 汉字加一加 (车)
|
| 'ChineseCharacterDictation2' // 根据拼音写汉字-提示 (tang)
|
||||||
| 'WordDictation' // 词语听写 (zhi re)
|
| 'CharacterRadicalAddition' // 汉字加一加 (车)
|
||||||
| 'IdiomWriting' // 成语-文字要求 (反义字)
|
| 'WordDictation' // 词语听写 (zhi re)
|
||||||
// | 'TEMPLATE_IDIOM_IMAGE' // 成语-看图
|
| 'IdiomWriting1' // 成语-文字要求 (反义字)
|
||||||
| 'PoetryComprehension' // 诗词理解-选择题
|
| 'IdiomWriting2' // 成语-文字要求 (看图写成语)
|
||||||
|
// | 'TEMPLATE_IDIOM_IMAGE' // 成语-看图
|
||||||
|
| 'PoetryComprehension' // 诗词理解-选择题
|
||||||
|
|
||||||
/** question score type */
|
/** question score type */
|
||||||
type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */
|
type QuestionScoreType = 0 | 1 /** 固定分数 | 答题个数 */
|
||||||
|
|||||||
2
apps/admin/src/typings/api/template.d.ts
vendored
2
apps/admin/src/typings/api/template.d.ts
vendored
@ -46,6 +46,8 @@ declare namespace Api {
|
|||||||
BackGroundUrl: string
|
BackGroundUrl: string
|
||||||
/** template content */
|
/** template content */
|
||||||
TempContent: string
|
TempContent: string
|
||||||
|
/** page number */
|
||||||
|
PageNo: string
|
||||||
/** competition id */
|
/** competition id */
|
||||||
CompetitionId?: number
|
CompetitionId?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,7 +163,7 @@ async function submitForm() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 根据 questionID 降序排序 (例如: 54455 -> 55544)
|
// 根据 questionID 降序排序 (例如: 54455 -> 55544)
|
||||||
questionParams.sort((a, b) => Number(b.questionID) - Number(a.questionID))
|
// questionParams.sort((a, b) => Number(b.questionID) - Number(a.questionID))
|
||||||
|
|
||||||
// 重新计算 questionIndex
|
// 重新计算 questionIndex
|
||||||
questionParams.forEach((item, index) => {
|
questionParams.forEach((item, index) => {
|
||||||
|
|||||||
@ -186,7 +186,7 @@ defineExpose({ validate, reset })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表内容 -->
|
<!-- 列表内容 -->
|
||||||
<div v-else class="flex flex-col gap-6 pb-10">
|
<div v-else class="flex flex-col gap-6 pb-40">
|
||||||
<div
|
<div
|
||||||
v-for="(round, roundIndex) in props.modelValue?.rounds" :key="round.id"
|
v-for="(round, roundIndex) in props.modelValue?.rounds" :key="round.id"
|
||||||
class="group relative overflow-hidden border border-gray-100 rounded-2xl bg-white p-1 shadow-sm transition-all hover:shadow-md"
|
class="group relative overflow-hidden border border-gray-100 rounded-2xl bg-white p-1 shadow-sm transition-all hover:shadow-md"
|
||||||
|
|||||||
@ -114,8 +114,8 @@ async function getQuestionList(id: number) {
|
|||||||
// 点击编辑模块
|
// 点击编辑模块
|
||||||
/** 请求编辑模块 */
|
/** 请求编辑模块 */
|
||||||
function handleRequestEdit(moduleName: string) {
|
function handleRequestEdit(moduleName: string) {
|
||||||
if (basicInfo.value.PublishStatus === PublishStatus.Processing || basicInfo.value.PublishStatus === PublishStatus.Finished) {
|
if (basicInfo.value.PublishStatus === PublishStatus.Processing || basicInfo.value.PublishStatus === PublishStatus.Finished || basicInfo.value.PublishStatus === PublishStatus.Published) {
|
||||||
window.$message?.warning('进行中或已结束的赛事无法编辑')
|
window.$message?.warning('已发布、进行中或已结束的赛事无法编辑')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (activeEditModule.value && activeEditModule.value !== moduleName) {
|
if (activeEditModule.value && activeEditModule.value !== moduleName) {
|
||||||
@ -220,7 +220,7 @@ async function handleSaveQuestion() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 根据 questionID 降序排序 (例如: 54455 -> 55544)
|
// 根据 questionID 降序排序 (例如: 54455 -> 55544)
|
||||||
flatQuestions.sort((a, b) => Number(b.QuestionID) - Number(a.QuestionID))
|
// flatQuestions.sort((a, b) => Number(b.QuestionID) - Number(a.QuestionID))
|
||||||
|
|
||||||
// 重新计算 questionIndex
|
// 重新计算 questionIndex
|
||||||
flatQuestions.forEach((item, index) => {
|
flatQuestions.forEach((item, index) => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { NButton, NGi, NGrid, NSpace } from 'naive-ui'
|
import { NButton, NGi, NGrid, NSpace } from 'naive-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouterPush } from '@/hooks/common/router'
|
import { useRouterPush } from '@/hooks/common/router'
|
||||||
import { fetchDeleteActivity, fetchGetActivityList } from '@/service/api/competition'
|
import { fetchCopyActivity, fetchDeleteActivity, fetchGetActivityList } from '@/service/api/competition'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import CompetitionAddModal from '../competition-add/index.vue'
|
import CompetitionAddModal from '../competition-add/index.vue'
|
||||||
|
|
||||||
@ -21,9 +21,23 @@ function handleAdd() {
|
|||||||
showAddModal.value = true
|
showAddModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopy(item: Api.Competition.CompetitionItem) {
|
async function handleCopy(item: Api.Competition.CompetitionItem) {
|
||||||
// TODO: 实现复制逻辑
|
// TODO: 实现复制逻辑
|
||||||
|
try {
|
||||||
|
const { error } = await fetchCopyActivity(item.Id)
|
||||||
|
if (error) {
|
||||||
|
window.$message?.error(error.message || '复制失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
window.$message?.error('复制失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
window.$message?.success(`已复制活动:${item.Name}`)
|
window.$message?.success(`已复制活动:${item.Name}`)
|
||||||
|
// 刷新列表
|
||||||
|
fetchActivityList()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(item: Api.Competition.CompetitionItem) {
|
function handleDelete(item: Api.Competition.CompetitionItem) {
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
|
<!-- eslint-disable no-console -->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { NButton, NCard, NImage, useMessage } from 'naive-ui'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { NButton, NCard, NImage, NInputNumber, NTag, useMessage } from 'naive-ui'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { QuestionCategoryEnum } from '@/enum/business'
|
import { QuestionCategoryEnum } from '@/enum/business'
|
||||||
import { fetchGetQuestionList } from '@/service/api/competition'
|
import { fetchGetQuestionList } from '@/service/api/competition'
|
||||||
import { fetchGetCurrentQuestionByRoomID, fetchGetGameStatistics, fetchGetGroupListByActivityID } from '@/service/api/game'
|
import { fetchGetCurrentQuestionByRoomID, fetchGetGameStatistics, fetchGetGroupListByActivityID } from '@/service/api/game'
|
||||||
import { updateCurrentQuestionUse } from '@/service/api/result'
|
import { updateCurrentQuestionUse, updateScore } from '@/service/api/result'
|
||||||
import GroupTabs, { type GroupItem } from './modules/GroupTabs.vue'
|
import GroupTabs, { type GroupItem } from './modules/GroupTabs.vue'
|
||||||
|
|
||||||
|
// 本地持久化存储手动修改的状态 (key: roomId-questionId-guid)
|
||||||
|
const manualStatusStorage = useStorage<Record<string, number>>('read-star-manual-status', {})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const activityId = computed(() => route.query?.activityId as string)
|
const activityId = computed(() => route.query?.activityId as string)
|
||||||
@ -24,17 +29,12 @@ const currentQuestion = ref<Api.Competition.GetCurrentQuestionInfo>() // 当前
|
|||||||
|
|
||||||
// 计算当前题目类型对应的布局
|
// 计算当前题目类型对应的布局
|
||||||
const currentLayout = computed(() => {
|
const currentLayout = computed(() => {
|
||||||
if (!currentQuestionType.value)
|
// 只有 汉字加一加 (CharacterRadicalAddition) 需要人工评分 (列表布局 - 分屏视图)
|
||||||
return 'list' // 默认列表
|
// 其他所有题型 (汉字听写、诗词、词语听写、成语) 都使用长框展示 (网格布局 - 带切换的大图)
|
||||||
|
if (currentQuestionType.value === QuestionCategoryEnum.CharacterRadicalAddition) {
|
||||||
// 汉字听写、汉字加一加、选择题 -> Grid (多个小方块)
|
return 'list'
|
||||||
const gridTypes = [
|
}
|
||||||
QuestionCategoryEnum.ChineseCharacterDictation,
|
return 'grid'
|
||||||
QuestionCategoryEnum.CharacterRadicalAddition,
|
|
||||||
QuestionCategoryEnum.PoetryComprehension,
|
|
||||||
]
|
|
||||||
|
|
||||||
return gridTypes.includes(currentQuestionType.value as QuestionCategoryEnum) ? 'grid' : 'list'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface TeamResult {
|
interface TeamResult {
|
||||||
@ -48,9 +48,28 @@ interface TeamResult {
|
|||||||
// 当前所有队伍的解析结果列表
|
// 当前所有队伍的解析结果列表
|
||||||
const teamResults = ref<TeamResult[]>([])
|
const teamResults = ref<TeamResult[]>([])
|
||||||
|
|
||||||
// 切换状态逻辑:点击后 Correct <-> Wrong
|
// 手动设置状态(确认或修改)
|
||||||
function toggleStatus(team: TeamResult, item: any) {
|
async function setStatus(team: TeamResult, item: any, status: number) {
|
||||||
item.Status = item.Status === 1 ? 0 : 1
|
item.Status = status
|
||||||
|
item._isManual = true
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
if (item.Guid && team.TeamId) {
|
||||||
|
const key = `${roomId.value}-${currentQuestionId.value}-${team.TeamId}-${item.Guid}`
|
||||||
|
manualStatusStorage.value[key] = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleManualScoreUpdate(item: any, newScore: number) {
|
||||||
|
item.Score = newScore
|
||||||
|
item._isManual = true
|
||||||
|
try {
|
||||||
|
await updateScore(item.TeamQuestionUseID, newScore)
|
||||||
|
message.success('分数已更新')
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
message.error(e.message || '保存分数失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNext() {
|
function handleNext() {
|
||||||
@ -86,72 +105,6 @@ function syncCurrentQuestionSelection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) => ({
|
|
||||||
AnswerValuePicture: answerCount > 1
|
|
||||||
? `https://placehold.co/100x100?text=Answer+${j + 1}`
|
|
||||||
: `https://placehold.co/400x100?text=Long+Answer`,
|
|
||||||
Status: Math.random() > 0.5 ? 1 : 0,
|
|
||||||
TeamQuestionUseID: 123,
|
|
||||||
Guid: `mock-guid-${i}-${j}`,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取分组列表
|
* 获取分组列表
|
||||||
*/
|
*/
|
||||||
@ -188,7 +141,7 @@ async function fetchQuestions() {
|
|||||||
if (!activityId.value)
|
if (!activityId.value)
|
||||||
return
|
return
|
||||||
try {
|
try {
|
||||||
const { data, error } = await fetchGetQuestionList(activityId.value)
|
const { data, error } = await fetchGetQuestionList(Number(activityId.value))
|
||||||
if (error) {
|
if (error) {
|
||||||
message.error(error.message)
|
message.error(error.message)
|
||||||
return
|
return
|
||||||
@ -205,6 +158,7 @@ async function fetchQuestions() {
|
|||||||
if (questionList.value.length > 0 && !currentQuestionId.value) {
|
if (questionList.value.length > 0 && !currentQuestionId.value) {
|
||||||
currentQuestionId.value = questionList.value[0].id
|
currentQuestionId.value = questionList.value[0].id
|
||||||
currentQuestionType.value = questionList.value[0].type
|
currentQuestionType.value = questionList.value[0].type
|
||||||
|
console.warn(questionList.value, 'questionList')
|
||||||
}
|
}
|
||||||
// 题目加载完成后尝试同步到“当前进行中的题目”
|
// 题目加载完成后尝试同步到“当前进行中的题目”
|
||||||
syncCurrentQuestionSelection()
|
syncCurrentQuestionSelection()
|
||||||
@ -217,7 +171,6 @@ async function fetchQuestions() {
|
|||||||
/**
|
/**
|
||||||
* 获取当前正在比赛的题目
|
* 获取当前正在比赛的题目
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function fetchCurrentQuestion() {
|
async function fetchCurrentQuestion() {
|
||||||
if (!roomId.value)
|
if (!roomId.value)
|
||||||
return
|
return
|
||||||
@ -243,47 +196,10 @@ async function fetchCurrentQuestion() {
|
|||||||
async function fetchAnswerData() {
|
async function fetchAnswerData() {
|
||||||
if (!currentGroupId.value || !currentQuestionId.value || !currentQuestion.value?.QuestionDetailID)
|
if (!currentGroupId.value || !currentQuestionId.value || !currentQuestion.value?.QuestionDetailID)
|
||||||
return
|
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) => ({
|
|
||||||
AnswerValuePicture: answerCount > 1
|
|
||||||
? `https://placehold.co/100x100?text=Answer+${j + 1}`
|
|
||||||
: `https://placehold.co/400x100?text=Long+Answer`,
|
|
||||||
Status: Math.random() > 0.5 ? 1 : 0,
|
|
||||||
TeamQuestionUseID: 123,
|
|
||||||
Guid: `mock-guid-${i}-${j}`,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里假设 fetchGetGameStatistics 需要的参数是 TeamGroupID, QuestionID, QuestionDetaiID
|
|
||||||
// 由于接口定义中 QuestionDetaiID 是必须的,但根据需求描述可能只知道 QuestionId
|
|
||||||
// 这里暂时传 0 或者从 questionList 中获取更多信息如果可用
|
|
||||||
const params = {
|
const params = {
|
||||||
TeamGroupID: Number(currentGroupId.value),
|
TeamGroupID: Number(currentGroupId.value),
|
||||||
QuestionID: Number(currentQuestionId.value),
|
QuestionID: Number(currentQuestionId.value),
|
||||||
// QuestionDetaiID: currentQuestion.value?.QuestionDetailID || 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await fetchGetGameStatistics(params)
|
const { data, error } = await fetchGetGameStatistics(params)
|
||||||
@ -294,26 +210,89 @@ async function fetchAnswerData() {
|
|||||||
|
|
||||||
// 假设返回的数据结构是数组,取第一条或者根据逻辑取特定的一条
|
// 假设返回的数据结构是数组,取第一条或者根据逻辑取特定的一条
|
||||||
const result = data?.data || []
|
const result = data?.data || []
|
||||||
console.warn('game statistics result', result)
|
console.log('接口返回的游戏统计数据:', result)
|
||||||
|
|
||||||
|
result.forEach((item) => {
|
||||||
|
const UserAnswerFont = JSON.parse(item.UserAnswerFont)
|
||||||
|
console.log('解析后的 UserAnswerFont:', UserAnswerFont)
|
||||||
|
})
|
||||||
|
|
||||||
if (result && result.length > 0) {
|
if (result && result.length > 0) {
|
||||||
|
// 创建现有项目的映射以保留手动编辑
|
||||||
|
const oldMap = new Map<string, any>()
|
||||||
|
if (teamResults.value && teamResults.value.length > 0) {
|
||||||
|
teamResults.value.forEach((t) => {
|
||||||
|
if (t.Answers) {
|
||||||
|
t.Answers.forEach((a: any) => {
|
||||||
|
if (a.Guid) {
|
||||||
|
oldMap.set(a.Guid, a)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
teamResults.value = result.map((answerData) => {
|
teamResults.value = result.map((answerData) => {
|
||||||
let parsedAnswers: any[] = []
|
let parsedAnswers: any[] = []
|
||||||
try {
|
try {
|
||||||
const parsedFonts = JSON.parse(answerData.UserAnswerFont)
|
if (answerData.UserAnswerFont) {
|
||||||
parsedAnswers = parsedFonts
|
const parsedFonts = JSON.parse(answerData.UserAnswerFont)
|
||||||
console.warn('parsed fonts', parsedFonts)
|
parsedAnswers = Array.isArray(parsedFonts) ? parsedFonts : []
|
||||||
|
console.warn('parsed fonts', parsedFonts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('解析 UserAnswerFont 失败', e)
|
console.error('解析 UserAnswerFont 失败', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将新答案与旧的手动编辑合并
|
||||||
|
const mergedAnswers = parsedAnswers.map((ans: any) => {
|
||||||
|
const oldAns = oldMap.get(ans.Guid)
|
||||||
|
|
||||||
|
// 检查本地存储是否有手动修改记录
|
||||||
|
const storageKey = `${roomId.value}-${currentQuestionId.value}-${answerData.TeamId}-${ans.Guid}`
|
||||||
|
const storedStatus = manualStatusStorage.value[storageKey]
|
||||||
|
const hasStoredStatus = storedStatus !== undefined
|
||||||
|
|
||||||
|
// 记录 AI 的原始推荐值(如果后端返回了)
|
||||||
|
const aiStatus = ans.Status
|
||||||
|
const aiScore = ans.Score
|
||||||
|
|
||||||
|
if ((oldAns && oldAns._isManual) || hasStoredStatus) {
|
||||||
|
// 优先使用本地存储的状态,其次是内存中的手动编辑状态
|
||||||
|
const finalStatus = hasStoredStatus ? storedStatus : (oldAns ? oldAns.Status : ans.Status)
|
||||||
|
const finalScore = oldAns ? oldAns.Score : ans.Score // 分数目前只在内存保留,也可以加 localStorage
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ans,
|
||||||
|
Score: finalScore,
|
||||||
|
Status: finalStatus,
|
||||||
|
_isManual: true,
|
||||||
|
_aiStatus: aiStatus, // 保存 AI 的最新状态供参考
|
||||||
|
_aiScore: aiScore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有手动编辑,不要自动应用 AI 的状态,保持“未评”状态
|
||||||
|
// 用 _isManual = false 来表示 "未人工介入",此时 Status 仍然跟随 AI (ans.Status),
|
||||||
|
// 但 UI 上会显示 "AI 推荐: Correct/Wrong",而不是 "已人工校准"。
|
||||||
|
// 只有当用户点击后,_isManual = true,才显示 "已人工校准"。
|
||||||
|
// 并且 Status = ans.Status (AI 的值)。
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ans,
|
||||||
|
Status: ans.Status, // 默认跟随 AI,但 UI 会标记为 "AI 推荐"
|
||||||
|
_aiStatus: aiStatus,
|
||||||
|
_aiScore: aiScore,
|
||||||
|
_isManual: false, // 明确标记为非手动
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
TeamId: answerData.TeamId,
|
TeamId: answerData.TeamId,
|
||||||
TeamName: answerData.TeamName,
|
TeamName: answerData.TeamName,
|
||||||
UserAnswerPicture: answerData.UserAnswerPicture,
|
UserAnswerPicture: answerData.UserAnswerPicture,
|
||||||
Points: answerData.Points,
|
Points: answerData.Points,
|
||||||
Answers: parsedAnswers,
|
Answers: mergedAnswers,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -340,18 +319,89 @@ async function handleNexCurrentQuestion() {
|
|||||||
|
|
||||||
function handlePublish() {
|
function handlePublish() {
|
||||||
if (currentQuestion.value?.TeamGroupQuestionID) {
|
if (currentQuestion.value?.TeamGroupQuestionID) {
|
||||||
window.$dialog?.info({
|
// 检查是否有未评分的题目
|
||||||
title: '发布确认',
|
let hasUnrated = false
|
||||||
content: '确定要发布当前题目吗?',
|
teamResults.value.forEach((team) => {
|
||||||
positiveText: '发布',
|
if (team.Answers) {
|
||||||
negativeText: '取消',
|
team.Answers.forEach((item: any) => {
|
||||||
|
if (!item._isManual) {
|
||||||
|
hasUnrated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasUnrated) {
|
||||||
|
window.$message?.error('还有未人工校准的题目,请完成后再发布!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.$dialog?.warning({
|
||||||
|
title: '二次确认',
|
||||||
|
content: '请仔细检查所有人工打分和AI推荐结果是否已核对无误。提交后结果将不可更改,确定要发布吗?',
|
||||||
|
positiveText: '确认发布',
|
||||||
|
negativeText: '再次检查',
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
await fetchUpdateCurrentQuestionUse(currentQuestion.value?.TeamGroupQuestionID || 0)
|
// 在发布前,将所有手动修改的分数/状态提交一次
|
||||||
|
const updatePromises: Promise<any>[] = []
|
||||||
|
|
||||||
|
teamResults.value.forEach((team) => {
|
||||||
|
if (team.Answers) {
|
||||||
|
team.Answers.forEach((item: any) => {
|
||||||
|
if (item._isManual) {
|
||||||
|
// 调用 updateScore 接口提交分数
|
||||||
|
// 对于客观题,如果 Status=1 (Correct),Score=1;Status=0 (Wrong),Score=0
|
||||||
|
// 假设客观题满分是 1 分 (或者根据业务逻辑调整)
|
||||||
|
let scoreToSubmit = item.Score
|
||||||
|
if (currentLayout.value === 'grid') {
|
||||||
|
scoreToSubmit = item.Status === 1 ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePromises.push(updateScore(item.TeamQuestionUseID, scoreToSubmit))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(updatePromises)
|
||||||
|
await fetchUpdateCurrentQuestionUse(currentQuestion.value?.TeamGroupQuestionID || 0)
|
||||||
|
// 发布成功后,清理本地存储
|
||||||
|
const prefix = `${roomId.value}-${currentQuestionId.value}-`
|
||||||
|
Object.keys(manualStatusStorage.value).forEach((k) => {
|
||||||
|
if (k.startsWith(prefix))
|
||||||
|
delete manualStatusStorage.value[k]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
window.$message?.error(error.message || '提交分数或发布失败,请重试')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费并发布当前题目
|
||||||
|
*/
|
||||||
|
const pollingTimer = ref<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
if (pollingTimer.value)
|
||||||
|
return
|
||||||
|
fetchAnswerData() // 立即获取
|
||||||
|
pollingTimer.value = setInterval(() => {
|
||||||
|
fetchAnswerData()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
pollingTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消费并发布当前题目
|
* 消费并发布当前题目
|
||||||
*/
|
*/
|
||||||
@ -361,6 +411,7 @@ async function fetchUpdateCurrentQuestionUse(TeamGroupQuestionID: number) {
|
|||||||
console.warn('updateCurrentQuestionUse result', data)
|
console.warn('updateCurrentQuestionUse result', data)
|
||||||
if (!error) {
|
if (!error) {
|
||||||
window?.$message?.success('结果已发布')
|
window?.$message?.success('结果已发布')
|
||||||
|
stopPolling() // 发布后停止轮询
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
@ -375,22 +426,32 @@ watch([currentGroupId, currentQuestionId], () => {
|
|||||||
if (question) {
|
if (question) {
|
||||||
currentQuestionType.value = question.type
|
currentQuestionType.value = question.type
|
||||||
}
|
}
|
||||||
fetchAnswerData()
|
// 重新启动新问题的轮询(通过获取重新开始隐式清除旧的手动编辑?不,等等。)
|
||||||
|
// 我们可能需要重置 teamResults 或根据需要处理手动编辑清除。
|
||||||
|
// 目前,fetchAnswerData 将根据新问题数据重新初始化。
|
||||||
|
// 但是,如果我们保持轮询,我们需要确保如果 ID 冲突(如果 Guid 是唯一的,则不太可能),我们不会保留上一个问题的手动编辑。
|
||||||
|
// 让我们停止并重新启动以保持清洁。
|
||||||
|
stopPolling()
|
||||||
|
teamResults.value = [] // 清除旧结果以避免闪烁或陈旧数据
|
||||||
|
startPolling()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchGroups()
|
// 并行拉取分组、题目与当前题目,完成后做一次同步
|
||||||
// 并行拉取题目与当前题目,完成后做一次同步
|
await Promise.all([fetchGroups(), fetchQuestions(), fetchCurrentQuestion()])
|
||||||
await Promise.all([fetchQuestions(), fetchCurrentQuestion()])
|
|
||||||
syncCurrentQuestionSelection()
|
syncCurrentQuestionSelection()
|
||||||
// generateMockData()
|
startPolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col gap-2 overflow-hidden bg-[#F9FAFB] p-6">
|
<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 justify-between rounded-2xl bg-white px-6 py-4 shadow-sm">
|
||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-8">
|
||||||
<h1 class="m-0 flex items-center gap-2 text-xl text-gray-800 font-bold">
|
<h1 class="m-0 flex items-center gap-2 text-xl text-gray-800 font-bold">
|
||||||
@ -419,20 +480,38 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- 主要内容 -->
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<div v-if="teamResults.length === 0" class="h-full flex items-center justify-center text-gray-400">
|
<div v-if="teamResults.length === 0" class="h-full flex items-center justify-center text-gray-400">
|
||||||
暂无队伍数据
|
暂无队伍数据
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex flex-col gap-6 pb-6">
|
<div v-else class="flex flex-col gap-6 pb-6">
|
||||||
<NCard
|
<NCard v-for="team in teamResults" :key="team.TeamId" hoverable>
|
||||||
v-for="team in teamResults" :key="team.TeamId" :title="team.TeamName ? `${team.TeamName}` : '队伍信息'"
|
<template #header>
|
||||||
hoverable
|
<div class="flex flex-col gap-4">
|
||||||
>
|
<!-- 队伍信息 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<NTag type="info" round size="large" class="text-base font-bold">
|
||||||
|
{{ team.TeamName || '队伍信息' }}
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 参考答案展示(如果有) -->
|
||||||
|
<div v-if="currentQuestion?.Answer" class="border border-green-200 rounded-lg bg-green-50 p-4">
|
||||||
|
<div class="mb-2 text-sm text-green-800 font-bold tracking-wider uppercase">
|
||||||
|
参考答案
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-green-900 font-medium leading-relaxed">
|
||||||
|
{{ currentQuestion.Answer }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-row items-start gap-6 px-4 py-2">
|
<div class="flex flex-row items-start gap-6 px-4 py-2">
|
||||||
<!-- 学生答题原图 -->
|
<!-- 学生答题原图 -->
|
||||||
<div class="UserAnswerPicture flex-shrink-0">
|
<!-- <div class="UserAnswerPicture flex-shrink-0">
|
||||||
<NImage
|
<NImage
|
||||||
v-if="team.UserAnswerPicture" :src="team.UserAnswerPicture" width="550" object-fit="contain"
|
v-if="team.UserAnswerPicture" :src="team.UserAnswerPicture" width="550" object-fit="contain"
|
||||||
class="block border border-gray-200 rounded-lg"
|
class="block border border-gray-200 rounded-lg"
|
||||||
@ -443,65 +522,175 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
暂无原图
|
暂无原图
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 解析答题里面的题目 -->
|
<!-- 解析答题里面的题目 -->
|
||||||
<div class="UserAnswerFont flex-1">
|
<div class="UserAnswerFont flex-1">
|
||||||
<div class="flex flex-wrap gap-x-8 gap-y-4">
|
<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-if="currentLayout === 'grid'" class="w-full flex flex-col gap-6">
|
||||||
<div
|
<div
|
||||||
v-for="(item) in team.Answers" :key="item.Guid"
|
v-for="(item) in team.Answers" :key="item.Guid"
|
||||||
class="relative cursor-pointer transition-all duration-200" @click="toggleStatus(team, item)"
|
class="relative w-full overflow-hidden border-2 rounded-xl border-dashed p-4 transition-all duration-300"
|
||||||
|
:class="[
|
||||||
|
!item._isManual ? 'border-gray-300 bg-gray-50/30' : (item.Status === 1 ? 'border-green-500 bg-green-50/30' : 'border-red-500 bg-red-50/30'),
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<!-- Image Container with Status Border -->
|
<!-- 图片上方的标签 -->
|
||||||
|
<div class="mb-2 text-center text-lg text-gray-700 font-bold">
|
||||||
|
{{ item.AnswerText || '答案' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Suggestion Indicator -->
|
||||||
<div
|
<div
|
||||||
class="relative overflow-hidden border-2 rounded-md border-dashed bg-white p-1" :class="[
|
v-if="!item._isManual"
|
||||||
item.Status === 1 ? 'border-green-500' : 'border-red-500',
|
class="mb-2 flex items-center justify-center gap-1 text-xs text-orange-500 font-bold"
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center">
|
<div class="i-carbon-ai-status" />
|
||||||
<NImage
|
AI 推荐: {{ item.Status === 1 ? '正确' : '错误' }}
|
||||||
:src="item.AnswerValuePicture || item.imageUrl" preview-disabled object-fit="contain"
|
</div>
|
||||||
/>
|
<!-- Manual Calibration Indicator -->
|
||||||
<!-- {{ item.AnswerText }} -->
|
<div
|
||||||
</div>
|
v-else
|
||||||
<!-- Status Icon Overlay -->
|
class="mb-2 flex items-center justify-center gap-1 text-xs text-green-600 font-bold"
|
||||||
<div class="absolute z-10 rounded-full bg-white -right-3px -top-3px">
|
>
|
||||||
<Icon v-if="item.Status === 1" icon="mdi:check-circle" class="text-2xl text-green-500" />
|
<div class="i-carbon-user-avatar-filled-alt" />
|
||||||
<Icon v-else icon="mdi:close-circle" class="text-2xl text-red-500" />
|
已人工校准: {{ item.Status === 1 ? '正确' : '错误' }}
|
||||||
|
</div>
|
||||||
|
<!-- 图片展示 -->
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<NImage
|
||||||
|
:src="item.AnswerValuePicture || item.imageUrl" object-fit="contain"
|
||||||
|
class="max-h-[300px] w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Button Overlay (Center) -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex flex-col items-center justify-center gap-2 opacity-0 transition-opacity duration-200 hover:opacity-100"
|
||||||
|
>
|
||||||
|
<!-- 如果未校准,显示两个按钮 -->
|
||||||
|
<template v-if="!item._isManual">
|
||||||
|
<NButton strong secondary round size="medium" type="success" class="shadow-xl" @click="setStatus(team, item, 1)">
|
||||||
|
确认正确 (绿)
|
||||||
|
</NButton>
|
||||||
|
<NButton strong secondary round size="medium" type="error" class="shadow-xl" @click="setStatus(team, item, 0)">
|
||||||
|
确认错误 (红)
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 如果已校准,显示切换按钮 -->
|
||||||
|
<template v-else>
|
||||||
|
<NButton
|
||||||
|
strong secondary round size="large" :type="item.Status === 1 ? 'error' : 'success'"
|
||||||
|
class="shadow-xl" @click="setStatus(team, item, item.Status === 1 ? 0 : 1)"
|
||||||
|
>
|
||||||
|
切换为 {{ item.Status === 1 ? '错误 (红)' : '正确 (绿)' }}
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Icon (Top Right) - Only show if manually calibrated -->
|
||||||
|
<div v-if="item._isManual" class="absolute right-4 top-4 z-10">
|
||||||
|
<div class="rounded-full bg-white shadow-md">
|
||||||
|
<Icon v-if="item.Status === 1" icon="mdi:check-circle" class="text-4xl text-green-500" />
|
||||||
|
<Icon v-else icon="mdi:close-circle" class="text-4xl text-red-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Label if exists -->
|
<!-- Manual Calibration Badge (Bottom Right) -->
|
||||||
<div class="mt-1 text-center text-sm text-gray-500">
|
<div class="absolute bottom-4 right-4 z-10">
|
||||||
{{ item.AnswerText }}
|
<div
|
||||||
|
class="flex items-center gap-1 border border-green-200 rounded bg-green-50 px-2 py-1 text-xs text-green-600"
|
||||||
|
>
|
||||||
|
<div class="i-carbon-checkmark" />
|
||||||
|
已人工校准
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- List Layout (Long Rectangle for Words/Idioms) -->
|
<!-- List Layout: 主观题 -> Split View (Image Left, Scoring Right) -->
|
||||||
<div v-else class="flex flex-wrap gap-x-8 gap-y-4">
|
<div v-else class="w-full flex flex-col gap-6">
|
||||||
<div
|
<div
|
||||||
v-for="(item, idx) in team.Answers" :key="item.Guid"
|
v-for="(item, idx) in team.Answers" :key="item.Guid"
|
||||||
class="relative cursor-pointer transition-all duration-200" @click="toggleStatus(team, item)"
|
class="flex flex-col gap-6 border border-gray-100 rounded-xl bg-white p-6 shadow-sm"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<!-- Title -->
|
||||||
<span class="w-6 text-gray-600 font-medium">{{ idx + 1 }}.</span>
|
<div class="text-lg text-gray-800 font-bold">
|
||||||
|
{{ idx + 1 }}. {{ item.AnswerText || '题目' }} (汉字加一加)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6 lg:flex-row">
|
||||||
|
<!-- Left: Original Image Display Area -->
|
||||||
<div
|
<div
|
||||||
class="relative overflow-hidden border-2 rounded-md border-dashed bg-white px-2 py-1" :class="[
|
class="relative min-h-[800px] flex-1 overflow-hidden border-2 border-gray-200 rounded-xl border-dashed bg-gray-50 p-4"
|
||||||
item.Status === 1 ? 'border-green-500' : 'border-red-500',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<NImage
|
<div class="absolute left-4 top-4 flex items-center gap-1 text-sm text-gray-400">
|
||||||
:src="item.AnswerValuePicture || item.imageUrl" preview-disabled object-fit="contain"
|
<div class="i-carbon-image" />
|
||||||
/>
|
主观题原图展示区
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center justify-center">
|
||||||
|
<NImage
|
||||||
|
v-if="item.AnswerValuePicture || item.imageUrl"
|
||||||
|
:src="item.AnswerValuePicture || item.imageUrl" object-fit="contain"
|
||||||
|
class="max-h-[800px] w-full"
|
||||||
|
/>
|
||||||
|
<div v-else class="text-gray-300">
|
||||||
|
<div class="i-carbon-no-image text-6xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Status Icon Overlay -->
|
<!-- Right: Scoring Panel -->
|
||||||
<div class="absolute z-10 rounded-full bg-white shadow-sm -right-3 -top-3">
|
<div class="w-full flex flex-col gap-6 rounded-xl bg-blue-50/30 p-6 lg:w-[320px]">
|
||||||
<Icon v-if="item.Status === 1" icon="mdi:check-circle" class="text-2xl text-green-500" />
|
<div>
|
||||||
<Icon v-else icon="mdi:close-circle" class="text-2xl text-red-500" />
|
<div class="text-lg text-gray-800 font-bold">
|
||||||
|
人工评分
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-500">
|
||||||
|
请根据对的字数进行打分
|
||||||
|
</div>
|
||||||
|
<div v-if="!item._isManual" class="mt-2 flex items-center gap-2 rounded bg-orange-50 px-2 py-1 text-xs text-orange-600">
|
||||||
|
<div class="i-carbon-ai-status" />
|
||||||
|
AI 推荐: {{ item.Score }} 分
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-2 flex items-center gap-2 rounded bg-green-50 px-2 py-1 text-xs text-green-600">
|
||||||
|
<div class="i-carbon-user-avatar-filled-alt" />
|
||||||
|
已人工校准: {{ item.Score }} 分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Score Input -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="text-sm text-gray-600 font-medium">
|
||||||
|
最终得分
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center border border-blue-100 rounded-xl bg-white px-4 py-3 shadow-sm">
|
||||||
|
<!-- <NInputNumber v-model:value="item.Score" :min="0" :max="100" :show-button="false"
|
||||||
|
class="flex-1 text-center text-3xl font-bold !border-none" placeholder="0" /> -->
|
||||||
|
<NInputNumber v-model:value="item.Score" button-placement="both" :min="0" :max="100" @update:value="(val) => handleManualScoreUpdate(item, val || 0)">
|
||||||
|
<template #suffix>
|
||||||
|
分
|
||||||
|
</template>
|
||||||
|
</NInputNumber>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Score Buttons -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="text-sm text-gray-600 font-medium">
|
||||||
|
快捷打分
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<NButton
|
||||||
|
v-for="score in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="score" secondary strong
|
||||||
|
:type="item.Score === score ? 'primary' : 'default'" class="h-10"
|
||||||
|
@click="() => handleManualScoreUpdate(item, score)"
|
||||||
|
>
|
||||||
|
{{ score }} 分
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -131,7 +131,7 @@ async function handleSave() {
|
|||||||
|
|
||||||
const params: Api.Template.AddTemplateParams = {
|
const params: Api.Template.AddTemplateParams = {
|
||||||
id,
|
id,
|
||||||
pageNo: '1761.172.8.24',
|
pageNo: templateInfo.value.pageNo || '1761.172.8.25',
|
||||||
name: templateInfo.value.name,
|
name: templateInfo.value.name,
|
||||||
backGroundUrl: templateInfo.value.backGroundUrl || '',
|
backGroundUrl: templateInfo.value.backGroundUrl || '',
|
||||||
width: Math.round(templateInfo.value.width),
|
width: Math.round(templateInfo.value.width),
|
||||||
@ -145,6 +145,10 @@ async function handleSave() {
|
|||||||
message.error('请输入模板名称')
|
message.error('请输入模板名称')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!params.pageNo) {
|
||||||
|
message.error('请输入页码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await fetchAddTemplate(params)
|
const { error } = await fetchAddTemplate(params)
|
||||||
|
|
||||||
@ -169,11 +173,12 @@ async function initData() {
|
|||||||
if (!error && response) {
|
if (!error && response) {
|
||||||
const { data, success } = response
|
const { data, success } = response
|
||||||
if (success) {
|
if (success) {
|
||||||
const { Name, Width, Height, BackGroundUrl, TempContent } = data || {}
|
const { Name, Width, Height, BackGroundUrl, TempContent, PageNo } = data || {}
|
||||||
templateInfo.value = {
|
templateInfo.value = {
|
||||||
name: Name,
|
name: Name,
|
||||||
width: Width,
|
width: Width,
|
||||||
height: Height,
|
height: Height,
|
||||||
|
pageNo: PageNo,
|
||||||
backGroundUrl: BackGroundUrl,
|
backGroundUrl: BackGroundUrl,
|
||||||
}
|
}
|
||||||
imgSrc.value = BackGroundUrl || ''
|
imgSrc.value = BackGroundUrl || ''
|
||||||
|
|||||||
@ -59,6 +59,9 @@ function handleSelect(region: Region, e: MouseEvent) {
|
|||||||
<NFormItem label="模版名称">
|
<NFormItem label="模版名称">
|
||||||
<NInput v-model:value="localTemplateInfo.name" />
|
<NInput v-model:value="localTemplateInfo.name" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<NFormItem label="页码(PageNo)">
|
||||||
|
<NInput v-model:value="localTemplateInfo.pageNo" placeholder="请输入页码" />
|
||||||
|
</NFormItem>
|
||||||
<NGrid :cols="2" :x-gap="12">
|
<NGrid :cols="2" :x-gap="12">
|
||||||
<NGridItem>
|
<NGridItem>
|
||||||
<NFormItem label="画布宽 (px)">
|
<NFormItem label="画布宽 (px)">
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface TemplateInfo {
|
|||||||
name: string
|
name: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
pageNo?: string
|
||||||
backGroundUrl?: string
|
backGroundUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,16 +79,17 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full flex p-6">
|
<div class="w-full flex p-6">
|
||||||
<!-- template:{{ template }} -->
|
<!-- <p>template:{{ template }}</p> -->
|
||||||
<!-- 模板A: 汉字听写-提示 (拼音+提示) -->
|
<!-- 模板A: 汉字听写1-提示 (拼音+提示) -->
|
||||||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation" class="text-center">
|
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation1" class="text-center">
|
||||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||||
<div class="ptions-container1 mb-2 text-center text-2xl">
|
<div class="ptions-container1 mb-2 text-center text-2xl">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="rich-content ml-3 flex gap-1">
|
<div class="rich-content ml-3 mt-4 flex gap-1 color-red-600">
|
||||||
<TianZiGe v-for="(char, index) in pinyinChars" :key="index" :char="char" />
|
<!-- <TianZiGe v-for="(char, index) in pinyinChars" :key="index" :char="char" /> -->
|
||||||
</div> -->
|
{{ content }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -126,7 +127,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模板E: 成语-文字要求 -->
|
<!-- 模板E: 成语-文字要求 -->
|
||||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting" class="text-center">
|
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting1" class="text-center">
|
||||||
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
||||||
<!-- {{ content }} -->
|
<!-- {{ content }} -->
|
||||||
<div class="rich-content" v-html="content" />
|
<div class="rich-content" v-html="content" />
|
||||||
|
|||||||
Reference in New Issue
Block a user