feat: frontend 架构基座(横切层 + 接通 tailwind)

- 横切层:utils/logger(统一日志,禁 console.log)、services/wsClient(通用 WS 客户端)、
  components/ErrorBoundary(错误边界);main.tsx 接 ErrorBoundary + Toaster
- 接通 tailwind:补 tailwind.config.js + postcss.config.js + @tailwind 指令
  (此前 tailwindcss/postcss/autoprefixer 是悬空依赖,无配置)
- vite-env.d.ts:VITE_ 环境变量类型(按 include_backend 渲染)
- App.tsx 改用 tailwind utility class(规则禁自定义 CSS)
- 验证:build/lint/typecheck 三门禁全过

至此三服务架构基座完成:copier 预生成横切层,每个生成项目第一天即结构完整、能跑、过门禁。
This commit is contained in:
baozaotumao
2026-06-02 00:46:47 -07:00
parent 5da86b9c6e
commit 008ffc4889
9 changed files with 112 additions and 21 deletions
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+6 -3
View File
@@ -1,8 +1,11 @@
export default function App() { export default function App() {
return ( return (
<main> <main className="mx-auto max-w-2xl p-8">
<h1>{{ project_name }}</h1> <h1 className="text-2xl font-bold">{{ project_name }}</h1>
<p>脚手架已就绪。按 <code>.claude/rules</code> 在 <code>src/</code> 下分层实现业务。</p> <p className="mt-2 text-gray-600">
脚手架已就绪。按 <code className="rounded bg-gray-100 px-1">.claude/rules</code>
{" "}在 <code className="rounded bg-gray-100 px-1">src/</code> 下分层实现业务。
</p>
</main> </main>
); );
} }
@@ -0,0 +1,26 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
import { logger } from "../utils/logger";
type Props = { children: ReactNode };
type State = { hasError: boolean };
// 组件级错误边界:捕获渲染异常,记录日志并展示兜底 UI。
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
logger.error("component error", { message: error.message, info });
}
render(): ReactNode {
if (this.state.hasError) {
return <div className="p-8 text-red-600"></div>;
}
return this.props.children;
}
}
+3 -17
View File
@@ -1,17 +1,3 @@
:root { @tailwind base;
font-family: system-ui, -apple-system, sans-serif; @tailwind components;
line-height: 1.5; @tailwind utilities;
color: #1a1a1a;
}
body {
margin: 0;
padding: 2rem;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: #f0f0f0;
padding: 0.1em 0.3em;
border-radius: 3px;
}
+5
View File
@@ -1,7 +1,9 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Toaster } from "sonner";
import App from "./App"; import App from "./App";
import { ErrorBoundary } from "./components/ErrorBoundary";
import "./index.css"; import "./index.css";
const root = document.getElementById("root"); const root = document.getElementById("root");
@@ -9,6 +11,9 @@ if (!root) throw new Error("root element not found");
createRoot(root).render( createRoot(root).render(
<StrictMode> <StrictMode>
<ErrorBoundary>
<App /> <App />
<Toaster />
</ErrorBoundary>
</StrictMode>, </StrictMode>,
); );
@@ -0,0 +1,33 @@
import { logger } from "../utils/logger";
export type WsHandlers = {
onMessage: (data: unknown) => void;
onOpen?: () => void;
onClose?: () => void;
};
// 通用 WebSocket 客户端:建连 + 收发 + 消息反序列化。
// 业务层在 hooks 中调用,组件不直接碰(见 .claude/rules/frontend.md 分层边界)。
export function connectWs(url: string, handlers: WsHandlers): WebSocket {
const ws = new WebSocket(url);
ws.onopen = (): void => {
logger.info("ws open", { url });
handlers.onOpen?.();
};
ws.onmessage = (e: MessageEvent): void => {
try {
handlers.onMessage(JSON.parse(e.data));
} catch (err) {
logger.warn("invalid ws message", { err });
}
};
ws.onclose = (): void => {
logger.info("ws closed");
handlers.onClose?.();
};
return ws;
}
export function sendWs(ws: WebSocket, message: object): void {
ws.send(JSON.stringify(message));
}
+14
View File
@@ -0,0 +1,14 @@
// 统一前端日志:dev 输出 consoleprod 可接 Sentry。
// 禁止在组件/hooks 直接用 console.log(见 .claude/rules/frontend-runtime.md)。
export const logger = {
info: (msg: string, ctx?: object): void => {
// eslint-disable-next-line no-console
if (import.meta.env.DEV) console.info(`[INFO] ${msg}`, ctx ?? "");
},
warn: (msg: string, ctx?: object): void => {
console.warn(`[WARN] ${msg}`, ctx ?? "");
},
error: (msg: string, ctx?: object): void => {
console.error(`[ERROR] ${msg}`, ctx ?? "");
},
};
+12
View File
@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
{%- if include_backend %}
readonly VITE_BACKEND_WS_URL: string;
readonly VITE_BACKEND_HTTP_URL: string;
{%- endif %}
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: { extend: {} },
plugins: [],
};