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,126 @@
|
||||
/**
|
||||
* HTTP / GraphQL 响应面板:Pretty / Raw / 响应头 + 状态条。
|
||||
*/
|
||||
import { useMemo } from "react";
|
||||
import { useWorkspace } from "../../workspaceStore";
|
||||
import { CopyButton, JsonView, cx, formatBytes } from "../common";
|
||||
|
||||
export function ResponsePanel({ nodeId }: { nodeId: string }) {
|
||||
const runtime = useWorkspace((s) => s.runtime[nodeId]);
|
||||
const setRuntime = useWorkspace((s) => s.setRuntime);
|
||||
const tab = runtime?.responseTab ?? "body";
|
||||
const result = runtime?.result ?? null;
|
||||
const error = runtime?.error ?? null;
|
||||
const loading = runtime?.loading ?? false;
|
||||
|
||||
const contentType = useMemo(() => {
|
||||
const ct = result?.headers?.["content-type"] ?? "";
|
||||
return ct.toLowerCase();
|
||||
}, [result]);
|
||||
|
||||
const isJson = contentType.includes("json") || (!contentType && looksLikeJson(result?.body ?? ""));
|
||||
const prettyBody = useMemo(() => {
|
||||
if (!result?.body) return "";
|
||||
if (isJson) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(result.body), null, 2);
|
||||
} catch {
|
||||
return result.body;
|
||||
}
|
||||
}
|
||||
return result.body;
|
||||
}, [result, isJson]);
|
||||
|
||||
return (
|
||||
<section className="flex min-h-0 flex-1 flex-col" style={{ borderTop: "1px solid var(--border)" }}>
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between px-3"
|
||||
style={{ borderBottom: "1px solid var(--border)" }}
|
||||
>
|
||||
<div className="flex">
|
||||
{(
|
||||
[
|
||||
["body", "响应体"],
|
||||
["raw", "Raw"],
|
||||
["headers", "响应头"],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
className={cx("ops-tab", tab === key && "ops-tab-active")}
|
||||
onClick={() => setRuntime(nodeId, { responseTab: key })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pr-1 text-xs">
|
||||
{result ? (
|
||||
<>
|
||||
<span>
|
||||
状态{" "}
|
||||
<b className={result.status < 400 ? "ops-success" : "ops-danger"}>{result.status}</b>
|
||||
</span>
|
||||
<span className="ops-dim">
|
||||
耗时 <b>{result.timeMs} ms</b>
|
||||
</span>
|
||||
<span className="ops-dim">
|
||||
大小 <b>{formatBytes(result.size)}</b>
|
||||
</span>
|
||||
{contentType && (
|
||||
<span className="ops-faint hidden truncate lg:inline" style={{ maxWidth: 200 }}>
|
||||
{result.headers["content-type"]}
|
||||
</span>
|
||||
)}
|
||||
<CopyButton text={result.body} label="复制体" />
|
||||
</>
|
||||
) : (
|
||||
<span className="ops-faint">Status · Time · Size</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ops-scroll min-h-0 flex-1 p-3">
|
||||
{loading && <div className="ops-dim animate-pulse py-6 text-center text-sm">请求发送中…</div>}
|
||||
{!loading && error && (
|
||||
<div
|
||||
className="ops-msg ops-msg-err mb-3 !font-sans text-[13px]"
|
||||
style={{ whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && !result && (
|
||||
<div className="ops-faint grid h-full place-items-center text-sm">
|
||||
发送请求后这里显示响应
|
||||
</div>
|
||||
)}
|
||||
{!loading && result && tab === "body" && (
|
||||
<div className="ops-code h-full overflow-auto">
|
||||
{isJson ? <JsonView text={prettyBody} /> : <pre className="m-0 whitespace-pre-wrap">{result.body}</pre>}
|
||||
</div>
|
||||
)}
|
||||
{!loading && result && tab === "raw" && (
|
||||
<pre className="ops-code m-0 h-full overflow-auto whitespace-pre-wrap break-all">{result.body}</pre>
|
||||
)}
|
||||
{!loading && result && tab === "headers" && (
|
||||
<table className="ops-code w-full border-collapse text-left">
|
||||
<tbody>
|
||||
{Object.entries(result.headers).map(([k, v]) => (
|
||||
<tr key={k} className="align-top">
|
||||
<td className="ops-accent pr-4 whitespace-nowrap">{k}</td>
|
||||
<td className="break-all">{v}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function looksLikeJson(s: string): boolean {
|
||||
const t = s.trimStart();
|
||||
return t.startsWith("{") || t.startsWith("[");
|
||||
}
|
||||
Reference in new issue
Block a user