mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
refactor(frontend): modularize i18n architecture
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { buildBreadcrumbs, buildNavigation } from '@/layouts/main-layout/navigation'
|
||||
import type { MessageKey } from '@/i18n'
|
||||
|
||||
const translate = (key: MessageKey) => `tx:${key}`
|
||||
|
||||
function route(path: string, name?: string, meta: Record<string, unknown> = {}): RouteLocationNormalizedLoaded {
|
||||
return {
|
||||
path,
|
||||
fullPath: path,
|
||||
query: {},
|
||||
hash: '',
|
||||
name,
|
||||
params: {},
|
||||
matched: [],
|
||||
meta,
|
||||
redirectedFrom: undefined,
|
||||
} as RouteLocationNormalizedLoaded
|
||||
}
|
||||
|
||||
describe('main layout navigation builder', () => {
|
||||
it('builds user navigation from translation keys and active modules', () => {
|
||||
const navigation = buildNavigation({
|
||||
canAccessAdmin: false,
|
||||
modules: {},
|
||||
isModuleActive: (name) => name === 'referral',
|
||||
t: translate,
|
||||
})
|
||||
|
||||
expect(navigation.map(group => group.title)).toEqual([
|
||||
'tx:nav.group.overview',
|
||||
'tx:nav.group.resources',
|
||||
'tx:nav.group.account',
|
||||
])
|
||||
expect(navigation.flatMap(group => group.items.map(item => item.name))).toContain('tx:nav.myReferral')
|
||||
})
|
||||
|
||||
it('builds admin navigation with dynamic module menu items sorted by menu order', () => {
|
||||
const navigation = buildNavigation({
|
||||
canAccessAdmin: true,
|
||||
modules: {
|
||||
first: {
|
||||
active: true,
|
||||
admin_route: '/admin/first',
|
||||
admin_menu_group: 'management',
|
||||
admin_menu_order: 2,
|
||||
admin_menu_icon: 'Gift',
|
||||
display_name: 'First module',
|
||||
},
|
||||
second: {
|
||||
active: true,
|
||||
admin_route: '/admin/second',
|
||||
admin_menu_group: 'management',
|
||||
admin_menu_order: 1,
|
||||
admin_menu_icon: 'Key',
|
||||
display_name: 'Second module',
|
||||
},
|
||||
},
|
||||
isModuleActive: () => false,
|
||||
t: translate,
|
||||
})
|
||||
|
||||
const managementItems = navigation.find(group => group.title === 'tx:nav.group.management')?.items ?? []
|
||||
expect(managementItems.map(item => item.name)).toEqual(expect.arrayContaining(['Second module', 'First module']))
|
||||
expect(managementItems.findIndex(item => item.name === 'Second module')).toBeLessThan(
|
||||
managementItems.findIndex(item => item.name === 'First module')
|
||||
)
|
||||
})
|
||||
|
||||
it('builds translated breadcrumbs for settings and routing detail pages', () => {
|
||||
const navigation = buildNavigation({
|
||||
canAccessAdmin: true,
|
||||
modules: {},
|
||||
isModuleActive: () => false,
|
||||
t: translate,
|
||||
})
|
||||
|
||||
expect(buildBreadcrumbs({
|
||||
route: route('/dashboard/settings'),
|
||||
navigation,
|
||||
modules: {},
|
||||
isNavActive: () => false,
|
||||
t: translate,
|
||||
})).toEqual([
|
||||
{ label: 'tx:nav.group.account' },
|
||||
{ label: 'tx:breadcrumb.personalSettings' },
|
||||
])
|
||||
|
||||
expect(buildBreadcrumbs({
|
||||
route: route('/admin/routing/new', 'RoutingProfileCreate'),
|
||||
navigation,
|
||||
modules: {},
|
||||
isNavActive: href => href === '/admin/routing',
|
||||
t: translate,
|
||||
})).toEqual([
|
||||
{ label: 'tx:nav.group.management' },
|
||||
{ label: 'tx:nav.routing', href: '/admin/routing' },
|
||||
{ label: 'tx:breadcrumb.routingCreate' },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,217 @@
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import type { LucideIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
Box,
|
||||
Cog,
|
||||
CreditCard,
|
||||
Database,
|
||||
FileUp,
|
||||
FolderTree,
|
||||
Gift,
|
||||
Gauge,
|
||||
Home,
|
||||
Key,
|
||||
KeyRound,
|
||||
Layers,
|
||||
Package,
|
||||
Puzzle,
|
||||
Send,
|
||||
Server,
|
||||
Shield,
|
||||
SlidersHorizontal,
|
||||
Users,
|
||||
Wallet,
|
||||
Zap,
|
||||
Megaphone,
|
||||
} from 'lucide-vue-next'
|
||||
import type { NavigationGroup } from '@/components/layout/SidebarNav.vue'
|
||||
import type { ModuleStatus } from '@/api/modules'
|
||||
import { BUILTIN_TOOL_BREADCRUMBS } from '@/config/builtin-tools'
|
||||
import type { MessageKey } from '@/i18n'
|
||||
|
||||
type ModuleRecord = Record<string, ModuleStatus>
|
||||
type Translate = (key: MessageKey) => string
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
type NavItem = NavigationGroup['items'][number]
|
||||
|
||||
const moduleIconMap: Record<string, LucideIcon> = {
|
||||
Key,
|
||||
KeyRound,
|
||||
FileUp,
|
||||
Shield,
|
||||
Puzzle,
|
||||
Server,
|
||||
Send,
|
||||
SlidersHorizontal,
|
||||
CreditCard,
|
||||
Gift,
|
||||
}
|
||||
|
||||
function activeModuleItems(modules: ModuleRecord, group: string): NavItem[] {
|
||||
return Object.values(modules)
|
||||
.filter(m => m.active && m.admin_route && m.admin_menu_group === group)
|
||||
.sort((a, b) => a.admin_menu_order - b.admin_menu_order)
|
||||
.map(m => ({
|
||||
name: m.display_name,
|
||||
href: m.admin_route ?? '',
|
||||
icon: moduleIconMap[m.admin_menu_icon || ''] || Puzzle
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildNavigation(options: {
|
||||
canAccessAdmin: boolean
|
||||
modules: ModuleRecord
|
||||
isModuleActive: (name: string) => boolean
|
||||
t?: Translate
|
||||
}): NavigationGroup[] {
|
||||
const { canAccessAdmin, modules, isModuleActive } = options
|
||||
const t = options.t ?? ((key: MessageKey) => key)
|
||||
|
||||
if (!canAccessAdmin) {
|
||||
return [
|
||||
{
|
||||
title: t('nav.group.overview'),
|
||||
items: [
|
||||
{ name: t('nav.dashboard'), href: '/dashboard', icon: Home },
|
||||
{ name: t('nav.healthMonitor'), href: '/dashboard/endpoint-status', icon: Activity },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('nav.group.resources'),
|
||||
items: [
|
||||
{ name: t('nav.modelCatalog'), href: '/dashboard/models', icon: Box },
|
||||
{ name: t('nav.apiKeys'), href: '/dashboard/api-keys', icon: Key },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('nav.group.account'),
|
||||
items: [
|
||||
{ name: t('nav.walletCenter'), href: '/dashboard/wallet', icon: Wallet },
|
||||
{ name: t('nav.billingCenter'), href: '/dashboard/billing', icon: Package },
|
||||
...(isModuleActive('referral') ? [{ name: t('nav.myReferral'), href: '/dashboard/referral', icon: Gift }] : []),
|
||||
{ name: t('nav.usageStats'), href: '/dashboard/usage', icon: BarChart3 },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const systemItems: NavItem[] = [
|
||||
{ name: t('nav.announcements'), href: '/admin/announcements', icon: Megaphone },
|
||||
{ name: t('nav.cacheMonitoring'), href: '/admin/cache-monitoring', icon: Gauge },
|
||||
...activeModuleItems(modules, 'system'),
|
||||
{ name: t('nav.moduleManagement'), href: '/admin/modules', icon: Puzzle },
|
||||
{ name: t('nav.systemSettings'), href: '/admin/system', icon: Cog },
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('nav.group.overview'),
|
||||
items: [
|
||||
{ name: t('nav.dashboard'), href: '/admin/dashboard', icon: Home },
|
||||
{ name: t('nav.operations'), href: '/admin/operations', icon: Activity },
|
||||
{ name: t('nav.healthMonitor'), href: '/admin/health-monitor', icon: Activity },
|
||||
{ name: t('nav.userStats'), href: '/admin/user-stats', icon: BarChart3 },
|
||||
{ name: t('nav.costAnalysis'), href: '/admin/cost-analysis', icon: Gauge },
|
||||
{ name: t('nav.performanceAnalysis'), href: '/admin/performance-analysis', icon: Activity },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('nav.group.management'),
|
||||
items: [
|
||||
{ name: t('nav.userManagement'), href: '/admin/users', icon: Users },
|
||||
{ name: t('nav.providers'), href: '/admin/providers', icon: FolderTree },
|
||||
{ name: t('nav.modelManagement'), href: '/admin/models', icon: Layers },
|
||||
{ name: t('nav.routing'), href: '/admin/routing', icon: SlidersHorizontal },
|
||||
{ name: t('nav.pool'), href: '/admin/pool', icon: Database },
|
||||
{ name: t('nav.standaloneKeys'), href: '/admin/keys', icon: Key },
|
||||
{ name: t('nav.walletManagement'), href: '/admin/wallets', icon: Wallet },
|
||||
{ name: t('nav.billingManagement'), href: '/admin/billing-plans', icon: Package },
|
||||
...activeModuleItems(modules, 'management'),
|
||||
{ name: t('nav.asyncTasks'), href: '/admin/async-tasks', icon: Zap },
|
||||
{ name: t('nav.usageRecords'), href: '/admin/usage', icon: BarChart3 },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('nav.group.system'),
|
||||
items: systemItems
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function buildBreadcrumbs(options: {
|
||||
route: RouteLocationNormalizedLoaded
|
||||
navigation: NavigationGroup[]
|
||||
modules: ModuleRecord
|
||||
isNavActive: (href: string) => boolean
|
||||
t?: Translate
|
||||
}): BreadcrumbItem[] {
|
||||
const { route, navigation, modules, isNavActive } = options
|
||||
const t = options.t ?? ((key: MessageKey) => key)
|
||||
|
||||
if (route.path === '/dashboard/settings') {
|
||||
return [
|
||||
{ label: t('nav.group.account') },
|
||||
{ label: t('breadcrumb.personalSettings') }
|
||||
]
|
||||
}
|
||||
|
||||
if (route.meta?.module) {
|
||||
const moduleName = route.meta.module as string
|
||||
const moduleStatus = modules[moduleName]
|
||||
const displayName = moduleStatus?.display_name || moduleName
|
||||
return [
|
||||
{ label: t('nav.group.system') },
|
||||
{ label: t('nav.moduleManagement'), href: '/admin/modules' },
|
||||
{ label: displayName }
|
||||
]
|
||||
}
|
||||
|
||||
if (BUILTIN_TOOL_BREADCRUMBS[route.path]) {
|
||||
return [
|
||||
{ label: t('nav.group.system') },
|
||||
{ label: t('nav.moduleManagement'), href: '/admin/modules' },
|
||||
{ label: BUILTIN_TOOL_BREADCRUMBS[route.path] }
|
||||
]
|
||||
}
|
||||
|
||||
if (route.path.startsWith('/admin/routing/') && route.path !== '/admin/routing') {
|
||||
return [
|
||||
{ label: t('nav.group.management') },
|
||||
{ label: t('nav.routing'), href: '/admin/routing' },
|
||||
{
|
||||
label: route.name === 'RoutingProfileCreate'
|
||||
? t('breadcrumb.routingCreate')
|
||||
: t('breadcrumb.routingConfig')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
for (const group of navigation) {
|
||||
const activeItem = group.items.find(item => isNavActive(item.href))
|
||||
if (activeItem) {
|
||||
return [
|
||||
{ label: group.title || '' },
|
||||
{ label: activeItem.name }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const currentModule = Object.values(modules).find(
|
||||
m => m.admin_route && route.path === m.admin_route
|
||||
)
|
||||
if (currentModule) {
|
||||
return [
|
||||
{ label: t('nav.moduleManagement'), href: '/admin/modules' },
|
||||
{ label: currentModule.display_name }
|
||||
]
|
||||
}
|
||||
|
||||
return [{ label: t('nav.dashboard') }]
|
||||
}
|
||||
Reference in New Issue
Block a user