194 lines
6.5 KiB
TypeScript
194 lines
6.5 KiB
TypeScript
import { useState } from "react";
|
||
import { DocumentationPage } from "./Documentation";
|
||
import { Brand, Icon, navigate, type Route } from "./ui";
|
||
|
||
export function PublicSite({ route }: { route: Exclude<Route, "console"> }) {
|
||
return (
|
||
<div className="public-site">
|
||
<PublicHeader route={route} />
|
||
{route === "docs" ? <DocumentationPage /> : <HomePage />}
|
||
<PublicFooter />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function scrollToSection(id: string) {
|
||
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
|
||
function PublicHeader({ route }: { route: Exclude<Route, "console"> }) {
|
||
return (
|
||
<header className="public-header">
|
||
<Brand />
|
||
<nav aria-label="主导航">
|
||
<button onClick={() => route === "home" ? scrollToSection("apis") : navigate("home")}>API</button>
|
||
<button className={route === "docs" ? "active" : ""} onClick={() => navigate("docs")}>文档</button>
|
||
</nav>
|
||
<button className="header-console" onClick={() => navigate("console")}>控制台 <Icon name="arrow" size={14} /></button>
|
||
</header>
|
||
);
|
||
}
|
||
|
||
function HomePage() {
|
||
return (
|
||
<main className="home-v2">
|
||
<section className="home-hero">
|
||
<div className="home-hero-copy">
|
||
<h1>车辆数据,按需开放。</h1>
|
||
<p>用统一 API 访问车辆日用氢量与日里程。每次调用受应用、车辆和日期范围约束。</p>
|
||
<div className="home-actions">
|
||
<button className="button home-primary" onClick={() => navigate("docs")}>阅读文档</button>
|
||
<button className="home-text-link" onClick={() => navigate("console")}>进入控制台 <Icon name="arrow" size={16} /></button>
|
||
</div>
|
||
</div>
|
||
<ApiWorkbench />
|
||
</section>
|
||
|
||
<section className="fact-rail" aria-label="接口事实">
|
||
<span><b>2</b> 项数据能力</span>
|
||
<span>VIN / 车牌授权</span>
|
||
<span>指定 1–200 辆 / 留空查全部</span>
|
||
<span>Trace ID</span>
|
||
</section>
|
||
|
||
<section className="api-directory" id="apis">
|
||
<header className="home-section-heading">
|
||
<span>01</span>
|
||
<h2>可用接口</h2>
|
||
</header>
|
||
<ApiRow mark="H₂" name="车辆日用氢量" path="/api/v1/vehicles/hydrogen-consumption/query" unit="kg" />
|
||
<ApiRow mark="KM" name="车辆日里程" path="/api/v1/vehicles/mileage/query" unit="km" />
|
||
</section>
|
||
|
||
<section className="integration-section">
|
||
<header className="home-section-heading">
|
||
<span>02</span>
|
||
<h2>开始接入</h2>
|
||
</header>
|
||
<div className="integration-line">
|
||
{[
|
||
["01", "获取 AppKey", "创建开放应用并保存一次性展示的 AppKey。"],
|
||
["02", "授权车辆", "按 VIN 或车牌配置车辆和有效期。"],
|
||
["03", "调用 API", "按自然日查询数据,并记录 Trace ID。"]
|
||
].map(([number, title, body]) => (
|
||
<article key={number}>
|
||
<span>{number}</span>
|
||
<div><h3>{title}</h3><p>{body}</p></div>
|
||
</article>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="docs-call-to-action">
|
||
<div>
|
||
<Icon name="book" size={34} />
|
||
<span><h2>查看完整接口文档</h2><p>请求参数、响应结构、错误码与重试策略。</p></span>
|
||
</div>
|
||
<button onClick={() => navigate("docs")}>打开文档 <Icon name="arrow" size={17} /></button>
|
||
</section>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
type ApiKind = "mileage" | "hydrogen";
|
||
|
||
const apiExamples: Record<ApiKind, {
|
||
tab: string;
|
||
path: string;
|
||
responseField: string;
|
||
value: string;
|
||
extraResponse?: string;
|
||
}> = {
|
||
mileage: {
|
||
tab: "日里程",
|
||
path: "/api/v1/vehicles/mileage/query",
|
||
responseField: "dailyMileageKm",
|
||
value: "215.6",
|
||
extraResponse: `\n "totalMileageKm": 12345.6,\n "dataTime": "2026-07-19T23:58:45+08:00",\n "updatedAt": "2026-07-20T05:10:00+08:00",`
|
||
},
|
||
hydrogen: {
|
||
tab: "日用氢量",
|
||
path: "/api/v1/vehicles/hydrogen-consumption/query",
|
||
responseField: "hydrogenConsumptionKg",
|
||
value: "8.42"
|
||
}
|
||
};
|
||
|
||
function ApiWorkbench() {
|
||
const [kind, setKind] = useState<ApiKind>("mileage");
|
||
const current = apiExamples[kind];
|
||
return (
|
||
<div className="api-workbench">
|
||
<div className="workbench-tabs" role="tablist" aria-label="接口示例">
|
||
{(Object.keys(apiExamples) as ApiKind[]).map((item) => (
|
||
<button
|
||
key={item}
|
||
role="tab"
|
||
aria-selected={kind === item}
|
||
className={kind === item ? "active" : ""}
|
||
onClick={() => setKind(item)}
|
||
>
|
||
{apiExamples[item].tab}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="workbench-grid">
|
||
<div className="workbench-request">
|
||
<header><b>POST</b><code>{current.path}</code></header>
|
||
<div className="workbench-pane-title">请求</div>
|
||
<label>
|
||
<span>plateNumbers</span>
|
||
<code>["辽A00001", "辽A00002"]</code>
|
||
</label>
|
||
<label>
|
||
<span>date</span>
|
||
<code>"2026-07-19"</code>
|
||
</label>
|
||
</div>
|
||
<div className="workbench-response">
|
||
<header><span>响应</span><b>200 OK</b></header>
|
||
<pre aria-label={`${current.tab}响应示例`}><code>{`{
|
||
"code": "SUCCESS",
|
||
"data": [{
|
||
"plateNumber": "辽A00001",
|
||
"date": "2026-07-19",
|
||
"${current.responseField}": ${current.value},
|
||
${current.extraResponse || ""}
|
||
"status": "NORMAL"
|
||
}],
|
||
"traceId": "b7ff5582ab1a4e13"
|
||
}`}</code></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ApiRow({ mark, name, path, unit }: { mark: string; name: string; path: string; unit: string }) {
|
||
return (
|
||
<article className="api-directory-row">
|
||
<span className="api-mark">{mark}</span>
|
||
<div className="api-row-main">
|
||
<h3>{name}</h3>
|
||
<p><b>POST</b><code>{path}</code></p>
|
||
</div>
|
||
<div className="api-unit"><small>单位</small><span>{unit}</span></div>
|
||
<button onClick={() => navigate("docs")}>接口说明 <Icon name="arrow" size={17} /></button>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
function PublicFooter() {
|
||
return (
|
||
<footer className="public-footer">
|
||
<Brand compact />
|
||
<nav aria-label="页脚导航">
|
||
<button onClick={() => scrollToSection("apis")}>API</button>
|
||
<button onClick={() => navigate("docs")}>文档</button>
|
||
<button onClick={() => navigate("console")}>控制台</button>
|
||
</nav>
|
||
<span>© 2026 Lingniu Technology</span>
|
||
</footer>
|
||
);
|
||
}
|