FE-01 用户登录 Implementation Plan
Execution: Parent skill
fe-feature-tddexecutes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 实现登录页 /login:表单(username + password + companyCode)+ submit → POST /api/v1/auth/login → Redux 存 accessToken + userInfo → 跳转 /users。同时落地前端项目骨架(Vite + React 18 + AntD 5 + Redux Toolkit + React Router v6 + Vitest + Playwright)供后续 FE 复用。
Architecture:
- Vite + React 18 + TypeScript + Ant Design 5(
ConfigProvider接tokens.cssCSS 变量)+ Redux Toolkit(accessToken 仅内存)+ React Router v6(含RequireAuth守卫)+ Axios(请求拦截器自动加 Authorization 头)。 - 测试栈:Vitest + @testing-library/react(jsdom 组件测试)+ Playwright(E2E)。
- 后端 Mock:组件测试用 MSW 拦截 axios;E2E 直接打真后端(与 backend test-gate 共享 .env.local 配置)。
Tech Stack: Node 20 LTS / Vite 5 / React 18 / TypeScript 5 / Ant Design 5 / Redux Toolkit / React Router v6 / Axios / Vitest 1 / @testing-library/react 14 / MSW 2 / Playwright 1.x / dayjs。
文件结构(与 docs/09 § 三 对齐,全部位于 frontend/ 下)
Bootstrap(FE-01 一次性投入,后续 FE 复用):
-
frontend/package.json、frontend/vite.config.ts、frontend/tsconfig.json、frontend/tsconfig.node.json、frontend/index.html、frontend/vitest.config.ts、frontend/playwright.config.ts、frontend/.eslintrc.cjs、frontend/.gitignore -
frontend/src/main.tsx、frontend/src/App.tsx -
frontend/src/styles/tokens.css(从仓库根src/styles/tokens.css复用并增补 docs/06 § 二全部变量) frontend/src/styles/global.cssfrontend/src/types/global.d.ts
通用基础层:
-
frontend/src/api/client.ts(Axios 实例 + 拦截器) -
frontend/src/api/auth.ts(login API 包装) -
frontend/src/api/errors.ts(BizError 类型 + code mapping) -
frontend/src/store/index.ts(configureStore) frontend/src/store/slices/authSlice.ts-
frontend/src/store/hooks.ts(typed useAppSelector / useAppDispatch) -
frontend/src/router/index.tsx(路由表) -
frontend/src/router/RequireAuth.tsx(路由守卫)
FE-01 业务层:
frontend/src/pages/login/LoginPage.tsxfrontend/src/pages/login/LoginForm.tsx-
frontend/src/pages/login/LoginHero.tsx(静态视觉,无逻辑) frontend/src/pages/login/LoginFooter.tsx-
frontend/src/pages/login/loginConstants.ts(公司硬编码 + 错误文案)
测试:
-
frontend/src/api/client.test.ts(jsdom:拦截器行为) -
frontend/src/store/slices/authSlice.test.ts(jsdom:reducer + thunk) -
frontend/src/router/RequireAuth.test.tsx(jsdom) -
frontend/src/pages/login/LoginForm.test.tsx(jsdom:表单校验 + submit 流转) -
frontend/src/pages/login/LoginPage.test.tsx(jsdom:错误状态机集成) -
frontend/tests/e2e/login.spec.ts(Playwright:成功 / 错误密码 / 锁定 三条路径) -
frontend/src/test-utils/msw-handlers.ts(MSW 拦截器,模拟 /api/v1/auth/login 各错误码) -
frontend/src/test-utils/setup.ts(vitest setup:jest-dom + MSW)
约束常量(跨任务一致)
API client 签名:
-
apiClient: AxiosInstance(baseURL =import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:9090',timeout 10s,响应拦截器统一抛BizError) BizError extends Error { code: number; data?: unknown }
Auth API 函数:
authApi.login(req: LoginReq): Promise<LoginVo>LoginReq { username: string; password: string; companyCode: string }LoginVo { accessToken: string; tokenType: 'Bearer'; expiresInSec: number; userInfo: UserInfo }UserInfo { userId: number; username: string; userType: 'NORMAL'|'SUPER_ADMIN'; language: string; employeeName?: string; companyCode: string }
Redux auth slice:
- state shape:
{ accessToken: string | null; userInfo: UserInfo | null; status: 'idle'|'submitting'|'success'|'failed' } - actions:
setSession({ accessToken, userInfo })/clearSession()/setStatus(status) - selector:
selectAccessToken/selectUserInfo/selectIsAuthenticated
LoginForm props:
<LoginForm onSubmit={(req: LoginReq) => Promise<void>} loading={boolean} errorMessage={string | null} fieldErrors={{username?: string; password?: string; companyCode?: string}} />
路由表(router/index.tsx 写死):
-
/login→<LoginPage />公开 -
/users→<RequireAuth><UsersPage /></RequireAuth>(占位组件,FE-02 实现) - 其它任意路径 →
RequireAuth守卫,未登录重定向到/login
错误码 → 文案映射常量(loginConstants.ts):
ERROR_MESSAGES = {
40001: '请检查字段格式',
40004: '公司不存在或已删除',
40101: '用户名或密码错误',
40103: '账号已被作废,禁止登录',
42301: '账号已锁定,请于 {lockUntil} 后再试',
NETWORK: '网络异常,请检查连接后重试',
UNKNOWN: '登录失败,请稍后重试',
}
COMPANY_OPTIONS = [{ value: 'HQ', label: '总部' }]
任务步骤
Task 1: Bootstrap Vite + React + AntD + Redux + RR + Vitest 项目骨架
Files:
- Create:
frontend/package.json、frontend/vite.config.ts、frontend/tsconfig.json、frontend/tsconfig.node.json、frontend/index.html、frontend/vitest.config.ts、frontend/.eslintrc.cjs、frontend/.gitignore - Create:
frontend/src/main.tsx、frontend/src/App.tsx、frontend/src/styles/global.css - Create:
frontend/src/styles/tokens.css(直接复用仓库根src/styles/tokens.css内容,确保 docs/06 § 二 token 全覆盖) - Test:
frontend/src/App.test.tsx - 测试先行类型: jsdom 组件测试
Goal: 让 npm run test 能跑通"App renders without crashing"最小 smoke test,证明 Vite + Vitest + React 18 + jsdom 集成完整。
package.json 关键 dependencies/devDependencies:
- runtime:
react,react-dom,react-router-dom@^6,@reduxjs/toolkit,react-redux,antd@^5,axios,dayjs - dev:
vite@^5,@vitejs/plugin-react,typescript@^5,@types/react,@types/react-dom,vitest,@testing-library/react,@testing-library/jest-dom,jsdom,msw@^2,@playwright/test,eslint,eslint-plugin-react,eslint-plugin-react-hooks,@typescript-eslint/parser,@typescript-eslint/eslint-plugin
scripts:
-
dev:vite -
build:tsc && vite build -
test:vitest run -
test:watch:vitest -
lint:eslint src --max-warnings 0 -
e2e:playwright test
vitest.config.ts:environment=jsdom;setupFiles=./src/test-utils/setup.ts(Task 5 创建);本任务 setupFiles 行先注释或指向最小 setup。
-
Step 1: 写失败测试
frontend/src/App.test.tsx- 测试名:
App renders without crashing - 意图: 渲染
<App />→ 期望页面上有 "Antler ERP" 文字(暂用作 placeholder;后续任务会被覆盖) - 子会话确认 FAIL(App / package.json / vite 都不存在)
- 测试名:
Step 2: 实现最小代码:全部 bootstrap 文件 + App.tsx 内最小返回
<div>Antler ERP</div>-
Step 3: 子会话验证 PASS
- 子会话跑
cd /Users/reporkey/Desktop/test5/frontend && npm install --no-audit --no-fund && npm test
- 子会话跑
-
Step 4: Commit
git add frontend/git commit -m "feat(frontend): bootstrap Vite + React + AntD + Vitest 骨架 FE-01"
Task 2: Design Tokens + AntD ConfigProvider 接入
Files:
- Modify:
frontend/src/main.tsx引入styles/tokens.css+global.css - Create:
frontend/src/App.tsx(更新:包 ConfigProvider,theme.token.colorPrimary 引用 var(--color-primary)) - Test:
frontend/src/App.test.tsx(追加 token assertion) - 测试先行类型: jsdom 组件测试
API shape:
-
<App />内层一定包<ConfigProvider theme={{ token: { colorPrimary: '#1677ff', ...overrides } }}>- 注意:AntD ConfigProvider token 不能直接接
var(--color-primary),需先在 css 解析为具体值。本任务把 ConfigProvider token 直接写 hex(与 tokens.css 同源),并备注"色值唯一改 tokens.css 时需同步改 ConfigProvider"——记为 docs/06 § 二注解。
- 注意:AntD ConfigProvider token 不能直接接
-
Step 1: 写失败测试
- 测试名:
App wraps children with ConfigProvider providing primary color - 意图: render
<App />后通过document.documentElement.style.getPropertyValue('--color-primary')断言 tokens.css 被加载;ConfigProvider 提供的 colorPrimary 与 token 一致(通过 querySelector 检测 AntD 主题)
- 测试名:
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): Design Tokens + AntD ConfigProvider FE-01
Task 3: Axios 客户端 + 错误码 mapping
Files:
- Create:
frontend/src/api/client.ts - Create:
frontend/src/api/errors.ts - Test:
frontend/src/api/client.test.ts - Create:
frontend/src/test-utils/setup.ts、frontend/src/test-utils/msw-handlers.ts - 测试先行类型: jsdom 组件测试 + MSW
API shape:
-
apiClient: AxiosInstance- baseURL:
import.meta.env.VITE_API_BASE_URL ?? '/api/v1'(Vite proxy 在 dev 模式把/api/v1转发到 9090) - timeout: 10000
- 请求拦截器: 若 Redux store 有 accessToken,注入
Authorization: Bearer ${accessToken} - 响应拦截器:
- HTTP 200 +
data.code === 200→response.data.data(直接返 payload) - HTTP 200 +
data.code !== 200→ 抛new BizError(code, message, data) - HTTP 非 200 → 抛
new BizError(httpStatus, message, undefined) - 网络错误 → 抛
new BizError(-1, 'NETWORK', undefined)
- baseURL:
BizError extends Error { code: number; data?: unknown }
vite.config.ts 配 proxy:/api/v1 → http://localhost:9090
-
Step 1: 写失败测试
client.test.ts-
client.unwraps Result envelope on success(MSW 返{code:200,data:{x:1}}→ client 返{x:1}) -
client.throws BizError on business error(MSW 返{code:40101,message:"X"}→ throws BizError code=40101) client.throws BizError on http 5xx-
client.throws BizError code=-1 on network error(MSW 不响应)
-
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): axios client + BizError + MSW 测试基建 FE-01
Task 4: Auth API 包装 authApi.login
Files:
- Create:
frontend/src/api/auth.ts - Test:
frontend/src/api/auth.test.ts - Modify:
frontend/src/test-utils/msw-handlers.ts增加/api/v1/auth/loginhandlers - 测试先行类型: jsdom 组件测试 + MSW
API shape:
-
export async function login(req: LoginReq): Promise<LoginVo>—apiClient.post('/auth/login', req) LoginReq、LoginVo、UserInfotypes 见"约束常量"-
Step 1: 写失败测试
-
login_returnsLoginVo_onSuccess(MSW 200 → 返 token + userInfo) login_throwsBizError_40101_onBadCredentialslogin_throwsBizError_42301_withLockUntilData_onLocked
-
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): authApi.login + MSW handlers FE-01
Task 5: Redux authSlice + typed hooks
Files:
- Create:
frontend/src/store/index.ts、frontend/src/store/hooks.ts - Create:
frontend/src/store/slices/authSlice.ts - Test:
frontend/src/store/slices/authSlice.test.ts - 测试先行类型: jsdom 组件测试
API shape:
-
authSlice.reducer、actions:setSession({accessToken, userInfo})/clearSession()/setStatus('idle'|'submitting'|'success'|'failed') - selectors:
selectAccessToken(state)、selectUserInfo(state)、selectIsAuthenticated(state)、selectAuthStatus(state) -
useAppDispatch、useAppSelectortyped hooks store shape:
{ auth: AuthState }-
Step 1: 写失败测试
setSession_writesAccessTokenAndUserInfoclearSession_resetsToNullsetStatus_updatesStatusselectIsAuthenticated_trueWhenTokenPresent
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): authSlice + typed hooks FE-01
Task 6: 路由配置 + RequireAuth 守卫
Files:
- Create:
frontend/src/router/index.tsx、frontend/src/router/RequireAuth.tsx - Modify:
frontend/src/App.tsx接入 RouterProvider + Provider(store) - Test:
frontend/src/router/RequireAuth.test.tsx - 测试先行类型: jsdom 组件测试
API shape:
-
RequireAuth({ children })— 读selectIsAuthenticated,true 渲染 children,false<Navigate to="/login" replace /> -
router = createBrowserRouter([...]):-
/login→<LoginPage />(FE-01 提供) -
/users→<RequireAuth><UsersPlaceholder /></RequireAuth>(占位组件,本任务用<div>users placeholder</div>占位;FE-02 替换) -
*→<Navigate to="/users" />
-
-
Step 1: 写失败测试
-
RequireAuth redirects to /login when no token(render with MemoryRouter + Provider with empty store) RequireAuth renders children when token present
-
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): RequireAuth 守卫 + 路由表 FE-01
Task 7: LoginForm 组件(pure 表单 + 校验)
Files:
- Create:
frontend/src/pages/login/LoginForm.tsx - Create:
frontend/src/pages/login/loginConstants.ts - Test:
frontend/src/pages/login/LoginForm.test.tsx - 测试先行类型: jsdom 组件测试
Props/API shape:
<LoginForm onSubmit={(req: LoginReq) => Promise<void>} loading={boolean} errorMessage={string | null} fieldErrors={Record<'username'|'password'|'companyCode', string | undefined>} />- 用 AntD
Form+Input+Input.Password+Select+Button - 内部状态:
form实例;submit 时先validateFields()(jakarta 风格前端校验:username/password 非空,companyCode 必选)→ 调onSubmit -
loading=true时所有字段 + submit disabled -
errorMessage != null时顶部Alert type="error"显示 fieldErrors.username等 → 通过Form.Item.help显示字段级错误-
Step 1: 写失败测试
LoginForm_renders_threeFields_and_submitButton-
LoginForm_emptySubmit_showsFieldErrors(点击 submit 不填字段 → 显示"请输入用户名 / 请输入密码 / 请选择公司") LoginForm_submitsValid_callsOnSubmit_withTypedReqLoginForm_loading_disablesAllInputsAndSubmitLoginForm_errorMessage_displaysInAlertLoginForm_fieldErrors_displayInFormItemHelp
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): LoginForm 组件 + 字段校验 FE-01
Task 8: LoginHero / LoginFooter 静态视觉组件
Files:
- Create:
frontend/src/pages/login/LoginHero.tsx - Create:
frontend/src/pages/login/LoginFooter.tsx - Test:
frontend/src/pages/login/LoginHero.test.tsx - 测试先行类型: jsdom 组件测试
API shape:
-
<LoginHero />渲染左侧文字(Enterprise Business Capability / 企业业务能力平台 / ERP)+ logo SVG -
<LoginFooter />渲染版权 + ICP 文字 + 盾牌图标
视觉细节直接复制 prototype screen-login 内 .login-head + .login-text + .login-foot 的内容;样式用 CSS modules 或 styled components 任选其一(统一选 CSS modules:LoginHero.module.css)。
- Step 1: 写失败测试
-
LoginHero_renders_brandText(含 "Antler ERP" / "Enterprise Business Capability" / "ERP") -
LoginFooter_renders_copyrightAndICP(含 "Antler Software" / "沪ICP备14034791号-1")
-
- Step 2: 实现最小代码
- Step 3: 子会话验证 PASS
- Step 4: Commit
feat(frontend): LoginHero + LoginFooter 静态视觉 FE-01
Task 9: LoginPage 组装 + 错误状态机集成
Files:
- Create:
frontend/src/pages/login/LoginPage.tsx - Test:
frontend/src/pages/login/LoginPage.test.tsx - 测试先行类型: jsdom 组件测试
API shape:
-
<LoginPage />— 默认导出,无 props - 内部:local state
{ errorMessage, fieldErrors };用useAppDispatch;调用authApi.login();成功 →dispatch(setSession())+navigate('/users', {replace:true});失败 → 根据 BizError.code 映射文案 + 切换 errorMessage 或 fieldErrors - 锁定(42301):从
err.data.lockUntil解析 dayjs,文案账号已锁定,请于 HH:mm 后再试 网络错误(code=-1):文案
网络异常,请检查连接后重试-
Step 1: 写失败测试
LoginPage_successFlow_dispatchesSetSessionAndNavigatesToUsersLoginPage_badCredentials_shows40101MessageLoginPage_locked_shows42301WithLockUntilLoginPage_deletedAccount_shows40103MessageLoginPage_unknownCompany_shows40004OnCompanyFieldLoginPage_networkError_showsNetworkMessage
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
Step 4: Commit
feat(frontend): LoginPage 组装 + 错误状态机 FE-01
Task 10: Playwright E2E — 登录三条路径
Files:
- Create:
frontend/playwright.config.ts - Create:
frontend/tests/e2e/login.spec.ts - 测试先行类型: Playwright E2E
E2E 三个场景(连真后端,复用 LoginTestSeeder 已建数据):
-
successLogin_redirectsToUsers:访问/login→ 填alice/Password1!/HQ→ submit → URL 变为/users -
badPassword_showsError:填alice/WrongPass/HQ→ submit → 页面上出现 "用户名或密码错误" -
unknownCompany_showsError:填alice/Password1!/NOPE→ submit → 出现 "公司不存在或已删除"
注:场景 3 需要
NOPE作为非法 companyCode,但前端 dropdown 默认硬编码只有 HQ;E2E 通过 evaluate 直接修改 Redux state 或 form value 模拟该路径。简化:场景 3 用 page.evaluate 把 dropdown 改为不存在的 code 后 submit。
E2E 前置:测试前必须有运行中的 backend(端口 9090)+ frontend dev server(5173);playwright.config.ts 配 webServer 自动起 frontend dev server,backend 由开发者预先启动(CI 由 scripts/test.sh 协调)。
- Step 1: 写失败测试
frontend/tests/e2e/login.spec.ts - Step 2: 实现最小代码 + Playwright config
- Step 3: 子会话验证 PASS(子会话先启动 backend
mvn spring-boot:run后台 → 跑npx playwright test) - Step 4: Commit
test(frontend): E2E 登录三路径 FE-01
提交计划
| Task | Commit message |
|---|---|
| 1 | feat(frontend): bootstrap Vite + React + AntD + Vitest 骨架 FE-01 |
| 2 | feat(frontend): Design Tokens + AntD ConfigProvider FE-01 |
| 3 | feat(frontend): axios client + BizError + MSW 测试基建 FE-01 |
| 4 | feat(frontend): authApi.login + MSW handlers FE-01 |
| 5 | feat(frontend): authSlice + typed hooks FE-01 |
| 6 | feat(frontend): RequireAuth 守卫 + 路由表 FE-01 |
| 7 | feat(frontend): LoginForm 组件 + 字段校验 FE-01 |
| 8 | feat(frontend): LoginHero + LoginFooter 静态视觉 FE-01 |
| 9 | feat(frontend): LoginPage 组装 + 错误状态机 FE-01 |
| 10 | test(frontend): E2E 登录三路径 FE-01 |