Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions dashboard/src/composables/usePluginSidebarItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { reactive, shallowRef, onMounted, watch } from "vue";
import axios from "axios";
import type { menu } from "@/layouts/full/vertical-sidebar/sidebarItem";

const DEFAULT_ICON = "mdi-puzzle";
const GROUP_I18N_KEY = "core.navigation.pluginWebui";
const GROUP_ICON = "mdi-puzzle-outline";

interface PluginEntry {
name: string;
display_name?: string | null;
activated: boolean;
pages: string[];
}

/** 模块级共享状态,由 useExtensionPage.getExtensions() 更新 */
export const pluginSidebarState = reactive<{
Comment thread
lxfight marked this conversation as resolved.
plugins: PluginEntry[];
}>({
plugins: [],
});

function buildPluginItems(plugins: PluginEntry[]): menu | null {
const activeWithPages = plugins.filter(
(p) => p.activated && Array.isArray(p.pages) && p.pages.length > 0,
);

if (activeWithPages.length === 0) return null;

const children: menu[] = activeWithPages.map((p) => {
const displayName = p.display_name || p.name || "Unknown Plugin";
const firstPage = p.pages[0];

return {
title: displayName,
icon: DEFAULT_ICON,
to: `/plugin-page/${encodeURIComponent(p.name)}/${encodeURIComponent(firstPage)}`,
isRawTitle: true,
};
});

return {
title: GROUP_I18N_KEY,
icon: GROUP_ICON,
children,
};
}

let initialFetched = false;

async function initPluginState() {
if (initialFetched) return;
initialFetched = true;
try {
const res = await axios.get("/api/plugin/get");
if (res.data?.status === "ok") {
pluginSidebarState.plugins = res.data.data ?? [];
}
} catch {
// 静默失败,后续 getExtensions() 会补充
}
}

export function usePluginSidebarItems() {
const pluginItems = shallowRef<menu | null>(null);

function refreshItems() {
pluginItems.value = buildPluginItems(pluginSidebarState.plugins);
}

onMounted(async () => {
await initPluginState();
refreshItems();
});

watch(
() => pluginSidebarState.plugins,
() => {
refreshItems();
},
);

return { pluginItems };
}
3 changes: 2 additions & 1 deletion dashboard/src/i18n/locales/en-US/core/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
"configTabs": {
"normal": "Normal Config",
"system": "System Config"
}
},
"pluginWebui": "Plugin Pages"
}
3 changes: 2 additions & 1 deletion dashboard/src/i18n/locales/ru-RU/core/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
"configTabs": {
"normal": "Обычная конфигурация",
"system": "Системная конфигурация"
}
},
"pluginWebui": "Страницы плагинов"
}
3 changes: 2 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/core/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
"configTabs": {
"normal": "普通配置",
"system": "系统配置"
}
},
"pluginWebui": "插件页面"
}
13 changes: 10 additions & 3 deletions dashboard/src/layouts/full/FullLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const routerLoadingStore = useRouterLoadingStore();
const isCurrentChatRoute = computed(
() => route.path === "/chat" || route.path.startsWith("/chat/"),
);
const isPluginPageRoute = computed(
() => route.path.startsWith("/plugin-page/"),
);
const isFullScreenRoute = computed(
() => isCurrentChatRoute.value || isPluginPageRoute.value,
);
const shouldMountChat = ref(isCurrentChatRoute.value);

const showSidebar = computed(() => !isCurrentChatRoute.value);
Expand Down Expand Up @@ -137,16 +143,17 @@ onMounted(() => {
class="page-wrapper"
:class="{ 'chat-mode-container': isCurrentChatRoute }"
:style="{
height: isCurrentChatRoute ? '100%' : 'calc(100% - 8px)',
padding: isCurrentChatRoute ? '0' : undefined,
minHeight: isCurrentChatRoute ? 'unset' : undefined,
height: isFullScreenRoute ? '100%' : 'calc(100% - 8px)',
padding: isFullScreenRoute ? '0' : undefined,
minHeight: isFullScreenRoute ? 'unset' : undefined,
}"
>
<div
:style="{
height: '100%',
width: '100%',
overflow: isCurrentChatRoute ? 'hidden' : undefined,
position: isPluginPageRoute ? 'relative' : undefined,
}"
>
<div
Expand Down
10 changes: 8 additions & 2 deletions dashboard/src/layouts/full/vertical-sidebar/NavItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const isItemActive = computed(() => {
}
return route.path === props.item.to;
});

const itemTitle = computed(() => {
if (!props.item?.title) return '';
return props.item.isRawTitle ? props.item.title : t(props.item.title);
});

</script>

<template>
Expand All @@ -32,7 +38,7 @@ const isItemActive = computed(() => {
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
:style="{ '--indent-padding': '0px' }">
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
{{ t(item.title) }}
{{ itemTitle }}
</v-list-item-title>
</v-list-item>
</template>
Expand All @@ -49,7 +55,7 @@ const isItemActive = computed(() => {
<template v-slot:prepend>
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
</template>
<v-list-item-title style="font-size: 14px;">{{ t(item.title) }}</v-list-item-title>
<v-list-item-title style="font-size: 14px;">{{ itemTitle }}</v-list-item-title>
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
{{ item.subCaption }}
</v-list-item-subtitle>
Expand Down
51 changes: 43 additions & 8 deletions dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,39 @@ import { ref, shallowRef, computed, onMounted, onUnmounted, watch } from 'vue';
import { useTheme } from 'vuetify';
import { useCustomizerStore } from '../../../stores/customizer';
import { useI18n } from '@/i18n/composables';
import sidebarItems from './sidebarItem';
import sidebarItems, { MORE_GROUP_KEY } from './sidebarItem';
import NavItem from './NavItem.vue';
import { applySidebarCustomization } from '@/utils/sidebarCustomization';
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
import { usePluginSidebarItems } from '@/composables/usePluginSidebarItems';

const { t, locale } = useI18n();

const customizer = useCustomizerStore();
const theme = useTheme();
const { pluginItems } = usePluginSidebarItems();

function buildSidebarMenu() {
const base = applySidebarCustomization(sidebarItems);
if (!pluginItems.value?.children?.length) return base;

const result = [];

for (const item of base) {
if (item.title === MORE_GROUP_KEY) {
result.push(pluginItems.value);
result.push(item);
} else {
result.push(item);
}
}

if (!base.some((item) => item.title === MORE_GROUP_KEY)) {
result.push(pluginItems.value);
}

return result;
}
Comment thread
lxfight marked this conversation as resolved.

function collectGroupValues(items, values = new Set()) {
items.forEach((item) => {
Expand Down Expand Up @@ -41,16 +65,22 @@ function getInitialOpenedItems(menuItems) {
}
}

const sidebarMenu = shallowRef(applySidebarCustomization(sidebarItems));
const sidebarMenu = shallowRef(buildSidebarMenu());

// 侧边栏分组展开状态持久化
const openedItems = ref(getInitialOpenedItems(sidebarMenu.value));
watch(openedItems, (val) => {
localStorage.setItem('sidebar_openedItems', JSON.stringify(sanitizeOpenedItems(val, sidebarMenu.value)));
}, { deep: true });

// 当插件项变化时(如插件启用/停用),刷新菜单
watch(pluginItems, () => {
sidebarMenu.value = buildSidebarMenu();
openedItems.value = sanitizeOpenedItems(openedItems.value, sidebarMenu.value);
});

function refreshSidebarMenu() {
sidebarMenu.value = applySidebarCustomization(sidebarItems);
sidebarMenu.value = buildSidebarMenu();
openedItems.value = sanitizeOpenedItems(openedItems.value, sidebarMenu.value);
}

Expand Down Expand Up @@ -232,26 +262,31 @@ function startSidebarResize(event) {
isResizing.value = true;
document.body.style.userSelect = 'none';
document.body.style.cursor = 'ew-resize';


// 拖拽时禁用 iframe 的 pointer-events,防止 iframe 截获 mousemove 事件导致拖拽卡住
const iframes = document.querySelectorAll('.plugin-page-frame');
iframes.forEach((el) => { el.style.pointerEvents = 'none'; });

const startX = event.clientX;
const startWidth = sidebarWidth.value;

function onMouseMoveResize(event) {
if (!isResizing.value) return;

const deltaX = event.clientX - startX;
const newWidth = Math.max(minSidebarWidth, Math.min(maxSidebarWidth, startWidth + deltaX));
sidebarWidth.value = newWidth;
}

function onMouseUpResize() {
isResizing.value = false;
document.body.style.userSelect = '';
document.body.style.cursor = '';
iframes.forEach((el) => { el.style.pointerEvents = ''; });
document.removeEventListener('mousemove', onMouseMoveResize);
document.removeEventListener('mouseup', onMouseUpResize);
}

document.addEventListener('mousemove', onMouseMoveResize);
document.addEventListener('mouseup', onMouseUpResize);
}
Expand Down
3 changes: 3 additions & 0 deletions dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ export interface menu {
disabled?: boolean;
type?: string;
subCaption?: string;
isRawTitle?: boolean;
}

export const MORE_GROUP_KEY = 'core.navigation.groups.more';

// 注意:这个文件现在包含i18n键值而不是直接的文本
// 在组件中使用时需要通过t()函数进行翻译
// 所有键名都使用 core.navigation.* 格式
Expand Down
6 changes: 4 additions & 2 deletions dashboard/src/utils/sidebarCustomization.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export function clearSidebarCustomization() {
* @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;

Expand All @@ -73,7 +75,7 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {

// 收集所有条目,按 title 建索引
defaultItems.forEach(item => {
if (item.children && item.title === 'core.navigation.groups.more') {
if (item.children && item.title === MORE_GROUP_KEY) {
item.children.forEach(child => {
all.set(child.title, cloneItems ? { ...child } : child);
defaultMore.push(child.title);
Expand Down Expand Up @@ -138,7 +140,7 @@ export function resolveSidebarItems(defaultItems, customization, options = {}) {
merged = [
...mainItems,
{
title: 'core.navigation.groups.more',
title: MORE_GROUP_KEY,
icon: 'mdi-dots-horizontal',
children
}
Expand Down
Loading
Loading