Unverified Commit 90c39afb authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(panel): 手机端加导航抽屉和登出入口,服务器与备份列表在窄屏改为卡片,桌面服务器表不再挤成竖排

parent 3a2166ca
Loading
Loading
Loading
Loading
+70 −5
Changes for panel/src/components/AppShell.tsx: 70 added lines, 5 removed lines.
Original line number Diff line number Diff line
import { useEffect, useState } from "react";
import { NavLink, Outlet, useLocation } from "react-router-dom";
import { Cat, Globe, Sun, Moon, LogOut, WifiOff } from "lucide-react";
import { Cat, Globe, Sun, Moon, LogOut, WifiOff, Menu, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import * as SelectPrimitive from "@radix-ui/react-select";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils";
import { useTier } from "@/lib/tier";
import { visibleSections, type NavSection } from "@/lib/nav";
@@ -12,7 +13,15 @@ import { api, CONNECTION_EVENT, isConnectionLost } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { ErrorBoundary } from "@/components/ErrorBoundary";

function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: boolean }) {
function SectionGroup({
  section,
  isFirst,
  onNavigate,
}: {
  section: NavSection;
  isFirst: boolean;
  onNavigate?: () => void;
}) {
  const { t } = useTranslation("navigation");

  return (
@@ -34,6 +43,7 @@ function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: bool
          key={item.to}
          to={item.to}
          end={item.end}
          onClick={onNavigate}
          className={({ isActive }) =>
            cn(
              "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
@@ -95,7 +105,7 @@ function UserStrip() {
        type="button"
        onClick={signOut}
        disabled={signingOut || !identity}
        className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background hover:text-destructive border border-transparent hover:border-border/50 focus:outline-none transition-all active:scale-95 disabled:opacity-50"
        className="flex h-10 w-10 md:h-7 md:w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background hover:text-destructive border border-transparent hover:border-border/50 focus:outline-none transition-all active:scale-95 disabled:opacity-50"
        aria-label={t("sign_out")}
        title={t("sign_out")}
      >
@@ -168,6 +178,60 @@ function ConnectionBanner() {
  );
}

// MobileNav is the phone-width way into every page: the sidebar is md-only, so
// below md a menu button opens the same sections (and the same sign-out strip)
// as a drawer from the left. Following a link closes it; so does a route change
// from anywhere else (back button, a link inside the page).
function MobileNav({ sections }: { sections: NavSection[] }) {
  const { t } = useTranslation("common");
  const { pathname } = useLocation();
  const [open, setOpen] = useState(false);
  useEffect(() => setOpen(false), [pathname]);

  return (
    <DialogPrimitive.Root open={open} onOpenChange={setOpen}>
      <DialogPrimitive.Trigger
        className={FOOT_ICON_BTN}
        aria-label={t("open_menu")}
        title={t("open_menu")}
      >
        <Menu className="h-4 w-4" />
      </DialogPrimitive.Trigger>
      <DialogPrimitive.Portal>
        <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 md:hidden" />
        <DialogPrimitive.Content
          aria-describedby={undefined}
          className="fixed inset-y-0 left-0 z-50 flex w-72 max-w-[85vw] flex-col border-r border-border bg-card p-4 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left md:hidden"
        >
          <div className="mb-5 flex items-center justify-between px-2">
            <DialogPrimitive.Title className="flex items-center gap-2 text-lg font-semibold tracking-tight">
              <Cat className="h-6 w-6 text-primary" />
              {t("brand_name")}
            </DialogPrimitive.Title>
            <DialogPrimitive.Close className={FOOT_ICON_BTN} aria-label={t("close_sr")}>
              <X className="h-4 w-4" />
            </DialogPrimitive.Close>
          </div>
          <nav className="flex flex-col overflow-y-auto">
            {sections.map((section, i) => (
              <SectionGroup
                key={section.id}
                section={section}
                isFirst={i === 0}
                onNavigate={() => setOpen(false)}
              />
            ))}
          </nav>
          <div className="mt-auto border-t border-border/50 pt-3">
            {/* The strip's sign-out button grows to a thumb-sized target below md. */}
            <UserStrip />
          </div>
        </DialogPrimitive.Content>
      </DialogPrimitive.Portal>
    </DialogPrimitive.Root>
  );
}

export function AppShell() {
  const { isAdmin, isOwner } = useTier();
  const { pathname } = useLocation();
@@ -222,8 +286,9 @@ export function AppShell() {

      <div className="flex min-w-0 flex-1 flex-col h-full overflow-y-auto">
        <ConnectionBanner />
        <header className="flex h-14 items-center justify-between border-b border-border px-4 md:hidden">
        <header className="flex h-14 shrink-0 items-center justify-between border-b border-border px-4 md:hidden">
          <div className="flex items-center gap-2">
            <MobileNav sections={sections} />
            <Cat className="h-5 w-5 text-primary" />
            <span className="font-semibold">{t("common:brand_name")}</span>
          </div>
@@ -232,7 +297,7 @@ export function AppShell() {
            <ThemeToggle />
          </div>
        </header>
        <main className="flex flex-1 flex-col p-6">
        <main className="flex flex-1 flex-col p-4 md:p-6">
          <div className="mx-auto flex w-full max-w-8xl flex-1 flex-col gap-6">
            {/* A crash on one page leaves the navigation usable; moving to
                another route clears it. */}
+1 −1
Changes for panel/src/components/PhaseBadge.tsx: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -55,7 +55,7 @@ export function phaseVariant(phase: Phase): BadgeVariant {
export function PhaseBadge({ phase }: { phase: Phase }) {
  const { t } = useTranslation();
  return (
    <Badge variant={phaseVariant(phase)} className="gap-1.5">
    <Badge variant={phaseVariant(phase)} className="gap-1.5 whitespace-nowrap">
      <span
        className={cn(
          "h-1.5 w-1.5 rounded-full",
+6 −4
Changes for panel/src/components/StatCard.tsx: 6 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -14,23 +14,25 @@ export interface StatCardProps {
  valueClass?: string;
}

const DEFAULT_ICON_CONTAINER = "rounded-md p-2 bg-muted/30 text-muted-foreground";
const DEFAULT_ICON_CONTAINER = "shrink-0 rounded-md p-1.5 sm:p-2 bg-muted/30 text-muted-foreground";

export function StatCard({ icon: Icon, label, value, accentClass, accentColor, valueClass }: StatCardProps) {
  const containerStyle = accentColor ? { backgroundColor: `${accentColor}26`, color: accentColor } : undefined;
  return (
    <Card>
      <CardContent className="flex items-center gap-3 p-4">
      {/* Two cards share a phone's width, so padding, icon and figure step down
          below sm to keep a value like "50 / 240" whole. */}
      <CardContent className="flex items-center gap-2.5 p-3 sm:gap-3 sm:p-4">
        <div
          className={cn(DEFAULT_ICON_CONTAINER, !accentColor && accentClass)}
          style={containerStyle}
        >
          <Icon className="h-5 w-5" />
          <Icon className="h-4 w-4 sm:h-5 sm:w-5" />
        </div>
        <div className="min-w-0 flex-1">
          <div
            className={cn(
              "text-2xl font-bold font-mono leading-none truncate text-foreground",
              "text-xl sm:text-2xl font-bold font-mono leading-none truncate text-foreground",
              valueClass,
            )}
            title={typeof value === "string" ? value : undefined}
+2 −1
Changes for panel/src/i18n/resources/en-US/common.json: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -30,5 +30,6 @@
  "not_found_title": "Page not found",
  "not_found_body": "The name in the link is not valid, or the page does not exist.",
  "connection_lost": "Can't reach Felis: the network is down, or your Cloudflare Access sign-in expired. This clears by itself once the network is back; an expired Access sign-in needs a page reload.",
  "reload_page": "Reload page"
  "reload_page": "Reload page",
  "open_menu": "Open menu"
}
+2 −1
Changes for panel/src/i18n/resources/zh-CN/common.json: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -30,5 +30,6 @@
  "not_found_title": "找不到这个页面",
  "not_found_body": "链接里的名称不合法,或者这个页面不存在。",
  "connection_lost": "连不上 Felis:网络断开了,或者 Cloudflare Access 的登录已过期。网络恢复后会自动消失;Access 过期需要刷新页面重新登录。",
  "reload_page": "刷新页面"
  "reload_page": "刷新页面",
  "open_menu": "打开菜单"
}
Loading