From 7153395267bb7e551967da14f47a390822c60b04 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 9 Apr 2026 11:01:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E9=A6=96=E5=B8=AD=E6=9E=B6?= =?UTF-8?q?=E6=A7=8B=E5=B8=AB=20P0=20=E4=BF=AE=E6=AD=A3=20=E2=80=94=20i18n?= =?UTF-8?q?=20=E7=A1=AC=E7=B7=A8=E7=A2=BC=20+=20=E6=95=88=E8=83=BD?= =?UTF-8?q?=E8=BC=AA=E8=A9=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: 首頁 4 Tab 30+ 處硬編碼中文改為 useTranslations - 新增 dashboard.tabs.* / alertEvents / approve / reject 等 30+ i18n key - zh-TW + en 雙語同步 C3: automation/operations Loading 改用 LobsterLoading (i18n) I1: 100ms setInterval 改為 popstate + 1s 低頻備援 (效能 10x 改善) Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/messages/en.json | 34 ++++++++- apps/web/messages/zh-TW.json | 34 ++++++++- apps/web/src/app/[locale]/automation/page.tsx | 4 +- apps/web/src/app/[locale]/operations/page.tsx | 3 +- apps/web/src/app/[locale]/page.tsx | 69 ++++++++++--------- 5 files changed, 106 insertions(+), 38 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index ea7cf65a..4500b1ec 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -163,7 +163,39 @@ "connectionError": "Connection failed", "metaVersion": "Version", "metaStats": "Stats", - "metaUpdatedAt": "Updated" + "metaUpdatedAt": "Updated", + "tabs": { + "overview": "Overview", + "alerts": "Alerts & Approvals", + "stream": "Activity Stream", + "disposition": "Disposition Stats" + }, + "alertEvents": "Alert Events", + "noActiveAlerts": "No active alerts", + "pendingApprovalsTitle": "Pending Approvals", + "noPendingApprovals": "No pending approvals", + "approve": "Approve", + "reject": "Reject", + "activityStream": "System Activity Stream", + "sseConnected": "SSE Connected", + "sseDisconnected": "Disconnected", + "waitingEvents": "Waiting for events...", + "statusLabel": "Status", + "hostsLabel": "Hosts", + "eventsCount": "{count} events", + "noDispositionData": "No disposition data available", + "totalDispositions": "Total Dispositions", + "autoRate": "Automation Rate", + "humanRate": "Human Intervention Rate", + "autoRepairLabel": "Auto Repair", + "humanApprovedLabel": "Human Approved", + "manualResolvedLabel": "Manual Resolved", + "coldStartLabel": "Cold Start", + "dispositionBreakdown": "Disposition Breakdown", + "hostView": "Hosts", + "topoView": "Topology", + "waitingHostData": "Waiting for host data...", + "dashboardConnecting": "Dashboard API connecting..." }, "openclaw": { "name": "OpenClaw", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 7074368e..bbbb5562 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -164,7 +164,39 @@ "connectionError": "無法連線", "metaVersion": "版本", "metaStats": "統計", - "metaUpdatedAt": "更新" + "metaUpdatedAt": "更新", + "tabs": { + "overview": "戰情總覽", + "alerts": "告警 & 授權", + "stream": "活動串流", + "disposition": "處置統計" + }, + "alertEvents": "告警事件", + "noActiveAlerts": "目前無活躍告警", + "pendingApprovalsTitle": "待批准授權", + "noPendingApprovals": "無待批准項目", + "approve": "批准", + "reject": "拒絕", + "activityStream": "系統活動串流", + "sseConnected": "SSE 連線中", + "sseDisconnected": "連線中斷", + "waitingEvents": "等待即時事件...", + "statusLabel": "狀態", + "hostsLabel": "主機", + "eventsCount": "{count} 筆", + "noDispositionData": "目前無處置統計資料", + "totalDispositions": "處置總次數", + "autoRate": "自動化率", + "humanRate": "人工介入率", + "autoRepairLabel": "自動修復", + "humanApprovedLabel": "人工審核", + "manualResolvedLabel": "手動處理", + "coldStartLabel": "冷啟動", + "dispositionBreakdown": "處置方式分佈", + "hostView": "主機", + "topoView": "拓撲", + "waitingHostData": "等待主機資料...", + "dashboardConnecting": "Dashboard API 連線中" }, "openclaw": { "name": "OpenClaw", diff --git a/apps/web/src/app/[locale]/automation/page.tsx b/apps/web/src/app/[locale]/automation/page.tsx index b8a3e5cc..c9bfe493 100644 --- a/apps/web/src/app/[locale]/automation/page.tsx +++ b/apps/web/src/app/[locale]/automation/page.tsx @@ -11,13 +11,15 @@ import { lazy, Suspense } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' +import { LobsterLoading } from '@/components/shared/lobster-loading' const AutoRepairContent = lazy(() => import('@/app/[locale]/auto-repair/page')) const NeuralCommandContent = lazy(() => import('@/app/[locale]/neural-command/page')) const DriftContent = lazy(() => import('@/app/[locale]/drift/page')) +// C3 修正: 用 LobsterLoading 取代硬編碼「載入中」 function Loading() { - return
載入中...
+ return } export default function AutomationPage({ params }: { params: { locale: string } }) { diff --git a/apps/web/src/app/[locale]/operations/page.tsx b/apps/web/src/app/[locale]/operations/page.tsx index 3f28c4ab..bdf4ca0b 100644 --- a/apps/web/src/app/[locale]/operations/page.tsx +++ b/apps/web/src/app/[locale]/operations/page.tsx @@ -11,6 +11,7 @@ import { lazy, Suspense } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' +import { LobsterLoading } from '@/components/shared/lobster-loading' const DeploymentsContent = lazy(() => import('@/app/[locale]/deployments/page')) const TicketsContent = lazy(() => import('@/app/[locale]/tickets/page')) @@ -19,7 +20,7 @@ const ActionLogsContent = lazy(() => import('@/app/[locale]/action-logs/page')) const BillingContent = lazy(() => import('@/app/[locale]/billing/page')) function Loading() { - return
載入中...
+ return } export default function OperationsPage({ params }: { params: { locale: string } }) { diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 8cf26c0c..6264ef4f 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -57,9 +57,9 @@ function AlertsAndApprovalsTab() {
{/* 左: 告警列表 */}
-
告警事件 ({alerts.length})
+
{t('alertEvents')} ({alerts.length})
{alerts.length === 0 ? ( -
目前無活躍告警
+
{t('noActiveAlerts')}
) : alerts.map((a: any, i: number) => (
@@ -72,16 +72,16 @@ function AlertsAndApprovalsTab() {
{/* 右: 授權待批准 */}
-
待批准授權 ({approvals.length})
+
{t('pendingApprovalsTitle')} ({approvals.length})
{approvals.length === 0 ? ( -
無待批准項目
+
{t('noPendingApprovals')}
) : approvals.map((ap: any, i: number) => (
{ap.action || ap.title || '--'}
{ap.resource || '--'}
- - + +
))} @@ -95,7 +95,7 @@ function AlertsAndApprovalsTab() { // ============================================================================= function ActivityStreamTab() { - const tc = useTranslations('common') + const t = useTranslations('dashboard') const [events, setEvents] = useState([]) const [connected, setConnected] = useState(false) @@ -116,19 +116,19 @@ function ActivityStreamTab() {
- 系統活動串流 - {connected ? 'SSE 連線中' : '連線中斷'} · {events.length} 筆 + {t('activityStream')} + {connected ? t('sseConnected') : t('sseDisconnected')} · {events.length}
{events.length === 0 ? ( -
等待即時事件...
+
{t('waitingEvents')}
) : events.map((ev, i) => (
{ev._time} {ev.type || 'EVENT'} - {ev.data?.overall_status && ` · 狀態: ${ev.data.overall_status}`} - {ev.data?.hosts && ` · ${ev.data.hosts.length} 主機`} + {ev.data?.overall_status && ` · ${t('statusLabel')}: ${ev.data.overall_status}`} + {ev.data?.hosts && ` · ${ev.data.hosts.length} ${t('hostsLabel')}`}
))} @@ -141,7 +141,7 @@ function ActivityStreamTab() { // ============================================================================= function DispositionTab() { - const tc = useTranslations('common') + const t = useTranslations('dashboard') const [data, setData] = useState(null) const [loading, setLoading] = useState(true) @@ -158,8 +158,8 @@ function DispositionTab() { const s = data?.summary if (!s || s.total === 0) return (
-
📊
-
目前無處置統計資料
+
--
+
{t('noDispositionData')}
) @@ -170,25 +170,25 @@ function DispositionTab() { {/* KPI 3 卡 */}
-
處置總次數
+
{t('totalDispositions')}
{s.total}
-
自動化率
+
{t('autoRate')}
{autoRate}%
-
人工介入率
+
{t('humanRate')}
{Math.round(s.human_rate * 100)}%
{/* 四大計數 */}
{[ - { label: '自動修復', count: s.auto_repair, color: '#22C55E', bg: 'rgba(34,197,94,0.06)', border: 'rgba(34,197,94,0.25)' }, - { label: '人工審核', count: s.human_approved, color: '#F59E0B', bg: 'rgba(249,115,22,0.06)', border: 'rgba(249,115,22,0.25)' }, - { label: '手動處理', count: s.manual_resolved, color: '#A855F7', bg: 'rgba(168,85,247,0.06)', border: 'rgba(168,85,247,0.25)' }, - { label: '冷啟動', count: s.cold_start_trust, color: '#4A90D9', bg: 'rgba(59,130,246,0.06)', border: 'rgba(59,130,246,0.25)' }, + { label: t('autoRepairLabel'), count: s.auto_repair, color: '#22C55E', bg: 'rgba(34,197,94,0.06)', border: 'rgba(34,197,94,0.25)' }, + { label: t('humanApprovedLabel'), count: s.human_approved, color: '#F59E0B', bg: 'rgba(249,115,22,0.06)', border: 'rgba(249,115,22,0.25)' }, + { label: t('manualResolvedLabel'), count: s.manual_resolved, color: '#A855F7', bg: 'rgba(168,85,247,0.06)', border: 'rgba(168,85,247,0.25)' }, + { label: t('coldStartLabel'), count: s.cold_start_trust, color: '#4A90D9', bg: 'rgba(59,130,246,0.06)', border: 'rgba(59,130,246,0.25)' }, ].map(item => (
{item.label}
@@ -198,7 +198,7 @@ function DispositionTab() {
{/* 堆疊分佈條 */}
-
處置方式分佈
+
{t('dispositionBreakdown')}
{s.total > 0 && <>
@@ -729,26 +729,27 @@ export default function Home({ params }: { params: { locale: string } }) { // Sprint 5: 4 Tab 配置 (統帥批准 2026-04-08) const alertsCount = incidents?.length ?? 0 const tabs: TabConfig[] = [ - { id: 'overview', label: '戰情總覽', content: null }, // Tab 1 用現有內容,下方直接渲染 - { id: 'alerts', label: '告警 & 授權', badge: alertsCount > 0 ? alertsCount : undefined, content: }, - { id: 'stream', label: '活動串流', content: }, - { id: 'disposition', label: '處置統計', content: }, + { id: 'overview', label: tDashboard('tabs.overview'), content: null }, + { id: 'alerts', label: tDashboard('tabs.alerts'), badge: alertsCount > 0 ? alertsCount : undefined, content: }, + { id: 'stream', label: tDashboard('tabs.stream'), content: }, + { id: 'disposition', label: tDashboard('tabs.disposition'), content: }, ] // Sprint 5: 從 URL 讀取當前 Tab const [activeTabId, setActiveTabId] = useState('overview') const [infraView, setInfraView] = useState<'host' | 'topo'>('host') - // 每 100ms 檢查 URL query 變化(PageTabs 用 router.push 更新) + // I1 修正: popstate 取代 100ms 輪詢 useEffect(() => { - const check = () => { - const params = new URLSearchParams(window.location.search) - const tab = params.get('tab') || 'overview' + const syncTab = () => { + const tab = new URLSearchParams(window.location.search).get('tab') || 'overview' setActiveTabId(prev => prev !== tab ? tab : prev) } - check() - const interval = setInterval(check, 100) - return () => clearInterval(interval) + syncTab() + window.addEventListener('popstate', syncTab) + // PageTabs router.push 後 URL 改變,用 hashchange + 定期低頻同步(1s)作為備援 + const fallback = setInterval(syncTab, 1000) + return () => { window.removeEventListener('popstate', syncTab); clearInterval(fallback) } }, []) return (