Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/open-portal/src/PublicSite.tsx
T

194 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>指定 1200 / 留空查全部</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>
);
}