diff --git a/apps/web/src/components/layout/page-tabs.tsx b/apps/web/src/components/layout/page-tabs.tsx
new file mode 100644
index 00000000..43b6bb4c
--- /dev/null
+++ b/apps/web/src/components/layout/page-tabs.tsx
@@ -0,0 +1,172 @@
+'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, Suspense, type ReactNode } from 'react'
+import { useSearchParams, useRouter, usePathname } from 'next/navigation'
+import { cn } from '@/lib/utils'
+
+// =============================================================================
+// 型別
+// =============================================================================
+
+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() {
+ return (
+
+ )
+}
+
+// =============================================================================
+// 元件
+// =============================================================================
+
+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)
+
+ // 切換 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()
+ // @ts-expect-error — Next.js router.push 型別限制,但動態路徑是合法的
+ router.push(`${pathname}${query ? `?${query}` : ''}`, { scroll: false })
+ }
+ }, [syncWithUrl, searchParams, router, pathname, defaultTab, tabs])
+
+ // 找到目前的 Tab 內容
+ const activeContent = useMemo(() => {
+ return tabs.find(t => t.id === activeTab)?.content ?? tabs[0]?.content
+ }, [tabs, activeTab])
+
+ return (
+ <>
+ {/* Tab Bar */}
+
+ {tabs.map(tab => {
+ const isActive = tab.id === activeTab
+ return (
+
+ )
+ })}
+
+
+ {/* Tab 內容 */}
+ }>
+ {activeContent}
+
+ >
+ )
+}
+
+export default PageTabs