- 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>
This commit is contained in:
@@ -38,7 +38,7 @@ const isItemActive = computed(() => {
|
||||
</template>
|
||||
|
||||
<!-- children -->
|
||||
<template v-for="(child, index) in item.children" :key="index">
|
||||
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
|
||||
<NavItem :item="child" :level="(level || 0) + 1" />
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
@@ -10,26 +10,60 @@ import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const sidebarMenu = shallowRef(sidebarItems);
|
||||
|
||||
function collectGroupValues(items, values = new Set()) {
|
||||
items.forEach((item) => {
|
||||
if (item?.children && item.title) {
|
||||
values.add(item.title);
|
||||
collectGroupValues(item.children, values);
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function sanitizeOpenedItems(items, menuItems) {
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupValues = collectGroupValues(menuItems);
|
||||
return items.filter((item) => typeof item === 'string' && groupValues.has(item));
|
||||
}
|
||||
|
||||
function getInitialOpenedItems(menuItems) {
|
||||
try {
|
||||
const stored = JSON.parse(localStorage.getItem('sidebar_openedItems') || '[]');
|
||||
return sanitizeOpenedItems(stored, menuItems);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const sidebarMenu = shallowRef(applySidebarCustomization(sidebarItems));
|
||||
|
||||
// 侧边栏分组展开状态持久化
|
||||
const openedItems = ref(JSON.parse(localStorage.getItem('sidebar_openedItems') || '[]'));
|
||||
watch(openedItems, (val) => localStorage.setItem('sidebar_openedItems', JSON.stringify(val)), { deep: true });
|
||||
const openedItems = ref(getInitialOpenedItems(sidebarMenu.value));
|
||||
watch(openedItems, (val) => {
|
||||
localStorage.setItem('sidebar_openedItems', JSON.stringify(sanitizeOpenedItems(val, sidebarMenu.value)));
|
||||
}, { deep: true });
|
||||
|
||||
function refreshSidebarMenu() {
|
||||
sidebarMenu.value = applySidebarCustomization(sidebarItems);
|
||||
openedItems.value = sanitizeOpenedItems(openedItems.value, sidebarMenu.value);
|
||||
}
|
||||
|
||||
// Apply customization on mount and listen for storage changes
|
||||
const handleStorageChange = (e) => {
|
||||
if (e.key === 'astrbot_sidebar_customization') {
|
||||
sidebarMenu.value = applySidebarCustomization(sidebarItems);
|
||||
refreshSidebarMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomEvent = () => {
|
||||
sidebarMenu.value = applySidebarCustomization(sidebarItems);
|
||||
refreshSidebarMenu();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
sidebarMenu.value = applySidebarCustomization(sidebarItems);
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
window.addEventListener('sidebar-customization-changed', handleCustomEvent);
|
||||
});
|
||||
@@ -255,7 +289,7 @@ function openChangelogDialog() {
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
<v-list class="pa-4 listitem flex-grow-1" v-model:opened="openedItems" :open-strategy="'multiple'">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="i">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
@@ -52,6 +52,21 @@ export function clearSidebarCustomization() {
|
||||
export function resolveSidebarItems(defaultItems, customization, options = {}) {
|
||||
const { cloneItems = false, assembleMoreGroup = false } = options;
|
||||
|
||||
const normalizeKeys = (keys = []) => {
|
||||
const list = Array.isArray(keys) ? keys : [];
|
||||
const deduped = [];
|
||||
const seen = new Set();
|
||||
|
||||
list.forEach((key) => {
|
||||
if (typeof key !== 'string') return;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
deduped.push(key);
|
||||
});
|
||||
|
||||
return deduped;
|
||||
};
|
||||
|
||||
const all = new Map();
|
||||
const defaultMain = [];
|
||||
const defaultMore = [];
|
||||
@@ -70,9 +85,23 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
|
||||
});
|
||||
|
||||
const hasCustomization = Boolean(customization);
|
||||
const mainKeys = hasCustomization ? customization.mainItems || [] : defaultMain;
|
||||
const moreKeys = hasCustomization ? customization.moreItems || [] : defaultMore;
|
||||
const used = hasCustomization ? new Set([...mainKeys, ...moreKeys]) : new Set(defaultMain.concat(defaultMore));
|
||||
let mainKeys = hasCustomization ? normalizeKeys(customization.mainItems || []) : [...defaultMain];
|
||||
let moreKeys = hasCustomization ? normalizeKeys(customization.moreItems || []) : [...defaultMore];
|
||||
|
||||
if (hasCustomization) {
|
||||
mainKeys = mainKeys.filter(title => all.has(title));
|
||||
moreKeys = moreKeys.filter(title => all.has(title));
|
||||
}
|
||||
|
||||
if (hasCustomization) {
|
||||
// 如果同一项同时出现在主区与更多区,主区优先。
|
||||
const mainSet = new Set(mainKeys);
|
||||
moreKeys = moreKeys.filter(title => !mainSet.has(title));
|
||||
}
|
||||
|
||||
const used = hasCustomization
|
||||
? new Set([...mainKeys, ...moreKeys])
|
||||
: new Set(defaultMain.concat(defaultMore));
|
||||
|
||||
const mainItems = mainKeys
|
||||
.map(title => all.get(title))
|
||||
@@ -119,7 +148,13 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
return { mainItems, moreItems, merged };
|
||||
return {
|
||||
mainItems,
|
||||
moreItems,
|
||||
merged,
|
||||
normalizedMainKeys: [...mainKeys],
|
||||
normalizedMoreKeys: [...moreKeys]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,9 +164,29 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
|
||||
*/
|
||||
export function applySidebarCustomization(defaultItems) {
|
||||
const customization = getSidebarCustomization();
|
||||
const { merged } = resolveSidebarItems(defaultItems, customization, {
|
||||
const {
|
||||
merged,
|
||||
normalizedMainKeys,
|
||||
normalizedMoreKeys
|
||||
} = resolveSidebarItems(defaultItems, customization, {
|
||||
cloneItems: true,
|
||||
assembleMoreGroup: true
|
||||
});
|
||||
|
||||
if (customization) {
|
||||
const rawMainKeys = Array.isArray(customization.mainItems) ? customization.mainItems : [];
|
||||
const rawMoreKeys = Array.isArray(customization.moreItems) ? customization.moreItems : [];
|
||||
const hasChanged =
|
||||
JSON.stringify(rawMainKeys) !== JSON.stringify(normalizedMainKeys) ||
|
||||
JSON.stringify(rawMoreKeys) !== JSON.stringify(normalizedMoreKeys);
|
||||
|
||||
if (hasChanged) {
|
||||
setSidebarCustomization({
|
||||
mainItems: normalizedMainKeys,
|
||||
moreItems: normalizedMoreKeys
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return merged || defaultItems;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user