Files
reader-star/apps/admin/src/views/user/draw/index.vue
rjl 18e3f6f7f2 feat(admin): 优化大屏开发体验并完善用户界面组件
- 新增大屏开发调试指南文档,包含设计基准、调试方法和字体配置说明
- 添加字体大小预览工具,支持多分辨率视口切换(2048x1080/1920x1080/3840x2160)
- 新增字体资源 README 文档和团队标题背景图片
- 优化用户界面背景图片(title-bg3.png、title-bg4.png)
- 更新 ActionButton 和 CompetitionLayout 组件样式
- 完善用户模块各页面布局(分析、封面、抽签、游戏、规则、团队)
- 优化题目渲染器组件和成绩详情页面
- 更新 UnoCSS 配置以支持大屏字体缩放方案
2026-03-20 17:05:23 +08:00

260 lines
7.4 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import drawImg from '@/assets/imgs/user/draw.webp'
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
import { useRouterPush } from '@/hooks/common/router'
import { fetchGetCurrentQuestion } from '@/service/api/game'
import { useActivityInfoStore } from '@/store/modules/activityinfo'
import { useCompetitionStore } from '@/store/modules/competition'
import { AudioController } from '@/utils/audio'
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 RoundType = computed(() => Number(route.query?.RoundType || 0) as Api.Competition.CompetitionRoundType)
const store = useCompetitionStore()
const loading = ref(false)
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
loading.value = true
try {
const { data: outlineData, error } = await fetchGetCurrentQuestion({
ActivityID: Number(activityId.value),
GroupID: Number(groupsId.value),
RoundType: RoundType.value,
})
// 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.CurrentQuestionResponse
store.setCurrentQuestionMainInfo(question.value)
}
}
catch (error) {
window.$message?.error('获取题目失败,请重试')
console.error(error)
}
finally {
loading.value = false
}
}
onMounted(() => {
initQuestions()
})
/**
* 根据抽题页的大纲 id 随机获取的当前题目基础提纲信息下面的题目信息,跳转至游戏页面
*/
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,
RoundType: RoundType.value,
},
})
return
}
if (flippedId.value)
return // 防止重复点击
try {
// 1. 播放翻牌音效 (忽略音频错误)
try {
audioController.play('flip')
}
catch (e) {
console.error('Audio play failed', e)
}
// 2. 触发翻转动画
flippedId.value = ID
// 3. 延迟1s后跳转
setTimeout(() => {
try {
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)
flippedId.value = null // 跳转失败则重置状态
window.$message?.error('跳转失败,请重试')
}
}, 1000)
}
catch (error) {
console.error(error)
flippedId.value = null
}
}
function handleBack() {
routerBack()
}
const questionMainTitle = computed(() => question.value?.QuestionList?.Name || '')
const questionTitle = computed(() => question.value?.QuestionList?.QuestionContent || question.value?.QuestionList?.Name || '')
const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4)
</script>
<template>
<CompetitionLayout
:show-back="true" :show-next="true" back-btn-text="返回" :title="questionMainTitle || '抽题'" next-btn-text="开始抽题"
:title-bg-type="titleBgTypeNum || 3" title-text-color="#ffffff"
@next="handleCardClick(question as Api.Competition.CurrentQuestionResponse)" @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-85% text-center">
<div class="text-4xl text-gray-800 font-medium leading-relaxed tracking-wide">
{{ questionTitle || '' }}
</div>
</div>
<!-- 题目卡片列表 -->
<div v-if="loading" class="text-xl text-gray-500">
加载中...
</div>
<div v-else class="w-full flex items-center justify-center pb-10">
<div v-if="question" class="lottery-stick-container">
<div
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>
</div>
<div class="stick-face stick-content">
<span class="writing-mode-vertical-rl text-2xl text-red-600 font-bold">{{ questionMainTitle
}}</span>
</div> -->
<!-- 禁用放大 -->
<NImage
:src="drawImg" class="h-full w-full object-cover" width="300" height="300"
:preview-disabled="true"
/>
</div>
</div>
</div>
<div v-if="!loading && !question" class="text-xl text-gray-400">
暂无题目
</div>
</div>
</CompetitionLayout>
</template>
<style scoped>
.lottery-stick-container {
/* width: 50px;
height: 240px; */
perspective: 1000px;
cursor: pointer;
transition: transform 0.3s;
}
.lottery-stick-container:hover {
transform: translateY(-20px);
}
.lottery-stick {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.stick-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
}
/* 竹签样式 */
.stick-body {
background: linear-gradient(to right, #e6cba5, #d4b081, #c69c6d);
border: 1px solid #b08d55;
}
.stick-top-mark {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 20px;
background-color: #ef4444;
/* red-500 */
border-radius: 8px 8px 0 0;
}
/* 翻转后的内容面 - 做成类似令牌的样子 */
.stick-content {
background: linear-gradient(to bottom, #fff9f0, #fff);
border: 2px solid #b08d55;
transform: rotateY(180deg);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
/* 选中(抽出)动画状态 */
.lottery-stick.is-selected {
transform: translateY(-100px) scale(1.4) rotateY(180deg);
z-index: 50;
}
/* 竖排文字 */
.writing-mode-vertical-rl {
writing-mode: vertical-rl;
text-orientation: upright;
}
</style>