- 服务端(Gin + modernc.org/sqlite,data/opsxapi.db WAL): - users/tokens/teams/team_members/workspaces 表;注册登录(加盐哈希 + Bearer 令牌) - /api:auth register/login/logout、me(用户+团队+空间)、teams CRUD/邀请成员、workspaces 读写(20MB 上限,成员鉴权) - 前端: - account.ts 客户端 + store 账号态:登录/注册/登出/团队/空间切换 - 顶栏账号区:未登录「登录」按钮;登录后用户胶囊(头像+空间名+已同步/同步中/失败)+ 空间切换菜单 + ⋯(立即同步/团队管理/退出) - 空间模型:本地(localStorage)与云端(SQLite)隔离,切云端空空间自动播种本地数据;云端改动 1.5s 去抖自动同步;切回本地恢复快照 - 登录/注册弹窗 + 团队管理弹窗(建团队、邮箱邀请);vite 代理 /api;docs 新增「账号与团队」页 - 验证:curl E2E(注册/冲突 409/错误密码 401/团队共享写/越权 401)+ 浏览器全链路(注册→播种上云 17KB→云端编辑自动同步→本地快照隔离)
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
/**
|
|
* 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 { TestingView } from "./components/TestingView";
|
|
import { startScheduler, stopScheduler } from "./scheduler";
|
|
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 { AuthDialog, TeamDialog } from "./components/dialogs/AccountDialogs";
|
|
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);
|
|
const view = useWorkspace((s) => s.view);
|
|
|
|
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);
|
|
});
|
|
}, []);
|
|
|
|
// 定时任务调度器(应用运行期间常驻)
|
|
useEffect(() => {
|
|
startScheduler();
|
|
return () => stopScheduler();
|
|
}, []);
|
|
|
|
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 />
|
|
{view === "testing" ? (
|
|
<TestingView />
|
|
) : (
|
|
<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 tab={dialog.tab} />}
|
|
{dialog?.type === "rename" && <RenameDialog id={dialog.id} />}
|
|
{dialog?.type === "auth" && <AuthDialog />}
|
|
{dialog?.type === "team" && <TeamDialog />}
|
|
</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>
|
|
);
|
|
}
|