import { ProxySettingsPanel } from "../../features/settings/ProxySettingsPanel"; import { MockSharingPanel } from "../../features/mock/MockSharingPanel"; import { AiProviderSettings } from "../../features/settings/AiProviderSettings"; /** * 工作区对话框:新建项 / 重命名 / 环境管理 / 设置。 */ import type { RequestKind } from "../../core/index"; import { generateMock, KIND_LABELS, MOCK_CHEATSHEET, walk } from "../../core/index"; import { useState } from "react"; import { useWorkspace } from "../../workspaceStore"; import { KVEditor, Modal, cx } from "../common"; import { EnvBadge } from "../EnvBadge"; import { Icon, type IconName } from "../icons"; import { KindIcon } from "../KindIcon"; import { Logo } from "../Logo"; import { WECHAT_GROUP_URL } from "../../links"; import { useLicense } from "../../license"; import { openExternal } from "../../openExternal"; const KIND_MENU: { kind: RequestKind | "folder"; icon: IconName; desc: string }[] = [ { kind: "http", icon: "globe", desc: "REST 接口调试" }, { kind: "graphql", icon: "hexagon", desc: "Query + Variables" }, { kind: "websocket", icon: "zap", desc: "双向实时通信" }, { kind: "sse", icon: "radio", desc: "服务端推送事件流" }, { kind: "socketio", icon: "plug-zap", desc: "Socket.IO 事件收发" }, { kind: "mqtt", icon: "mailbox", desc: "IoT 消息订阅发布" }, { kind: "folder", icon: "folder-plus", desc: "接口分组目录" }, ]; export function NewItemDialog({ collectionId, parentId, }: { collectionId: string; parentId: string | null; }) { const collections = useWorkspace((s) => s.collections); const addRequest = useWorkspace((s) => s.addRequest); const addFolder = useWorkspace((s) => s.addFolder); const setDialog = useWorkspace((s) => s.setDialog); const [kind, setKind] = useState("http"); const [name, setName] = useState(""); const [targetCollection, setTargetCollection] = useState(collectionId || (collections[0]?.id ?? "")); const [targetParent, setTargetParent] = useState(parentId); const folderOptions: { id: string | null; label: string }[] = [{ id: null, label: "(集合根目录)" }]; const collection = collections.find((c) => c.id === targetCollection); if (collection) { walk(collection.children, (node) => { if (node.type === "folder") { folderOptions.push({ id: node.id, label: ` `.repeat(0) + node.name }); } }); } const create = () => { if (kind === "folder") addFolder(targetCollection, targetParent, name || undefined); else addRequest(targetCollection, targetParent, kind, name || undefined); setDialog(null); }; return ( setDialog(null)} footer={ <> } >
{KIND_MENU.map((k) => ( ))}
setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && create()} autoFocus />
); } export function RenameDialog({ id }: { id: string }) { const collections = useWorkspace((s) => s.collections); const renameNode = useWorkspace((s) => s.renameNode); const updateEnvironment = useWorkspace((s) => s.updateEnvironment); const setDialog = useWorkspace((s) => s.setDialog); const isCollection = collections.some((c) => c.id === id); const isEnv = useWorkspace((s) => s.environments.some((e) => e.id === id)); const [name, setName] = useState(() => resolveName(id, isEnv)); const submit = () => { if (isEnv) { updateEnvironment(id, { name: name.trim() || "未命名环境" }); } else { renameNode(id, name.trim() || "未命名"); } setDialog(null); }; void isCollection; return ( setDialog(null)} footer={ <> } > setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submit()} autoFocus /> ); } function resolveName(id: string, isEnv: boolean): string { const state = useWorkspace.getState(); if (isEnv) return state.environments.find((e) => e.id === id)?.name ?? ""; const collection = state.collections.find((c) => c.id === id); if (collection) return collection.name; let name = ""; for (const c of state.collections) { walk(c.children, (n) => { if (n.id === id) name = n.name; }); } return name; } export function EnvironmentsDialog() { const environments = useWorkspace((s) => s.environments); const activeEnvironmentId = useWorkspace((s) => s.activeEnvironmentId); const setActiveEnvironment = useWorkspace((s) => s.setActiveEnvironment); const addEnvironment = useWorkspace((s) => s.addEnvironment); const duplicateEnvironment = useWorkspace((s) => s.duplicateEnvironment); const removeEnvironment = useWorkspace((s) => s.removeEnvironment); const updateEnvironment = useWorkspace((s) => s.updateEnvironment); const addEnvVariable = useWorkspace((s) => s.addEnvVariable); const updateEnvVariable = useWorkspace((s) => s.updateEnvVariable); const removeEnvVariable = useWorkspace((s) => s.removeEnvVariable); const setDialog = useWorkspace((s) => s.setDialog); const [selected, setSelected] = useState(activeEnvironmentId ?? environments[0]?.id ?? ""); const [search, setSearch] = useState(""); const env = environments.find((e) => e.id === selected); const isActive = env?.id === activeEnvironmentId; const filteredVars = (env?.variables ?? []).filter((v) => { const q = search.trim().toLowerCase(); if (!q) return true; return v.key.toLowerCase().includes(q) || (v.description ?? "").toLowerCase().includes(q); }); return ( setDialog(null)} width="750px" footer={
修改自动保存并立即生效
} >
{/* 左栏:环境列表 */} {/* 右栏:编辑区 */}
{env ? ( <>
环境详情 · {isActive ? "当前使用" : "未启用"}
updateEnvironment(env.id, { name: e.target.value })} placeholder="环境名称" /> {isActive ? ( ) : ( )}
updateEnvironment(env.id, { baseUrl: e.target.value })} placeholder="https://api.example.com(请求路径以 / 开头时自动拼接)" />

相对路径(如 /graphql)会随环境切换前缀;完整 URL 不受影响。了解环境变量

变量
{env.variables.length} 项 setSearch(e.target.value)} />
{env.variables.length === 0 ? (
暂无变量,可添加 token 等请求参数
) : ( addEnvVariable(env.id)} onUpdate={(rowId, patch) => updateEnvVariable(env.id, rowId, patch)} onRemove={(rowId) => removeEnvVariable(env.id, rowId)} keyPlaceholder="变量名" valuePlaceholder="值" withDescription compact showAddButton={false} /> )} {search && filteredVars.length === 0 && env.variables.length > 0 && (
没有匹配的变量
)}

在请求中使用 {"{{变量名}}"} 引用变量,切换环境时自动替换。

) : (
{environments.length === 0 ? "左侧点击「新建环境」开始" : "选择左侧环境"}
)}
); } /* ---------------- 设置弹窗(左侧导航 + 分区) ---------------- */ type SettingsTab = "appearance" | "network" | "mock" | "mockdata" | "ai" | "data" | "license" | "about"; const SETTINGS_TABS: { id: SettingsTab; label: string; desc: string; icon: IconName }[] = [ { id: "appearance", label: "外观", desc: "主题与界面", icon: "palette" }, { id: "network", label: "网络", desc: "请求与超时", icon: "network" }, { id: "mock", label: "Mock 服务", desc: "服务开关与访问地址", icon: "flask-conical" }, { id: "ai", label: "大模型", desc: "供应商与默认模型", icon: "bot" }, { id: "data", label: "数据管理", desc: "工作区与备份", icon: "database" }, { id: "license", label: "授权", desc: "试用与许可证", icon: "shield-check" }, { id: "about", label: "关于", desc: "版本与链接", icon: "info" }, ]; function SettingRow({ label, desc, children, }: { label: string; desc?: React.ReactNode; children?: React.ReactNode; }) { return (
{label}
{desc ?
{desc}
: null}
{children}
); } function SettingGroup({ title, children }: { title?: string; children: React.ReactNode }) { return (
{title ?
{title}
: null} {children}
); } function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { return ( ); } export function SettingsDialog({ tab }: { tab?: SettingsTab }) { const settings = useWorkspace((s) => s.settings); const environments = useWorkspace((s) => s.environments); const collections = useWorkspace((s) => s.collections); const setTheme = useWorkspace((s) => s.setTheme); const setTimeoutMs = useWorkspace((s) => s.setTimeoutMs); const setMockOption = useWorkspace((s) => s.setMockOption); const updateMockRule = useWorkspace((s) => s.updateMockRule); const addMockRule = useWorkspace((s) => s.addMockRule); const removeMockRule = useWorkspace((s) => s.removeMockRule); const resetMockRules = useWorkspace((s) => s.resetMockRules); const resetWorkspace = useWorkspace((s) => s.resetWorkspace); const setDialog = useWorkspace((s) => s.setDialog); const [active, setActive] = useState(tab ?? "appearance"); const [ruleSearch, setRuleSearch] = useState(""); const [previewRule, setPreviewRule] = useState(""); const mock = settings.mock; const filteredRules = mock.rules.filter((r) => { const q = ruleSearch.trim().toLowerCase(); if (!q) return true; return ( r.pattern.toLowerCase().includes(q) || r.rule.toLowerCase().includes(q) || (r.note ?? "").toLowerCase().includes(q) ); }); const requestCount = collections.reduce( (acc, c) => acc + (c.children.filter((n) => n.type === "request").length ? 1 : 0), 0, ); let totalRequests = 0; for (const c of collections) { walk(c.children, (n) => { if (n.type === "request") totalRequests++; }); } void requestCount; const currentMeta = SETTINGS_TABS.find((t) => t.id === active) ?? { label: "请求数据生成" }; return ( setDialog(null)} width="860px" footer={} >
{active === "mockdata" && } {currentMeta.label}
{active === "appearance" && ( <>
{(["dark", "light", "system"] as const).map((t) => ( ))}
{environments.slice(0, 6).map((e) => ( ))}
)} {active === "network" && ( <> setTimeoutMs(Number(e.target.value) || 30000)} /> 自动管理 )} {active === "mock" && } {active === "mockdata" && ( <> setMockOption("enabled", v)} /> setMockOption("smart", v)} />
按从上到下的顺序匹配,先命中先用;* 通配任意字符,类型需与 JSON 字段一致。 语法速查 →
setRuleSearch(e.target.value)} /> {mock.rules.length} 条
数据类型 匹配方式 匹配规则 Mock 表达式 备注 启用
{filteredRules.map((r) => (
updateMockRule(r.id, { pattern: e.target.value })} /> updateMockRule(r.id, { rule: e.target.value })} /> updateMockRule(r.id, { note: e.target.value })} /> updateMockRule(r.id, { enabled: v })} />
))} {filteredRules.length === 0 && (
没有匹配的规则
)}
setPreviewRule(e.target.value)} /> {previewRule.trim().startsWith("@") && ( {generateMock(previewRule.trim())} )}
{MOCK_CHEATSHEET.map((c) => ( ))}
)} {active === "ai" && } {active === "data" && ( <> 浏览器本地 {collections.length} 个集合 · {totalRequests} 个接口 · {environments.length} 个环境 )} {active === "license" && } {active === "about" && ( <>

OpsxAPI · 专业接口调试工作台 · HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT

MIT License · Rust

)}
); } /* ---------------- 授权:试用状态 + 在线/离线激活 ---------------- */ function LicensePane() { const status = useLicense((s) => s.status); const busy = useLicense((s) => s.busy); const error = useLicense((s) => s.error); const activate = useLicense((s) => s.activate); const deactivate = useLicense((s) => s.deactivate); const load = useLicense((s) => s.load); const [key, setKey] = useState(""); const [email, setEmail] = useState(""); const [ok, setOk] = useState(false); const doActivate = async () => { if (!key.trim()) return; setOk(false); const done = await activate(key, email); if (done) { setKey(""); setOk(true); } }; if (!status) { return (
授权状态仅在桌面端可用;网页预览环境不涉及许可证。
); } const until = status.expiresAt ? new Date(status.expiresAt * 1000).toLocaleDateString("zh-CN") : "永久"; return (
{status.phase === "active" ? (
已授权
{status.planType ? `计划:${status.planType} · ` : ""} {status.maxDevices ? `设备数:${status.maxDevices} · ` : ""} 到期:{until} {status.email ? ` · ${status.email}` : ""}
设备 ID:{status.deviceId}
) : status.phase === "trialing" ? (
试用中 · 剩 {status.daysLeft} 天
30 天全功能试用自首次启动自动开始。试用期结束后个人非商业用途可继续使用;工作中商用需购买授权。
) : (
试用已结束
个人非商业用途可继续使用;在工作中商用请购买授权。
)} {status.phase !== "active" && (
激活许可证
{ setKey(e.target.value); setOk(false); }} onKeyDown={(e) => e.key === "Enter" && void doActivate()} /> setEmail(e.target.value)} />
LIC- 联网激活、可自助解绑;LICOFF- 完全离线激活(绑定本机)
)} {ok &&
激活成功,已授权 ✓
} {error &&
{error}
}
定价与购买
); }