XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
import { useState, type FormEvent } from 'react';
import { Command, History, KeyRound, LockKeyhole, Send } from 'lucide-react';
import { useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import type { OperationStatus } from '../types';
import { Badge, Button, EmptyState, Field, Input, PageHeader, Panel, Textarea, Toast, formatDate } from '../components/ui';

export function ConsolePage() {
  const { t, locale } = useI18n();
  const { api, features, session, connection } = useServer();
  const [command, setCommand] = useState('');
  const [reason, setReason] = useState('');
  const [confirmed, setConfirmed] = useState(false);
  const [busy, setBusy] = useState(false);
  const [history, setHistory] = useState<OperationStatus[]>([]);
  const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
  const actor = session?.actor;
  const allowed = connection === 'online' && features?.consoleEnabled === true && actor?.roles.includes('owner') && actor.totpVerified && !actor.reauthenticationRequired;

  const submit = async (event: FormEvent) => {
    event.preventDefault();
    if (!allowed || !command.trim() || !reason.trim()) return;
    if (!confirmed) {
      setConfirmed(true);
      return;
    }
    setBusy(true);
    try {
      const operation = await api.executeConsole(command.trim().replace(/^\//, ''), reason.trim());
      setHistory((current) => [operation, ...current]);
      setToast({ message: `${t('operation.queued')} · ${operation.id}`, tone: 'good' });
      setCommand('');
      setReason('');
      setConfirmed(false);
    } catch (error) {
      setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="page">
      <PageHeader title={t('console.title')} description={t('console.description')} />
      {!allowed ? (
        <Panel><EmptyState icon={<LockKeyhole />} title={t('console.locked')} detail={t('console.warning')} /></Panel>
      ) : (
        <div className="console-layout">
          <Panel title={<><Command size={18} />{t('console.title')}</>}>
            <div className="security-strip"><KeyRound size={17} /><span>{t('console.warning')}</span><Badge tone="good">TOTP</Badge></div>
            <form className="form-stack console-form" onSubmit={submit}>
              <Field label={t('console.command')}><div className="command-input"><span>/</span><Input value={command} onChange={(event) => { setCommand(event.target.value); setConfirmed(false); }} placeholder="save-all flush" required spellCheck={false} autoComplete="off" /></div></Field>
              <Field label={t('common.reason')} hint={t('common.reasonRequired')}><Textarea value={reason} onChange={(event) => { setReason(event.target.value); setConfirmed(false); }} rows={3} required /></Field>
              {confirmed && <div className="confirm-box confirm-box--danger"><Command /><div><strong>/{command.replace(/^\//, '')}</strong><p>{reason}</p></div></div>}
              <Button type="submit" variant={confirmed ? 'danger' : 'primary'} disabled={busy || !command.trim() || !reason.trim()}><Send size={16} />{confirmed ? t('console.execute') : t('common.preview')}</Button>
            </form>
          </Panel>
          <Panel title={<><History size={18} />{t('console.history')}</>}>
            {history.length === 0 ? <EmptyState title={t('common.empty')} /> : <div className="operation-list">{history.map((operation) => <article key={operation.id}><div><strong>{operation.kind}</strong><code>{operation.id}</code></div><div><Badge tone={operation.state === 'FAILED' ? 'danger' : operation.state === 'SUCCEEDED' ? 'good' : 'info'}>{operation.state}</Badge><small>{formatDate(operation.createdAt, locale)}</small></div></article>)}</div>}
          </Panel>
        </div>
      )}
      {toast && <Toast {...toast} onClose={() => setToast(undefined)} />}
    </div>
  );
}