From 008ffc4889e8c80cd5c126bf3e1b66718dda5b64 Mon Sep 17 00:00:00 2001 From: baozaotumao Date: Tue, 2 Jun 2026 00:46:47 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20frontend=20=E6=9E=B6=E6=9E=84=E5=9F=BA?= =?UTF-8?q?=E5=BA=A7=EF=BC=88=E6=A8=AA=E5=88=87=E5=B1=82=20+=20=E6=8E=A5?= =?UTF-8?q?=E9=80=9A=20tailwind=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 横切层: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 预生成横切层,每个生成项目第一天即结构完整、能跑、过门禁。 --- template/frontend/postcss.config.js | 6 ++++ template/frontend/src/App.tsx.jinja | 9 +++-- .../frontend/src/components/ErrorBoundary.tsx | 26 +++++++++++++++ template/frontend/src/index.css | 20 ++--------- template/frontend/src/main.tsx | 7 +++- template/frontend/src/services/wsClient.ts | 33 +++++++++++++++++++ template/frontend/src/utils/logger.ts | 14 ++++++++ template/frontend/src/vite-env.d.ts.jinja | 12 +++++++ template/frontend/tailwind.config.js | 6 ++++ 9 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 template/frontend/postcss.config.js create mode 100644 template/frontend/src/components/ErrorBoundary.tsx create mode 100644 template/frontend/src/services/wsClient.ts create mode 100644 template/frontend/src/utils/logger.ts create mode 100644 template/frontend/src/vite-env.d.ts.jinja create mode 100644 template/frontend/tailwind.config.js diff --git a/template/frontend/postcss.config.js b/template/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/template/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/template/frontend/src/App.tsx.jinja b/template/frontend/src/App.tsx.jinja index fde86f4..687f8a7 100644 --- a/template/frontend/src/App.tsx.jinja +++ b/template/frontend/src/App.tsx.jinja @@ -1,8 +1,11 @@ export default function App() { return ( -
-

{{ project_name }}

-

脚手架已就绪。按 .claude/rulessrc/ 下分层实现业务。

+
+

{{ project_name }}

+

+ 脚手架已就绪。按 .claude/rules + {" "}在 src/ 下分层实现业务。 +

); } diff --git a/template/frontend/src/components/ErrorBoundary.tsx b/template/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..acbe31d --- /dev/null +++ b/template/frontend/src/components/ErrorBoundary.tsx @@ -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 { + 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
页面出错了,请刷新重试。
; + } + return this.props.children; + } +} diff --git a/template/frontend/src/index.css b/template/frontend/src/index.css index fea9d9c..b5c61c9 100644 --- a/template/frontend/src/index.css +++ b/template/frontend/src/index.css @@ -1,17 +1,3 @@ -:root { - font-family: system-ui, -apple-system, sans-serif; - line-height: 1.5; - 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; -} +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/template/frontend/src/main.tsx b/template/frontend/src/main.tsx index e6f7a53..85e6854 100644 --- a/template/frontend/src/main.tsx +++ b/template/frontend/src/main.tsx @@ -1,7 +1,9 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { Toaster } from "sonner"; import App from "./App"; +import { ErrorBoundary } from "./components/ErrorBoundary"; import "./index.css"; const root = document.getElementById("root"); @@ -9,6 +11,9 @@ if (!root) throw new Error("root element not found"); createRoot(root).render( - + + + + , ); diff --git a/template/frontend/src/services/wsClient.ts b/template/frontend/src/services/wsClient.ts new file mode 100644 index 0000000..39a9de7 --- /dev/null +++ b/template/frontend/src/services/wsClient.ts @@ -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)); +} diff --git a/template/frontend/src/utils/logger.ts b/template/frontend/src/utils/logger.ts new file mode 100644 index 0000000..98da7dc --- /dev/null +++ b/template/frontend/src/utils/logger.ts @@ -0,0 +1,14 @@ +// 统一前端日志:dev 输出 console,prod 可接 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 ?? ""); + }, +}; diff --git a/template/frontend/src/vite-env.d.ts.jinja b/template/frontend/src/vite-env.d.ts.jinja new file mode 100644 index 0000000..a663596 --- /dev/null +++ b/template/frontend/src/vite-env.d.ts.jinja @@ -0,0 +1,12 @@ +/// + +interface ImportMetaEnv { +{%- if include_backend %} + readonly VITE_BACKEND_WS_URL: string; + readonly VITE_BACKEND_HTTP_URL: string; +{%- endif %} +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/template/frontend/tailwind.config.js b/template/frontend/tailwind.config.js new file mode 100644 index 0000000..a440ac6 --- /dev/null +++ b/template/frontend/tailwind.config.js @@ -0,0 +1,6 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { extend: {} }, + plugins: [], +};