feat: 新增题目导入服务并优化用户端答题流程
- 新增 question-importer 服务,支持从 JSON 文件批量导入题目到题库 - 重构用户端答题流程,整合抽题、答题、结果分析页面状态管理 - 将题目分类从 UI 模板类型切换为业务分类(question_category_name) - 在题库管理页面支持题目批量选择和删除功能 - 优化用户端组别选择,增加“已结束”状态提示和禁用逻辑 - 修复题目新增/更新 API 请求参数格式问题 - 为富文本编辑器配置排除不必要的工具栏按键
This commit is contained in:
@ -30,7 +30,8 @@ const props = defineProps<{
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
const { options: timeOptions } = useDict('time_out')
|
||||
const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
// const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
const { options: uiTypeOptions } = useDict('question_category_name', 'string')
|
||||
|
||||
const {
|
||||
// getRoundScore,
|
||||
|
||||
@ -29,7 +29,7 @@ const modelValue = ref<{
|
||||
ActitvityQuestionName: string
|
||||
QuestionRule: number
|
||||
Point: number
|
||||
UIType?: Api.Competition.QuestionTemplateId
|
||||
UIType?: Api.Competition.QuestionCategoryName
|
||||
TemplateID?: number
|
||||
// 额外字段用于回显时保留原始信息
|
||||
ID?: number
|
||||
@ -42,7 +42,8 @@ const modelValue = ref<{
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
const { options: timeOptions } = useDict('time_out')
|
||||
const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
// const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
const { options: uiTypeOptions } = useDict('question_category_name', 'string')
|
||||
|
||||
// 我们构造一个包含 modelValue 属性的对象,类似于 props
|
||||
const hookProps = reactive({
|
||||
|
||||
@ -21,6 +21,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { options: questionTypeOptions } = useDict('question_type', 'string')
|
||||
const { options: categoryOptions } = useDict('question_category_name', 'string')
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
@ -237,7 +238,12 @@ onMounted(() => {
|
||||
<NForm>
|
||||
<!-- 分类显示名称 -->
|
||||
<NFormItem label="分类名称">
|
||||
<NInput v-model:value="categoryForm.name" placeholder="请输入分类名称" @keyup.enter="submitCategory" />
|
||||
<NSelect
|
||||
v-model:value="categoryForm.name"
|
||||
:options="categoryOptions"
|
||||
value-field="label"
|
||||
placeholder="请选择分类名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
<!-- 题目类型 -->
|
||||
<NFormItem label="题目类型">
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
NBreadcrumbItem,
|
||||
NButton,
|
||||
NCard,
|
||||
NCheckbox,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NEmpty,
|
||||
@ -26,6 +27,13 @@ import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||
import { QuestionType } from '@/enum/business'
|
||||
import { fetchAddQuestionLibrary, fetchDeleteQuestionLibrary, fetchGetQuestionLibraryListAll, fetchUpdateQuestionLibrary } from '@/service/api/question'
|
||||
|
||||
/**
|
||||
* 为了user端题目展示的效果以及布局可控,指定规则:
|
||||
* 1.普通类型 =》用存文字存 ['汉字加一加','词语听写','汉字听写','成语写一写']
|
||||
* 2.复杂样式 =》用富文本 ['诗词理解']
|
||||
*
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
currentCategory: any
|
||||
}>()
|
||||
@ -112,6 +120,64 @@ const fileList = ref<UploadFileInfo[]>([])
|
||||
|
||||
const questionFormRef = ref<FormInst | null>(null)
|
||||
|
||||
// 批量选择
|
||||
const selectedQuestionIds = ref<number[]>([])
|
||||
|
||||
// 全选状态
|
||||
const isAllSelected = computed(() => {
|
||||
return questionList.value.length > 0 && selectedQuestionIds.value.length === questionList.value.length
|
||||
})
|
||||
|
||||
// 半选状态
|
||||
const isIndeterminate = computed(() => {
|
||||
return selectedQuestionIds.value.length > 0 && selectedQuestionIds.value.length < questionList.value.length
|
||||
})
|
||||
|
||||
function handleSelectAll(checked: boolean) {
|
||||
if (checked) {
|
||||
selectedQuestionIds.value = questionList.value.map(q => q.Id)
|
||||
}
|
||||
else {
|
||||
selectedQuestionIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(id: number, checked: boolean) {
|
||||
if (checked) {
|
||||
selectedQuestionIds.value.push(id)
|
||||
}
|
||||
else {
|
||||
selectedQuestionIds.value = selectedQuestionIds.value.filter(v => v !== id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
if (selectedQuestionIds.value.length === 0) {
|
||||
window?.$message?.warning('请先选择要删除的题目')
|
||||
return
|
||||
}
|
||||
|
||||
window?.$dialog?.warning({
|
||||
title: '批量删除警告',
|
||||
content: `确定要删除选中的 ${selectedQuestionIds.value.length} 道题目吗?此操作无法撤销。`,
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const { error } = await fetchDeleteQuestionLibrary(selectedQuestionIds.value)
|
||||
if (!error) {
|
||||
window?.$message?.success('批量删除成功')
|
||||
selectedQuestionIds.value = []
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
}
|
||||
else {
|
||||
window?.$message?.error('批量删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入题目正文内容', trigger: ['blur'] }],
|
||||
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
|
||||
@ -320,6 +386,13 @@ async function submitQuestion() {
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<NCheckbox
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@update:checked="handleSelectAll"
|
||||
>
|
||||
全选 ({{ selectedQuestionIds.length }})
|
||||
</NCheckbox>
|
||||
<NInput v-model:value="searchText" placeholder="输入题目关键字在当前分类下搜索..." class="!w-80" @keyup.enter="handleSearch">
|
||||
<template #prefix>
|
||||
<SvgIcon icon="carbon:search" class="text-gray-400" />
|
||||
@ -327,12 +400,20 @@ async function submitQuestion() {
|
||||
</NInput>
|
||||
</div>
|
||||
|
||||
<NButton type="primary" @click="handleAddQuestion">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:add" />
|
||||
</template>
|
||||
新增题目
|
||||
</NButton>
|
||||
<div class="flex gap-3">
|
||||
<NButton v-if="selectedQuestionIds.length > 0" type="error" secondary @click="handleBatchDelete">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:trash-can" />
|
||||
</template>
|
||||
批量删除 ({{ selectedQuestionIds.length }})
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleAddQuestion">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:add" />
|
||||
</template>
|
||||
新增题目
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -357,9 +438,18 @@ async function submitQuestion() {
|
||||
<!-- 题目列表 -->
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<NCard v-for="q in questionList" :key="q.Id" size="small" hoverable class="rounded-xl">
|
||||
<NCard
|
||||
v-for="q in questionList" :key="q.Id" size="small" hoverable class="rounded-xl transition-all" :class="{
|
||||
'ring-2 ring-primary-500 bg-primary-50': selectedQuestionIds.includes(q.Id),
|
||||
}"
|
||||
@click="handleSelect(q.Id, !selectedQuestionIds.includes(q.Id))"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2" @click.stop>
|
||||
<NCheckbox
|
||||
:checked="selectedQuestionIds.includes(q.Id)"
|
||||
@update:checked="(val) => handleSelect(q.Id, val)"
|
||||
/>
|
||||
<NTag size="small" type="primary" :bordered="false">
|
||||
ID: {{ q.Id }}
|
||||
</NTag>
|
||||
@ -391,7 +481,7 @@ async function submitQuestion() {
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<template #action>
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="flex justify-end gap-2" @click.stop>
|
||||
<NButton size="tiny" quaternary type="primary" @click="handleEditQuestion(q.Id)">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:edit" />
|
||||
@ -443,7 +533,10 @@ async function submitQuestion() {
|
||||
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
||||
<NFormItem label="题目正文内容" path="name">
|
||||
<div class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
||||
<WangEditor v-model="questionForm.name" placeholder="请输入题目内容..." height="300px" />
|
||||
<WangEditor
|
||||
v-model="questionForm.name" placeholder="请输入题目内容..." height="300px"
|
||||
:exclude-keys="['header1', 'fontSize', 'fontFamily', 'lineHeight', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'group-image', 'group-video', 'insertTable', 'headerSelect']"
|
||||
/>
|
||||
</div>
|
||||
</NFormItem>
|
||||
|
||||
@ -504,7 +597,7 @@ async function submitQuestion() {
|
||||
<style lang="scss" scoped>
|
||||
:deep(.n-card__content) {
|
||||
img {
|
||||
height: 100px !important ;
|
||||
height: 100px !important;
|
||||
width: 100px !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { fetchUpdateCurrentQuestionUse } from '@/service/api/result'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import GroupTabs, { type GroupItem } from './modules/GroupTabs.vue'
|
||||
import TeamResultCard, { type AICharacter, type TeamResult } from './modules/TeamResultCard.vue'
|
||||
|
||||
const activityInfoStore = useActivityInfoStore()
|
||||
const { activityId, groupId } = storeToRefs(activityInfoStore)
|
||||
|
||||
// --- 模拟数据和状态 ---
|
||||
|
||||
const groups = ref<GroupItem[]>(Array.from({ length: 8 }).map((_, i) => ({
|
||||
@ -17,9 +23,9 @@ const teamsData = ref<TeamResult[]>([]) // 当前组别下的队伍数据
|
||||
const isPolling = ref(true) // 是否正在轮询
|
||||
|
||||
// 初始化某组的队伍结构
|
||||
function initGroupTeams(groupId: number | string) {
|
||||
function initGroupTeams(gid: number | string) {
|
||||
return Array.from({ length: 4 }).map((_, i) => ({
|
||||
id: `${groupId}-${i + 1}`,
|
||||
id: `${gid}-${i + 1}`,
|
||||
name: `第 ${i + 1} 队`,
|
||||
correctCount: 0,
|
||||
score: 0,
|
||||
@ -167,9 +173,30 @@ function handleNext() {
|
||||
}
|
||||
|
||||
function handlePublish() {
|
||||
fetchGetQuestionDetailData()
|
||||
window.$message?.success('结果已发布')
|
||||
}
|
||||
|
||||
async function fetchGetQuestionDetailData() {
|
||||
const params = {
|
||||
ActivityID: Number(activityId.value),
|
||||
GroupID: Number(groupId.value),
|
||||
RoundType: 0 as Api.Competition.CompetitionRoundType,
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: outlineData, error } = await fetchUpdateCurrentQuestionUse({ ...params, isUse: true })
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(outlineData, 'outlineData')
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
window?.$message?.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
pause()
|
||||
@ -185,11 +212,7 @@ onUnmounted(() => {
|
||||
<div class="i-carbon-chart-line-data text-2xl text-blue-600" />
|
||||
实时结果
|
||||
</h1>
|
||||
<GroupTabs
|
||||
:groups="groups"
|
||||
:model-value="currentGroupId"
|
||||
@update:model-value="handleGroupChange"
|
||||
/>
|
||||
<GroupTabs :groups="groups" :model-value="currentGroupId" @update:model-value="handleGroupChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
@ -209,9 +232,7 @@ onUnmounted(() => {
|
||||
<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"
|
||||
v-for="team in teamsData" :key="team.id" :data="team"
|
||||
@toggle-correct="(charId) => handleToggleCorrect(team.id, charId)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
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 { useCompetitionStore } from '@/store/modules/competition'
|
||||
|
||||
const store = useCompetitionStore()
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
const route = useRoute()
|
||||
const activityId = computed(() => route.query?.activityId as string)
|
||||
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 TeamGroup_QuestionID = computed(() => currentQuestionMainInfo.value?.TeamGroup_QuestionID || 0)
|
||||
|
||||
const zoomedImage = ref<string | null>(null)
|
||||
const isCompleted = ref(false)
|
||||
const showNextBtn = ref(false)
|
||||
|
||||
// Mock team data
|
||||
const teams = ref([
|
||||
@ -51,25 +67,105 @@ function handleZoom(img: string) {
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
routerPushByKey('user_game')
|
||||
routerPushByKey('user_game', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
groupsId: groupsId.value,
|
||||
teamId: teamId.value,
|
||||
teamIds: teamIds.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
routerPushByKey('user_draw')
|
||||
if (isCompleted.value) {
|
||||
routerPushByKey('user_groups', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
routerPushByKey('user_draw', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
groupsId: groupsId.value,
|
||||
teamId: teamId.value,
|
||||
teamIds: teamIds.value,
|
||||
},
|
||||
})
|
||||
// 清空当前题目信息
|
||||
store.initData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目详情
|
||||
* 检测是否还有题可以抽取,如果data为0,则提示没有题可抽取
|
||||
*/
|
||||
async function initQuestions() {
|
||||
if (!activityId.value)
|
||||
return
|
||||
|
||||
try {
|
||||
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
||||
ActivityID: Number(activityId.value),
|
||||
GroupID: Number(groupsId.value),
|
||||
RoundType: 0,
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(outlineData, 'outlineData')
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
}
|
||||
isCompleted.value = outlineData?.data?.IsCompleted || false
|
||||
}
|
||||
catch (error) {
|
||||
window.$message?.error('获取题目失败,请重试')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const timer = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||||
|
||||
/** 定时刷新 检查评委是否已经完成打分,是否可以下一步 fetchCheckNextStep */
|
||||
async function checkNextStep() {
|
||||
if (!activityId.value || !groupsId.value)
|
||||
return
|
||||
|
||||
try {
|
||||
const { data: nextStepData, error } = await fetchCheckNextStep(TeamGroup_QuestionID.value || 0)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(nextStepData, 'nextStepData')
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
}
|
||||
const isUse = nextStepData?.data?.IsUse || false
|
||||
showNextBtn.value = isUse
|
||||
if (isUse) {
|
||||
await initQuestions()
|
||||
clearInterval(timer.value)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
window.$message?.error('检查是否可以下一步失败,请重试')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 定时刷新 检查是否可以下一步
|
||||
timer.value = setInterval(checkNextStep, 2000)
|
||||
|
||||
/** 销毁 定时刷新 检查是否可以下一步 */
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timer.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
:show-title="true"
|
||||
:show-back="false"
|
||||
:show-next="true"
|
||||
title="答题图解"
|
||||
action-position="top"
|
||||
back-btn-text="返回"
|
||||
next-btn-text="继续抽题"
|
||||
btn-theme="light"
|
||||
@back="handleBack"
|
||||
:show-title="true" :show-back="false" :show-next="showNextBtn" title="答题图解" action-position="top"
|
||||
back-btn-text="返回" :next-btn-text="isCompleted ? '该组已完成,回到选组' : '下一题'" btn-theme="light" @back="handleBack"
|
||||
@next="handleNext"
|
||||
>
|
||||
<div class="relative h-full w-full flex flex-col items-center px-12 pt-10 font-sans">
|
||||
@ -115,8 +211,7 @@ function handleNext() {
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="team.manualScore"
|
||||
type="number"
|
||||
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="留个输入框,修改正确积分"
|
||||
>
|
||||
@ -128,8 +223,7 @@ function handleNext() {
|
||||
|
||||
<!-- Zoom Modal -->
|
||||
<div
|
||||
v-if="zoomedImage"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
v-if="zoomedImage" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
@click="zoomedImage = null"
|
||||
>
|
||||
<div class="relative max-h-[90vh] max-w-[90vw] p-4">
|
||||
|
||||
@ -3,7 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetQuestionList } from '@/service/api/competition'
|
||||
import { fetchGetQuestionTableByActivityID } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
import { filterRepeat } from '@/utils/data'
|
||||
@ -39,6 +39,7 @@ const route = useRoute()
|
||||
const activityId = computed(() => activityInfoStore.activityId || Number(route.query?.activityId))
|
||||
const groupsId = computed(() => Number(route.query?.groupsId)) || activityInfoStore.groupId
|
||||
const teamId = computed(() => activityInfoStore.teamId || Number(route.query?.teamId))
|
||||
const teamIds = computed(() => route.query?.teamIds as string)
|
||||
|
||||
async function getQuestions() {
|
||||
if (!activityId.value || !groupsId.value || !teamId.value) {
|
||||
@ -46,26 +47,22 @@ async function getQuestions() {
|
||||
return
|
||||
}
|
||||
|
||||
// 存 activityId,groupsId,teamId
|
||||
activityInfoStore.setActivityId(activityId.value)
|
||||
activityInfoStore.setGroupId(groupsId.value)
|
||||
activityInfoStore.setTeamId(teamId.value)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { data, error } = await fetchGetQuestionList(String(activityId.value))
|
||||
const { data, error } = await fetchGetQuestionTableByActivityID(String(activityId.value))
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
return
|
||||
}
|
||||
|
||||
const list = data?.data || []
|
||||
|
||||
if (list && Array.isArray(list)) {
|
||||
// 根据ActitvityQuestionName去重
|
||||
const uniqueData = filterRepeat(list as any[], 'ActitvityQuestionName')
|
||||
const uniqueData = filterRepeat(list as any[], 'Name')
|
||||
questions.value = uniqueData.map((item: any) => ({
|
||||
id: item.Id,
|
||||
moduleName: item.ActitvityQuestionName,
|
||||
moduleName: item.Name,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -95,7 +92,14 @@ function handleBack() {
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
routerPushByKey('user_rules', { query: { activityId: activityId.value, groupsId: groupsId.value, teamId: teamId.value } })
|
||||
routerPushByKey('user_rules', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
groupsId: groupsId.value,
|
||||
teamId: teamId.value,
|
||||
teamIds: teamIds.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function toggleFlip(index: number) {
|
||||
@ -108,8 +112,23 @@ function toggleFlip(index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getQuestions()
|
||||
async function initSyncRouteParams() {
|
||||
if (route.query.activityId)
|
||||
activityInfoStore.setActivityId(Number(route.query.activityId))
|
||||
if (route.query.groupsId)
|
||||
activityInfoStore.setGroupId(Number(route.query.groupsId))
|
||||
if (route.query.teamId)
|
||||
activityInfoStore.setTeamId(Number(route.query.teamId))
|
||||
if (route.query.teamIds) {
|
||||
const ids = String(route.query.teamIds).split(',').map(Number).filter(id => !Number.isNaN(id))
|
||||
if (ids.length > 0)
|
||||
activityInfoStore.setTeamIds(ids)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initSyncRouteParams()
|
||||
await getQuestions()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -118,13 +137,13 @@ onMounted(() => {
|
||||
:show-back="true" :show-next="showNextButton" :title="activityName" @back="handleBack"
|
||||
@next="handleNext"
|
||||
>
|
||||
<div class="h-screen w-full overflow-hidden font-sans">
|
||||
<div class="relative z-10 h-full w-full">
|
||||
<div class="h-full w-full flex flex-col items-center pt-24">
|
||||
<div class="h-screen w-full overflow-y-auto font-sans">
|
||||
<div class="relative z-10 min-h-full w-full">
|
||||
<div class="min-h-full w-full flex flex-col items-center pb-12 pt-24">
|
||||
<!-- 卡片列表 -->
|
||||
<div class="perspective-container grid grid-cols-4 max-w-7xl w-full gap-10 px-12">
|
||||
<div class="perspective-container max-w-[1800px] w-full flex flex-wrap justify-center gap-4 px-4 md:gap-10 md:px-12">
|
||||
<div
|
||||
v-for="(node, index) in questions.slice(0, 4)" :key="node.id" class="card-wrapper"
|
||||
v-for="(node, index) in questions" :key="node.id" class="w-[44%] card-wrapper 2xl:w-[14%] lg:w-[21%] md:w-[28%] xl:w-[17%]"
|
||||
@click="toggleFlip(index)"
|
||||
>
|
||||
<div class="flip-card-inner" :class="{ 'is-flipped': flippedCards.has(index) }">
|
||||
|
||||
@ -3,7 +3,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetQuestionOutline } from '@/service/api/game'
|
||||
import { fetchGetCurrentQuestion } from '@/service/api/game'
|
||||
import { useActivityInfoStore } from '@/store/modules/activityinfo'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
import { AudioController } from '@/utils/audio'
|
||||
@ -14,40 +14,43 @@ const { routerPushByKey, routerBack } = useRouterPush()
|
||||
const route = useRoute()
|
||||
const activityInfoStore = useActivityInfoStore()
|
||||
const activityId = computed(() => activityInfoStore.activityId || route.query?.activityId as string)
|
||||
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
|
||||
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
|
||||
const teamIds = computed(() => route.query?.teamIds as string)
|
||||
|
||||
const store = useCompetitionStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const question = ref<Api.Competition.QuestionListRecord | null>(null)
|
||||
const question = ref<Api.Competition.CurrentQuestionResponse | null>(null)
|
||||
const flippedId = ref<number | null>(null)
|
||||
const audioController = new AudioController()
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const pageDescription = computed(() => {
|
||||
if (question.value) {
|
||||
return question.value.QuestionSubTitle || ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
async function initQuestions() {
|
||||
if (!activityId.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: outlineData, error } = await fetchGetQuestionOutline(Number(activityId.value), 0)
|
||||
const { data: outlineData, error } = await fetchGetCurrentQuestion({
|
||||
ActivityID: Number(activityId.value),
|
||||
GroupID: Number(groupsId.value),
|
||||
RoundType: 0,
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(outlineData, 'outlineData')
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
return
|
||||
}
|
||||
|
||||
// 直接使用返回的对象数据,不进行数组转换
|
||||
if (outlineData && outlineData.data) {
|
||||
question.value = outlineData.data as unknown as Api.Competition.QuestionListRecord
|
||||
store.setQuestionList([question.value]) // Store expects array, so wrap it
|
||||
// const object = {
|
||||
// ...outlineData.data?.Activity_Question || {},
|
||||
// ...outlineData.data?.QuestionList || {},
|
||||
// TeamGroup_QuestionID: outlineData.data?.TeamGroup_QuestionID || 0,
|
||||
// }
|
||||
question.value = outlineData.data as unknown as Api.Competition.CurrentQuestionResponse
|
||||
store.setCurrentQuestionMainInfo(question.value)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@ -63,7 +66,20 @@ onMounted(() => {
|
||||
initQuestions()
|
||||
})
|
||||
|
||||
async function handleCardClick(item: Api.Competition.QuestionListRecord) {
|
||||
async function handleCardClick(item: Api.Competition.CurrentQuestionResponse) {
|
||||
const { ID } = item?.Activity_Question || {}
|
||||
|
||||
if (!ID) {
|
||||
window.$message?.error('题目ID不存在')
|
||||
// 回到选组页面
|
||||
routerPushByKey('user_groups', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (flippedId.value)
|
||||
return // 防止重复点击
|
||||
|
||||
@ -75,15 +91,21 @@ async function handleCardClick(item: Api.Competition.QuestionListRecord) {
|
||||
catch (e) {
|
||||
console.error('Audio play failed', e)
|
||||
}
|
||||
|
||||
// 2. 触发翻转动画
|
||||
flippedId.value = item.ID
|
||||
flippedId.value = ID
|
||||
|
||||
// 3. 延迟1s后跳转
|
||||
setTimeout(() => {
|
||||
try {
|
||||
store.setCurrentQuestionInfo(item)
|
||||
routerPushByKey('user_game')
|
||||
store.setCurrentQuestionInfo(item.Activity_Question || {})
|
||||
routerPushByKey('user_game', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
groupsId: groupsId.value,
|
||||
teamId: teamId.value,
|
||||
teamIds: teamIds.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Navigation failed', error)
|
||||
@ -101,16 +123,21 @@ async function handleCardClick(item: Api.Competition.QuestionListRecord) {
|
||||
function handleBack() {
|
||||
routerBack()
|
||||
}
|
||||
|
||||
const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompetitionLayout :show-back="true" :show-next="false" :title="question?.ActitvityQuestionName || '抽题'" @back="handleBack">
|
||||
<CompetitionLayout
|
||||
:show-back="true" :show-next="false" :title="questionMainTitle || '抽题'"
|
||||
@back="handleBack"
|
||||
>
|
||||
<div class="h-full w-full flex flex-col items-center pt-10 font-sans">
|
||||
<div v-if="question" class="mb-10 max-w-4xl text-center">
|
||||
<!-- <div v-if="question" class="mb-10 max-w-4xl text-center">
|
||||
<div class="text-2xl text-gray-800 font-medium leading-relaxed tracking-wide">
|
||||
{{ question?.ActitvityQuestionName || '' }}
|
||||
{{ questionMainTitle || '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 题目卡片列表 -->
|
||||
<div v-if="loading" class="text-xl text-gray-500">
|
||||
@ -119,20 +146,22 @@ function handleBack() {
|
||||
<div v-else class="w-full flex flex-1 items-center justify-center pb-10">
|
||||
<div v-if="question" class="lottery-stick-container">
|
||||
<div
|
||||
class="lottery-stick"
|
||||
:class="{ 'is-selected': flippedId === question.ID }"
|
||||
class="lottery-stick" :class="{ 'is-selected': flippedId === question?.Activity_Question?.ID }"
|
||||
@click="handleCardClick(question)"
|
||||
>
|
||||
<!-- 签身 (默认显示) -->
|
||||
<div class="stick-face stick-body">
|
||||
<div class="stick-top-mark" />
|
||||
<div class="writing-mode-vertical-rl h-full flex items-center justify-center text-xl text-yellow-900 font-bold tracking-widest opacity-80">
|
||||
<div
|
||||
class="writing-mode-vertical-rl h-full flex items-center justify-center text-xl text-yellow-900 font-bold tracking-widest opacity-80"
|
||||
>
|
||||
抽 题
|
||||
</div>
|
||||
</div>
|
||||
<!-- 签面 (翻转后显示 - 实际上是放大显示的) -->
|
||||
<div class="stick-face stick-content">
|
||||
<span class="writing-mode-vertical-rl text-2xl text-red-600 font-bold">{{ question.ActitvityQuestionName }}</span>
|
||||
<span class="writing-mode-vertical-rl text-2xl text-red-600 font-bold">{{ questionMainTitle
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -190,7 +219,8 @@ function handleBack() {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #ef4444; /* red-500 */
|
||||
background-color: #ef4444;
|
||||
/* red-500 */
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
|
||||
@ -13,9 +13,13 @@ const { routerPushByKey } = useRouterPush()
|
||||
|
||||
const store = useCompetitionStore()
|
||||
const activityInfoStore = useActivityInfoStore()
|
||||
const { activityId, groupId, activityInfo } = storeToRefs(activityInfoStore)
|
||||
|
||||
// 直接从 Store 解构所需状态,保持单一数据源
|
||||
const { activityId, groupId, activityInfo, teamId, teamIds } = storeToRefs(activityInfoStore)
|
||||
const currentQuestionInfo = computed(() => store.currentQuestionInfo)
|
||||
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||||
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 本地倒计时控制(为了在页面上显示“开始答题”还是倒计时)
|
||||
@ -24,6 +28,11 @@ const isStarted = ref(false)
|
||||
console.log(activityInfo.value, 'activityInfo.value')
|
||||
console.log(currentQuestionInfo.value, 'currentQuestionInfo.value')
|
||||
console.log(currentQuestionDetail.value, 'currentQuestionDetail.value')
|
||||
console.log(currentQuestionMainInfo.value, 'currentQuestionMainInfo.value')
|
||||
|
||||
const mainTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.Name || '')
|
||||
|
||||
// const subTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.QuestionContent || '')
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchGetQuestionDetailData()
|
||||
@ -33,7 +42,14 @@ onMounted(async () => {
|
||||
watch(() => store.timeLeft, (newVal) => {
|
||||
if (newVal === 0 && isStarted.value) {
|
||||
store.stopTimer()
|
||||
routerPushByKey('user_analysis')
|
||||
routerPushByKey('user_analysis', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
groupsId: String(groupId.value),
|
||||
teamId: String(teamId.value),
|
||||
teamIds: teamIds.value.join(','),
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -69,7 +85,14 @@ const chartData = [
|
||||
|
||||
function handleBack() {
|
||||
store.stopTimer()
|
||||
routerPushByKey('user_draw')
|
||||
routerPushByKey('user_draw', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
groupsId: String(groupId.value),
|
||||
teamId: String(teamId.value),
|
||||
teamIds: teamIds.value.join(','),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,12 +103,15 @@ async function fetchGetQuestionDetailData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const currentActivityId = String(activityId.value || '')
|
||||
const questionId = store.currentQuestionInfo.QuestionID || (store.currentQuestionInfo as any).Id
|
||||
const questionId = store.currentQuestionInfo?.QuestionID || (store.currentQuestionInfo as any).Id
|
||||
const { data: detailData } = await fetchGetQuestionDetail(currentActivityId, questionId)
|
||||
console.log(detailData, 'detailData')
|
||||
if (detailData && detailData.data) {
|
||||
store.setCurrentQuestionDetail(detailData.data)
|
||||
}
|
||||
else {
|
||||
window?.$message?.error('题目详情不存在,请联系管理员')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch question detail', error)
|
||||
@ -126,7 +152,14 @@ async function handleNext() {
|
||||
else {
|
||||
// 备用逻辑:如果已经在答题中(理论上不会触发,因为上面已经跳转),直接跳转
|
||||
store.stopTimer()
|
||||
routerPushByKey('user_analysis')
|
||||
routerPushByKey('user_analysis', {
|
||||
query: {
|
||||
activityId: String(activityId.value),
|
||||
groupsId: String(groupId.value),
|
||||
teamId: String(teamId.value),
|
||||
teamIds: teamIds.value.join(','),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,18 +180,19 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
action-position="top" :show-back="false" :show-next="true"
|
||||
:title="store.currentQuestionInfo?.ActitvityQuestionName || ''" back-btn-text="返回"
|
||||
:next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="false" btn-theme="light" @back="handleBack"
|
||||
@next="handleNext"
|
||||
action-position="top" :show-back="false" :show-next="true" :title="mainTitle || ''"
|
||||
back-btn-text="返回" :next-btn-text="isStarted ? formattedTime : '开始答题'" :next-disabled="isStarted" btn-theme="light"
|
||||
@back="handleBack" @next="handleNext"
|
||||
>
|
||||
<div class="relative h-full w-full flex flex-col items-center overflow-hidden px-12 font-sans">
|
||||
<!-- 题目说明 -->
|
||||
<!-- <p>{{ store.currentQuestionInfo.ActitvityQuestionName }}</p> -->
|
||||
<!-- <p class="mt-8px text-3xl">
|
||||
{{ subTitle }}
|
||||
</p> -->
|
||||
<!-- 题目内容区域 -->
|
||||
<div v-if="template && content" class="w-full flex flex-1 items-center justify-center overflow-hidden py-4">
|
||||
<div class="max-h-full max-w-full flex items-center justify-center">
|
||||
<QuestionRenderer :template="template" :content="content" class="origin-center scale-150 transform" />
|
||||
<div class="max-h-full max-w-full">
|
||||
<QuestionRenderer class="origin-center scale-150 transform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -3,20 +3,15 @@ import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetGroupList } from '@/service/api/competition'
|
||||
import { fetchGetGroupListByActivityID } from '@/service/api/game'
|
||||
|
||||
const { routerPushByKey, routerBack } = useRouterPush()
|
||||
const route = useRoute()
|
||||
|
||||
interface GroupItem {
|
||||
id: number
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const groups = ref<GroupItem[]>([])
|
||||
const groups = ref<Api.Competition.ActivityTeamGroup[]>([])
|
||||
const loading = ref(false)
|
||||
const activityId = route.query.activityId as string
|
||||
const isAllEnd = ref(false)
|
||||
|
||||
async function getGroups() {
|
||||
if (!activityId)
|
||||
@ -24,7 +19,7 @@ async function getGroups() {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { data, error } = await fetchGetGroupList(activityId)
|
||||
const { data, error } = await fetchGetGroupListByActivityID(activityId, 0)
|
||||
if (error) {
|
||||
window?.$message?.error(error.message)
|
||||
return
|
||||
@ -33,11 +28,14 @@ async function getGroups() {
|
||||
const list = data?.data || []
|
||||
if (list && Array.isArray(list)) {
|
||||
groups.value = list.map((item: any) => ({
|
||||
id: item.Id,
|
||||
name: item.Name,
|
||||
...item,
|
||||
// id: item.Id,
|
||||
// name: item.Name,
|
||||
icon: 'activity', // Default icon since API might not provide one
|
||||
}))
|
||||
}
|
||||
isAllEnd.value = list.every((item: Api.Competition.ActivityTeamGroup) => item.IsEnd)
|
||||
// console.log(groups.value, 'groups.value')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@ -51,12 +49,15 @@ function handleBack() {
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
routerPushByKey('user_teams')
|
||||
routerPushByKey('user')
|
||||
}
|
||||
|
||||
function selectGroup(_id: number) {
|
||||
function selectGroup(item: Api.Competition.ActivityTeamGroup) {
|
||||
if (item.IsEnd)
|
||||
return
|
||||
|
||||
// 选择组别逻辑
|
||||
routerPushByKey('user_teams', { query: { activityId, groupsId: _id } })
|
||||
routerPushByKey('user_teams', { query: { activityId, groupsId: item.Id } })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@ -70,7 +71,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<CompetitionLayout
|
||||
:show-back="true"
|
||||
:show-next="false"
|
||||
:show-next="isAllEnd"
|
||||
next-btn-text="查看队伍结果"
|
||||
title="展示组别"
|
||||
@back="handleBack"
|
||||
@next="handleNext"
|
||||
@ -81,10 +83,11 @@ onMounted(() => {
|
||||
<div
|
||||
v-for="(item, index) in groups"
|
||||
v-show="showContent"
|
||||
:key="item.id"
|
||||
:key="item.Id"
|
||||
class="group-item"
|
||||
:class="{ 'is-disabled': item.IsEnd }"
|
||||
:style="{ '--delay': `${index * 0.05}s` }"
|
||||
@click="selectGroup(item.id)"
|
||||
@click="selectGroup(item)"
|
||||
>
|
||||
<div class="icon-wrapper">
|
||||
<div class="flower-bg" />
|
||||
@ -94,7 +97,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="group-name-tag">
|
||||
<span class="sun-icon">☀️</span>
|
||||
{{ item.name }}
|
||||
{{ item.Name }}
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
@ -133,6 +136,16 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(100%);
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
@ -1,58 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { QuestionContent, UiTemplateType } from '../types'
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { QuestionCategoryEnum } from '@/enum/business'
|
||||
import { useCompetitionStore } from '@/store/modules/competition'
|
||||
|
||||
const props = defineProps<{
|
||||
template: UiTemplateType
|
||||
content: QuestionContent
|
||||
}>()
|
||||
// const props = defineProps<{
|
||||
// template: QuestionCategoryName
|
||||
// content: QuestionContent
|
||||
// }>()
|
||||
|
||||
const store = useCompetitionStore()
|
||||
const currentQuestionMainInfo = computed(() => store.currentQuestionMainInfo)
|
||||
const currentQuestionDetail = computed(() => store.currentQuestionDetail)
|
||||
// console.log(currentQuestionMainInfo.value, 'currentQuestionMainInfo')
|
||||
// 题目
|
||||
const title = computed(() => currentQuestionMainInfo.value?.QuestionList?.QuestionContent)
|
||||
// 归属大纲
|
||||
// const mainTitle = computed(() => currentQuestionMainInfo.value?.QuestionList?.Name)
|
||||
// 题目内容
|
||||
const content = computed(() => currentQuestionDetail.value.Name)
|
||||
// 模版类型
|
||||
const template = computed(() => currentQuestionMainInfo.value?.Activity_Question?.UIType)
|
||||
|
||||
/**
|
||||
* 组合后的标题(用于汉字加一加模板)
|
||||
* 将 content 插入到 title 的引号中,并拆分以支持不同样式
|
||||
*/
|
||||
const parsedComponentAddTitle = computed(() => {
|
||||
const t = title.value || ''
|
||||
const c = content.value || ''
|
||||
|
||||
// 尝试替换全角空引号
|
||||
if (t.includes('“”')) {
|
||||
const [prefix, suffix] = t.split('“”')
|
||||
return {
|
||||
type: 'quote_full',
|
||||
prefix,
|
||||
content: c,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
// 尝试替换半角空引号
|
||||
if (t.includes('""')) {
|
||||
const [prefix, suffix] = t.split('""')
|
||||
return {
|
||||
type: 'quote_half',
|
||||
prefix,
|
||||
content: c,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到空引号,直接追加
|
||||
return {
|
||||
type: 'normal',
|
||||
prefix: t,
|
||||
content: c,
|
||||
suffix: '',
|
||||
}
|
||||
})
|
||||
|
||||
/** 获取富文本里面的text */
|
||||
const pinyinChars = computed(() => {
|
||||
if (props.template === 'TEMPLATE_WORD_DICTATION' && props.content.title) {
|
||||
if (template.value === QuestionCategoryEnum.WordDictation && content.value) {
|
||||
// 创建临时元素提取HTML文本内容
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = props.content.title
|
||||
div.innerHTML = content.value
|
||||
const text = div.textContent || ''
|
||||
return text.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [currentQuestionMainInfo, currentQuestionDetail],
|
||||
() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Current currentQuestionMainInfo:', currentQuestionMainInfo.value)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Current currentQuestionMainInfo:', currentQuestionMainInfo)
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[400px] w-full flex items-center justify-center p-6">
|
||||
<div class="w-full flex p-6" style="border: 1px solid red;">
|
||||
<!-- template:{{ template }} -->
|
||||
<!-- 模板A: 汉字听写-提示 (拼音+提示) -->
|
||||
<div v-if="template === 'TEMPLATE_DICTATION_HINT'" class="text-center">
|
||||
<div
|
||||
class="mb-6 inline-block text-5xl font-bold leading-none"
|
||||
style="text-shadow: 2px 2px 4px rgba(0,0,0,0.1);"
|
||||
>
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
<!-- “{{ content.title }}” -->
|
||||
<div v-if="template === QuestionCategoryEnum.ChineseCharacterDictation" class="text-center">
|
||||
<div class="mb-6 inline-block text-5xl font-bold leading-none">
|
||||
<div class="ptions-container1 text-2xl">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="rich-content1">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="text-4xl text-gray-800 font-bold">
|
||||
{{ content.meta?.hint }}
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 模板B: 汉字听写-同音字 (大字) -->
|
||||
<div v-else-if="template === 'TEMPLATE_DICTATION_HOMOPHONE'" class="text-center">
|
||||
<div class="homophone text-[100px] text-red-600 font-bold">
|
||||
<!-- “{{ content.title }}” -->
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 暂时未匹配到枚举,保持原样或删除 -->
|
||||
<!-- <div v-else-if="template === 'TEMPLATE_DICTATION_HOMOPHONE'" class="text-center"> -->
|
||||
|
||||
<!-- 模板C: 汉字加一加 (提示+部件) -->
|
||||
<div v-else-if="template === 'TEMPLATE_COMPONENT_ADD'" class="text-center">
|
||||
<div class="text-[100px] text-red-600 font-bold">
|
||||
<!-- {{ content.title }} -->
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
<div v-else-if="template === QuestionCategoryEnum.CharacterRadicalAddition" class="text-center">
|
||||
<div class="text-[50px] font-bold">
|
||||
<template v-if="parsedComponentAddTitle.type === 'quote_full'">
|
||||
<span class="text-black">{{ parsedComponentAddTitle.prefix }}“</span>
|
||||
<span class="text-red-600">{{ parsedComponentAddTitle.content }}</span>
|
||||
<span class="text-black">”{{ parsedComponentAddTitle.suffix }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="parsedComponentAddTitle.type === 'quote_half'">
|
||||
<span class="text-black">{{ parsedComponentAddTitle.prefix }}"</span>
|
||||
<span class="text-red-600">{{ parsedComponentAddTitle.content }}</span>
|
||||
<span class="text-black">"{{ parsedComponentAddTitle.suffix }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span class="text-black">{{ parsedComponentAddTitle.prefix }}</span>
|
||||
<span class="ml-4 text-red-600">{{ parsedComponentAddTitle.content }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模板D: 词语听写 (拼音+提示) -->
|
||||
<div v-else-if="template === 'TEMPLATE_WORD_DICTATION'" class="flex flex-col items-center">
|
||||
<div v-else-if="template === QuestionCategoryEnum.WordDictation" class="flex flex-col items-center">
|
||||
<div class="mb-8 flex flex-wrap justify-center gap-6">
|
||||
<div
|
||||
v-for="(char, index) in pinyinChars" :key="index"
|
||||
@ -78,69 +151,30 @@ const pinyinChars = computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 模板E: 成语-文字要求 -->
|
||||
<div v-else-if="template === 'TEMPLATE_IDIOM_TEXT_REQ'" class="text-center">
|
||||
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting" class="text-center">
|
||||
<div class="mb-8 text-4xl text-gray-800 font-bold">
|
||||
<!-- {{ content.title }} -->
|
||||
<div class="rich-content" v-html="content.title" />
|
||||
<div class="rich-content" v-html="content" />
|
||||
</div>
|
||||
<!-- <div class="rounded bg-gray-100 px-4 py-2 text-xl text-gray-500">
|
||||
示例:{{ content.meta?.example }}
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 模板F: 成语-看图 (使用Div占位) -->
|
||||
<div v-else-if="template === 'TEMPLATE_IDIOM_IMAGE'" class="flex flex-col items-center text-center">
|
||||
<!-- 模板F: 诗词理解 -->
|
||||
<!-- <div v-else-if="template === 'TEMPLATE_IDIOM_IMAGE'" class="flex flex-col items-center text-center">
|
||||
<div class="mb-6 h-60 w-80 flex items-center justify-center">
|
||||
<!-- <span class="text-gray-400">图片展示区域<br></span> -->
|
||||
<img :src="content.titleImage" alt="idiom image" class="h-full w-full object-contain">
|
||||
</div>
|
||||
<!-- <div class="text-xl text-gray-600 font-bold">
|
||||
提示:{{ content.meta?.hint }}
|
||||
</div> -->
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 模板G: 诗词理解-选择题 (通用选择题模板) -->
|
||||
<div v-else-if="template === 'TEMPLATE_POETRY_MULTIPLE_CHOICE'" class="w-full flex flex-col items-center">
|
||||
<div class="mb-1 text-2xl text-gray-800 font-bold leading-relaxed">
|
||||
{{ content.title }}
|
||||
<div v-else-if="template === QuestionCategoryEnum.PoetryComprehension" class="w-full flex flex-col items-center">
|
||||
<div class="options-container text-2xl">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<div
|
||||
v-for="(option, index) in content.options" :key="index" class="option-card group" :class="{
|
||||
'is-mixed': !option.renderType || option.renderType === 'mixed' || option.renderType === 'image',
|
||||
'is-text': option.renderType === 'text',
|
||||
}"
|
||||
>
|
||||
<!-- 1. 图片渲染模式 -->
|
||||
<div v-if="option.image" class="option-image-wrapper">
|
||||
<img :src="option.image" :alt="option.label" class="option-image">
|
||||
</div>
|
||||
|
||||
<!-- 2. 田字格渲染模式 -->
|
||||
<div v-if="option.renderType === 'tianzige'" class="tianzige-wrapper">
|
||||
<!-- 米字格背景 -->
|
||||
<div class="tianzige-bg">
|
||||
<div class="line-horizontal" />
|
||||
<div class="line-vertical" />
|
||||
<svg width="100%" height="100%" class="line-diagonal">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="#f87171" stroke-width="1" stroke-dasharray="4 2" />
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="#f87171" stroke-width="1" stroke-dasharray="4 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="tianzige-text">{{ option.text }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 选项标签和文字 -->
|
||||
<div class="option-content">
|
||||
<div class="option-label">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<!-- 如果不是田字格模式,显示普通文本 -->
|
||||
<span v-if="option.renderType !== 'tianzige'" class="option-text">
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 text-3xl text-gray-800 font-bold leading-relaxed">
|
||||
<div class="rich-content" v-html="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ const route = useRoute()
|
||||
const activityId = computed(() => activityInfoStore.activityId || route.query?.activityId as string)
|
||||
const groupsId = computed(() => activityInfoStore.groupId || route.query?.groupsId as string)
|
||||
const teamId = computed(() => activityInfoStore.teamId || route.query?.teamId as string)
|
||||
const teamIds = computed(() => route.query?.teamIds as string)
|
||||
|
||||
const { routerPushByKey, routerBack } = useRouterPush()
|
||||
|
||||
@ -33,7 +34,14 @@ function handleBack() {
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
routerPushByKey('user_draw', { query: { activityId: activityId.value, groupsId: groupsId.value, teamId: teamId.value } })
|
||||
routerPushByKey('user_draw', {
|
||||
query: {
|
||||
activityId: activityId.value,
|
||||
groupsId: groupsId.value,
|
||||
teamId: teamId.value,
|
||||
teamIds: teamIds.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -16,8 +16,9 @@ interface TeamItem {
|
||||
|
||||
const teams = ref<TeamItem[]>([])
|
||||
const loading = ref(false)
|
||||
const activityId = route.query.activityId
|
||||
const groupsId = route.query.groupsId
|
||||
const activityId = (route.query?.activityId) as string || ''
|
||||
const groupsId = (route.query?.groupsId) as string || ''
|
||||
const teamIds = ref<number[]>([])
|
||||
|
||||
async function getTeams() {
|
||||
if (!groupsId)
|
||||
@ -37,6 +38,7 @@ async function getTeams() {
|
||||
name: item.Name,
|
||||
icon: 'star', // Default icon
|
||||
}))
|
||||
teamIds.value = list.map((item: any) => item.Id)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
@ -57,7 +59,7 @@ function handleNext() {
|
||||
|
||||
function selectTeam(_id: number) {
|
||||
// 选择队伍逻辑
|
||||
routerPushByKey('user_cover', { query: { activityId, groupsId, teamId: _id } })
|
||||
routerPushByKey('user_cover', { query: { activityId, groupsId, teamId: _id, teamIds: teamIds.value.join(',') } })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 废弃
|
||||
*/
|
||||
export type UiTemplateType =
|
||||
| 'TEMPLATE_DICTATION_HINT' // 汉字听写-提示 (jiu)
|
||||
| 'TEMPLATE_DICTATION_HOMOPHONE' // 汉字听写-同音 (tang)
|
||||
@ -7,6 +10,17 @@ export type UiTemplateType =
|
||||
| 'TEMPLATE_IDIOM_IMAGE' // 成语-看图
|
||||
| 'TEMPLATE_POETRY_MULTIPLE_CHOICE' // 诗词理解-选择题
|
||||
|
||||
/**
|
||||
* 题库类型
|
||||
*/
|
||||
export type QuestionCategoryName =
|
||||
| 'ChineseCharacterDictation' // 汉字听写-提示 (jiu)
|
||||
| 'CharacterRadicalAddition' // 汉字加一加 (车)
|
||||
| 'WordDictation' // 词语听写 (zhi re)
|
||||
| 'IdiomWriting' // 成语-文字要求 (反义字)
|
||||
// | 'TEMPLATE_IDIOM_IMAGE' // 成语-看图
|
||||
| 'PoetryComprehension' // 诗词理解-选择题
|
||||
|
||||
export interface QuestionOption {
|
||||
label: string // A, B, C
|
||||
text: string // 选项文本/值
|
||||
|
||||
Reference in New Issue
Block a user