feat: 新增测试页面并优化用户界面及API调用

- 新增测试页面 `/test`,用于展示题目答案的交互式网格和列表布局
- 为 CompetitionLayout 组件添加标题背景类型支持,优化视觉一致性
- 更新 `fetchUpdateCurrentQuestionUse` API 参数,简化调用逻辑
- 优化用户界面细节,包括图标替换、样式调整和布局改进
This commit is contained in:
2026-02-27 08:37:45 +08:00
parent f1ff053b68
commit e70409082a
16 changed files with 356 additions and 63 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 825 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.0 MiB

View File

@ -1,7 +1,9 @@
<script lang="ts" setup>
import VScaleScreen from 'v-scale-screen'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import _defaultBg from '@/assets/imgs/user/home-bg.svg'
import titleBg2 from '@/assets/imgs/user/title-bg2.svg'
import _defaultTitleBg from '@/assets/imgs/user/title-bg.svg'
import { useRouterPush } from '@/hooks/common/router'
interface Props {
@ -25,6 +27,8 @@ interface Props {
btnTheme?: 'primary' | 'light'
/** 是否禁用下一步按钮 */
nextDisabled?: boolean
/** 标题区域背景图片URL不传则使用默认 title-bg.svg */
titleBgType?: number
}
const props = withDefaults(defineProps<Props>(), {
@ -38,6 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
backBtnText: undefined,
btnTheme: 'primary',
nextDisabled: false,
titleBgType: 1,
})
const emit = defineEmits<{
@ -45,8 +50,15 @@ const emit = defineEmits<{
(e: 'next'): void
}>()
const titleBgUrlMapping: Record<number, string> = {
1: _defaultTitleBg,
2: titleBg2,
}
const { routerPushByKey } = useRouterPush()
const titleBgUrl = computed(() => titleBgUrlMapping[props.titleBgType] || _defaultBg)
// const router = useRouter()
const innerBgUrl = ref(_defaultBg)
@ -108,7 +120,7 @@ onMounted(() => {
<section v-if="showTitle" class="title-section">
<Transition name="fade-slide-down" appear>
<div v-if="showContent" class="title-section">
<div class="scroll-bg">
<div class="scroll-bg" :style="{ backgroundImage: `url(${titleBgUrl})` }">
<h1 class="main-title">
{{ title }}
</h1>
@ -123,16 +135,20 @@ onMounted(() => {
</main>
<!-- Footer Action -->
<footer class="page-footer w-full flex items-center justify-between px-20" :class="[actionPosition, btnTheme]">
<button class="action-btn back-btn" :class="{ 'text-btn': backBtnText }" :style="{ visibility: showBack ? 'visible' : 'hidden' }" @click="handleBack">
<footer
class="page-footer w-full flex items-center justify-between px-20"
:class="[actionPosition, btnTheme]"
>
<button
class="action-btn back-btn" :class="{ 'text-btn': backBtnText }"
:style="{ visibility: showBack ? 'visible' : 'hidden' }" @click="handleBack"
>
<span v-if="backBtnText" class="btn-text">{{ backBtnText }}</span>
<SvgIcon v-else icon="mdi:arrow-left" class="arrow-icon" />
</button>
<button
v-if="showNext"
class="action-btn next-btn text-btn"
:class="{ 'is-disabled': nextDisabled }"
v-if="showNext" class="action-btn next-btn text-btn" :class="{ 'is-disabled': nextDisabled }"
@click="handleNext"
>
<span class="btn-text">{{ nextBtnText || '下一步' }}</span>
@ -148,14 +164,14 @@ onMounted(() => {
<style lang="scss" scoped>
.title-section {
margin: 10px auto;
width: 405px;
height: 150px;
width: 412px;
height: 165px;
.scroll-bg {
width: 100%;
height: 100%;
position: relative;
background: url('@/assets/imgs/user/title-bg.svg') no-repeat center center;
background: no-repeat center center;
background-size: 100% 100%;
display: flex;
align-items: center;

View File

@ -31,6 +31,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
results: () => import("@/views/results/index.vue"),
"template_template-detail": () => import("@/views/template/template-detail/index.vue"),
"template_template-list": () => import("@/views/template/template-list/index.vue"),
test: () => import("@/views/test/index.vue"),
user_analysis: () => import("@/views/user/analysis/index.vue"),
user_cover: () => import("@/views/user/cover/index.vue"),
user_draw: () => import("@/views/user/draw/index.vue"),

View File

@ -223,6 +223,15 @@ export const generatedRoutes: GeneratedRoute[] = [
}
]
},
{
name: 'test',
path: '/test',
component: 'layout.base$view.test',
meta: {
title: 'test',
i18nKey: 'route.test'
}
},
{
name: 'user',
path: '/user',

View File

@ -182,6 +182,7 @@ const routeMap: RouteMap = {
"template": "/template",
"template_template-detail": "/template/template-detail",
"template_template-list": "/template/template-list",
"test": "/test",
"user": "/user",
"user_analysis": "/user/analysis",
"user_cover": "/user/cover",

View File

@ -1,10 +1,10 @@
import { request } from '../request'
/** 评委消费题目,表示用了该题目 */
export function fetchUpdateCurrentQuestionUse(data: Api.Competition.GetCurrentQuestionParams) {
export function fetchUpdateCurrentQuestionUse(TeamGroupID: string) {
return request<App.Service.Response>({
url: `/Base/ActivityMain/UpdateCurrentQuestionUse?ActivityID=${data.ActivityID}&GroupID=${data.GroupID}&RoundType=${data.RoundType}&isUse=${data.isUse}`,
url: `/Base/ActivityMain/UpdateCurrentQuestionUse?TeamGroupID=${TeamGroupID}`,
method: 'post',
data,
data: { TeamGroupID },
})
}

View File

@ -36,6 +36,7 @@ declare module "@elegant-router/types" {
"template": "/template";
"template_template-detail": "/template/template-detail";
"template_template-list": "/template/template-list";
"test": "/test";
"user": "/user";
"user_analysis": "/user/analysis";
"user_cover": "/user/cover";
@ -88,6 +89,7 @@ declare module "@elegant-router/types" {
| "rank"
| "results"
| "template"
| "test"
| "user"
>;
@ -121,6 +123,7 @@ declare module "@elegant-router/types" {
| "results"
| "template_template-detail"
| "template_template-list"
| "test"
| "user_analysis"
| "user_cover"
| "user_draw"

View File

@ -2,15 +2,20 @@
import { useIntervalFn } from '@vueuse/core'
import { NButton } from 'naive-ui'
import { storeToRefs } from 'pinia'
import { onUnmounted, ref } from 'vue'
import { computed, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
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()
// eslint-disable-next-line unused-imports/no-unused-vars
const { activityId, groupId } = storeToRefs(activityInfoStore)
const route = useRoute()
const TeamGroupID = computed(() => route.query?.TeamGroupID as string)
// --- 模拟数据和状态 ---
const groups = ref<GroupItem[]>(Array.from({ length: 8 }).map((_, i) => ({
@ -173,19 +178,15 @@ function handleNext() {
}
function handlePublish() {
fetchGetQuestionDetailData()
window.$message?.success('结果已发布')
if (TeamGroupID.value) {
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 })
const { data: outlineData, error } = await fetchUpdateCurrentQuestionUse(TeamGroupID.value)
// eslint-disable-next-line no-console
console.log(outlineData, 'outlineData')
if (error) {

View File

@ -131,7 +131,7 @@ async function handleSave() {
const params: Api.Template.AddTemplateParams = {
id,
pageNo: '1',
pageNo: '1761.172.8.66',
name: templateInfo.value.name,
backGroundUrl: templateInfo.value.backGroundUrl || '',
width: Math.round(templateInfo.value.width),

View File

@ -0,0 +1,229 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { NCard, NImage, NSpace } from 'naive-ui'
import { ref, watch } from 'vue'
interface AnswerItem {
id: number
label?: string
imageUrl: string // 在真实场景中是学生答题截图的URL
status: 'correct' | 'wrong' // 初始状态由大模型判定
type: 'grid' | 'line' // 'grid'对应方格题(如汉字),'line'对应横线题(如词语)
}
interface QuestionGroup {
id: number
title: string
items: AnswerItem[]
layout: 'grid' | 'list' // 布局方式
}
interface Round {
id: number
title: string
subTitle?: string
groups: QuestionGroup[]
}
// 模拟数据
const rounds = ref<Round[]>([
{
id: 1,
title: '第一轮',
subTitle: '汉字大比拼',
groups: [
{
id: 101,
title: '汉字听写 1',
layout: 'grid',
items: [
{ id: 1, label: '1', imageUrl: 'https://placehold.co/100x100?text=汉', status: 'correct', type: 'grid' },
{ id: 2, label: '2', imageUrl: 'https://placehold.co/100x100?text=字', status: 'correct', type: 'grid' },
{ id: 3, label: '3', imageUrl: 'https://placehold.co/100x100?text=听', status: 'correct', type: 'grid' },
{ id: 4, label: '4', imageUrl: 'https://placehold.co/100x100?text=写', status: 'correct', type: 'grid' },
{ id: 5, label: '5', imageUrl: 'https://placehold.co/100x100?text=一', status: 'correct', type: 'grid' },
],
},
{
id: 102,
title: '汉字听写 2',
layout: 'grid',
items: [
{ id: 6, label: '1', imageUrl: 'https://placehold.co/100x100?text=错', status: 'wrong', type: 'grid' },
{ id: 7, label: '2', imageUrl: 'https://placehold.co/100x100?text=误', status: 'wrong', type: 'grid' },
{ id: 8, label: '3', imageUrl: 'https://placehold.co/100x100?text=示', status: 'wrong', type: 'grid' },
{ id: 9, label: '4', imageUrl: 'https://placehold.co/100x100?text=例', status: 'wrong', type: 'grid' },
{ id: 10, label: '5', imageUrl: 'https://placehold.co/100x100?text=!', status: 'wrong', type: 'grid' },
],
},
{
id: 103,
title: '汉字加一加',
layout: 'grid',
items: Array.from({ length: 10 }).map((_, i) => ({
id: 20 + i,
imageUrl: `https://placehold.co/100x100?text=${i + 1}`,
status: i === 4 ? 'wrong' : 'correct', // 第5个模拟错误
type: 'grid',
})),
},
],
},
{
id: 2,
title: '第一轮',
subTitle: '词语大比拼',
groups: [
{
id: 201,
title: '词语听写',
layout: 'list',
items: [
{ id: 301, label: '1.', imageUrl: 'https://placehold.co/400x60?text=词语答案一', status: 'correct', type: 'line' },
{ id: 302, label: '2.', imageUrl: 'https://placehold.co/400x60?text=词语答案二', status: 'correct', type: 'line' },
],
},
{
id: 202,
title: '成语写一写 1',
layout: 'list',
items: [
{ id: 303, label: '1.', imageUrl: 'https://placehold.co/200x60?text=成语一', status: 'correct', type: 'line' },
{ id: 304, label: '2.', imageUrl: 'https://placehold.co/200x60?text=成语二', status: 'wrong', type: 'line' },
{ id: 305, label: '3.', imageUrl: 'https://placehold.co/200x60?text=成语三', status: 'wrong', type: 'line' },
],
},
{
id: 203,
title: '成语写一写 2',
layout: 'list',
items: [
{ id: 306, label: '1.', imageUrl: 'https://placehold.co/600x60?text=长成语答案测试', status: 'correct', type: 'line' },
{ id: 307, label: '2.', imageUrl: 'https://placehold.co/600x60?text=另一个答案', status: 'correct', type: 'line' },
],
},
],
},
])
// 切换状态逻辑:点击后 Correct <-> Wrong
function toggleStatus(item: AnswerItem) {
item.status = item.status === 'correct' ? 'wrong' : 'correct'
}
watch(() => rounds.value, (val) => {
// eslint-disable-next-line no-console
console.log(val, 'v')
}, {
deep: true,
})
</script>
<template>
<div class="min-h-screen bg-gray-50 p-4">
<NSpace vertical size="large">
<NCard v-for="round in rounds" :key="round.id" :title="`${round.title} · ${round.subTitle}`" size="huge">
<div class="flex flex-col gap-8">
<div v-for="group in round.groups" :key="group.id">
<h3 class="mb-4 text-lg text-gray-700 font-medium">
{{ group.title }}
</h3>
<!-- Grid Layout (Grid Items) -->
<div v-if="group.layout === 'grid'" class="flex flex-wrap gap-4">
<div
v-for="item in group.items"
:key="item.id"
class="group relative cursor-pointer transition-all duration-200"
@click="toggleStatus(item)"
>
<!-- Image Container with Status Border -->
<div
class="relative overflow-hidden border-2 rounded-md border-dashed bg-white p-1"
:class="[
item.status === 'correct' ? 'border-green-500' : 'border-red-500',
]"
>
<NImage
:src="item.imageUrl"
preview-disabled
object-fit="contain"
:class="group.layout === 'grid' ? 'w-24 h-24' : ''"
/>
<!-- Status Icon Overlay -->
<div class="absolute z-10 rounded-full bg-white -right-3 -top-3">
<Icon
v-if="item.status === 'correct'"
icon="mdi:check-circle"
class="text-2xl text-green-500"
/>
<Icon
v-else
icon="mdi:close-circle"
class="text-2xl text-red-500"
/>
</div>
</div>
<!-- Label if exists -->
<div v-if="item.label" class="mt-1 text-center text-sm text-gray-500">
{{ item.label }}
</div>
</div>
</div>
<!-- List Layout (Line Items) -->
<div v-else class="flex flex-col gap-4">
<!-- Using a grid for lines to handle multiple per row if needed, or just flex-wrap -->
<div class="flex flex-wrap gap-x-8 gap-y-4">
<div
v-for="item in group.items"
:key="item.id"
class="relative cursor-pointer transition-all duration-200"
@click="toggleStatus(item)"
>
<div class="flex items-center gap-2">
<span v-if="item.label" class="w-6 text-gray-600 font-medium">{{ item.label }}</span>
<div
class="relative overflow-hidden border-2 rounded-md border-dashed bg-white px-2 py-1"
:class="[
item.status === 'correct' ? 'border-green-500' : 'border-red-500',
]"
>
<NImage
:src="item.imageUrl"
preview-disabled
object-fit="contain"
height="50"
/>
<!-- Status Icon Overlay -->
<div class="absolute z-10 rounded-full bg-white shadow-sm -right-3 -top-3">
<Icon
v-if="item.status === 'correct'"
icon="mdi:check-circle"
class="text-2xl text-green-500"
/>
<Icon
v-else
icon="mdi:close-circle"
class="text-2xl text-red-500"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</NCard>
</NSpace>
</div>
</template>
<style scoped>
/* Ensure icons are visible and positioned correctly */
</style>

View File

@ -134,7 +134,8 @@ onMounted(async () => {
<template>
<CompetitionLayout
:show-back="true" :show-next="showNextButton" :title="activityName" @back="handleBack"
:show-back="true" :show-next="showNextButton" :title="activityName" :title-bg-type="2"
@back="handleBack"
@next="handleNext"
>
<div class="h-screen w-full overflow-y-auto font-sans">

View File

@ -70,24 +70,15 @@ onMounted(() => {
<template>
<CompetitionLayout
:show-back="true"
:show-next="isAllEnd"
next-btn-text="查看队伍结果"
title="展示组别"
@back="handleBack"
@next="handleNext"
:show-back="true" :show-next="isAllEnd" next-btn-text="查看队伍结果" :title-bg-type="2"
title="展示组别" @back="handleBack" @next="handleNext"
>
<!-- Groups Grid -->
<div class="groups-container">
<TransitionGroup name="list-anim" tag="div" class="groups-grid">
<div
v-for="(item, index) in groups"
v-show="showContent"
:key="item.Id"
class="group-item"
:class="{ 'is-disabled': item.IsEnd }"
:style="{ '--delay': `${index * 0.05}s` }"
@click="selectGroup(item)"
v-for="(item, index) in groups" v-show="showContent" :key="item.Id" class="group-item"
:class="{ 'is-disabled': item.IsEnd }" :style="{ '--delay': `${index * 0.05}s` }" @click="selectGroup(item)"
>
<div class="icon-wrapper">
<div class="flower-bg" />

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { NCarousel } from 'naive-ui'
import { NCarousel, NImage } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
import beacon from '@/assets/imgs/user/event-card-bg-icon.svg'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchGetActivityList } from '@/service/api/competition'
@ -81,7 +82,8 @@ onMounted(() => {
<template>
<CompetitionLayout
:show-back="false" :show-next="false" title="赛事总览" action-position="top" back-btn-text="返回"
btn-theme="light" @back="handleBack" @next="handleNext"
btn-theme="light" :title-bg-type="2" @back="handleBack"
@next="handleNext"
>
<!-- Cards Section -->
<div class="cards-container">
@ -106,7 +108,11 @@ onMounted(() => {
<div class="card-top-bar" />
<div class="card-body">
<div class="icon-wrapper">
<SvgIcon :local-icon="item.icon" class="event-icon" />
<NImage
:src="beacon"
preview-disabled
object-fit="contain"
/>
</div>
<div class="text-content">
<h3 class="event-title">
@ -168,20 +174,12 @@ onMounted(() => {
}
.icon-wrapper {
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
width: 170px;
height: 161px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
.event-icon {
font-size: 48px;
color: $secondary-color;
}
margin: 30px 0;
}
.text-content {
@ -203,8 +201,8 @@ onMounted(() => {
transform: translateY(-10px);
.icon-wrapper {
transform: scale(1.1) rotate(5deg);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform: scale(1.1) rotate(2deg);
transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
}
}

View File

@ -62,8 +62,6 @@ function handleNext() {
</template>
<style lang="scss" scoped>
@import '@/styles/scss/variables.scss';
.rules-content-wrapper {
width: 100%;
max-height: 80%;
@ -76,7 +74,6 @@ function handleNext() {
margin-top: 10px;
font-size: 28px;
line-height: 2;
// color: #5d4037; // Dark brown/grey text
.intro-text {
margin-bottom: 30px;