Files
opsxapi/src/App.tsx
T
logan c304b9f251 授权体系落地:30 天试用 + LIC- 在线 / LICOFF- 离线激活 + /pricing 定价页
借鉴 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 天徽章渲染正常;浏览器态授权页正确显示仅桌面端提示
2026-10-16 03:27:45 +08:00

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