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

XFEServerManager

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

公开
关注 0 Fork 0 Star 0
UTF-8
import { useEffect, useRef, useState, type ButtonHTMLAttributes, type CSSProperties, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes } from 'react';
import { createPortal } from 'react-dom';
import { AlertTriangle, CircleCheck, CircleX, LoaderCircle, RefreshCcw, WifiOff, X } from 'lucide-react';
import { useI18n } from '../lib/i18n';
import { useServer } from '../context/server-context';

export function Button({ className = '', variant = 'primary', ...props }: ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
}) {
  return <button className={`button button--${variant} ${className}`} {...props} />;
}

export function Input({ className = '', ...props }: InputHTMLAttributes<HTMLInputElement>) {
  return <input className={`input ${className}`} {...props} />;
}

export function Select({ className = '', children, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
  return <select className={`input select ${className}`} {...props}>{children}</select>;
}

export function Textarea({ className = '', ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {
  return <textarea className={`input textarea ${className}`} {...props} />;
}

export function Field({ label, hint, children }: { label: ReactNode; hint?: ReactNode; children: ReactNode }) {
  return (
    <label className="field">
      <span className="field__label">{label}</span>
      {children}
      {hint && <span className="field__hint">{hint}</span>}
    </label>
  );
}

export function Panel({ children, className = '', title, action }: {
  children: ReactNode;
  className?: string;
  title?: ReactNode;
  action?: ReactNode;
}) {
  return (
    <section className={`panel ${className}`}>
      {(title || action) && <div className="panel__header"><h2>{title}</h2>{action}</div>}
      {children}
    </section>
  );
}

export function Badge({ children, tone = 'neutral' }: {
  children: ReactNode;
  tone?: 'neutral' | 'good' | 'warn' | 'danger' | 'info';
}) {
  return <span className={`badge badge--${tone}`}>{children}</span>;
}

export function PageHeader({ eyebrow, title, description, actions }: {
  eyebrow?: ReactNode;
  title: ReactNode;
  description: ReactNode;
  actions?: ReactNode;
}) {
  return (
    <header className="page-header">
      <div>
        {eyebrow && <div className="page-header__eyebrow">{eyebrow}</div>}
        <h1>{title}</h1>
        <p>{description}</p>
      </div>
      {actions && <div className="page-header__actions">{actions}</div>}
    </header>
  );
}

export function EmptyState({ title, detail, icon }: { title: ReactNode; detail?: ReactNode; icon?: ReactNode }) {
  return (
    <div className="empty-state">
      <div className="empty-state__icon">{icon ?? <span className="pixel-mark" />}</div>
      <strong>{title}</strong>
      {detail && <p>{detail}</p>}
    </div>
  );
}

export function ResourceState({ loading, error, empty, onRetry, children }: {
  loading: boolean;
  error?: string;
  empty?: boolean;
  onRetry?: () => void;
  children: ReactNode;
}) {
  const { t } = useI18n();
  const { connection } = useServer();
  if (connection === 'offline') {
    return <EmptyState icon={<WifiOff />} title={t('connection.offline')} detail={t('connection.offlineDetail')} />;
  }
  if (loading) return <div className="resource-state"><LoaderCircle className="spin" /> {t('common.loading')}</div>;
  if (error) {
    return (
      <div className="resource-state resource-state--error">
        <AlertTriangle />
        <div><strong>{t('common.error')}</strong><span>{error}</span></div>
        {onRetry && <Button variant="secondary" onClick={onRetry}><RefreshCcw size={15} />{t('common.refresh')}</Button>}
      </div>
    );
  }
  if (empty) return <EmptyState title={t('common.empty')} />;
  return <>{children}</>;
}

export function FeatureUnavailable({ children }: { children: ReactNode }) {
  return <div className="feature-unavailable"><AlertTriangle size={18} /><span>{children}</span></div>;
}

export function TableShell({ children }: { children: ReactNode }) {
  return <div className="table-shell"><table>{children}</table></div>;
}

function toastViewport(): HTMLElement | undefined {
  if (typeof document === 'undefined') return undefined;
  const existing = document.getElementById('xfesm-toast-viewport');
  if (existing) return existing;
  const viewport = document.createElement('div');
  viewport.id = 'xfesm-toast-viewport';
  viewport.className = 'toast-viewport';
  viewport.setAttribute('aria-live', 'polite');
  viewport.setAttribute('aria-atomic', 'false');
  document.body.append(viewport);
  return viewport;
}

export function Toast({ message, title, tone = 'good', duration = 5_000, onClose }: {
  message: string;
  title?: string;
  tone?: 'good' | 'danger' | 'warn';
  /** Set to 0 to keep the notification visible until it is closed. */
  duration?: number;
  onClose?: () => void;
}) {
  const { t } = useI18n();
  const [visible, setVisible] = useState(true);
  const closeHandler = useRef(onClose);
  closeHandler.current = onClose;

  useEffect(() => {
    setVisible(true);
    if (duration <= 0) return undefined;
    const timer = globalThis.setTimeout(() => {
      setVisible(false);
      closeHandler.current?.();
    }, duration);
    return () => globalThis.clearTimeout(timer);
  }, [duration, message, title, tone]);

  if (!visible) return null;
  const viewport = toastViewport();
  const dismiss = () => {
    setVisible(false);
    closeHandler.current?.();
  };
  const Icon = tone === 'good' ? CircleCheck : tone === 'danger' ? CircleX : AlertTriangle;
  const content = (
    <article
      className={`toast toast--${tone}`}
      role={tone === 'danger' ? 'alert' : 'status'}
      style={{ '--toast-duration': `${duration}ms` } as CSSProperties}
    >
      <Icon className="toast__icon" aria-hidden="true" />
      <div className="toast__content">
        <strong>{title ?? message}</strong>
        {title && <span>{message}</span>}
      </div>
      <button className="toast__close" type="button" aria-label={t('common.close')} onClick={dismiss}><X aria-hidden="true" /></button>
      {duration > 0 && <span className="toast__countdown" aria-hidden="true" />}
    </article>
  );
  return viewport ? createPortal(content, viewport) : content;
}

export function MetricCard({ label, value, unit, detail, tone }: {
  label: ReactNode;
  value: ReactNode;
  unit?: ReactNode;
  detail?: ReactNode;
  tone?: 'good' | 'warn' | 'danger';
}) {
  return (
    <div className={`metric-card ${tone ? `metric-card--${tone}` : ''}`}>
      <span className="metric-card__label">{label}</span>
      <div className="metric-card__value">{value}<small>{unit}</small></div>
      {detail && <span className="metric-card__detail">{detail}</span>}
    </div>
  );
}

export function formatBytes(value?: number): string {
  if (value === undefined || !Number.isFinite(value)) return '—';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  let amount = value;
  let unit = 0;
  while (Math.abs(amount) >= 1024 && unit < units.length - 1) {
    amount /= 1024;
    unit += 1;
  }
  return `${amount.toFixed(unit < 2 ? 0 : 1)} ${units[unit]}`;
}

export function formatDuration(seconds?: number): string {
  if (seconds === undefined || !Number.isFinite(seconds)) return '—';
  const days = Math.floor(seconds / 86_400);
  const hours = Math.floor((seconds % 86_400) / 3_600);
  const minutes = Math.floor((seconds % 3_600) / 60);
  return days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}

export function formatDate(value?: string, locale = 'zh-CN'): string {
  if (!value) return '—';
  const date = new Date(value);
  return Number.isNaN(date.valueOf()) ? value : new Intl.DateTimeFormat(locale, {
    dateStyle: 'medium',
    timeStyle: 'medium',
  }).format(date);
}