29 KiB
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(排除产物)
在仓库根目录执行:
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,可用:
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
test -f apps/basic/package.json && test ! -d apps/basic/node_modules && echo OK
Expected: 输出 OK
- Step 3: Commit
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 中删除这两行:
"i18n:extract": "vp exec lingui extract",
"i18n:compile": "vp exec lingui compile"
从 dependencies 中删除:
"@lingui/core": "^5.9.3",
"@lingui/react": "^5.9.3",
从 devDependencies 中删除:
"@lingui/cli": "^5.9.3",
"@lingui/swc-plugin": "^5.11.0",
"@lingui/vite-plugin": "^5.9.3",
其余字段保持与源应用一致。
- Step 2: 安装依赖并更新 lockfile
cd /Users/zuiidea/web/antd-admin
pnpm install
Expected: 退出码 0,pnpm-lock.yaml 出现与 antd-admin-basic 相关的解析记录。
- Step 3: Commit
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全文替换为以下内容
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下跑类型检查
cd apps/basic && pnpm exec tsc --noEmit
Expected: 退出码 0(若此时仍因其它文件引用 Lingui 失败,完成 Task 4–8 后再重跑)。
- Step 3: Commit
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: 删除文件与目录
rm -f apps/basic/lingui.config.ts apps/basic/src/lingui.d.ts
rm -rf apps/basic/src/locales
- Step 2: Commit
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全文替换为
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(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
})
.catch((err) => {
console.error("Failed to initialize app:", err);
});
- Step 2: 将
apps/basic/src/routes/__root.tsx全文替换为
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 (
<QueryClientProvider client={queryClient}>
<ConfigProvider {...configProviderProps} locale={enUS}>
<App>
<Outlet />
</App>
</ConfigProvider>
</QueryClientProvider>
);
}
export const Route = createRootRoute({
component: RootComponent,
notFoundComponent: NotFound,
});
- Step 3: 运行 TypeScript
cd apps/basic && pnpm exec tsc --noEmit
- Step 4: Commit
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全文替换为
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<SettingsState>(
(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
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全文替换
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 (
<Flex
vertical
align="center"
justify="center"
gap={token.marginMD}
className="data-table-empty"
style={{
paddingBlock: token.paddingXL,
paddingInline: token.paddingLG,
maxWidth: 360,
marginInline: "auto",
}}
>
<Flex
align="center"
justify="center"
style={{
width: 44,
height: 44,
borderRadius: token.borderRadiusLG,
background: token.colorFillTertiary,
color: token.colorTextQuaternary,
}}
>
<BarChart3 size={22} strokeWidth={1.75} aria-hidden />
</Flex>
<Flex vertical align="center" gap={token.marginXXS} style={{ textAlign: "center" }}>
<span
style={{
fontSize: token.fontSizeLG,
fontWeight: token.fontWeightStrong,
color: token.colorText,
lineHeight: token.lineHeightLG,
}}
>
No data
</span>
<span
style={{
fontSize: token.fontSize,
fontWeight: 400,
color: token.colorTextSecondary,
lineHeight: token.lineHeight,
}}
>
Nothing to show in this list yet
</span>
</Flex>
</Flex>
);
}
- Step 2:
NotFound/index.tsx全文替换
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 (
<Flex
vertical
style={{
/* Lock to viewport so tall Result + padding cannot grow past 100vh and scroll the page */
height: "100dvh",
maxHeight: "100dvh",
overflow: "hidden",
boxSizing: "border-box",
background: token.colorBgLayout,
color: token.colorText,
}}
>
<Flex
flex={1}
align="center"
justify="center"
style={{
minHeight: 0,
width: "100%",
padding: token.paddingXL,
overflow: "hidden",
boxSizing: "border-box",
}}
>
<Result
// Avoid status="404"|"403"|"500": antd ignores `icon` and shows built-in art; keep Lucide as the main icon.
icon={
<SearchX size={64} strokeWidth={1.25} color={token.colorTextQuaternary} aria-hidden />
}
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Space wrap size="middle">
<Button type="primary" icon={<Home size={16} aria-hidden />} onClick={goDashboard}>
Back to Home
</Button>
<Button icon={<ArrowLeft size={16} aria-hidden />} onClick={goBack}>
Go back
</Button>
</Space>
}
/>
</Flex>
</Flex>
);
}
- Step 3:
403/index.tsx全文替换
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 (
<Flex
flex={1}
align="center"
justify="center"
style={{ minHeight: 0, width: "100%", padding: token.paddingXL }}
>
<Result
// Avoid status="403": antd ignores `icon` for built-in illustrations; keep Lucide as the main icon.
icon={
<ShieldAlert size={64} strokeWidth={1.25} color={token.colorTextQuaternary} aria-hidden />
}
title="403"
subTitle="Sorry, you don't have permission to access this page."
extra={
<Button type="primary" icon={<Home size={16} aria-hidden />} onClick={goDashboard}>
Back to Home
</Button>
}
/>
</Flex>
);
}
- Step 4:
AppFooter/index.tsx全文替换
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 (
<Flex
align="center"
justify="center"
wrap
gap={4}
style={{
lineHeight: token.lineHeight,
}}
>
<Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM, marginBottom: 0 }}>
Powered by
</Typography.Text>
<a
href={ANTD_ADMIN_REPO}
target="_blank"
rel="noopener noreferrer"
style={{
color: token.colorLink,
display: "inline-flex",
alignItems: "center",
gap: 4,
}}
>
<GitHub size={iconSize} />
antd-admin
</a>
</Flex>
);
}
- Step 5: Commit
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全文替换为
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<string, string> = {
"/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 = (
<>
<Icon size={iconSize} aria-hidden style={{ flexShrink: 0, opacity: 0.88 }} />
<span>{label}</span>
</>
);
const rowStyle = {
display: "inline-flex" as const,
alignItems: "center" as const,
gap: token.marginXS,
color: "inherit" as const,
};
if (linkTo) {
return (
<Link to={linkTo} style={rowStyle}>
{row}
</Link>
);
}
return <span style={rowStyle}>{row}</span>;
};
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 (
<AntHeader
style={{
background: "transparent",
borderBottom: `1px solid ${token.colorBorderSecondary}`,
padding: `0 ${token.padding}px`,
gap: token.sizeLG,
display: "flex",
}}
>
<Flex align="center" flex={1} style={{ minWidth: 0 }}>
{isMobile ? (
<Button
type="text"
size="small"
onClick={toggleSidebar}
icon={<PanelLeft size={token.size} />}
aria-label="Toggle sidebar"
/>
) : null}
{showBreadcrumb ? (
<>
{isMobile ? <Divider vertical /> : null}
<Breadcrumb items={breadcrumbItems} />
</>
) : null}
</Flex>
<Space>
<Button
type="text"
onClick={toggleDarkMode}
icon={<Theme size={token.size} />}
aria-label="Toggle Theme"
/>
</Space>
</AntHeader>
);
}
- Step 2: Commit
git add apps/basic/src/components/Layout/Header/index.tsx
git commit -m "feat(basic): remove locale toggle from header"
Task 9: Login 页
Files:
-
Modify:
apps/basic/src/routes/login/index.tsx -
Step 1: 编辑该文件
- 删除
import { useLingui } from "@lingui/react/macro"; - 删除
import { Languages } from "lucide-react"; - 删除
const { t } = useLingui(); - 删除
const locale = useSettingsStore((s) => s.locale);、const setLocale = useSettingsStore((s) => s.setLocale); - 删除整个
toggleLocale函数。 - 将所有
t`...`替换为对应英文双引号字符串(与encatalog 一致),例如:t`Login successful`→"Login successful"t`Login failed`→"Login failed"t`Username`→"Username"t`Please enter username`→"Please enter username"t`Password`→"Password"t`Please enter password`→"Please enter password"t`Auto login`→"Auto login"t`Forgot password?`→"Forgot password?"t`Sign In`→"Sign In"
- 在底部
Space中删除语言切换Button(Languages图标、toggleLocale、aria-label为 Switch language 的按钮),保留主题切换按钮与AppFooter。 - 确认文件顶部不再引用
useSettingsStore的 locale(若仅用于语言切换,可移除对应 selector import 行)。
- Step 2: Commit
git add apps/basic/src/routes/login/index.tsx
git commit -m "feat(basic): English login copy and remove language toggle"
Task 10: Sidebar 组件
Files:
-
Modify:
apps/basic/src/components/Layout/Sidebar/index.tsx -
Step 1: 删除 Lingui import
删除:
import { useLingui } from "@lingui/react/macro";
import { msg } from "@lingui/core/macro";
- Step 2: 将
MENU_LABELS改为英文字符串表
/** API menu `name` → English labels for known keys; unknown keys pass through as `menu.name`. */
const MENU_LABELS: Record<string, string> = {
Platform: "Platform",
Projects: "Projects",
Dashboard: "Dashboard",
Users: "Users",
"Design Engineering": "Design Engineering",
"Sales & Marketing": "Sales & Marketing",
};
- Step 3: 修改
buildMenuItems函数签名
将:
function buildMenuItems(
menus: MenuItemType[],
t: ReturnType<typeof useLingui>["t"],
token: ReturnType<typeof theme.useToken>["token"],
改为:
function buildMenuItems(
menus: MenuItemType[],
token: ReturnType<typeof theme.useToken>["token"],
- Step 4: 将标签解析行
从:
const label = MENU_LABELS[menu.name] ? t(MENU_LABELS[menu.name]) : menu.name;
改为:
const label = MENU_LABELS[menu.name] ?? menu.name;
- Step 5: 更新所有
buildMenuItems递归调用
每一处 buildMenuItems(menu.children, t, token, ...) 改为 buildMenuItems(menu.children, token, ...);第一处调用从 buildMenuItems(menus, t, token, ...) 改为 buildMenuItems(menus, token, ...)。
- Step 6: 在
Sidebar组件内
删除 const { t } = useLingui();,将 useMemo 依赖数组中的 t 去掉,将 t`Sign Out` 改为 "Sign Out",将所有 t`Toggle sidebar` 改为 "Toggle sidebar"。
- Step 7: Commit
git add apps/basic/src/components/Layout/Sidebar/index.tsx
git commit -m "feat(basic): English sidebar labels without Lingui"
Task 11: Dashboard 与 Users 路由
Files:
-
Modify:
apps/basic/src/routes/_auth/dashboard/index.tsx -
Modify:
apps/basic/src/routes/_auth/users/index.tsx -
Modify:
apps/basic/src/routes/_auth/users/-Toolbar.tsx -
Modify:
apps/basic/src/routes/_auth/users/-FormModal.tsx -
Step 1: 在每个文件中删除
useLinguiimport 与const { t } = useLingui(); -
Step 2: 将每个
t`...`与带插值的t\`...\替换为等价英文
规则:打开 apps/with-lingui/src/locales/en/messages.po,按 msgid 文本替换;带 {variable} 的使用模板字符串,例如 t\`{total} rows\` → `${total} rows`(变量名以源码为准)。
- Step 3: 运行 TypeScript
cd apps/basic && pnpm exec tsc --noEmit
- Step 4: Commit
git add apps/basic/src/routes/_auth/dashboard/index.tsx \
apps/basic/src/routes/_auth/users/index.tsx \
apps/basic/src/routes/_auth/users/-Toolbar.tsx \
apps/basic/src/routes/_auth/users/-FormModal.tsx
git commit -m "feat(basic): English strings on dashboard and users routes"
Task 12: 全库确认无 Lingui 残留
Files:
-
(grep 修复任意遗漏文件)
-
Step 1: 搜索残留
cd /Users/zuiidea/web/antd-admin
rg "@lingui|useLingui|I18nProvider|from \"@lingui" apps/basic --glob "*.{ts,tsx}"
Expected: 无匹配输出。
- Step 2: 若有匹配,逐个文件按 Task 7–11 模式删除并提交
Task 13: 构建、Lint、E2E
Files:
-
(无新文件,验证)
-
Step 1: Lint
cd apps/basic && pnpm run lint
Expected: 退出码 0。
- Step 2: 生产构建
cd apps/basic && pnpm run build
Expected: 退出码 0,生成 apps/basic/dist/。
- Step 3: Playwright
cd apps/basic && pnpm run test:e2e:core
Expected: 全部通过。若选择器依赖中文,改为英文文案对应断言。
- Step 4: Commit(若仅修复测试/配置)
git add -A apps/basic
git commit -m "test(basic): align e2e with English UI" || true
Task 14: 文档
Files:
-
Modify:
README.md -
Modify:
apps/basic/README.md -
Step 1: 编辑
apps/basic/README.md
在标题下增加简短说明:
-
本模板为 English-only,不包含 Lingui。
-
需要中英文与
.po工作流时使用apps/with-lingui。 -
Step 2: 编辑根目录
README.md -
增加 Templates(或「项目结构」)小节:说明
apps/with-lingui与apps/basic的差异。 -
给出从仓库根进入各应用运行的示例,例如:
cd apps/basic && vp dev
cd apps/with-lingui && vp dev
-
将技术栈表格中 i18n 一行改为注明「仅
with-lingui」或拆成两行,避免读者以为basic含 Lingui。 -
Step 3: Commit
git add README.md apps/basic/README.md
git commit -m "docs: document with-lingui vs basic templates"
计划自检(对照 spec)
| Spec 要求 | 对应 Task |
|---|---|
拷贝并减法得到 apps/basic |
Task 1 |
| 去掉依赖、脚本、Vite 插件 | Task 2–3 |
删除 lingui.config、lingui.d.ts、locales |
Task 4 |
main / __root 固定英文与 enUS |
Task 5 |
| settings 去 locale + 独立 persist name | Task 6 |
组件与路由去 t/msg |
Task 7–11 |
| 无 Lingui 残留 | Task 12 |
| build / lint / e2e | Task 13 |
根 README + apps/basic README |
Task 14 |
占位符扫描: 本计划不包含 TBD /「适当处理」类步骤。
类型一致性: settings 已移除 Locale;Header 不再 import Locale;buildMenuItems 全文件统一新签名。
执行方式(实现阶段)
计划已保存到 docs/superpowers/plans/2026-04-14-apps-basic-template.md。
可选执行方式:
- Subagent-Driven(推荐) — 每个 Task 派生子代理,任务间审查,迭代快。
- Inline Execution — 本会话内按 Task 顺序执行,配合 checkpoints 审查。
你更倾向哪一种?(若不需要代理编排,直接在实现时按本文 checkbox 顺序执行即可。)