chore(admin): 优化PWA配置并更新资源文件

- 增强PWA开发环境支持,添加devOptions配置以便调试
- 优化Workbox缓存策略,排除大型SVG文件预缓存
- 添加运行时缓存规则支持SVG、图片等资源的CacheFirst策略
- 更新PWA manifest图标路径,使用public文件夹中的新图标资源
- 添加Service Worker注册文件和Workbox库文件
- 替换favicon.svg为favicon.ico,新增多种尺寸的PWA图标
- 生成PWA构建报告文件
- 优化manifest描述信息
This commit is contained in:
2026-03-16 10:55:53 +08:00
parent 40a6850c3f
commit 782c61a38c
15 changed files with 5121 additions and 91 deletions

View File

@ -3,9 +3,9 @@
# VITE_SERVICE_BASE_URL=http://192.168.5.140:9999
# 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_SERVICE_BASE_URL=https://172.16.10.130:5000
VITE_OTHER_SERVICE_BASE_URL= `{
"demo": "http://localhost:9528"

View File

@ -1,11 +1,24 @@
import type { PluginOption } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export function setupPWA() {
export function setupPWA(): PluginOption {
return VitePWA({
registerType: 'autoUpdate',
// 添加 devOptions 以便在开发环境调试
devOptions: {
enabled: false, // 开发环境启用
type: 'module',
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,jpg,jpeg}'],
// 排除大型 SVG 文件,避免预缓存
globIgnores: [
'**/team-title-bg*.svg',
'**/cover-bg-active*.svg',
'**/*-bg-animation*.svg',
],
globPatterns: ['**/*.{js,css,html,ico,png,webp,jpg,jpeg}', '**/*.svg'],
runtimeCaching: [
// 字体缓存
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
@ -17,34 +30,46 @@ export function setupPWA() {
},
},
},
// 大型 SVG 文件运行时缓存
{
urlPattern: /.*\.(svg)$/,
handler: 'CacheFirst',
options: {
cacheName: 'svg-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
},
},
},
// 图片资源缓存
{
urlPattern: /.*\.(png|jpg|jpeg|webp|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
},
},
},
],
},
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
manifest: {
name: '阅读之星竞赛系统',
short_name: '阅读之星',
description: '教育学会树人研究院阅读之星竞赛系统',
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',
},
{ src: 'images/pwa_logo_192.png', sizes: '192x192', type: 'image/png' },
{ src: 'images/pwa_logo_512.png', sizes: '512x512', type: 'image/png' },
{ src: 'images/pwa_logo_512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
{ src: 'images/pwa_logo_512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
],
},
})

View File

@ -2,7 +2,7 @@
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>%VITE_APP_TITLE%</title>

View File

@ -16,7 +16,7 @@
"dev": "vite --mode dev",
"dev:prod": "vite --mode prod",
"gen-route": "sa gen-route",
"lint": "eslint . --fix",
"lint": "eslint src --fix",
"prepare": "simple-git-hooks",
"preview": "vite preview",
"release": "sa release",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,60 +0,0 @@
<svg width="100%" height="100%" version="1.1" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<path
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none" />
<path
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient_2)" fill-rule="nonzero" opacity="1" stroke="none" />
<path d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
fill="url(#LinearGradient_3)" fill-rule="nonzero" opacity="1" stroke="none" />
<path d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
fill="url(#LinearGradient_4)" fill-rule="nonzero" opacity="1" stroke="none" />
<path d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
fill="url(#LinearGradient_5)" fill-rule="nonzero" opacity="1" stroke="none" />
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
fill="url(#LinearGradient_6)" fill-rule="nonzero" opacity="1" stroke="none" />
<path
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
fill="url(#LinearGradient_7)" fill-rule="nonzero" opacity="1" stroke="none" />
</g>
<defs>
<linearGradient gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
gradientUnits="userSpaceOnUse" id="LinearGradient" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#373ebf" />
<stop offset="1" stop-color="#5058e6" />
</linearGradient>
<linearGradient gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
gradientUnits="userSpaceOnUse" id="LinearGradient_2" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#c2d6ff" />
<stop offset="1" stop-color="#646cff" />
</linearGradient>
<linearGradient gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
gradientUnits="userSpaceOnUse" id="LinearGradient_3" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#5058e6" />
<stop offset="1" stop-color="#373ebf" />
</linearGradient>
<linearGradient gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
gradientUnits="userSpaceOnUse" id="LinearGradient_4" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#91a7ff" />
<stop offset="1" stop-color="#5058e6" />
</linearGradient>
<linearGradient gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
gradientUnits="userSpaceOnUse" id="LinearGradient_5" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#646cff" />
<stop offset="1" stop-color="#c2d6ff" />
</linearGradient>
<linearGradient gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
gradientUnits="userSpaceOnUse" id="LinearGradient_6" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#c2d6ff" />
<stop offset="1" stop-color="#646cff" />
</linearGradient>
<linearGradient gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
gradientUnits="userSpaceOnUse" id="LinearGradient_7" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#373ebf" />
<stop offset="1" stop-color="#5058e6" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

4950
apps/admin/report.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import VScaleScreen from 'v-scale-screen'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
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'
@ -67,6 +67,9 @@ const titleBgUrlMapping: Record<number, string> = {
const { routerPushByKey } = useRouterPush()
const titleBgUrl = computed(() => titleBgUrlMapping[props.titleBgType] || _defaultBg)
const titleBgLoaded = ref(false)
const currentTitleBg = ref('')
const imageLoadStatus = ref<Record<string, boolean>>({})
// const router = useRouter()
const innerBgUrl = ref(_defaultBg)
@ -110,10 +113,62 @@ function handleBackHome() {
}
onMounted(() => {
fetchSystemConfig()
setTimeout(() => {
showContent.value = true
}, 100)
// 预加载所有标题背景图,并跟踪加载状态
const preloadAllImages = () => {
const promises = Object.entries(titleBgUrlMapping).map(([_key, url]) => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
imageLoadStatus.value[url] = true
resolve(true)
}
img.onerror = () => {
imageLoadStatus.value[url] = false
resolve(false)
}
img.src = url
})
})
// 也预加载默认背景
const defaultBgPromise = new Promise((resolve) => {
const img = new Image()
img.onload = () => {
imageLoadStatus.value[_defaultBg] = true
resolve(true)
}
img.onerror = () => {
imageLoadStatus.value[_defaultBg] = false
resolve(false)
}
img.src = _defaultBg
})
return Promise.all([...promises, defaultBgPromise])
}
// 开始预加载
preloadAllImages()
.then(() => {
// 所有图片预加载完成后,设置当前背景
currentTitleBg.value = titleBgUrl.value
titleBgLoaded.value = true
fetchSystemConfig()
setTimeout(() => {
showContent.value = true
}, 100)
})
.catch(() => {
// 即使预加载失败也继续
currentTitleBg.value = titleBgUrl.value
titleBgLoaded.value = true
fetchSystemConfig()
setTimeout(() => {
showContent.value = true
}, 100)
})
// 延迟显示内容,等待标题卷轴展开
const delay = props.showTitle ? 500 : 100
@ -126,6 +181,28 @@ onMounted(() => {
showButton.value = true
}, delay + 500)
})
// 监听 titleBgUrl 变化,确保图片切换平滑
watch(titleBgUrl, (newUrl) => {
if (newUrl && imageLoadStatus.value[newUrl]) {
// 如果图片已经加载过,直接切换
currentTitleBg.value = newUrl
}
else if (newUrl) {
// 如果图片还没加载,预加载它
const img = new Image()
img.onload = () => {
imageLoadStatus.value[newUrl] = true
currentTitleBg.value = newUrl
}
img.onerror = () => {
imageLoadStatus.value[newUrl] = false
// 加载失败时使用默认背景
currentTitleBg.value = _defaultBg
}
img.src = newUrl
}
})
</script>
<template>
@ -146,7 +223,22 @@ onMounted(() => {
<section v-if="showTitle" class="title-section">
<Transition name="scroll-unfold" appear>
<div v-if="showContent" class="title-wrapper">
<div class="scroll-bg" :style="{ backgroundImage: `url(${titleBgUrl})` }">
<div class="scroll-bg">
<!-- 预渲染所有背景图 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"
>
<Transition name="fade-in-delay" appear>
<h1 v-if="showContent" class="main-title">
{{ title }}
@ -221,14 +313,33 @@ onMounted(() => {
width: 400px !important; /* Fixed width to prevent squishing during animation */
height: 155px !important;
position: relative;
background: no-repeat center center;
background-size: contain;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 10px;
flex-shrink: 0; /* Prevent shrinking */
.scroll-bg-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
&.active {
opacity: 1;
}
&.loading {
opacity: 0;
filter: blur(2px);
}
}
.main-title {
font-size: 2.5rem;
color: $text-color;
@ -236,6 +347,8 @@ onMounted(() => {
letter-spacing: 4px;
text-align: center;
white-space: nowrap; /* Prevent text wrapping */
position: relative;
z-index: 1;
}
}
}

View File

@ -11,6 +11,7 @@ export default readStarConfig(
'**/public/**',
'pnpm-lock.yaml',
'**/*.md',
'**/report.html',
],
},
{