- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
/**
|
||
* 集合 → Markdown 接口文档。
|
||
*/
|
||
import type { Collection, CollectionNode, HttpRequestData } from "../model";
|
||
import { buildUrlWithQuery } from "./curl";
|
||
|
||
export function exportMarkdown(collection: Collection): string {
|
||
const lines: string[] = [];
|
||
lines.push(`# ${collection.name}`, "");
|
||
if (collection.description) lines.push(collection.description, "");
|
||
lines.push("> 由 OpsxAPI 导出 · " + new Date().toLocaleString("zh-CN"), "");
|
||
|
||
const toc: string[] = [];
|
||
const visitToc = (nodes: CollectionNode[], prefix: string) => {
|
||
for (const node of nodes) {
|
||
if (node.type === "folder") {
|
||
toc.push(`${prefix}- 📁 ${node.name}`);
|
||
visitToc(node.children, `${prefix} `);
|
||
} else {
|
||
toc.push(`${prefix}- [${node.name}](#${anchor(node.name)})`);
|
||
}
|
||
}
|
||
};
|
||
visitToc(collection.children, "");
|
||
if (toc.length) lines.push("## 目录", "", ...toc, "");
|
||
|
||
const visit = (nodes: CollectionNode[], depth: number) => {
|
||
for (const node of nodes) {
|
||
const hashes = "#".repeat(Math.min(depth + 1, 6));
|
||
if (node.type === "folder") {
|
||
lines.push(`${hashes} ${node.name}`, "");
|
||
if (node.description) lines.push(node.description, "");
|
||
visit(node.children, depth + 1);
|
||
continue;
|
||
}
|
||
lines.push(`${hashes} ${node.name}`, "");
|
||
if (node.description) lines.push(node.description, "");
|
||
|
||
if (node.kind === "http") {
|
||
const d: HttpRequestData = node.data;
|
||
lines.push(
|
||
"",
|
||
`- **Method**:\`${d.method}\``,
|
||
`- **URL**:\`${buildUrlWithQuery(d)}\``,
|
||
);
|
||
if (d.auth.mode !== "none") {
|
||
lines.push(`- **鉴权**:\`${d.auth.mode}\``);
|
||
}
|
||
if (d.query.filter((q) => q.enabled && q.key).length) {
|
||
lines.push("", "**Query 参数**", "", "| 参数 | 示例值 | 说明 |", "| --- | --- | --- |");
|
||
for (const q of d.query) {
|
||
if (!q.enabled || !q.key) continue;
|
||
lines.push(`| \`${q.key}\` | \`${escapeTable(q.value)}\` | ${escapeTable(q.description ?? "")} |`);
|
||
}
|
||
}
|
||
if (d.headers.filter((h) => h.enabled && h.key).length) {
|
||
lines.push("", "**Headers**", "", "| Header | 示例值 | 说明 |", "| --- | --- | --- |");
|
||
for (const h of d.headers) {
|
||
if (!h.enabled || !h.key) continue;
|
||
lines.push(`| \`${h.key}\` | \`${escapeTable(h.value)}\` | ${escapeTable(h.description ?? "")} |`);
|
||
}
|
||
}
|
||
if (d.bodyMode !== "none") {
|
||
const lang = d.bodyMode === "json" ? "json" : d.bodyMode === "xml" ? "xml" : "";
|
||
if (d.bodyMode === "urlencoded" || d.bodyMode === "form-data") {
|
||
lines.push("", `**请求体(${d.bodyMode})**`, "", "| 字段 | 示例值 |", "| --- | --- |");
|
||
for (const f of d.bodyForm) {
|
||
if (!f.enabled || !f.key) continue;
|
||
lines.push(`| \`${f.key}\` | \`${escapeTable(f.value)}\` |`);
|
||
}
|
||
} else if (d.bodyRaw) {
|
||
lines.push("", `**请求体(${d.bodyMode})**`, "", "```" + lang, d.bodyRaw, "```");
|
||
}
|
||
}
|
||
lines.push("");
|
||
} else {
|
||
const d = node.data as { url?: string; brokerUrl?: string };
|
||
const url = "url" in d ? d.url : d.brokerUrl ?? "";
|
||
lines.push("", `- **协议**:\`${node.kind}\``, `- **地址**:\`${url}\``);
|
||
lines.push("");
|
||
}
|
||
}
|
||
};
|
||
visit(collection.children, 1);
|
||
|
||
return lines.join("\n");
|
||
}
|
||
|
||
function escapeTable(s: string): string {
|
||
return s.replaceAll("|", "\\|").replaceAll("\n", " ");
|
||
}
|
||
|
||
function anchor(name: string): string {
|
||
return name.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
|
||
}
|