- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
88 lines
4.5 KiB
TypeScript
88 lines
4.5 KiB
TypeScript
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
|
|
import type { Environment } from "../core/model";
|
|
import { EnvBadge } from "./EnvBadge";
|
|
import { Icon } from "./icons";
|
|
|
|
export function EnvironmentSwitcher({ environments, activeId, onSelect }: {
|
|
environments: Environment[];
|
|
activeId: string | null;
|
|
onSelect: (id: string | null) => void;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const root = useRef<HTMLDivElement>(null);
|
|
const trigger = useRef<HTMLButtonElement>(null);
|
|
const menu = useRef<HTMLDivElement>(null);
|
|
const active = environments.find((env) => env.id === activeId);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const selected = menu.current?.querySelector<HTMLElement>('[aria-checked="true"]');
|
|
(selected ?? menu.current?.querySelector<HTMLElement>('[role="menuitemradio"]'))?.focus();
|
|
const closeOutside = (event: PointerEvent) => {
|
|
if (event.target instanceof Node && !root.current?.contains(event.target)) setOpen(false);
|
|
};
|
|
const closeOnResize = () => setOpen(false);
|
|
window.addEventListener("pointerdown", closeOutside, true);
|
|
window.addEventListener("resize", closeOnResize);
|
|
return () => {
|
|
window.removeEventListener("pointerdown", closeOutside, true);
|
|
window.removeEventListener("resize", closeOnResize);
|
|
};
|
|
}, [open]);
|
|
|
|
const choose = (id: string | null) => {
|
|
onSelect(id);
|
|
setOpen(false);
|
|
trigger.current?.focus();
|
|
};
|
|
|
|
const onMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
setOpen(false);
|
|
trigger.current?.focus();
|
|
return;
|
|
}
|
|
if (event.key === "Tab") {
|
|
setOpen(false);
|
|
return;
|
|
}
|
|
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
|
|
event.preventDefault();
|
|
const options = Array.from(menu.current?.querySelectorAll<HTMLButtonElement>('[role="menuitemradio"]') ?? []);
|
|
if (!options.length) return;
|
|
const current = options.indexOf(document.activeElement as HTMLButtonElement);
|
|
const next = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1
|
|
: (current + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length;
|
|
options[next].focus();
|
|
};
|
|
|
|
return <div className="env-switcher" ref={root}>
|
|
<button ref={trigger} type="button" className="env-switch-trigger" aria-label={`当前环境:${active?.name ?? "不使用环境"}`}
|
|
aria-haspopup="menu" aria-expanded={open} aria-controls={open ? "env-switch-menu" : undefined}
|
|
title={active?.baseUrl || "选择请求环境"} onClick={() => setOpen((value) => !value)}>
|
|
{active ? <EnvBadge envId={active.id} name={active.name} size={18} /> : <Icon name="circle-slash" size={16} className="ops-faint" />}
|
|
<span className="truncate">{active?.name ?? "不使用环境"}</span>
|
|
<Icon name="chevron-down" size={14} className="ops-faint ml-auto shrink-0" />
|
|
</button>
|
|
{open && <div id="env-switch-menu" ref={menu} role="menu" aria-label="切换请求环境" className="env-switch-menu" onKeyDown={onMenuKeyDown}>
|
|
<div className="env-switch-heading">请求环境</div>
|
|
<button type="button" role="menuitemradio" aria-checked={!active} className="env-switch-option" onClick={() => choose(null)}>
|
|
<Icon name="circle-slash" size={17} className="ops-faint shrink-0" />
|
|
<span className="min-w-0 flex-1 text-left"><span className="block font-semibold">不使用环境</span><span className="ops-faint block text-[11px]">直接使用请求中填写的完整地址</span></span>
|
|
{!active && <Icon name="circle-check" size={15} className="ops-accent shrink-0" />}
|
|
</button>
|
|
<div className="env-switch-divider" />
|
|
<div className="env-switch-options">
|
|
{environments.map((env) => <button type="button" role="menuitemradio" aria-checked={env.id === activeId}
|
|
key={env.id} className="env-switch-option" onClick={() => choose(env.id)}>
|
|
<EnvBadge envId={env.id} name={env.name} size={22} />
|
|
<span className="min-w-0 flex-1 text-left"><span className="block truncate font-semibold">{env.name}</span><span className="ops-faint block truncate text-[11px]" title={env.baseUrl || "未设置前置 URL"}>{env.baseUrl || "未设置前置 URL"}</span></span>
|
|
{env.id === activeId && <Icon name="circle-check" size={15} className="ops-accent shrink-0" />}
|
|
</button>)}
|
|
</div>
|
|
{environments.length === 0 && <p className="ops-faint px-3 py-2 text-xs">暂无环境,可点击左侧“管理环境”创建。</p>}
|
|
</div>}
|
|
</div>;
|
|
}
|