chore(admin): 优化构建配置并升级资源格式

- 新增构建插件:打包分析、压缩、自动部署、图片优化和PWA支持
- 将SVG资源转换为WebP格式以提升加载性能
- 添加新的背景图片资源用于用户界面优化
- 更新环境配置:调整基础URL和服务地址
- 优化用户界面组件以适配新的资源格式
- 更新package.json依赖以支持新的构建功能
- 改进构建流程以支持自动部署和性能监控
This commit is contained in:
2026-03-13 17:52:13 +08:00
parent 9d801ba6b4
commit 40a6850c3f
42 changed files with 2310 additions and 288 deletions

View File

@ -1,6 +1,6 @@
# 应用的基础URL默认为 "/"
# 如果使用子目录,必须以 "/" 结尾,例如 "/admin/" 而不是 "/admin"
# VITE_BASE_URL='/admin'
VITE_BASE_URL='/'
# VITE_BASE_URL="https://api.qyzhjy.com"

View File

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

View File

@ -0,0 +1,11 @@
import type { PluginOption } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export function setupAnalyzer() {
return visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'report.html',
}) as PluginOption
}

View File

@ -0,0 +1,11 @@
import viteCompression from 'vite-plugin-compression'
export function setupCompression() {
return viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz',
})
}

View File

@ -0,0 +1,21 @@
import { viteAutoDeployPlugin } from 'vite-auto-deploy'
import { name } from '../../package.json'
export function setupDeploy(isBuild: boolean) {
return viteAutoDeployPlugin({
/** 上传地址 */
uploadUrl: 'http://47.109.17.238:8200/api/upload/code/deploy',
/** 项目名称 (如果多个项目都是用的同上传地址时,服务端可用此字段区分是哪一个项目) */
projectKey: name,
/** 上传完成后,是否保留压缩包到本地 */
saveLocalZip: true,
/** 是否开启自动部署 */
autoDeploy: isBuild,
headers: {
'Custom-Timestamp': new Date().getTime().toString(),
'Custom-Run-Platform': 'app',
'Custom-Device-Id': Math.random().toString(36).substring(2),
'Custom-Os-Name': 'windows',
},
})
}

View File

@ -0,0 +1,51 @@
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
export function setupImageOptimizer() {
return ViteImageOptimizer({
logStats: true,
// SVG 优化配置
svg: {
multipass: true,
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
cleanupIds: false,
},
},
},
'sortAttrs',
],
},
// PNG 高质量压缩
png: {
quality: 90,
compressionLevel: 9,
},
// JPEG 优化
jpeg: {
quality: 85,
mozjpeg: true,
},
jpg: {
quality: 85,
mozjpeg: true,
},
// WebP 转换
webp: {
quality: 80,
lossless: false,
force: true,
},
includePublic: true,
cache: true,
cacheLocation: 'node_modules/.cache/image-optimizer',
})
}

View File

@ -2,14 +2,20 @@ import type { PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import progress from 'vite-plugin-progress'
import { setupAnalyzer } from './analyzer'
import { setupCompression } from './compression'
import { setupDeploy } from './deploy'
import { setupDevtoolsPlugin } from './devtools'
import { setupHtmlPlugin } from './html'
import { setupImageOptimizer } from './image-optimizer'
import { setupPWA } from './pwa'
import { setupElegantRouter } from './router'
import { setupUnocss } from './unocss'
import { setupUnplugin } from './unplugin'
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string, isBuild: boolean) {
const plugins: PluginOption = [
// 基础插件
vue(),
vueJsx(),
setupDevtoolsPlugin(viteEnv),
@ -18,7 +24,18 @@ export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
...setupUnplugin(viteEnv),
progress(),
setupHtmlPlugin(buildTime),
// 优化插件
setupImageOptimizer(),
setupCompression(),
setupPWA(),
setupDeploy(isBuild),
]
// 构建时添加打包分析插件
if (isBuild) {
plugins.push(setupAnalyzer())
}
return plugins
}

View File

@ -0,0 +1,51 @@
import { VitePWA } from 'vite-plugin-pwa'
export function setupPWA() {
return VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,jpg,jpeg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
},
},
],
},
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
manifest: {
name: '阅读之星竞赛系统',
short_name: '阅读之星',
description: '教育学会树人研究院阅读之星竞赛系统',
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
orientation: 'landscape',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
},
})
}

View File

@ -82,6 +82,7 @@
"eslint": "9.39.2",
"eslint-plugin-vue": "10.6.2",
"kolorist": "1.8.0",
"rollup-plugin-visualizer": "^7.0.1",
"sass": "1.97.1",
"simple-git-hooks": "2.13.1",
"tsx": "4.21.0",
@ -90,7 +91,10 @@
"unplugin-vue-components": "30.0.0",
"vite": "7.3.0",
"vite-auto-deploy": "^2.0.34",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-image-optimizer": "^2.0.3",
"vite-plugin-progress": "0.0.7",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "8.0.5",
"vue-eslint-parser": "10.2.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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) {

View File

@ -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 },
)

View File

@ -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'

View File

@ -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;

View File

@ -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;

View File

@ -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 {

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ export default defineConfig((configEnv) => {
},
},
},
plugins: setupVitePlugins(viteEnv, buildTime),
plugins: setupVitePlugins(viteEnv, buildTime, configEnv.command === 'build'),
define: {
BUILD_TIME: JSON.stringify(buildTime),
},

2083
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff