Files
awoooi/apps/web/src/components/layout/app-layout.tsx
Your Name 55d1df24e7
All checks were successful
CD Pipeline / tests (push) Successful in 1m20s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 3m44s
CD Pipeline / post-deploy-checks (push) Successful in 2m1s
feat(web): render automation blueprint diagrams
2026-05-26 10:15:07 +08:00

153 lines
4.2 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.
'use client'
/**
* AppLayout - 全域應用佈局
* ========================
* CPO-102: 整合側邊欄 + 頂部狀態列
*
* Features:
* - 響應式側邊欄折疊
* - 主內容區域自動適應
* - 持久化折疊狀態 (localStorage)
* - **Phase 19 修復**: 全局 SSE 連接 (所有頁面共享連線狀態)
*
* @updated 2026-03-28 - 修復 Header「已斷線」狀態問題
*/
import { useState, useEffect } from 'react'
import { Sidebar } from './sidebar'
import { Header } from './header'
import { DotMatrixBg } from '@/components/ui/dot-matrix-bg'
import { cn } from '@/lib/utils'
import { useDashboardStore } from '@/stores/dashboard.store'
// =============================================================================
// Types
// =============================================================================
interface AppLayoutProps {
locale: string
children: React.ReactNode
showBackground?: boolean
/** 主頁用:移除 pt-16 + p-6讓 children 自行控制佈局 */
fullBleed?: boolean
}
// =============================================================================
// Constants
// =============================================================================
const SIDEBAR_STATE_KEY = 'awoooi-sidebar-collapsed'
const MOBILE_SHELL_MEDIA = '(max-width: 768px)'
// =============================================================================
// Component
// =============================================================================
export function AppLayout({
locale,
children,
showBackground = true,
fullBleed = false,
}: AppLayoutProps) {
const [collapsed, setCollapsed] = useState(false)
const [mobileShell, setMobileShell] = useState(false)
const [mounted, setMounted] = useState(false)
// Phase 19 修復: 全局 SSE 連接
const { connect, disconnect } = useDashboardStore()
// Load collapsed state from localStorage
useEffect(() => {
setMounted(true)
const saved = localStorage.getItem(SIDEBAR_STATE_KEY)
if (saved === 'true') {
setCollapsed(true)
}
}, [])
useEffect(() => {
const media = window.matchMedia(MOBILE_SHELL_MEDIA)
const updateMobileShell = () => setMobileShell(media.matches)
updateMobileShell()
media.addEventListener('change', updateMobileShell)
return () => {
media.removeEventListener('change', updateMobileShell)
}
}, [])
// Phase 19 修復: 全局啟動 SSE 連接 (所有頁面共享)
useEffect(() => {
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || ''
if (apiBaseUrl) {
console.log('[AppLayout] 全局 SSE 連接啟動...', apiBaseUrl)
connect(apiBaseUrl)
}
return () => {
console.log('[AppLayout] SSE 連接關閉')
disconnect()
}
}, [connect, disconnect])
// Save collapsed state to localStorage
const handleToggle = () => {
const newState = !collapsed
setCollapsed(newState)
localStorage.setItem(SIDEBAR_STATE_KEY, String(newState))
}
// Keep the navigation shell in the server-rendered HTML. If a rolling deploy
// or stale browser cache delays hydration, the operator still has navigation.
const effectiveCollapsed = mounted ? collapsed : false
const shellCollapsed = mobileShell || effectiveCollapsed
return (
<div className="min-h-screen bg-nothing-gray-50">
{/* Background */}
{showBackground && (
<DotMatrixBg
spacing={24}
dotSize={1}
dotColor="rgba(0, 0, 0, 0.06)"
bgColor="#FAFAFA"
/>
)}
{/* Sidebar */}
<Sidebar
locale={locale}
collapsed={shellCollapsed}
onToggle={handleToggle}
/>
{/* Header */}
<Header
locale={locale}
sidebarCollapsed={shellCollapsed}
/>
{/* Main Content */}
<main
className={cn(
'relative z-10',
'pt-[68px]',
'transition-all duration-300 ease-out',
shellCollapsed ? 'ml-16' : 'ml-[224px]'
)}
>
{fullBleed ? (
// fullBleed: 無 p-6children 自行控制佈局(主頁用)
children
) : (
<div className="p-6">
{children}
</div>
)}
</main>
</div>
)
}