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:
commit
dec66db5aa
182 files changed
+23377
No files matched your search
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* OpsxAPI 工作台入口:组装顶栏 / 侧栏 / 标签页 / 编辑器 / 对话框。
|
||||
*/
|
||||
import { findNode } from "@opsxapi/core";
|
||||
import { useEffect } from "react";
|
||||
import { closeSession } from "./send";
|
||||
import { Icon } from "./components/icons";
|
||||
import { Logo } from "./components/Logo";
|
||||
import { useWorkspace } from "./workspaceStore";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { TabBar } from "./components/TabBar";
|
||||
import { TopBar } from "./components/TopBar";
|
||||
import { GraphqlEditor } from "./components/editors/GraphqlEditor";
|
||||
import { HttpEditor } from "./components/editors/HttpEditor";
|
||||
import { MqttEditor } from "./components/editors/MqttEditor";
|
||||
import { SocketIoEditor } from "./components/editors/SocketIoEditor";
|
||||
import { SseEditor } from "./components/editors/SseEditor";
|
||||
import { WebsocketEditor } from "./components/editors/WebsocketEditor";
|
||||
import { ExportDialog } from "./components/dialogs/ExportDialog";
|
||||
import { ImportDialog } from "./components/dialogs/ImportDialog";
|
||||
import {
|
||||
EnvironmentsDialog,
|
||||
NewItemDialog,
|
||||
RenameDialog,
|
||||
SettingsDialog,
|
||||
} from "./components/dialogs/WorkspaceDialogs";
|
||||
|
||||
export function OpsxWorkspace() {
|
||||
const init = useWorkspace((s) => s.init);
|
||||
const inited = useWorkspace((s) => s.collections.length > 0 || s.environments.length > 0);
|
||||
const collections = useWorkspace((s) => s.collections);
|
||||
const activeTabId = useWorkspace((s) => s.activeTabId);
|
||||
const dialog = useWorkspace((s) => s.dialog);
|
||||
const openTab = useWorkspace((s) => s.openTab);
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, [init]);
|
||||
|
||||
// 关闭标签页时断开实时会话
|
||||
useEffect(() => {
|
||||
useWorkspace.subscribe((state, prev) => {
|
||||
const closed = prev.openTabIds.filter((id) => !state.openTabIds.includes(id));
|
||||
for (const id of closed) closeSession(id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activeRef = activeTabId ? findNode(collections, activeTabId) : null;
|
||||
const activeNode = activeRef?.node;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen min-h-[640px] flex-col overflow-hidden">
|
||||
<TopBar />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<Sidebar />
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
<TabBar />
|
||||
{inited && activeNode === undefined && (
|
||||
<EmptyState onQuick={(kind) => collections[0] && openTab(collections[0].id)} />
|
||||
)}
|
||||
{activeNode && activeNode.type === "request" && (
|
||||
<>
|
||||
{activeNode.kind === "http" && <HttpEditor node={activeNode} />}
|
||||
{activeNode.kind === "graphql" && <GraphqlEditor node={activeNode} />}
|
||||
{activeNode.kind === "websocket" && <WebsocketEditor node={activeNode} />}
|
||||
{activeNode.kind === "sse" && <SseEditor node={activeNode} />}
|
||||
{activeNode.kind === "socketio" && <SocketIoEditor node={activeNode} />}
|
||||
{activeNode.kind === "mqtt" && <MqttEditor node={activeNode} />}
|
||||
</>
|
||||
)}
|
||||
{activeNode && activeNode.type === "folder" && (
|
||||
<EmptyState onQuick={() => undefined} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{dialog?.type === "new" && <NewItemDialog collectionId={dialog.collectionId} parentId={dialog.parentId} />}
|
||||
{dialog?.type === "import" && <ImportDialog />}
|
||||
{dialog?.type === "export" && <ExportDialog collectionId={dialog.collectionId} />}
|
||||
{dialog?.type === "environments" && <EnvironmentsDialog />}
|
||||
{dialog?.type === "settings" && <SettingsDialog />}
|
||||
{dialog?.type === "rename" && <RenameDialog id={dialog.id} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ onQuick }: { onQuick: (kind: string) => void }) {
|
||||
const setDialog = useWorkspace((s) => s.setDialog);
|
||||
void onQuick;
|
||||
return (
|
||||
<div className="grid min-h-0 flex-1 place-items-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-3 opacity-80"><Logo size={56} /></div>
|
||||
<div className="ops-dim mb-1 text-sm font-semibold">OpsxAPI · 接口工作台</div>
|
||||
<div className="ops-faint mb-5 text-xs">
|
||||
从左侧选择接口,或
|
||||
<button className="ops-link mx-1" onClick={() => setDialog({ type: "import" })}>
|
||||
导入数据
|
||||
</button>
|
||||
/
|
||||
<button
|
||||
className="ops-link ml-1"
|
||||
onClick={() =>
|
||||
setDialog({ type: "new", collectionId: useWorkspace.getState().collections[0]?.id ?? "", parentId: null })
|
||||
}
|
||||
>
|
||||
新建请求
|
||||
</button>
|
||||
</div>
|
||||
<div className="ops-faint text-[11px] leading-6">
|
||||
HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT
|
||||
<br />
|
||||
支持 OpenAPI / Postman / Apifox / cURL / HAR 等格式导入导出
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user