Files
dpb/frontend/web/src/App.vue
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

190 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ElConfigProvider
:size="size"
:locale="locale"
:z-index="3000"
:card="{
shadow: 'never',
}"
>
<ElWatermark
:font="{ color: fontColor }"
:content="showWatermark ? watermarkContent : ''"
:z-index="9999"
class="wh-full"
>
<RouterView></RouterView>
<!-- AI 助手 -->
<FaAiAssistant v-if="enableAiAssistant" />
</ElWatermark>
</ElConfigProvider>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, onErrorCaptured, onMounted, onUnmounted } from "vue";
import { useWindowSize } from "@vueuse/core";
import { useAppStore, useUserStore } from "./store";
import { useSettingsStore } from "./store/modules/setting.store";
import { defaultSettings } from "./config/setting";
import { ComponentSize } from "./enums/settings/layout.enum";
import { MOBILE_BREAKPOINT } from "./utils/constants/definitions";
import { hexToRgba, toggleTransition } from "./utils/ui";
import { initializeTheme } from "./hooks/core/useTheme";
import { useAppBootstrap } from "@/hooks/core/useAppBootstrap";
import { ThemeMode } from "./enums";
import en from "element-plus/es/locale/lang/en";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import { router } from "@/router";
import { initIconifyAsync } from "./plugins/iconify";
import { msg } from "@/utils/message";
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const { width } = useWindowSize();
// H5 用小尺寸,桌面用用户设置的大小
const size = computed(() => {
if (width.value < MOBILE_BREAKPOINT) return "small" as ComponentSize;
return appStore.size as ComponentSize;
});
const showWatermark = computed(() => settingsStore.showWatermark);
const watermarkContent = defaultSettings.watermarkContent;
// 根据语言设置返回对应的语言包
const locale = computed(() => {
return appStore.language === "en" ? en : zhCn;
});
// 只有在启用 AI 助手且用户已登录时才显示
const enableAiAssistant = computed(() => {
const isEnabled = settingsStore.userEnableAi;
const isLoggedIn = userStore.basicInfo && Object.keys(userStore.basicInfo).length > 0;
return isEnabled && isLoggedIn;
});
// 水印文字默认使用当前主题色(半透明),随主题色设置变化
const fontColor = computed(() => {
const hex = settingsStore.themeColor || defaultSettings.themeColor;
const alpha = settingsStore.theme === ThemeMode.DARK ? 0.22 : 0.16;
try {
return hexToRgba(hex, alpha).rgba;
} catch {
return hexToRgba(defaultSettings.themeColor, alpha).rgba;
}
});
/**
* 应用根组件生命周期:
*
* onBeforeMount
* 1. toggleTransition(true) —— 临时禁用页面过渡,避免主题切换时的闪烁
* 2. initializeTheme() —— 加载主题配色(CSS 变量)、暗色模式 class、auto 监听
*
* onMounted
* 1. bootstrap() —— 存储检查 → 过渡恢复 → 版本升级 → 站点配置
* 2. 监听 "app:storage-invalidated" 事件 —— 存储异常时由 storage 模块派发
*/
onBeforeMount(() => {
toggleTransition(true);
initializeTheme();
});
// 存储失效时跳转登录页(由 storage 模块 detect 到异常后派发)
const handleStorageInvalidated = () => {
router.push({ name: "Login" });
};
const { bootstrap } = useAppBootstrap();
// ── 全局键盘快捷键 ──
const handleGlobalKeydown = (e: KeyboardEvent) => {
const isMac = navigator.userAgent.includes("Mac");
const modKey = isMac ? e.metaKey : e.ctrlKey;
// Ctrl+S / Cmd+S:提交弹窗表单
if (modKey && e.key === "s") {
// 优先触发 useCrudForm 中已注册的键盘监听(由其自行 e.preventDefault
// 兜底:查找可见 dialog/drawer 中的确认按钮
const confirmBtn = document.querySelector<HTMLElement>(
[
".el-dialog:not(.is-hidden) .el-dialog__footer .el-button--primary",
".el-overlay-dialog:not(.is-hidden) .el-dialog__footer .el-button--primary",
".el-drawer:not(.is-hidden) .el-drawer__footer .el-button--primary",
].join(", ")
);
if (confirmBtn && document.querySelector(".el-overlay-dialog")) {
confirmBtn.click();
}
// useCrudForm 中的 useFormKeyboardSubmit 已自行 e.preventDefault,此处也调用防止默认
e.preventDefault();
return;
}
// Ctrl+F / Cmd+F:聚焦搜索输入框
if (modKey && e.key === "f") {
const searchInput = document.querySelector<HTMLElement>(".fa-search-bar input");
if (searchInput) {
e.preventDefault();
searchInput.focus();
}
return;
}
// ESC:由 ElDialog / ElDrawer 内置逻辑处理,此处不做额外干预
};
window.addEventListener("keydown", handleGlobalKeydown);
const handleOffline = () => {
msg.notify({
title: "网络已断开",
message: "请检查您的网络连接",
type: "error",
duration: 0,
});
};
const handleOnline = () => {
msg.success("网络已恢复");
};
onMounted(() => {
bootstrap();
// 存储检测到异常并已清除数据 → 由路由守卫完成登出清理
window.addEventListener("app:storage-invalidated", handleStorageInvalidated);
// 全局网络状态监听
window.addEventListener("offline", handleOffline);
window.addEventListener("online", handleOnline);
// 异步加载图标集,避免阻塞首屏
initIconifyAsync();
});
onUnmounted(() => {
window.removeEventListener("app:storage-invalidated", handleStorageInvalidated);
window.removeEventListener("keydown", handleGlobalKeydown);
window.removeEventListener("offline", handleOffline);
window.removeEventListener("online", handleOnline);
});
// ─── 全局错误边界 ───
onErrorCaptured((err, _instance, info) => {
console.error(`[ErrorBoundary] ${info}:`, err);
// 开发环境弹窗提示,生产环境静默上报
if (import.meta.env.DEV) {
msg.notify({
title: "组件渲染异常",
message: `${info}: ${err instanceof Error ? err.message : String(err)}`,
type: "error",
duration: 5000,
});
}
return false; // 阻止异常向上冒泡
});
</script>