# apps/basic 模板实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在 monorepo 中新增 `apps/basic`,由 `apps/with-lingui` 拷贝并去除 Lingui 与语言切换,界面为英文硬编码,Ant Design 固定 `en_US`。 **Architecture:** 目录级「拷贝后减法」;不抽 shared package。`settings` 持久化使用独立 storage key;`package.json` 使用唯一 `name`(如 `antd-admin-basic`)。 **Tech Stack:** 与 `with-lingui` 相同(Vite+、React 19、TanStack Router/Query、Ant Design 6、Zustand、MSW、Playwright),不包含任何 `@lingui/*`。 --- ## 文件映射(创建 / 修改 / 删除) | 路径 | 操作 | |------|------| | `apps/basic/**` | 自 `apps/with-lingui` 拷贝生成(排除 `node_modules`、`dist`、`.turbo`、coverage 等产物) | | `apps/basic/lingui.config.ts` | 删除 | | `apps/basic/src/lingui.d.ts` | 删除 | | `apps/basic/src/locales/**` | 删除整个目录 | | `apps/basic/package.json` | 修改:`name`、依赖、脚本 | | `apps/basic/vite.config.ts` | 修改:去掉 Lingui 插件 | | `apps/basic/src/main.tsx` | 修改:去掉 i18n 启动 | | `apps/basic/src/routes/__root.tsx` | 修改:固定 `enUS`,去掉 `I18nProvider` | | `apps/basic/src/stores/settings.ts` | 修改:去掉 locale,改 persist `name` | | `apps/basic/src/components/Layout/Sidebar/index.tsx` | 修改:`MENU_LABELS` 与 `buildMenuItems` 签名 | | `apps/basic/src/components/Layout/Header/index.tsx` | 修改:去掉语言按钮与 Lingui | | `apps/basic/src/routes/login/index.tsx` | 修改:去掉语言按钮与 Lingui | | `apps/basic/src/components/Layout/AppFooter/index.tsx` 等 8+ 处路由/组件 | 修改:见各 Task | | `pnpm-lock.yaml`(仓库根) | 修改:`pnpm install` 后更新 | | `README.md`(仓库根) | 修改:Templates 说明 | | `apps/basic/README.md` | 修改或新增:英文-only 说明 | --- ### Task 1: 拷贝应用骨架 **Files:** - Create: `apps/basic/**`(整树,来自 with-lingui) - [ ] **Step 1: 从 with-lingui 拷贝到 basic(排除产物)** 在仓库根目录执行: ```bash cd /Users/zuiidea/web/antd-admin rsync -a --delete \ --exclude node_modules --exclude dist --exclude .turbo --exclude coverage \ --exclude ".vite-hooks" \ apps/with-lingui/ apps/basic/ ``` 若无 `rsync`,可用: ```bash cp -R apps/with-lingui apps/basic rm -rf apps/basic/node_modules apps/basic/dist apps/basic/.turbo ``` - [ ] **Step 2: 确认 `apps/basic` 存在且不含 `node_modules`** ```bash test -f apps/basic/package.json && test ! -d apps/basic/node_modules && echo OK ``` Expected: 输出 `OK` - [ ] **Step 3: Commit** ```bash git add apps/basic git commit -m "chore: scaffold apps/basic from with-lingui" ``` (若 pre-commit 失败且仅因 hook 配置,可按团队约定使用 `--no-verify`。) --- ### Task 2: package.json 与锁文件 **Files:** - Modify: `apps/basic/package.json` - Modify: `pnpm-lock.yaml`(根目录) - [ ] **Step 1: 编辑 `apps/basic/package.json`** 将 `name` 改为 `"antd-admin-basic"`。 从 `scripts` 中**删除**这两行: ```json "i18n:extract": "vp exec lingui extract", "i18n:compile": "vp exec lingui compile" ``` 从 `dependencies` 中**删除**: ```json "@lingui/core": "^5.9.3", "@lingui/react": "^5.9.3", ``` 从 `devDependencies` 中**删除**: ```json "@lingui/cli": "^5.9.3", "@lingui/swc-plugin": "^5.11.0", "@lingui/vite-plugin": "^5.9.3", ``` 其余字段保持与源应用一致。 - [ ] **Step 2: 安装依赖并更新 lockfile** ```bash cd /Users/zuiidea/web/antd-admin pnpm install ``` Expected: 退出码 0,`pnpm-lock.yaml` 出现与 `antd-admin-basic` 相关的解析记录。 - [ ] **Step 3: Commit** ```bash git add apps/basic/package.json pnpm-lock.yaml git commit -m "chore(basic): drop Lingui deps and rename package" ``` --- ### Task 3: Vite 配置 **Files:** - Modify: `apps/basic/vite.config.ts` - [ ] **Step 1: 将 `apps/basic/vite.config.ts` 全文替换为以下内容** ```typescript import { defineConfig } from "vite-plus"; import react from "@vitejs/plugin-react-swc"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; export default defineConfig({ plugins: [ tanstackRouter({ routesDirectory: "./src/routes", generatedRouteTree: "./src/routeTree.gen.ts", }), react(), ], resolve: { alias: { "@": "/src", }, }, build: { rollupOptions: { output: { manualChunks: (id: string) => { if (id.includes("node_modules/antd")) { return "vendor-antd"; } if (id.includes("@tanstack/react-router") || id.includes("@tanstack/react-query")) { return "vendor-tanstack"; } if (id.includes("lucide-react")) { return "vendor-ui"; } }, }, }, chunkSizeWarningLimit: 1024, }, staged: { "*": "vp check --fix", }, lint: { options: { typeAware: true, typeCheck: true } }, }); ``` - [ ] **Step 2: 在 `apps/basic` 下跑类型检查** ```bash cd apps/basic && pnpm exec tsc --noEmit ``` Expected: 退出码 0(若此时仍因其它文件引用 Lingui 失败,完成 Task 4–8 后再重跑)。 - [ ] **Step 3: Commit** ```bash git add apps/basic/vite.config.ts git commit -m "chore(basic): remove Lingui vite and swc plugins" ``` --- ### Task 4: 删除 Lingui 专属文件 **Files:** - Delete: `apps/basic/lingui.config.ts` - Delete: `apps/basic/src/lingui.d.ts` - Delete: `apps/basic/src/locales/`(目录) - [ ] **Step 1: 删除文件与目录** ```bash rm -f apps/basic/lingui.config.ts apps/basic/src/lingui.d.ts rm -rf apps/basic/src/locales ``` - [ ] **Step 2: Commit** ```bash git add -A apps/basic git commit -m "chore(basic): remove lingui config and locale catalogs" ``` --- ### Task 5: 入口与根路由 **Files:** - Modify: `apps/basic/src/main.tsx` - Modify: `apps/basic/src/routes/__root.tsx` - [ ] **Step 1: 将 `apps/basic/src/main.tsx` 全文替换为** ```typescript import React from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; import { useAuthStore } from "./stores/auth"; import { fetchSessionAndApplyToStore } from "./utils/session"; const router = createRouter({ routeTree }); declare module "@tanstack/react-router" { interface Register { router: typeof router; } } async function enableMocking() { const enableMockInBuild = import.meta.env.VITE_ENABLE_MOCK === "true"; if (!import.meta.env.DEV && !enableMockInBuild) return; const { worker } = await import("./mocks/browser"); return worker.start({ onUnhandledRequest: "bypass" }); } enableMocking() .then(async () => { await useAuthStore.persist.rehydrate(); const { isAuthenticated, tokens } = useAuthStore.getState(); if (isAuthenticated && tokens) { try { await fetchSessionAndApplyToStore(); } catch { useAuthStore.getState().logout(); } } ReactDOM.createRoot(document.getElementById("root")!).render( , ); }) .catch((err) => { console.error("Failed to initialize app:", err); }); ``` - [ ] **Step 2: 将 `apps/basic/src/routes/__root.tsx` 全文替换为** ```typescript import { useEffect, useLayoutEffect } from "react"; import { createRootRoute, Outlet } from "@tanstack/react-router"; import { ConfigProvider, App } from "antd"; import enUS from "antd/locale/en_US"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useSettingsStore } from "@/stores/settings"; import { useAppTheme } from "@/hooks/useAppTheme"; import { NotFound } from "@/components/NotFound"; import "@/index.css"; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false }, }, }); function RootComponent() { const darkMode = useSettingsStore((s) => s.darkMode); const configProviderProps = useAppTheme(); useLayoutEffect(() => { document.documentElement.lang = "en"; }, []); useEffect(() => { document.documentElement.setAttribute("data-theme", darkMode ? "dark" : "light"); }, [darkMode]); return ( ); } export const Route = createRootRoute({ component: RootComponent, notFoundComponent: NotFound, }); ``` - [ ] **Step 3: 运行 TypeScript** ```bash cd apps/basic && pnpm exec tsc --noEmit ``` - [ ] **Step 4: Commit** ```bash git add apps/basic/src/main.tsx apps/basic/src/routes/__root.tsx git commit -m "feat(basic): remove Lingui bootstrap and fix Antd to en_US" ``` --- ### Task 6: Settings store **Files:** - Modify: `apps/basic/src/stores/settings.ts` - [ ] **Step 1: 将 `apps/basic/src/stores/settings.ts` 全文替换为** ```typescript import { createPersistentStore } from "./createPersistentStore"; function getInitialDarkMode() { if (typeof window === "undefined") { return false; } return window.matchMedia("(prefers-color-scheme: dark)").matches; } interface SettingsState { darkMode: boolean; sidebarCollapsed: boolean; toggleDarkMode: () => void; toggleSidebar: () => void; setSidebarCollapsed: (collapsed: boolean) => void; } export const useSettingsStore = createPersistentStore( (set) => ({ darkMode: getInitialDarkMode(), sidebarCollapsed: false, toggleDarkMode: () => set((s) => ({ darkMode: !s.darkMode })), toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }), }), { name: "settings-storage-basic", partialize: (state) => ({ darkMode: state.darkMode, sidebarCollapsed: state.sidebarCollapsed, }), }, ); ``` - [ ] **Step 2: Commit** ```bash git add apps/basic/src/stores/settings.ts git commit -m "feat(basic): drop locale from settings store" ``` --- ### Task 7: 小型组件(完整替换) **Files:** - Modify: `apps/basic/src/components/DataTable/DataTableEmpty.tsx` - Modify: `apps/basic/src/components/NotFound/index.tsx` - Modify: `apps/basic/src/routes/_auth/403/index.tsx` - Modify: `apps/basic/src/components/Layout/AppFooter/index.tsx` - [ ] **Step 1: `DataTableEmpty.tsx` 全文替换** ```typescript import { Flex, theme } from "antd"; import { BarChart3 } from "lucide-react"; import type { ReactElement } from "react"; /** * Table empty state: icon in soft tile + bold title + secondary description (dashboard-style). */ export function DataTableEmpty(): ReactElement { const { token } = theme.useToken(); return ( No data Nothing to show in this list yet ); } ``` - [ ] **Step 2: `NotFound/index.tsx` 全文替换** ```typescript import { useNavigate } from "@tanstack/react-router"; import { ArrowLeft, Home, SearchX } from "lucide-react"; import { Button, Flex, Result, Space, theme } from "antd"; /** Shared 404 UI for `/404` route and root `notFoundComponent`. */ export function NotFound() { const navigate = useNavigate(); const { token } = theme.useToken(); const goDashboard = () => { void navigate({ to: "/dashboard" }); }; const goBack = () => { if (typeof window !== "undefined" && window.history.length > 1) { window.history.back(); } else { goDashboard(); } }; return ( } title="404" subTitle="Sorry, the page you visited does not exist." extra={ } /> ); } ``` - [ ] **Step 3: `403/index.tsx` 全文替换** ```typescript import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { Home, ShieldAlert } from "lucide-react"; import { Button, Flex, Result, theme } from "antd"; export const Route = createFileRoute("/_auth/403/")({ component: ForbiddenPage, }); function ForbiddenPage() { const navigate = useNavigate(); const { token } = theme.useToken(); const goDashboard = () => { void navigate({ to: "/dashboard" }); }; return ( } title="403" subTitle="Sorry, you don't have permission to access this page." extra={ } /> ); } ``` - [ ] **Step 4: `AppFooter/index.tsx` 全文替换** ```typescript import { Flex, Typography, theme } from "antd"; import { GitHub } from "@/components/Icon"; const ANTD_ADMIN_REPO = "https://github.com/zuiidea/antd-admin"; export function AppFooter() { const { token } = theme.useToken(); const iconSize = Math.max(12, Math.round(Number(token.fontSizeSM))); return ( Powered by antd-admin ); } ``` - [ ] **Step 5: Commit** ```bash git add apps/basic/src/components/DataTable/DataTableEmpty.tsx \ apps/basic/src/components/NotFound/index.tsx \ apps/basic/src/routes/_auth/403/index.tsx \ apps/basic/src/components/Layout/AppFooter/index.tsx git commit -m "feat(basic): replace Lingui strings in small layout components" ``` --- ### Task 8: Header 组件 **Files:** - Modify: `apps/basic/src/components/Layout/Header/index.tsx` - [ ] **Step 1: 将 `apps/basic/src/components/Layout/Header/index.tsx` 全文替换为** ```typescript import { Layout, Button, Space, theme, Breadcrumb, Flex, Divider, Grid } from "antd"; import type { ItemType } from "antd/es/breadcrumb/Breadcrumb"; import { useSettingsStore } from "@/stores/settings"; import { Link, useLocation, useMatches } from "@tanstack/react-router"; import { Home, PanelLeft, ShieldAlert, Users } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; import { Theme } from "@/components/Icon"; const { Header: AntHeader } = Layout; const PATH_LABEL: Record = { "/dashboard": "Dashboard", "/users": "Users", "/403": "403", }; function normalizePath(pathname: string): string { if (pathname === "/") return pathname; return pathname.replace(/\/+$/, "") || "/"; } export type HeaderProps = { /** * When `false`, breadcrumb is hidden; left `Flex` still uses `flex={1}` so header actions stay right-aligned. * Routes may also set `staticData: { hideBreadcrumb: true }` (deepest matching route wins). */ showBreadcrumb?: boolean; }; export function Header({ showBreadcrumb: showBreadcrumbProp = true }: HeaderProps) { const toggleSidebar = useSettingsStore((s) => s.toggleSidebar); const toggleDarkMode = useSettingsStore((s) => s.toggleDarkMode); const location = useLocation(); const matches = useMatches(); const { token } = theme.useToken(); const screens = Grid.useBreakpoint(); const isMobile = !screens.lg; const iconSize = token.fontSize; const crumb = (Icon: LucideIcon, label: ReactNode, linkTo?: "/dashboard") => { const row = ( <> {label} ); const rowStyle = { display: "inline-flex" as const, alignItems: "center" as const, gap: token.marginXS, color: "inherit" as const, }; if (linkTo) { return ( {row} ); } return {row}; }; const path = normalizePath(location.pathname); const segments = path.split("/").filter(Boolean); const firstSegmentPath = segments.length ? `/${segments[0]}` : "/dashboard"; const leafLabelKey = PATH_LABEL[firstSegmentPath] ?? segments[0] ?? "Dashboard"; const leafIcon: LucideIcon = firstSegmentPath === "/users" ? Users : firstSegmentPath === "/403" ? ShieldAlert : Home; const leafLabel = leafLabelKey; const breadcrumbItems: ItemType[] = []; const onDashboard = path === "/dashboard" || path === "/"; if (onDashboard) { breadcrumbItems.push({ title: crumb(Home, "Dashboard"), }); } else { breadcrumbItems.push({ title: crumb(Home, "Dashboard", "/dashboard"), }); breadcrumbItems.push({ title: crumb(leafIcon, leafLabel), }); if (segments.length > 1) { const tail = segments.slice(1).join(" / "); if (tail) { breadcrumbItems.push({ title: tail }); } } } const leafStatic = matches.at(-1)?.staticData as { hideBreadcrumb?: boolean } | undefined; const hideBreadcrumbFromRoute = leafStatic?.hideBreadcrumb === true; const showBreadcrumb = showBreadcrumbProp && !hideBreadcrumbFromRoute && breadcrumbItems.length > 0; return ( {isMobile ? (