feat: Implement dashboard caching and refresh functionality (#11100)

* feat: Implement dashboard caching and refresh functionality

* feat: Enhance dashboard and launcher with hover refresh functionality and improve caching strategy

* feat: Introduce dashboard caching utility functions for improved data management
This commit is contained in:
KOMATA 2025-11-27 17:54:54 +08:00 committed by GitHub
parent b5e56c6b65
commit e89311d5d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 249 additions and 19 deletions

View file

@ -238,6 +238,10 @@ const logout = () => {
})
.then(async () => {
await logOutApi();
sessionStorage.removeItem('dashboardCache');
localStorage.removeItem('dashboardCache');
sessionStorage.removeItem('upgradeChecked');
localStorage.removeItem('upgradeChecked');
router.push({ name: 'entrance', params: { code: globalStore.entrance } });
globalStore.setLogStatus(false);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));

View file

@ -0,0 +1,61 @@
const DASHBOARD_CACHE_KEY = 'dashboardCache';
type CacheEntry = {
value: any;
expireAt: number;
};
const readCache = (): Record<string, CacheEntry> | null => {
try {
const cacheRaw = localStorage.getItem(DASHBOARD_CACHE_KEY);
return cacheRaw ? JSON.parse(cacheRaw) : {};
} catch {
return null;
}
};
export const getDashboardCache = (key: string) => {
const cache = readCache();
if (!cache) return null;
const entry = cache[key];
if (entry && entry.expireAt > Date.now()) {
return entry.value;
}
return null;
};
export const setDashboardCache = (key: string, value: any, ttl: number) => {
try {
const cacheRaw = localStorage.getItem(DASHBOARD_CACHE_KEY);
const cache = cacheRaw ? JSON.parse(cacheRaw) : {};
cache[key] = {
value,
expireAt: Date.now() + ttl,
};
localStorage.setItem(DASHBOARD_CACHE_KEY, JSON.stringify(cache));
} catch {
localStorage.removeItem(DASHBOARD_CACHE_KEY);
}
};
export const clearDashboardCache = () => {
localStorage.removeItem(DASHBOARD_CACHE_KEY);
};
export const clearDashboardCacheByPrefix = (prefixes: string[]) => {
try {
const cacheRaw = localStorage.getItem(DASHBOARD_CACHE_KEY);
if (!cacheRaw) return;
const cache = JSON.parse(cacheRaw);
Object.keys(cache).forEach((key: string) => {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
delete cache[key];
}
});
localStorage.setItem(DASHBOARD_CACHE_KEY, JSON.stringify(cache));
} catch {
clearDashboardCache();
}
};
export { DASHBOARD_CACHE_KEY };

View file

@ -1,7 +1,13 @@
<template>
<div>
<CardWithHeader :header="$t('app.app')" class="card-interval" v-loading="loading">
<CardWithHeader
:header="$t('app.app')"
class="card-interval"
v-loading="loading"
@mouseenter="refreshLauncherOnHover"
>
<template #header-r>
<el-button class="h-button-setting" link icon="Refresh" @click="refreshLauncher" />
<el-popover placement="left" :width="226" trigger="click">
<el-input size="small" v-model="filter" clearable @input="loadOption()" />
<el-table :show-header="false" :data="options" max-height="150px">
@ -185,25 +191,41 @@ import { changeLauncherStatus, loadAppLauncher, loadAppLauncherOption } from '@/
import i18n from '@/lang';
import { GlobalStore } from '@/store';
import { MsgSuccess } from '@/utils/message';
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { jumpToPath } from '@/utils/util';
import { jumpToInstall } from '@/utils/app';
import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
import { clearDashboardCacheByPrefix, getDashboardCache, setDashboardCache } from '@/utils/dashboardCache';
const router = useRouter();
const globalStore = GlobalStore();
const DASHBOARD_CACHE_TTL = {
launcherOption: 5 * 60 * 1000,
launcher: 10 * 60 * 1000,
systemIP: 10 * 60 * 1000,
};
const clearLauncherCache = () => {
clearDashboardCacheByPrefix(['appLauncherOption-', 'appLauncher', 'systemIP']);
};
let loading = ref(false);
let apps = ref([]);
const options = ref([]);
const filter = ref();
const launcherFromCache = ref(false);
const launcherOptionFromCache = ref(false);
const systemIPFromCache = ref(false);
const hasRefreshedLauncherOnHover = ref(false);
const mobile = computed(() => {
return globalStore.isMobile();
});
const defaultLink = ref('');
const acceptParams = (): void => {
hasRefreshedLauncherOnHover.value = false;
search();
loadOption();
getConfig();
@ -215,17 +237,31 @@ const goInstall = (key: string, type: string) => {
}
};
const search = async () => {
const search = async (force?: boolean) => {
loading.value = true;
const cache = force ? null : getDashboardCache('appLauncher');
if (cache !== null) {
apps.value = cache;
launcherFromCache.value = true;
for (const item of apps.value) {
if (item.detail && item.detail.length !== 0) {
item.currentRow = item.detail[0];
}
}
loading.value = false;
return;
}
await loadAppLauncher()
.then((res) => {
loading.value = false;
apps.value = res.data;
launcherFromCache.value = false;
for (const item of apps.value) {
if (item.detail && item.detail.length !== 0) {
item.currentRow = item.detail[0];
}
}
setDashboardCache('appLauncher', apps.value, DASHBOARD_CACHE_TTL.launcher);
})
.finally(() => {
loading.value = false;
@ -238,7 +274,9 @@ const onChangeStatus = async (row: any) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
clearLauncherCache();
search();
loadOption();
})
.catch(() => {
loading.value = false;
@ -249,12 +287,18 @@ const toLink = (link: string) => {
window.open(link, '_blank');
};
const getConfig = async () => {
const getConfig = async (force?: boolean) => {
try {
const res = await getAgentSettingByKey('SystemIP');
if (res.data != '') {
defaultLink.value = res.data;
const cache = force ? null : getDashboardCache('systemIP');
if (cache !== null) {
defaultLink.value = cache;
systemIPFromCache.value = true;
return;
}
const res = await getAgentSettingByKey('SystemIP');
defaultLink.value = res.data || '';
systemIPFromCache.value = false;
setDashboardCache('systemIP', defaultLink.value, DASHBOARD_CACHE_TTL.systemIP);
} catch (error) {}
};
@ -286,9 +330,33 @@ const onOperate = async (operation: string, row: any) => {
});
};
const loadOption = async () => {
const loadOption = async (force?: boolean) => {
const cacheKey = `appLauncherOption-${filter.value || ''}`;
const cache = force ? null : getDashboardCache(cacheKey);
if (cache !== null) {
options.value = cache;
launcherOptionFromCache.value = true;
return;
}
const res = await loadAppLauncherOption(filter.value || '');
options.value = res.data || [];
launcherOptionFromCache.value = false;
setDashboardCache(cacheKey, options.value, DASHBOARD_CACHE_TTL.launcherOption);
};
const refreshLauncher = async () => {
clearLauncherCache();
hasRefreshedLauncherOnHover.value = false;
await Promise.allSettled([loadOption(true), search(true), getConfig(true)]);
};
const refreshLauncherOnHover = async () => {
if (hasRefreshedLauncherOnHover.value) return;
if (!launcherFromCache.value && !launcherOptionFromCache.value && !systemIPFromCache.value) return;
hasRefreshedLauncherOnHover.value = true;
await loadOption(true);
await search(true);
await getConfig(true);
};
defineExpose({

View file

@ -77,6 +77,7 @@
:header="$t('menu.monitor')"
class="card-interval chart-card"
v-loading="!chartsOption['networkChart']"
@mouseenter="refreshOptionsOnHover"
>
<template #header-r>
<el-radio-group
@ -173,6 +174,7 @@
<el-carousel-item key="systemInfo">
<CardWithHeader :header="$t('home.systemInfo')">
<template #header-r>
<el-button class="h-button-setting" @click="refreshDashboard" link icon="Refresh" />
<el-button
class="h-button-setting"
@click="toggleSensitiveInfo"
@ -334,9 +336,18 @@ import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
import { routerToFileWithPath, routerToPath } from '@/utils/router';
import { getWelcomePage } from '@/api/modules/auth';
import { clearDashboardCache, getDashboardCache, setDashboardCache } from '@/utils/dashboardCache';
const router = useRouter();
const globalStore = GlobalStore();
const DASHBOARD_CACHE_TTL = {
safeStatus: 10 * 60 * 1000,
netOptions: 60 * 60 * 1000,
ioOptions: 60 * 60 * 1000,
};
const UPGRADE_CHECK_KEY = 'upgradeChecked';
const UPGRADE_CHECK_EXPIRE = 24 * 60 * 60 * 1000;
const statusRef = ref();
const appRef = ref();
@ -364,6 +375,9 @@ const timeNetDatas = ref<Array<string>>([]);
const simpleNodes = ref([]);
const ioOptions = ref();
const netOptions = ref();
const netOptionsFromCache = ref(false);
const ioOptionsFromCache = ref(false);
const hasRefreshedOptionsOnHover = ref(false);
const licenseRef = ref();
const quickJumpRef = ref();
@ -463,10 +477,30 @@ const changeOption = async () => {
loadData();
};
const onLoadNetworkOptions = async () => {
const applyDefaultNetOption = () => {
if (!netOptions.value || netOptions.value.length === 0) return;
const defaultNet = globalStore.defaultNetwork || netOptions.value[0];
if (defaultNet && searchInfo.netOption !== defaultNet) {
searchInfo.netOption = defaultNet;
if (!isStatusInit.value) {
onLoadBaseInfo(false, 'network');
}
}
};
const onLoadNetworkOptions = async (force?: boolean) => {
const cache = force ? null : getDashboardCache('netOptions');
if (cache !== null) {
netOptions.value = cache;
netOptionsFromCache.value = true;
applyDefaultNetOption();
return;
}
const res = await getNetworkOptions();
netOptions.value = res.data;
searchInfo.netOption = globalStore.defaultNetwork || (netOptions.value && netOptions.value[0]);
netOptionsFromCache.value = false;
setDashboardCache('netOptions', res.data, DASHBOARD_CACHE_TTL.netOptions);
applyDefaultNetOption();
};
const onLoadSimpleNode = async () => {
@ -474,10 +508,30 @@ const onLoadSimpleNode = async () => {
simpleNodes.value = res.data || [];
};
const onLoadIOOptions = async () => {
const applyDefaultIOOption = () => {
if (!ioOptions.value || ioOptions.value.length === 0) return;
const defaultIO = globalStore.defaultIO || ioOptions.value[0];
if (defaultIO && searchInfo.ioOption !== defaultIO) {
searchInfo.ioOption = defaultIO;
if (!isStatusInit.value) {
onLoadBaseInfo(false, 'io');
}
}
};
const onLoadIOOptions = async (force?: boolean) => {
const cache = force ? null : getDashboardCache('ioOptions');
if (cache !== null) {
ioOptions.value = cache;
ioOptionsFromCache.value = true;
applyDefaultIOOption();
return;
}
const res = await getIOOptions();
ioOptions.value = res.data;
searchInfo.ioOption = globalStore.defaultIO || (ioOptions.value && ioOptions.value[0]);
ioOptionsFromCache.value = false;
setDashboardCache('ioOptions', ioOptions.value, DASHBOARD_CACHE_TTL.ioOptions);
applyDefaultIOOption();
};
const onLoadBaseInfo = async (isInit: boolean, range: string) => {
@ -530,6 +584,15 @@ const toggleSensitiveInfo = () => {
showSensitiveInfo.value = !showSensitiveInfo.value;
};
const refreshDashboard = async () => {
clearDashboardCache();
localStorage.removeItem(UPGRADE_CHECK_KEY);
hasRefreshedOptionsOnHover.value = false;
await onLoadBaseInfo(false, 'all');
await Promise.allSettled([onLoadSimpleNode(), onLoadNetworkOptions(true), onLoadIOOptions(true), loadSafeStatus()]);
await loadUpgradeStatus();
};
const jumpPanel = (row: any) => {
let entrance = row.securityEntrance.startsWith('/') ? row.securityEntrance.slice(1) : row.securityEntrance;
entrance = entrance ? '/' + entrance : '';
@ -670,17 +733,26 @@ const hideEntrance = () => {
};
const loadUpgradeStatus = async () => {
const checkedAt = Number(localStorage.getItem(UPGRADE_CHECK_KEY));
if (checkedAt && Date.now() - checkedAt < UPGRADE_CHECK_EXPIRE) return;
const res = await loadUpgradeInfo();
if (res && (res.data.testVersion || res.data.newVersion || res.data.latestVersion)) {
globalStore.hasNewVersion = true;
} else {
globalStore.hasNewVersion = false;
}
localStorage.setItem(UPGRADE_CHECK_KEY, Date.now().toString());
};
const loadSafeStatus = async () => {
const cache = getDashboardCache('safeStatus');
if (cache !== null) {
isSafety.value = cache;
return;
}
const res = await getSettingInfo();
isSafety.value = res.data.securityEntrance;
setDashboardCache('safeStatus', isSafety.value, DASHBOARD_CACHE_TTL.safeStatus);
};
const loadSource = (row: any) => {
@ -712,15 +784,40 @@ const toUpload = () => {
licenseRef.value.acceptParams();
};
const fetchData = () => {
const refreshOptionsOnHover = async () => {
if (hasRefreshedOptionsOnHover.value) return;
if (!netOptionsFromCache.value && !ioOptionsFromCache.value) return;
hasRefreshedOptionsOnHover.value = true;
if (netOptionsFromCache.value) {
await onLoadNetworkOptions(true);
}
if (ioOptionsFromCache.value) {
await onLoadIOOptions(true);
}
};
const scheduleDeferredFetch = () => {
setTimeout(() => {
onLoadSimpleNode();
}, 200);
setTimeout(() => {
onLoadNetworkOptions();
}, 400);
setTimeout(() => {
onLoadIOOptions();
}, 600);
setTimeout(() => {
loadUpgradeStatus();
}, 800);
};
const fetchData = async () => {
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
loadSafeStatus();
loadUpgradeStatus();
onLoadNetworkOptions();
onLoadIOOptions();
onLoadBaseInfo(true, 'all');
onLoadSimpleNode();
hasRefreshedOptionsOnHover.value = false;
await loadSafeStatus();
await onLoadBaseInfo(true, 'all');
scheduleDeferredFetch();
};
const loadWelcome = async () => {