diff --git a/src/modules/scheduling/NotificationHistory.tsx b/src/modules/scheduling/NotificationHistory.tsx
index 69d8b2d..e1bd56a 100644
--- a/src/modules/scheduling/NotificationHistory.tsx
+++ b/src/modules/scheduling/NotificationHistory.tsx
@@ -81,7 +81,7 @@ export default function NotificationHistory({ onClose, onChange }: Props) {
};
const handleCancel = async (rec: NotificationRecord) => {
- if (!confirm(`确定取消 ${rec.currentPlate} → ${rec.candidatePlate} 的替换通知?`)) return;
+ if (!confirm(`确定取消 ${rec.currentPlate} → ${rec.candidatePlate} 的干预?`)) return;
setMutatingId(rec.id);
try {
await updateNotification(rec.id, { status: 'cancelled' });
diff --git a/src/modules/scheduling/SchedulingModule.tsx b/src/modules/scheduling/SchedulingModule.tsx
index edc19df..7ee6b87 100644
--- a/src/modules/scheduling/SchedulingModule.tsx
+++ b/src/modules/scheduling/SchedulingModule.tsx
@@ -177,11 +177,13 @@ export default function SchedulingModule() {
useEffect(() => { loadData(); }, [loadData]);
const handleNotifySuccess = useCallback(() => { loadData(); }, [loadData]);
- // Close detail modal if selected suggestion is filtered out or no longer exists
+ // Keep selectedSuggestion synced with latest data so candidate notification
+ // status changes (登记 / 取消干预) propagate into the open detail modal.
useEffect(() => {
if (!selectedSuggestion || !data) return;
- const stillExists = data.suggestions.some(s => s.id === selectedSuggestion.id);
- if (!stillExists) setSelectedSuggestion(null);
+ const fresh = data.suggestions.find(s => s.id === selectedSuggestion.id);
+ if (!fresh) setSelectedSuggestion(null);
+ else if (fresh !== selectedSuggestion) setSelectedSuggestion(fresh);
}, [data, selectedSuggestion]);
const toggleSelect = useCallback((id: string) => {
@@ -227,7 +229,7 @@ export default function SchedulingModule() {
exitSelectMode();
} catch (e) {
console.error('batch notify failed:', e);
- setBatchResultMsg('批量通知失败,请重试');
+ setBatchResultMsg('批量干预失败,请重试');
} finally {
setBatchInFlight(false);
}
@@ -534,7 +536,7 @@ export default function SchedulingModule() {
disabled={selectedIds.size === 0}
className="flex items-center gap-1.5 text-xs font-bold bg-blue-600 hover:bg-blue-500 disabled:bg-slate-700 disabled:text-slate-400 text-white px-3 py-1.5 rounded-lg cursor-pointer disabled:cursor-not-allowed transition-colors"
>
- 批量通知
+ 批量干预
@@ -551,7 +553,7 @@ export default function SchedulingModule() {
className="bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden flex flex-col max-h-[80vh] sm:mx-4"
>
- 确认批量通知
+ 确认批量干预
- 将发送 {batchItems.length} 条替换通知,已排除无可用候选车的建议。
+ 将登记 {batchItems.length} 条干预,已排除无可用候选车的建议。
{batchItems.map(({ suggestion, candidate }) => (
@@ -597,7 +599,7 @@ export default function SchedulingModule() {
disabled={batchInFlight || batchItems.length === 0}
className="flex-1 py-2 text-xs font-bold text-white bg-blue-600 hover:bg-blue-500 rounded-lg cursor-pointer disabled:opacity-50 transition-colors"
>
- {batchInFlight ? '发送中...' : `确认发送 ${batchItems.length} 条`}
+ {batchInFlight ? '登记中...' : `确认登记 ${batchItems.length} 条`}
diff --git a/src/modules/scheduling/SuggestionDetail.tsx b/src/modules/scheduling/SuggestionDetail.tsx
index 71e922f..2d23d57 100644
--- a/src/modules/scheduling/SuggestionDetail.tsx
+++ b/src/modules/scheduling/SuggestionDetail.tsx
@@ -113,14 +113,13 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
diff --git a/src/modules/scheduling/SuggestionList.tsx b/src/modules/scheduling/SuggestionList.tsx
index 9fa62cd..9d467a0 100644
--- a/src/modules/scheduling/SuggestionList.tsx
+++ b/src/modules/scheduling/SuggestionList.tsx
@@ -133,7 +133,7 @@ export default function SuggestionList({ suggestions, onSelect, selectMode = fal
{v.region}
{notified && (
- 已通知
+ 已干预
)}
diff --git a/src/modules/scheduling/SwapPreview.tsx b/src/modules/scheduling/SwapPreview.tsx
index 28874ea..470d4cb 100644
--- a/src/modules/scheduling/SwapPreview.tsx
+++ b/src/modules/scheduling/SwapPreview.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
-import { ArrowDownUp, CheckCircle, Send, X } from 'lucide-react';
-import { sendNotify } from './api';
+import { ArrowDownUp, CheckCircle, Send, X, Ban } from 'lucide-react';
+import { sendNotify, updateNotification } from './api';
import type { SchedulingSuggestion, CandidateVehicle } from './types';
import Blur from '../../components/Blur';
@@ -23,10 +23,15 @@ function fmtRate(rate: number): string {
export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSuccess }: Props) {
const [sending, setSending] = useState(false);
const [sent, setSent] = useState(false);
+ const [cancelling, setCancelling] = useState(false);
const v = s.currentVehicle;
+ const alreadyIntervened =
+ !sent && (c.notificationStatus === 'sent' || c.notificationStatus === 'executed');
+ const isExecuted = c.notificationStatus === 'executed';
+
const handleSend = async () => {
- if (sending || sent) return;
+ if (sending || sent || alreadyIntervened) return;
setSending(true);
try {
const result = await sendNotify({ suggestionId: s.id, currentPlate: v.plateNumber, candidatePlate: c.plateNumber });
@@ -34,6 +39,21 @@ export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSu
} catch { alert('网络错误'); } finally { setSending(false); }
};
+ const handleCancel = async () => {
+ if (!c.notificationId || cancelling) return;
+ if (isExecuted) {
+ if (!confirm('此干预已标记为执行。确定要取消吗?')) return;
+ } else {
+ if (!confirm(`确定取消 ${v.plateNumber} → ${c.plateNumber} 的干预?`)) return;
+ }
+ setCancelling(true);
+ try {
+ const result = await updateNotification(c.notificationId, { status: 'cancelled' });
+ if (result.success) { onSuccess(); onClose(); }
+ else { alert(result.message || '取消失败'); }
+ } catch { alert('网络错误'); } finally { setCancelling(false); }
+ };
+
return (
{/* Header */}
@@ -115,16 +135,32 @@ export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSu
{/* Bottom */}
-
-
+
+ {alreadyIntervened && (
+
+
+ 此车已{isExecuted ? '执行干预' : '登记干预'},如需重新干预请先取消。
+
+ )}
+ {alreadyIntervened ? (
+
+ ) : (
+
+ )}
diff --git a/src/modules/scheduling/csv-export.ts b/src/modules/scheduling/csv-export.ts
index 223d3c4..d6454e0 100644
--- a/src/modules/scheduling/csv-export.ts
+++ b/src/modules/scheduling/csv-export.ts
@@ -41,7 +41,7 @@ const HEADERS = [
'候选替换后预估(km)',
'候选可达标',
'候选区域',
- '通知状态',
+ '干预状态',
] as const;
export function buildSuggestionsCsv(suggestions: SchedulingSuggestion[]): string {
diff --git a/src/server/routes/scheduling/db-schema.ts b/src/server/routes/scheduling/db-schema.ts
index e92c19f..0e046e8 100644
--- a/src/server/routes/scheduling/db-schema.ts
+++ b/src/server/routes/scheduling/db-schema.ts
@@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS tab_scheduling_notifications (
INDEX idx_candidate_plate (candidate_plate),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能调度通知/执行记录'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能调度干预/执行记录'
`;
export async function ensureSchedulingTables(): Promise
{
diff --git a/src/server/routes/scheduling/notify.ts b/src/server/routes/scheduling/notify.ts
index 6b74641..50e6e62 100644
--- a/src/server/routes/scheduling/notify.ts
+++ b/src/server/routes/scheduling/notify.ts
@@ -119,12 +119,12 @@ app.post('/', async (c) => {
return c.json({
success: true,
- message: `替换通知已发送:${currentPlate} → ${candidatePlate}`,
+ message: `干预已登记:${currentPlate} → ${candidatePlate}`,
record: result,
});
} catch (e: unknown) {
console.error('scheduling notify error:', e);
- return c.json({ success: false, message: '发送通知失败' }, 500);
+ return c.json({ success: false, message: '登记干预失败' }, 500);
}
});
@@ -166,12 +166,12 @@ app.post('/batch', async (c) => {
return c.json({
success: true,
- message: `批量通知:成功 ${result.success},跳过 ${result.skipped},失败 ${result.failed}`,
+ message: `批量干预:成功 ${result.success},跳过 ${result.skipped},失败 ${result.failed}`,
result,
});
} catch (e: unknown) {
console.error('scheduling batch notify error:', e);
- return c.json({ success: false, message: '批量通知失败' }, 500);
+ return c.json({ success: false, message: '批量干预失败' }, 500);
}
});