feat(web): consolidate product IA shell
Some checks failed
Code Review / ai-code-review (push) Successful in 18s
CD Pipeline / tests (push) Failing after 1m37s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 18s
CD Pipeline / tests (push) Failing after 1m37s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -18,22 +18,15 @@ import { useTranslations } from 'next-intl'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { useLocale } from 'next-intl'
|
||||
import {
|
||||
Activity,
|
||||
BookOpen,
|
||||
BrainCircuit,
|
||||
ClipboardList,
|
||||
GitBranch,
|
||||
Home,
|
||||
Radar,
|
||||
Search,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Wrench,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { useTerminalStore } from '@/stores/terminal.store'
|
||||
import { Z_INDEX } from '@/lib/constants/z-index'
|
||||
import {
|
||||
PRODUCT_BOTTOM_NAV_ITEMS,
|
||||
PRODUCT_NAV_SECTIONS,
|
||||
} from '@/lib/navigation/product-ia'
|
||||
|
||||
interface PaletteItem {
|
||||
id: string
|
||||
@@ -45,8 +38,18 @@ interface PaletteItem {
|
||||
secondary?: boolean
|
||||
}
|
||||
|
||||
const COMMAND_PALETTE_PRIMARY_NAV_IDS = new Set([
|
||||
'command-center',
|
||||
'awooop-overview',
|
||||
'observability',
|
||||
'iwooos-security',
|
||||
'knowledge-automation',
|
||||
'operations',
|
||||
])
|
||||
|
||||
export function CommandPalette() {
|
||||
const t = useTranslations('commandPalette')
|
||||
const tNav = useTranslations('nav')
|
||||
const router = useRouter()
|
||||
const locale = useLocale()
|
||||
const pathname = usePathname()
|
||||
@@ -63,6 +66,29 @@ export function CommandPalette() {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const navigationItems: PaletteItem[] = [
|
||||
...PRODUCT_NAV_SECTIONS.flatMap(section => section.items),
|
||||
...PRODUCT_BOTTOM_NAV_ITEMS,
|
||||
].map((item) => {
|
||||
const path = item.href === '/' ? '' : item.href
|
||||
|
||||
return {
|
||||
id: `nav-${item.id}`,
|
||||
label: tNav(item.labelKey),
|
||||
group: t('groupNav'),
|
||||
icon: <item.Icon size={14} />,
|
||||
action: () => nav(path),
|
||||
keywords: [
|
||||
item.id,
|
||||
item.href,
|
||||
item.labelKey,
|
||||
...(item.aliases ?? []),
|
||||
...(item.relatedPaths ?? []),
|
||||
],
|
||||
secondary: !COMMAND_PALETTE_PRIMARY_NAV_IDS.has(item.id),
|
||||
}
|
||||
})
|
||||
|
||||
const items: PaletteItem[] = [
|
||||
// 快速動作
|
||||
{
|
||||
@@ -73,146 +99,7 @@ export function CommandPalette() {
|
||||
action: () => { openTerminal(); setOpen(false) },
|
||||
keywords: ['terminal', '終端', 'omni', 'cmd', '指令'],
|
||||
},
|
||||
// 導航
|
||||
{
|
||||
id: 'home',
|
||||
label: t('actionGoHome'),
|
||||
group: t('groupNav'),
|
||||
icon: <Home size={14} />,
|
||||
action: () => nav(''),
|
||||
keywords: ['home', '首頁', '指令中心', 'dashboard'],
|
||||
},
|
||||
{
|
||||
id: 'approvals',
|
||||
label: t('actionGoApprovals'),
|
||||
group: t('groupNav'),
|
||||
icon: <Zap size={14} />,
|
||||
action: () => nav('/authorizations'),
|
||||
keywords: ['授權', 'approve', '批准', 'authorization'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'observability',
|
||||
label: t('actionGoObservability'),
|
||||
group: t('groupNav'),
|
||||
icon: <Activity size={14} />,
|
||||
action: () => nav('/observability'),
|
||||
keywords: ['observability', '可觀測性', 'monitor', '監控'],
|
||||
},
|
||||
{
|
||||
id: 'awooop-home',
|
||||
label: t('actionGoAwoooP'),
|
||||
group: t('groupNav'),
|
||||
icon: <BrainCircuit size={14} />,
|
||||
action: () => nav('/awooop'),
|
||||
keywords: ['awooop', 'operator', 'console', '操作控制台', '總覽'],
|
||||
},
|
||||
{
|
||||
id: 'knowledge-automation',
|
||||
label: t('actionGoKnowledgeAutomation'),
|
||||
group: t('groupNav'),
|
||||
icon: <BookOpen size={14} />,
|
||||
action: () => nav('/knowledge-base'),
|
||||
keywords: ['knowledge', 'automation', '自動化', '知識', 'kb', 'playbook', 'runbook'],
|
||||
},
|
||||
{
|
||||
id: 'governance-security',
|
||||
label: t('actionGoGovernanceSecurity'),
|
||||
group: t('groupNav'),
|
||||
icon: <ShieldCheck size={14} />,
|
||||
action: () => nav('/governance'),
|
||||
keywords: ['governance', 'security', 'iwooos', 'code review', '治理', '安全', '資安', '審查'],
|
||||
},
|
||||
{
|
||||
id: 'awooop-work-items',
|
||||
label: t('actionGoWorkItems'),
|
||||
group: t('groupNav'),
|
||||
icon: <ClipboardList size={14} />,
|
||||
action: () => nav('/awooop/work-items'),
|
||||
keywords: ['work items', '工作鏈路', '工作項', 'recurrence'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'awooop-runs',
|
||||
label: t('actionGoRuns'),
|
||||
group: t('groupNav'),
|
||||
icon: <Activity size={14} />,
|
||||
action: () => nav('/awooop/runs'),
|
||||
keywords: ['runs', 'run monitor', '執行監控', 'run 監控'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'awooop-approvals',
|
||||
label: t('actionGoApprovalQueue'),
|
||||
group: t('groupNav'),
|
||||
icon: <ShieldCheck size={14} />,
|
||||
action: () => nav('/awooop/approvals'),
|
||||
keywords: ['approval queue', '審批佇列', '人工閘門', 'approve'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'automation',
|
||||
label: t('actionGoAutomation'),
|
||||
group: t('groupNav'),
|
||||
icon: <Wrench size={14} />,
|
||||
action: () => nav('/automation'),
|
||||
keywords: ['automation', '自動化', 'auto'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
label: t('actionGoOperations'),
|
||||
group: t('groupNav'),
|
||||
icon: <GitBranch size={14} />,
|
||||
action: () => nav('/operations'),
|
||||
keywords: ['operations', '營運', 'ops'],
|
||||
},
|
||||
{
|
||||
id: 'iwooos',
|
||||
label: t('actionGoIwooos'),
|
||||
group: t('groupNav'),
|
||||
icon: <Radar size={14} />,
|
||||
action: () => nav('/iwooos'),
|
||||
keywords: [
|
||||
'iwooos',
|
||||
'information security',
|
||||
'security',
|
||||
'安全',
|
||||
'安全合規',
|
||||
'compliance',
|
||||
'合規',
|
||||
'資安網',
|
||||
'資安態勢',
|
||||
],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
label: t('actionGoKnowledge'),
|
||||
group: t('groupNav'),
|
||||
icon: <BookOpen size={14} />,
|
||||
action: () => nav('/knowledge-base'),
|
||||
keywords: ['knowledge', '知識', '殿堂', 'kb'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: t('actionGoSettings'),
|
||||
group: t('groupNav'),
|
||||
icon: <Settings size={14} />,
|
||||
action: () => nav('/settings'),
|
||||
keywords: ['settings', '設定', 'config'],
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
id: 'terminal-page',
|
||||
label: t('actionGoTerminal'),
|
||||
group: t('groupNav'),
|
||||
icon: <Terminal size={14} />,
|
||||
action: () => nav('/terminal'),
|
||||
keywords: ['terminal', '終端', 'shell'],
|
||||
secondary: true,
|
||||
},
|
||||
...navigationItems,
|
||||
]
|
||||
|
||||
const filtered = query.trim() === ''
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* PageTabs — 共用頁籤容器元件
|
||||
* ============================
|
||||
* Sprint 5: 多頁籤整合介面的核心元件
|
||||
*
|
||||
* 功能:
|
||||
* - Tab 切換 + URL query 同步 (?tab=alerts)
|
||||
* - Badge 數字 (如告警數)
|
||||
* - 瀏覽器後退回到上一個 Tab
|
||||
* - React.lazy + Suspense 按需載入
|
||||
* - 骨架屏 Loading 狀態
|
||||
*
|
||||
* 設計規範:
|
||||
* - Tab Bar 高度: 36px
|
||||
* - Active: 底部 2px accent (#d97757) + 文字加粗
|
||||
* - 字體: DM Mono 12px
|
||||
* - 邊框: 0.5px solid #e0ddd4
|
||||
*
|
||||
* 建立時間: 2026-04-08 (台北時區)
|
||||
* 建立者: Claude Code (Sprint 5 Phase 1)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect, Suspense, type ReactNode } from 'react'
|
||||
import type { Route } from 'next'
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
// =============================================================================
|
||||
// 型別
|
||||
// =============================================================================
|
||||
|
||||
export interface TabConfig {
|
||||
/** Tab ID (用於 URL query: ?tab=alerts) */
|
||||
id: string
|
||||
/** 顯示名稱 */
|
||||
label: string
|
||||
/** Badge 數字 (如告警數, 待審批數) */
|
||||
badge?: number
|
||||
/** Tab 內容 (React 元件) */
|
||||
content: ReactNode
|
||||
}
|
||||
|
||||
export interface PageTabsProps {
|
||||
/** Tab 配置清單 */
|
||||
tabs: TabConfig[]
|
||||
/** 預設顯示的 Tab ID */
|
||||
defaultTab?: string
|
||||
/** 是否同步 URL query (?tab=xxx) */
|
||||
syncWithUrl?: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 骨架屏
|
||||
// =============================================================================
|
||||
|
||||
function TabSkeleton() {
|
||||
const t = useTranslations('dashboard')
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
|
||||
<style>{`
|
||||
@keyframes tab-lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-3px)} }
|
||||
@keyframes tab-lobster-wave { 0%,100%{opacity:.4} 50%{opacity:1} }
|
||||
`}</style>
|
||||
<div style={{ animation: 'tab-lobster-bob 1.2s ease-in-out infinite' }}>
|
||||
<svg width="32" height="36" viewBox="0 0 18 20" fill="none">
|
||||
<ellipse cx="9" cy="13" rx="5.5" ry="6.5" fill="#E85530" opacity="0.9" />
|
||||
<circle cx="9" cy="7.5" r="4.5" fill="#E85530" opacity="0.9" />
|
||||
<circle cx="7" cy="6.5" r="1" fill="#b03a1a" />
|
||||
<circle cx="11" cy="6.5" r="1" fill="#b03a1a" />
|
||||
<path d="M3.5 10 Q1 9 1.5 12 Q2 14 4 13" stroke="#E85530" strokeWidth="1.2" fill="none" strokeLinecap="round" />
|
||||
<ellipse cx="1.5" cy="12" rx="1.2" ry="1.5" fill="#E85530" opacity="0.7" transform="rotate(-10 1.5 12)" />
|
||||
<path d="M14.5 10 Q17 9 16.5 12 Q16 14 14 13" stroke="#E85530" strokeWidth="1.2" fill="none" strokeLinecap="round" />
|
||||
<ellipse cx="16.5" cy="12" rx="1.2" ry="1.5" fill="#E85530" opacity="0.7" transform="rotate(10 16.5 12)" />
|
||||
<path d="M7 3 Q5 1 3 2" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round" />
|
||||
<path d="M11 3 Q13 1 15 2" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round" />
|
||||
<path d="M6 19 Q9 21 12 19" stroke="#E85530" strokeWidth="1.2" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#87867f', fontFamily: "'DM Mono', monospace", animation: 'tab-lobster-wave 2s ease-in-out infinite' }}>
|
||||
{t('loading')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 元件
|
||||
// =============================================================================
|
||||
|
||||
export function PageTabs({ tabs, defaultTab, syncWithUrl = true }: PageTabsProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
// 從 URL 讀取當前 Tab,或使用預設值
|
||||
const urlTab = syncWithUrl ? searchParams.get('tab') : null
|
||||
const initialTab = urlTab || defaultTab || tabs[0]?.id || ''
|
||||
|
||||
const [activeTab, setActiveTab] = useState(initialTab)
|
||||
const tabIds = tabs.map(t => t.id).join('|')
|
||||
const firstTabId = tabs[0]?.id || ''
|
||||
const knownTabIds = useMemo(() => tabIds.split('|'), [tabIds])
|
||||
const urlActiveTab = syncWithUrl && urlTab && knownTabIds.includes(urlTab) ? urlTab : null
|
||||
const resolvedActiveTab = urlActiveTab || activeTab || defaultTab || firstTabId
|
||||
|
||||
useEffect(() => {
|
||||
const nextTab = (syncWithUrl ? urlTab : null) || defaultTab || firstTabId
|
||||
if (!nextTab || !knownTabIds.includes(nextTab)) return
|
||||
setActiveTab(prev => prev === nextTab ? prev : nextTab)
|
||||
}, [defaultTab, firstTabId, knownTabIds, syncWithUrl, urlTab])
|
||||
|
||||
// 切換 Tab
|
||||
const switchTab = useCallback((tabId: string) => {
|
||||
setActiveTab(tabId)
|
||||
if (syncWithUrl) {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (tabId === (defaultTab || tabs[0]?.id)) {
|
||||
params.delete('tab')
|
||||
} else {
|
||||
params.set('tab', tabId)
|
||||
}
|
||||
const query = params.toString()
|
||||
const nextPath = `${pathname}${query ? `?${query}` : ''}` as Route
|
||||
router.push(nextPath, { scroll: false })
|
||||
}
|
||||
}, [syncWithUrl, searchParams, router, pathname, defaultTab, tabs])
|
||||
|
||||
// 找到目前的 Tab 內容
|
||||
const activeContent = useMemo(() => {
|
||||
return tabs.find(t => t.id === resolvedActiveTab)?.content ?? tabs[0]?.content
|
||||
}, [tabs, resolvedActiveTab])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Tab Bar */}
|
||||
<div
|
||||
className="page-tabs-bar"
|
||||
style={{
|
||||
height: 36,
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
background: '#fff',
|
||||
flexShrink: 0,
|
||||
padding: '0 20px',
|
||||
maxWidth: '100%',
|
||||
overflowX: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
const isActive = tab.id === resolvedActiveTab
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
data-tab-id={tab.id}
|
||||
className="page-tabs-button"
|
||||
onClick={() => switchTab(tab.id)}
|
||||
style={{
|
||||
padding: '0 14px',
|
||||
fontSize: 12,
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
color: isActive ? '#d97757' : '#87867f',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottomWidth: 2,
|
||||
borderBottomStyle: 'solid',
|
||||
borderBottomColor: isActive ? '#d97757' : 'transparent',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
transition: 'all 0.12s',
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.badge != null && tab.badge > 0 && (
|
||||
<span
|
||||
style={{
|
||||
background: '#cc2200',
|
||||
color: '#fff',
|
||||
fontSize: 8,
|
||||
padding: '0 5px',
|
||||
borderRadius: 4,
|
||||
fontWeight: 700,
|
||||
minWidth: 14,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<style jsx global>{`
|
||||
@media (max-width: 640px) {
|
||||
.page-tabs-bar {
|
||||
height: auto !important;
|
||||
min-height: 36px;
|
||||
flex-wrap: wrap;
|
||||
overflow-x: hidden !important;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
.page-tabs-button {
|
||||
flex: 1 1 auto;
|
||||
min-width: min(100%, 86px);
|
||||
justify-content: center;
|
||||
padding: 0 8px !important;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Tab 內容 */}
|
||||
<Suspense fallback={<TabSkeleton />}>
|
||||
{activeContent}
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageTabs
|
||||
@@ -26,26 +26,14 @@ import { usePathname } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
BarChart3,
|
||||
Bell,
|
||||
BookOpen,
|
||||
BrainCircuit,
|
||||
Building2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ClipboardList,
|
||||
FileText,
|
||||
GitPullRequest,
|
||||
HelpCircle,
|
||||
LayoutDashboard,
|
||||
Monitor,
|
||||
Package,
|
||||
PlayCircle,
|
||||
Radar,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
PRODUCT_BOTTOM_NAV_ITEMS,
|
||||
PRODUCT_NAV_SECTIONS,
|
||||
type ProductNavItem,
|
||||
} from '@/lib/navigation/product-ia'
|
||||
// Phase 8.0 #15: 改用 approval store SSE (移除 polling)
|
||||
import { useApprovalStore } from '@/stores/approval.store'
|
||||
|
||||
@@ -61,22 +49,6 @@ interface SidebarProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
type NavItemConfig = {
|
||||
id: string
|
||||
href: string
|
||||
labelKey: string
|
||||
Icon: typeof LayoutDashboard
|
||||
aliases?: string[]
|
||||
relatedPaths?: string[]
|
||||
exact?: boolean
|
||||
badge?: boolean // 是否顯示動態徽章
|
||||
}
|
||||
|
||||
type NavSection = {
|
||||
sectionKey: string
|
||||
items: NavItemConfig[]
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2026-06-26: Operator-first IA repair
|
||||
// ============================================================
|
||||
@@ -84,129 +56,6 @@ type NavSection = {
|
||||
// 側欄放高頻任務與產品導航,頁面內不再重複用二層分頁藏入口。
|
||||
// ============================================================
|
||||
|
||||
const NAV_SECTIONS: NavSection[] = [
|
||||
{
|
||||
sectionKey: 'workspaces',
|
||||
items: [
|
||||
{ id: 'command-center', href: '/', labelKey: 'commandCenter', Icon: LayoutDashboard },
|
||||
{
|
||||
id: 'awooop-overview',
|
||||
href: '/awooop',
|
||||
labelKey: 'awooopHome',
|
||||
Icon: BrainCircuit,
|
||||
exact: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionKey: 'queues',
|
||||
items: [
|
||||
{
|
||||
id: 'awooop-work-items',
|
||||
href: '/awooop/work-items',
|
||||
labelKey: 'workItems',
|
||||
Icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
id: 'awooop-runs',
|
||||
href: '/awooop/runs',
|
||||
labelKey: 'runMonitor',
|
||||
Icon: PlayCircle,
|
||||
},
|
||||
{
|
||||
id: 'awooop-approvals',
|
||||
href: '/awooop/approvals',
|
||||
labelKey: 'approvalQueue',
|
||||
Icon: ShieldCheck,
|
||||
aliases: ['/authorizations'],
|
||||
badge: true,
|
||||
},
|
||||
{
|
||||
id: 'awooop-contracts',
|
||||
href: '/awooop/contracts',
|
||||
labelKey: 'contracts',
|
||||
Icon: FileText,
|
||||
},
|
||||
{
|
||||
id: 'awooop-tenants',
|
||||
href: '/awooop/tenants',
|
||||
labelKey: 'tenants',
|
||||
Icon: Building2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionKey: 'monitoring',
|
||||
items: [
|
||||
{
|
||||
id: 'observability',
|
||||
href: '/observability',
|
||||
labelKey: 'observability',
|
||||
Icon: Monitor,
|
||||
aliases: ['/monitoring', '/apm', '/errors', '/apps', '/services', '/topology'],
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
href: '/alerts',
|
||||
labelKey: 'alerts',
|
||||
Icon: Bell,
|
||||
aliases: ['/alert-operation-logs', '/notifications'],
|
||||
},
|
||||
{
|
||||
id: 'iwooos-security',
|
||||
href: '/governance',
|
||||
labelKey: 'iwooos',
|
||||
Icon: ShieldCheck,
|
||||
aliases: ['/security-compliance'],
|
||||
relatedPaths: ['/iwooos', '/security', '/compliance'],
|
||||
},
|
||||
{
|
||||
id: 'code-review',
|
||||
href: '/code-review',
|
||||
labelKey: 'codeReview',
|
||||
Icon: GitPullRequest,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionKey: 'knowledge',
|
||||
items: [
|
||||
{
|
||||
id: 'knowledge-automation',
|
||||
href: '/knowledge-base',
|
||||
labelKey: 'knowledgeAutomation',
|
||||
Icon: BookOpen,
|
||||
aliases: ['/knowledge', '/automation', '/auto-repair', '/drift', '/neural-command'],
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
href: '/reports',
|
||||
labelKey: 'reports',
|
||||
Icon: BarChart3,
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
href: '/operations',
|
||||
labelKey: 'operationsOverview',
|
||||
Icon: Package,
|
||||
aliases: ['/deployments', '/tickets', '/cost', '/billing', '/action-logs'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionKey: 'system',
|
||||
items: [
|
||||
{ id: 'terminal', href: '/terminal', labelKey: 'terminal', Icon: Terminal },
|
||||
{ id: 'settings', href: '/settings', labelKey: 'settings', Icon: Settings },
|
||||
{ id: 'classic', href: '/classic', labelKey: 'classicAICenter', Icon: Radar },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const BOTTOM_NAV_ITEMS: NavItemConfig[] = [
|
||||
{ id: 'help', href: '/help', labelKey: 'help', Icon: HelpCircle },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// Component
|
||||
// =============================================================================
|
||||
@@ -244,7 +93,7 @@ export function Sidebar({
|
||||
return pathname === fullHref || pathname.startsWith(fullHref + '/')
|
||||
}
|
||||
|
||||
const isActive = (item: NavItemConfig) => (
|
||||
const isActive = (item: ProductNavItem) => (
|
||||
isRouteActive(item.href, item.exact) ||
|
||||
item.aliases?.some(alias => isRouteActive(alias)) === true ||
|
||||
item.relatedPaths?.some(path => isRouteActive(path)) === true
|
||||
@@ -270,7 +119,7 @@ export function Sidebar({
|
||||
|
||||
{/* 導航列表 - Operator workflow sections */}
|
||||
<nav className="flex-1 py-2 overflow-y-auto">
|
||||
{NAV_SECTIONS.map(section => (
|
||||
{PRODUCT_NAV_SECTIONS.map(section => (
|
||||
<div key={section.sectionKey} style={{ marginBottom: 4 }}>
|
||||
{!collapsed && (
|
||||
<div style={{
|
||||
@@ -340,7 +189,7 @@ export function Sidebar({
|
||||
<div style={{ borderTop: '0.5px solid #e0ddd4', margin: '6px 0' }} />
|
||||
|
||||
{/* 底部菜單 */}
|
||||
{BOTTOM_NAV_ITEMS.map(item => {
|
||||
{PRODUCT_BOTTOM_NAV_ITEMS.map(item => {
|
||||
const active = isActive(item)
|
||||
return (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user