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

XFEServerManager

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

公开
关注 0 Fork 0 Star 0
UTF-8
import { Blocks, Map, Pencil, Plus, Search, ShieldAlert, Trash2 } from 'lucide-react';
import { useState, type FormEvent } from 'react';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import type { ClaimSummary } from '../types';
import { Badge, Button, FeatureUnavailable, Field, Input, PageHeader, Panel, ResourceState, TableShell, Textarea, Toast } from '../components/ui';

const flagNames = ['BREAK', 'PLACE', 'INTERACT', 'CONTAINER', 'ENTITY_INTERACT', 'ENTITY_DAMAGE', 'PVP', 'EXPLOSION', 'MOB_GRIEF', 'PISTON_CROSS_BOUNDARY', 'FLUID_CROSS_BOUNDARY'];

function parseChunks(dimension: string, value: string) {
  return value.split(/\r?\n/).filter((line) => line.trim()).map((line) => {
    const parts = line.split(',').map((part) => Number(part.trim()));
    if (parts.length !== 2 || parts.some((part) => !Number.isInteger(part))) throw new Error(`Invalid chunk: ${line}`);
    return { dimension, x: parts[0], z: parts[1] };
  });
}

function parseMembers(value: string) {
  const members: Record<string, string> = {};
  value.split(/\r?\n/).filter((line) => line.trim()).forEach((line) => {
    const [uuid, role = 'member'] = line.split('=').map((part) => part.trim());
    if (!uuid || !['manager', 'member', 'trusted'].includes(role)) throw new Error(`Invalid member: ${line}`);
    members[uuid] = role;
  });
  return members;
}

export function ClaimsPage() {
  const { t } = useI18n();
  const { api, features } = useServer();
  const enabled = features?.claimsEnabled === true;
  const [searchInput, setSearchInput] = useState(''); const [query, setQuery] = useState('');
  const [editing, setEditing] = useState<ClaimSummary>(); const [owner, setOwner] = useState('');
  const [name, setName] = useState(''); const [dimension, setDimension] = useState('minecraft:overworld');
  const [chunks, setChunks] = useState('0,0'); const [members, setMembers] = useState('');
  const [flags, setFlags] = useState<string[]>([]); const [reason, setReason] = useState('');
  const [newOwner, setNewOwner] = useState(''); const [transferToken, setTransferToken] = useState('');
  const [acceptingPlayer, setAcceptingPlayer] = useState('');
  const [deletePreview, setDeletePreview] = useState(false); const [busy, setBusy] = useState(false);
  const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
  const resource = useApiResource(() => api.claims(undefined, query), [query], ['claim'], enabled);
  const mode = editing ? t('claims.edit') : t('claims.create');

  const reset = () => { setEditing(undefined); setOwner(''); setName(''); setDimension('minecraft:overworld'); setChunks('0,0'); setMembers(''); setFlags([]); setReason(''); setDeletePreview(false); setNewOwner(''); setTransferToken(''); setAcceptingPlayer(''); };
  const select = (claim: ClaimSummary) => {
    setEditing(claim); setOwner(claim.ownerUuid); setName(claim.name ?? ''); setDimension(claim.dimension);
    setChunks(claim.chunks.map((chunk) => `${chunk.x},${chunk.z}`).join('\n'));
    setMembers(Object.entries(claim.members).map(([uuid, role]) => `${uuid}=${role}`).join('\n'));
    setFlags(Object.entries(claim.flags).filter(([, allowed]) => allowed).map(([flag]) => flag.toUpperCase())); setReason(''); setDeletePreview(false);
  };
  const payload = () => ({ ownerId: owner.trim(), name: name.trim(), chunks: parseChunks(dimension.trim(), chunks), members: parseMembers(members), flags, reason: reason.trim(), revision: editing?.revision });
  const submit = async (event: FormEvent) => {
    event.preventDefault(); setBusy(true);
    try { if (!reason.trim()) throw new Error(t('common.reasonRequired')); if (editing) await api.updateClaim(editing.id, payload()); else await api.createClaim(payload()); setToast({ message: t('claims.saved'), tone: 'good' }); reset(); resource.reload(); }
    catch (error) { setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' }); } finally { setBusy(false); }
  };
  const remove = async () => {
    if (!editing || !reason.trim()) return; if (!deletePreview) { setDeletePreview(true); return; } setBusy(true);
    try { await api.deleteClaim(editing.id, editing.revision, reason.trim()); setToast({ message: t('claims.deleted'), tone: 'good' }); reset(); resource.reload(); }
    catch (error) { setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' }); } finally { setBusy(false); }
  };
  const beginTransfer = async () => {
    if (!editing || !newOwner.trim() || !reason.trim()) return; setBusy(true);
    try { const ticket = await api.beginClaimTransfer(editing.id, editing.ownerUuid, newOwner.trim(), reason.trim()); setTransferToken(ticket.token); setAcceptingPlayer(newOwner.trim()); setToast({ message: `${t('claims.transferReady')} ${ticket.expiresAt}`, tone: 'good' }); }
    catch (error) { setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' }); } finally { setBusy(false); }
  };
  const confirmTransfer = async () => {
    if (!transferToken.trim() || !acceptingPlayer.trim() || !reason.trim()) return; setBusy(true);
    try { await api.confirmClaimTransfer(transferToken.trim(), acceptingPlayer.trim(), reason.trim()); setToast({ message: t('claims.transferred'), tone: 'good' }); setTransferToken(''); setAcceptingPlayer(''); reset(); resource.reload(); }
    catch (error) { setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' }); } finally { setBusy(false); }
  };

  return <div className="page"><PageHeader title={t('claims.title')} description={t('claims.description')} actions={<form className="search-box" onSubmit={(event) => { event.preventDefault(); setQuery(searchInput.trim()); }}><Search size={16} /><Input value={searchInput} onChange={(event) => setSearchInput(event.target.value)} aria-label={t('common.search')} placeholder={t('common.search')} /></form>} />
    {!enabled && <FeatureUnavailable>{t('claims.featureOff')}</FeatureUnavailable>}<div className="moderation-layout">
      <Panel title={<><Plus size={18} />{mode}</>}><form className="form-stack" onSubmit={submit}>
        <Field label={t('claims.owner')}><Input value={owner} onChange={(event) => setOwner(event.target.value)} disabled={Boolean(editing)} required /></Field>
        <Field label={t('claims.name')}><Input value={name} onChange={(event) => setName(event.target.value)} required /></Field>
        <Field label={t('claims.dimension')}><Input value={dimension} onChange={(event) => setDimension(event.target.value)} disabled={Boolean(editing)} required /></Field>
        <Field label={t('claims.chunkLines')}><Textarea value={chunks} onChange={(event) => setChunks(event.target.value)} required /></Field>
        <Field label={t('claims.memberLines')}><Textarea value={members} onChange={(event) => setMembers(event.target.value)} /></Field>
        <Field label={t('claims.flags')}><div className="checkbox-grid">{flagNames.map((flag) => <label key={flag}><input type="checkbox" checked={flags.includes(flag)} onChange={(event) => setFlags((current) => event.target.checked ? [...current, flag] : current.filter((item) => item !== flag))} />{flag}</label>)}</div></Field>
        <Field label={t('common.reason')}><Textarea value={reason} onChange={(event) => { setReason(event.target.value); setDeletePreview(false); }} required /></Field>
        {editing && <div className="confirm-box"><ShieldAlert /><div className="form-stack"><strong>{t('claims.transfer')}</strong><Input value={newOwner} onChange={(event) => setNewOwner(event.target.value)} placeholder={t('claims.newOwner')} /><Button type="button" variant="secondary" onClick={() => void beginTransfer()} disabled={busy || !newOwner.trim() || !reason.trim()}>{t('claims.beginTransfer')}</Button></div></div>}
        <div className="form-grid"><Field label={t('claims.transferToken')}><Input value={transferToken} onChange={(event) => setTransferToken(event.target.value)} /></Field><Field label={t('claims.acceptingPlayer')}><Input value={acceptingPlayer} onChange={(event) => setAcceptingPlayer(event.target.value)} /></Field></div>
        <Button type="button" variant="secondary" onClick={() => void confirmTransfer()} disabled={busy || !transferToken.trim() || !acceptingPlayer.trim() || !reason.trim()}>{t('claims.confirmTransfer')}</Button>
        {deletePreview && <div className="confirm-box confirm-box--danger"><ShieldAlert /><p>{t('claims.deleteWarning')}</p></div>}
        <div className="button-row"><Button type="submit" disabled={busy || !enabled}><Pencil size={16} />{mode}</Button>{editing && <Button type="button" variant="danger" onClick={() => void remove()} disabled={busy || !reason.trim()}><Trash2 size={16} />{deletePreview ? t('common.confirm') : t('common.delete')}</Button>}<Button type="button" variant="ghost" onClick={reset}>{t('common.cancel')}</Button></div>
      </form></Panel>
      <Panel title={<><Map size={18} />{t('claims.title')}</>}><ResourceState loading={resource.loading} error={resource.error} empty={resource.data?.items.length === 0} onRetry={resource.reload}><TableShell><thead><tr><th>{t('claims.owner')}</th><th>{t('claims.dimension')}</th><th>{t('claims.chunks')}</th><th>{t('claims.members')}</th><th /></tr></thead><tbody>{resource.data?.items.map((claim) => <tr key={claim.id}><td><strong>{claim.ownerName ?? claim.ownerUuid}</strong><small className="table-subline">{claim.name}</small></td><td><Badge tone="info">{claim.dimension}</Badge></td><td><span className="action-cell"><Blocks size={14} />{claim.chunks.length}</span></td><td>{Object.keys(claim.members).length}</td><td><Button variant="ghost" onClick={() => select(claim)}>{t('common.edit')}</Button></td></tr>)}</tbody></TableShell></ResourceState></Panel>
    </div>{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}</div>;
}