mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-03-21 09:13:10 +00:00
重构代码,优化组件加载和样式,移除不必要的依赖,增强首屏渲染性能
This commit is contained in:
parent
97c70f6706
commit
cf3412db6a
67
index.html
67
index.html
@ -9,73 +9,10 @@
|
|||||||
<link rel="apple-touch-icon" href="/image/apple-touch-icon.png" sizes="180x180" />
|
<link rel="apple-touch-icon" href="/image/apple-touch-icon.png" sizes="180x180" />
|
||||||
<link rel="mask-icon" href="/image/mask-icon.svg" color="#212121" />
|
<link rel="mask-icon" href="/image/mask-icon.svg" color="#212121" />
|
||||||
<meta name="theme-color" content="#212121" />
|
<meta name="theme-color" content="#212121" />
|
||||||
<style>
|
|
||||||
/* Material 3 风格:纯 CSS 加载覆盖层 */
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
/* 作为主色的近似值,后续由应用接管主题 */
|
|
||||||
--md3-primary: #6750A4; /* light primary */
|
|
||||||
--md3-primary-dark: #D0BCFF; /* dark primary */
|
|
||||||
--loader-bg: #ffffff;
|
|
||||||
--loader-fg: var(--md3-primary);
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--loader-bg: #121212;
|
|
||||||
--loader-fg: var(--md3-primary-dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#app-loader {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 2147483647; /* 确保在最上层 */
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: var(--loader-bg);
|
|
||||||
transition: opacity .2s ease;
|
|
||||||
}
|
|
||||||
#app-loader .md3-loader {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
color: var(--loader-fg);
|
|
||||||
}
|
|
||||||
/* 圆形不确定进度条(近似 M3) */
|
|
||||||
#app-loader .spinner {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
/* 通过 conic-gradient 形成 90° 弧,并旋转实现不确定动画 */
|
|
||||||
background:
|
|
||||||
conic-gradient(from 0deg, currentColor 0 90deg, transparent 90deg 360deg);
|
|
||||||
/* 用 mask 形成环形厚度(4px) */
|
|
||||||
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
|
|
||||||
mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
|
|
||||||
animation: md3-spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes md3-spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
#app-loader .label {
|
|
||||||
font: 500 14px/1.2 system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
|
|
||||||
color: var(--loader-fg);
|
|
||||||
letter-spacing: .2px;
|
|
||||||
opacity: .85;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
/* 当被移除或隐藏时可渐隐(由应用控制) */
|
|
||||||
body.app-loaded #app-loader { opacity: 0; pointer-events: none; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- 应用加载前显示的覆盖层:纯 CSS,无脚本依赖 -->
|
|
||||||
<div id="app-loader" aria-live="polite" aria-busy="true">
|
|
||||||
<div class="md3-loader">
|
|
||||||
<div class="spinner" role="progressbar" aria-label="正在加载"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="display: none;">xICP备x号-4</a>
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="display: none;">xICP备x号-4</a>
|
||||||
|
|||||||
@ -19,9 +19,7 @@
|
|||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"js-base64": "^3.7.8",
|
"js-base64": "^3.7.8",
|
||||||
"js-yaml": "^4.1.1",
|
|
||||||
"lucide-vue-next": "^0.555.0",
|
"lucide-vue-next": "^0.555.0",
|
||||||
"marked": "^17.0.1",
|
|
||||||
"pinyin-pro": "^3.27.0",
|
"pinyin-pro": "^3.27.0",
|
||||||
"ratelimit-header-parser": "^0.1.0",
|
"ratelimit-header-parser": "^0.1.0",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -32,15 +32,9 @@ importers:
|
|||||||
js-base64:
|
js-base64:
|
||||||
specifier: ^3.7.8
|
specifier: ^3.7.8
|
||||||
version: 3.7.8
|
version: 3.7.8
|
||||||
js-yaml:
|
|
||||||
specifier: ^4.1.1
|
|
||||||
version: 4.1.1
|
|
||||||
lucide-vue-next:
|
lucide-vue-next:
|
||||||
specifier: ^0.555.0
|
specifier: ^0.555.0
|
||||||
version: 0.555.0(vue@3.5.25(typescript@5.9.3))
|
version: 0.555.0(vue@3.5.25(typescript@5.9.3))
|
||||||
marked:
|
|
||||||
specifier: ^17.0.1
|
|
||||||
version: 17.0.1
|
|
||||||
pinyin-pro:
|
pinyin-pro:
|
||||||
specifier: ^3.27.0
|
specifier: ^3.27.0
|
||||||
version: 3.27.0
|
version: 3.27.0
|
||||||
@ -2186,6 +2180,7 @@ packages:
|
|||||||
glob@11.1.0:
|
glob@11.1.0:
|
||||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
globals@14.0.0:
|
globals@14.0.0:
|
||||||
@ -2552,11 +2547,6 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
marked@17.0.1:
|
|
||||||
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -6261,8 +6251,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
marked@17.0.1: {}
|
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|||||||
@ -32,6 +32,11 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
/* 全局样式(从 index.vue 迁移,确保全局可用且仅加载一次) */
|
||||||
|
@import "@/styles/index.scss";
|
||||||
|
@import "@/styles/transitions.scss";
|
||||||
|
@import "@/styles/global.scss";
|
||||||
|
|
||||||
.md3-enter-active,
|
.md3-enter-active,
|
||||||
.md3-leave-active {
|
.md3-leave-active {
|
||||||
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
|||||||
@ -242,7 +242,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { pinyin } from "pinyin-pro";
|
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
import { getSetting } from "@/utils/settings";
|
import { getSetting } from "@/utils/settings";
|
||||||
|
|
||||||
@ -328,11 +327,7 @@ export default {
|
|||||||
|
|
||||||
return Array.from(surnameMap.entries())
|
return Array.from(surnameMap.entries())
|
||||||
.map(([name, count]) => ({ name, count }))
|
.map(([name, count]) => ({ name, count }))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||||
const pinyinA = pinyin(a.name, { toneType: "none" });
|
|
||||||
const pinyinB = pinyin(b.name, { toneType: "none" });
|
|
||||||
return pinyinA.localeCompare(pinyinB);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
21
src/components/common/AsyncLoadingPlaceholder.vue
Normal file
21
src/components/common/AsyncLoadingPlaceholder.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="async-loading-placeholder d-flex justify-center align-center" :style="{ minHeight: height }">
|
||||||
|
<v-progress-circular indeterminate color="primary" size="28" width="2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '120px'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.async-loading-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/components/common/HomeSkeleton.vue
Normal file
71
src/components/common/HomeSkeleton.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 纯 CSS 骨架屏,避免使用 VSkeletonLoader 引起的渲染冲突 -->
|
||||||
|
<v-container class="main-window" fluid>
|
||||||
|
<div class="skeleton-grid">
|
||||||
|
<div
|
||||||
|
v-for="n in cardCount"
|
||||||
|
:key="n"
|
||||||
|
class="skeleton-card"
|
||||||
|
>
|
||||||
|
<div class="skeleton-heading skeleton-pulse" />
|
||||||
|
<div class="skeleton-line skeleton-pulse" />
|
||||||
|
<div class="skeleton-line skeleton-line--short skeleton-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-center mt-6 ga-3">
|
||||||
|
<div class="skeleton-btn skeleton-pulse" />
|
||||||
|
<div class="skeleton-btn skeleton-pulse" />
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
const { mobile } = useDisplay()
|
||||||
|
const cardCount = computed(() => mobile.value ? 3 : 6)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.skeleton-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.skeleton-card {
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity, 0.12));
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.skeleton-heading {
|
||||||
|
height: 24px;
|
||||||
|
width: 60%;
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.skeleton-line {
|
||||||
|
height: 14px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.skeleton-line--short {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
.skeleton-btn {
|
||||||
|
height: 36px;
|
||||||
|
width: 100px;
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.skeleton-pulse {
|
||||||
|
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes skeleton-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -218,8 +218,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import UnsavedWarning from "../common/UnsavedWarning.vue";
|
import UnsavedWarning from "../common/UnsavedWarning.vue";
|
||||||
import "@/styles/warnings.scss";
|
import "@/styles/warnings.scss";
|
||||||
import {pinyin} from "pinyin-pro";
|
|
||||||
import dataProvider from "@/utils/dataProvider";
|
import dataProvider from "@/utils/dataProvider";
|
||||||
|
|
||||||
|
// pinyin-pro (~100KB) 按需动态加载
|
||||||
|
let _pinyin = null;
|
||||||
|
async function loadPinyin() {
|
||||||
|
if (!_pinyin) {
|
||||||
|
_pinyin = (await import('pinyin-pro')).pinyin;
|
||||||
|
}
|
||||||
|
return _pinyin;
|
||||||
|
}
|
||||||
import {getSetting} from "@/utils/settings";
|
import {getSetting} from "@/utils/settings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -440,10 +448,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sortStudentsByPinyin() {
|
async sortStudentsByPinyin() {
|
||||||
|
const pinyinFn = await loadPinyin();
|
||||||
const sorted = [...this.modelValue.list].sort((a, b) => {
|
const sorted = [...this.modelValue.list].sort((a, b) => {
|
||||||
const pinyinA = pinyin(a.name, {toneType: "none"});
|
const pinyinA = pinyinFn(a.name, {toneType: "none"});
|
||||||
const pinyinB = pinyin(b.name, {toneType: "none"});
|
const pinyinB = pinyinFn(b.name, {toneType: "none"});
|
||||||
return pinyinA.localeCompare(pinyinB);
|
return pinyinA.localeCompare(pinyinB);
|
||||||
});
|
});
|
||||||
sorted.forEach((s, i) => s.id = i + 1);
|
sorted.forEach((s, i) => s.id = i + 1);
|
||||||
|
|||||||
@ -21,8 +21,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Typewriter from "typewriter-effect/dist/core";
|
|
||||||
import quotes from "@/data/echoChamber.json";
|
import quotes from "@/data/echoChamber.json";
|
||||||
|
|
||||||
|
// typewriter-effect 按需动态加载
|
||||||
|
let TypewriterClass = null;
|
||||||
|
async function getTypewriter() {
|
||||||
|
if (!TypewriterClass) {
|
||||||
|
TypewriterClass = (await import('typewriter-effect/dist/core')).default;
|
||||||
|
}
|
||||||
|
return TypewriterClass;
|
||||||
|
}
|
||||||
import SettingsCard from "@/components/SettingsCard.vue";
|
import SettingsCard from "@/components/SettingsCard.vue";
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
const INITIAL_STATE = {
|
||||||
@ -50,7 +58,8 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
initTypewriters() {
|
async initTypewriters() {
|
||||||
|
const Typewriter = await getTypewriter();
|
||||||
this.typewriter = new Typewriter(this.$refs.typewriter, TYPEWRITER_CONFIG.main);
|
this.typewriter = new Typewriter(this.$refs.typewriter, TYPEWRITER_CONFIG.main);
|
||||||
this.sourceWriter = new Typewriter(this.$refs.sourceWriter, TYPEWRITER_CONFIG.source);
|
this.sourceWriter = new Typewriter(this.$refs.sourceWriter, TYPEWRITER_CONFIG.source);
|
||||||
this.typeQuote(INITIAL_STATE);
|
this.typeQuote(INITIAL_STATE);
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view/>
|
<router-view />
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-app>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
210
src/main.js
210
src/main.js
@ -1,207 +1,61 @@
|
|||||||
/**
|
/**
|
||||||
* main.js
|
* main.js
|
||||||
*
|
*
|
||||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
* 精简启动流水线:快速挂载 Vue app,重型依赖(Sentry/Clarity)异步加载
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Plugins
|
// 核心插件(Vuetify / Router / Pinia)
|
||||||
import {registerPlugins} from '@/plugins'
|
import { registerPlugins } from '@/plugins'
|
||||||
import {createPinia} from 'pinia'
|
|
||||||
import router from './router'
|
|
||||||
|
|
||||||
const pinia = createPinia()
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import GlobalMessage from '@/components/GlobalMessage.vue'
|
import GlobalMessage from '@/components/GlobalMessage.vue'
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
import {createApp} from 'vue'
|
import { createApp } from 'vue'
|
||||||
//import TDesign from 'tdesign-vue-next'
|
|
||||||
//import 'tdesign-vue-next/es/style/index.css'
|
|
||||||
//import '@examaware-cs/player/dist/player.css'
|
|
||||||
|
|
||||||
import messageService from './utils/message';
|
import messageService from './utils/message'
|
||||||
import { getVisitorId } from './utils/visitorId';
|
|
||||||
|
|
||||||
import * as Sentry from "@sentry/vue";
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
// 保存 feedback integration 实例的引用
|
|
||||||
let feedbackIntegration = null;
|
|
||||||
|
|
||||||
// 初始化 Sentry,但暂不启用 Replay
|
|
||||||
Sentry.init({
|
|
||||||
app,
|
|
||||||
dsn: "https://dc34ab47426f49c0925445f0d87b7007@report.houlang.cloud/6",
|
|
||||||
// Setting this option to true will send default PII data to Sentry.
|
|
||||||
// For example, automatic IP address collection on events
|
|
||||||
sendDefaultPii: true,
|
|
||||||
integrations: [
|
|
||||||
Sentry.browserTracingIntegration({ router }),
|
|
||||||
Sentry.replayIntegration({
|
|
||||||
// 默认不自动录制,只在手动触发或发生错误时录制
|
|
||||||
maskAllText: false,
|
|
||||||
blockAllMedia: false,
|
|
||||||
}),
|
|
||||||
feedbackIntegration = Sentry.feedbackIntegration({
|
|
||||||
// 自动注入反馈按钮,但我们会手动触发
|
|
||||||
autoInject: false,
|
|
||||||
colorScheme: 'system',
|
|
||||||
showBranding: false,
|
|
||||||
showName: true,
|
|
||||||
showEmail: true,
|
|
||||||
isNameRequired: false,
|
|
||||||
isEmailRequired: false,
|
|
||||||
useSentryUser: {
|
|
||||||
name: 'username',
|
|
||||||
email: 'email',
|
|
||||||
},
|
|
||||||
themeDark: {
|
|
||||||
submitBackground: '#6200EA',
|
|
||||||
submitBackgroundHover: '#7C4DFF',
|
|
||||||
},
|
|
||||||
themeLight: {
|
|
||||||
submitBackground: '#6200EA',
|
|
||||||
submitBackgroundHover: '#7C4DFF',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
],
|
|
||||||
// Tracing
|
|
||||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
|
||||||
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
|
|
||||||
tracePropagationTargets: ["localhost", /^https?:\/\/cs\.(houlang\.cloud|houlangs\.com)/],
|
|
||||||
// Session Replay - 关闭自动录制
|
|
||||||
replaysSessionSampleRate: 0, // 不自动录制会话
|
|
||||||
replaysOnErrorSampleRate: 0, // 不在错误时自动录制(改为手动触发)
|
|
||||||
// Logs
|
|
||||||
enableLogs: true,
|
|
||||||
// 在初始化时设置用户识别的钩子
|
|
||||||
beforeSend(event) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 异步设置用户 fingerprint
|
|
||||||
getVisitorId().then(visitorId => {
|
|
||||||
Sentry.setUser({
|
|
||||||
id: visitorId,
|
|
||||||
username: visitorId,
|
|
||||||
});
|
|
||||||
Sentry.setTag('fingerprint', visitorId);
|
|
||||||
console.log('Sentry 用户标识已设置:', visitorId);
|
|
||||||
}).catch(error => {
|
|
||||||
console.warn('设置 Sentry 用户标识失败:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导出用于手动打开反馈表单的函数
|
|
||||||
window.openSentryFeedback = () => {
|
|
||||||
try {
|
|
||||||
if (!feedbackIntegration) {
|
|
||||||
console.warn('Sentry Feedback integration 未初始化');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof feedbackIntegration.createWidget === 'function') {
|
|
||||||
const widget = feedbackIntegration.createWidget();
|
|
||||||
if (widget && typeof widget.open === 'function') {
|
|
||||||
widget.open();
|
|
||||||
console.log('Sentry Feedback 对话框已打开');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof feedbackIntegration.openDialog === 'function') {
|
|
||||||
feedbackIntegration.openDialog();
|
|
||||||
console.log('Sentry Feedback 对话框已打开');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn('无法找到打开 Feedback 的方法');
|
|
||||||
console.log('可用方法:', Object.keys(feedbackIntegration));
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('打开 Sentry Feedback 时出错:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导出用于手动启动录制的函数
|
|
||||||
window.startSentryReplay = () => {
|
|
||||||
try {
|
|
||||||
const client = Sentry.getClient();
|
|
||||||
if (!client) {
|
|
||||||
console.warn('Sentry 客户端未初始化');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 Replay integration 实例
|
|
||||||
const integrations = client.getOptions().integrations || [];
|
|
||||||
const replayIntegration = integrations.find(
|
|
||||||
integration => integration && integration.name === 'Replay'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (replayIntegration && typeof replayIntegration.start === 'function') {
|
|
||||||
replayIntegration.start();
|
|
||||||
console.log('Sentry Replay 已手动启动');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn('无法找到 Sentry Replay integration');
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('启动 Sentry Replay 时出错:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
registerPlugins(app)
|
registerPlugins(app)
|
||||||
//app.use(TDesign)
|
app.use(messageService)
|
||||||
app.use(messageService);
|
|
||||||
app.use(pinia)
|
|
||||||
|
|
||||||
app.component('GlobalMessage', GlobalMessage)
|
app.component('GlobalMessage', GlobalMessage)
|
||||||
|
|
||||||
|
// 挂载 Vue app(首要目标:尽快渲染首屏)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
// 异步加载 Clarity 以提升初始加载速度
|
// ====== 以下全部异步,不阻塞首屏渲染 ======
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
loadClarity();
|
|
||||||
} else {
|
|
||||||
window.addEventListener('load', loadClarity, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadClarity() {
|
// 异步初始化 Sentry(延迟到首帧渲染完成后,防止 errorHandler 与渲染周期冲突)
|
||||||
|
setTimeout(() => {
|
||||||
|
import('./utils/sentry').then(({ initSentry }) => {
|
||||||
|
const router = app.config.globalProperties.$router
|
||||||
|
initSentry(app, router)
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('Sentry 初始化失败:', err)
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// 异步加载 Clarity(在页面完全加载后)
|
||||||
|
const loadClarity = async () => {
|
||||||
try {
|
try {
|
||||||
const Clarity = (await import('@microsoft/clarity')).default;
|
const { getVisitorId } = await import('./utils/visitorId')
|
||||||
const projectId = "rhp8uqoc3l";
|
const Clarity = (await import('@microsoft/clarity')).default
|
||||||
Clarity.init(projectId);
|
Clarity.init('rhp8uqoc3l')
|
||||||
|
|
||||||
// 获取并设置访客标识
|
const visitorId = await getVisitorId()
|
||||||
const visitorId = await getVisitorId();
|
console.log('Visitor ID:', visitorId)
|
||||||
console.log('Visitor ID:', visitorId);
|
Clarity.identify(visitorId)
|
||||||
Clarity.identify(visitorId);
|
Clarity.setTag('fingerprintjs', visitorId)
|
||||||
Clarity.setTag('fingerprintjs', visitorId);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Clarity 加载或标识设置失败:', error);
|
console.warn('Clarity 加载或标识设置失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
|
if (document.readyState === 'complete') {
|
||||||
try {
|
loadClarity()
|
||||||
const removeLoader = () => {
|
} else {
|
||||||
document.body.classList.add('app-loaded');
|
window.addEventListener('load', loadClarity, { once: true })
|
||||||
const el = document.getElementById('app-loader');
|
|
||||||
if (!el) return;
|
|
||||||
// 与 CSS 过渡对齐,稍等再移除节点,避免闪烁
|
|
||||||
setTimeout(() => el.remove(), 220);
|
|
||||||
};
|
|
||||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
||||||
removeLoader();
|
|
||||||
} else {
|
|
||||||
window.addEventListener('DOMContentLoaded', removeLoader, {once: true});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 安全失败:即便移除失败也不影响应用
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,10 @@
|
|||||||
@token-info-updated="updateTokenDisplayInfo"
|
@token-info-updated="updateTokenDisplayInfo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!shouldShowInit" class="d-flex">
|
<!-- 首屏骨架屏(数据加载中显示) -->
|
||||||
|
<HomeSkeleton v-if="!shouldShowInit && !dataReady" />
|
||||||
|
|
||||||
|
<div v-if="!shouldShowInit && dataReady" class="d-flex">
|
||||||
<!-- 主要内容区域 -->
|
<!-- 主要内容区域 -->
|
||||||
<v-container class="main-window flex-grow-1 no-select bloom-container" fluid>
|
<v-container class="main-window flex-grow-1 no-select bloom-container" fluid>
|
||||||
<!-- 常驻通知区域 -->
|
<!-- 常驻通知区域 -->
|
||||||
@ -430,23 +433,73 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MessageLog from "@/components/MessageLog.vue";
|
import { defineAsyncComponent } from "vue";
|
||||||
import RandomPicker from "@/components/RandomPicker.vue";
|
import AsyncLoadingPlaceholder from "@/components/common/AsyncLoadingPlaceholder.vue";
|
||||||
import FloatingToolbar from "@/components/FloatingToolbar.vue";
|
|
||||||
import FloatingICP from "@/components/FloatingICP.vue";
|
// ===== 首屏核心组件(同步加载)=====
|
||||||
import ChatWidget from "@/components/ChatWidget.vue";
|
|
||||||
import HomeworkEditDialog from "@/components/HomeworkEditDialog.vue";
|
|
||||||
import InitServiceChooser from "@/components/InitServiceChooser.vue";
|
|
||||||
import StudentNameManager from "@/components/StudentNameManager.vue";
|
|
||||||
import UrgentTestDialog from "@/components/UrgentTestDialog.vue";
|
|
||||||
import AttendanceSidebar from "@/components/attendance/AttendanceSidebar.vue";
|
|
||||||
import AttendanceManagementDialog from "@/components/attendance/AttendanceManagementDialog.vue";
|
|
||||||
import HomeworkGrid from "@/components/home/HomeworkGrid.vue";
|
import HomeworkGrid from "@/components/home/HomeworkGrid.vue";
|
||||||
import HomeActions from "@/components/home/HomeActions.vue";
|
import HomeActions from "@/components/home/HomeActions.vue";
|
||||||
import PwaInstallCard from "@/components/PwaInstallCard.vue";
|
import FloatingICP from "@/components/FloatingICP.vue";
|
||||||
import ExamScheduleCard from "@/components/home/ExamScheduleCard.vue";
|
|
||||||
import ExamConfigEditor from "@/components/ExamConfigEditor.vue";
|
|
||||||
import HitokotoCard from "@/components/HitokotoCard.vue";
|
import HitokotoCard from "@/components/HitokotoCard.vue";
|
||||||
|
import HomeSkeleton from "@/components/common/HomeSkeleton.vue";
|
||||||
|
|
||||||
|
// ===== 非首屏 / 条件渲染组件(异步懒加载)=====
|
||||||
|
const MessageLog = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/MessageLog.vue"),
|
||||||
|
loadingComponent: AsyncLoadingPlaceholder,
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const RandomPicker = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/RandomPicker.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
|
const FloatingToolbar = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/FloatingToolbar.vue"),
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const ChatWidget = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/ChatWidget.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
|
const HomeworkEditDialog = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/HomeworkEditDialog.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
|
const InitServiceChooser = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/InitServiceChooser.vue"),
|
||||||
|
loadingComponent: AsyncLoadingPlaceholder,
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const StudentNameManager = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/StudentNameManager.vue"),
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const UrgentTestDialog = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/UrgentTestDialog.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
|
const AttendanceSidebar = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/attendance/AttendanceSidebar.vue"),
|
||||||
|
loadingComponent: AsyncLoadingPlaceholder,
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const AttendanceManagementDialog = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/attendance/AttendanceManagementDialog.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
|
const PwaInstallCard = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/PwaInstallCard.vue"),
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const ExamScheduleCard = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/home/ExamScheduleCard.vue"),
|
||||||
|
loadingComponent: AsyncLoadingPlaceholder,
|
||||||
|
delay: 200,
|
||||||
|
});
|
||||||
|
const ExamConfigEditor = defineAsyncComponent({
|
||||||
|
loader: () => import("@/components/ExamConfigEditor.vue"),
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
import dataProvider from "@/utils/dataProvider";
|
import dataProvider from "@/utils/dataProvider";
|
||||||
import { useExamStore } from "@/stores/examStore";
|
import { useExamStore } from "@/stores/examStore";
|
||||||
import {
|
import {
|
||||||
@ -457,9 +510,6 @@ import {
|
|||||||
} from "@/utils/settings";
|
} from "@/utils/settings";
|
||||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
import "../styles/index.scss";
|
|
||||||
import "../styles/transitions.scss";
|
|
||||||
import "../styles/global.scss";
|
|
||||||
import { debounce, throttle } from "@/utils/debounce";
|
import { debounce, throttle } from "@/utils/debounce";
|
||||||
import { Base64 } from "js-base64";
|
import { Base64 } from "js-base64";
|
||||||
import {
|
import {
|
||||||
@ -491,6 +541,7 @@ export default {
|
|||||||
PwaInstallCard,
|
PwaInstallCard,
|
||||||
ExamScheduleCard,
|
ExamScheduleCard,
|
||||||
ExamConfigEditor,
|
ExamConfigEditor,
|
||||||
|
HomeSkeleton,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { mobile } = useDisplay();
|
const { mobile } = useDisplay();
|
||||||
@ -563,6 +614,7 @@ export default {
|
|||||||
students: false,
|
students: false,
|
||||||
copyToToday: false,
|
copyToToday: false,
|
||||||
},
|
},
|
||||||
|
dataReady: false,
|
||||||
debouncedUpload: null,
|
debouncedUpload: null,
|
||||||
debouncedAttendanceSave: null,
|
debouncedAttendanceSave: null,
|
||||||
throttledReflow: null,
|
throttledReflow: null,
|
||||||
@ -948,6 +1000,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
this.updateBackendUrl();
|
this.updateBackendUrl();
|
||||||
await this.initializeData();
|
await this.initializeData();
|
||||||
|
this.dataReady = true;
|
||||||
// 拉取设备/命名空间信息用于标题显示
|
// 拉取设备/命名空间信息用于标题显示
|
||||||
await this.loadDeviceInfo();
|
await this.loadDeviceInfo();
|
||||||
this.setupAutoRefresh();
|
this.setupAutoRefresh();
|
||||||
|
|||||||
128
src/utils/sentry.js
Normal file
128
src/utils/sentry.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* Sentry 异步初始化模块
|
||||||
|
*
|
||||||
|
* 从 main.js 中抽离,在 Vue app 挂载后异步加载,
|
||||||
|
* 避免 @sentry/vue (~60KB gzip) 阻塞首屏渲染。
|
||||||
|
*/
|
||||||
|
import * as Sentry from '@sentry/vue'
|
||||||
|
import { getVisitorId } from './visitorId'
|
||||||
|
|
||||||
|
// 保存 feedback integration 实例的引用
|
||||||
|
let feedbackIntegration = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步初始化 Sentry(在 app mount 后调用)
|
||||||
|
* @param {import('vue').App} app - Vue app 实例
|
||||||
|
* @param {import('vue-router').Router} router - Vue Router 实例
|
||||||
|
*/
|
||||||
|
export function initSentry(app, router) {
|
||||||
|
Sentry.init({
|
||||||
|
app,
|
||||||
|
dsn: 'https://dc34ab47426f49c0925445f0d87b7007@report.houlang.cloud/6',
|
||||||
|
sendDefaultPii: true,
|
||||||
|
integrations: [
|
||||||
|
Sentry.browserTracingIntegration({ router }),
|
||||||
|
Sentry.replayIntegration({
|
||||||
|
maskAllText: false,
|
||||||
|
blockAllMedia: false,
|
||||||
|
}),
|
||||||
|
feedbackIntegration = Sentry.feedbackIntegration({
|
||||||
|
autoInject: false,
|
||||||
|
colorScheme: 'system',
|
||||||
|
showBranding: false,
|
||||||
|
showName: true,
|
||||||
|
showEmail: true,
|
||||||
|
isNameRequired: false,
|
||||||
|
isEmailRequired: false,
|
||||||
|
useSentryUser: {
|
||||||
|
name: 'username',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
themeDark: {
|
||||||
|
submitBackground: '#6200EA',
|
||||||
|
submitBackgroundHover: '#7C4DFF',
|
||||||
|
},
|
||||||
|
themeLight: {
|
||||||
|
submitBackground: '#6200EA',
|
||||||
|
submitBackgroundHover: '#7C4DFF',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
tracePropagationTargets: [
|
||||||
|
'localhost',
|
||||||
|
/^https?:\/\/cs\.(houlang\.cloud|houlangs\.com)/,
|
||||||
|
],
|
||||||
|
replaysSessionSampleRate: 0,
|
||||||
|
replaysOnErrorSampleRate: 0,
|
||||||
|
enableLogs: true,
|
||||||
|
beforeSend(event) {
|
||||||
|
return event
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 异步设置用户 fingerprint
|
||||||
|
getVisitorId()
|
||||||
|
.then((visitorId) => {
|
||||||
|
Sentry.setUser({ id: visitorId, username: visitorId })
|
||||||
|
Sentry.setTag('fingerprint', visitorId)
|
||||||
|
console.log('Sentry 用户标识已设置:', visitorId)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('设置 Sentry 用户标识失败:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册全局函数:打开反馈表单
|
||||||
|
window.openSentryFeedback = () => {
|
||||||
|
try {
|
||||||
|
if (!feedbackIntegration) {
|
||||||
|
console.warn('Sentry Feedback integration 未初始化')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof feedbackIntegration.createWidget === 'function') {
|
||||||
|
const widget = feedbackIntegration.createWidget()
|
||||||
|
if (widget && typeof widget.open === 'function') {
|
||||||
|
widget.open()
|
||||||
|
console.log('Sentry Feedback 对话框已打开')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof feedbackIntegration.openDialog === 'function') {
|
||||||
|
feedbackIntegration.openDialog()
|
||||||
|
console.log('Sentry Feedback 对话框已打开')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
console.warn('无法找到打开 Feedback 的方法')
|
||||||
|
console.log('可用方法:', Object.keys(feedbackIntegration))
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开 Sentry Feedback 时出错:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册全局函数:手动启动录制
|
||||||
|
window.startSentryReplay = () => {
|
||||||
|
try {
|
||||||
|
const client = Sentry.getClient()
|
||||||
|
if (!client) {
|
||||||
|
console.warn('Sentry 客户端未初始化')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const integrations = client.getOptions().integrations || []
|
||||||
|
const replayIntegration = integrations.find(
|
||||||
|
(integration) => integration && integration.name === 'Replay'
|
||||||
|
)
|
||||||
|
if (replayIntegration && typeof replayIntegration.start === 'function') {
|
||||||
|
replayIntegration.start()
|
||||||
|
console.log('Sentry Replay 已手动启动')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
console.warn('无法找到 Sentry Replay integration')
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动 Sentry Replay 时出错:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
vercel.json
40
vercel.json
@ -7,11 +7,47 @@
|
|||||||
],
|
],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"source": "/:path*",
|
"source": "/assets/:path*",
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Cache-Control",
|
"key": "Cache-Control",
|
||||||
"value": "no-cache, no-store, must-revalidate"
|
"value": "public, max-age=31536000, immutable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/pwa/:path*",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "public, max-age=604800"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/index.html",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "no-cache"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/sw.js",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "no-cache"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/sw-cache-manager.js",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "no-cache"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,11 +158,11 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Components({
|
Components({
|
||||||
//resolvers: [
|
// 排除已在 index.vue 中通过 defineAsyncComponent 手动懒加载的组件
|
||||||
// TDesignResolver({
|
// 避免 unplugin-vue-components 生成冲突的静态 import
|
||||||
// library: 'vue-next'
|
directoryAsNamespace: false,
|
||||||
// })
|
globs: ['src/components/**/[A-Z]*.vue'],
|
||||||
//]
|
exclude: [/pages\/index\.vue$/],
|
||||||
}),
|
}),
|
||||||
Fonts({
|
Fonts({
|
||||||
google: {
|
google: {
|
||||||
@ -198,6 +198,26 @@ export default defineConfig({
|
|||||||
'.vue',
|
'.vue',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
// ===== Chunk 分割优化 =====
|
||||||
|
chunkSizeWarningLimit: 500,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
// 核心框架(极少变动,长缓存)
|
||||||
|
'vendor-vue': ['vue', 'vue-router', 'pinia'],
|
||||||
|
// UI 框架
|
||||||
|
'vendor-vuetify': ['vuetify'],
|
||||||
|
// 监控(异步加载,独立 chunk)
|
||||||
|
'vendor-sentry': ['@sentry/vue'],
|
||||||
|
// 实时通信
|
||||||
|
'vendor-socket': ['socket.io-client'],
|
||||||
|
// 通用工具库
|
||||||
|
'vendor-utils': ['axios', 'uuid', 'js-base64'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3031,
|
port: 3031,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user