FE-01 登录页 — 任务级 TDD 计划(前端)
阶段:前端(frontend)。作用域:
frontend/**(页面 / 组件 / 路由 / store / api / 样式 / 测试 / 工程配置)。禁止写backend/**/sql/**/scripts/**。 上游 SSoT:specdocs/superpowers/specs/2026-06-01-FE-01.md;需求卡片docs/01-需求清单/USR-用户管理/REQ-USR-004.md;API 契约docs/05-API接口契约.md§ REQ-USR-004;原型prototype/erp.html(#screen-login布局/交互权威);技术规范docs/04-技术规范.md§ 零 / § 二;Design Tokenssrc/styles/tokens.css;配置config-vars.yaml。 本计划告诉 TDD 执行者做什么 / 文件边界 / 测试意图 / props 与 API 形状 / 完成判据;具体代码由红-绿-提交循环产出,不在此 dump 整个组件 / 配置文件内容。 本 REQ 是仓库首个前端任务:frontend/目录尚不存在,FE-01 需先搭建最小前端工程骨架(package.json / vite / 测试栈 / Redux / Router / AntD / Axios 封装),再实现登录页。后续 FE-02~FE-04 复用此骨架。
Goal(目标)
在 /login 路由实现登录页 LoginPage,复刻原型 #screen-login 的三段式布局(品牌头 / 深蓝主视觉 + 右侧浮层登录卡 / 页脚版权)与交互语义,但表单校验、提交、版本下拉取数、错误反馈全部真实对接后端:
- 页面挂载即调
GET /api/usr/companies预加载「版本」下拉项(companiesLoading→idle/empty);取数失败给重试入口 +message.error。 - 用户填用户名 / 密码、选版本后点「登录」→ 前端必填校验通过 → 调
POST /api/usr/login(body{ sUserName, password, companyId });提交中submitting(按钮 loading、字段禁用、防重复提交)。 - 成功(
code=0):拿token+user写入 ReduxauthSlice.setCredentials,token 持久化到localStorage(键xly_erp_token),message.success("登录成功"),navigate('/', { replace:true })。 - 失败:按错误码(
40001/40101/40302/42901/网络异常)渲染对应中文文案;40101/42901后清空密码框并聚焦,保留用户名与版本。 - 状态机覆盖 ≥5 态:
companiesLoading/idle/empty/submitting/error/success(spec § 3)。 - 业务规则前端复刻 BR1~BR11(spec § 5),其中身份真伪 / 禁用 / 限流均由后端裁决,前端仅按返回码渲染。
Architecture(架构 / 分层)
遵循 docs/04 § 2.1,前端为仓库根 frontend/ 子项目,包名 xly-erp-web(config-vars frontend.pkg_name)。本 REQ 新建工程骨架 + 登录页相关文件:
frontend/
├── package.json # 【新增】name=xly-erp-web;scripts dev/build/lint/test:unit/test:e2e;依赖 react18 / antd5 / @reduxjs/toolkit / react-redux / react-router-dom@6 / axios / @ant-design/icons + devDeps vite / vitest / jsdom / @testing-library/react|jest-dom|user-event / @playwright/test / typescript / eslint
├── tsconfig.json # 【新增】TS 配置(jsx react-jsx、strict)
├── vite.config.ts # 【新增】React 插件 + server.port=5173 + server.proxy '/api'→http://localhost:5172(D2)+ vitest test 配置(environment jsdom、globals、setupFiles)
├── playwright.config.ts # 【新增】e2e:testDir=tests/e2e,baseURL 取 dev server
├── index.html # 【新增】挂载点 #root + 引入 main.tsx
├── tests/
│ ├── setup.ts # 【新增】vitest setup:import '@testing-library/jest-dom';引入 tokens.css 供 jsdom(可选)
│ ├── unit/ # 【新增】jsdom 组件 / store / api 单测(Vitest + RTL)
│ └── e2e/ # 【新增】Playwright E2E
├── src/
│ ├── main.tsx # 【新增】入口:Redux Provider + BrowserRouter + AntD ConfigProvider(theme.colorPrimary=var(--color-primary) 取值) + 引入 styles/tokens 与全局样式
│ ├── App.tsx # 【新增】挂载路由 <AppRouter/>
│ ├── router/index.tsx # 【新增】React Router v6 路由表:/login → LoginPage;'/' 占位(FE-02 落地,本 REQ 仅需 /login 可达 + 成功后 navigate('/'))
│ ├── store/store.ts # 【新增】configureStore({ reducer: { auth: authReducer } }) + 导出 RootState / AppDispatch
│ ├── store/slices/authSlice.ts # 【新增】authSlice:state { token, user };reducer setCredentials / clearCredentials
│ ├── api/request.ts # 【新增】Axios 实例:baseURL '/api';请求拦截器(已登录注入 Authorization: Bearer,登录端点放行);响应拦截器(拆 Result:code=0 取 data,非 0 抛 ApiError,网络异常兜底)
│ ├── api/usrApi.ts # 【新增】login(payload) / fetchCompanies();调 request.ts,返回 data
│ ├── pages/usr/Login/LoginPage.tsx # 【新增】登录页根组件(容器 + 状态机 + 提交逻辑)
│ ├── pages/usr/Login/components/ # 【新增】LoginHeader / LoginHero / LoginCard / LoginForm 等区域子组件(按需拆分;纯展示子组件可内联)
│ ├── pages/usr/Login/Login.module.css # 【新增】登录页 scoped 样式:语义色用 var(--color-*);主视觉深蓝渐变/网格为局部装饰(D7,不新增全局 token)
│ └── styles/ # 【新增】全局样式入口,import 仓库根 ../../src/styles/tokens.css(Design Tokens SSoT,见 D9)
└── (eslint 配置 .eslintrc.cjs 或 eslint.config.js 按 vite-react 模板)
-
跨模块 / 跨阶段:本 REQ 落点全在
frontend/**,不触backend//sql//scripts/。新建的store/router/api/request.ts/main.tsx/ 工程配置属全前端共享骨架,FE-02~FE-04 复用(非 FE-01 私有),在《模块完成报告》留痕「FE-01 搭建前端工程骨架,后续 FE 复用」。 -
状态管理(docs/04 § 2.2):全局登录态(token + user)进 Redux
authSlice;页面内瞬时态(companiesLoading/companies列表 /empty/submitting)就近用useState,不塞全局。 -
请求封装(docs/04 § 2.3):统一走
api/request.ts,页面只调usrApi.ts封装方法,不散用 axios。响应拦截器拆Result,非 0code抛出携带code的错误供页面分流文案;网络/超时/5xx 兜底。 -
错误处理(docs/04 § 2.4):表单校验错误就近 AntD 红字;提交失败按错误码
message.error;版本取数失败空态 + 重试。 -
Design Tokens(docs/04 § 2.1 / spec § 7):语义色(按钮 / 文字 / 边框 / 错误 / 背景)只用
var(--color-*),禁止硬编码 hex/rgba;AntDcolorPrimary经ConfigProvider对齐--color-primary;主视觉深蓝渐变 / 网格透视为登录页局部装饰,scoped 保留,不挪用语义 token、不新增全局 token(D7)。
Tech Stack(技术栈,源自 docs/04 § 零 + config-vars)
- React 18.x / Ant Design 5.x / Redux Toolkit(最新稳定)/ React Router v6 / Vite(最新稳定)/ Axios(最新稳定)/ TypeScript;
@ant-design/icons(UserOutlined/LockOutlined)。 - 测试:单测 Vitest(jsdom 环境)+
@testing-library/react/@testing-library/jest-dom/@testing-library/user-event;E2E Playwright(@playwright/test)。 - 命令(docs/04 § 零,package.json scripts 必须提供):build
npm run build;lintnpm run lint;unitnpm run test:unit;e2enpm run test:e2e;另需dev(Vite dev server,端口 5173)。 - 端口 / 转发:dev server 端口
5173(config-varsfrontend.dev_port),Vite proxy/api→http://localhost:5172(config-varsbackend.http_port,D2);不硬编码到组件,集中在vite.config.ts。
合同级常量(跨 task 必须一致)
- 路由 path:
/login(登录页);登录成功跳转目标/(D3,{ replace: true })。 - API client 签名(
api/usrApi.ts,跨 task 一致):-
login(payload: LoginPayload): Promise<LoginResult>,其中LoginPayload = { sUserName: string; password: string; companyId: number };LoginResult = { token: string; user: AuthUser };AuthUser = { id: number; sUserName: string; sUserType: string; sLanguage: string }(对齐 docs/05 § REQ-USR-004 响应Result<{ token, user:{ id, sUserName, sUserType, sLanguage } }>)。 -
fetchCompanies(): Promise<CompanyOption[]>,其中CompanyOption = { id: number; sCompanyName: string; sVersion: string | null }(对齐 spec § 4 / D8;sVersion可空)。
-
- HTTP 形状(
api/request.ts):-
baseURL = '/api';登录端点POST /api/usr/login(最终 path/usr/login)放行、不带Authorization;版本端点GET /api/usr/companies(最终 path/usr/companies)放行。 - 后端统一响应
Result<T> = { code: number; message: string; data: T }(docs/04 § 1.4);响应拦截器:code===0返回data;code!==0抛ApiError(携带code:number+message:string);无响应(网络/超时/5xx)抛ApiError(code置统一标识,如-1表网络异常)。 - 请求拦截器:从
localStorage读xly_erp_token,存在则注入Authorization: Bearer <token>;登录 / 版本端点本就放行,无 token 时不注入(自然跳过)。
-
- token 持久化键名:
localStoragekey =xly_erp_token(D6)。集中常量(如src/api/request.ts或src/store/slices/authSlice.ts导出TOKEN_STORAGE_KEY),跨 task 引用同一常量,不各处写字面量。 - 错误码 → 前端文案(对齐 spec § 4 / docs/05;登录失败分流,严格沿用,不得细化
40101): | code | 前端文案 | 展示方式 | |---|---|---| |0| 「登录成功」 |message.success后跳转 | |40001| 「请填写用户名、密码并选择版本」 |message.error(兜底,正常前端校验已拦截) | |40101| 「用户名或密码错误」 |message.error+ 清空密码框聚焦(不细化为「账号不存在」等枚举文案,BR6) | |40302| 「该账号已被禁用,请联系管理员」 |message.error| |42901| 「登录尝试过于频繁,请稍后再试」 |message.error+ 清空密码框聚焦 | | 网络/超时/5xx | 「网络异常,请稍后重试」 | 响应拦截器兜底message.error| > 文案集中在一处映射(如LoginPage内LOGIN_ERROR_MESSAGES: Record<number, string>或usrApi/util 常量),按ApiError.code查表,未命中走网络异常兜底文案。 - 表单字段名(AntD
Formname,提交时映射到LoginPayload):sUserName/password/companyId;占位文案:用户名「请输入你的用户名」、密码「请输入你的密码」(沿用原型)。 - 版本下拉文案:
placeholder「请选择版本」;加载中placeholder「加载版本中…」;空态notFoundContent「暂无可用版本」。 - 校验文案(AntD
rules.message):用户名「请输入用户名」(BR1);密码「请输入密码」(BR2);版本「请选择版本」(BR4)。 - 版本下拉 label 规则(D8):返回项
sVersion非空 → label =`${sCompanyName}(${sVersion})`(全角括号);sVersion为空/null → label =sCompanyName;value恒取id(提交作companyId);列表仅 1 项时默认选中该项(spec § 6.3)。
关键签名(首次出现处给出,跨 task 保持一致)
-
authSlice(store/slices/authSlice.ts):-
AuthState = { token: string | null; user: AuthUser | null },initialState从localStorage读xly_erp_token初始化token(user 初始 null)。 -
setCredentials(state, action: PayloadAction<{ token: string; user: AuthUser }>):写 token + user,并localStorage.setItem(TOKEN_STORAGE_KEY, token)。 -
clearCredentials(state):清 token + user,并localStorage.removeItem(TOKEN_STORAGE_KEY)。 - 导出
authReducer(default)、setCredentials/clearCredentialsactions。 > 注:reducer 内写 localStorage 属副作用,为简洁集中放此(MVP 取舍);若 TDD 期偏好纯 reducer,可改由LoginPage在 dispatch 后单独持久化——二者择一,全项目统一(默认前者,登记 D6)。
-
-
request.ts:导出 axios 实例request(default)+TOKEN_STORAGE_KEY常量 +ApiError(含code: number、message: string)。 -
usrApi.ts:login(payload)/fetchCompanies()(签名见合同级常量)。 -
LoginPage(pages/usr/Login/LoginPage.tsx,default export,无 props,路由组件);内部用useDispatch<AppDispatch>()+useNavigate()+ AntDForm.useForm()+App.useApp()/message(AntD 5 推荐App包裹用message实例,否则用静态message——择一统一)。 - 区域子组件(若拆分,均在
pages/usr/Login/components/,纯展示,props 明确):-
LoginHeader(无 props 或{ brandName?: string }):渲染 Logo SVG + 「Antler ERP」+ 「欢迎登录EBC平台」。 -
LoginHero({ children: ReactNode }或无):深蓝主视觉容器 + 标语文本,children槽放登录卡。 -
LoginCard({ children }):右侧浮层卡片容器 + 标题「用户登录」。 -
LoginFooter(无 props):版权 / 备案号文本。 - 表单逻辑(取数 / 提交 / 状态)集中在
LoginPage(或抽LoginForm子组件接收{ companies, companiesLoading, companiesError, submitting, onSubmit, onRetryCompanies, formRef }props);抽与不抽由 TDD 期决定,但若抽LoginForm,其 props 契约以此为准,跨 task 一致。
-
测试栈说明
-
jsdom 组件 / store / api 单测(Vitest + RTL):默认 mock
api/usrApi或 axios(用vi.mock)以隔离网络;断言渲染、交互、状态切换、dispatch、navigate、message 文案。覆盖 spec § 3 状态机与 § 5 BR1~BR11 中可在组件层验证者。 -
Playwright E2E:覆盖关键用户旅程(页面可达 + 校验拦截 + 成功跳转),通过拦截/桩
**/api/usr/companies与**/api/usr/login路由响应(page.route)模拟后端,不依赖真实后端起服。
任务列表(每个 task = red → green → 子会话验证 → commit)
硬护栏:以下每个
impl_file/test_file均以frontend/开头;无任何backend//sql//scripts/落点。 提交信息格式:<type>(<scope>): <subject> REQ-USR-004(FE-01 关联 REQ-USR-004;scope 用fe-login或usr)。
T0 — 前端工程骨架可启动 + 测试栈可运行(chore,先建地基)
- 1. 写失败测试:
frontend/tests/unit/smoke.test.tsx::renders a trivial component——一个最小冒烟用例(渲染<div>ok</div>或一个空App),用于驱动 Vitest + jsdom + RTL 配置就绪;初始因无工程 / 无配置而失败。 - 2. 实现最小代码:
frontend/package.json+frontend/tsconfig.json+frontend/vite.config.ts(含 vitest test 配置 environment=jsdom / globals=true / setupFiles=tests/setup.ts、server.port=5173、proxy/api→http://localhost:5172)+frontend/tests/setup.ts(import@testing-library/jest-dom)+frontend/index.html+frontend/src/main.tsx(暂最小,可仅挂载占位)+frontend/eslint配置。安装依赖(npm i)。scripts 提供dev/build/lint/test:unit(vitest run)/test:e2e(playwright test)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit冒烟用例通过;npm run lint与npm run build可执行(build 至少不因配置错误失败)。 - 4. commit:
chore(fe-login): 初始化前端工程骨架与测试栈 REQ-USR-004
T1 — Axios 请求封装拆 Result + 拦截器(jsdom 单测)
- 1. 写失败测试:
frontend/tests/unit/request.test.ts:-
::baseURL is /api——实例baseURL === '/api'。 -
::unwraps data when code is 0——mock 适配器返回{ code:0, message:'success', data:{ foo:1 } },request.get(...)resolve 为{ foo:1 }(拆 data)。 -
::throws ApiError carrying business code when code is non-zero——返回{ code:40101, message:'认证失败', data:null },调用 reject 为ApiError且err.code===40101。 -
::throws network ApiError on no-response error——模拟无响应(网络异常),reject 为ApiError,code为网络异常标识(如-1)。 -
::injects Authorization header when token present——localStorage预置xly_erp_token,请求拦截器把Authorization: 'Bearer <token>'加到 config.headers;无 token 时不加。
-
- 2. 实现最小代码:
frontend/src/api/request.ts——导出 axios 实例(baseURL:'/api')、TOKEN_STORAGE_KEY='xly_erp_token'、ApiError;请求拦截器注入 token,响应拦截器拆Result/ 抛ApiError/ 网络兜底。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- request - 4. commit:
feat(fe-login): Axios 实例与 Result 拆包/错误拦截 REQ-USR-004
T2 — usrApi.login / fetchCompanies(jsdom 单测)
- 1. 写失败测试:
frontend/tests/unit/usrApi.test.ts(vi.mock('../../src/api/request')桩实例):-
::login posts to /usr/login with sUserName/password/companyId——断言request.post收到 path/usr/login与 body{ sUserName, password, companyId },返回值透传为{ token, user }。 -
::fetchCompanies gets /usr/companies and returns list——断言request.get('/usr/companies'),返回CompanyOption[]。
-
- 2. 实现最小代码:
frontend/src/api/usrApi.ts——login(payload)/fetchCompanies()(签名见合同级常量)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- usrApi - 4. commit:
feat(fe-login): usrApi 登录与版本下拉取数封装 REQ-USR-004
T3 — authSlice setCredentials / clearCredentials + token 持久化(jsdom 单测)
- 1. 写失败测试:
frontend/tests/unit/authSlice.test.ts:-
::setCredentials stores token and user and persists token——dispatchsetCredentials({ token:'t', user:{...} }),state.token/user 更新,且localStorage.getItem('xly_erp_token')==='t'。 -
::clearCredentials clears state and removes persisted token——清空 state 且localStorage该键被移除。 -
::initialState reads persisted token——预置localStoragetoken 后初始化 reducer,token为该值。
-
- 2. 实现最小代码:
frontend/src/store/slices/authSlice.ts(签名见关键签名)+frontend/src/store/store.ts(configureStore,挂authreducer,导出RootState/AppDispatch)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- authSlice - 4. commit:
feat(fe-login): authSlice 登录态与 token 持久化 REQ-USR-004
T4 — 登录页布局与区域结构渲染(jsdom 组件测)
- 1. 写失败测试:
frontend/tests/unit/LoginPage.layout.test.tsx(用 Redux Provider + MemoryRouter 包裹渲染;mockusrApi.fetchCompaniesresolve 空列表避免 act 警告):-
::renders brand header / hero slogan / footer——存在品牌名「Antler ERP」、副标题「欢迎登录EBC平台」、主视觉中文「企业业务能力平台」与「ERP」、页脚版权文本(含备案号)。 -
::renders login card title 用户登录——卡片标题文案存在。 -
::renders username/password/version fields and submit button 登 录——用户名Input(占位「请输入你的用户名」)、密码Input.Password(占位「请输入你的密码」、type掩码,BR3)、版本Select、提交按钮文案「登 录」。
-
- 2. 实现最小代码:
frontend/src/pages/usr/Login/LoginPage.tsx(+ 按需components/区域子组件、Login.module.css);frontend/src/router/index.tsx挂/login;frontend/src/App.tsx/frontend/src/main.tsx接入 Provider + Router +ConfigProvider(colorPrimary 对齐--color-primary);frontend/src/styles/入口 import 仓库根 tokens.css。语义色用var(--color-*),主视觉装饰 scoped。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- LoginPage.layout - 4. commit:
feat(fe-login): 登录页三段式布局与区域结构 REQ-USR-004
T5 — 版本下拉预加载状态机:loading / idle / empty / 取数失败重试(jsdom 组件测)
- 1. 写失败测试:
frontend/tests/unit/LoginPage.companies.test.tsx(vi.mockusrApi):-
::shows loading placeholder and disabled select while fetching——fetchCompaniespending 时版本Select处 loading/禁用、placeholder含「加载版本中」(companiesLoading态)。 -
::renders options with label rule on resolve (idle)——resolve[{id:1,sCompanyName:'甲公司',sVersion:'标准版'},{id:2,sCompanyName:'乙公司',sVersion:null}],下拉项 label 分别为「甲公司(标准版)」「乙公司」(D8);展开可见。 -
::auto-selects when single option——resolve 仅 1 项时该项默认选中(spec § 6.3)。 -
::empty state when companies is empty——resolve[],Select空态文案「暂无可用版本」+ 轻量提示「未获取到可登录版本,请联系管理员」(empty态)。 -
::shows error with retry when fetch fails——fetchCompaniesreject,出现「版本加载失败」+ 重试入口;点重试再次调用fetchCompanies(BR5)。
-
- 2. 实现最小代码:在
LoginPage中加挂载即取数(useEffect)、companies/companiesLoading/companiesError本地态、label 映射(D8)、单项自动选中、空态与重试逻辑。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- LoginPage.companies - 4. commit:
feat(fe-login): 版本下拉预加载/空态/重试 REQ-USR-004
T6 — 表单必填校验拦截提交(BR1/BR2/BR4)(jsdom 组件测)
- 1. 写失败测试:
frontend/tests/unit/LoginPage.validation.test.tsx(mockusrApi,companies 预置非空):-
::blocks submit and shows required messages when empty——直接点「登录」,出现「请输入用户名」「请输入密码」「请选择版本」三条校验红字,且usrApi.login未被调用(BR1/BR2/BR4 + spec § 5)。 -
::submits with payload when all filled——填全三项后点登录,usrApi.login收到{ sUserName, password, companyId }(companyId 为所选项 id)。
-
- 2. 实现最小代码:在
LoginForm/LoginPage配置 AntDFormrules(三字段 required + 对应 message)、onFinish组装LoginPayload调usrApi.login。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- LoginPage.validation - 4. commit:
feat(fe-login): 登录表单必填校验与提交装配 REQ-USR-004
T7 — 提交中态防重复提交(submitting,BR10)(jsdom 组件测)
- 1. 写失败测试:
frontend/tests/unit/LoginPage.submitting.test.tsx:-
::button loading and fields disabled while submitting——usrApi.login返回 pending Promise,提交后按钮loading且三字段禁用(submitting态)。 -
::ignores duplicate submit while pending——pending 期间再次点击 / 回车,usrApi.login仅被调用 1 次(防重复提交,BR10)。
-
- 2. 实现最小代码:
LoginPage加submitting本地态(提交前置 true、settle 后置 false),按钮loading={submitting}、字段disabled,提交时若已submitting直接 return。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- LoginPage.submitting - 4. commit:
feat(fe-login): 提交中态与防重复提交 REQ-USR-004
T8 — 登录成功:写 authSlice + 持久化 + 跳转 /(success,BR9)(jsdom 组件测)
- 1. 写失败测试:
frontend/tests/unit/LoginPage.success.test.tsx(真实 store +useNavigatespy / MemoryRouter 记录跳转;mockusrApi.loginresolve{ token:'tk', user:{ id:1, sUserName:'admin', sUserType:'超级管理员', sLanguage:'中文' } };mockmessage):-
::dispatches setCredentials and persists token on success——成功后 storeauth.token==='tk'、auth.user写入,localStorage.xly_erp_token==='tk'。 -
::shows success message and navigates to '/' with replace——message.success('登录成功')被调,navigate('/', { replace:true })被调(D3)。
-
- 2. 实现最小代码:
onFinish成功分支 dispatchsetCredentials({ token, user })、message.success('登录成功')、navigate('/', { replace:true })。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- LoginPage.success - 4. commit:
feat(fe-login): 登录成功落地登录态与跳转 REQ-USR-004
T9 — 登录失败错误码分流文案 + 失败后清空聚焦(error,BR6/BR7/BR8 + D5)(jsdom 组件测)
- 1. 写失败测试:
frontend/tests/unit/LoginPage.error.test.tsx(mockusrApi.loginrejectApiError;mockmessage;表单预填合法值):-
::40101 shows 用户名或密码错误 and clears+focuses password——rejectcode:40101,message.error('用户名或密码错误'),密码框被清空且获焦(BR6 + D5)。 -
::40302 shows 该账号已被禁用,请联系管理员——message.error文案匹配(BR7)。 -
::42901 shows 登录尝试过于频繁,请稍后再试 and clears password——文案匹配 + 密码清空(BR8 + D5)。 -
::40001 shows 请填写用户名、密码并选择版本——兜底文案(spec § 4)。 -
::network error shows 网络异常,请稍后重试——reject 网络异常ApiError(如code:-1),兜底文案。 -
::button recovers clickable and username/version preserved after failure——失败后按钮恢复可点、用户名与版本保留(spec § 3 error 态 / D5)。
-
- 2. 实现最小代码:
onFinish失败分支按err.code查LOGIN_ERROR_MESSAGES表(未命中走网络异常文案)message.error(...);40101/42901后form.setFieldValue('password', '')并聚焦密码框;恢复submitting=false。 - 3. 子会话验证 PASS:
cd frontend && npm run test:unit -- LoginPage.error - 4. commit:
feat(fe-login): 登录失败错误码分流文案与失败后处理 REQ-USR-004
T10 — E2E 登录关键旅程(Playwright)
- 1. 写失败测试:
frontend/tests/e2e/login.spec.ts(page.route桩**/api/usr/companies返回{code:0,...,data:[...]}、**/api/usr/login按用例返回成功 /40101):-
::loads /login and shows version options——访问/login,版本下拉渲染桩返回项。 -
::blocks submit with validation when empty——空提交看到必填校验提示,未发起 login 请求。 -
::successful login navigates away from /login——填全 + 桩成功响应 → URL 离开/login(到/)、可见「登录成功」提示。 -
::failed login stays on /login with error——桩40101→ 停留/login、可见「用户名或密码错误」。
-
- 2. 实现最小代码:补齐
playwright.config.ts(webServer起npm run dev、baseURL指向 dev server)及任何为可测性需要的最小data-testid(仅在 RTL 无法稳定定位时添加)。 - 3. 子会话验证 PASS:
cd frontend && npm run test:e2e -- login(首次需npx playwright install装浏览器)。 - 4. commit:
test(fe-login): 登录页 E2E 关键旅程 REQ-USR-004
T11 — 全量门禁回归 + 收尾(chore)
- 1. 写失败测试:无新增测试;本任务跑全量验证。
- 2. 实现最小代码:修复 lint / build / 类型问题(如有);确认语义色全部
var(--color-*)、无硬编码 hex/rgba(主视觉装饰 scoped 除外,D7)。 - 3. 子会话验证 PASS:
cd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e全绿。 - 4. commit:
chore(fe-login): FE-01 门禁回归通过 REQ-USR-004
完成判据(Definition of Done)
-
/login路由可达,渲染复刻原型#screen-login的品牌头 / 深蓝主视觉 + 右侧浮层登录卡 / 页脚三段式(spec § 2 / § 6.1-6.2)。 - 状态机 ≥5 态全部覆盖并有测试固化:
companiesLoading/idle/empty/submitting/error/success(spec § 3)。 - 业务规则 BR1~BR11 在组件层 / E2E 有对应断言(身份真伪 / 禁用 / 限流由后端裁决,前端按返回码渲染,不复制后端逻辑)(spec § 5)。
- 消费
GET /api/usr/companies(预加载、空态、重试)与POST /api/usr/login(成功 /40001/40101/40302/42901/ 网络异常)按错误码表分流文案(spec § 4),文案逐字一致。 - 成功后写 Redux
authSlice+ 持久化localStorage[xly_erp_token]+navigate('/', { replace:true })(spec § 6.6 / D3 / D6)。 - 统一走
api/request.ts+api/usrApi.ts,不在页面散用 axios(docs/04 § 2.3)。 - 语义色只用
var(--color-*),AntDcolorPrimary对齐--color-primary,主视觉装饰 scoped 不新增全局 token(spec § 7 / D7)。 - 全部落点在
frontend/**,无backend//sql//scripts/改动。 - 门禁全绿:
npm run lint/npm run build/npm run test:unit/npm run test:e2e(docs/04 § 零)。
自审记录
-
占位符扫描:本计划无
【人工填写:】/TBD/TODO占位。 - spec coverage:spec § 2 组件树→T4;§ 3 状态机→T5(loading/idle/empty)/T7(submitting)/T8(success)/T9(error);§ 4 端点与错误码→T1/T2/T8/T9/T10;§ 5 BR1-BR11→T6(BR1/2/4)/T4(BR3)/T5(BR5)/T9(BR6/7/8)/T8(BR9)/T7(BR10)/T2+T6(BR11,前端原样提交不处理密码);§ 6 交互→T4/T5/T8/T9;§ 7 tokens→T4/T11;§ 8 decisions D1-D8 已在合同级常量 / 架构中落实,新增 D9(本计划层)见下。
-
本计划新增决策 D9(tokens.css 复用路径):Design Tokens SSoT 为仓库根
src/styles/tokens.css(docs/04 § 2.1,由 skeleton-gen 生成),前端工程在frontend/子目录内。本计划选择由frontend/src/styles/入口以相对路径import '../../../src/styles/tokens.css'(或经 Vite alias)复用该唯一文件,不在frontend/内拷贝一份,避免双 SSoT 漂移;依据 docs/04 § 2.1「色值单一来源在仓库根src/styles/tokens.css,前端在 main.tsx/全局样式中引入」。置信度 high。该决策亦登记进返回的 decisions[]。 -
类型一致性:
LoginPayload/LoginResult/AuthUser/CompanyOption/ApiError/TOKEN_STORAGE_KEY/ 错误码文案表 / 路由 path / API path 跨 T1-T10 一致;与 docs/05 § REQ-USR-004 响应形状(token+user{ id, sUserName, sUserType, sLanguage })及 companies{ id, sCompanyName, sVersion }对齐。