1
0

feat: 搭建前后端可执行程序示例

This commit is contained in:
2026-05-09 12:25:39 +08:00
commit 5b412c624d
27 changed files with 1860 additions and 0 deletions

91
src/web/app.tsx Normal file
View File

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import type { DemoResponse } from "../shared/api";
type DemoState =
| { status: "loading" }
| { status: "success"; data: DemoResponse }
| { status: "error"; message: string };
export function App() {
const [demoState, setDemoState] = useState<DemoState>({ status: "loading" });
useEffect(() => {
const abortController = new AbortController();
async function loadDemo() {
try {
const response = await fetch("/api/demo", { signal: abortController.signal });
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
const data = (await response.json()) as DemoResponse;
setDemoState({ status: "success", data });
} catch (error) {
if (abortController.signal.aborted) return;
setDemoState({
status: "error",
message: error instanceof Error ? error.message : "未知错误",
});
}
}
void loadDemo();
return () => abortController.abort();
}, []);
return (
<main className="shell">
<section className="hero" aria-labelledby="page-title">
<p className="eyebrow">Vite + React + Bun</p>
<h1 id="page-title">Gateway Checker Demo</h1>
<p className="summary">Bun API </p>
</section>
<section className="card" aria-live="polite">
<div className="card-header">
<span className="status-dot" data-state={demoState.status} />
<h2></h2>
</div>
{demoState.status === "loading" ? <p> /api/demo...</p> : null}
{demoState.status === "error" ? (
<div className="error">
<strong></strong>
<p>{demoState.message}</p>
</div>
) : null}
{demoState.status === "success" ? (
<div className="result">
<p className="message">{demoState.data.message}</p>
<dl>
<div>
<dt></dt>
<dd>{demoState.data.runtime.mode}</dd>
</div>
<div>
<dt>Bun </dt>
<dd>{demoState.data.runtime.bunVersion}</dd>
</div>
<div>
<dt></dt>
<dd>
{demoState.data.runtime.platform}/{demoState.data.runtime.arch}
</dd>
</div>
<div>
<dt></dt>
<dd>{demoState.data.runtime.timestamp}</dd>
</div>
</dl>
</div>
) : null}
</section>
</main>
);
}

13
src/web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Gateway Checker Vite React Bun executable demo" />
<title>Gateway Checker Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

16
src/web/main.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import "./styles.css";
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("找不到前端挂载节点 #root");
}
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
);

169
src/web/styles.css Normal file
View File

@@ -0,0 +1,169 @@
:root {
color: #102033;
background: #edf3f8;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
}
.shell {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 460px);
gap: 32px;
align-items: center;
min-height: 100vh;
padding: 56px;
background:
radial-gradient(circle at top left, rgba(55, 125, 255, 0.18), transparent 34rem),
linear-gradient(135deg, #f8fbff 0%, #e3edf7 100%);
}
.hero,
.card {
border: 1px solid rgba(49, 83, 126, 0.14);
border-radius: 28px;
background: rgba(255, 255, 255, 0.78);
box-shadow: 0 24px 80px rgba(34, 57, 91, 0.16);
backdrop-filter: blur(18px);
}
.hero {
padding: 48px;
}
.eyebrow {
margin: 0 0 18px;
color: #356dd2;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
max-width: 760px;
margin-bottom: 20px;
font-size: clamp(3rem, 8vw, 7rem);
line-height: 0.9;
letter-spacing: -0.08em;
}
.summary {
max-width: 620px;
margin-bottom: 0;
color: #42546c;
font-size: 1.2rem;
line-height: 1.8;
}
.card {
padding: 32px;
}
.card-header {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 24px;
}
.card-header h2 {
margin: 0;
font-size: 1.25rem;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 999px;
background: #f5a524;
box-shadow: 0 0 0 8px rgba(245, 165, 36, 0.14);
}
.status-dot[data-state="success"] {
background: #1fbf75;
box-shadow: 0 0 0 8px rgba(31, 191, 117, 0.14);
}
.status-dot[data-state="error"] {
background: #e5484d;
box-shadow: 0 0 0 8px rgba(229, 72, 77, 0.14);
}
.error {
padding: 16px;
border: 1px solid rgba(229, 72, 77, 0.25);
border-radius: 18px;
color: #9f2228;
background: rgba(255, 240, 240, 0.8);
}
.error p,
.message {
margin-bottom: 0;
}
.result {
display: grid;
gap: 24px;
}
.message {
color: #1c3f73;
font-size: 1.05rem;
font-weight: 700;
line-height: 1.6;
}
dl {
display: grid;
gap: 12px;
margin: 0;
}
dl div {
display: grid;
gap: 4px;
padding: 14px 16px;
border-radius: 16px;
background: rgba(236, 243, 252, 0.74);
}
dt {
color: #61728a;
font-size: 0.78rem;
}
dd {
margin: 0;
overflow-wrap: anywhere;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
@media (max-width: 860px) {
.shell {
grid-template-columns: 1fr;
padding: 24px;
}
.hero,
.card {
padding: 28px;
border-radius: 22px;
}
}