Skip to content

Commit 82e7502

Browse files
catDforDGargantua
andauthored
fix(dashboard): stabilize sidebar customization state (#5405) (#5670)
- use stable sidebar list keys to avoid vnode reuse drift - sanitize persisted opened groups against current sidebar menu - guard non-array customization keys from localStorage Co-authored-by: Gargantua <22532097@zju.edu.cn>
1 parent 866e546 commit 82e7502

File tree

3 files changed

+103
-14
lines changed

3 files changed

+103
-14
lines changed

dashboard/src/layouts/full/vertical-sidebar/NavItem.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const isItemActive = computed(() => {
3838
</template>
3939
4040
<!-- children -->
41-
<template v-for="(child, index) in item.children" :key="index">
41+
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
4242
<NavItem :item="child" :level="(level || 0) + 1" />
4343
</template>
4444
</v-list-group>

dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,60 @@ import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
1010
const { t, locale } = useI18n();
1111
1212
const customizer = useCustomizerStore();
13-
const sidebarMenu = shallowRef(sidebarItems);
13+
14+
function collectGroupValues(items, values = new Set()) {
15+
items.forEach((item) => {
16+
if (item?.children && item.title) {
17+
values.add(item.title);
18+
collectGroupValues(item.children, values);
19+
}
20+
});
21+
return values;
22+
}
23+
24+
function sanitizeOpenedItems(items, menuItems) {
25+
if (!Array.isArray(items)) {
26+
return [];
27+
}
28+
29+
const groupValues = collectGroupValues(menuItems);
30+
return items.filter((item) => typeof item === 'string' && groupValues.has(item));
31+
}
32+
33+
function getInitialOpenedItems(menuItems) {
34+
try {
35+
const stored = JSON.parse(localStorage.getItem('sidebar_openedItems') || '[]');
36+
return sanitizeOpenedItems(stored, menuItems);
37+
} catch {
38+
return [];
39+
}
40+
}
41+
42+
const sidebarMenu = shallowRef(applySidebarCustomization(sidebarItems));
1443
1544
// 侧边栏分组展开状态持久化
16-
const openedItems = ref(JSON.parse(localStorage.getItem('sidebar_openedItems') || '[]'));
17-
watch(openedItems, (val) => localStorage.setItem('sidebar_openedItems', JSON.stringify(val)), { deep: true });
45+
const openedItems = ref(getInitialOpenedItems(sidebarMenu.value));
46+
watch(openedItems, (val) => {
47+
localStorage.setItem('sidebar_openedItems', JSON.stringify(sanitizeOpenedItems(val, sidebarMenu.value)));
48+
}, { deep: true });
49+
50+
function refreshSidebarMenu() {
51+
sidebarMenu.value = applySidebarCustomization(sidebarItems);
52+
openedItems.value = sanitizeOpenedItems(openedItems.value, sidebarMenu.value);
53+
}
1854
1955
// Apply customization on mount and listen for storage changes
2056
const handleStorageChange = (e) => {
2157
if (e.key === 'astrbot_sidebar_customization') {
22-
sidebarMenu.value = applySidebarCustomization(sidebarItems);
58+
refreshSidebarMenu();
2359
}
2460
};
2561
2662
const handleCustomEvent = () => {
27-
sidebarMenu.value = applySidebarCustomization(sidebarItems);
63+
refreshSidebarMenu();
2864
};
2965
3066
onMounted(() => {
31-
sidebarMenu.value = applySidebarCustomization(sidebarItems);
32-
3367
window.addEventListener('storage', handleStorageChange);
3468
window.addEventListener('sidebar-customization-changed', handleCustomEvent);
3569
});
@@ -255,7 +289,7 @@ function openChangelogDialog() {
255289
>
256290
<div class="sidebar-container">
257291
<v-list class="pa-4 listitem flex-grow-1" v-model:opened="openedItems" :open-strategy="'multiple'">
258-
<template v-for="(item, i) in sidebarMenu" :key="i">
292+
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
259293
<NavItem :item="item" class="leftPadding" />
260294
</template>
261295
</v-list>

dashboard/src/utils/sidebarCustomization.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ export function clearSidebarCustomization() {
5252
export function resolveSidebarItems(defaultItems, customization, options = {}) {
5353
const { cloneItems = false, assembleMoreGroup = false } = options;
5454

55+
const normalizeKeys = (keys = []) => {
56+
const list = Array.isArray(keys) ? keys : [];
57+
const deduped = [];
58+
const seen = new Set();
59+
60+
list.forEach((key) => {
61+
if (typeof key !== 'string') return;
62+
if (seen.has(key)) return;
63+
seen.add(key);
64+
deduped.push(key);
65+
});
66+
67+
return deduped;
68+
};
69+
5570
const all = new Map();
5671
const defaultMain = [];
5772
const defaultMore = [];
@@ -70,9 +85,23 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
7085
});
7186

7287
const hasCustomization = Boolean(customization);
73-
const mainKeys = hasCustomization ? customization.mainItems || [] : defaultMain;
74-
const moreKeys = hasCustomization ? customization.moreItems || [] : defaultMore;
75-
const used = hasCustomization ? new Set([...mainKeys, ...moreKeys]) : new Set(defaultMain.concat(defaultMore));
88+
let mainKeys = hasCustomization ? normalizeKeys(customization.mainItems || []) : [...defaultMain];
89+
let moreKeys = hasCustomization ? normalizeKeys(customization.moreItems || []) : [...defaultMore];
90+
91+
if (hasCustomization) {
92+
mainKeys = mainKeys.filter(title => all.has(title));
93+
moreKeys = moreKeys.filter(title => all.has(title));
94+
}
95+
96+
if (hasCustomization) {
97+
// 如果同一项同时出现在主区与更多区,主区优先。
98+
const mainSet = new Set(mainKeys);
99+
moreKeys = moreKeys.filter(title => !mainSet.has(title));
100+
}
101+
102+
const used = hasCustomization
103+
? new Set([...mainKeys, ...moreKeys])
104+
: new Set(defaultMain.concat(defaultMore));
76105

77106
const mainItems = mainKeys
78107
.map(title => all.get(title))
@@ -119,7 +148,13 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
119148
}
120149
}
121150

122-
return { mainItems, moreItems, merged };
151+
return {
152+
mainItems,
153+
moreItems,
154+
merged,
155+
normalizedMainKeys: [...mainKeys],
156+
normalizedMoreKeys: [...moreKeys]
157+
};
123158
}
124159

125160
/**
@@ -129,9 +164,29 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
129164
*/
130165
export function applySidebarCustomization(defaultItems) {
131166
const customization = getSidebarCustomization();
132-
const { merged } = resolveSidebarItems(defaultItems, customization, {
167+
const {
168+
merged,
169+
normalizedMainKeys,
170+
normalizedMoreKeys
171+
} = resolveSidebarItems(defaultItems, customization, {
133172
cloneItems: true,
134173
assembleMoreGroup: true
135174
});
175+
176+
if (customization) {
177+
const rawMainKeys = Array.isArray(customization.mainItems) ? customization.mainItems : [];
178+
const rawMoreKeys = Array.isArray(customization.moreItems) ? customization.moreItems : [];
179+
const hasChanged =
180+
JSON.stringify(rawMainKeys) !== JSON.stringify(normalizedMainKeys) ||
181+
JSON.stringify(rawMoreKeys) !== JSON.stringify(normalizedMoreKeys);
182+
183+
if (hasChanged) {
184+
setSidebarCustomization({
185+
mainItems: normalizedMainKeys,
186+
moreItems: normalizedMoreKeys
187+
});
188+
}
189+
}
190+
136191
return merged || defaultItems;
137192
}

0 commit comments

Comments
 (0)