feat: OpsxAPI v1.0 全协议接口工作台
- 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT
- 集合树/多标签/多环境变量/历史记录,localStorage 持久化
- 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、
Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖)
- 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、
全量备份;单请求生成 cURL/fetch/Python/Go 代码
- Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、
WebSocket 隧道(浏览器自定义握手 Header)
- 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付)
- lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题
- 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全
This commit is contained in:
commit
dec66db5aa
182 files changed
+23377
No files matched your search
@@ -0,0 +1,695 @@
|
||||
/**
|
||||
* 工作台主 store:集合 / 环境 / 标签页 / 历史 / 设置 / 运行时状态。
|
||||
* 持久化:localStorage(debounce),实时会话等运行时数据不落盘。
|
||||
*/
|
||||
import type {
|
||||
Collection,
|
||||
CollectionNode,
|
||||
Environment,
|
||||
FolderNode,
|
||||
HistoryEntry,
|
||||
HttpRequestData,
|
||||
RequestKind,
|
||||
RequestNode,
|
||||
ThemeMode,
|
||||
WorkspaceData,
|
||||
WorkspaceSettings,
|
||||
} from "@opsxapi/core";
|
||||
import {
|
||||
createRequest,
|
||||
createSeedWorkspace,
|
||||
defaultRequestData,
|
||||
deepCopyNode,
|
||||
findNode,
|
||||
insertIntoCollection,
|
||||
kv,
|
||||
moveNode,
|
||||
newId,
|
||||
uniqueName,
|
||||
} from "@opsxapi/core";
|
||||
import { create } from "zustand";
|
||||
|
||||
const STORAGE_KEY = "opsxapi.workspace.v2";
|
||||
|
||||
export type LiveMessage = {
|
||||
id: string;
|
||||
dir: "in" | "out" | "system" | "error";
|
||||
at: number;
|
||||
text: string;
|
||||
/** mqtt topic / socketio event / sse event */
|
||||
topic?: string;
|
||||
};
|
||||
|
||||
export type RequestRuntime = {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
/** http/graphql 响应 */
|
||||
result: {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
body: string;
|
||||
timeMs: number;
|
||||
size: number;
|
||||
} | null;
|
||||
responseTab: "body" | "headers" | "raw";
|
||||
/** 实时协议状态 */
|
||||
status: string;
|
||||
messages: LiveMessage[];
|
||||
sent: number;
|
||||
received: number;
|
||||
startedAt: number | null;
|
||||
};
|
||||
|
||||
export type Dialog =
|
||||
| { type: "new"; collectionId: string; parentId: string | null }
|
||||
| { type: "import" }
|
||||
| { type: "export"; collectionId: string }
|
||||
| { type: "environments" }
|
||||
| { type: "settings" }
|
||||
| { type: "rename"; id: string }
|
||||
| { type: "confirm"; title: string; message: string; action: () => void };
|
||||
|
||||
export function emptyRuntime(): RequestRuntime {
|
||||
return {
|
||||
loading: false,
|
||||
error: null,
|
||||
result: null,
|
||||
responseTab: "body",
|
||||
status: "closed",
|
||||
messages: [],
|
||||
sent: 0,
|
||||
received: 0,
|
||||
startedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
type PersistedShape = Pick<
|
||||
WorkspaceData,
|
||||
"collections" | "environments" | "activeEnvironmentId" | "history" | "settings"
|
||||
>;
|
||||
|
||||
type WorkspaceStore = PersistedShape & {
|
||||
openTabIds: string[];
|
||||
activeTabId: string | null;
|
||||
search: string;
|
||||
expanded: Record<string, boolean>;
|
||||
dialog: Dialog | null;
|
||||
runtime: Record<string, RequestRuntime>;
|
||||
|
||||
init(): void;
|
||||
|
||||
addCollection(name?: string): string;
|
||||
renameNode(id: string, name: string): void;
|
||||
removeNodeById(id: string): void;
|
||||
duplicateNodeById(id: string): void;
|
||||
moveNodeTo(id: string, collectionId: string, parentId: string | null): void;
|
||||
addFolder(collectionId: string, parentId: string | null, name?: string): string;
|
||||
addRequest(collectionId: string, parentId: string | null, kind: RequestKind, name?: string): string;
|
||||
updateRequestData<K extends RequestKind>(
|
||||
id: string,
|
||||
patch: Partial<Extract<RequestNode, { kind: K }>["data"]>,
|
||||
): void;
|
||||
updateNodeDescription(id: string, description: string): void;
|
||||
importCollections(collections: Collection[], envVars: { key: string; value: string }[]): string[];
|
||||
loadWorkspaceData(data: WorkspaceData): void;
|
||||
resetWorkspace(): void;
|
||||
|
||||
openTab(id: string): void;
|
||||
closeTab(id: string): void;
|
||||
closeOtherTabs(id: string): void;
|
||||
setActiveTab(id: string | null): void;
|
||||
|
||||
setActiveEnvironment(id: string | null): void;
|
||||
addEnvironment(name?: string): string;
|
||||
duplicateEnvironment(id: string): string;
|
||||
removeEnvironment(id: string): void;
|
||||
updateEnvironment(id: string, patch: Partial<Omit<Environment, "id">>): void;
|
||||
addEnvVariable(envId: string): void;
|
||||
updateEnvVariable(envId: string, rowId: string, patch: { key?: string; value?: string; enabled?: boolean }): void;
|
||||
removeEnvVariable(envId: string, rowId: string): void;
|
||||
|
||||
setTheme(theme: ThemeMode): void;
|
||||
setTimeoutMs(ms: number): void;
|
||||
|
||||
setRuntime(id: string, patch: Partial<RequestRuntime>): void;
|
||||
pushLive(id: string, dir: LiveMessage["dir"], text: string, topic?: string): void;
|
||||
clearLive(id: string): void;
|
||||
resetRuntime(id: string): void;
|
||||
|
||||
addHistory(entry: HistoryEntry): void;
|
||||
clearHistory(): void;
|
||||
setSearch(term: string): void;
|
||||
toggleExpanded(id: string, force?: boolean): void;
|
||||
setDialog(dialog: Dialog | null): void;
|
||||
};
|
||||
|
||||
/** 自愈历史坏数据:请求节点缺 data 时按 kind 补默认值(保名称与说明) */
|
||||
function sanitizeCollections(collections: Collection[]): Collection[] {
|
||||
const fix = (nodes: CollectionNode[]): CollectionNode[] =>
|
||||
nodes.map((n) => {
|
||||
if (n.type === "folder") return { ...n, children: fix(n.children) };
|
||||
if (n.type === "request" && (!n.data || !n.kind)) {
|
||||
const kind = (n.kind ?? "http") as RequestKind;
|
||||
return {
|
||||
...n,
|
||||
kind,
|
||||
data: { ...defaultRequestData(kind) },
|
||||
} as RequestNode;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
return collections.map((c) => ({ ...c, children: fix(c.children) }));
|
||||
}
|
||||
|
||||
function loadPersisted(): WorkspaceData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const data = JSON.parse(raw) as Partial<WorkspaceData>;
|
||||
if (!Array.isArray(data.collections) || !Array.isArray(data.environments)) return null;
|
||||
return {
|
||||
version: 2,
|
||||
collections: sanitizeCollections(data.collections),
|
||||
environments: data.environments,
|
||||
activeEnvironmentId: data.activeEnvironmentId ?? data.environments[0]?.id ?? null,
|
||||
history: Array.isArray(data.history) ? data.history : [],
|
||||
settings: {
|
||||
theme: data.settings?.theme === "light" ? "light" : "dark",
|
||||
timeoutMs: data.settings?.timeoutMs ?? 30000,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function schedulePersist(state: WorkspaceStore): void {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
try {
|
||||
const data: PersistedShape & { version: number } = {
|
||||
version: 2,
|
||||
collections: state.collections,
|
||||
environments: state.environments,
|
||||
activeEnvironmentId: state.activeEnvironmentId,
|
||||
history: state.history.slice(0, 100),
|
||||
settings: state.settings,
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.warn("[opsxapi] 工作区保存失败", e);
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
export function applyThemeToDocument(theme: ThemeMode): void {
|
||||
if (typeof document === "undefined") return;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}
|
||||
|
||||
export const useWorkspace = create<WorkspaceStore>((set, get) => {
|
||||
const persist = () => schedulePersist(get());
|
||||
|
||||
return {
|
||||
collections: [],
|
||||
environments: [],
|
||||
activeEnvironmentId: null,
|
||||
history: [],
|
||||
settings: { theme: "dark", timeoutMs: 30000 },
|
||||
|
||||
openTabIds: [],
|
||||
activeTabId: null,
|
||||
search: "",
|
||||
expanded: {},
|
||||
dialog: null,
|
||||
|
||||
init() {
|
||||
const data = loadPersisted() ?? createSeedWorkspace();
|
||||
set({
|
||||
collections: data.collections,
|
||||
environments: data.environments,
|
||||
activeEnvironmentId: data.activeEnvironmentId,
|
||||
history: data.history,
|
||||
settings: data.settings,
|
||||
});
|
||||
applyThemeToDocument(data.settings.theme);
|
||||
// 展开全部目录
|
||||
const expanded: Record<string, boolean> = {};
|
||||
for (const c of data.collections) {
|
||||
walkFolders(c.children, (f) => {
|
||||
expanded[f.id] = true;
|
||||
});
|
||||
}
|
||||
set({ expanded });
|
||||
},
|
||||
|
||||
addCollection(name) {
|
||||
const collection: Collection = {
|
||||
id: newId(),
|
||||
type: "collection",
|
||||
name: name || "新建集合",
|
||||
children: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
set({ collections: [...get().collections, collection] });
|
||||
persist();
|
||||
return collection.id;
|
||||
},
|
||||
|
||||
renameNode(id, name) {
|
||||
set({
|
||||
collections: get().collections.map((c) =>
|
||||
c.id === id
|
||||
? { ...c, name, updatedAt: Date.now() }
|
||||
: renameIn(c, id, name),
|
||||
),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
removeNodeById(id) {
|
||||
const state = get();
|
||||
const openTabIds = state.openTabIds.filter((t) => t !== id);
|
||||
const activeTabId =
|
||||
state.activeTabId === id ? (openTabIds[openTabIds.length - 1] ?? null) : state.activeTabId;
|
||||
const collections = state.collections.map((c) => removeFrom(c, id));
|
||||
set({ collections, openTabIds, activeTabId });
|
||||
persist();
|
||||
},
|
||||
|
||||
duplicateNodeById(id) {
|
||||
const state = get();
|
||||
const ref = findNode(state.collections, id);
|
||||
if (!ref) return;
|
||||
const copy = deepCopyNode(ref.node, (name) => `${name} 副本`);
|
||||
const collections = insertIntoCollection(
|
||||
state.collections,
|
||||
ref.collection.id,
|
||||
ref.parent?.id ?? null,
|
||||
copy,
|
||||
);
|
||||
set({ collections });
|
||||
if (copy.type === "request") get().openTab(copy.id);
|
||||
persist();
|
||||
},
|
||||
|
||||
moveNodeTo(id, collectionId, parentId) {
|
||||
set({ collections: moveNode(get().collections, id, collectionId, parentId) });
|
||||
persist();
|
||||
},
|
||||
|
||||
addFolder(collectionId, parentId, name) {
|
||||
const state = get();
|
||||
const siblings = parentChildren(state.collections, collectionId, parentId);
|
||||
const folder: FolderNode = {
|
||||
id: newId(),
|
||||
type: "folder",
|
||||
name: uniqueName(siblings, name ?? "新建目录"),
|
||||
children: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
set({
|
||||
collections: insertIntoCollection(state.collections, collectionId, parentId, folder),
|
||||
expanded: { ...state.expanded, [folder.id]: true },
|
||||
});
|
||||
persist();
|
||||
return folder.id;
|
||||
},
|
||||
|
||||
addRequest(collectionId, parentId, kind, name) {
|
||||
const state = get();
|
||||
const siblings = parentChildren(state.collections, collectionId, parentId);
|
||||
const node: RequestNode = createRequest(kind, uniqueName(siblings, name ?? defaultNames(kind)));
|
||||
set({ collections: insertIntoCollection(state.collections, collectionId, parentId, node) });
|
||||
get().openTab(node.id);
|
||||
persist();
|
||||
return node.id;
|
||||
},
|
||||
|
||||
updateRequestData(id, patch) {
|
||||
set({
|
||||
collections: get().collections.map((c) => patchRequestIn(c, id, patch)),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
updateNodeDescription(id, description) {
|
||||
set({
|
||||
collections: get().collections.map((c) => descRequestIn(c, id, description)),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
importCollections(collections, envVars) {
|
||||
const state = get();
|
||||
const existingNames = new Set(state.collections.map((c) => c.name));
|
||||
const renamed = collections.map((c) => {
|
||||
if (!existingNames.has(c.name)) return c;
|
||||
let i = 2;
|
||||
while (existingNames.has(`${c.name} ${i}`)) i++;
|
||||
return { ...c, name: `${c.name} ${i}` };
|
||||
});
|
||||
// 环境变量并入当前环境
|
||||
let environments = state.environments;
|
||||
let activeEnvironmentId = state.activeEnvironmentId;
|
||||
if (envVars.length) {
|
||||
const active = environments.find((e) => e.id === activeEnvironmentId) ?? environments[0];
|
||||
if (active) {
|
||||
const known = new Set(active.variables.map((v) => v.key));
|
||||
for (const v of envVars) {
|
||||
if (!known.has(v.key)) {
|
||||
active.variables.push(kv(v.key, v.value));
|
||||
known.add(v.key);
|
||||
}
|
||||
}
|
||||
environments = environments.map((e) => (e.id === active.id ? { ...active } : e));
|
||||
} else {
|
||||
const env: Environment = {
|
||||
id: newId(),
|
||||
name: "默认环境",
|
||||
variables: envVars.map((v) => kv(v.key, v.value)),
|
||||
};
|
||||
environments = [...environments, env];
|
||||
activeEnvironmentId = env.id;
|
||||
}
|
||||
}
|
||||
set({ collections: [...state.collections, ...renamed], environments, activeEnvironmentId });
|
||||
persist();
|
||||
return renamed.map((c) => c.id);
|
||||
},
|
||||
|
||||
loadWorkspaceData(data) {
|
||||
set({
|
||||
collections: data.collections,
|
||||
environments: data.environments,
|
||||
activeEnvironmentId: data.activeEnvironmentId ?? data.environments[0]?.id ?? null,
|
||||
history: data.history ?? [],
|
||||
settings: data.settings,
|
||||
openTabIds: [],
|
||||
activeTabId: null,
|
||||
});
|
||||
applyThemeToDocument(data.settings.theme);
|
||||
persist();
|
||||
},
|
||||
|
||||
resetWorkspace() {
|
||||
const seed = createSeedWorkspace();
|
||||
get().loadWorkspaceData(seed);
|
||||
},
|
||||
|
||||
openTab(id) {
|
||||
const state = get();
|
||||
if (state.openTabIds.includes(id)) {
|
||||
set({ activeTabId: id });
|
||||
return;
|
||||
}
|
||||
set({ openTabIds: [...state.openTabIds, id], activeTabId: id });
|
||||
},
|
||||
|
||||
closeTab(id) {
|
||||
const state = get();
|
||||
const openTabIds = state.openTabIds.filter((t) => t !== id);
|
||||
set({
|
||||
openTabIds,
|
||||
activeTabId:
|
||||
state.activeTabId === id ? (openTabIds[openTabIds.length - 1] ?? null) : state.activeTabId,
|
||||
});
|
||||
},
|
||||
|
||||
closeOtherTabs(id) {
|
||||
set({ openTabIds: [id], activeTabId: id });
|
||||
},
|
||||
|
||||
setActiveTab(id) {
|
||||
set({ activeTabId: id });
|
||||
},
|
||||
|
||||
setActiveEnvironment(id) {
|
||||
set({ activeEnvironmentId: id });
|
||||
persist();
|
||||
},
|
||||
|
||||
addEnvironment(name) {
|
||||
const env: Environment = {
|
||||
id: newId(),
|
||||
name: name || `环境 ${get().environments.length + 1}`,
|
||||
variables: [],
|
||||
};
|
||||
set({ environments: [...get().environments, env] });
|
||||
persist();
|
||||
return env.id;
|
||||
},
|
||||
|
||||
duplicateEnvironment(id) {
|
||||
const state = get();
|
||||
const source = state.environments.find((e) => e.id === id);
|
||||
if (!source) return "";
|
||||
let name = `${source.name} 副本`;
|
||||
let i = 2;
|
||||
const names = new Set(state.environments.map((e) => e.name));
|
||||
while (names.has(name)) name = `${source.name} 副本 ${i++}`;
|
||||
const copy: Environment = {
|
||||
id: newId(),
|
||||
name,
|
||||
variables: source.variables.map((v) => kv(v.key, v.value, v.enabled, v.description)),
|
||||
};
|
||||
set({ environments: [...state.environments, copy] });
|
||||
persist();
|
||||
return copy.id;
|
||||
},
|
||||
|
||||
removeEnvironment(id) {
|
||||
const state = get();
|
||||
const environments = state.environments.filter((e) => e.id !== id);
|
||||
set({
|
||||
environments,
|
||||
activeEnvironmentId:
|
||||
state.activeEnvironmentId === id ? (environments[0]?.id ?? null) : state.activeEnvironmentId,
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
updateEnvironment(id, patch) {
|
||||
set({
|
||||
environments: get().environments.map((e) => (e.id === id ? { ...e, ...patch } : e)),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
addEnvVariable(envId) {
|
||||
set({
|
||||
environments: get().environments.map((e) =>
|
||||
e.id === envId ? { ...e, variables: [...e.variables, kv()] } : e,
|
||||
),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
updateEnvVariable(envId, rowId, patch) {
|
||||
set({
|
||||
environments: get().environments.map((e) =>
|
||||
e.id === envId
|
||||
? {
|
||||
...e,
|
||||
variables: e.variables.map((v) => (v.id === rowId ? { ...v, ...patch } : v)),
|
||||
}
|
||||
: e,
|
||||
),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
removeEnvVariable(envId, rowId) {
|
||||
set({
|
||||
environments: get().environments.map((e) =>
|
||||
e.id === envId ? { ...e, variables: e.variables.filter((v) => v.id !== rowId) } : e,
|
||||
),
|
||||
});
|
||||
persist();
|
||||
},
|
||||
|
||||
setTheme(theme) {
|
||||
set({ settings: { ...get().settings, theme } });
|
||||
applyThemeToDocument(theme);
|
||||
persist();
|
||||
},
|
||||
|
||||
setTimeoutMs(ms) {
|
||||
set({ settings: { ...get().settings, timeoutMs: ms } });
|
||||
persist();
|
||||
},
|
||||
|
||||
setRuntime(id, patch) {
|
||||
const state = get();
|
||||
set({
|
||||
runtime: {
|
||||
...state.runtime,
|
||||
[id]: { ...(state.runtime[id] ?? emptyRuntime()), ...patch },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
pushLive(id, dir, text, topic) {
|
||||
const state = get();
|
||||
const prev = state.runtime[id] ?? emptyRuntime();
|
||||
const msg: LiveMessage = { id: newId(), dir, at: Date.now(), text, topic };
|
||||
const messages = [...prev.messages, msg].slice(-500);
|
||||
set({
|
||||
runtime: {
|
||||
...state.runtime,
|
||||
[id]: {
|
||||
...prev,
|
||||
messages,
|
||||
sent: dir === "out" ? prev.sent + 1 : prev.sent,
|
||||
received: dir === "in" ? prev.received + 1 : prev.received,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
clearLive(id) {
|
||||
const state = get();
|
||||
const prev = state.runtime[id] ?? emptyRuntime();
|
||||
set({ runtime: { ...state.runtime, [id]: { ...prev, messages: [] } } });
|
||||
},
|
||||
|
||||
resetRuntime(id) {
|
||||
const state = get();
|
||||
set({ runtime: { ...state.runtime, [id]: emptyRuntime() } });
|
||||
},
|
||||
|
||||
addHistory(entry) {
|
||||
set({ history: [entry, ...get().history].slice(0, 100) });
|
||||
persist();
|
||||
},
|
||||
|
||||
clearHistory() {
|
||||
set({ history: [] });
|
||||
persist();
|
||||
},
|
||||
|
||||
setSearch(term) {
|
||||
set({ search: term });
|
||||
},
|
||||
|
||||
toggleExpanded(id, force) {
|
||||
const expanded = { ...get().expanded };
|
||||
expanded[id] = force ?? !expanded[id];
|
||||
set({ expanded });
|
||||
},
|
||||
|
||||
setDialog(dialog) {
|
||||
set({ dialog });
|
||||
},
|
||||
|
||||
runtime: {} as Record<string, RequestRuntime>,
|
||||
};
|
||||
});
|
||||
|
||||
// ---------- 纯帮助函数 ----------
|
||||
|
||||
function walkFolders(nodes: CollectionNode[], fn: (f: FolderNode) => void): void {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "folder") {
|
||||
fn(node);
|
||||
walkFolders(node.children, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renameIn(collection: Collection, id: string, name: string): Collection {
|
||||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||||
nodes.map((n) => {
|
||||
if (n.id === id) return { ...n, name, updatedAt: Date.now() };
|
||||
if (n.type === "folder") return { ...n, children: visit(n.children) };
|
||||
return n;
|
||||
});
|
||||
return { ...collection, children: visit(collection.children) };
|
||||
}
|
||||
|
||||
function removeFrom(collection: Collection, id: string): Collection {
|
||||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||||
nodes
|
||||
.filter((n) => n.id !== id)
|
||||
.map((n) => (n.type === "folder" ? { ...n, children: visit(n.children) } : n));
|
||||
return { ...collection, children: visit(collection.children) };
|
||||
}
|
||||
|
||||
function patchRequestIn(
|
||||
collection: Collection,
|
||||
id: string,
|
||||
patch: Record<string, unknown>,
|
||||
): Collection {
|
||||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||||
nodes.map((n) => {
|
||||
if (n.id === id && n.type === "request") {
|
||||
const merged = { ...n.data, ...patch } as RequestNode["data"];
|
||||
return { ...n, data: merged, updatedAt: Date.now() } as CollectionNode;
|
||||
}
|
||||
if (n.type === "folder") return { ...n, children: visit(n.children) };
|
||||
return n;
|
||||
});
|
||||
return { ...collection, children: visit(collection.children) };
|
||||
}
|
||||
|
||||
function descRequestIn(collection: Collection, id: string, description: string): Collection {
|
||||
const visit = (nodes: CollectionNode[]): CollectionNode[] =>
|
||||
nodes.map((n) => {
|
||||
if (n.id === id) return { ...n, description: description || undefined, updatedAt: Date.now() };
|
||||
if (n.type === "folder") return { ...n, children: visit(n.children) };
|
||||
return n;
|
||||
});
|
||||
return { ...collection, children: visit(collection.children) };
|
||||
}
|
||||
|
||||
function parentChildren(
|
||||
collections: Collection[],
|
||||
collectionId: string,
|
||||
parentId: string | null,
|
||||
): CollectionNode[] {
|
||||
const collection = collections.find((c) => c.id === collectionId);
|
||||
if (!collection) return [];
|
||||
if (parentId === null) return collection.children;
|
||||
let foundChildren: CollectionNode[] | null = null;
|
||||
const visit = (nodes: CollectionNode[]) => {
|
||||
for (const n of nodes) {
|
||||
if (n.id === parentId) {
|
||||
if (n.type === "folder") foundChildren = n.children;
|
||||
return;
|
||||
}
|
||||
if (n.type === "folder") visit(n.children);
|
||||
}
|
||||
};
|
||||
visit(collection.children);
|
||||
return foundChildren ?? collection.children;
|
||||
}
|
||||
|
||||
function defaultNames(kind: RequestKind): string {
|
||||
const labels: Record<RequestKind, string> = {
|
||||
http: "新建 HTTP 接口",
|
||||
graphql: "新建 GraphQL",
|
||||
websocket: "新建 WebSocket",
|
||||
sse: "新建 SSE",
|
||||
socketio: "新建 Socket.IO",
|
||||
mqtt: "新建 MQTT",
|
||||
};
|
||||
return labels[kind];
|
||||
}
|
||||
|
||||
/** 便捷选择器:当前环境变量表 */
|
||||
export function activeEnvMap(): Record<string, string> {
|
||||
const state = useWorkspace.getState();
|
||||
const active = state.environments.find((e) => e.id === state.activeEnvironmentId);
|
||||
const out: Record<string, string> = {};
|
||||
for (const v of active?.variables ?? []) {
|
||||
if (v.enabled && v.key) out[v.key] = v.value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 便捷选择器:请求体构建辅助(http/graphql 共用) */
|
||||
export type { HttpRequestData };
|
||||
Reference in new issue
Block a user