mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-03-21 09:13:10 +00:00
Compare commits
No commits in common. "e70d46436c60d29e12b64c9f8c0d6e9a3158a5b1" and "834883b2a45cd9b9017d6bb9a6df0dcf681f8663" have entirely different histories.
e70d46436c
...
834883b2a4
@ -34,9 +34,7 @@ export default [
|
||||
confirm: 'readonly',
|
||||
prompt: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
fetch: 'readonly',
|
||||
XMLHttpRequest: 'readonly',
|
||||
URL: 'readonly',
|
||||
|
||||
93
index.html
93
index.html
@ -5,38 +5,77 @@
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Classworks 作业板</title>
|
||||
|
||||
<!-- SEO -->
|
||||
<meta name="description" content="Classworks —— 适用于班级大屏的作业板小工具,支持记录、查看并同步作业,开源免费。" />
|
||||
<meta name="keywords" content="Classworks,作业板,班级大屏,作业管理,课表,作业同步,开源" />
|
||||
<meta name="author" content="Sunwuyuan" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="https://cs.houlang.cloud/" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Classworks 作业板" />
|
||||
<meta property="og:description" content="适用于班级大屏的作业板小工具,支持记录、查看并同步作业,开源免费。" />
|
||||
<meta property="og:url" content="https://cs.houlang.cloud/" />
|
||||
<meta property="og:site_name" content="Classworks" />
|
||||
<meta property="og:image" content="https://cs.houlang.cloud/banner.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:locale" content="zh_CN" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="Classworks 作业板" />
|
||||
<meta name="twitter:description" content="适用于班级大屏的作业板小工具,支持记录、查看并同步作业,开源免费。" />
|
||||
<meta name="twitter:image" content="https://cs.houlang.cloud/banner.png" />
|
||||
|
||||
<meta name="description" content="记录,查看并同步作业" />
|
||||
<link rel="apple-touch-icon" href="/image/apple-touch-icon.png" sizes="180x180" />
|
||||
<link rel="mask-icon" href="/image/mask-icon.svg" color="#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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -19,7 +19,9 @@
|
||||
"axios": "^1.13.2",
|
||||
"idb": "^8.0.3",
|
||||
"js-base64": "^3.7.8",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lucide-vue-next": "^0.555.0",
|
||||
"marked": "^17.0.1",
|
||||
"pinyin-pro": "^3.27.0",
|
||||
"ratelimit-header-parser": "^0.1.0",
|
||||
"roboto-fontface": "*",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -32,9 +32,15 @@ importers:
|
||||
js-base64:
|
||||
specifier: ^3.7.8
|
||||
version: 3.7.8
|
||||
js-yaml:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
lucide-vue-next:
|
||||
specifier: ^0.555.0
|
||||
version: 0.555.0(vue@3.5.25(typescript@5.9.3))
|
||||
marked:
|
||||
specifier: ^17.0.1
|
||||
version: 17.0.1
|
||||
pinyin-pro:
|
||||
specifier: ^3.27.0
|
||||
version: 3.27.0
|
||||
@ -2180,7 +2186,6 @@ packages:
|
||||
glob@11.1.0:
|
||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||
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
|
||||
|
||||
globals@14.0.0:
|
||||
@ -2547,6 +2552,11 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
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:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -6251,6 +6261,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
marked@17.0.1: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 650 KiB |
@ -32,11 +32,6 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
/* 全局样式(从 index.vue 迁移,确保全局可用且仅加载一次) */
|
||||
@import "@/styles/index.scss";
|
||||
@import "@/styles/transitions.scss";
|
||||
@import "@/styles/global.scss";
|
||||
|
||||
.md3-enter-active,
|
||||
.md3-leave-active {
|
||||
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="time-card"
|
||||
elevation="2"
|
||||
border
|
||||
rounded="xl"
|
||||
height="100%"
|
||||
>
|
||||
<v-card-text
|
||||
class="pa-6 d-flex flex-column "
|
||||
style="height: 100%"
|
||||
>
|
||||
<!-- 时间显示 -->
|
||||
<div
|
||||
class="time-display"
|
||||
:style="timeStyle"
|
||||
>
|
||||
{{ timeString }}<span
|
||||
class="seconds-text"
|
||||
:style="secondsStyle"
|
||||
>{{ secondsString }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 日期 + 星期 + 时段 -->
|
||||
<div
|
||||
class="date-line mt-3"
|
||||
:style="dateStyle"
|
||||
>
|
||||
{{ dateString }} {{ weekdayString }} {{ periodOfDay }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SettingsManager, watchSettings } from '@/utils/settings'
|
||||
|
||||
// 时间字体大小比例(大屏场景)
|
||||
const TIME_FONT_RATIO = 2.0
|
||||
const SECONDS_FONT_RATIO = 0.9
|
||||
const DATE_FONT_RATIO = 0.6
|
||||
|
||||
export default {
|
||||
name: 'TimeCard',
|
||||
data() {
|
||||
return {
|
||||
now: new Date(),
|
||||
timer: null,
|
||||
unwatch: null,
|
||||
fontSize: 28,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
timeString() {
|
||||
const h = String(this.now.getHours()).padStart(2, '0')
|
||||
const m = String(this.now.getMinutes()).padStart(2, '0')
|
||||
return `${h}:${m}`
|
||||
},
|
||||
secondsString() {
|
||||
return `:${String(this.now.getSeconds()).padStart(2, '0')}`
|
||||
},
|
||||
dateString() {
|
||||
const y = this.now.getFullYear()
|
||||
const m = this.now.getMonth() + 1
|
||||
const d = this.now.getDate()
|
||||
return `${y}年${m}月${d}日`
|
||||
},
|
||||
weekdayString() {
|
||||
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
return days[this.now.getDay()]
|
||||
},
|
||||
periodOfDay() {
|
||||
const h = this.now.getHours()
|
||||
if (h < 6) return '凌晨'
|
||||
if (h < 8) return '早晨'
|
||||
if (h < 11) return '上午'
|
||||
if (h < 13) return '中午'
|
||||
if (h < 17) return '下午'
|
||||
if (h < 19) return '傍晚'
|
||||
if (h < 22) return '晚上'
|
||||
return '深夜'
|
||||
},
|
||||
timeStyle() {
|
||||
return {
|
||||
'font-size': `${this.fontSize * TIME_FONT_RATIO}px`,
|
||||
'font-weight': '700',
|
||||
'line-height': '1',
|
||||
'letter-spacing': '4px',
|
||||
'font-variant-numeric': 'tabular-nums',
|
||||
}
|
||||
},
|
||||
secondsStyle() {
|
||||
return {
|
||||
'font-size': `${this.fontSize * SECONDS_FONT_RATIO}px`,
|
||||
'font-variant-numeric': 'tabular-nums',
|
||||
'vertical-align': 'baseline',
|
||||
'margin-left': '4px',
|
||||
'opacity': '0.6',
|
||||
}
|
||||
},
|
||||
dateStyle() {
|
||||
return {
|
||||
'font-size': `${this.fontSize * DATE_FONT_RATIO}px`,
|
||||
'letter-spacing': '1px',
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings()
|
||||
this.startTimer()
|
||||
this.unwatch = watchSettings(() => {
|
||||
this.loadSettings()
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopTimer()
|
||||
if (this.unwatch) {
|
||||
this.unwatch()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadSettings() {
|
||||
this.fontSize = SettingsManager.getSetting('font.size')
|
||||
},
|
||||
startTimer() {
|
||||
// 每秒更新一次
|
||||
this.timer = setInterval(() => {
|
||||
this.now = new Date()
|
||||
}, 1000)
|
||||
},
|
||||
stopTimer() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.time-card {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seconds-text {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.date-line {
|
||||
opacity: 0.75;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
@ -242,6 +242,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pinyin } from "pinyin-pro";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
|
||||
@ -327,7 +328,11 @@ export default {
|
||||
|
||||
return Array.from(surnameMap.entries())
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||
.sort((a, b) => {
|
||||
const pinyinA = pinyin(a.name, { toneType: "none" });
|
||||
const pinyinB = pinyin(b.name, { toneType: "none" });
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
<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>
|
||||
@ -1,71 +0,0 @@
|
||||
<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>
|
||||
@ -11,13 +11,8 @@
|
||||
}"
|
||||
class="grid-item"
|
||||
>
|
||||
<!-- 时间卡片 -->
|
||||
<div v-if="item.type === 'time'" style="height: 100%">
|
||||
<time-card />
|
||||
</div>
|
||||
|
||||
<!-- 一言卡片 -->
|
||||
<div v-else-if="item.type === 'hitokoto'" style="height: 100%">
|
||||
<div v-if="item.type === 'hitokoto'" style="height: 100%">
|
||||
<hitokoto-card />
|
||||
</div>
|
||||
|
||||
@ -209,7 +204,6 @@
|
||||
|
||||
<script>
|
||||
import HitokotoCard from "@/components/HitokotoCard.vue";
|
||||
import TimeCard from "@/components/TimeCard.vue";
|
||||
import ConciseExamCard from "@/components/home/ConciseExamCard.vue";
|
||||
import {getSetting} from "@/utils/settings.js";
|
||||
|
||||
@ -222,7 +216,6 @@ export default {
|
||||
},
|
||||
components: {
|
||||
HitokotoCard,
|
||||
TimeCard,
|
||||
ConciseExamCard,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -218,16 +218,8 @@
|
||||
<script>
|
||||
import UnsavedWarning from "../common/UnsavedWarning.vue";
|
||||
import "@/styles/warnings.scss";
|
||||
import {pinyin} from "pinyin-pro";
|
||||
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";
|
||||
|
||||
export default {
|
||||
@ -448,11 +440,10 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async sortStudentsByPinyin() {
|
||||
const pinyinFn = await loadPinyin();
|
||||
sortStudentsByPinyin() {
|
||||
const sorted = [...this.modelValue.list].sort((a, b) => {
|
||||
const pinyinA = pinyinFn(a.name, {toneType: "none"});
|
||||
const pinyinB = pinyinFn(b.name, {toneType: "none"});
|
||||
const pinyinA = pinyin(a.name, {toneType: "none"});
|
||||
const pinyinB = pinyin(b.name, {toneType: "none"});
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
sorted.forEach((s, i) => s.id = i + 1);
|
||||
|
||||
@ -21,16 +21,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Typewriter from "typewriter-effect/dist/core";
|
||||
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";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
@ -58,8 +50,7 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initTypewriters() {
|
||||
const Typewriter = await getTypewriter();
|
||||
initTypewriters() {
|
||||
this.typewriter = new Typewriter(this.$refs.typewriter, TYPEWRITER_CONFIG.main);
|
||||
this.sourceWriter = new Typewriter(this.$refs.sourceWriter, TYPEWRITER_CONFIG.source);
|
||||
this.typeQuote(INITIAL_STATE);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view/>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
216
src/main.js
216
src/main.js
@ -1,11 +1,15 @@
|
||||
/**
|
||||
* main.js
|
||||
*
|
||||
* 精简启动流水线:快速挂载 Vue app,重型依赖(Sentry/Clarity)异步加载
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// 核心插件(Vuetify / Router / Pinia)
|
||||
// Plugins
|
||||
import {registerPlugins} from '@/plugins'
|
||||
import {createPinia} from 'pinia'
|
||||
import router from './router'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
@ -13,49 +17,191 @@ import GlobalMessage from '@/components/GlobalMessage.vue'
|
||||
|
||||
// Composables
|
||||
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)
|
||||
|
||||
// 保存 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)
|
||||
app.use(messageService)
|
||||
//app.use(TDesign)
|
||||
app.use(messageService);
|
||||
app.use(pinia)
|
||||
|
||||
app.component('GlobalMessage', GlobalMessage)
|
||||
|
||||
// 挂载 Vue app(首要目标:尽快渲染首屏)
|
||||
app.mount('#app')
|
||||
|
||||
// ====== 以下全部异步,不阻塞首屏渲染 ======
|
||||
|
||||
// 异步初始化 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 {
|
||||
const { getVisitorId } = await import('./utils/visitorId')
|
||||
const Clarity = (await import('@microsoft/clarity')).default
|
||||
Clarity.init('rhp8uqoc3l')
|
||||
|
||||
const visitorId = await getVisitorId()
|
||||
console.log('Visitor ID:', visitorId)
|
||||
Clarity.identify(visitorId)
|
||||
Clarity.setTag('fingerprintjs', visitorId)
|
||||
} catch (error) {
|
||||
console.warn('Clarity 加载或标识设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 异步加载 Clarity 以提升初始加载速度
|
||||
if (document.readyState === 'complete') {
|
||||
loadClarity()
|
||||
loadClarity();
|
||||
} else {
|
||||
window.addEventListener('load', loadClarity, { once: true })
|
||||
window.addEventListener('load', loadClarity, { once: true });
|
||||
}
|
||||
|
||||
async function loadClarity() {
|
||||
try {
|
||||
const Clarity = (await import('@microsoft/clarity')).default;
|
||||
const projectId = "rhp8uqoc3l";
|
||||
Clarity.init(projectId);
|
||||
|
||||
// 获取并设置访客标识
|
||||
const visitorId = await getVisitorId();
|
||||
console.log('Visitor ID:', visitorId);
|
||||
Clarity.identify(visitorId);
|
||||
Clarity.setTag('fingerprintjs', visitorId);
|
||||
} catch (error) {
|
||||
console.warn('Clarity 加载或标识设置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
|
||||
try {
|
||||
const removeLoader = () => {
|
||||
document.body.classList.add('app-loaded');
|
||||
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,10 +63,7 @@
|
||||
@token-info-updated="updateTokenDisplayInfo"
|
||||
/>
|
||||
|
||||
<!-- 首屏骨架屏(数据加载中显示) -->
|
||||
<HomeSkeleton v-if="!shouldShowInit && !dataReady" />
|
||||
|
||||
<div v-if="!shouldShowInit && dataReady" class="d-flex">
|
||||
<div v-if="!shouldShowInit" class="d-flex">
|
||||
<!-- 主要内容区域 -->
|
||||
<v-container class="main-window flex-grow-1 no-select bloom-container" fluid>
|
||||
<!-- 常驻通知区域 -->
|
||||
@ -433,73 +430,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import AsyncLoadingPlaceholder from "@/components/common/AsyncLoadingPlaceholder.vue";
|
||||
|
||||
// ===== 首屏核心组件(同步加载)=====
|
||||
import MessageLog from "@/components/MessageLog.vue";
|
||||
import RandomPicker from "@/components/RandomPicker.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 HomeActions from "@/components/home/HomeActions.vue";
|
||||
import FloatingICP from "@/components/FloatingICP.vue";
|
||||
import PwaInstallCard from "@/components/PwaInstallCard.vue";
|
||||
import ExamScheduleCard from "@/components/home/ExamScheduleCard.vue";
|
||||
import ExamConfigEditor from "@/components/ExamConfigEditor.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 { useExamStore } from "@/stores/examStore";
|
||||
import {
|
||||
@ -510,6 +457,9 @@ import {
|
||||
} from "@/utils/settings";
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
import { useDisplay } from "vuetify";
|
||||
import "../styles/index.scss";
|
||||
import "../styles/transitions.scss";
|
||||
import "../styles/global.scss";
|
||||
import { debounce, throttle } from "@/utils/debounce";
|
||||
import { Base64 } from "js-base64";
|
||||
import {
|
||||
@ -541,7 +491,6 @@ export default {
|
||||
PwaInstallCard,
|
||||
ExamScheduleCard,
|
||||
ExamConfigEditor,
|
||||
HomeSkeleton,
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
@ -614,7 +563,6 @@ export default {
|
||||
students: false,
|
||||
copyToToday: false,
|
||||
},
|
||||
dataReady: false,
|
||||
debouncedUpload: null,
|
||||
debouncedAttendanceSave: null,
|
||||
throttledReflow: null,
|
||||
@ -687,21 +635,11 @@ export default {
|
||||
return this.mobile;
|
||||
},
|
||||
titleText() {
|
||||
const provider = getSetting("server.provider");
|
||||
const isOnline = provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
let displayName;
|
||||
if (isOnline && this.state.namespaceInfo) {
|
||||
// 非离线模式:优先使用自动获取的命名空间名称
|
||||
displayName =
|
||||
this.state.namespaceInfo?.name ||
|
||||
// 优先展示当前设备名称(如果已从云端获取)
|
||||
const deviceName =
|
||||
this.state.namespaceInfo?.device?.name ||
|
||||
this.state.classNumber ||
|
||||
"高三八班";
|
||||
} else {
|
||||
// 离线模式:使用本地设置的班级编号
|
||||
displayName = this.state.classNumber || "高三八班";
|
||||
}
|
||||
|
||||
const today = this.getToday();
|
||||
const yesterday = new Date(today);
|
||||
@ -712,11 +650,11 @@ export default {
|
||||
const yesterdayStr = this.formatDate(yesterday);
|
||||
|
||||
if (currentDateStr === todayStr) {
|
||||
return displayName + " - 今天的作业";
|
||||
return deviceName + " - 今天的作业";
|
||||
} else if (currentDateStr === yesterdayStr) {
|
||||
return displayName + " - 昨天的作业";
|
||||
return deviceName + " - 昨天的作业";
|
||||
} else {
|
||||
return `${displayName} - ${currentDateStr}的作业`;
|
||||
return `${deviceName} - ${currentDateStr}的作业`;
|
||||
}
|
||||
},
|
||||
sortedItems() {
|
||||
@ -775,17 +713,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加时间卡片
|
||||
if (getSetting("timeCard.enabled")) {
|
||||
items.push({
|
||||
key: "time-card",
|
||||
name: "时间",
|
||||
type: "time",
|
||||
order: 9997,
|
||||
rowSpan: 150,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加一言卡片
|
||||
if (getSetting("hitokoto.enabled")) {
|
||||
items.push({
|
||||
@ -1010,7 +937,6 @@ export default {
|
||||
try {
|
||||
this.updateBackendUrl();
|
||||
await this.initializeData();
|
||||
this.dataReady = true;
|
||||
// 拉取设备/命名空间信息用于标题显示
|
||||
await this.loadDeviceInfo();
|
||||
this.setupAutoRefresh();
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,14 +100,6 @@ const settingsDefinitions = {
|
||||
icon: "mdi-card-outline",
|
||||
},
|
||||
|
||||
// 时间卡片设置
|
||||
"timeCard.enabled": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "启用时间卡片",
|
||||
icon: "mdi-clock-outline",
|
||||
},
|
||||
|
||||
// 一言设置
|
||||
"hitokoto.enabled": {
|
||||
type: "boolean",
|
||||
|
||||
40
vercel.json
40
vercel.json
@ -7,47 +7,11 @@
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/assets/:path*",
|
||||
"source": "/:path*",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"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"
|
||||
"value": "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -158,11 +158,11 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
Components({
|
||||
// 排除已在 index.vue 中通过 defineAsyncComponent 手动懒加载的组件
|
||||
// 避免 unplugin-vue-components 生成冲突的静态 import
|
||||
directoryAsNamespace: false,
|
||||
globs: ['src/components/**/[A-Z]*.vue'],
|
||||
exclude: [/pages\/index\.vue$/],
|
||||
//resolvers: [
|
||||
// TDesignResolver({
|
||||
// library: 'vue-next'
|
||||
// })
|
||||
//]
|
||||
}),
|
||||
Fonts({
|
||||
google: {
|
||||
@ -198,26 +198,6 @@ export default defineConfig({
|
||||
'.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: {
|
||||
port: 3031,
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user