feat(user): 重构用户端竞赛流程,集成后端API并优化交互体验

- 新增 `activityinfo` store 管理活动信息,支持持久化
- 重构 `competition` store,分离数据层与流程控制,新增音效管理
- 新增游戏API模块 (`game.ts`),实现抽题、题目详情、提交结果等接口
- 用户端页面全面对接后端数据:首页轮播展示活动列表,组别/队伍页动态加载,封面页显示题目卡片
- 抽题页改为竹签翻转动画,游戏页集成题目详情与倒计时
- 优化布局组件,支持点击返回首页,统一路由参数传递
- 更新类型定义,支持富文本题目渲染
- 添加 `activityinfo` 拼写检查词条
This commit is contained in:
2026-02-10 08:45:26 +08:00
parent 0fd8ef527a
commit 61a7412fc1
23 changed files with 962 additions and 643 deletions

View File

@ -9,7 +9,11 @@ const props = defineProps<{
const pinyinChars = computed(() => {
if (props.template === 'TEMPLATE_WORD_DICTATION' && props.content.title) {
return props.content.title.split(' ')
// 创建临时元素提取HTML文本内容
const div = document.createElement('div')
div.innerHTML = props.content.title
const text = div.textContent || ''
return text.trim().split(/\s+/).filter(Boolean)
}
return []
})
@ -19,8 +23,12 @@ const pinyinChars = computed(() => {
<div class="min-h-[400px] w-full flex items-center justify-center p-6">
<!-- 模板A: 汉字听写-提示 (拼音+提示) -->
<div v-if="template === 'TEMPLATE_DICTATION_HINT'" class="text-center">
<div class="mb-6 inline-block text-5xl text-red-500 font-bold leading-none" style="text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
{{ content.title }}
<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>
<!-- <div class="text-4xl text-gray-800 font-bold">
{{ content.meta?.hint }}
@ -29,15 +37,17 @@ const pinyinChars = computed(() => {
<!-- 模板B: 汉字听写-同音字 (大字) -->
<div v-else-if="template === 'TEMPLATE_DICTATION_HOMOPHONE'" class="text-center">
<div class="text-[100px] text-red-600 font-bold">
{{ content.title }}
<div class="homophone text-[100px] text-red-600 font-bold">
<!-- {{ content.title }} -->
<div class="rich-content" v-html="content.title" />
</div>
</div>
<!-- 模板C: 汉字加一加 (提示+部件) -->
<div v-else-if="template === 'TEMPLATE_COMPONENT_ADD'" class="text-center">
<div class="text-[100px] text-red-600 font-bold">
{{ content.title }}
<!-- {{ content.title }} -->
<div class="rich-content" v-html="content.title" />
</div>
</div>
@ -45,8 +55,7 @@ const pinyinChars = computed(() => {
<div v-else-if="template === 'TEMPLATE_WORD_DICTATION'" 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"
v-for="(char, index) in pinyinChars" :key="index"
class="relative h-32 w-32 flex select-none items-center justify-center border-2 border-red-500 bg-white"
>
<!-- 米字格背景 -->
@ -71,7 +80,8 @@ const pinyinChars = computed(() => {
<!-- 模板E: 成语-文字要求 -->
<div v-else-if="template === 'TEMPLATE_IDIOM_TEXT_REQ'" class="text-center">
<div class="mb-8 text-4xl text-gray-800 font-bold">
{{ content.title }}
<!-- {{ content.title }} -->
<div class="rich-content" v-html="content.title" />
</div>
<!-- <div class="rounded bg-gray-100 px-4 py-2 text-xl text-gray-500">
示例{{ content.meta?.example }}
@ -96,10 +106,7 @@ const pinyinChars = computed(() => {
</div>
<div class="options-container">
<div
v-for="(option, index) in content.options"
:key="index"
class="option-card group"
:class="{
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',
}"
@ -164,16 +171,21 @@ const pinyinChars = computed(() => {
align-items: center;
justify-content: center;
border: 2px solid transparent;
border-radius: 0.75rem; /* rounded-xl */
padding: 1rem; /* p-4 */
border-radius: 0.75rem;
/* rounded-xl */
padding: 1rem;
/* p-4 */
transition: all 0.3s;
&:hover {
border-color: #f87171; /* border-red-400 */
background-color: #fef2f2; /* bg-red-50 */
border-color: #f87171;
/* border-red-400 */
background-color: #fef2f2;
/* bg-red-50 */
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */
0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* shadow-lg */
}
&.is-mixed {
@ -182,7 +194,8 @@ const pinyinChars = computed(() => {
&.is-text {
flex-direction: row;
gap: 1rem; /* gap-4 */
gap: 1rem;
/* gap-4 */
}
}
@ -194,8 +207,10 @@ const pinyinChars = computed(() => {
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 0.5rem; /* rounded-lg */
padding: 0.5rem; /* p-2 */
border-radius: 0.5rem;
/* rounded-lg */
padding: 0.5rem;
/* p-2 */
}
.option-image {
@ -211,14 +226,18 @@ const pinyinChars = computed(() => {
.tianzige-wrapper {
position: relative;
margin-bottom: 0.75rem; /* mb-3 */
height: 8rem; /* h-32 */
width: 8rem; /* w-32 */
margin-bottom: 0.75rem;
/* mb-3 */
height: 8rem;
/* h-32 */
width: 8rem;
/* w-32 */
display: flex;
user-select: none;
align-items: center;
justify-content: center;
border: 2px solid #ef4444; /* border-red-500 */
border: 2px solid #ef4444;
/* border-red-500 */
background-color: white;
}
@ -236,7 +255,8 @@ const pinyinChars = computed(() => {
top: 50%;
height: 1px;
width: 100%;
border-top: 1px dashed #f87171; /* border-red-400 */
border-top: 1px dashed #f87171;
/* border-red-400 */
opacity: 0.6;
}
@ -246,7 +266,8 @@ const pinyinChars = computed(() => {
top: 0;
height: 100%;
width: 1px;
border-left: 1px dashed #f87171; /* border-red-400 */
border-left: 1px dashed #f87171;
/* border-red-400 */
opacity: 0.6;
}
@ -258,43 +279,73 @@ const pinyinChars = computed(() => {
}
.tianzige-text {
font-family: 'KaiTi', 'STKaiti', serif; /* font-kaaiti */
font-family: 'KaiTi', 'STKaiti', serif;
/* font-kaaiti */
z-index: 10;
font-size: 3.75rem; /* text-6xl */
font-size: 3.75rem;
/* text-6xl */
font-weight: 700;
color: #1f2937; /* text-gray-800 */
color: #1f2937;
/* text-gray-800 */
}
.option-content {
display: flex;
align-items: center;
gap: 0.75rem; /* gap-3 */
gap: 0.75rem;
/* gap-3 */
}
.option-label {
height: 2rem; /* h-8 */
width: 2rem; /* w-8 */
height: 2rem;
/* h-8 */
width: 2rem;
/* w-8 */
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px; /* rounded-full */
background-color: #fee2e2; /* bg-red-100 */
color: #dc2626; /* text-red-600 */
border-radius: 9999px;
/* rounded-full */
background-color: #fee2e2;
/* bg-red-100 */
color: #dc2626;
/* text-red-600 */
font-weight: 700;
.group:hover & {
background-color: #ef4444; /* bg-red-500 */
background-color: #ef4444;
/* bg-red-500 */
color: white;
}
}
.option-text {
font-size: 1.25rem; /* text-xl */
color: #374151; /* text-gray-700 */
font-size: 1.25rem;
/* text-xl */
color: #374151;
/* text-gray-700 */
font-weight: 500;
.group:hover & {
color: #b91c1c; /* text-red-700 */
color: #b91c1c;
/* text-red-700 */
}
}
.homophone {
:deep(img) {
height: 230px !important;
width: auto !important;
object-fit: contain;
}
}
.rich-content {
:deep(img) {
max-width: 100%;
height: auto;
object-fit: contain;
vertical-align: middle;
}
}
</style>