- icon.svg 取自 opsx-icon-options/api/B-brackets.svg,全套光栅重生成: icon.icns(iconutil)、icon.ico(PNG-in-ICO 6 尺寸)、16-1024 PNG、Windows Square*/StoreLogo - 替换 Tauri 脚手架默认五角星图标;GUI 重启实测 Dock 图标已生效 - 服务全量重启:server 8787(官网/文档 200)+ web 5173 + 桌面 GUI
128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
/**
|
|
* HTTP / GraphQL 响应面板:Pretty / Raw / 响应头 + 状态条。
|
|
*/
|
|
import { useMemo } from "react";
|
|
import { useWorkspace } from "../../workspaceStore";
|
|
import { CopyButton, JsonView, cx, formatBytes } from "../common";
|
|
import { ProxyErrorBanner } from "../ProxyErrorBanner";
|
|
|
|
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 className={cx(
|
|
"ops-status-pill",
|
|
result.status >= 500 ? "ops-status-5xx" : result.status >= 400 ? "ops-status-4xx" : result.status >= 300 ? "ops-status-3xx" : "ops-status-2xx",
|
|
)}>
|
|
{result.status}
|
|
</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 && (
|
|
<ProxyErrorBanner
|
|
message={error}
|
|
onDismiss={() => setRuntime(nodeId, { error: null })}
|
|
/>
|
|
)}
|
|
{!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("[");
|
|
}
|