gettopic 前端开发规约
Claude 已熟悉 React 19 / TanStack / Zod;以下是 gettopic 特有规则。最全范式页:
web/src/routes/_authed/users.tsx(次选params.tsx)。
数据访问与失效
- hook 导入:
import { createConnectQueryKey, useMutation, useQuery } from "@connectrpc/connect-query"。 - method 导入路径形状:
@/gen/zerx/v1/<entity>-<Service>_connectquery,如@/gen/zerx/v1/user-UserService_connectquery(导出listUsers/createUser/updateUser/deleteUser)。注意 entity 前缀按文件名,如site-SiteSettingsService_connectquery。 - 查询:
useQuery(listUsers, { page: { page, pageSize }, keyword });无入参useQuery(listRoles)。 - 变更:
const m = useMutation(createUser); await m.mutateAsync(value);loading 用m.isPending。 - 失效(确切对象参数):
queryClient.invalidateQueries({ queryKey: createConnectQueryKey({ schema: listUsers, cardinality: "finite" }), });
坑表
| 错误写法 | 正解 |
|---|---|
cacheTime / isLoading |
react-query v5:gcTime / isPending / placeholderData |
tailwindcss-animate |
Tailwind v4 动画用 tw-animate-css |
改 tailwind.config.js / postcss.config.js |
二者不存在;主题变量在 web/src/styles.css |
z.string().email() |
Zod4 顶层格式函数 z.email() |
期望 onSubmit 拿到 transform 后值 |
Standard Schema 不 transform,拿到的是输入值;object 级 .refine/.check 仅基础字段通过后运行 |
| 拦截器数组首项先执行 | connect Interceptor 数组末尾先执行(洋葱) |
id 当 number |
uint64→bigint;显示 String(id),列/key 用 String(info.getValue()) |
用 shadcn form / react-hook-form |
表单用 @tanstack/react-form |
依赖 exactOptionalPropertyTypes |
已关闭(与 shadcn/Radix 不兼容);其余严格 flag(strict/noUncheckedIndexedAccess/noUnused*)保留 |
pnpm / npm 装前端依赖 |
包管理器是 bun(web/packageManager: bun@1.4.0)。进 web/ 再 bun install / bun run <script>(bun build 是 bun 自带打包器,不是 vite) |
表格(react-table v8)
const columnHelper = createColumnHelper<User>() → columnHelper.accessor("col", { header, cell }) / columnHelper.display({ id, header, cell }) → useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) → flexRender。
表单(react-form)
useForm({ defaultValues, validators: { onChange: zodSchema }, onSubmit: async ({ value }) => … });<form.Field name="...">;错误用 firstErrorMessage(field.state.meta.errors)(web/src/lib/form.ts);可复用字段组件用 AnyFieldApi 类型。
i18n(web/src/lib/i18n/index.tsx + locales/{en,zh}.ts)
- 结构锁:
locales/en.ts导出en,locales/zh.ts为zh: typeof en。新增文案 en/zh 同步加同名 key(缺 key 编译失败;勿用可选属性,否则结构锁失效)。 - 用法:
const { t, locale, setLocale, toggleLocale } = useI18n(); t(key, params?);缺失回退 en 再回退 key。
主题(web/src/lib/theme.tsx)
const { theme, setTheme, toggleTheme } = useTheme()。- 样式一律用语义 token(
bg-background/text-foreground等,定义于web/src/styles.css的:root/.dark,@theme inline映射),勿写死颜色。 - Toaster 主题由
__root.tsx透传useTheme()。
按钮(web/src/components/ui/button.tsx)
页级操作默认不透明白底;ghost/link 才是显式透明。
| 场景 | variant |
|---|---|
| 主操作(新建) | default(primary) |
| 次操作(导出/清理/搜索/分页/取消) | outline(bg-card 白底,禁 bg-background/bg-transparent) |
| 顶栏 icon、表格行内编辑删除 | ghost(唯一默认透明变体;须明确要透明才用) |
| 危险确认 | destructive |
禁止在 Button 上覆写 bg-background / bg-transparent(与页面灰底融为一体)。
权限显隐(纯 UX,非安全边界)
<Can code="user:create">…</Can>(组件web/src/components/can.tsx,props{ code, children })。- 判定源
web/src/lib/permissions.tsx:usePermissions().can(code)=roles.includes("admin") || codes.has(code)(roles: string[]多角色;接口PermissionContextValue{ roles, can, isLoading },isLoading=me或getUserButtons任一isPending,页面据此渲染骨架而非误判无权限;me返user.roles);数据来自me+getUserButtonsquery。 - 真正鉴权在同名 procedure 的 Casbin 策略上,见
skill://zerx-authz。
lib 速查
transport.ts:transport、authedFetch(input, init?)(401 single-flight 刷新→重试一次→失败清 token 跳登录)。query-client.ts:queryClient(staleTime 30s, gcTime 5min, retry 1)。auth.ts:getAccessToken/setTokens/clearTokens/isAuthenticated/auth。menu-icons.ts:iconByName: Record<string, LucideIcon>、menuIcon(name)(fallbackCircleIcon)。- 生成物命名:
web/src/gen/zerx/v1/<entity>_pb.ts、<entity>-<Service>_connectquery.ts、web/src/gen/buf/validate/validate_pb.ts。 - 路由:
web/vite.config.ts用tanstackRouter({ target:"react", autoCodeSplitting:true })生成并提交src/routeTree.gen.ts;404 / 错误边界在router.tsx(defaultNotFoundComponent/defaultErrorComponent→components/{not-found,error-view}.tsx,error-view按ConnectError.code区分 403 / 服务器错误)。 - CSP:后端对 SPA 下发
script-src 'self',禁止 inline<script>;主题预加载脚本在web/public/theme-init.js(index.html以src引用)。 - 构建:Vite 8 底层是 rolldown,不用
manualChunks(兼容层会递归吞依赖);分包用rollupOptions.output.codeSplitting.groups(first-match-wins):vendor(react/react-dom/scheduler/@tanstack)→connect(@connectrpc/@bufbuild)→charts(recharts + d3-*,includeDependenciesRecursively:false);sourcemap:false、target:"es2022"。 - 测试:vitest + jsdom + Testing Library(
web/vitest.config.ts,bun run test,文件src/**/*.test.{ts,tsx},显式import { … } from "vitest");现有用例lib/transport.test.ts(401 单飞刷新)、lib/permissions.test.tsx。
源码锚点
web/src/routes/_authed/users.tsx、web/src/components/ui/button.tsx、web/src/components/can.tsx、web/src/lib/{permissions.tsx,i18n.tsx,transport.ts,theme.tsx,query-client.ts,form.ts,auth.ts,menu-icons.ts}。