chore(admin): 优化构建配置并升级资源格式
- 新增构建插件:打包分析、压缩、自动部署、图片优化和PWA支持 - 将SVG资源转换为WebP格式以提升加载性能 - 添加新的背景图片资源用于用户界面优化 - 更新环境配置:调整基础URL和服务地址 - 优化用户界面组件以适配新的资源格式 - 更新package.json依赖以支持新的构建功能 - 改进构建流程以支持自动部署和性能监控
BIN
apps/admin/src/assets/imgs/user/cover-back.webp
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
apps/admin/src/assets/imgs/user/cover-bg-active.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
BIN
apps/admin/src/assets/imgs/user/draw.webp
Normal file
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
BIN
apps/admin/src/assets/imgs/user/event-card-bg-icon.webp
Normal file
|
After Width: | Height: | Size: 740 KiB |
|
Before Width: | Height: | Size: 7.2 MiB |
BIN
apps/admin/src/assets/imgs/user/event-card-bg.webp
Normal file
|
After Width: | Height: | Size: 445 KiB |
BIN
apps/admin/src/assets/imgs/user/home-bg.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
apps/admin/src/assets/imgs/user/read-logo 1.webp
Normal file
|
After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 10 MiB |
BIN
apps/admin/src/assets/imgs/user/star-icon.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
apps/admin/src/assets/imgs/user/team-bg-animation.webp
Normal file
|
After Width: | Height: | Size: 907 KiB |
|
Before Width: | Height: | Size: 16 MiB |
BIN
apps/admin/src/assets/imgs/user/team-bg.webp
Normal file
|
After Width: | Height: | Size: 907 KiB |
|
Before Width: | Height: | Size: 5.5 MiB |
BIN
apps/admin/src/assets/imgs/user/team-title-icon.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
BIN
apps/admin/src/assets/imgs/user/title-bg.webp
Normal file
|
After Width: | Height: | Size: 549 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
BIN
apps/admin/src/assets/imgs/user/title-bg2.webp
Normal file
|
After Width: | Height: | Size: 449 KiB |
@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import VScaleScreen from 'v-scale-screen'
|
||||
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 _defaultBg from '@/assets/imgs/user/home-bg.jpg'
|
||||
import titleBg2 from '@/assets/imgs/user/title-bg2.webp'
|
||||
import _defaultTitleBg from '@/assets/imgs/user/title-bg.webp'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
|
||||
import ActionButton from './ActionButton.vue'
|
||||
@ -29,7 +29,7 @@ interface Props {
|
||||
btnTheme?: 'primary' | 'light'
|
||||
/** 是否禁用下一步按钮 */
|
||||
nextDisabled?: boolean
|
||||
/** 标题区域背景图片URL,不传则使用默认 title-bg.svg */
|
||||
/** 标题区域背景图片URL,不传则使用默认 title-bg.webp */
|
||||
titleBgType?: number
|
||||
/** 是否显示第二个下一步按钮 */
|
||||
showNextBtn2?: boolean
|
||||
@ -218,11 +218,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.scroll-bg {
|
||||
width: 412px !important; /* Fixed width to prevent squishing during animation */
|
||||
height: 165px !important;
|
||||
width: 400px !important; /* Fixed width to prevent squishing during animation */
|
||||
height: 155px !important;
|
||||
position: relative;
|
||||
background: no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
background-size: contain;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@ -344,7 +344,7 @@ onMounted(async () => {
|
||||
.card-back-design {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('@/assets/imgs/user/cover-back.svg') no-repeat center center; // 背景面
|
||||
background: url('@/assets/imgs/user/cover-back.webp') no-repeat center center; // 背景面
|
||||
background-size: contain; // 保持比例
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import drawImg from '@/assets/imgs/user/draw.svg'
|
||||
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'
|
||||
@ -169,7 +169,7 @@ const questionTitle = computed(() => question.value?.QuestionList?.QuestionConte
|
||||
}}</span>
|
||||
</div> -->
|
||||
<!-- 禁用放大 -->
|
||||
<NImage :src="drawImg" class="h-full w-full object-cover" :preview-disabled="true" />
|
||||
<NImage :src="drawImg" class="h-full w-full object-cover" width="300" height="300" :preview-disabled="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -228,7 +228,16 @@ async function fetchGetGameStatisticsData(_params: Api.Competition.QuestionAddPa
|
||||
correct: item.Points || 0, // 预估得分 (使用外层 Points)
|
||||
})
|
||||
})
|
||||
chartData.value = Array.from(teamMap.values())
|
||||
|
||||
const newChartData = Array.from(teamMap.values())
|
||||
|
||||
// 只有数据真正变化时才更新
|
||||
const currentDataStr = JSON.stringify(chartData.value)
|
||||
const newDataStr = JSON.stringify(newChartData)
|
||||
|
||||
if (currentDataStr !== newDataStr) {
|
||||
chartData.value = newChartData
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts/core'
|
||||
import { watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { type ECOption, useEcharts } from '@/hooks/common/echarts'
|
||||
|
||||
@ -15,6 +15,21 @@ const props = defineProps<{
|
||||
height?: number
|
||||
}>()
|
||||
|
||||
// 缓存上一次的数据,用于深度比较
|
||||
const lastDataCache = ref<string>('')
|
||||
|
||||
/**
|
||||
* 深度比较数据是否发生变化
|
||||
*/
|
||||
function isDataChanged(newData: BarDatum[]): boolean {
|
||||
const newDataStr = JSON.stringify(newData)
|
||||
if (newDataStr === lastDataCache.value) {
|
||||
return false
|
||||
}
|
||||
lastDataCache.value = newDataStr
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建柱状图配置
|
||||
* 说明:根据传入的数据生成双系列柱状图(答题数量/正确数量)
|
||||
@ -25,6 +40,8 @@ function buildOption(list: BarDatum[]): ECOption {
|
||||
const correct = list.map(i => i.correct)
|
||||
|
||||
return {
|
||||
// 禁用动画,避免频繁刷新时的抖动
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
@ -113,12 +130,15 @@ const { domRef, updateOptions } = useEcharts<ECOption>(() => buildOption(props.d
|
||||
|
||||
/**
|
||||
* 监听数据变化并更新图表
|
||||
* 说明:当父组件 chartData 更新时,动态刷新柱状图
|
||||
* 说明:只有当数据真正发生变化时才更新图表,避免不必要的重绘
|
||||
*/
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
updateOptions(() => buildOption(val || []))
|
||||
(newData) => {
|
||||
// 只有数据真正变化时才更新
|
||||
if (isDataChanged(newData || [])) {
|
||||
updateOptions(() => buildOption(newData || []))
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import groupType1 from '@/assets/imgs/user/group-type-1.svg'
|
||||
import groupType2 from '@/assets/imgs/user/group-type-2.svg'
|
||||
import startIcon from '@/assets/imgs/user/star-icon.svg'
|
||||
import startIcon from '@/assets/imgs/user/star-icon.webp'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { fetchGetGroupListByActivityID } from '@/service/api/game'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { NCarousel, NImage } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import beacon from '@/assets/imgs/user/event-card-bg-icon.svg'
|
||||
import beacon from '@/assets/imgs/user/event-card-bg-icon.webp'
|
||||
import CompetitionLayout from '@/components/custom/user/CompetitionLayout.vue'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
@ -156,7 +156,7 @@ onMounted(() => {
|
||||
.event-card {
|
||||
width: 340px;
|
||||
height: 480px;
|
||||
background: url('@/assets/imgs/user/event-card-bg.svg') no-repeat center center;
|
||||
background: url('@/assets/imgs/user/event-card-bg.webp') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 8px 8px 40px 40px;
|
||||
position: relative;
|
||||
|
||||
@ -177,7 +177,7 @@ function rowClassName(row: TeamData) {
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url('@/assets/imgs/user/team-bg.svg') no-repeat center center;
|
||||
background: url('@/assets/imgs/user/team-bg.webp') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
transform-origin: center;
|
||||
padding: 40px 20px;
|
||||
|
||||
@ -111,7 +111,7 @@ onMounted(() => {
|
||||
:style="{ '--delay': `${index * 0.4}s` }" @click="selectTeam(item.id)"
|
||||
>
|
||||
<div class="icon-wrapper">
|
||||
<img src="@/assets/imgs/user/team-title-icon.svg" alt="team-icon" class="h-full w-full object-cover">
|
||||
<img src="@/assets/imgs/user/team-title-icon.webp" alt="team-icon" class="h-full w-full object-cover">
|
||||
</div>
|
||||
<div class="team-name">
|
||||
{{ item.name }}
|
||||
@ -175,10 +175,21 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 浮动动画关键帧
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.title-section {
|
||||
position: absolute;
|
||||
top: 18%; // 使用百分比定位,适应不同比例
|
||||
left: 7%; // 微调位置,根据 1920x1080 下的视觉效果调整
|
||||
top: 130px; // 使用固定像素值,确保在卷轴内部
|
||||
left: 40px; // 距离左边缘的固定距离
|
||||
z-index: 10;
|
||||
|
||||
.scroll-bg {
|
||||
@ -193,6 +204,38 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加响应式适配
|
||||
@media (max-width: 1920px) {
|
||||
.title-section {
|
||||
top: 70px;
|
||||
left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.title-section {
|
||||
top: 60px;
|
||||
left: 40px;
|
||||
|
||||
.scroll-bg .main-title {
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.title-section {
|
||||
top: 50px;
|
||||
left: 30px;
|
||||
|
||||
.scroll-bg .main-title {
|
||||
font-size: 2rem;
|
||||
letter-spacing: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.competition-layout :deep(.main-content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -217,10 +260,12 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url('@/assets/imgs/user/team-bg-animation.svg') no-repeat center center;
|
||||
background: url('@/assets/imgs/user/team-bg-animation.webp') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
transform-origin: center; // 确保从中心展开
|
||||
will-change: clip-path; // 优化性能
|
||||
will-change: clip-path, transform; // 优化性能
|
||||
animation: float 3s ease-in-out infinite; // 添加浮动动画
|
||||
padding: 60px 80px; // 添加内边距,确保内容在卷轴内部
|
||||
|
||||
&::before {
|
||||
left: -15px;
|
||||
@ -232,11 +277,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
width: 80%; // 减小宽度,为标题留出空间
|
||||
height: 70%; // 调整高度
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// margin-left: 120px; // 为左侧标题留出空间
|
||||
}
|
||||
|
||||
.teams-grid {
|
||||
|
||||