Files
opsxapi/packages/ui/src/App.tsx
T
logan aabd20bd75 账号与团队:SQLite 云端空间(个人 / 团队共享 + 自动同步)
- 服务端(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→云端编辑自动同步→本地快照隔离)
2026-09-18 11:42:34 +08:00

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