- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
105 lines
4.1 KiB
TypeScript
105 lines
4.1 KiB
TypeScript
/**
|
|
* GraphQL 编辑器:Query / Variables / Headers / Auth + 响应。
|
|
*/
|
|
import type { GraphqlApiNode, GraphqlRequestData, KVRow } from "../../core/index";
|
|
import { useState } from "react";
|
|
import { sendGraphqlRequest } from "../../send";
|
|
import { useWorkspace } from "../../workspaceStore";
|
|
import { KVEditor, cx } from "../common";
|
|
import { Icon } from "../icons";
|
|
import { UrlPathInput } from "../UrlPathInput";
|
|
import { AuthEditor } from "./HttpEditor";
|
|
import { ResponsePanel } from "./ResponsePanel";
|
|
|
|
const TABS = ["Query", "Variables", "Headers", "Auth"] as const;
|
|
type Tab = (typeof TABS)[number];
|
|
|
|
export function GraphqlEditor({ node }: { node: GraphqlApiNode }) {
|
|
const data = node.data;
|
|
const update = useWorkspace((s) => s.updateRequestData);
|
|
const runtime = useWorkspace((s) => s.runtime[node.id]);
|
|
const [tab, setTab] = useState<Tab>("Query");
|
|
|
|
const patch = (p: Partial<GraphqlRequestData>) => update(node.id, p);
|
|
|
|
const variablesValid = (() => {
|
|
try {
|
|
JSON.parse(data.variables || "{}");
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
return (
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
<div className="flex shrink-0 flex-wrap items-center gap-2 px-3 py-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
<span className="ops-chip inline-flex items-center gap-1" style={{ padding: "2px 10px" }}>POST <Icon name="hexagon" size={12} /> GraphQL</span>
|
|
<UrlPathInput
|
|
className="ops-input flex-1 font-mono text-[13px]"
|
|
placeholder="https://api.example.com/graphql 或 /graphql"
|
|
value={data.url}
|
|
onChange={(v) => patch({ url: v })}
|
|
onKeyDown={(e) => e.key === "Enter" && sendGraphqlRequest(node)}
|
|
/>
|
|
<button
|
|
className="ops-btn ops-btn-primary min-w-[84px]"
|
|
disabled={runtime?.loading}
|
|
onClick={() => sendGraphqlRequest(node)}
|
|
>
|
|
<Icon name="play" size={13} /> {runtime?.loading ? "发送中…" : "执行"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 px-2" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
{TABS.map((t) => (
|
|
<button key={t} className={cx("ops-tab", tab === t && "ops-tab-active")} onClick={() => setTab(t)}>
|
|
{t}
|
|
{t === "Headers" && data.headers.some((h: { enabled: boolean; key: string }) => h.enabled && h.key) ? (
|
|
<span className="ops-count">{data.headers.filter((h: { enabled: boolean; key: string }) => h.enabled && h.key).length}</span>
|
|
) : null}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="ops-scroll shrink-0 p-3.5" style={{ maxHeight: "42%", minHeight: 120 }}>
|
|
{tab === "Query" && (
|
|
<textarea
|
|
className="ops-code min-h-48 w-full resize-y font-mono"
|
|
spellCheck={false}
|
|
placeholder="query Hero($id: ID!) {\n hero(id: $id) {\n name\n }\n}"
|
|
value={data.query}
|
|
onChange={(e) => patch({ query: e.target.value })}
|
|
/>
|
|
)}
|
|
{tab === "Variables" && (
|
|
<div className="space-y-2">
|
|
<textarea
|
|
className="ops-code min-h-40 w-full resize-y font-mono"
|
|
spellCheck={false}
|
|
placeholder='{\n "id": "1"\n}'
|
|
value={data.variables}
|
|
onChange={(e) => patch({ variables: e.target.value })}
|
|
style={{ borderColor: variablesValid ? undefined : "var(--danger)" }}
|
|
/>
|
|
{!variablesValid && <div className="ops-danger text-[11px]">Variables 不是合法 JSON</div>}
|
|
</div>
|
|
)}
|
|
{tab === "Headers" && (
|
|
<KVEditor rows={data.headers} onChange={(rows: KVRow[]) => patch({ headers: rows })} />
|
|
)}
|
|
{tab === "Auth" && (
|
|
<AuthEditor
|
|
data={{ ...data, method: "POST", query: [], cookies: [], bodyMode: "json", bodyRaw: "", bodyForm: [] } as never}
|
|
patch={(p: { auth?: GraphqlRequestData["auth"] }) => {
|
|
if (p.auth) patch({ auth: p.auth });
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<ResponsePanel nodeId={node.id} />
|
|
</div>
|
|
);
|
|
}
|