feat(admin): 更新用户界面组件并优化样式

- 移除 uno.config.ts 中的注释以简化字体配置
- 更新 .env.dev 文件中的服务基础 URL
- 新增用户排名标题背景图片并优化相关组件样式
- 调整 ActionButton 组件的最小宽度
- 修改 CompetitionLayout 组件中的图像和样式以提升视觉效果
- 更新全局 CSS 文件以增加基础字体大小
- 优化 results-detail 视图中的发布逻辑延迟
- 调整用户分析和封面页面的文本样式
- 更新团队页面的团队名称样式以适应设计规范
This commit is contained in:
2026-03-26 15:18:45 +08:00
parent 3d3f7afcd2
commit c617307c79
14 changed files with 89 additions and 76 deletions

View File

@ -5,7 +5,7 @@
# VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com
VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
VITE_SERVICE_BASE_URL=http://api.ydzx.qyzhjy.com/
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9528"

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

@ -135,7 +135,7 @@ function handleClick() {
.custom-btn {
background: v-bind(customBg);
background-color: transparent;
min-width: 210px;
min-width: 220px;
width: auto;
height: 60px;
padding: 0 20px;

View File

@ -230,7 +230,7 @@ watch(titleBgUrl, (newUrl) => {
<header class="page-header flex items-center px-8 text-[30px] text-[#333333] font-bold">
<!-- 左侧logo -->
<div class="flex cursor-pointer items-center" @click="handleBackHome">
<img :src="cqLogo" alt="重庆logo" class="h-20 w-auto">
<img :src="cqLogo" alt="重庆logo" class="h-18 w-auto">
</div>
<!-- 中间文字 -->
@ -249,17 +249,23 @@ watch(titleBgUrl, (newUrl) => {
<section v-if="showTitle" class="title-section">
<Transition name="scroll-unfold" appear>
<div v-if="showContent" class="title-wrapper">
<div class="scroll-bg">
<div
class="scroll-bg"
:style="{ width: props.titleBgType === 3 ? '400px' : props.titleBgType === 4 ? '420px' : '480px' }"
>
<!-- 预渲染所有背景图 opacity 切换避免加载抖动 -->
<img
v-for="(url, _key) in titleBgUrlMapping" :key="_key" :src="url" class="scroll-bg-img" :class="{
active: titleBgUrl === url && imageLoadStatus[url] !== false,
loading: imageLoadStatus[url] === undefined,
}" :style="{ visibility: imageLoadStatus[url] === false ? 'hidden' : 'visible' }" aria-hidden="true"
@load="imageLoadStatus[url] = true" @error="imageLoadStatus[url] = false"
}" :style="{ visibility: imageLoadStatus[url] === false ? 'hidden' : 'visible' }"
aria-hidden="true" @load="imageLoadStatus[url] = true" @error="imageLoadStatus[url] = false"
>
<Transition name="fade-in-delay" appear>
<h1 v-if="showContent" class="main-title" :style="{ color: titleTextColor }">
<h1
v-if="showContent" class="main-title"
:style="{ color: titleTextColor, letterSpacing: props.titleBgType === 2 ? '6px' : props.titleBgType === 4 ? '1px' : '4px', fontSize: props.titleBgType === 4 ? '38px' : '' }"
>
{{ title }}
</h1>
</Transition>
@ -285,7 +291,7 @@ watch(titleBgUrl, (newUrl) => {
class="back-btn" :style="{ visibility: showBack ? 'visible' : 'hidden' }" type="custom"
:text="backBtnText || ''" :theme="btnTheme" :font-size="fontSize" @click="handleBack"
>
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="40" class="arrow-icon" />
<SvgIcon v-if="!backBtnText" icon="mdi:arrow-left" size="42" class="arrow-icon" />
</ActionButton>
</div>
@ -294,14 +300,14 @@ watch(titleBgUrl, (newUrl) => {
v-if="showNext" type="custom" :text="nextBtnText || '下一步'" :theme="btnTheme"
:disabled="nextDisabled" :font-size="fontSize" @click="handleNext"
>
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="40" />
<SvgIcon v-if="!nextBtnText" icon="mdi:arrow-right" class="arrow-icon" size="42" />
</ActionButton>
<ActionButton
v-if="showNextBtn2" :type="nextBtnText2 ? 'text' : 'button'" :text="nextBtnText2 || '下一步'"
:theme="btnTheme" :disabled="nextDisabled" @click="handleNext2"
>
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" size="40" />
<SvgIcon v-if="!nextBtnText2" icon="mdi:arrow-right" class="arrow-icon" size="42" />
</ActionButton>
</div>
</footer>
@ -330,9 +336,9 @@ watch(titleBgUrl, (newUrl) => {
}
.scroll-bg {
width: 400px !important;
width: 400px;
/* Fixed width to prevent squishing during animation */
height: 155px !important;
height: 180px !important;
position: relative;
display: flex;
align-items: center;
@ -363,10 +369,10 @@ watch(titleBgUrl, (newUrl) => {
}
.main-title {
font-size: 2.6rem;
font-size: 2.66rem;
color: $text-color;
margin: 0;
letter-spacing: 3px;
letter-spacing: 2px;
text-align: center;
white-space: nowrap;
/* Prevent text wrapping */
@ -444,12 +450,13 @@ watch(titleBgUrl, (newUrl) => {
.page-header {
position: absolute;
top: -20px;
top: -50px;
left: 0;
width: 100%;
height: 180px;
display: flex;
justify-content: space-between;
padding: 20px 40px;
padding: 20px 44px;
font-weight: 700;
color: $secondary-color;
z-index: 10;

View File

@ -7,6 +7,7 @@ body,
#app {
height: 100%;
text-autospace: normal;
font-size: 18px;
}
html {

View File

@ -473,7 +473,7 @@ async function handleNexCurrentQuestion() {
function handlePublish() {
const endTimeStr = (currentQuestion.value as any)?.EndTime
if (endTimeStr && Date.now() < new Date(endTimeStr).getTime() + 5000) {
if (endTimeStr && Date.now() < new Date(endTimeStr).getTime() + 1000) {
window.$message?.warning('倒计时未结束需等待5秒延迟暂不能发布')
return
}
@ -589,7 +589,7 @@ const canPublish = computed(() => {
if (!endTime)
return true
// 结束时间 + 5秒延迟才能发布
const end = new Date(endTime).getTime() + 5000
const end = new Date(endTime).getTime() + 2000
return nowTs.value >= end
})
@ -599,8 +599,8 @@ function startPolling() {
// 检查是否已经结束
const endTime = (currentQuestion.value as any)?.EndTime
if (endTime) {
// 增加 12s 缓冲时间
const end = new Date(endTime).getTime() + 12000
// 增加 5s 缓冲时间
const end = new Date(endTime).getTime() + 3000
const now = new Date().getTime()
if (now > end) {
console.log('当前题目已结束含12s缓冲仅获取一次最终结果不启动轮询', endTime)
@ -622,10 +622,10 @@ function startPolling() {
// 每次轮询前再次检查是否过期
const currentEndTime = (currentQuestion.value as any)?.EndTime
if (currentEndTime) {
// 增加 12s 缓冲时间
const endTs = new Date(currentEndTime).getTime() + 12000
// 增加 5s 缓冲时间
const endTs = new Date(currentEndTime).getTime() + 3000
if (Date.now() > endTs) {
console.log('题目时间已到(含12s缓冲停止轮询', currentEndTime)
console.log('题目时间已到(含3s缓冲停止轮询', currentEndTime)
stopPolling()
return
}

View File

@ -161,7 +161,7 @@ onBeforeUnmount(() => {
<template>
<CompetitionLayout
:show-title="true" :show-back="false" :show-next="showNextBtn" :title-bg-type="3" title-text-color="#ffffff" title="答题图解" action-position="top"
back-btn-text="返回" :next-btn-text="isCompleted ? '该组已完成回到选组' : '下一题'" btn-theme="light" @back="handleBack"
back-btn-text="返回" :next-btn-text="isCompleted ? '已完成选组' : '下一题'" btn-theme="light" @back="handleBack"
@next="handleNext"
>
<div class="relative h-full w-full flex flex-col items-center px-12 pt-10 font-sans">

View File

@ -240,7 +240,7 @@ onMounted(async () => {
<!-- 文字内容 -->
<div class="relative z-10 transform text-center">
<div
class="card-text text-2xl text-[#333333] font-bold leading-tight font-serif drop-shadow-sm"
class="card-text text-26px text-[#333333] font-bold leading-tight font-serif drop-shadow-sm"
>
<span class="block">{{ node.moduleName.slice(0, 2) }}</span>
<span class="block">{{ node.moduleName.slice(2) }}</span>

View File

@ -285,7 +285,7 @@ const titleBgTypeNum = computed(() => RoundType.value === 0 ? 3 : 4) || 3
>
<!-- 图表标题栏 -->
<div v-if="isStarted" class="z-index-999 relative mb-4">
<img src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度" class="z-index-999 absolute left--26px top--22px h-auto max-h-16 w-auto">
<img src="@/assets/imgs/user/rank-bg-kuang.png" alt="答题进度" class="z-index-999 absolute left--30px top--25px h-auto max-h-16 w-auto">
</div>
<!-- 柱状图ECharts -->

View File

@ -126,7 +126,7 @@ watch(
{{ title }}
</div>
<div class="mb-1 text-gray-800 font-bold leading-relaxed">
<div class="rich-content content-with-bg pinyin-text ml-3 mt-4 flex text-7xl !max-w-100% !px-16 !py-8 !tracking-[0.08em]">
<div class="rich-content content-with-bg pinyin-text ml-3 mt-4 flex text-7xl !max-w-100% !px-16 !py-4 !tracking-[0.08em]">
{{ content }}
</div>
</div>
@ -135,8 +135,8 @@ watch(
<!-- 模板C: 汉字加一加 (提示+部件) -->
<div v-else-if="template === QuestionCategoryEnum.CharacterRadicalAddition" class="text-center">
<div class="mb-6 inline-block text-5xl font-bold leading-none">
<div class="rich-content ml-3 mt-4 flex gap-1 text-6xl tracking-[0.06em]">
请写出含有 <span class="text-color-red !text-6xl">{{ content }}</span>的汉字书写时间为60秒
<div class="rich-content ml-3 mt-4 flex gap-1 text-5xl tracking-[0.06em]">
请写出含有 <span class="text-color-red !text-5xl">{{ content }}</span>的汉字书写时间为60秒
</div>
</div>
</div>
@ -153,7 +153,7 @@ watch(
<!-- 模板E: 成语-文字要求 -->
<div v-else-if="template === QuestionCategoryEnum.IdiomWriting2" class="text-center">
<div class="mb-2">
<img :src="ImageUrl" preview-disabled class="max-h-380px max-w-700px" object-fit="cover">
<img :src="ImageUrl" preview-disabled class="max-h-368px max-w-700px" object-fit="cover">
</div>
</div>
@ -173,8 +173,8 @@ watch(
<!-- 模板: 场景猜诗句 -->
<div v-else-if="template === QuestionCategoryEnum.SceneBasedPoemGuessing" class="w-full flex flex-col items-center">
<div class="mb-1 p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-20">
<div class="rich-content" v-html="content" />
<div class="mb-1px p-16 text-3xl text-gray-800 font-bold leading-relaxed line-height-16">
<div class="rich-content indent-[2em]" v-html="content" />
</div>
</div>

View File

@ -3,7 +3,7 @@ import type { DataTableColumns } from 'naive-ui'
import { NDataTable, NImage, NTag, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import title from '@/assets/imgs/user/rank-title.svg'
import title from '@/assets/imgs/user/rank-title.png'
import ActionButton from '@/components/custom/user/ActionButton.vue'
import { fetchUserRankList } from '@/service/api/rank'
import ExtraTimeModal from './components/ExtraTimeModal.vue'
@ -57,6 +57,23 @@ const columns = ref<DataTableColumns<TeamData>>([
const tableData = ref<TeamData[]>([])
// eslint-disable-next-line unused-imports/no-unused-vars
const mockUserRankList: Api.Rank.ActivityRankResList = Array.from({ length: 13 }, (_, teamIndex) => {
// eslint-disable-next-line ts/no-shadow
const questions = Array.from({ length: 10 }, (_, questionIndex) => {
const QuestionNO = questionIndex + 1
const ResultPotin = Math.max(0, 10 - teamIndex - (questionIndex % 3))
return { QuestionNO, ResultPotin }
})
return {
TeamID: 1001 + teamIndex,
Name: `测试队伍${teamIndex + 1}`,
TotalPoint: questions.reduce((sum, item) => sum + item.ResultPotin, 0),
Questions: questions,
}
})
async function getUserRankList() {
const { data, error } = await fetchUserRankList(MainID, RoundType)
if (error) {
@ -64,9 +81,11 @@ async function getUserRankList() {
return
}
if (data?.data && data.data.length > 0) {
const rankSource = data?.data || []
if (rankSource.length > 0) {
// 动态生成题目列
const firstTeam = data.data[0]
const firstTeam = rankSource[0]
const questionColumns = firstTeam.Questions.map(q => ({
title: `${q.QuestionNO}`,
key: `q${q.QuestionNO}`,
@ -94,12 +113,12 @@ async function getUserRankList() {
return <span>{rank}</span>
},
},
{ title: '队伍名称', key: 'teamName', align: 'left', width: 150, className: 'team-name' },
{ title: '队伍名称', key: 'teamName', align: 'left', width: 190, className: 'team-name' },
...questionColumns,
{ title: '总积分', key: 'total', align: 'center', className: 'total-score' },
]
tableData.value = data.data.map((item, index) => {
tableData.value = rankSource.map((item, index) => {
const questions: Record<string, number> = {}
item.Questions.forEach((q) => {
questions[`q${q.QuestionNO}`] = q.ResultPotin
@ -114,7 +133,7 @@ async function getUserRankList() {
} as TeamData
})
teamOptions.value = data.data.map(item => ({ label: item.Name, value: item.TeamID }))
teamOptions.value = rankSource.map(item => ({ label: item.Name, value: item.TeamID }))
}
}
@ -137,21 +156,21 @@ function rowClassName(row: TeamData) {
<template>
<CompetitionLayout :show-back="false" :show-next="false" :show-title="false">
<div class="leaderboard-container">
<div class="leaderboard-container h-100%">
<div class="leaderboard-header">
<NImage :src="title" alt="logo" preview-disabled class="mt--60px" object-fit="contain" />
<p class="mt--20px text-center text-lg text-#6b778d">
<NImage :src="title" alt="logo" preview-disabled class="mt--42px" object-fit="contain" />
<!-- <p class="mt--16px text-center text-18px text-#6b778d">
"勤学如春起之苗,不见其增,日有所长"
</p>
</p> -->
<ActionButton
v-if="hasExtraTimeBtn" class="position-absolute bottom-20px right-20px" type="text" text="加时赛"
@click="handleAddTime"
v-if="hasExtraTimeBtn" class="z-index-99 position-absolute right-20px top-20px" type="text"
text="加时赛" @click="handleAddTime"
/>
</div>
<div class="leaderboard-content">
<NDataTable
:columns="columns" :data="tableData" :bordered="false" :bottom-bordered="false" size="large"
:row-class-name="rowClassName" class="rank-table"
:columns="columns" :data="tableData" :bordered="false" max-height="100%" :bottom-bordered="false"
size="large" :row-class-name="rowClassName" class="rank-table !h-100%"
/>
</div>
</div>
@ -163,7 +182,7 @@ function rowClassName(row: TeamData) {
/* 1. 整体容器和背景 */
.leaderboard-container {
width: 100%;
min-height: 100vh;
height: 100%;
background-image: url('https://raw.githubusercontent.com/0x3f3f3f3f/figure-bed/main/img/20240524150352.png');
background-size: cover;
background-position: center;
@ -193,19 +212,12 @@ function rowClassName(row: TeamData) {
}
.leaderboard-header h1 {
font-size: 2.5rem;
font-size: 2.2rem;
font-weight: 700;
color: #1a2a44;
margin: 0;
}
/* .leaderboard-header p {
font-size: 1rem;
color: #6b778d;
margin-top: 8px;
} */
/* 3. 表格内容区 */
.leaderboard-content {
width: 100%;
/* max-width: 1200px; */
@ -213,6 +225,7 @@ function rowClassName(row: TeamData) {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(26, 42, 68, 0.05);
overflow: hidden;
height: calc(100% - 220px);
}
/* 4. Naive UI 表格深度定制 */
@ -221,7 +234,7 @@ function rowClassName(row: TeamData) {
}
:deep(.rank-table .n-data-table-th) {
font-size: 14px;
font-size: 24px;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6 !important;
@ -236,11 +249,11 @@ function rowClassName(row: TeamData) {
}
:deep(.rank-table .n-data-table-td) {
font-size: 15px;
font-size: 24px;
color: #333;
border-bottom: 1px solid #e9ecef !important;
padding-top: 16px !important;
padding-bottom: 16px !important;
/* padding-top: 16px !important; */
/* padding-bottom: 16px !important; */
}
/* 奇偶行交替色 */
@ -256,7 +269,7 @@ function rowClassName(row: TeamData) {
:deep(.total-score) {
font-weight: 700;
font-size: 1.1rem;
font-size: 2.1rem;
color: #e67e22;
}
@ -284,4 +297,11 @@ function rowClassName(row: TeamData) {
:deep(.rank-top-3 .total-score) {
color: #cd7f32;
}
/* 强制表格容器占满父级 */
.leaderboard-content :deep(.n-data-table) {
height: 100% !important;
max-height: 100% !important;
overflow-y: auto;
}
</style>

View File

@ -116,7 +116,7 @@ onMounted(() => {
<div class="icon-wrapper">
<img src="@/assets/imgs/user/team-title-icon.webp" alt="team-icon" class="h-full w-full object-cover">
</div>
<div class="team-name text-3xl">
<div class="team-name text-2xl">
{{ item.name }}
</div>
</div>