181 lines
7.9 KiB
TypeScript
181 lines
7.9 KiB
TypeScript
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
|
||
}
|
||
})
|
||
|
||
|