181 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export interface AppMenuItem {
id: string
key: string
label: string
icon?: string
type: 'pagetype' | 'route' | 'url' | 'workspace' | 'group' // 菜单类型,新增 group
pagetype?: string // 页面类型名称Local AI Agent
routeName?: string // 路由名
url?: string // URL路径
workspaceName?: string // Workspace 名称(工作区文档名)
// 层级关系
parentId?: string | null
order?: number
hidden?: boolean
children?: AppMenuItem[]
page?: {
pagetype: string
list?: boolean
detail?: boolean
order_by?: string
}
}
const STORAGE_KEY = 'app.menu.items'
function loadFromStorage(): AppMenuItem[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return parsed
return []
} catch {
return []
}
}
function saveToStorage(items: AppMenuItem[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
}
// 默认菜单,与现有路由对应
function getDefaultMenus(): AppMenuItem[] {
return [
{ id: 'dashboard', key: 'Dashboard', label: 'Dashboard', icon: 'tabler:dashboard', routeName: 'Dashboard', order: 1, type: 'route' },
{ id: 'work', key: 'work', label: 'Work', icon: 'tabler:device-desktop', type: 'workspace', workspaceName: 'work', url: '/workspace/work', order: 2 },
{ id: 'design', key: 'design', label: 'Design', icon: 'tabler:pencil', type: 'workspace', workspaceName: 'design', url: '/workspace/design', order: 3 },
{ id: 'website', key: 'website', label: 'Website', icon: 'tabler:world', type: 'workspace', workspaceName: 'jsite', url: '/workspace/jsite', order: 4 },
{ id: 'agents', key: 'local-ai-agent', label: 'Agents', icon: 'hugeicons:robotic', type: 'pagetype', pagetype: 'Local Ai Agent', order: 5 },
{ id: 'nodes', key: 'local-ai-node', label: 'Nodes', icon: 'carbon:add-child-node', type: 'pagetype', pagetype: 'Local Ai Node', order: 6 },
{ id: 'localJobs', key: 'LocalJobList', label: 'Task Queue', icon: 'iconoir:task-list', type: 'route', routeName: 'LocalJobList', order: 7 },
{ id: 'scheduledJobs', key: 'ScheduledJobList', label: 'Scheduled Jobs', icon: 'carbon:event-schedule', type: 'route', routeName: 'ScheduledJobList', order: 8 },
{ id: 'dev-group', key: 'dev-group', label: 'Development', icon: 'tabler:code', type: 'group', order: 9 },
{ id: 'dev-template', key: 'dev-template', label: 'PageType Template', icon: 'tabler:file-code', type: 'route', routeName: 'CreatePagetypeTemplate', parentId: 'dev-group', order: 1 },
{ id: 'dev-app-template', key: 'dev-app-template', label: 'App Template', icon: 'tabler:app-window', type: 'route', routeName: 'CreateAppTemplate', parentId: 'dev-group', order: 1.5 },
{ id: 'package', key: 'package', label: 'Package', icon: 'tabler:package', type: 'pagetype', pagetype: 'Package', parentId: 'dev-group', order: 2 },
{ id: 'package-release', key: 'package-release', label: 'Package Release', icon: 'tabler:package-export', type: 'pagetype', pagetype: 'Package Release', parentId: 'dev-group', order: 3 },
{ id: 'app-installer', key: 'AppInstaller', label: 'App Installer', icon: 'tabler:upload', type: 'route', routeName: 'AppInstaller', parentId: 'dev-group', order: 5 },
{ id: 'installed-apps', key: 'InstalledApps', label: 'Installed Apps', icon: 'tabler:apps', type: 'route', routeName: 'InstalledApps', parentId: 'dev-group', order: 6 },
{ id: 'my-published-apps', key: 'MyPublishedApps', label: 'My Published Apps', icon: 'tabler:cloud-upload', type: 'route', routeName: 'MyPublishedApps', parentId: 'dev-group', order: 7 },
{ id: 'my-published-nodes', key: 'MyPublishedNodes', label: '已发布节点', icon: 'carbon:add-child-node', type: 'route', routeName: 'MyPublishedNodes', parentId: 'dev-group', order: 7.1 },
{ id: 'my-published-agents', key: 'MyPublishedAgents', label: '已发布智能体', icon: 'hugeicons:robotic', type: 'route', routeName: 'MyPublishedAgents', parentId: 'dev-group', order: 7.2 },
{ id: 'app-marketplace', key: 'AppMarketplace', label: 'App Marketplace', icon: 'tabler:shopping-cart', type: 'route', routeName: 'AppMarketplace', parentId: 'dev-group', order: 7.5 },
{ id: 'node-marketplace', key: 'NodeMarketplace', label: 'Node Marketplace', icon: 'carbon:add-child-node', type: 'route', routeName: 'NodeMarketplace', parentId: 'dev-group', order: 8 },
{ id: 'agent-marketplace', key: 'AgentMarketplace', label: 'Agent Marketplace', icon: 'hugeicons:robotic', type: 'route', routeName: 'AgentMarketplace', parentId: 'dev-group', order: 8.5 },
{ id: 'menuManager', key: 'MenuManager', label: 'Menu Management', icon: 'tabler:menu-2', type: 'route', routeName: 'MenuManager', order: 10 },
{ id: 'settings', key: 'Settings', label: 'Settings', icon: 'tabler:settings', routeName: 'Settings', order: 11, type: 'route' }
]
}
export const useMenuStore = defineStore('menu', () => {
// 正确初始化为数组,而不是函数
const initItems = () => {
const cached = loadFromStorage()
if (Array.isArray(cached) && cached.length) return cached as AppMenuItem[]
const defaults = getDefaultMenus()
saveToStorage(defaults)
return defaults
}
const items = ref<AppMenuItem[]>(initItems())
// 统一保存
function persist() {
// 分层排序:先按层级分组,再分别排序
const rootMenus = items.value.filter(m => !m.parentId)
const childMenus = items.value.filter(m => m.parentId)
// 对根菜单排序
rootMenus.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
// 对子菜单按父菜单分组排序
const groupedChildren = childMenus.reduce((acc, child) => {
const parentId = child.parentId!
if (!acc[parentId]) acc[parentId] = []
acc[parentId].push(child)
return acc
}, {} as Record<string, AppMenuItem[]>)
// 对每个父菜单的子菜单排序
Object.values(groupedChildren).forEach(children => {
children.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
})
// 重新组合:先放根菜单,再按父菜单顺序放子菜单
const sorted: AppMenuItem[] = []
rootMenus.forEach(root => {
sorted.push(root)
if (groupedChildren[root.id]) {
sorted.push(...groupedChildren[root.id])
}
})
saveToStorage(sorted)
items.value = sorted
}
function addMenu(item: AppMenuItem) {
const newItem = { ...item, id: item.id || crypto.randomUUID() }
// 如果没有指定order自动分配
if (newItem.order === undefined || newItem.order === null) {
if (newItem.parentId) {
// 子菜单在父菜单下分配独立的order
const siblings = items.value.filter(m => m.parentId === newItem.parentId)
const maxSiblingOrder = Math.max(...siblings.map(s => s.order ?? 0), 0)
newItem.order = maxSiblingOrder + 1
} else {
// 父菜单在根级别分配独立的order
const rootMenus = items.value.filter(m => !m.parentId)
const maxRootOrder = Math.max(...rootMenus.map(r => r.order ?? 0), 0)
newItem.order = maxRootOrder + 1
}
}
items.value.push(newItem)
persist()
}
function updateMenu(id: string, patch: Partial<AppMenuItem>) {
const idx = items.value.findIndex(m => m.id === id)
if (idx >= 0) {
items.value[idx] = { ...items.value[idx], ...patch }
persist()
}
}
function removeMenu(id: string) {
items.value = items.value.filter(m => m.id !== id)
persist()
}
function resetDefault() {
items.value = getDefaultMenus()
persist()
}
function clearCache() {
localStorage.removeItem(STORAGE_KEY)
items.value = getDefaultMenus()
persist()
}
const visibleItems = computed(() => items.value.filter(m => !m.hidden))
return {
items,
visibleItems,
addMenu,
updateMenu,
removeMenu,
resetDefault,
clearCache,
persist
}
})