feat(docs): add README.md button opening shared doc drawer

Reuses the existing API.md drawer for README; switching between
docs reloads content and updates the drawer title. Backend serves
README.md via /api/docs/readme-md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-26 16:27:51 +08:00
parent c2cac19f7e
commit 45b2317ee8
7 changed files with 42 additions and 10 deletions

View File

@ -21,3 +21,20 @@ pub async fn get_api_md() -> Result<impl IntoResponse, ApiErr> {
Ok((StatusCode::OK, headers, content))
}
pub async fn get_readme_md() -> Result<impl IntoResponse, ApiErr> {
let content = tokio::fs::read_to_string("README.md")
.await
.map_err(|err| {
tracing::error!("Failed to read README.md: {}", err);
ApiErr::NotFound("README.md not found".to_string(), None)
})?;
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/markdown; charset=utf-8"),
);
Ok((StatusCode::OK, headers, content))
}

View File

@ -280,7 +280,8 @@ fn build_router(state: AppState) -> Router {
)
.route("/api/logs", get(handler::log::get_logs))
.route("/api/logs/stream", get(handler::log::stream_logs))
.route("/api/docs/api-md", get(handler::doc::get_api_md));
.route("/api/docs/api-md", get(handler::doc::get_api_md))
.route("/api/docs/readme-md", get(handler::doc::get_readme_md));
Router::new()
.merge(all_route)

View File

@ -5,6 +5,7 @@
<button type="button" class="tab-btn" id="tabConfig">配置</button>
</div>
<div class="topbar-actions">
<button type="button" class="secondary" id="openReadmeDoc">README.md</button>
<button type="button" class="secondary" id="openApiDoc">API.md</button>
<div class="status" id="statusText">
<span class="ws-dot" id="wsDot"></span>

View File

@ -1,7 +1,7 @@
import { withStatus } from "./api.js";
import { openChart, renderChart } from "./chart.js";
import { dom } from "./dom.js";
import { closeApiDocDrawer, openApiDocDrawer } from "./docs.js";
import { closeApiDocDrawer, openApiDocDrawer, openReadmeDrawer } from "./docs.js";
import { loadEvents } from "./events.js";
import {
applyBatchEquipmentUnit,
@ -130,6 +130,7 @@ function bindEvents() {
});
});
dom.openReadmeDocBtn.addEventListener("click", () => withStatus(openReadmeDrawer()));
dom.openApiDocBtn.addEventListener("click", () => withStatus(openApiDocDrawer()));
dom.closeApiDocBtn.addEventListener("click", closeApiDocDrawer);
dom.refreshEventBtn.addEventListener("click", () => withStatus(loadEvents()));

View File

@ -82,11 +82,11 @@ function parseMarkdown(text) {
return { html: blocks.join(""), headings };
}
export async function loadApiDoc() {
const text = await apiFetch("/api/docs/api-md");
async function loadDoc(url, emptyMessage) {
const text = await apiFetch(url);
const { html, headings } = parseMarkdown(text || "");
dom.apiDocContent.innerHTML = html || "<p>API.md 为空</p>";
dom.apiDocContent.innerHTML = html || `<p>${emptyMessage}</p>`;
dom.apiDocToc.innerHTML = headings.length
? headings
.map(
@ -110,14 +110,25 @@ export async function loadApiDoc() {
}
});
});
state.apiDocLoaded = true;
}
export async function openApiDocDrawer() {
const title = dom.apiDocDrawer.querySelector("h3");
if (title) title.textContent = "API.md";
dom.apiDocDrawer.classList.remove("hidden");
if (!state.apiDocLoaded) {
await loadApiDoc();
if (state.docDrawerSource !== "api") {
state.docDrawerSource = "api";
await loadDoc("/api/docs/api-md", "API.md 为空");
}
}
export async function openReadmeDrawer() {
const title = dom.apiDocDrawer.querySelector("h3");
if (title) title.textContent = "README.md";
dom.apiDocDrawer.classList.remove("hidden");
if (state.docDrawerSource !== "readme") {
state.docDrawerSource = "readme";
await loadDoc("/api/docs/readme-md", "README.md 为空");
}
}

View File

@ -81,6 +81,7 @@ export const dom = {
batchBindingSignalRole: byId("batchBindingSignalRole"),
apiDocToc: byId("apiDocToc"),
apiDocContent: byId("apiDocContent"),
openReadmeDocBtn: byId("openReadmeDoc"),
openApiDocBtn: byId("openApiDoc"),
closeApiDocBtn: byId("closeApiDoc"),
refreshChartBtn: byId("refreshChart"),

View File

@ -19,7 +19,7 @@ export const state = {
chartPointName: "",
chartData: [],
pointSocket: null,
apiDocLoaded: false,
docDrawerSource: null, // null | "api" | "readme"
runtimes: new Map(), // unit_id -> UnitRuntime
activeView: "ops", // "ops" | "config"
opsPointEls: new Map(), // point_id -> { pillEl, syncBtns? }