153 lines
4.2 KiB
TypeScript
153 lines
4.2 KiB
TypeScript
'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-6,children 自行控制佈局(主頁用)
|
||
children
|
||
) : (
|
||
<div className="p-6">
|
||
{children}
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|