+
+ {/* KPI mini-cards */}
+ {stats && (
+
+ {[
+ { label: '总计', value: stats.total, color: theme.text },
+ { label: '高影响', value: stats.high_impact, color: '#d64545' },
+ { label: '中影响', value: stats.medium_impact, color: '#ff8800' },
+ { label: '近90天', value: stats.recent_90d, color: theme.accent },
+ ].map(({ label, value, color }) => (
+
+ ))}
+
+ )}
+
+ {/* Filter row */}
+
+
+ {sources.map(s => (
+
+ ))}
+
+
+
+ {impacts.map(lvl => (
+
+ ))}
+
+
+ {/* Event list */}
+
+ {loading && (
+
加载中...
+ )}
+ {!loading && events.length === 0 && (
+
暂无法规动态
+ )}
+ {events.map(evt => {
+ const cfg = IMPACT_CONFIG[evt.impact_level];
+ const isSelected = evt.id === selectedId;
+ const srcColor = SOURCE_COLORS[evt.source] || theme.accent;
+ return (
+
onSelect(evt.id)}
+ style={{
+ padding: '14px 16px',
+ background: isSelected ? (isDark ? '#1e1e35' : '#fdf0f7') : theme.bgCard,
+ borderRadius: 10,
+ border: `1px solid ${isSelected ? theme.accent : theme.border}`,
+ borderLeft: `4px solid ${cfg.color}`,
+ cursor: 'pointer',
+ transition: 'all 0.15s ease',
+ boxShadow: isSelected ? `0 0 0 1px ${theme.accent}40` : (!isDark ? '0 1px 4px rgba(0,0,0,0.04)' : 'none'),
+ }}
+ >
+ {/* Source + Status row */}
+
+ {evt.source}
+ {evt.standard_code}
+
+ {STATUS_LABEL[evt.status] ?? evt.status}
+
+
+
+ {/* Title */}
+
+ {evt.title}
+
+
+ {/* Date + impact */}
+
+
+ {evt.published_at}{evt.effective_at ? ` → ${evt.effective_at}` : ''}
+
+ {cfg.dot} {cfg.label}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/frontend/src/pages/Perception/PerceptionPage.tsx b/frontend/src/pages/Perception/PerceptionPage.tsx
new file mode 100644
index 0000000..a19d09f
--- /dev/null
+++ b/frontend/src/pages/Perception/PerceptionPage.tsx
@@ -0,0 +1,144 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { useTheme } from '../../contexts';
+import { Content } from '../../components/layout/Content';
+import { TPattern } from '../../components/common/TPattern';
+import {
+ listEvents,
+ getPerceptionStats,
+ analyzeEvent,
+ type RegulationEvent,
+ type PerceptionStats,
+ type AffectedDoc,
+} from '../../api/perception';
+import { EventFeed } from './EventFeed';
+import { AnalysisPanel } from './AnalysisPanel';
+
+export const PerceptionPage: React.FC = () => {
+ const { theme } = useTheme();
+
+ // Feed state
+ const [events, setEvents] = useState