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

XFEServerManager

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

公开
关注 0 Fork 0 Star 0
UTF-8
import { useEffect, useMemo, useRef, useState } from 'react';
import {
  AlertTriangle,
  Check,
  LoaderCircle,
  Plus,
  RefreshCcw,
  Search,
  Server,
  Terminal,
  Workflow,
  X,
} from 'lucide-react';
import type { PolicyCommandCatalogEntry, PolicyCommandSource } from '../types';
import { ModalPortal } from './modal-portal';
import { Button, Input, Select } from './ui';

interface CommandCatalogPickerProps {
  commands: PolicyCommandCatalogEntry[];
  selectedCommandIds: string[];
  loading: boolean;
  error?: string;
  onAdd: (commandId: string) => void;
  onReload: () => void;
}

type SourceFilter = 'all' | PolicyCommandSource;

function commandRoot(root: string): string {
  const normalized = root.trim().replace(/^\/+/, '');
  return normalized ? `/${normalized}` : '/';
}

function commandReferenceForms(reference: string): string[] {
  const token = reference.trim().split(/\s+/, 1)[0]?.replace(/^\/+/, '').toLocaleLowerCase() ?? '';
  if (!token) return [];
  return token.includes(':') ? [token] : [token, `minecraft:${token}`];
}

/** Treat hand-written `/root`, `root`, aliases and canonical IDs as the same command. */
export function commandIsSelected(command: PolicyCommandCatalogEntry, selectedCommandIds: string[]): boolean {
  const catalogForms = new Set([
    ...commandReferenceForms(command.id),
    ...commandReferenceForms(command.root),
    ...(command.aliases ?? []).flatMap(commandReferenceForms),
  ]);
  return selectedCommandIds.some((selected) => commandReferenceForms(selected).some((form) => catalogForms.has(form)));
}

function searchableText(command: PolicyCommandCatalogEntry): string {
  return [
    command.id,
    command.root,
    ...command.usages,
    ...(command.aliases ?? []),
    ...(command.arguments ?? []),
    command.triggerId ?? '',
    command.triggerName ?? '',
    command.source === 'trigger' ? '触发器 trigger 自定义指令' : '服务器 server 原版 模组',
  ].join(' ').toLocaleLowerCase();
}

export function CommandCatalogPicker({
  commands,
  selectedCommandIds,
  loading,
  error,
  onAdd,
  onReload,
}: CommandCatalogPickerProps) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState('');
  const [source, setSource] = useState<SourceFilter>('all');
  const launcherRef = useRef<HTMLButtonElement>(null);

  const filteredCommands = useMemo(() => {
    const normalizedQuery = query.trim().toLocaleLowerCase();
    return commands.filter((command) => (
      (source === 'all' || command.source === source)
      && (!normalizedQuery || searchableText(command).includes(normalizedQuery))
    ));
  }, [commands, query, source]);
  const visibleCommands = filteredCommands.slice(0, 200);

  const close = () => {
    setOpen(false);
    globalThis.requestAnimationFrame?.(() => launcherRef.current?.focus());
  };

  useEffect(() => {
    if (!open) return undefined;
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key !== 'Escape') return;
      event.preventDefault();
      close();
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [open]);

  return <>
    <button ref={launcherRef} type="button" className="button button--secondary command-catalog-launcher"
      aria-haspopup="dialog" aria-expanded={open} onClick={() => setOpen(true)}>
      <Search size={15} />添加命令 / Add command
    </button>
    {open && <ModalPortal>
      <div className="command-catalog-backdrop" onMouseDown={(event) => {
        if (event.target === event.currentTarget) close();
      }}>
        <section className="command-catalog" role="dialog" aria-modal="true"
          aria-labelledby="command-catalog-title">
          <header className="command-catalog__header">
            <div>
              <span id="command-catalog-title"><Terminal size={17} />命令目录 / Command catalog</span>
              <small>搜索服务器和触发器实时注册的命令,添加后将写入当前规则的规范命令 ID。</small>
            </div>
            <div className="command-catalog__header-actions">
              <Button type="button" variant="ghost" onClick={onReload} disabled={loading}>
                <RefreshCcw className={loading ? 'spin' : ''} size={15} />刷新
              </Button>
              <button type="button" aria-label="关闭命令目录 / Close command catalog" onClick={close}><X size={18} /></button>
            </div>
          </header>

          <div className="command-catalog__filters">
            <label className="command-catalog__search"><Search size={15} /><Input type="search" autoFocus
              aria-label="搜索命令 / Search commands" value={query}
              onChange={(event) => setQuery(event.target.value)}
              placeholder="搜索命令 ID、/指令、用法、别名或参数" /></label>
            <Select aria-label="命令来源 / Command source" value={source}
              onChange={(event) => setSource(event.target.value as SourceFilter)}>
              <option value="all">全部命令 / All commands</option>
              <option value="server">服务器命令 / Server</option>
              <option value="trigger">触发器指令 / Trigger</option>
            </Select>
          </div>

          <div className="command-catalog__summary" aria-live="polite">
            显示 {filteredCommands.length} / {commands.length} 个命令
            {filteredCommands.length > visibleCommands.length
              && <span>· 当前列出前 {visibleCommands.length} 项,请继续输入关键词缩小范围</span>}
            {loading && commands.length > 0 && <span><LoaderCircle className="spin" size={12} />正在刷新…</span>}
          </div>

          <div className="command-catalog__results">
            {loading && commands.length === 0 && <div className="command-catalog__state" role="status">
              <LoaderCircle className="spin" size={23} /><strong>正在加载命令目录…</strong>
              <span>正在读取服务器命令与触发器动态指令。</span>
            </div>}
            {!loading && error && commands.length === 0 && <div className="command-catalog__state command-catalog__state--error" role="alert">
              <AlertTriangle size={23} /><strong>命令目录加载失败</strong><span>{error}</span>
              <Button type="button" variant="secondary" onClick={onReload}><RefreshCcw size={14} />重新加载</Button>
            </div>}
            {error && commands.length > 0 && <div className="command-catalog__stale-warning" role="status">
              <AlertTriangle size={14} />刷新失败,当前显示上次成功加载的目录:{error}
            </div>}
            {!loading && !error && commands.length === 0 && <div className="command-catalog__state">
              <Terminal size={23} /><strong>服务器尚未提供可用命令</strong>
              <span>创建并启用指令触发器后,它也会出现在这里。</span>
              <Button type="button" variant="secondary" onClick={onReload}><RefreshCcw size={14} />刷新目录</Button>
            </div>}
            {commands.length > 0 && filteredCommands.length === 0 && <div className="command-catalog__state">
              <Search size={23} /><strong>没有匹配的命令</strong>
              <span>请尝试命令 ID、根命令、用法、别名、参数名称或切换来源。</span>
            </div>}
            {visibleCommands.map((command) => {
              const selected = commandIsSelected(command, selectedCommandIds);
              const SourceIcon = command.source === 'trigger' ? Workflow : Server;
              const usages = command.usages.length > 0 ? command.usages : [commandRoot(command.root)];
              return <article className={`command-catalog__item${selected ? ' is-selected' : ''}`} key={command.id}>
                <div className="command-catalog__item-heading">
                  <span className="command-catalog__root"><Terminal size={14} /><strong>{commandRoot(command.root)}</strong></span>
                  <span className={`command-catalog__source command-catalog__source--${command.source}`}>
                    <SourceIcon size={12} />{command.source === 'trigger' ? '触发器指令' : '服务器命令'}
                  </span>
                </div>
                <div className="command-catalog__canonical"><span>规范 ID / Canonical ID</span><code>{command.id}</code></div>
                {command.source === 'trigger' && (command.triggerName || command.triggerId) && <p className="command-catalog__trigger">
                  触发器:<strong>{command.triggerName ?? command.triggerId}</strong>
                  {command.triggerName && command.triggerId && <code>{command.triggerId}</code>}
                </p>}
                {(command.arguments?.length ?? 0) > 0 && <div className="command-catalog__arguments">
                  <span>已声明参数:</span>{command.arguments?.map((argument) => <code key={argument}>{argument}</code>)}
                </div>}
                {(command.aliases?.length ?? 0) > 0 && <div className="command-catalog__aliases">
                  <span>别名:</span>{command.aliases?.map((alias) => <code key={alias}>{commandRoot(alias)}</code>)}
                </div>}
                <div className="command-catalog__usages">
                  <span>用法 / Usage</span>
                  {usages.slice(0, 4).map((usage, index) => (
                    <code key={`${index}:${usage}`}>{usage}</code>
                  ))}
                  {usages.length > 4 && <small>另有 {usages.length - 4} 种用法,可通过搜索匹配。</small>}
                </div>
                <Button type="button" variant={selected ? 'ghost' : 'secondary'} disabled={selected}
                  aria-label={selected ? `命令 ${command.id} 已添加` : `添加命令 ${command.id}`}
                  onClick={() => onAdd(command.id)}>
                  {selected ? <Check size={14} /> : <Plus size={14} />}{selected ? '已添加' : '添加到规则'}
                </Button>
              </article>;
            })}
          </div>
        </section>
      </div>
    </ModalPortal>}
  </>;
}