feat: 优化用户界面组件与调整布局样式
- 新增田字格组件用于汉字书写练习 - 调整模板详情页的生成参数和页面编号 - 优化用户主页轮播图和游戏页布局 - 修复答题结果页图片显示和状态切换问题 - 移除卡片悬停阴影并优化背景图片显示 - 更新状态管理注释和类型声明
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 8.8 MiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 11 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 7.2 MiB |
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 19 MiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 3.2 MiB |
48
apps/admin/src/components/custom/user/TianZiGe.vue
Normal 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>
|
||||
@ -7,7 +7,7 @@ export const useCompetitionStore = defineStore('competition', () => {
|
||||
// State - 核心数据
|
||||
// ==========================================
|
||||
|
||||
/** 题目列表数据 */
|
||||
/** 从抽题页获取当前题目基础提纲信息 */
|
||||
const currentQuestionMainInfo = ref<Api.Competition.CurrentQuestionResponse>({} as Api.Competition.CurrentQuestionResponse)
|
||||
|
||||
/** 当前选中的题目基础信息 (从抽题页获取) */
|
||||
|
||||
2
apps/admin/src/typings/components.d.ts
vendored
@ -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']
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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); // [悬停效果] 鼠标移上去时卡片轻微上浮
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||