mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-16 15:06:04 +08:00
fix(dashboard): keep content frame fixed and remove sidebar customization
This commit is contained in:
@@ -4,3 +4,8 @@
|
||||
- Raise the model image input cap from 32 MiB to 64 MiB. Larger originals are skipped before reading image bytes, with a model notice retaining their paths and suggesting the file-reading tool or a smaller upload; accepted inputs still produce images strictly below 512 KiB.
|
||||
- Local Agent input images are always prepared as JPEG/PNG files strictly below 512 KiB. Compliant local images are reused without copying. Transparent previews retain PNG alpha; animations become 3×3 montages. Original attachment paths remain available to tools, and event-owned previews are deleted after use without a shared conversion cache.
|
||||
- Configuration mapping: **Enable image compression** (`provider_settings.image_compress_enabled`) is replaced by always-on preparation; **JPEG quality** (`provider_settings.image_compress_options.quality`) is replaced by automatic size control; **Maximum edge length** is renamed to **Input image maximum edge length** (`provider_settings.image_compress_options.max_size`). User attachments in CUA sessions follow the same limits.
|
||||
|
||||
- WebUI: Keep content corners and borders fixed while pages scroll inside the card. Add 6px right and bottom gaps on desktop; mobile keeps a full-width, flat layout. (#10479)
|
||||
- WebUI: **System Settings → Appearance → Customize Sidebar → removed**; the sidebar uses the default menu layout. Theme colors and extension collapsing, pinning, and grouping remain available.
|
||||
- WebUI:页面在内容卡片内部滚动,圆角和边线保持固定。桌面端右侧和底部各保留 6px 间隔,移动端保持全宽平面布局。 (#10479)
|
||||
- WebUI:**系统设置 → 外观 → 自定义侧边栏 → 已移除**;侧边栏使用默认菜单布局。仍可调整主题颜色,以及折叠、置顶和按插件分组扩展功能。
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div style="margin-top: 16px;">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="openDialog"
|
||||
style="margin-bottom: 8px;"
|
||||
>
|
||||
{{ t('features.settings.sidebar.customize.title') }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="700px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 pa-4 pb-0 pl-6 d-flex justify-space-between align-center">
|
||||
<span>{{ t('features.settings.sidebar.customize.title') }}</span>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="dialog = false"
|
||||
></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p class="text-body-2 mb-4">{{ t('features.settings.sidebar.customize.subtitle') }}</p>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-2 font-weight-medium">{{ t('features.settings.sidebar.customize.mainItems') }}</div>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="custom-list"
|
||||
@dragover.prevent
|
||||
@drop="handleDropToList($event, 'main')"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in mainItems"
|
||||
:key="item.title"
|
||||
class="mb-1 draggable-item"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, 'main', index)"
|
||||
@dragover.prevent
|
||||
@drop.stop="handleDrop($event, 'main', index)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="typeof item.icon === 'string'" :icon="item.icon" size="small" class="mr-2" />
|
||||
<component :is="item.icon" v-else-if="item.icon" :size="18" class="mr-2" />
|
||||
</template>
|
||||
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
icon="mdi-arrow-right"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
@click="moveToMore(index)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-2 font-weight-medium">{{ t('features.settings.sidebar.customize.moreItems') }}</div>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="custom-list"
|
||||
@dragover.prevent
|
||||
@drop="handleDropToList($event, 'more')"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in moreItems"
|
||||
:key="item.title"
|
||||
class="mb-1 draggable-item"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, 'more', index)"
|
||||
@dragover.prevent
|
||||
@drop.stop="handleDrop($event, 'more', index)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="typeof item.icon === 'string'" :icon="item.icon" size="small" class="mr-2" />
|
||||
<component :is="item.icon" v-else-if="item.icon" :size="18" class="mr-2" />
|
||||
</template>
|
||||
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
@click="moveToMain(index)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
{{ t('features.settings.sidebar.customize.reset') }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="saveCustomization"
|
||||
>
|
||||
{{ t('core.actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import sidebarItems from '@/layouts/full/vertical-sidebar/sidebarItem';
|
||||
import {
|
||||
getSidebarCustomization,
|
||||
setSidebarCustomization,
|
||||
clearSidebarCustomization,
|
||||
resolveSidebarItems
|
||||
} from '@/utils/sidebarCustomization';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialog = ref(false);
|
||||
const mainItems = ref([]);
|
||||
const moreItems = ref([]);
|
||||
const draggedItem = ref(null);
|
||||
|
||||
function initializeItems() {
|
||||
const customization = getSidebarCustomization();
|
||||
const { mainItems: resolvedMain, moreItems: resolvedMore } = resolveSidebarItems(
|
||||
sidebarItems,
|
||||
customization
|
||||
);
|
||||
mainItems.value = resolvedMain;
|
||||
moreItems.value = resolvedMore;
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
initializeItems();
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
function handleDragStart(event, listType, index) {
|
||||
draggedItem.value = {
|
||||
type: listType,
|
||||
index: index,
|
||||
item: listType === 'main' ? mainItems.value[index] : moreItems.value[index]
|
||||
};
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
function handleDrop(event, targetListType, targetIndex) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!draggedItem.value) return;
|
||||
|
||||
const sourceListType = draggedItem.value.type;
|
||||
const sourceIndex = draggedItem.value.index;
|
||||
const item = draggedItem.value.item;
|
||||
|
||||
// Remove from source
|
||||
if (sourceListType === 'main') {
|
||||
mainItems.value.splice(sourceIndex, 1);
|
||||
} else {
|
||||
moreItems.value.splice(sourceIndex, 1);
|
||||
}
|
||||
|
||||
// Add to target
|
||||
if (targetListType === 'main') {
|
||||
mainItems.value.splice(targetIndex, 0, item);
|
||||
} else {
|
||||
moreItems.value.splice(targetIndex, 0, item);
|
||||
}
|
||||
|
||||
draggedItem.value = null;
|
||||
}
|
||||
|
||||
function handleDropToList(event, targetListType) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!draggedItem.value) return;
|
||||
|
||||
const sourceListType = draggedItem.value.type;
|
||||
const sourceIndex = draggedItem.value.index;
|
||||
const item = draggedItem.value.item;
|
||||
|
||||
// Remove from source
|
||||
if (sourceListType === 'main') {
|
||||
mainItems.value.splice(sourceIndex, 1);
|
||||
} else {
|
||||
moreItems.value.splice(sourceIndex, 1);
|
||||
}
|
||||
|
||||
// Add to target list at the end
|
||||
if (targetListType === 'main') {
|
||||
mainItems.value.push(item);
|
||||
} else {
|
||||
moreItems.value.push(item);
|
||||
}
|
||||
|
||||
draggedItem.value = null;
|
||||
}
|
||||
|
||||
function moveToMore(index) {
|
||||
const item = mainItems.value.splice(index, 1)[0];
|
||||
moreItems.value.push(item);
|
||||
}
|
||||
|
||||
function moveToMain(index) {
|
||||
const item = moreItems.value.splice(index, 1)[0];
|
||||
mainItems.value.push(item);
|
||||
}
|
||||
|
||||
function saveCustomization() {
|
||||
const config = {
|
||||
mainItems: mainItems.value.map(item => item.title),
|
||||
moreItems: moreItems.value.map(item => item.title)
|
||||
};
|
||||
|
||||
setSidebarCustomization(config);
|
||||
|
||||
// Notify the sidebar to reload
|
||||
window.dispatchEvent(new CustomEvent('sidebar-customization-changed'));
|
||||
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
clearSidebarCustomization();
|
||||
initializeItems();
|
||||
|
||||
// Notify the sidebar to reload
|
||||
window.dispatchEvent(new CustomEvent('sidebar-customization-changed'));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeItems();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.draggable-item {
|
||||
cursor: move;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--v-theme-surface));
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.draggable-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
|
||||
.custom-list {
|
||||
min-height: 200px;
|
||||
border: 1px dashed rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -148,16 +148,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "Sidebar",
|
||||
"customize": {
|
||||
"title": "Customize Sidebar",
|
||||
"subtitle": "Drag to reorder modules, or move modules in/out of the \"More Features\" group. Settings are saved locally in your browser.",
|
||||
"reset": "Reset to Default",
|
||||
"mainItems": "Main Modules",
|
||||
"moreItems": "More Features"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"dialog": {
|
||||
"title": "Backup Manager"
|
||||
|
||||
@@ -148,16 +148,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "サイドバー",
|
||||
"customize": {
|
||||
"title": "サイドバーをカスタマイズ",
|
||||
"subtitle": "ドラッグしてモジュールを並べ替えるか、「その他の機能」グループへ移動またはグループから取り出します。設定はブラウザー内にのみ保存されます。",
|
||||
"reset": "既定に戻す",
|
||||
"mainItems": "メインモジュール",
|
||||
"moreItems": "その他の機能"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"dialog": {
|
||||
"title": "バックアップ管理"
|
||||
|
||||
@@ -148,16 +148,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "Боковая панель",
|
||||
"customize": {
|
||||
"title": "Настройка меню",
|
||||
"subtitle": "Перетаскивайте элементы, чтобы изменить их порядок или скрыть в группе «Дополнительно». Настройки сохраняются локально в браузере.",
|
||||
"reset": "Сбросить порядок",
|
||||
"mainItems": "Основные разделы",
|
||||
"moreItems": "Дополнительно"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"dialog": {
|
||||
"title": "Резервное копирование"
|
||||
|
||||
@@ -148,16 +148,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "侧边栏",
|
||||
"customize": {
|
||||
"title": "自定义侧边栏",
|
||||
"subtitle": "拖拽以调整模块顺序,或者将模块移入/移出\"更多功能\"分组。设置仅保存在浏览器本地。",
|
||||
"reset": "恢复默认",
|
||||
"mainItems": "主要模块",
|
||||
"moreItems": "更多功能"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"dialog": {
|
||||
"title": "备份管理"
|
||||
|
||||
@@ -127,13 +127,7 @@ onMounted(() => {
|
||||
/>
|
||||
<VerticalSidebarVue v-if="showSidebar" />
|
||||
<VerticalHeaderVue />
|
||||
<v-main
|
||||
:class="{ 'chat-main': isCurrentChatRoute }"
|
||||
:style="{
|
||||
height: isViewportLockedRoute ? '100vh' : undefined,
|
||||
overflow: isViewportLockedRoute ? 'hidden' : undefined,
|
||||
}"
|
||||
>
|
||||
<v-main :class="{ 'chat-main': isCurrentChatRoute }">
|
||||
<v-container
|
||||
fluid
|
||||
class="page-wrapper"
|
||||
@@ -143,23 +137,14 @@ onMounted(() => {
|
||||
isProviderPageRoute || isPlatformPageRoute,
|
||||
'fullscreen-container': isFullScreenRoute,
|
||||
}"
|
||||
:style="{
|
||||
height:
|
||||
isFullScreenRoute || isProviderPageRoute || isPlatformPageRoute
|
||||
? '100%'
|
||||
: 'calc(100% - 8px)',
|
||||
padding: isFullScreenRoute ? '0' : undefined,
|
||||
minHeight:
|
||||
isFullScreenRoute || isProviderPageRoute || isPlatformPageRoute
|
||||
? 'unset'
|
||||
: undefined,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="page-content"
|
||||
:class="{
|
||||
'page-content--locked': isViewportLockedRoute || isPluginViewRoute,
|
||||
}"
|
||||
:style="{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: isViewportLockedRoute ? 'hidden' : undefined,
|
||||
padding: isFullScreenRoute ? '0' : undefined,
|
||||
position: isPluginViewRoute ? 'relative' : undefined,
|
||||
}"
|
||||
>
|
||||
@@ -185,18 +170,6 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-mode-container {
|
||||
min-height: unset !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.viewport-locked-container {
|
||||
min-height: unset !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
@@ -219,31 +192,32 @@ onMounted(() => {
|
||||
:global(.v-main) {
|
||||
height: calc(100vh - var(--astrbot-toolbar-height, 40px)) !important;
|
||||
padding-top: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
scrollbar-width: none;
|
||||
overflow: hidden !important;
|
||||
/* The document no longer scrolls; the content area does. Sticky offsets that were
|
||||
written for the document layout must be measured from the content area's own top. */
|
||||
--v-layout-top: 0px !important;
|
||||
}
|
||||
|
||||
:global(.v-main::-webkit-scrollbar) {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* The content area is the opaque card next to the sidebar. */
|
||||
/* Keep the card frame fixed while its content scrolls inside the rounded edges. */
|
||||
:global(.page-wrapper) {
|
||||
--astrbot-content-gap: 6px;
|
||||
height: calc(100vh - var(--astrbot-toolbar-height, 40px) - var(--astrbot-content-gap)) !important;
|
||||
width: calc(100% - var(--astrbot-content-gap));
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
border-top-left-radius: 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* On small screens there is no permanent sidebar to separate from, so the
|
||||
card's left edge treatments would only read as stray lines. */
|
||||
@media (max-width: 959.98px) {
|
||||
:global(.page-wrapper) {
|
||||
--astrbot-content-gap: 0px;
|
||||
border-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,19 +227,23 @@ onMounted(() => {
|
||||
border-top: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
/* Normal pages grow with their content so the main area has something to scroll. */
|
||||
:global(.page-wrapper:not(.viewport-locked-container):not(.chat-mode-container):not(.fullscreen-container)) {
|
||||
height: auto !important;
|
||||
min-height: calc(100vh - var(--astrbot-toolbar-height, 40px));
|
||||
.page-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Viewport-locked and full-screen pages keep a fixed height and scroll internally.
|
||||
Clipping them keeps full-bleed children from painting outside the rounded corner. */
|
||||
:global(.viewport-locked-container),
|
||||
:global(.chat-mode-container),
|
||||
:global(.fullscreen-container) {
|
||||
height: calc(100vh - var(--astrbot-toolbar-height, 40px)) !important;
|
||||
overflow: hidden !important;
|
||||
.page-content::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Chat, workspace and plugin pages manage their own internal scrolling. */
|
||||
.page-content--locked {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* macOS desktop vibrancy: the window material shows through wherever the UI stays
|
||||
@@ -290,14 +268,9 @@ onMounted(() => {
|
||||
--astrbot-vibrancy-tint: rgba(26, 26, 26, 0.92);
|
||||
}
|
||||
|
||||
/* Off macOS the sidebar column is opaque; extend its color behind the content
|
||||
card's rounded corner so the notch does not contrast with the sidebar. */
|
||||
/* Extend the sidebar surface behind every corner and the right/bottom gaps. */
|
||||
:global(html:not([data-astrbot-desktop-platform='macos']) .v-main) {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgb(var(--v-theme-surface)) 0 calc(var(--v-layout-left) + 12px),
|
||||
transparent calc(var(--v-layout-left) + 12px) 100%
|
||||
) !important;
|
||||
background: rgb(var(--v-theme-surface)) !important;
|
||||
}
|
||||
|
||||
:global(html[data-astrbot-desktop-platform='macos']),
|
||||
@@ -307,14 +280,9 @@ onMounted(() => {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* The sidebar tint lives on the main area's own background (behind everything), covering
|
||||
the sidebar column plus a small overhang that reaches the content corner. */
|
||||
/* Keep the macOS tint continuous behind the sidebar and the card gaps. */
|
||||
:global(html[data-astrbot-desktop-platform='macos'] .v-main) {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--astrbot-vibrancy-tint, transparent) 0 calc(var(--v-layout-left) + 12px),
|
||||
transparent calc(var(--v-layout-left) + 12px) 100%
|
||||
) !important;
|
||||
background: var(--astrbot-vibrancy-tint, transparent) !important;
|
||||
}
|
||||
|
||||
/* Vuetify paints its own surface behind every list, which would sit on top of the
|
||||
|
||||
@@ -41,10 +41,6 @@ export interface menu {
|
||||
export const SYSTEM_GROUP_KEY = 'core.navigation.groups.system';
|
||||
export const EXTENSION_GROUP_KEY = 'core.navigation.groups.extension';
|
||||
|
||||
// Kept for the legacy sidebar customization storage; the default layout no
|
||||
// longer uses a collapsible "more" group.
|
||||
export const MORE_GROUP_KEY = 'core.navigation.groups.more';
|
||||
|
||||
// 注意:这个文件现在包含i18n键值而不是直接的文本
|
||||
// 在组件中使用时需要通过t()函数进行翻译
|
||||
// 所有键名都使用 core.navigation.* 格式
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
// Utility for managing sidebar customization in localStorage
|
||||
const STORAGE_KEY = 'astrbot_sidebar_customization';
|
||||
|
||||
/**
|
||||
* Get the customized sidebar configuration from localStorage
|
||||
* @returns {Object|null} The customization config or null if not set
|
||||
*/
|
||||
export function getSidebarCustomization() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
} catch (error) {
|
||||
console.error('Error reading sidebar customization:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the sidebar customization to localStorage
|
||||
* @param {Object} config - The customization configuration
|
||||
* @param {Array} config.mainItems - Array of item titles for main sidebar
|
||||
* @param {Array} config.moreItems - Array of item titles for "More Features" group
|
||||
*/
|
||||
export function setSidebarCustomization(config) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
||||
} catch (error) {
|
||||
console.error('Error saving sidebar customization:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the sidebar customization (reset to default)
|
||||
*/
|
||||
export function clearSidebarCustomization() {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error('Error clearing sidebar customization:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析侧边栏默认项与用户定制,返回主区/更多区及可选的合并结果
|
||||
* @param {Array} defaultItems - 默认侧边栏结构
|
||||
* @param {Object|null} customization - 用户定制(mainItems/moreItems)
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.cloneItems=false] - 是否克隆条目以避免外部引用被修改
|
||||
* @param {boolean} [options.assembleMoreGroup=false] - 是否组装带更多分组的整体数组
|
||||
* @returns {{ mainItems: Array, moreItems: Array, merged?: Array }}
|
||||
*/
|
||||
import { MORE_GROUP_KEY } from "@/layouts/full/vertical-sidebar/sidebarItem";
|
||||
|
||||
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 = [];
|
||||
const defaultMoreGroup = defaultItems.find(item => item.children && item.title === MORE_GROUP_KEY);
|
||||
|
||||
// 收集所有条目,按 title 建索引
|
||||
defaultItems.forEach(item => {
|
||||
if (item.children && item.title === MORE_GROUP_KEY) {
|
||||
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 hasCustomization = Boolean(customization);
|
||||
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))
|
||||
.filter(Boolean);
|
||||
|
||||
if (hasCustomization) {
|
||||
// 补充新增默认主区项
|
||||
defaultMain.forEach(title => {
|
||||
if (!used.has(title)) {
|
||||
const item = all.get(title);
|
||||
if (item) mainItems.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const moreItems = moreKeys
|
||||
.map(title => all.get(title))
|
||||
.filter(Boolean);
|
||||
|
||||
if (hasCustomization) {
|
||||
// 补充新增默认更多区项
|
||||
defaultMore.forEach(title => {
|
||||
if (!used.has(title)) {
|
||||
const item = all.get(title);
|
||||
if (item) moreItems.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let merged;
|
||||
if (assembleMoreGroup) {
|
||||
const children = cloneItems ? moreItems.map(item => ({ ...item })) : [...moreItems];
|
||||
if (children.length > 0) {
|
||||
merged = [
|
||||
...mainItems,
|
||||
{
|
||||
title: MORE_GROUP_KEY,
|
||||
icon: defaultMoreGroup?.icon || 'mdi-dots-horizontal',
|
||||
children
|
||||
}
|
||||
];
|
||||
} else {
|
||||
merged = [...mainItems];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mainItems,
|
||||
moreItems,
|
||||
merged,
|
||||
normalizedMainKeys: [...mainKeys],
|
||||
normalizedMoreKeys: [...moreKeys]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用侧边栏定制,返回包含更多分组的完整结构
|
||||
* @param {Array} defaultItems - 默认侧边栏结构
|
||||
* @returns {Array} 自定义后的结构(新数组,不修改入参)
|
||||
*/
|
||||
export function applySidebarCustomization(defaultItems) {
|
||||
const customization = getSidebarCustomization();
|
||||
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;
|
||||
}
|
||||
@@ -98,16 +98,6 @@
|
||||
</div>
|
||||
<div class="settings-section__content">
|
||||
<div class="settings-list-card">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item__label">
|
||||
<div class="settings-item__title">{{ tm('sidebar.customize.title') }}</div>
|
||||
<div class="settings-item__subtitle">{{ tm('sidebar.customize.subtitle') }}</div>
|
||||
</div>
|
||||
<div class="settings-item__control">
|
||||
<SidebarCustomizer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-item settings-item--color">
|
||||
<div class="settings-item__label">
|
||||
<div class="settings-item__title">{{ tm('theme.customize.title') }}</div>
|
||||
@@ -490,7 +480,6 @@ import { apiKeyApi, systemConfigApi } from '@/api/v1';
|
||||
import AstrBotConfigV4 from '@/components/shared/AstrBotConfigV4.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
|
||||
import BackupDialog from '@/components/shared/BackupDialog.vue';
|
||||
import StorageCleanupPanel from '@/components/shared/StorageCleanupPanel.vue';
|
||||
import DashboardTwoFactorDialog from '@/components/shared/DashboardTwoFactorDialog.vue';
|
||||
|
||||
@@ -4,7 +4,7 @@ The AstrBot admin panel features plugin management, log viewing, visual configur
|
||||
|
||||
## Navigation and Previous Entry Points
|
||||
|
||||
These paths use the current default sidebar. If you customized it, open `System Settings → Appearance → Customize Sidebar` at the bottom of the sidebar to review or reset the layout.
|
||||
These paths use the current default sidebar with a fixed menu order. Extension items still support collapsing, pinning, and grouping by plugin.
|
||||
|
||||
| Previous entry point or name | Current entry point |
|
||||
| --- | --- |
|
||||
@@ -17,6 +17,7 @@ These paths use the current default sidebar. If you customized it, open `System
|
||||
| Config → System Config | System Settings → General, Appearance, Network, or Security, depending on the setting |
|
||||
| Commands / Command Management | Extensions → Handlers → Command |
|
||||
| Standalone MCP / Skills entries | Extensions → MCP Servers / Skills |
|
||||
| System Settings → Appearance → Customize Sidebar | Removed; the sidebar uses the default menu layout |
|
||||
| Custom Rules / Future Tasks / SubAgents | Custom Rules is under the sidebar `System` group; Future Tasks and SubAgents are under the `Extensions` group |
|
||||
|
||||
Old log, trace, conversation, and statistics URLs still redirect to the corresponding tabs. Agent runners are now saved in each profile; they are no longer created as model providers. See [Agent Runners](./agent-runner.md) for the setup steps.
|
||||
@@ -94,7 +95,7 @@ Use the `{}` icon labeled `Edit Configuration File` to edit the current profile
|
||||
Global settings are under `System Settings` at the bottom of the sidebar:
|
||||
|
||||
- `General`: timezone, external callback address, logs, and cache.
|
||||
- `Appearance`: sidebar and theme.
|
||||
- `Appearance`: theme colors.
|
||||
- `Network`: HTTP proxy, Python package sources, and GitHub proxy. For the address to use when AstrBot runs in Docker, see [Deploy with Docker](/en/deploy/astrbot/docker.md).
|
||||
- `Security`: WebUI HTTPS, login rate limits, and TOTP.
|
||||
- `Maintenance`: backup, restore, and restart.
|
||||
|
||||
@@ -4,7 +4,7 @@ AstrBot 管理面板具有管理插件、查看日志、可视化配置、查看
|
||||
|
||||
## 菜单与旧入口对照
|
||||
|
||||
以下入口对应当前 WebUI 的默认侧边栏。若你调整过侧边栏,可在左下角 `系统设置 → 外观 → 自定义侧边栏` 中查看或恢复默认布局。
|
||||
以下入口对应当前 WebUI 的默认侧边栏。侧边栏使用固定的菜单顺序;扩展功能仍支持折叠、置顶和按插件分组。
|
||||
|
||||
| 旧入口或称呼 | 当前入口 |
|
||||
| --- | --- |
|
||||
@@ -17,6 +17,7 @@ AstrBot 管理面板具有管理插件、查看日志、可视化配置、查看
|
||||
| 配置文件 → 系统配置 | 左下角系统设置 → 常规、外观、网络或安全,按配置用途选择 |
|
||||
| 指令管理 | 插件 → 管理行为 → 指令 |
|
||||
| 独立 MCP / 技能入口 | 插件 → MCP / 技能 |
|
||||
| 系统设置 → 外观 → 自定义侧边栏 | 已移除;侧边栏使用默认菜单布局 |
|
||||
| 自定义规则 / 未来任务 / 子代理编排 | 自定义规则位于侧边栏 `系统` 分组,未来任务与子代理编排位于 `扩展功能` 分组 |
|
||||
|
||||
旧日志、追踪、对话和统计页面地址仍会跳转到对应的新标签页。Agent 执行器现在随配置文件保存,不再通过新增模型提供商创建;操作步骤见 [Agent 执行器](./agent-runner.md)。
|
||||
@@ -94,7 +95,7 @@ ChatUI 支持以下常用能力:
|
||||
全局设置位于左下角 `系统设置`,不在配置文件选择器中:
|
||||
|
||||
- `常规`:时区、外部回调地址、日志和缓存。
|
||||
- `外观`:侧边栏和主题。
|
||||
- `外观`:主题颜色。
|
||||
- `网络`:HTTP 代理、Python 依赖源和 GitHub 加速地址。Docker 下的代理填法见 [Docker 部署](/deploy/astrbot/docker.md)。
|
||||
- `安全`:WebUI HTTPS、登录限速和 TOTP。
|
||||
- `维护`:备份、恢复和重启。
|
||||
|
||||
Reference in New Issue
Block a user