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:
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
export default function App() {
|
||||
return (
|
||||
<main>
|
||||
<h1>{{ project_name }}</h1>
|
||||
<p>脚手架已就绪。按 <code>.claude/rules</code> 在 <code>src/</code> 下分层实现业务。</p>
|
||||
<main className="mx-auto max-w-2xl p-8">
|
||||
<h1 className="text-2xl font-bold">{{ project_name }}</h1>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
</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));
|
||||
}
|
||||
@@ -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 ?? "");
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
theme: { extend: {} },
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user