feat: OpsxAPI v1.0 全协议接口工作台

- 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT
- 集合树/多标签/多环境变量/历史记录,localStorage 持久化
- 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、
  Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖)
- 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、
  全量备份;单请求生成 cURL/fetch/Python/Go 代码
- Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、
  WebSocket 隧道(浏览器自定义握手 Header)
- 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付)
- lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题
- 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全
This commit is contained in:
logan committed 2026-09-18 00:16:59 +08:00
commit dec66db5aa
182 files changed
+23377

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
/**
* 请求标签页栏。
*/
import { KIND_LABELS, findNode } from "@opsxapi/core";
import { useState } from "react";
import { useWorkspace } from "../workspaceStore";
import { ContextMenu, cx, type MenuItem } from "./common";
const KIND_TAB_ICON: Record<string, string> = {
http: "",
graphql: "◈",
websocket: "⚡",
sse: "📡",
socketio: "🔌",
mqtt: "📮",
};
export function TabBar() {
const collections = useWorkspace((s) => s.collections);
const openTabIds = useWorkspace((s) => s.openTabIds);
const activeTabId = useWorkspace((s) => s.activeTabId);
const setActiveTab = useWorkspace((s) => s.setActiveTab);
const closeTab = useWorkspace((s) => s.closeTab);
const closeOtherTabs = useWorkspace((s) => s.closeOtherTabs);
const [menu, setMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const nodes = openTabIds
.map((id) => findNode(collections, id)?.node)
.filter((n): n is NonNullable<typeof n> => Boolean(n));
const menuItems = (id: string): MenuItem[] => [
{ label: "关闭", onClick: () => closeTab(id) },
{ label: "关闭其他", onClick: () => closeOtherTabs(id) },
{
label: "关闭全部",
onClick: () => {
for (const n of nodes) closeTab(n.id);
},
},
];
return (
<div
className="ops-panel-2 flex min-h-[38px] shrink-0 items-stretch overflow-x-auto"
style={{ borderBottom: "1px solid var(--border)" }}
onContextMenu={(e) => {
if (e.target === e.currentTarget) e.preventDefault();
}}
>
{nodes.length === 0 && (
<div className="ops-faint flex items-center px-4 text-xs">没有打开的请求</div>
)}
{nodes.map((node) => {
const active = node.id === activeTabId;
const runtime = useWorkspace.getState().runtime[node.id];
const busy = runtime?.loading || runtime?.status === "connecting" || runtime?.status === "open";
return (
<div
key={node.id}
className={cx(
"group flex cursor-pointer items-center gap-2 whitespace-nowrap px-3.5 text-xs transition",
active ? "" : "ops-faint hover:opacity-100",
)}
style={
active
? { background: "var(--panel)", borderTop: "2px solid var(--accent)", color: "var(--text)" }
: { borderTop: "2px solid transparent" }
}
onClick={() => setActiveTab(node.id)}
onAuxClick={(e) => e.button === 1 && closeTab(node.id)}
onContextMenu={(e) => {
e.preventDefault();
setMenu({ x: e.clientX, y: e.clientY, id: node.id });
}}
title={node.name}
>
{node.type === "request" && node.kind === "http" ? (
<span className={`ops-method-${node.data.method} font-bold`}>{node.data.method}</span>
) : node.type === "request" ? (
<span title={KIND_LABELS[node.kind]}>{KIND_TAB_ICON[node.kind]}</span>
) : null}
<span className="max-w-48 truncate">{node.name}</span>
{busy && <span className="ops-dot ops-dot-connecting" />}
<button
className={cx(
"rounded px-1 text-[13px] leading-none opacity-40 transition group-hover:opacity-100",
)}
style={{ color: active ? "var(--text)" : "inherit" }}
onClick={(e) => {
e.stopPropagation();
closeTab(node.id);
}}
>
×
</button>
</div>
);
})}
<ContextMenu
menu={menu ? { x: menu.x, y: menu.y, items: menuItems(menu.id) } : null}
onClose={() => setMenu(null)}
/>
</div>
);
}