/** * Copyright (c) 2013-Now https://jeesite.com All rights reserved. * No deletion without permission, or be held responsible to law. * @author ThinkGem */ import 'virtual:uno.css'; import 'ant-design-vue/dist/reset.css'; import '@/core/design/index.less'; // AI 数据分析模块全局样式(设计 token / markdown 正文 / 消息布局基元) import '@/ai/index'; import App from './App.vue'; import { createApp } from 'vue'; import { isDevMode } from '@/core/utils/env'; import { registerGlobComp } from '@/core/components/registerGlobComp'; import { initAppConfigStore } from '@/core/logics/initAppConfig'; import { setupErrorHandle } from '@/core/logics/error-handle'; import { setupGlobDirectives } from '@/core/directives'; import { setupI18n } from '@/core/locales/setupI18n'; import { setupRouter, router } from '@/core/router'; import { setupRouterGuard } from '@/core/router/guard'; import { setupStore } from '@/core/store'; import { authorizationInfoApi, checkAuthApi, healthApi } from '@/core/api/sys/login'; import { PageEnum } from '@/core/enums/pageEnum'; import { useUserStoreWithOut } from '@/core/store/modules/case'; import { getToken } from '@/core/utils/auth'; import { extractAuthorizationCache, getAuthorizationExpiredMessage, isAuthorizationExpired, } from '@/core/utils/license'; import { setupAntdTabsListenerPatch } from './setupAntdTabsListenerPatch'; import { setupAntdPopupBehavior } from './setupAntdPopupBehavior'; import { closeSmartWindow, focusMainWindow, initWindowRole, isSmartWindow } from '@/core/logics/appWindow'; import { initSessionSync } from '@/core/logics/sessionSync'; import { isServerSetupMissing } from '@/core/settings/serverSetting'; const STARTUP_PROBE_RETRIES = 10; const STARTUP_PROBE_INTERVAL = 3000; /** * 更新启动 loading 页上的提示。 * *
此时 Vue 还没 mount,只能直接操作 index.html 里的静态 DOM。
* 重连中显示红色文案 + 文字前的小旋转图标;成功或最终失败时移除旋转图标。
*/
function updateStartupLoadingTip(text: string, state: 'retrying' | 'success' | 'failed' = 'retrying') {
const tip = document.querySelector 按固定节拍重试而不是「探测超时 + 再等一个间隔」,单次探测上限也是 3 秒,
* 保证整体耗时不随失败次数膨胀;10 次仍未连上就停转,由调用方展示重连/修改服务地址入口。
*/
async function waitForBackendReady(
maxRetries: number = STARTUP_PROBE_RETRIES,
interval: number = STARTUP_PROBE_INTERVAL,
): Promise 重连会重新执行一轮 10 次探测;连上后整页 reload,走正常启动流程。
* 修改服务地址则进入授权页的地址卡片(带 error=unreachable),避免绑错地址后无路可走。
*/
function showReconnectState(onServerSetup: () => void) {
const reconnect = document.querySelector 放在 license 校验之后:license 未通过时应停在 /authorization,
* 不能反过来把用户推到别处。所以这里遇到入口页直接返回。
* 入口页自身会根据 `checkAuthApi()` 结果决定显示授权卡片还是登录卡片。
*/
async function ensureLoggedIn() {
const current = router.currentRoute.value;
// 自持页面不做登录门禁:授权页本身就是登录入口,地址页必须在未登录时也能操作
if (current.path === PageEnum.BASE_AUTHORIZATION || current.path === PageEnum.BASE_SERVER_SETUP) {
return;
}
if (!getToken()) {
// /login 已删除,不作为回跳来源,否则登录后会落到 404
const source =
current.fullPath && current.fullPath !== '/' && current.path !== PageEnum.LEGACY_LOGIN ? current.fullPath : '';
await router.replace({
path: PageEnum.BASE_AUTHORIZATION,
query: source ? { redirect: source } : undefined,
});
}
}
async function bootstrap() {
// 窗口角色必须先解析:路由守卫要靠它区分主窗口(传统壳)与智能工作台窗口。
// 浏览器里没有 IPC,initWindowRole 会回落到 'main',一切保持单窗口行为。
await initWindowRole();
setupAntdPopupBehavior();
const app = createApp(App);
// Configure store
setupStore(app);
// Initialize internal system configuration
initAppConfigStore();
// 跨窗口共享状态:案件/账号/凭据失效的广播处理(必须在路由初始化前注册)
initSessionSync();
// Register global components
registerGlobComp(app);
// Multilingual configuration
// Asynchronous case: language files may be obtained from the server side
await setupI18n(app);
// Configure routing
setupRouter(app);
// router-guard
setupRouterGuard(router);
// 桌面端未绑定后端地址:先绑定,健康检查 / 授权 / 登录都要等地址确定后才有意义。
// 必须在 waitForBackendReady 之前判断,否则会在没有地址的情况下空等。
// 地址卡片已并入 /authorization,由页面根据 isServerSetupMissing() 自己切换形态。
if (isServerSetupMissing()) {
if (isSmartWindow()) {
await focusMainWindow();
void closeSmartWindow();
return;
}
await router.replace(PageEnum.BASE_AUTHORIZATION);
await router.isReady();
setupGlobDirectives(app);
setupErrorHandle(app);
app.mount('#app');
return;
}
// Wait for backend to be ready
// 统一 10 次 × 3 秒(约 30 秒),loading 页展示重试次数;
// 直接打开授权页(含旧的服务地址页地址)时不做探测:页面对失败已有自己的展示。
const startupPath = getStartupRouteInfo().path;
const skipBackendProbe = startupPath === PageEnum.BASE_AUTHORIZATION || startupPath === PageEnum.BASE_SERVER_SETUP;
const backendReady = skipBackendProbe ? true : await waitForBackendReady();
// 后端不可达:停在 loading 页提供「重连 / 修改服务地址」,不先带着坏地址进入授权校验
if (!backendReady) {
showReconnectState(async () => {
// 智能窗口只承载工作台:地址/授权问题交回主窗口处理
if (isSmartWindow()) {
await focusMainWindow();
void closeSmartWindow();
return;
}
await router.replace({ path: PageEnum.BASE_AUTHORIZATION, query: { error: 'unreachable' } });
await router.isReady();
setupGlobDirectives(app);
setupErrorHandle(app);
app.mount('#app');
});
return;
}
await resolveStartupRoute();
// ★ 必须先等首次导航落定,再判登录态。
// 否则此时 router.currentRoute 还是初始占位(path='/'),ensureLoggedIn 里的
// replace 会按「来源不可用」走到 `query: undefined`,把 URL 上的
// redirect / forceAuth / expired / message 全部冲掉 —— 入口页自己的 query 契约就失效了。
await router.isReady();
// 授权通过后再校验登录态:无 token 直接进授权/登录入口页
await ensureLoggedIn();
// 智能窗口只承载工作台:bootstrap 若被判定到授权页,说明凭据/授权已失效。
// 关闭自己、把主窗口拉到前台,由主窗口做唯一的提示与跳转。
if (isSmartWindow() && router.currentRoute.value.path === PageEnum.BASE_AUTHORIZATION) {
await focusMainWindow();
void closeSmartWindow();
return;
}
// Register global directive
setupGlobDirectives(app);
// Configure global error handling
setupErrorHandle(app);
// https://next.router.js.org/api/#isready
// await router.isReady();
// Dynamic Form
app.mount('#app');
}
// 仅开发模式显示
if (isDevMode()) {
setupAntdTabsListenerPatch();
}
if (!isDevMode()) {
}
bootstrap().then();