mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
- Implemented `telegramAPIURL` and `validateTelegramAPIURL` functions for constructing and validating Telegram API URLs. - Added semantic versioning utilities in `version.go` to compare versions and validate semantic version formats. - Created tests for version comparison logic in `version_test.go`. - Introduced IMS call handling in `call_runtime.go`, including methods for dialing, answering, and hanging up calls. - Added tests for incoming call handling and validation in `call_runtime_test.go`. - Developed a new `PluginsCard` component for managing plugins via URL or file upload in the web interface. - Implemented plugin management functions in `extensions.ts` and created an `ExtensionPage` for displaying plugin contributions.
231 lines
8.6 KiB
TypeScript
231 lines
8.6 KiB
TypeScript
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";
|
|
import { VersionBadge } from "./VersionBadge";
|
|
import { listPlugins, type InstalledPlugin } from "../../extensions";
|
|
|
|
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 [plugins, setPlugins] = useState<InstalledPlugin[]>([]);
|
|
const { logout, user } = useAuth();
|
|
const { t, lang } = 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(() => {
|
|
let active = true;
|
|
const load = () => listPlugins().then((items) => {
|
|
if (active) setPlugins(items || []);
|
|
}).catch(() => undefined);
|
|
void load();
|
|
window.addEventListener("vocat:plugins-changed", load);
|
|
return () => { active = false; window.removeEventListener("vocat:plugins-changed", load); };
|
|
}, []);
|
|
|
|
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) {
|
|
const sidebarPlugins = plugins
|
|
.filter((plugin) => plugin.enabled)
|
|
.flatMap((plugin) => plugin.contributions
|
|
.filter((contribution) => contribution.location === "sidebar")
|
|
.map((contribution) => ({ plugin, contribution })));
|
|
const navItems: Array<(typeof NAV)[number] | { to: string; label: string; icon: typeof GlobeRegular; pluginLabelZH?: string }> = [];
|
|
for (const item of NAV) {
|
|
navItems.push(item);
|
|
if (item.to === "/sms") {
|
|
for (const extension of sidebarPlugins.filter((entry) => !entry.contribution.after || entry.contribution.after === "sms")) {
|
|
navItems.push({
|
|
to: `/extensions/${encodeURIComponent(extension.plugin.id)}/${encodeURIComponent(extension.contribution.id)}`,
|
|
label: extension.contribution.label,
|
|
pluginLabelZH: extension.contribution.labelZh,
|
|
icon: GlobeRegular,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return (
|
|
<nav className={cx("sidebar-menu mt-2", collapse && "is-collapsed")} aria-label={t("主导航")}>
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const label = "pluginLabelZH" in item && item.pluginLabelZH
|
|
? (lang === "zh" ? item.pluginLabelZH : item.label)
|
|
: t(item.label);
|
|
return (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
end={"end" in item ? item.end : undefined}
|
|
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("高通模块测试工具")}</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("高通模块测试工具")}</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">
|
|
<VersionBadge />
|
|
<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>
|
|
);
|
|
}
|