feat(workbench): expand SCADA monitoring and investigation presentation
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user