This commit is contained in:
MengMengCode
2026-08-09 05:33:21 +08:00
commit 7c3c0ebb4b
261 changed files with 61250 additions and 0 deletions
@@ -0,0 +1,195 @@
import { useEffect, useState } from "react";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import {
BoardRegular,
DocumentTextRegular,
GlobeRegular,
MailRegular,
PanelLeftContractRegular,
PanelLeftExpandRegular,
RouterRegular,
SettingsRegular,
SignOutRegular,
} from "@fluentui/react-icons";
import { useAuth } from "../../store/auth";
import { useI18n } from "../../lib/i18n";
import { confirmDialog } from "../ui/MessageBox";
import { LanguageSwitch } from "../ui/LanguageSwitch";
import { SwitchDark } from "../ui/SwitchDark";
import { Drawer } from "../ui/Drawer";
import { ErrorBoundary } from "../ui/ErrorBoundary";
import { cx } from "../../lib/utils";
import { BrandLogo } from "./BrandLogo";
const NAV = [
{ to: "/", label: "仪表盘", icon: BoardRegular, end: true },
{ to: "/devices", label: "设备管理", icon: RouterRegular },
{ to: "/proxy", label: "代理管理", icon: GlobeRegular },
{ to: "/sms", label: "短信检测", icon: MailRegular },
{ to: "/logs", label: "实时日志", icon: DocumentTextRegular },
{ to: "/settings", label: "系统设置", icon: SettingsRegular },
];
export function AuthenticatedShell({
isDark,
onToggleTheme,
}: {
isDark: boolean;
onToggleTheme: () => void;
}) {
const [collapsed, setCollapsed] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const { logout, user } = useAuth();
const { t } = useI18n();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const mq = window.matchMedia("(max-width: 767px)");
const update = () => {
setIsMobile(mq.matches);
if (!mq.matches) setMobileOpen(false);
};
update();
window.addEventListener("resize", update, { passive: true });
return () => window.removeEventListener("resize", update);
}, []);
useEffect(() => {
setMobileOpen(false);
}, [location.pathname]);
async function onLogout() {
const ok = await confirmDialog(t("确认退出登录?"), t("提示"), {
confirmText: t("退出"),
cancelText: t("取消"),
type: "warning",
});
if (ok) {
await logout();
navigate("/login");
}
}
function toggle() {
if (isMobile) setMobileOpen(true);
else setCollapsed((value) => !value);
}
function menuList(collapse: boolean) {
return (
<nav className={cx("sidebar-menu mt-2", collapse && "is-collapsed")} aria-label={t("主导航")}>
{NAV.map((item) => {
const Icon = item.icon;
const label = t(item.label);
return (
<NavLink
key={item.to}
to={item.to}
end={item.end}
title={collapse ? label : undefined}
className={({ isActive }) => cx("vocat-menu-item", isActive && "is-active")}
>
<span className="vocat-menu-icon">
<Icon />
</span>
<span className="sidebar-menu-label">{label}</span>
</NavLink>
);
})}
</nav>
);
}
function userCard() {
return (
<div className="ui-panel-muted flex items-center gap-3 p-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-300">
<SettingsRegular className="h-[18px] w-[18px]" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-bold">{user?.username || "Admin"}</div>
<div className="truncate text-xs text-gray-400">{user?.role || "Administrator"}</div>
</div>
<button
type="button"
onClick={onLogout}
aria-label={t("退出登录")}
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10"
>
<SignOutRegular className="h-[18px] w-[18px]" />
</button>
</div>
);
}
return (
<div className="flex h-full">
{!isMobile && (
<aside
className={cx(
"ui-glass sidebar-shell relative h-full transition-[width] duration-200",
collapsed ? "w-[52px]" : "w-[232px]",
)}
>
<div className={cx("flex h-14 items-center px-4", collapsed && "justify-center px-0")}>
<BrandLogo className="sidebar-brand-logo" />
{!collapsed && (
<div className="ml-3">
<div className="sidebar-brand-title">vocat</div>
<div className="text-[10px] font-medium leading-tight tracking-wide text-gray-400 dark:text-gray-500">{t("EC20 出厂检测工具")}</div>
</div>
)}
</div>
{menuList(collapsed)}
{!collapsed && <div className="absolute bottom-4 w-full px-3">{userCard()}</div>}
</aside>
)}
<Drawer open={isMobile && mobileOpen} onClose={() => setMobileOpen(false)} className="mobile-drawer">
<div className="sidebar-shell relative h-full bg-white/95 backdrop-blur-md dark:bg-[#141418]/95">
<div className="flex h-16 items-center px-4">
<BrandLogo className="sidebar-brand-logo" />
<div className="ml-3">
<div className="sidebar-brand-title">vocat</div>
<div className="text-[10px] font-medium leading-tight tracking-wide text-gray-400 dark:text-gray-500">{t("EC20 出厂检测工具")}</div>
</div>
</div>
{menuList(false)}
<div className="absolute bottom-4 w-full px-3">{userCard()}</div>
</div>
</Drawer>
<div className="flex h-full min-w-0 flex-1 flex-col">
<header className="ui-glass sticky top-0 z-10 flex h-14 items-center justify-between border-b border-gray-100 px-4 dark:border-white/5 sm:px-5">
<div className="flex items-center gap-2">
<button
type="button"
onClick={toggle}
aria-label={collapsed ? t("展开侧栏") : t("收起侧栏")}
className="rounded-lg px-2 py-1.5 text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200"
>
{!isMobile && !collapsed ? (
<PanelLeftContractRegular className="h-5 w-5" />
) : (
<PanelLeftExpandRegular className="h-5 w-5" />
)}
</button>
</div>
<div className="flex items-center gap-3">
<LanguageSwitch />
<SwitchDark isDark={isDark} onToggle={onToggleTheme} />
</div>
</header>
<main className="flex-1 overflow-auto bg-gray-50/50 p-4 dark:bg-transparent sm:p-6">
<div className="main-inner mx-auto w-full">
<ErrorBoundary title={t("页面渲染失败")}>
<Outlet />
</ErrorBoundary>
</div>
</main>
</div>
</div>
);
}
+15
View File
@@ -0,0 +1,15 @@
export function BrandLogo({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M326.255787 457.728c23.637333 0.597333 37.717333 15.872 37.461333 40.533333a40.362667 40.362667 0 0 1-40.021333 42.069334c-20.138667-0.938667-34.816-18.602667-34.645334-41.642667 0.170667-24.832 15.274667-41.472 37.205334-40.96z" fill="#1296db" />
<path d="M982.127787 565.333333c-11.605333 0-17.066667-2.389333-17.066667-15.786666a377.941333 377.941333 0 0 0-33.28-143.872 46.506667 46.506667 0 0 1-2.474667-27.562667c11.178667-64.597333 21.930667-129.28 30.805334-194.218667 4.949333-36.181333-12.885333-49.322667-47.36-37.290666-54.357333 18.858667-108.714667 37.888-162.730667 57.685333a44.714667 44.714667 0 0 1-34.133333-0.853333c-131.669333-50.773333-263.594667-52.394667-395.264 0a47.36 47.36 0 0 1-36.608 0.682666c-51.797333-19.029333-103.936-37.12-155.989334-55.466666-42.666667-14.933333-59.136-0.938667-52.053333 44.288s13.824 90.112 22.357333 134.826666c7.509333 39.594667 11.008 77.312-7.936 115.882667a217.770667 217.770667 0 0 0-19.797333 88.32c-0.768 21.162667-3.328 35.498667-25.6 28.416H26.82112c-13.653333 1.450667-26.112 6.144-26.794667 22.101333-0.682667 17.066667 11.946667 23.125333 26.709334 24.576a171.434667 171.434667 0 0 0 28.842666 0.512c11.690667-0.768 17.066667 3.242667 19.029334 15.445334a220.245333 220.245333 0 0 0 28.928 80.810666c7.253333 12.288 7.253333 19.370667-6.4 26.282667a309.76 309.76 0 0 0-40.704 25.6c-11.178667 8.021333-18.346667 18.944-9.898667 32.938667s21.248 13.312 33.621333 6.570666c16.128-8.533333 31.744-18.602667 47.104-28.672 9.045333-5.973333 15.274667-7.936 24.490667 1.194667 51.2 51.2 114.602667 80.725333 184.661333 96.426667 100.352 22.528 201.898667 20.309333 303.274667 10.325333a417.450667 417.450667 0 0 0 195.242667-67.754667c17.834667-12.032 32.768-34.133333 51.2-37.376s36.522667 19.712 55.552 29.610667l2.133333 1.109333c12.714667 5.632 25.6 7.594667 34.133333-6.314666s1.962667-24.917333-9.301333-33.024a335.786667 335.786667 0 0 0-38.4-24.234667c-11.178667-5.973333-12.032-11.605333-5.205333-22.101333a230.4 230.4 0 0 0 33.621333-84.138667c2.645333-14.336 8.533333-18.176 21.76-18.090667 18.602667 0 43.776 3.84 43.776-23.893333-0.085333-26.026667-23.893333-23.210667-42.069333-22.954667z m-83.541334 1.450667a458.837333 458.837333 0 0 0-55.210666 1.365333c-13.738667 0.768-23.210667 7.68-22.954667 22.357334a21.674667 21.674667 0 0 0 23.466667 22.186666 436.309333 436.309333 0 0 0 52.821333 0c17.066667-1.536 19.029333 5.546667 13.994667 19.114667-3.072 8.533333-5.376 17.066667-8.533334 24.917333-18.346667 47.189333-18.517333 47.274667-62.208 24.661334-13.653333-7.082667-26.624-9.642667-35.498666 5.546666s0 25.6 12.458666 34.133334c29.184 20.906667 29.184 21.077333 0 41.813333-64 45.738667-137.472 65.792-214.101333 69.376-91.648 4.352-184.149333 8.533333-274.261333-17.066667a355.242667 355.242667 0 0 1-129.450667-65.792c-13.397333-11.178667-18.858667-19.285333 1.194667-28.074666a56.149333 56.149333 0 0 0 13.909333-9.386667c9.301333-8.533333 13.141333-18.346667 5.888-29.781333a20.650667 20.650667 0 0 0-29.098667-8.021334c-3.669333 1.706667-7.168 3.669333-10.666666 5.632-30.549333 17.066667-30.634667 17.066667-44.544-14.592a167.253333 167.253333 0 0 1-16.213334-57.002666h58.282667c14.506667 0 25.6-6.229333 26.197333-22.016S194.074453 563.882667 179.311787 563.2a292.864 292.864 0 0 0-42.666667-1.194667c-21.504 2.133333-23.125333-7.594667-20.736-25.6a426.666667 426.666667 0 0 1 34.133333-121.856 54.272 54.272 0 0 0 3.669334-34.389333c-9.984-55.125333-17.578667-110.933333-27.477334-165.632-3.242667-17.92 2.133333-17.066667 15.36-12.117333 48.128 17.493333 96.597333 34.133333 144.554667 51.2a49.066667 49.066667 0 0 0 41.386667-1.877334A397.141333 397.141333 0 0 1 501.44512 213.333333c73.728-0.597333 145.066667 6.656 211.797333 41.813334a40.96 40.96 0 0 0 34.730667-0.768c48.042667-17.066667 96.426667-34.133333 145.066667-51.2a23.808 23.808 0 0 1 17.834666-2.304c-9.472 59.050667-18.090667 118.186667-29.013333 176.896a64.512 64.512 0 0 0 4.778667 41.301333 429.056 429.056 0 0 1 33.536 124.672c1.536 17.237333-2.730667 24.234667-21.589334 23.04z" fill="#1296db" />
<path d="M516.634453 535.296c42.666667 0.853333 59.221333 10.581333 59.221334 35.925333s-31.146667 57.344-55.210667 56.490667c-29.184-0.938667-60.16-31.573333-59.733333-58.794667 0.426667-23.381333 18.517333-34.304 55.722666-33.621333zM713.15712 462.592C737.050453 463.189333 751.04512 477.866667 751.04512 503.466667a40.533333 40.533333 0 0 1-40.106667 42.069333c-20.053333-0.853333-34.901333-18.688-34.645333-41.642667-0.085333-25.344 15.018667-41.813333 36.864-41.301333z" fill="#1296db" />
</svg>
);
}
@@ -0,0 +1,22 @@
import { Outlet } from "react-router-dom";
import { LanguageSwitch } from "../ui/LanguageSwitch";
import { SwitchDark } from "../ui/SwitchDark";
// UnauthenticatedShell: centers the login card, theme toggle pinned top-right.
export function UnauthenticatedShell({
isDark,
onToggleTheme,
}: {
isDark: boolean;
onToggleTheme: () => void;
}) {
return (
<div className="flex h-screen items-center justify-center bg-gray-100 transition-colors duration-300 dark:bg-gray-950">
<div className="absolute right-4 top-4 z-50 flex items-center gap-2">
<LanguageSwitch />
<SwitchDark isDark={isDark} onToggle={onToggleTheme} />
</div>
<Outlet />
</div>
);
}