(optionsFactory: () => T, hooks: C
// render chart
await render();
+
+ if (chart) {
+ await onUpdated?.(chart);
+ }
}
scope.run(() => {
diff --git a/src/layouts/modules/global-header/index.vue b/src/layouts/modules/global-header/index.vue
index 592048db..d1e680a7 100644
--- a/src/layouts/modules/global-header/index.vue
+++ b/src/layouts/modules/global-header/index.vue
@@ -38,7 +38,7 @@ const { isFullscreen, toggle } = useFullscreen();
-
+
();
@@ -82,12 +80,8 @@ function getContextMenuDisabledKeys(tabId: string) {
return disabledKeys;
}
-async function handleCloseTab(tab: App.Global.Tab) {
- await tabStore.removeTab(tab.id);
-
- if (themeStore.resetCacheStrategy === 'close') {
- routeStore.resetRouteCache(tab.routeKey);
- }
+function handleCloseTab(tab: App.Global.Tab) {
+ tabStore.removeTab(tab.id);
}
async function refresh() {
diff --git a/src/layouts/modules/theme-drawer/modules/page-fun.vue b/src/layouts/modules/theme-drawer/modules/page-fun.vue
index ef48adc0..aa0aadab 100644
--- a/src/layouts/modules/theme-drawer/modules/page-fun.vue
+++ b/src/layouts/modules/theme-drawer/modules/page-fun.vue
@@ -130,6 +130,9 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
+
+
+
diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts
index 8e3eb7f6..396a1d86 100644
--- a/src/locales/langs/en-us.ts
+++ b/src/locales/langs/en-us.ts
@@ -112,6 +112,9 @@ const local: App.I18n.Schema = {
},
multilingual: {
visible: 'Display multilingual button'
+ },
+ globalSearch: {
+ visible: 'Display GlobalSearch button'
}
},
tab: {
diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts
index 318c88e2..053a3241 100644
--- a/src/locales/langs/zh-cn.ts
+++ b/src/locales/langs/zh-cn.ts
@@ -112,6 +112,9 @@ const local: App.I18n.Schema = {
},
multilingual: {
visible: '显示多语言按钮'
+ },
+ globalSearch: {
+ visible: '显示全局搜索按钮'
}
},
tab: {
diff --git a/src/plugins/app.ts b/src/plugins/app.ts
index 1a0d8999..4943341f 100644
--- a/src/plugins/app.ts
+++ b/src/plugins/app.ts
@@ -25,8 +25,8 @@ export function setupAppVersionNotification() {
const buildTime = await getHtmlBuildTime();
- // If build time hasn't changed, no update is needed
- if (buildTime === BUILD_TIME) {
+ // If failed to get build time or build time hasn't changed, no update is needed.
+ if (!buildTime || buildTime === BUILD_TIME) {
return;
}
@@ -88,16 +88,22 @@ export function setupAppVersionNotification() {
}
}
-async function getHtmlBuildTime() {
+async function getHtmlBuildTime(): Promise {
const baseUrl = import.meta.env.VITE_BASE_URL || '/';
- const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
+ try {
+ const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
- const html = await res.text();
+ if (!res.ok) {
+ console.error('getHtmlBuildTime error:', res.status, res.statusText);
+ return null;
+ }
- const match = html.match(//);
-
- const buildTime = match?.[1] || '';
-
- return buildTime;
+ const html = await res.text();
+ const match = html.match(//);
+ return match?.[1] || null;
+ } catch (error) {
+ console.error('getHtmlBuildTime error:', error);
+ return null;
+ }
}
diff --git a/src/plugins/iconify.ts b/src/plugins/iconify.ts
index f58d669b..8d52c266 100644
--- a/src/plugins/iconify.ts
+++ b/src/plugins/iconify.ts
@@ -1,4 +1,4 @@
-import { addAPIProvider, disableCache } from '@iconify/vue';
+import { addAPIProvider } from '@iconify/vue';
/** Setup the iconify offline */
export function setupIconifyOffline() {
@@ -6,7 +6,5 @@ export function setupIconifyOffline() {
if (VITE_ICONIFY_URL) {
addAPIProvider('', { resources: [VITE_ICONIFY_URL] });
-
- disableCache('all');
}
}
diff --git a/src/service-alova/request/index.ts b/src/service-alova/request/index.ts
index 5b9ba105..642540b9 100644
--- a/src/service-alova/request/index.ts
+++ b/src/service-alova/request/index.ts
@@ -2,8 +2,8 @@ import { createAlovaRequest } from '@sa/alova';
import { createAlovaMockAdapter } from '@sa/alova/mock';
import adapterFetch from '@sa/alova/fetch';
import { useAuthStore } from '@/store/modules/auth';
-import { $t } from '@/locales';
import { getServiceBaseURL } from '@/utils/service';
+import { $t } from '@/locales';
import featureUsers20241014 from '../mocks/feature-users-20241014';
import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type';
diff --git a/src/store/modules/auth/index.ts b/src/store/modules/auth/index.ts
index 71d10a97..ce3327cd 100644
--- a/src/store/modules/auth/index.ts
+++ b/src/store/modules/auth/index.ts
@@ -40,6 +40,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
/** Reset auth store */
async function resetStore() {
+ recordUserId();
+
clearAuthStorage();
authStore.$reset();
@@ -52,6 +54,41 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
routeStore.resetStore();
}
+ /** Record the user ID of the previous login session Used to compare with the current user ID on next login */
+ function recordUserId() {
+ if (!userInfo.userId) {
+ return;
+ }
+
+ // Store current user ID locally for next login comparison
+ localStg.set('lastLoginUserId', userInfo.userId);
+ }
+
+ /**
+ * Check if current login user is different from previous login user If different, clear all tabs
+ *
+ * @returns {boolean} Whether to clear all tabs
+ */
+ function checkTabClear(): boolean {
+ if (!userInfo.userId) {
+ return false;
+ }
+
+ const lastLoginUserId = localStg.get('lastLoginUserId');
+
+ // Clear all tabs if current user is different from previous user
+ if (!lastLoginUserId || lastLoginUserId !== userInfo.userId) {
+ localStg.remove('globalTabs');
+ tabStore.clearTabs();
+
+ localStg.remove('lastLoginUserId');
+ return true;
+ }
+
+ localStg.remove('lastLoginUserId');
+ return false;
+ }
+
/**
* Login
*
@@ -68,7 +105,15 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const pass = await loginByToken(loginToken);
if (pass) {
- await redirectFromLogin(redirect);
+ // Check if the tab needs to be cleared
+ const isClear = checkTabClear();
+ let needRedirect = redirect;
+
+ if (isClear) {
+ // If the tab needs to be cleared,it means we don't need to redirect.
+ needRedirect = false;
+ }
+ await redirectFromLogin(needRedirect);
window.$notification?.success({
title: $t('page.login.common.loginSuccess'),
diff --git a/src/store/modules/tab/index.ts b/src/store/modules/tab/index.ts
index e6fe927c..b9c91c8d 100644
--- a/src/store/modules/tab/index.ts
+++ b/src/store/modules/tab/index.ts
@@ -98,13 +98,24 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
const removeTabIndex = tabs.value.findIndex(tab => tab.id === tabId);
if (removeTabIndex === -1) return;
+ const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
const isRemoveActiveTab = activeTabId.value === tabId;
- const nextTab = tabs.value[removeTabIndex + 1] || homeTab.value;
+ // if remove the last tab, then switch to the second last tab
+ const nextTab = tabs.value[removeTabIndex + 1] || tabs.value[removeTabIndex - 1] || homeTab.value;
+
+ // remove tab
tabs.value.splice(removeTabIndex, 1);
+
+ // if current tab is removed, then switch to next tab
if (isRemoveActiveTab && nextTab) {
await switchRouteByTab(nextTab);
}
+
+ // reset route cache if cache strategy is close
+ if (themeStore.resetCacheStrategy === 'close') {
+ routeStore.resetRouteCache(removedTabRouteKey);
+ }
}
/** remove active tab */
@@ -131,9 +142,26 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
*/
async function clearTabs(excludes: string[] = []) {
const remainTabIds = [...getFixedTabIds(tabs.value), ...excludes];
- const removedTabsIds = tabs.value.map(tab => tab.id).filter(id => !remainTabIds.includes(id));
+
+ // Identify tabs to be removed and collect their routeKeys if strategy is 'close'
+ const tabsToRemove = tabs.value.filter(tab => !remainTabIds.includes(tab.id));
+ const routeKeysToReset: RouteKey[] = [];
+
+ if (themeStore.resetCacheStrategy === 'close') {
+ for (const tab of tabsToRemove) {
+ routeKeysToReset.push(tab.routeKey);
+ }
+ }
+
+ const removedTabsIds = tabsToRemove.map(tab => tab.id);
+
+ // If no tabs are actually being removed based on excludes and fixed tabs, exit
+ if (removedTabsIds.length === 0) {
+ return;
+ }
const isRemoveActiveTab = removedTabsIds.includes(activeTabId.value);
+ // filterTabsByIds returns tabs NOT in removedTabsIds, so these are the tabs that will remain
const updatedTabs = filterTabsByIds(removedTabsIds, tabs.value);
function update() {
@@ -142,13 +170,21 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
if (!isRemoveActiveTab) {
update();
- return;
+ } else {
+ const activeTabCandidate = updatedTabs[updatedTabs.length - 1] || homeTab.value;
+
+ if (activeTabCandidate) {
+ // Ensure there's a tab to switch to
+ await switchRouteByTab(activeTabCandidate);
+ }
+ // Update the tabs array regardless of switch success or if a candidate was found
+ update();
}
- const activeTab = updatedTabs[updatedTabs.length - 1] || homeTab.value;
-
- await switchRouteByTab(activeTab);
- update();
+ // After tabs are updated and route potentially switched, reset cache for removed tabs
+ for (const routeKey of routeKeysToReset) {
+ routeStore.resetRouteCache(routeKey);
+ }
}
const { routerPushByKey } = useRouterPush();
diff --git a/src/theme/settings.ts b/src/theme/settings.ts
index 4cfa185f..63c2cb25 100644
--- a/src/theme/settings.ts
+++ b/src/theme/settings.ts
@@ -30,6 +30,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
},
multilingual: {
visible: true
+ },
+ globalSearch: {
+ visible: true
}
},
tab: {
diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts
index 13603b97..a8657d25 100644
--- a/src/typings/app.d.ts
+++ b/src/typings/app.d.ts
@@ -58,6 +58,10 @@ declare namespace App {
/** Whether to show the multilingual */
visible: boolean;
};
+ globalSearch: {
+ /** Whether to show the GlobalSearch */
+ visible: boolean;
+ };
};
/** Tab */
tab: {
@@ -377,6 +381,9 @@ declare namespace App {
multilingual: {
visible: string;
};
+ globalSearch: {
+ visible: string;
+ };
};
tab: {
visible: string;
diff --git a/src/typings/common.d.ts b/src/typings/common.d.ts
index 06b526c9..652bc3bc 100644
--- a/src/typings/common.d.ts
+++ b/src/typings/common.d.ts
@@ -14,7 +14,7 @@ declare namespace CommonType {
* @property value: The option value
* @property label: The option label
*/
- type Option = { value: K; label: string };
+ type Option = { value: K; label: M };
type YesOrNo = 'Y' | 'N';
diff --git a/src/typings/storage.d.ts b/src/typings/storage.d.ts
index 02668fcc..b7cc3f1c 100644
--- a/src/typings/storage.d.ts
+++ b/src/typings/storage.d.ts
@@ -37,5 +37,7 @@ declare namespace StorageType {
layout: UnionKey.ThemeLayoutMode;
siderCollapse: boolean;
};
+ /** The last login user id */
+ lastLoginUserId: string;
}
}
diff --git a/src/typings/vite-env.d.ts b/src/typings/vite-env.d.ts
index d6ba76f6..c90fb2c2 100644
--- a/src/typings/vite-env.d.ts
+++ b/src/typings/vite-env.d.ts
@@ -108,6 +108,8 @@ declare namespace Env {
readonly VITE_AUTOMATICALLY_DETECT_UPDATE?: CommonType.YesOrNo;
/** show proxy url log in terminal */
readonly VITE_PROXY_LOG?: CommonType.YesOrNo;
+ /** The launch editor */
+ readonly VITE_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
}
}
diff --git a/src/utils/common.ts b/src/utils/common.ts
index dc9a368f..b7e7f654 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -22,7 +22,7 @@ export function transformRecordToOption>(record
return Object.entries(record).map(([value, label]) => ({
value,
label
- })) as CommonType.Option[];
+ })) as CommonType.Option[];
}
/**
@@ -30,10 +30,10 @@ export function transformRecordToOption>(record
*
* @param options
*/
-export function translateOptions(options: CommonType.Option[]) {
+export function translateOptions(options: CommonType.Option[]) {
return options.map(option => ({
...option,
- label: $t(option.label as App.I18n.I18nKey)
+ label: $t(option.label)
}));
}
diff --git a/src/views/about/index.vue b/src/views/about/index.vue
index 2812fbb1..701e3ce6 100644
--- a/src/views/about/index.vue
+++ b/src/views/about/index.vue
@@ -1,7 +1,7 @@