feat: 优化用户界面组件与调整布局样式

- 新增田字格组件用于汉字书写练习
- 调整模板详情页的生成参数和页面编号
- 优化用户主页轮播图和游戏页布局
- 修复答题结果页图片显示和状态切换问题
- 移除卡片悬停阴影并优化背景图片显示
- 更新状态管理注释和类型声明
This commit is contained in:
2026-03-03 08:35:28 +08:00
parent 81643c61ae
commit 94af0bec6a
16 changed files with 121 additions and 157 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 8.8 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 11 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 7.2 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 19 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.0 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
interface Props {
char?: string
width?: string | number
height?: string | number
fontSize?: string | number
showChar?: boolean
}
const _props = withDefaults(defineProps<Props>(), {
char: '',
width: '10.8rem', // 对应 w-42 (42 * 0.25rem = 10.5rem)
height: '9.5rem', // 对应 h-38 (38 * 0.25rem = 9.5rem)
// fontSize: '3.25rem', // 对应 text-4xl
showChar: true,
})
</script>
<template>
<div
class="relative flex select-none items-center justify-center border-2 border-red-500 bg-white"
:style="{ width: typeof width === 'number' ? `${width}px` : width, height: typeof height === 'number' ? `${height}px` : height }"
>
<!-- 田字格背景线 -->
<div class="pointer-events-none absolute inset-0 z-0 h-full w-full">
<!-- 横向虚线 -->
<div class="absolute left-0 top-1/2 h-[1px] w-full border-t border-red-400 border-dashed opacity-60" />
<!-- 纵向虚线 -->
<div class="absolute left-1/2 top-0 h-full w-[1px] border-l border-red-400 border-dashed opacity-60" />
<!-- 对角线 -->
<svg width="100%" height="100%" class="absolute inset-0 opacity-60">
<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
v-if="showChar"
class="z-10 text-6xl text-gray-800 font-bold font-sans"
>
{{ char }}
</span>
<slot />
</div>
</template>
<style scoped>
</style>

View File

@ -7,7 +7,7 @@ export const useCompetitionStore = defineStore('competition', () => {
// State - 核心数据
// ==========================================
/** 题目列表数据 */
/** 从抽题页获取当前题目基础提纲信息 */
const currentQuestionMainInfo = ref<Api.Competition.CurrentQuestionResponse>({} as Api.Competition.CurrentQuestionResponse)
/** 当前选中的题目基础信息 (从抽题页获取) */

View File

@ -173,6 +173,7 @@ declare module 'vue' {
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
TianZiGe: typeof import('./../components/custom/user/TianZiGe.vue')['default']
UpFileDialog: typeof import('./../components/common/rest-basic-editor/components/up-file-dialog.vue')['default']
WangEditor: typeof import('./../components/common/wang-editor.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
@ -342,6 +343,7 @@ declare global {
const TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
const TianZiGe: typeof import('./../components/custom/user/TianZiGe.vue')['default']
const UpFileDialog: typeof import('./../components/common/rest-basic-editor/components/up-file-dialog.vue')['default']
const WangEditor: typeof import('./../components/common/wang-editor.vue')['default']
const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']

View File

@ -105,9 +105,9 @@ function generateMockData() {
// 4. 词语听写 (Long)
// 5. 成语写一写 (Long)
const mockTypes = [
QuestionCategoryEnum.ChineseCharacterDictation,
QuestionCategoryEnum.CharacterRadicalAddition,
QuestionCategoryEnum.PoetryComprehension,
// QuestionCategoryEnum.ChineseCharacterDictation,
// QuestionCategoryEnum.CharacterRadicalAddition,
// QuestionCategoryEnum.PoetryComprehension,
QuestionCategoryEnum.WordDictation,
QuestionCategoryEnum.IdiomWriting,
]
@ -434,7 +434,7 @@ onMounted(async () => {
<!-- 学生答题原图 -->
<div class="UserAnswerPicture flex-shrink-0">
<NImage
v-if="team.UserAnswerPicture" :src="team.UserAnswerPicture" width="250" object-fit="contain"
v-if="team.UserAnswerPicture" :src="team.UserAnswerPicture" width="550" object-fit="contain"
class="block border border-gray-200 rounded-lg"
/>
<div
@ -451,7 +451,7 @@ onMounted(async () => {
<!-- Grid Layout -->
<div v-if="currentLayout === 'grid'" class="flex flex-wrap gap-4">
<div
v-for="(item, idx) 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)"
>
<!-- Image Container with Status Border -->
@ -460,9 +460,11 @@ onMounted(async () => {
item.Status === 1 ? 'border-green-500' : 'border-red-500',
]"
>
<!-- <NImage :src="item.AnswerValuePicture || item.imageUrl" preview-disabled object-fit="contain" class="h-24 w-24" /> -->
<div class="h-24 w-24 flex items-center justify-center">
{{ item.AnswerText }}
<div class="flex items-center justify-center">
<NImage
:src="item.AnswerValuePicture || item.imageUrl" preview-disabled object-fit="contain"
/>
<!-- {{ item.AnswerText }} -->
</div>
<!-- Status Icon Overlay -->
<div class="absolute z-10 rounded-full bg-white -right-3px -top-3px">
@ -473,7 +475,7 @@ onMounted(async () => {
<!-- Label if exists -->
<div class="mt-1 text-center text-sm text-gray-500">
{{ idx + 1 }}.
{{ item.AnswerText }}
</div>
</div>
</div>
@ -494,7 +496,6 @@ onMounted(async () => {
>
<NImage
:src="item.AnswerValuePicture || item.imageUrl" preview-disabled object-fit="contain"
height="50"
/>
<!-- Status Icon Overlay -->

View File

@ -28,9 +28,9 @@ const imgSrc = ref('')
const genSettings = ref<GenSettings>({
cols: 6,
rows: 1,
w: 296,
h: 309,
gapX: 5,
w: 137,
h: 133,
gapX: 20,
gapY: 0,
startX: 0,
startY: 0,
@ -131,7 +131,7 @@ async function handleSave() {
const params: Api.Template.AddTemplateParams = {
id,
pageNo: '1761.172.8.66',
pageNo: '1761.172.8.24',
name: templateInfo.value.name,
backGroundUrl: templateInfo.value.backGroundUrl || '',
width: Math.round(templateInfo.value.width),

View File

@ -239,6 +239,7 @@ onMounted(async () => {
aspect-ratio: 3/4;
cursor: pointer;
transition: transform 0.3s ease;
box-shadow: none !important;
&:hover {
transform: translateY(-10px); // [悬停效果] 鼠标移上去时卡片轻微上浮

View File

@ -25,6 +25,11 @@ const question = ref<Api.Competition.CurrentQuestionResponse | null>(null)
const flippedId = ref<number | null>(null)
const audioController = new AudioController()
/**
* 初始化题目
* 1. 从服务器获取当前题目
* 2. 存储题目信息到全局状态
*/
async function initQuestions() {
if (!activityId.value)
return
@ -42,14 +47,9 @@ async function initQuestions() {
window?.$message?.error(error.message)
return
}
// 直接使用返回的对象数据,不进行数组转换
if (outlineData && outlineData.data) {
// 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)
}
}
@ -66,6 +66,9 @@ onMounted(() => {
initQuestions()
})
/**
* 根据抽题页的大纲 id 随机获取的当前题目基础提纲信息下面的题目信息,跳转至游戏页面
*/
async function handleCardClick(item: Api.Competition.CurrentQuestionResponse) {
const { ID } = item?.Activity_Question || {}
@ -128,10 +131,7 @@ const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '
</script>
<template>
<CompetitionLayout
:show-back="true" :show-next="false" :title="questionMainTitle || '抽题'"
@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 class="text-2xl text-gray-800 font-medium leading-relaxed tracking-wide">

View File

@ -236,9 +236,9 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
{{ subTitle }}
</p> -->
<!-- 题目内容区域 -->
<div v-if="template && content" class="w-full flex flex-1 items-center justify-center overflow-hidden py-4">
<div v-if="template && content" class="w-full flex flex-1 items-start justify-center overflow-hidden py-4">
<div class="max-h-full max-w-full">
<QuestionRenderer class="origin-center scale-150 transform" />
<QuestionRenderer class="origin-center transform" />
</div>
</div>
@ -253,11 +253,10 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
<!-- 底部图表区域 -->
<div
v-if="isStarted"
class="relative z-20 h-64 max-w-5xl w-full flex flex-col shrink-0 border-2 border-blue-300 rounded-xl bg-white/90 p-4 shadow-lg backdrop-blur"
>
<!-- 图表标题栏 -->
<div class="mb-4 flex items-center justify-between px-2">
<div v-if="isStarted" class="mb-4 flex items-center justify-between px-2">
<div class="flex items-center gap-2">
<div class="h-3 w-3 rotate-45 bg-orange-400" />
<span class="text-lg text-blue-800 font-bold">答题进度</span>

View File

@ -90,7 +90,7 @@ onMounted(() => {
<div v-if="loading" class="h-full w-full flex-center">
<div class="i-svg-spinners-90-ring-with-bg text-4xl text-primary" />
</div>
<NCarousel v-else-if="events.length > 0" show-arrow draggable dot-placement="bottom" class="group h-full">
<NCarousel v-else-if="events.length > 0" show-arrow :show-dots="false" draggable dot-placement="bottom" class="group h-full">
<template #arrow="{ prev, next }">
<div class="custom-arrow left" @click="prev">
<icon-ic-round-arrow-back class="text-4xl text-icon text-white" />
@ -154,6 +154,7 @@ onMounted(() => {
width: 340px;
height: 480px;
background: url('@/assets/imgs/user/event-card-bg.svg') no-repeat center center;
background-size: 100% 100%;
border-radius: 8px 8px 40px 40px;
position: relative;
cursor: pointer;

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, watch } from 'vue'
import TianZiGe from '@/components/custom/user/TianZiGe.vue'
import { QuestionCategoryEnum } from '@/enum/business'
import { useCompetitionStore } from '@/store/modules/competition'
@ -85,28 +86,9 @@ watch(
<div class="ptions-container1 mb-2 text-center text-2xl">
{{ title }}
</div>
<div class="rich-content flex gap-1">
<div
v-for="(char, index) in pinyinChars" :key="index"
class="relative h-38 w-42 flex select-none items-center justify-center border-2 border-red-500 bg-white"
>
<!-- 米字格背景 -->
<div class="pointer-events-none absolute inset-0 z-0 h-full w-full">
<!-- 横虚线 -->
<div class="absolute left-0 top-1/2 h-[1px] w-full border-t border-red-400 border-dashed opacity-60" />
<!-- 竖虚线 -->
<div class="absolute left-1/2 top-0 h-full w-[1px] border-l border-red-400 border-dashed opacity-60" />
<!-- 斜虚线 (SVG) -->
<svg width="100%" height="100%" class="absolute inset-0 opacity-60">
<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="z-10 text-4xl text-gray-800 font-bold font-sans">{{ char }}</span>
</div>
</div>
<!-- <div class="rich-content ml-3 flex gap-1">
<TianZiGe v-for="(char, index) in pinyinChars" :key="index" :char="char" />
</div> -->
</div>
</div>
@ -139,26 +121,7 @@ watch(
<!-- 模板D: 词语听写 (拼音+提示) -->
<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"
class="relative h-32 w-32 flex select-none items-center justify-center border-2 border-red-500 bg-white"
>
<!-- 米字格背景 -->
<div class="pointer-events-none absolute inset-0 z-0 h-full w-full">
<!-- 横虚线 -->
<div class="absolute left-0 top-1/2 h-[1px] w-full border-t border-red-400 border-dashed opacity-60" />
<!-- 竖虚线 -->
<div class="absolute left-1/2 top-0 h-full w-[1px] border-l border-red-400 border-dashed opacity-60" />
<!-- 斜虚线 (SVG) -->
<svg width="100%" height="100%" class="absolute inset-0 opacity-60">
<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="z-10 text-5xl text-gray-800 font-bold font-sans">{{ char }}</span>
</div>
<TianZiGe v-for="(char, index) in pinyinChars" :key="index" :char="char" />
</div>
</div>