Fix run log ingest and terminal console layout

This commit is contained in:
npc0-hue
2026-08-03 12:49:46 +08:00
parent 1d5fb586d0
commit a1bf3aa86a
15 changed files with 390 additions and 34 deletions
@@ -1,4 +1,4 @@
import { Pause, Play, RotateCw, Send, Sparkles, Terminal, Trash2, X } from "lucide-react";
import { ListChecks, Pause, Play, RotateCw, Send, Sparkles, Terminal, Trash2, X } from "lucide-react";
import { type FormEvent, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
@@ -13,6 +13,12 @@ type TerminalLine = { id: string; tone: "input" | "info" | "success" | "error";
const liveLogPollMs = 2000;
const maxLogEntries = 500;
const terminalQuickCommands = [
{ label: "查询玩家", command: "ListPlayers" },
{ label: "服务器状态", command: "ServerInfo" },
{ label: "设为中午", command: "SetTime 12" },
{ label: "保存世界", command: "SaveWorld" }
];
interface LiveOperationDrawerProps {
open: boolean;
@@ -20,9 +26,12 @@ interface LiveOperationDrawerProps {
description?: string;
onClose: () => void;
children: ReactNode;
backdropClassName?: string;
panelClassName?: string;
bodyClassName?: string;
}
function LiveOperationDrawer({ open, title, description, onClose, children }: LiveOperationDrawerProps) {
function LiveOperationDrawer({ open, title, description, onClose, children, backdropClassName, panelClassName, bodyClassName }: LiveOperationDrawerProps) {
useEffect(() => {
if (!open) return undefined;
const previous = document.body.style.overflow;
@@ -39,8 +48,8 @@ function LiveOperationDrawer({ open, title, description, onClose, children }: Li
if (!open) return null;
return (
<div className="drawer-backdrop" role="presentation" onClick={onClose}>
<aside className="drawer-panel live-operation-drawer" role="dialog" aria-modal="true" aria-label={title} onClick={(event) => event.stopPropagation()}>
<div className={cx("drawer-backdrop", backdropClassName)} role="presentation" onClick={onClose}>
<aside className={cx("drawer-panel live-operation-drawer", panelClassName)} role="dialog" aria-modal="true" aria-label={title} onClick={(event) => event.stopPropagation()}>
<div className="panel-header">
<div>
<h2>{title}</h2>
@@ -48,7 +57,7 @@ function LiveOperationDrawer({ open, title, description, onClose, children }: Li
</div>
<button type="button" className="theme-upload drawer-close" aria-label={`关闭${title}`} onClick={onClose}><X size={14} /><span></span></button>
</div>
{children}
<div className={cx("live-operation-content", bodyClassName)}>{children}</div>
</aside>
</div>
);
@@ -200,21 +209,31 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
}
return (
<LiveOperationDrawer open={open} title="管理终端" description={`${serverName} · 平台授权的一次性命令调度`} onClose={onClose}>
<div className="terminal-output" role="log" aria-live="polite">
{lines.map((line) => <div key={line.id} className={`terminal-line terminal-line-${line.tone}`}><time>{line.at}</time><span>{line.text}</span></div>)}
</div>
{result && <ResultBadge status={result.status} label={result.label} />}
{!supportsCommands && <EmptyState icon={<Terminal size={24} />} title="终端不可用" description="当前插件没有声明平台可调度的即时命令能力,因此不会开放浏览器直连 shell。" />}
{supportsCommands && (
<form className="terminal-command-form" onSubmit={(event) => void submitCommand(event)}>
<label>
SCUM
<input value={command} disabled={!canManage || pending} placeholder={canManage ? "输入单行命令,回车提交" : "当前账号没有运行操作权限"} onChange={(event) => setCommand(event.target.value)} />
</label>
<button type="submit" className="primary-command" disabled={!canManage || pending || !command.trim()}>{pending ? <Sparkles size={15} /> : <Send size={15} />}<span>{pending ? "提交中…" : "发送"}</span></button>
</form>
)}
<LiveOperationDrawer open={open} title="管理终端" description={`${serverName} · 平台授权的一次性命令调度`} onClose={onClose} backdropClassName="terminal-drawer-backdrop" panelClassName="management-terminal-drawer" bodyClassName="management-terminal-body">
<section className="terminal-output-panel" aria-label="terminal output">
<div className="terminal-output" role="log" aria-live="polite">
{lines.map((line) => <div key={line.id} className={`terminal-line terminal-line-${line.tone}`}><time>{line.at}</time><span>{line.text}</span></div>)}
</div>
</section>
<section className="terminal-command-dock" aria-label="terminal command controls">
<div className="terminal-command-dock-header">
<span><ListChecks size={14} /></span>
{result && <ResultBadge status={result.status} label={result.label} />}
</div>
<div className="terminal-quick-command-list">
{terminalQuickCommands.map((item) => <button key={item.command} type="button" className="terminal-quick-command" disabled={!supportsCommands || !canManage || pending} onClick={() => setCommand(item.command)}><strong>{item.label}</strong><span>{item.command}</span></button>)}
</div>
{!supportsCommands && <EmptyState icon={<Terminal size={24} />} title="终端不可用" description="当前插件没有声明平台可调度的即时命令能力,因此不会开放浏览器直连 shell。" />}
{supportsCommands && (
<form className="terminal-command-form" onSubmit={(event) => void submitCommand(event)}>
<label>
SCUM
<input value={command} disabled={!canManage || pending} placeholder={canManage ? "输入单行命令,回车提交" : "当前账号没有运行操作权限"} onChange={(event) => setCommand(event.target.value)} />
</label>
<button type="submit" className="primary-command" disabled={!canManage || pending || !command.trim()}>{pending ? <Sparkles size={15} /> : <Send size={15} />}<span>{pending ? "提交中…" : "发送"}</span></button>
</form>
)}
</section>
</LiveOperationDrawer>
);
}