| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- /**
- * 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<boolean> {
- 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 则进授权/登录入口页。
- *
- * <p>放在 license 校验<b>之后</b>:license 未通过时应停在 /authorization,
- * 不能反过来把用户推到别处。所以这里遇到入口页直接返回。
- * <p>入口页自身会根据 `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();
|