fix(dashboard): 自定义侧边栏中错误展开“更多功能”页面问题 (#5405)#5670
fix(dashboard): 自定义侧边栏中错误展开“更多功能”页面问题 (#5405)#5670Soulter merged 1 commit intoAstrBotDevs:masterfrom
Conversation
- 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
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取请求旨在解决仪表盘侧边栏自定义后分组状态不稳定以及“更多功能”页面错误展开的问题。通过优化 Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Hey——我这边发现了 1 个问题,并补充了一些整体性的反馈:
- 在
VerticalSidebar.vue和NavItem.vue里使用item.title || item.to作为:key,如果不同分组之间存在重复的 title 或路由,仍然有可能发生 key 冲突;建议为每个菜单项新增或派生一个保证唯一的标识符(例如稳定的id或完整路径),并用它作为 key。 sidebar_openedItems这个 localStorage key 字符串现在在多个地方被引用(getInitialOpenedItems、watch等);可以考虑把它集中到一个共享常量里,这样将来如果需要修改 key,就能减少出现细微 bug 的风险。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Using `item.title || item.to` as the `:key` in both `VerticalSidebar.vue` and `NavItem.vue` may still lead to collisions if titles or routes repeat across groups; consider adding or deriving a guaranteed-unique identifier for each menu item (e.g., a stable `id` or full path) and using that as the key.
- The `sidebar_openedItems` localStorage key string is now referenced in multiple places (`getInitialOpenedItems`, the `watch`, etc.); centralizing this into a shared constant will reduce the chance of subtle bugs if the key ever needs to change.
## Individual Comments
### Comment 1
<location path="dashboard/src/utils/sidebarCustomization.js" line_range="55" />
<code_context>
export function resolveSidebarItems(defaultItems, customization, options = {}) {
const { cloneItems = false, assembleMoreGroup = false } = options;
+ const normalizeKeys = (keys = []) => {
+ const list = Array.isArray(keys) ? keys : [];
+ const deduped = [];
</code_context>
<issue_to_address>
**issue (complexity):** 可以考虑抽取一个可复用的侧边栏自定义配置归一化(normalization)辅助方法,并让 `applySidebarCustomization` 负责持久化逻辑,这样 `resolveSidebarItems` 就能专注在根据 key 解析 items 上。
在保留当前行为(归一化 + 持久化)的前提下,通过以下方式降低耦合度与复杂度:
1. **抽取一个专用的归一化辅助函数**
2. **让 `applySidebarCustomization` 负责持久化和变更检测**
3. **让 `resolveSidebarItems` 只专注于根据 key 解析出 items**
### 1. 抽取归一化辅助函数
将归一化流水线从 `resolveSidebarItems` 中抽离出来,使其可复用:
```js
function normalizeSidebarCustomization(customization, all) {
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;
};
if (!customization) {
return {
mainKeys: null, // signal "use defaults"
moreKeys: null,
};
}
let mainKeys = normalizeKeys(customization.mainItems);
let moreKeys = normalizeKeys(customization.moreItems);
// 过滤不存在的 key
mainKeys = mainKeys.filter(title => all.has(title));
moreKeys = moreKeys.filter(title => all.has(title));
// 如果同一项同时出现在主区与更多区,主区优先
const mainSet = new Set(mainKeys);
moreKeys = moreKeys.filter(title => !mainSet.has(title));
return { mainKeys, moreKeys };
}
```
这样一来,`resolveSidebarItems` 就可以被简化,也不再需要返回归一化之后的 keys:
```js
export function resolveSidebarItems(defaultItems, customization, options = {}) {
const { cloneItems = false, assembleMoreGroup = false } = options;
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, cloneItems ? { ...child } : child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, cloneItems ? { ...item } : item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const effectiveMainKeys = mainKeys ?? [...defaultMain];
const effectiveMoreKeys = moreKeys ?? [...defaultMore];
const used = new Set([...effectiveMainKeys, ...effectiveMoreKeys]);
const mainItems = effectiveMainKeys
.map(title => all.get(title))
.filter(Boolean);
// ... existing logic to build moreItems / merged using `used` ...
return { mainItems, moreItems, merged };
}
```
### 2. 在 `applySidebarCustomization` 内部显式地进行变更检测
使用同一个归一化辅助函数进行持久化,同时用一个小而明确的数组相等性辅助方法替代 `JSON.stringify`:
```js
function areArraysShallowEqual(a, b) {
if (a === b) return true;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}
export function applySidebarCustomization(defaultItems) {
const customization = getSidebarCustomization();
// Build the same `all` map as in resolveSidebarItems or refactor to share it
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const normalizedMainKeys = mainKeys ?? defaultMain;
const normalizedMoreKeys = moreKeys ?? defaultMore;
const { merged } = 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 =
!areArraysShallowEqual(rawMainKeys, normalizedMainKeys) ||
!areArraysShallowEqual(rawMoreKeys, normalizedMoreKeys);
if (hasChanged) {
setSidebarCustomization({
mainItems: normalizedMainKeys,
moreItems: normalizedMoreKeys,
});
}
}
return merged || defaultItems;
}
```
要点:
- `resolveSidebarItems` 不再返回 `normalizedMainKeys` / `normalizedMoreKeys`,其职责被限定为将 keys 转换为 items。
- 归一化逻辑(`normalizeSidebarCustomization`)是一条单一的、可组合的流水线,被 `resolveSidebarItems` 和 `applySidebarCustomization` 共同复用。
- `applySidebarCustomization` 通过 `areArraysShallowEqual` 而不是 `JSON.stringify` 来进行清晰的持久化与变更检测。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- Using
item.title || item.toas the:keyin bothVerticalSidebar.vueandNavItem.vuemay still lead to collisions if titles or routes repeat across groups; consider adding or deriving a guaranteed-unique identifier for each menu item (e.g., a stableidor full path) and using that as the key. - The
sidebar_openedItemslocalStorage key string is now referenced in multiple places (getInitialOpenedItems, thewatch, etc.); centralizing this into a shared constant will reduce the chance of subtle bugs if the key ever needs to change.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Using `item.title || item.to` as the `:key` in both `VerticalSidebar.vue` and `NavItem.vue` may still lead to collisions if titles or routes repeat across groups; consider adding or deriving a guaranteed-unique identifier for each menu item (e.g., a stable `id` or full path) and using that as the key.
- The `sidebar_openedItems` localStorage key string is now referenced in multiple places (`getInitialOpenedItems`, the `watch`, etc.); centralizing this into a shared constant will reduce the chance of subtle bugs if the key ever needs to change.
## Individual Comments
### Comment 1
<location path="dashboard/src/utils/sidebarCustomization.js" line_range="55" />
<code_context>
export function resolveSidebarItems(defaultItems, customization, options = {}) {
const { cloneItems = false, assembleMoreGroup = false } = options;
+ const normalizeKeys = (keys = []) => {
+ const list = Array.isArray(keys) ? keys : [];
+ const deduped = [];
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting a reusable sidebar customization normalization helper and letting `applySidebarCustomization` handle persistence so that `resolveSidebarItems` stays focused on resolving items from keys.
You can keep the new behavior (normalization + persistence) but reduce coupling/complexity by:
1. **Extracting a dedicated normalization helper**
2. **Letting `applySidebarCustomization` own persistence & change detection**
3. **Keeping `resolveSidebarItems` focused on item resolution**
### 1. Extract a normalization helper
Move the normalization pipeline out of `resolveSidebarItems` and make it reusable:
```js
function normalizeSidebarCustomization(customization, all) {
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;
};
if (!customization) {
return {
mainKeys: null, // signal "use defaults"
moreKeys: null,
};
}
let mainKeys = normalizeKeys(customization.mainItems);
let moreKeys = normalizeKeys(customization.moreItems);
// 过滤不存在的 key
mainKeys = mainKeys.filter(title => all.has(title));
moreKeys = moreKeys.filter(title => all.has(title));
// 如果同一项同时出现在主区与更多区,主区优先
const mainSet = new Set(mainKeys);
moreKeys = moreKeys.filter(title => !mainSet.has(title));
return { mainKeys, moreKeys };
}
```
Now `resolveSidebarItems` can be simplified and no longer needs to return normalized keys:
```js
export function resolveSidebarItems(defaultItems, customization, options = {}) {
const { cloneItems = false, assembleMoreGroup = false } = options;
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, cloneItems ? { ...child } : child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, cloneItems ? { ...item } : item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const effectiveMainKeys = mainKeys ?? [...defaultMain];
const effectiveMoreKeys = moreKeys ?? [...defaultMore];
const used = new Set([...effectiveMainKeys, ...effectiveMoreKeys]);
const mainItems = effectiveMainKeys
.map(title => all.get(title))
.filter(Boolean);
// ... existing logic to build moreItems / merged using `used` ...
return { mainItems, moreItems, merged };
}
```
### 2. Make change detection explicit and local to `applySidebarCustomization`
Use the same helper for persistence and a small explicit equality helper instead of `JSON.stringify`:
```js
function areArraysShallowEqual(a, b) {
if (a === b) return true;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}
export function applySidebarCustomization(defaultItems) {
const customization = getSidebarCustomization();
// Build the same `all` map as in resolveSidebarItems or refactor to share it
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const normalizedMainKeys = mainKeys ?? defaultMain;
const normalizedMoreKeys = moreKeys ?? defaultMore;
const { merged } = 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 =
!areArraysShallowEqual(rawMainKeys, normalizedMainKeys) ||
!areArraysShallowEqual(rawMoreKeys, normalizedMoreKeys);
if (hasChanged) {
setSidebarCustomization({
mainItems: normalizedMainKeys,
moreItems: normalizedMoreKeys,
});
}
}
return merged || defaultItems;
}
```
Key points:
- `resolveSidebarItems` no longer returns `normalizedMainKeys` / `normalizedMoreKeys`, keeping its responsibility focused on converting keys → items.
- Normalization (`normalizeSidebarCustomization`) is a single, composable pipeline used by both `resolveSidebarItems` and `applySidebarCustomization`.
- `applySidebarCustomization` handles persistence and change detection clearly via `areArraysShallowEqual` instead of `JSON.stringify`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| export function resolveSidebarItems(defaultItems, customization, options = {}) { | ||
| const { cloneItems = false, assembleMoreGroup = false } = options; | ||
|
|
||
| const normalizeKeys = (keys = []) => { |
There was a problem hiding this comment.
issue (complexity): 可以考虑抽取一个可复用的侧边栏自定义配置归一化(normalization)辅助方法,并让 applySidebarCustomization 负责持久化逻辑,这样 resolveSidebarItems 就能专注在根据 key 解析 items 上。
在保留当前行为(归一化 + 持久化)的前提下,通过以下方式降低耦合度与复杂度:
- 抽取一个专用的归一化辅助函数
- 让
applySidebarCustomization负责持久化和变更检测 - 让
resolveSidebarItems只专注于根据 key 解析出 items
1. 抽取归一化辅助函数
将归一化流水线从 resolveSidebarItems 中抽离出来,使其可复用:
function normalizeSidebarCustomization(customization, all) {
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;
};
if (!customization) {
return {
mainKeys: null, // signal "use defaults"
moreKeys: null,
};
}
let mainKeys = normalizeKeys(customization.mainItems);
let moreKeys = normalizeKeys(customization.moreItems);
// 过滤不存在的 key
mainKeys = mainKeys.filter(title => all.has(title));
moreKeys = moreKeys.filter(title => all.has(title));
// 如果同一项同时出现在主区与更多区,主区优先
const mainSet = new Set(mainKeys);
moreKeys = moreKeys.filter(title => !mainSet.has(title));
return { mainKeys, moreKeys };
}这样一来,resolveSidebarItems 就可以被简化,也不再需要返回归一化之后的 keys:
export function resolveSidebarItems(defaultItems, customization, options = {}) {
const { cloneItems = false, assembleMoreGroup = false } = options;
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, cloneItems ? { ...child } : child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, cloneItems ? { ...item } : item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const effectiveMainKeys = mainKeys ?? [...defaultMain];
const effectiveMoreKeys = moreKeys ?? [...defaultMore];
const used = new Set([...effectiveMainKeys, ...effectiveMoreKeys]);
const mainItems = effectiveMainKeys
.map(title => all.get(title))
.filter(Boolean);
// ... existing logic to build moreItems / merged using `used` ...
return { mainItems, moreItems, merged };
}2. 在 applySidebarCustomization 内部显式地进行变更检测
使用同一个归一化辅助函数进行持久化,同时用一个小而明确的数组相等性辅助方法替代 JSON.stringify:
function areArraysShallowEqual(a, b) {
if (a === b) return true;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}
export function applySidebarCustomization(defaultItems) {
const customization = getSidebarCustomization();
// Build the same `all` map as in resolveSidebarItems or refactor to share it
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const normalizedMainKeys = mainKeys ?? defaultMain;
const normalizedMoreKeys = moreKeys ?? defaultMore;
const { merged } = 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 =
!areArraysShallowEqual(rawMainKeys, normalizedMainKeys) ||
!areArraysShallowEqual(rawMoreKeys, normalizedMoreKeys);
if (hasChanged) {
setSidebarCustomization({
mainItems: normalizedMainKeys,
moreItems: normalizedMoreKeys,
});
}
}
return merged || defaultItems;
}要点:
resolveSidebarItems不再返回normalizedMainKeys/normalizedMoreKeys,其职责被限定为将 keys 转换为 items。- 归一化逻辑(
normalizeSidebarCustomization)是一条单一的、可组合的流水线,被resolveSidebarItems和applySidebarCustomization共同复用。 applySidebarCustomization通过areArraysShallowEqual而不是JSON.stringify来进行清晰的持久化与变更检测。
Original comment in English
issue (complexity): Consider extracting a reusable sidebar customization normalization helper and letting applySidebarCustomization handle persistence so that resolveSidebarItems stays focused on resolving items from keys.
You can keep the new behavior (normalization + persistence) but reduce coupling/complexity by:
- Extracting a dedicated normalization helper
- Letting
applySidebarCustomizationown persistence & change detection - Keeping
resolveSidebarItemsfocused on item resolution
1. Extract a normalization helper
Move the normalization pipeline out of resolveSidebarItems and make it reusable:
function normalizeSidebarCustomization(customization, all) {
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;
};
if (!customization) {
return {
mainKeys: null, // signal "use defaults"
moreKeys: null,
};
}
let mainKeys = normalizeKeys(customization.mainItems);
let moreKeys = normalizeKeys(customization.moreItems);
// 过滤不存在的 key
mainKeys = mainKeys.filter(title => all.has(title));
moreKeys = moreKeys.filter(title => all.has(title));
// 如果同一项同时出现在主区与更多区,主区优先
const mainSet = new Set(mainKeys);
moreKeys = moreKeys.filter(title => !mainSet.has(title));
return { mainKeys, moreKeys };
}Now resolveSidebarItems can be simplified and no longer needs to return normalized keys:
export function resolveSidebarItems(defaultItems, customization, options = {}) {
const { cloneItems = false, assembleMoreGroup = false } = options;
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, cloneItems ? { ...child } : child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, cloneItems ? { ...item } : item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const effectiveMainKeys = mainKeys ?? [...defaultMain];
const effectiveMoreKeys = moreKeys ?? [...defaultMore];
const used = new Set([...effectiveMainKeys, ...effectiveMoreKeys]);
const mainItems = effectiveMainKeys
.map(title => all.get(title))
.filter(Boolean);
// ... existing logic to build moreItems / merged using `used` ...
return { mainItems, moreItems, merged };
}2. Make change detection explicit and local to applySidebarCustomization
Use the same helper for persistence and a small explicit equality helper instead of JSON.stringify:
function areArraysShallowEqual(a, b) {
if (a === b) return true;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}
export function applySidebarCustomization(defaultItems) {
const customization = getSidebarCustomization();
// Build the same `all` map as in resolveSidebarItems or refactor to share it
const all = new Map();
const defaultMain = [];
const defaultMore = [];
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
item.children.forEach(child => {
all.set(child.title, child);
defaultMore.push(child.title);
});
} else {
all.set(item.title, item);
defaultMain.push(item.title);
}
});
const { mainKeys, moreKeys } = normalizeSidebarCustomization(customization, all);
const normalizedMainKeys = mainKeys ?? defaultMain;
const normalizedMoreKeys = moreKeys ?? defaultMore;
const { merged } = 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 =
!areArraysShallowEqual(rawMainKeys, normalizedMainKeys) ||
!areArraysShallowEqual(rawMoreKeys, normalizedMoreKeys);
if (hasChanged) {
setSidebarCustomization({
mainItems: normalizedMainKeys,
moreItems: normalizedMoreKeys,
});
}
}
return merged || defaultItems;
}Key points:
resolveSidebarItemsno longer returnsnormalizedMainKeys/normalizedMoreKeys, keeping its responsibility focused on converting keys → items.- Normalization (
normalizeSidebarCustomization) is a single, composable pipeline used by bothresolveSidebarItemsandapplySidebarCustomization. applySidebarCustomizationhandles persistence and change detection clearly viaareArraysShallowEqualinstead ofJSON.stringify.
| 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); |
There was a problem hiding this comment.
当前检测变更的逻辑没有正确处理 localStorage 中的 customization.mainItems 或 customization.moreItems 不是数组的情况。在这种情况下,rawMainKeys 会被视为空数组,导致 hasChanged 错误地判断为 false,从而无法修复 localStorage 中的脏数据。这里的检查逻辑需要更健壮,以包含对类型的校验。
const hasChanged =
!Array.isArray(customization.mainItems) ||
!Array.isArray(customization.moreItems) ||
JSON.stringify(customization.mainItems) !== JSON.stringify(normalizedMainKeys) ||
JSON.stringify(customization.moreItems) !== JSON.stringify(normalizedMoreKeys);| 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; | ||
| }; |
|
No docs changes were generated in this run (docs repo had no updates). Docs repo: AstrBotDevs/AstrBot-docs AI change summary (not committed):
Experimental bot notice:
|
Fixes #5405
Modifications / 改动点
本 PR 修复了侧边栏在自定义后分组状态不稳定的问题,并增强了本地持久化数据的容错能力。
修改
dashboard/src/layouts/full/vertical-sidebar/NavItem.vue子项渲染 key 从索引改为稳定 key(
title/to/fallback),避免 vnode 复用错位。修改
dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue对
sidebar_openedItems做清洗,只保留当前菜单结构中有效的分组 key。自定义变化后刷新菜单,并重新清洗展开状态。
顶层侧边栏渲染改为稳定 key。
修改
dashboard/src/utils/sidebarCustomization.js规范化 main/more 项配置(去重、过滤无效项、处理冲突)。
将规范化后的结果回写 localStorage(自愈旧数据)。
增加非数组保护,避免脏 localStorage 数据触发运行时异常。
This is NOT a breaking change. / 这不是一个破坏性变更。
Screenshots or Test Results / 运行截图或测试结果
现在 v4.18.3 版本中测试现象为:当点击”平台日志“或其它功能时会连带展开“更多功能”,将其移出侧边栏后再移回去,该现象则会消失,但是当重启 AstrBot 后该现象依旧存在,如下视频,
redpandacompress_bug.mp4
经修复过后,点击“平台日志”或其它功能不会连带展开“更多功能”,重启 AstrBot 后也不会有影响,测试结果如下视频,
redpandacompress_new.mp4
验证步骤:
本地验证:
cd dashboard && pnpm typecheck:通过cd dashboard && pnpm build:通过(有既有 CSS minify warning,不影响构建)cd dashboard && pnpm lint:当前分支存在全局解析基线问题(非本 PR 引入)ruff check .:通过说明:
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txtandpyproject.toml.Summary by Sourcery
通过使项目键值和持久化状态对自定义及本地陈旧数据更具鲁棒性,稳定仪表盘垂直侧边栏的行为。
Bug 修复:
改进:
Original summary in English
Summary by Sourcery
Stabilize the dashboard vertical sidebar behavior by making item keys and persisted state resilient to customization and stale local data.
Bug Fixes:
Enhancements: