重构代码,优化组件加载和样式,移除不必要的依赖,增强首屏渲染性能
This commit is contained in:
@@ -32,6 +32,11 @@ 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),
|
||||
|
||||
@@ -242,7 +242,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pinyin } from "pinyin-pro";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
|
||||
@@ -328,11 +327,7 @@ export default {
|
||||
|
||||
return Array.from(surnameMap.entries())
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => {
|
||||
const pinyinA = pinyin(a.name, { toneType: "none" });
|
||||
const pinyinB = pinyin(b.name, { toneType: "none" });
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
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 {
|
||||
@@ -440,10 +448,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
sortStudentsByPinyin() {
|
||||
async sortStudentsByPinyin() {
|
||||
const pinyinFn = await loadPinyin();
|
||||
const sorted = [...this.modelValue.list].sort((a, b) => {
|
||||
const pinyinA = pinyin(a.name, {toneType: "none"});
|
||||
const pinyinB = pinyin(b.name, {toneType: "none"});
|
||||
const pinyinA = pinyinFn(a.name, {toneType: "none"});
|
||||
const pinyinB = pinyinFn(b.name, {toneType: "none"});
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
sorted.forEach((s, i) => s.id = i + 1);
|
||||
|
||||
@@ -21,8 +21,16 @@
|
||||
</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 = {
|
||||
@@ -50,7 +58,8 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
initTypewriters() {
|
||||
async initTypewriters() {
|
||||
const Typewriter = await getTypewriter();
|
||||
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,9 +1,7 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view/>
|
||||
</v-main>
|
||||
</v-app>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
+32
-178
@@ -1,207 +1,61 @@
|
||||
/**
|
||||
* main.js
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
* 精简启动流水线:快速挂载 Vue app,重型依赖(Sentry/Clarity)异步加载
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import {registerPlugins} from '@/plugins'
|
||||
import {createPinia} from 'pinia'
|
||||
import router from './router'
|
||||
|
||||
const pinia = createPinia()
|
||||
// 核心插件(Vuetify / Router / Pinia)
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
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 { createApp } from 'vue'
|
||||
|
||||
import messageService from './utils/message';
|
||||
import { getVisitorId } from './utils/visitorId';
|
||||
|
||||
import * as Sentry from "@sentry/vue";
|
||||
import messageService from './utils/message'
|
||||
|
||||
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(TDesign)
|
||||
app.use(messageService);
|
||||
app.use(pinia)
|
||||
app.use(messageService)
|
||||
|
||||
app.component('GlobalMessage', GlobalMessage)
|
||||
|
||||
// 挂载 Vue 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 {
|
||||
const Clarity = (await import('@microsoft/clarity')).default;
|
||||
const projectId = "rhp8uqoc3l";
|
||||
Clarity.init(projectId);
|
||||
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);
|
||||
const visitorId = await getVisitorId()
|
||||
console.log('Visitor ID:', visitorId)
|
||||
Clarity.identify(visitorId)
|
||||
Clarity.setTag('fingerprintjs', visitorId)
|
||||
} catch (error) {
|
||||
console.warn('Clarity 加载或标识设置失败:', 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 {
|
||||
// 安全失败:即便移除失败也不影响应用
|
||||
if (document.readyState === 'complete') {
|
||||
loadClarity()
|
||||
} else {
|
||||
window.addEventListener('load', loadClarity, { once: true })
|
||||
}
|
||||
|
||||
+71
-18
@@ -63,7 +63,10 @@
|
||||
@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>
|
||||
<!-- 常驻通知区域 -->
|
||||
@@ -430,23 +433,73 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 { defineAsyncComponent } from "vue";
|
||||
import AsyncLoadingPlaceholder from "@/components/common/AsyncLoadingPlaceholder.vue";
|
||||
|
||||
// ===== 首屏核心组件(同步加载)=====
|
||||
import HomeworkGrid from "@/components/home/HomeworkGrid.vue";
|
||||
import HomeActions from "@/components/home/HomeActions.vue";
|
||||
import PwaInstallCard from "@/components/PwaInstallCard.vue";
|
||||
import ExamScheduleCard from "@/components/home/ExamScheduleCard.vue";
|
||||
import ExamConfigEditor from "@/components/ExamConfigEditor.vue";
|
||||
import FloatingICP from "@/components/FloatingICP.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 {
|
||||
@@ -457,9 +510,6 @@ 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 {
|
||||
@@ -491,6 +541,7 @@ export default {
|
||||
PwaInstallCard,
|
||||
ExamScheduleCard,
|
||||
ExamConfigEditor,
|
||||
HomeSkeleton,
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
@@ -563,6 +614,7 @@ export default {
|
||||
students: false,
|
||||
copyToToday: false,
|
||||
},
|
||||
dataReady: false,
|
||||
debouncedUpload: null,
|
||||
debouncedAttendanceSave: null,
|
||||
throttledReflow: null,
|
||||
@@ -948,6 +1000,7 @@ export default {
|
||||
try {
|
||||
this.updateBackendUrl();
|
||||
await this.initializeData();
|
||||
this.dataReady = true;
|
||||
// 拉取设备/命名空间信息用于标题显示
|
||||
await this.loadDeviceInfo();
|
||||
this.setupAutoRefresh();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function getServerUrl() {
|
||||
if (isRotationEnabled()) {
|
||||
return getEffectiveServerUrl();
|
||||
}
|
||||
|
||||
|
||||
// Prefer configured server domain; fallback to env; then current origin
|
||||
const cfg = getSetting('server.domain');
|
||||
const envUrl = import.meta?.env?.VITE_SERVER_URL;
|
||||
@@ -34,7 +34,7 @@ export function getSocket() {
|
||||
socket = null;
|
||||
}
|
||||
connectedDomain = serverUrl;
|
||||
|
||||
|
||||
// For classworkscloud, create socket with the first server in rotation
|
||||
// Note: Socket.IO's built-in reconnection will retry the same server URL.
|
||||
// Server rotation is handled at the HTTP request level, not Socket.IO level.
|
||||
|
||||
Reference in New Issue
Block a user