/** * 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 { isServerBindingEnabled, isServerSetupMissing } from '@/core/settings/serverSetting'; async function waitForBackendReady(maxRetries: number = 60, interval: number = 2000): Promise { console.log('[bootstrap] waiting for backend to be ready...'); for (let i = 0; i < maxRetries; i++) { try { const response = await healthApi(); console.log(response); if (response?.code === 200) { console.log(`[bootstrap] backend ready after ${i + 1} attempts`); return true; } } catch (error) { // Backend not ready yet, continue waiting } if (i < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, interval)); } } console.warn(`[bootstrap] backend not ready after ${maxRetries} attempts, continuing anyway`); return false; } function getStartupRouteInfo() { // hash 模式:路径在 location.hash 中(如 #/data/index) const hash = window.location.hash?.replace(/^#/, '') || ''; let fullPath: string; if (hash) { const [pathWithQuery = '/'] = hash.split('#'); fullPath = pathWithQuery || '/'; } else if (import.meta.env.VITE_ROUTE_WEB_HISTORY != 'true') { // hash 模式且没有 hash:不能退回 pathname —— 打包后是 file:///.../index.html, // 拿它当路由会落到 404,统一按根路由处理 fullPath = '/'; } else { // history 模式:无 hash,从 pathname 恢复,并剥离 VITE_PUBLIC_PATH 前缀 let pathname = window.location.pathname || '/'; const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/'; if (publicPath && publicPath !== '/') { const normalizedBase = publicPath.replace(/\/+$/, ''); if (normalizedBase && pathname.startsWith(normalizedBase + '/')) { pathname = pathname.slice(normalizedBase.length); } } fullPath = pathname + (window.location.search || ''); } const [path = '/'] = fullPath.split('?'); return { path: path || '/', fullPath: fullPath || '/', }; } function shouldRunStartupAuthCheck(path: string) { // 授权页与服务地址页都是自持页面:地址页是机器级设置,不参与启动授权流程 return path !== PageEnum.BASE_AUTHORIZATION && path !== PageEnum.BASE_SERVER_SETUP; } function getAuthorizedTarget(fullPath: string, path: string) { // 首页、旧的测试页、入口页自身、服务地址页、以及已删除的 /login,统一落到案件页 if ( path === '/' || path === '/test/testData/list' || path === PageEnum.BASE_AUTHORIZATION || path === PageEnum.BASE_SERVER_SETUP || path === PageEnum.LEGACY_LOGIN ) { return PageEnum.BASE_CASE; } return fullPath || PageEnum.BASE_CASE; } function syncAuthorizationCache(source?: unknown) { const userStore = useUserStoreWithOut(); const { authorizationLabel, overDateTime, authorizationUnit } = extractAuthorizationCache(source); userStore.setPageCache('authorizationLabel', authorizationLabel); userStore.setPageCache('licenseOverDateTime', overDateTime); userStore.setPageCache('authorizationUnit', authorizationUnit); return { authorizationLabel, overDateTime, authorizationUnit, }; } async function resolveAuthorizationCache(source?: unknown) { const cache = syncAuthorizationCache(source); if (cache.overDateTime) { return cache; } try { const res = await authorizationInfoApi(); return syncAuthorizationCache(res); } catch { return cache; } } async function redirectToExpiredAuthorization(overDateTime?: unknown) { await router.replace({ path: PageEnum.BASE_AUTHORIZATION, query: { skipAuthCheck: '1', expired: '1', message: getAuthorizationExpiredMessage(overDateTime), }, }); } async function resolveStartupRoute() { const { path, fullPath } = getStartupRouteInfo(); if (!shouldRunStartupAuthCheck(path)) { return; } try { const res = await checkAuthApi(); if (res?.code === 200) { const cache = await resolveAuthorizationCache(res); if (isAuthorizationExpired(cache.overDateTime)) { await redirectToExpiredAuthorization(cache.overDateTime); return; } const targetPath = getAuthorizedTarget(fullPath, path); await router.replace(targetPath); return; } } catch (error: any) { const status = error?.response?.status || error?.status; if (status === 200 && error?.response?.data?.code === 200) { const cache = await resolveAuthorizationCache(error?.response?.data); if (isAuthorizationExpired(cache.overDateTime)) { await redirectToExpiredAuthorization(cache.overDateTime); return; } const targetPath = getAuthorizedTarget(fullPath, path); await router.replace(targetPath); return; } } syncAuthorizationCache(); await router.replace({ path: PageEnum.BASE_AUTHORIZATION, query: { skipAuthCheck: '1', }, }); } /** * 登录门禁:授权校验通过后,若本地没有 token 则进授权/登录入口页。 * *

放在 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 // 绑定地址是刚测试通过的,按「本地后端冷启动」等两分钟没有意义,收紧到约 1 分钟。 // 直接打开授权页(含旧的服务地址页地址)时不做探测:页面对失败已有自己的展示。 const bindingEnabled = isServerBindingEnabled(); const startupPath = getStartupRouteInfo().path; const skipBackendProbe = startupPath === PageEnum.BASE_AUTHORIZATION || startupPath === PageEnum.BASE_SERVER_SETUP; const backendReady = skipBackendProbe ? true : await waitForBackendReady(bindingEnabled ? 6 : 60, bindingEnabled ? 1500 : 2000); // 地址已绑定但服务不可达:回到入口页并切到服务地址卡片,而不是带着坏地址继续跑授权校验 if (!backendReady && bindingEnabled) { 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();