feat: 搭建前后端可执行程序示例
This commit is contained in:
91
src/web/app.tsx
Normal file
91
src/web/app.tsx
Normal 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
13
src/web/index.html
Normal 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
16
src/web/main.tsx
Normal 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
169
src/web/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user