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
@@ -1,4 +1,4 @@
import { ChevronLeft, ChevronRight, Radio } from "lucide-react";
import { ChevronLeft, ChevronRight, Pause, Play, Radio } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/ui/select";
import type { PresentationHistoryControls } from "../utils/presentation-view-history";
@@ -9,88 +9,116 @@ export function PresentationHistorySwitcher({
entries,
currentEntryId,
selectedId,
onSelect
onSelect,
playback
}: PresentationHistoryControls) {
const currentEntry = entries.find((entry) => entry.id === currentEntryId);
const available = entries.filter(
(entry) => entry.status !== "running" && entry.id !== currentEntryId
(entry) => entry.status !== "running" && (entry.id !== currentEntryId || entry.id === selectedId)
);
const index =
selectedId === null
? available.length
: available.findIndex((entry) => entry.id === selectedId);
const playable = entries.filter((entry) => entry.status === "completed");
const progress = Math.max(0, playable.findIndex((entry) => entry.id === selectedId));
return (
<div className="mt-2 flex min-w-0 items-center gap-1" role="group" aria-label="展示动作历史">
<Button
type="button"
variant="ghost"
size="icon-sm"
className="size-8 shrink-0"
aria-label="上一个动作"
disabled={index <= 0}
onClick={() => onSelect(available[index - 1].id)}
>
<ChevronLeft size={14} aria-hidden />
</Button>
<Select
value={selectedId === null ? "current" : String(selectedId)}
onValueChange={(value) => onSelect(value === "current" ? null : Number(value))}
>
<SelectTrigger
aria-label="切换展示动作"
className="h-8 min-w-0 flex-1 px-2 text-xs [&>span]:truncate"
>
<SelectValue />
</SelectTrigger>
<SelectContent
position="popper"
sideOffset={4}
className="workbench-shadcn-surface z-[90] max-w-[calc(100vw-24px)]"
>
<SelectItem value="current" className="text-xs">
当前(最新)
{currentEntry ? ` · ${entries.indexOf(currentEntry) + 1}. ${currentEntry.label}` : ""} ·{" "}
{entries.length} 个动作
</SelectItem>
{entries
.slice()
.reverse()
.map((entry, position) =>
entry.id === currentEntryId ? null : (
<SelectItem
key={entry.id}
value={String(entry.id)}
disabled={entry.status === "running"}
className="whitespace-normal text-xs [&>span:last-child]:break-words"
>
{entries.length - position}. {entry.label} · {statusLabels[entry.status]}
</SelectItem>
)
)}
</SelectContent>
</Select>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="size-8 shrink-0"
aria-label="下一个动作"
disabled={selectedId === null}
onClick={() => onSelect(index + 1 < available.length ? available[index + 1].id : null)}
>
<ChevronRight size={14} aria-hidden />
</Button>
{selectedId !== null ? (
<div className="mt-2 min-w-0 space-y-1.5" role="group" aria-label="展示动作历史">
<div className="flex min-w-0 items-center gap-1">
<Button
type="button"
variant="secondary"
size="sm"
className="h-8 shrink-0 gap-1 px-2 text-[11px]"
onClick={() => onSelect(null)}
variant="ghost"
size="icon-sm"
className="size-8 shrink-0"
aria-label="上一个动作"
disabled={index <= 0}
onClick={() => onSelect(available[index - 1].id)}
>
<Radio size={12} aria-hidden />
回到当前
<ChevronLeft size={14} aria-hidden />
</Button>
<Select
value={selectedId === null ? "current" : String(selectedId)}
onValueChange={(value) => onSelect(value === "current" ? null : Number(value))}
>
<SelectTrigger
aria-label="切换展示动作"
className="h-8 min-w-0 flex-1 px-2 text-xs [&>span]:truncate"
>
<SelectValue />
</SelectTrigger>
<SelectContent
position="popper"
sideOffset={4}
className="workbench-shadcn-surface z-[90] max-w-[calc(100vw-24px)]"
>
<SelectItem value="current" className="text-xs">
当前(最新)
{currentEntry ? ` · ${entries.indexOf(currentEntry) + 1}. ${currentEntry.label}` : ""} ·{" "}
{entries.length} 个动作
</SelectItem>
{entries
.slice()
.reverse()
.map((entry, position) =>
entry.id === currentEntryId && entry.id !== selectedId ? null : (
<SelectItem
key={entry.id}
value={String(entry.id)}
disabled={entry.status === "running"}
className="whitespace-normal text-xs [&>span:last-child]:break-words"
>
{entries.length - position}. {entry.label} · {statusLabels[entry.status]}
</SelectItem>
)
)}
</SelectContent>
</Select>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="size-8 shrink-0"
aria-label="下一个动作"
disabled={selectedId === null}
onClick={() => onSelect(index + 1 < available.length ? available[index + 1].id : null)}
>
<ChevronRight size={14} aria-hidden />
</Button>
{selectedId !== null ? (
<Button
type="button"
variant="secondary"
size="sm"
className="h-8 shrink-0 gap-1 px-2 text-[11px]"
onClick={() => onSelect(null)}
>
<Radio size={12} aria-hidden />
回到当前
</Button>
) : null}
</div>
{playback ? (
<div className="flex min-w-0 items-center gap-2" role="group" aria-label="历史播放">
<Button type="button" variant="outline" size="icon-sm" className="size-8 shrink-0"
aria-label={playback.playing ? "暂停历史播放" : "播放历史"}
disabled={!playable.length} onClick={playback.onToggle}>
{playback.playing ? <Pause size={14} aria-hidden /> : <Play size={14} aria-hidden />}
</Button>
<input type="range" aria-label="历史播放进度" className="h-8 w-0 min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-default"
min={0} max={Math.max(0, playable.length - 1)} step={1} value={selectedId === null ? Math.max(0, playable.length - 1) : progress}
disabled={playable.length < 2}
aria-valuetext={selectedId === null ? "当前最新结果" : `第 ${progress + 1} 个,共 ${playable.length} 个结果`}
onChange={(event) => { const entry = playable[Number(event.target.value)]; if (entry) playback.onSeek(entry.id); }} />
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground" aria-label="历史播放位置">
{selectedId === null ? playable.length : playable.length ? progress + 1 : 0}/{playable.length}
</span>
<Select value={String(playback.speed)} onValueChange={(value) => playback.onSpeedChange(Number(value))}>
<SelectTrigger aria-label="历史播放速度" className="h-8 w-[68px] shrink-0 gap-1 px-2 text-xs"><SelectValue /></SelectTrigger>
<SelectContent position="popper" className="workbench-shadcn-surface z-[90]">
{[0.5, 1, 2].map((speed) => <SelectItem key={speed} value={String(speed)} className="text-xs">{speed}×</SelectItem>)}
</SelectContent>
</Select>
</div>
) : null}
</div>
);