fix(ui): 架構師 Review 修復 — i18n/keyframe/型別/版面
Critical: - flow-pipeline.tsx: 移除 4 個重複 lobster-bob keyframe,統一在父元件注入 修正 isResolved 路由邏輯,保留嚴重度視覺識別 (P0 resolved 仍用 StyleA) - incident-card.tsx: 修復 4 個硬編碼中文字串 (affectedServices/signalCount/statusLabel/aiProposal) 新增對應 i18n key 到 zh-TW.json + en.json Warning: - page.tsx: MetricItem type 提升至 module scope,pendingApprovals null 安全檢查 Metrics Strip 移除固定 height:68px 改為 auto + padding:8px Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -310,7 +310,11 @@
|
||||
"suggestedAction": "> Suggested action:",
|
||||
"authorize": "Authorize",
|
||||
"reject": "Reject",
|
||||
"anomaly": "anomaly"
|
||||
"anomaly": "anomaly",
|
||||
"affectedServices": "Affected Services",
|
||||
"signalCount": "Signals",
|
||||
"statusLabel": "Status",
|
||||
"aiProposal": "AI Proposal"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -311,7 +311,11 @@
|
||||
"suggestedAction": "> 建議行動:",
|
||||
"authorize": "授權",
|
||||
"reject": "拒絕",
|
||||
"anomaly": "異常"
|
||||
"anomaly": "異常",
|
||||
"affectedServices": "影響服務",
|
||||
"signalCount": "信號數",
|
||||
"statusLabel": "狀態",
|
||||
"aiProposal": "AI 提案"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -20,6 +20,19 @@ import { OpenClawPanel } from '@/components/ai/openclaw-panel'
|
||||
import { HostGrid, type HostInfo } from '@/components/infra/host-grid'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
interface MetricItem {
|
||||
label: string
|
||||
value: string | number
|
||||
sub?: string
|
||||
badge?: { text: string; color: string; bg: string }
|
||||
sparkline?: { values: number[]; color: string }
|
||||
valueColor?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mini Sparkline (SVG inline)
|
||||
// =============================================================================
|
||||
@@ -118,14 +131,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
|
||||
// ── 7 Metrics Strip ─────────────────────────────────────────────────────────
|
||||
|
||||
type MetricItem = {
|
||||
label: string
|
||||
value: string | number
|
||||
sub?: string
|
||||
badge?: { text: string; color: string; bg: string }
|
||||
sparkline?: { values: number[]; color: string }
|
||||
valueColor?: string
|
||||
}
|
||||
const hasPendingApprovals = pendingApprovals !== null && pendingApprovals !== undefined && pendingApprovals > 0
|
||||
|
||||
const metrics: MetricItem[] = [
|
||||
{
|
||||
@@ -144,9 +150,9 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
{
|
||||
label: tDashboard('pendingApprovals'),
|
||||
value: pendingApprovals ?? '--',
|
||||
sub: pendingApprovals > 0 ? undefined : tDashboard('stable'),
|
||||
badge: pendingApprovals > 0 ? { text: tDashboard('pendingApprovals'), color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
valueColor: pendingApprovals > 0 ? '#F59E0B' : undefined,
|
||||
sub: hasPendingApprovals ? undefined : tDashboard('stable'),
|
||||
badge: hasPendingApprovals ? { text: `⏳ ${pendingApprovals}`, color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
valueColor: hasPendingApprovals ? '#F59E0B' : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('autoRemediationRate'),
|
||||
@@ -184,8 +190,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<div style={{
|
||||
background: '#faf9f3',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
height: 68,
|
||||
padding: '0 20px',
|
||||
padding: '8px 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
flexShrink: 0,
|
||||
@@ -208,7 +213,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
{/* Value row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{
|
||||
fontSize: 22,
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: m.valueColor ?? '#141413',
|
||||
lineHeight: 1,
|
||||
|
||||
@@ -95,7 +95,6 @@ function PipelineStyleA({ activeStage, isResolved }: { activeStage: FlowStage; i
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes pulse-wave {
|
||||
0% { box-shadow: 0 0 0 0 rgba(204,34,0,0.6); }
|
||||
70% { box-shadow: 0 0 0 8px rgba(204,34,0,0); }
|
||||
@@ -174,9 +173,6 @@ function PipelineStyleB({ activeStage, isResolved }: { activeStage: FlowStage; i
|
||||
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 12px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
`}</style>
|
||||
{/* 進度條軌道 */}
|
||||
<div style={{ position: 'relative', height: 54 }}>
|
||||
{/* 底部軌道 */}
|
||||
@@ -248,7 +244,6 @@ function PipelineStyleC({ activeStage, isResolved }: { activeStage: FlowStage; i
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes card-glow {
|
||||
0%,100% { box-shadow: 0 0 0 0 rgba(74,144,217,0.3); }
|
||||
50% { box-shadow: 0 0 6px 2px rgba(74,144,217,0.3); }
|
||||
@@ -316,12 +311,6 @@ function PipelineStyleD({ activeStage, isResolved }: { activeStage: FlowStage; i
|
||||
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes dash-march {
|
||||
to { stroke-dashoffset: -20; }
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 0, minWidth: 'max-content' }}>
|
||||
{FLOW_NODES.map((node, idx) => {
|
||||
const status = getNodeStatus(node.id, activeStage, isResolved)
|
||||
@@ -382,22 +371,28 @@ function PipelineStyleD({ activeStage, isResolved }: { activeStage: FlowStage; i
|
||||
)
|
||||
}
|
||||
|
||||
// ── Shared Keyframes (injected once at top level to avoid duplication) ────────
|
||||
|
||||
const SHARED_KEYFRAMES = `
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes dash-march { to { stroke-dashoffset: -20; } }
|
||||
`
|
||||
|
||||
// ── Main Component ────────────────────────────────────────────────────────────
|
||||
|
||||
export function FlowPipeline({ activeStage, isResolved = false, severity = 'P3', tooltips: _tooltips }: FlowPipelineProps) {
|
||||
// 2026-04-02 Claude Code: severity → pipeline style mapping
|
||||
// P0=StyleA(脈衝光波) P1=StyleB(進度條) P2=StyleC(卡片步驟) P3=StyleD(時間軸)
|
||||
if (isResolved || severity === 'P3') {
|
||||
return <PipelineStyleD activeStage={activeStage} isResolved={isResolved} />
|
||||
}
|
||||
if (severity === 'P0') {
|
||||
return <PipelineStyleA activeStage={activeStage} isResolved={false} />
|
||||
}
|
||||
if (severity === 'P1') {
|
||||
return <PipelineStyleB activeStage={activeStage} isResolved={false} />
|
||||
}
|
||||
// P2
|
||||
return <PipelineStyleC activeStage={activeStage} isResolved={false} />
|
||||
// isResolved 傳入各 Style 自行處理顏色,保留嚴重度視覺識別
|
||||
return (
|
||||
<>
|
||||
<style>{SHARED_KEYFRAMES}</style>
|
||||
{severity === 'P0' && <PipelineStyleA activeStage={activeStage} isResolved={isResolved} />}
|
||||
{severity === 'P1' && <PipelineStyleB activeStage={activeStage} isResolved={isResolved} />}
|
||||
{severity === 'P2' && <PipelineStyleC activeStage={activeStage} isResolved={isResolved} />}
|
||||
{(severity === 'P3' || !['P0','P1','P2'].includes(severity)) && <PipelineStyleD activeStage={activeStage} isResolved={isResolved} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowPipeline
|
||||
|
||||
@@ -306,13 +306,13 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
flexWrap: 'wrap' as const,
|
||||
}}>
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>
|
||||
影響服務 <strong style={{ color: '#141413' }}>{incident.affected_services?.length ?? 0}</strong>
|
||||
{t('affectedServices')} <strong style={{ color: '#141413' }}>{incident.affected_services?.length ?? 0}</strong>
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>
|
||||
信號數 <strong style={{ color: '#141413' }}>{incident.signal_count ?? '--'}</strong>
|
||||
{t('signalCount')} <strong style={{ color: '#141413' }}>{incident.signal_count ?? '--'}</strong>
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>
|
||||
狀態 <strong style={{ color: '#141413' }}>{incident.status}</strong>
|
||||
{t('statusLabel')} <strong style={{ color: '#141413' }}>{incident.status}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -340,7 +340,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#d97757' }}>▶</span>
|
||||
<span>AI 提案: {decisionAction.slice(0, 50)}{decisionAction.length > 50 ? '...' : ''}</span>
|
||||
<span>{t('aiProposal')}: {decisionAction.slice(0, 50)}{decisionAction.length > 50 ? '...' : ''}</span>
|
||||
<span style={{ marginLeft: 'auto' }}>{aiExpanded ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{aiExpanded && (
|
||||
|
||||
Reference in New Issue
Block a user