feat(scheduling): add 近期已干预 summary card (last 7 days)

Restore 替换建议 card and add a new emerald 近期已干预 card. Clicking
opens the history modal pre-filtered to the last 7 days (excluding
cancelled) via a toggle chip users can switch off.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-17 09:25:59 +08:00
parent 1b2ad68743
commit ba1e0e9f16
6 changed files with 73 additions and 11 deletions

View File

@@ -8,8 +8,12 @@ import Blur from '../../components/Blur';
interface Props {
onClose: () => void;
onChange?: () => void;
/** When true, pre-filter to the last 7 days (excluding cancelled). */
recentOnly?: boolean;
}
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
type StatusTab = 'all' | NotificationStatus;
const STATUS_TABS: { key: StatusTab; label: string }[] = [
@@ -36,15 +40,23 @@ function fmtDateTime(iso: string): string {
return `${y}-${m}-${day} ${hh}:${mm}`;
}
export default function NotificationHistory({ onClose, onChange }: Props) {
export default function NotificationHistory({ onClose, onChange, recentOnly = false }: Props) {
const [records, setRecords] = useState<NotificationRecord[]>([]);
const [loading, setLoading] = useState(false);
const [tab, setTab] = useState<StatusTab>('all');
const [recent7d, setRecent7d] = useState(recentOnly);
const [mutatingId, setMutatingId] = useState<number | null>(null);
const [executeTarget, setExecuteTarget] = useState<NotificationRecord | null>(null);
const [afterMileageInput, setAfterMileageInput] = useState('');
const [notesInput, setNotesInput] = useState('');
const visibleRecords = recent7d
? records.filter(r => {
const t = Date.parse(r.createdAt);
return Number.isFinite(t) && Date.now() - t <= SEVEN_DAYS_MS && r.status !== 'cancelled';
})
: records;
const load = useCallback(async () => {
setLoading(true);
try {
@@ -117,7 +129,7 @@ export default function NotificationHistory({ onClose, onChange }: Props) {
</div>
{/* Status tabs */}
<div className="border-b border-slate-100 px-4 py-2 flex gap-1.5 flex-shrink-0">
<div className="border-b border-slate-100 px-4 py-2 flex gap-1.5 flex-shrink-0 flex-wrap items-center">
{STATUS_TABS.map(t => (
<button
key={t.key}
@@ -129,6 +141,17 @@ export default function NotificationHistory({ onClose, onChange }: Props) {
{t.label}
</button>
))}
<div className="ml-auto">
<button
onClick={() => setRecent7d(v => !v)}
className={`text-[11px] px-3 py-1 rounded-full font-medium cursor-pointer transition-colors ${
recent7d ? 'bg-emerald-600 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
title="仅看最近 7 天(不含已取消)"
>
7
</button>
</div>
</div>
{/* Body */}
@@ -137,14 +160,14 @@ export default function NotificationHistory({ onClose, onChange }: Props) {
<div className="py-16 text-center text-slate-400 text-xs flex items-center justify-center gap-2">
<Loader2 size={14} className="animate-spin" />
</div>
) : records.length === 0 ? (
) : visibleRecords.length === 0 ? (
<div className="py-16 text-center text-slate-400">
<Clock className="w-8 h-8 text-slate-200 mx-auto mb-2" />
<p className="text-sm"></p>
<p className="text-sm">{recent7d ? '最近 7 天暂无干预记录' : '暂无记录'}</p>
</div>
) : (
<div className="divide-y divide-slate-50">
{records.map(rec => {
{visibleRecords.map(rec => {
const badge = statusBadge(rec.status);
const busy = mutatingId === rec.id;
return (

View File

@@ -171,6 +171,7 @@ export default function SchedulingModule() {
const [batchInFlight, setBatchInFlight] = useState(false);
const [batchResultMsg, setBatchResultMsg] = useState<string | null>(null);
const [showHistory, setShowHistory] = useState(false);
const [historyRecentOnly, setHistoryRecentOnly] = useState(false);
const loadData = useCallback(async () => {
setLoading(true);
@@ -277,7 +278,7 @@ export default function SchedulingModule() {
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 md:pb-0">
{/* ===== Summary Cards ===== */}
<div className="grid grid-cols-3 gap-2.5">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2.5">
{/* 里程高·换下 — warm orange */}
<button
onClick={() => setTypeFilter(typeFilter === 'qualified' ? 'all' : 'qualified')}
@@ -330,7 +331,7 @@ export default function SchedulingModule() {
}`}
>
<div className={`text-[10px] font-bold mb-1 ${typeFilter === 'all' ? 'text-slate-300' : 'text-slate-500'}`}>
</div>
<div className={`text-2xl font-black ${typeFilter === 'all' ? 'text-white' : 'text-slate-800'}`}>
{loading && !data ? '-' : summary?.suggestionCount ?? 0}
@@ -340,6 +341,23 @@ export default function SchedulingModule() {
+{summary?.estimatedGain ?? 0}
</div>
</button>
{/* 近期已干预 — emerald */}
<button
onClick={() => { setShowHistory(true); setHistoryRecentOnly(true); }}
className="p-3.5 rounded-2xl text-left transition-all cursor-pointer bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-200/60"
>
<div className="text-[10px] font-bold mb-1 text-emerald-600">
</div>
<div className="text-2xl font-black text-emerald-700">
{loading && !data ? '-' : summary?.recentInterventionCount ?? 0}
<span className="text-[10px] font-normal ml-1 text-emerald-400"></span>
</div>
<div className="text-[9px] mt-0.5 text-emerald-400">
7 ·
</div>
</button>
</div>
{/* ===== List Card ===== */}
@@ -363,7 +381,7 @@ export default function SchedulingModule() {
<Download size={15} />
</button>
<button
onClick={() => setShowHistory(true)}
onClick={() => { setShowHistory(true); setHistoryRecentOnly(false); }}
className="p-1.5 text-slate-400 hover:text-slate-600 transition-colors rounded-lg hover:bg-slate-50 cursor-pointer"
title="调度记录"
>
@@ -510,7 +528,11 @@ export default function SchedulingModule() {
)}
{showHistory && (
<NotificationHistory onClose={() => setShowHistory(false)} onChange={loadData} />
<NotificationHistory
onClose={() => setShowHistory(false)}
onChange={loadData}
recentOnly={historyRecentOnly}
/>
)}
{/* Batch action bar */}