借鉴 Yaak 授权模型 + 复用自家 OpsXShell 生产级授权协议(opsx-licence 服务端): - Rust services/license:30 天试用自首次启动自动开始;LIC- 在线激活/周检/解绑 (双节点容灾 + 响应 Ed25519 签名校验 + 时钟回拨检测 + 30 天离线容忍); LICOFF- 离线证本地自验(产品码 opsxapi + 设备绑定 + ≤1 年期限) - 前端 src/license.ts + 活动栏授权徽章(试用 N 天/已授权/未授权)+ 设置→授权页(状态横幅/激活表单/解绑/定价链接带 ?intent= 追踪) - /pricing 独立定价页(三站同结构):30 天试用条款、在线/离线激活说明三卡、 年付↔终身切换、授权 FAQ、独立开发者声明;导航全部接入 - opsxDB 个人 ¥69/年·¥199 终身 / 团队 ¥69 / 私有化 ¥6000(与 API 定位错开) - GUI 实测:试用 30 天徽章渲染正常;浏览器态授权页正确显示仅桌面端提示
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
import { MockView } from "./features/mock/MockView";
|
|
import { DocsView } from "./features/docs/DocsView";
|
|
import { useWorkspaceLifecycle } from "./features/workspace/useWorkspaceLifecycle";
|
|
/**
|
|
* OpsxAPI 工作台入口:组装顶栏 / 侧栏 / 标签页 / 编辑器 / 对话框。
|
|
*/
|
|
import { findNode } from "./core/index";
|
|
import { useWorkspace } from "./workspaceStore";
|
|
import { useLicense } from "./license";
|
|
import { useEffect } from "react";
|
|
import { ActivityRail } from "./components/ActivityRail";
|
|
import { Sidebar } from "./components/Sidebar";
|
|
import { TestingView } from "./features/testing/TestingView";
|
|
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 { WorkspaceDialogs } from "./features/workspace/WorkspaceDialogs";
|
|
import { EmptyState } from "./features/workspace/EmptyState";
|
|
|
|
export function OpsxWorkspace() {
|
|
useWorkspaceLifecycle();
|
|
// 授权状态(桌面端):30 天试用倒计时 / 激活状态;浏览器环境自动跳过
|
|
useEffect(() => { void useLicense.getState().load(); }, []);
|
|
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 toast = useWorkspace((s) => s.toast);
|
|
const clearToast = useWorkspace((s) => s.clearToast);
|
|
const view = useWorkspace((s) => s.view);
|
|
|
|
const activeRef = activeTabId ? findNode(collections, activeTabId) : null;
|
|
const activeNode = activeRef?.node;
|
|
|
|
return (
|
|
<div className="flex h-screen flex-col overflow-hidden">
|
|
<TopBar />
|
|
<div className="flex min-h-0 min-w-0 flex-1">
|
|
<ActivityRail />
|
|
{view === "docs" ? <DocsView /> : view === "mock" ? <MockView /> : view === "testing" ? (
|
|
<div className="flex min-h-0 min-w-0 flex-1">
|
|
<TestingView />
|
|
</div>
|
|
) : (
|
|
<div className="flex min-h-0 min-w-0 flex-1">
|
|
<Sidebar />
|
|
<main className="flex min-w-0 flex-1 flex-col">
|
|
<TabBar />
|
|
{inited && activeNode === undefined && (
|
|
<EmptyState />
|
|
)}
|
|
{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 />
|
|
)}
|
|
</main>
|
|
</div>
|
|
|
|
)}
|
|
</div>
|
|
<WorkspaceDialogs />
|
|
|
|
{toast && (
|
|
<div
|
|
className="ops-toast"
|
|
role="status"
|
|
data-tone={toast.tone}
|
|
onClick={() => clearToast()}
|
|
>
|
|
{toast.message}
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|