feat(workbench): expand SCADA monitoring and investigation presentation

This commit is contained in:
2026-11-07 19:50:14 +08:00
parent 0801e161ec
commit ba71e2d529
133 changed files with 6754 additions and 1438 deletions
@@ -10,12 +10,10 @@ import {
} from "../utils/investigation-dev-cases";
import type { OperationalTask } from "../api/operational-task-client";
import {
investigationActionLabels,
investigationReportJsonl,
investigationReportUiSummary,
parseInvestigationDevJsonl,
type InvestigationDevPreview,
type InvestigationPresentationActivity
type InvestigationDevPreview
} from "../utils/investigation-dev-preview";
export type InvestigationDevToolsProps = {
@@ -23,7 +21,6 @@ export type InvestigationDevToolsProps = {
mapReady: boolean;
charts: number;
overlay: string | null;
activity: InvestigationPresentationActivity[];
onSample: (caseId?: InvestigationDevCaseId) => string;
onPreview: (preview: InvestigationDevPreview, label?: string) => Promise<void>;
onClear: () => void;
@@ -34,7 +31,6 @@ export function InvestigationDevTools({
mapReady,
charts,
overlay,
activity,
onSample,
onPreview,
onClear
@@ -43,11 +39,9 @@ export function InvestigationDevTools({
const [input, setInput] = useState("");
const [caseId, setCaseId] = useState<InvestigationDevCaseId | undefined>();
const [checked, setChecked] = useState<InvestigationDevPreview | null>(null);
const [preview, setPreview] = useState<InvestigationDevPreview | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(false);
const [selectedRecord, setSelectedRecord] = useState(0);
const previewActive = useRef(false);
const inputId = useId();
const summary = useMemo(() => investigationReportUiSummary(task), [task]);
@@ -56,10 +50,8 @@ export function InvestigationDevTools({
setInput(value);
setCaseId(nextCaseId);
setChecked(null);
setPreview(null);
setMessage(null);
setError(null);
setSelectedRecord(0);
}
function clearLoadedPreview() {
if (!previewActive.current) return;
@@ -100,7 +92,6 @@ export function InvestigationDevTools({
}
setRunning(true);
setError(null);
setPreview(parsed);
previewActive.current = true;
try {
await onPreview(parsed, selectedCase?.label ?? "JSONL");
@@ -174,7 +165,7 @@ export function InvestigationDevTools({
<div id="investigation-dev-lab" className="min-w-0 space-y-3">
<h3 className="text-xs font-semibold">01 准备数据</h3>
<p className="text-[11px] leading-5 text-muted-foreground">
仅在当前浏览器预览,不写入任务或触发调查。合成样例不代表现场事实。
仅在当前浏览器预览,不写入任务或触发调查。空间位置与要素 ID 以 tjwater_v2 为基准;读数与方案为合成样例,不代表现场事实。
</p>
<FieldSelect
label="JSONL 测试案例"
@@ -197,7 +188,7 @@ export function InvestigationDevTools({
</p>
) : null}
<p className="text-[11px] leading-5 text-muted-foreground">
切换即载入,执行时自动校验;已执行的本地预览会清除。
切换即载入,执行时自动校验;已执行的本地预览会清除。定位和高亮请使用“地图调试”页签的业务源与要素 ID。
</p>
<div className="grid grid-cols-2 gap-2">
<Button
@@ -296,7 +287,6 @@ export function InvestigationDevTools({
onClick={() => {
onClear();
previewActive.current = false;
setPreview(null);
setMessage("已清除当前浏览器的临时展示。");
setError(null);
}}
@@ -319,92 +309,8 @@ export function InvestigationDevTools({
{error}
</p>
) : null}
{preview ? (
<section aria-label="JSONL 检查结果" className="min-w-0 space-y-3 overflow-x-auto">
<p className="text-xs text-muted-foreground">
业务结果已在展示面板打开,可切换动作历史回看。
</p>
<Disclosure label="原始数据检查">
{preview.records.length ? (
<div className="max-h-48 overflow-auto">
<table className="w-full text-left text-[11px] leading-5">
<caption className="sr-only">已识别的调查数据,仅供预览</caption>
<thead>
<tr className="border-b">
<th className="py-1 pr-3 font-medium">类型</th>
<th className="py-1 pr-3 font-medium">对象</th>
<th className="py-1 pr-3 font-medium">来源</th>
<th className="py-1 text-right font-medium">证据</th>
</tr>
</thead>
<tbody>
{preview.records.map((record, index) => (
<tr key={index} className="border-b last:border-0">
<td className="break-all py-1.5 pr-3">{String(record.record_type)}</td>
<td className="py-1.5 pr-3">
<button
type="button"
className="break-all text-left underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-[var(--action-focus-ring)]"
aria-label={`查看第 ${index + 1} 条调查数据`}
aria-pressed={selectedRecord === index}
onClick={() => setSelectedRecord(index)}
>
{String(
record.element_id ??
record.device_id ??
record.action_id ??
record.item ??
`记录 ${index + 1}`
)}
</button>
</td>
<td className="py-1.5 pr-3">{String(record.origin)}</td>
<td className="py-1.5 text-right tabular-nums">
{(record.source_evidence_refs as string[]).length}
</td>
</tr>
))}
</tbody>
</table>
<pre
aria-label="调查数据详情"
className="mt-2 max-h-36 overflow-auto whitespace-pre-wrap break-all rounded-md bg-muted p-2 font-mono text-[11px] leading-5"
>
{JSON.stringify(preview.records[selectedRecord], null, 2)}
</pre>
</div>
) : null}
</Disclosure>
</section>
) : null}
</div>
) : null}
{activity.length ? (
<section aria-label="最近展示动作" className="space-y-2 border-t border-border pt-4">
<h3 className="text-xs font-semibold">最近展示动作</h3>
<ol
aria-label="动作识别记录"
className="space-y-1 border-l border-border pl-3 text-[11px] leading-5"
>
{activity.slice(-3).map((item) => (
<li key={`${item.source}-${item.id}`} className="break-words">
<span className="text-muted-foreground">
{item.source === "preview" ? "本地测试" : "实时指令"} ·{" "}
</span>
{investigationActionLabels[item.action]} ·{" "}
{item.status === "completed"
? "已应用"
: item.status === "error"
? "未完成"
: "展示中"}
{item.message ? (
<span className="text-[var(--status-warning-foreground)]">:{item.message}</span>
) : null}
</li>
))}
</ol>
</section>
) : null}
</div>
);
}