feat(web): S4+S5 處置統計環形圖 + 最近活動時間線 — Sprint 5R
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

- S4: DispositionMini 元件 (SVG 環形圖 + 四類列表)
- S5: RecentActivity 元件 (時間線 + 色點 + JetBrains Mono)
- 左欄改為 flex:6 可滾動多卡片列
- 右欄改為 flex:4 (60:40 比例)
- 左欄結構: 活躍事件 → 處置統計 → 最近活動

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 15:51:54 +08:00
parent 7a2e07f74f
commit b85a0e232e
3 changed files with 252 additions and 50 deletions

View File

@@ -28,6 +28,8 @@ import { PageTabs, type TabConfig } from '@/components/layout/page-tabs'
import { LobsterLoading } from '@/components/shared/lobster-loading'
import { ServiceTopology } from '@/components/topology'
import { BarChart3, Flame, Telescope, FlaskConical, Activity, GitBranch } from 'lucide-react'
import { DispositionMini } from '@/components/shared/disposition-mini'
import { RecentActivity } from '@/components/shared/recent-activity'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -766,70 +768,78 @@ export default function Home({ params }: { params: { locale: string } }) {
{/* ── 主體 2 欄 ─────────────────────────────────────────────────────── */}
<div style={{ display: 'flex', flex: 1, overflow: 'hidden', padding: '14px 20px', gap: 20, background: '#f5f4ed' }}>
{/* ── Feed活躍事件 (flex:1) ───────────────────────────────────── */}
{/* ── 左欄 (60%): 活躍事件 + 處置統計 + 最近活動 ─────────────── */}
<div style={{
flex: 1,
background: '#fff',
border: '0.5px solid #e0ddd4',
borderRadius: 12,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
flex: 6, minWidth: 0, overflowY: 'auto',
display: 'flex', flexDirection: 'column', gap: 14,
paddingBottom: 40,
}}>
{/* 活躍事件 */}
<div style={{
padding: '10px 14px',
borderBottom: '0.5px solid #e0ddd4',
display: 'flex', alignItems: 'center', gap: 8,
flexShrink: 0, background: '#faf9f3',
background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10,
overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.04)', flexShrink: 0,
}}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>
{tDashboard('activeIncidents')}
</span>
{(incidents?.length ?? 0) > 0 && (
<span style={{
fontSize: 12, background: 'rgba(217,119,87,0.1)', color: '#a04010',
padding: '2px 8px', fontWeight: 700,
border: '0.5px solid rgba(217,119,87,0.25)', borderRadius: 10,
}}>
{incidents?.length}
<div style={{
padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4',
display: 'flex', alignItems: 'center', gap: 8, background: '#faf9f3',
}}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>
{tDashboard('activeIncidents')}
</span>
)}
{(incidents?.length ?? 0) > 0 && (
<span style={{
fontSize: 12, background: 'rgba(217,119,87,0.1)', color: '#a04010',
padding: '2px 8px', fontWeight: 700,
border: '0.5px solid rgba(217,119,87,0.25)', borderRadius: 10,
}}>
{incidents?.length}
</span>
)}
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}> </span>
</div>
<div style={{ padding: '14px' }}>
{isIncidentsLoading ? (
<LobsterLoading size="sm" />
) : incidentsError ? (
<div style={{ padding: 16, fontSize: 13, color: '#cc2200' }}>{incidentsError}</div>
) : (incidents?.length ?? 0) === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22C55E' }} />
<span style={{ fontSize: 13, color: '#87867f' }}>{tDashboard('stable')} · 0 {tDashboard('activeIncidents')}</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{incidents?.map((incident) => (
<IncidentCard
key={incident.incident_id}
incident={incident}
decision={incident.decision}
/>
))}
</div>
)}
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '14px' }}>
{isIncidentsLoading ? (
<LobsterLoading size="sm" />
) : incidentsError ? (
<div style={{ padding: 16, fontSize: 13, color: '#cc2200' }}>{incidentsError}</div>
) : (incidents?.length ?? 0) === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22C55E' }} />
<span style={{ fontSize: 13, color: '#87867f' }}>{tDashboard('stable')} · 0 {tDashboard('activeIncidents')}</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{incidents?.map((incident) => (
<IncidentCard
key={incident.incident_id}
incident={incident}
decision={incident.decision}
/>
))}
</div>
)}
</div>
{/* 處置統計迷你版 (S4) */}
<DispositionMini />
{/* 最近活動 (S5) */}
<RecentActivity />
</div>
{/* ── Right PanelAI + Infra (width:530px) ────────────────────── */}
{/* ── 右欄 (40%): OpenClaw + 基礎架構 + 監控工具 ─────────────── */}
<div style={{
width: 530,
flexShrink: 0,
flex: 4, minWidth: 0,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 14,
paddingBottom: 40,
}}>
{/* WoooClaw + Reasoning Stream */}