Files
opsxapi/src/components/EnvironmentSwitcher.tsx
T
logan 2bb4ef7a95 桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
2026-10-16 03:03:36 +08:00

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>;
}