2026-06-01-FE-01.md 31 KB

FE-01 登录页 — 任务级 TDD 计划(前端)

阶段:前端(frontend)。作用域:frontend/**(页面 / 组件 / 路由 / store / api / 样式 / 测试 / 工程配置)。禁止backend/** / sql/** / scripts/**。 上游 SSoT:spec docs/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 Tokens src/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 预加载「版本」下拉项(companiesLoadingidle/empty);取数失败给重试入口 + message.error
  • 用户填用户名 / 密码、选版本后点「登录」→ 前端必填校验通过 → 调 POST /api/usr/login(body { sUserName, password, companyId });提交中 submitting(按钮 loading、字段禁用、防重复提交)。
  • 成功(code=0):拿 token + user 写入 Redux authSlice.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,非 0 code 抛出携带 code 的错误供页面分流文案;网络/超时/5xx 兜底。
  • 错误处理(docs/04 § 2.4):表单校验错误就近 AntD 红字;提交失败按错误码 message.error;版本取数失败空态 + 重试。
  • Design Tokens(docs/04 § 2.1 / spec § 7):语义色(按钮 / 文字 / 边框 / 错误 / 背景)只用 var(--color-*),禁止硬编码 hex/rgba;AntD colorPrimaryConfigProvider 对齐 --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/iconsUserOutlined / 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;lint npm run lint;unit npm run test:unit;e2e npm run test:e2e;另需 dev(Vite dev server,端口 5173)。
  • 端口 / 转发:dev server 端口 5173(config-vars frontend.dev_port),Vite proxy /apihttp://localhost:5172(config-vars backend.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 返回 datacode!==0ApiError(携带 code:number + message:string);无响应(网络/超时/5xx)抛 ApiErrorcode 置统一标识,如 -1 表网络异常)。
    • 请求拦截器:从 localStoragexly_erp_token,存在则注入 Authorization: Bearer <token>;登录 / 版本端点本就放行,无 token 时不注入(自然跳过)。
  • token 持久化键名:localStorage key = xly_erp_token(D6)。集中常量(如 src/api/request.tssrc/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 | > 文案集中在一处映射(如 LoginPageLOGIN_ERROR_MESSAGES: Record<number, string>usrApi/util 常量),按 ApiError.code 查表,未命中走网络异常兜底文案。
  • 表单字段名(AntD Form name,提交时映射到 LoginPayload):sUserName / password / companyId;占位文案:用户名「请输入你的用户名」、密码「请输入你的密码」(沿用原型)。
  • 版本下拉文案:placeholder「请选择版本」;加载中 placeholder「加载版本中…」;空态 notFoundContent「暂无可用版本」。
  • 校验文案(AntD rules.message):用户名「请输入用户名」(BR1);密码「请输入密码」(BR2);版本「请选择版本」(BR4)。
  • 版本下拉 label 规则(D8):返回项 sVersion 非空 → label = `${sCompanyName}(${sVersion})`(全角括号);sVersion 为空/null → label = sCompanyNamevalue 恒取 id(提交作 companyId);列表仅 1 项时默认选中该项(spec § 6.3)。

关键签名(首次出现处给出,跨 task 保持一致)

  • authSlicestore/slices/authSlice.ts):
    • AuthState = { token: string | null; user: AuthUser | null }initialStatelocalStoragexly_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 / clearCredentials actions。 > 注:reducer 内写 localStorage 属副作用,为简洁集中放此(MVP 取舍);若 TDD 期偏好纯 reducer,可改由 LoginPage 在 dispatch 后单独持久化——二者择一,全项目统一(默认前者,登记 D6)。
  • request.ts:导出 axios 实例 request(default)+ TOKEN_STORAGE_KEY 常量 + ApiError(含 code: numbermessage: string)。
  • usrApi.tslogin(payload) / fetchCompanies()(签名见合同级常量)。
  • LoginPagepages/usr/Login/LoginPage.tsx,default export,无 props,路由组件);内部用 useDispatch<AppDispatch>() + useNavigate() + AntD Form.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-loginusr)。

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 /apihttp://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:unitvitest run)/ test:e2eplaywright test)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit 冒烟用例通过;npm run lintnpm run build 可执行(build 至少不因配置错误失败)。
  • 4. commitchore(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 为 ApiErrorerr.code===40101
    • ::throws network ApiError on no-response error——模拟无响应(网络异常),reject 为 ApiErrorcode 为网络异常标识(如 -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. 子会话验证 PASScd frontend && npm run test:unit -- request
  • 4. commitfeat(fe-login): Axios 实例与 Result 拆包/错误拦截 REQ-USR-004

T2 — usrApi.login / fetchCompanies(jsdom 单测)

  • 1. 写失败测试frontend/tests/unit/usrApi.test.tsvi.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. 子会话验证 PASScd frontend && npm run test:unit -- usrApi
  • 4. commitfeat(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——dispatch setCredentials({ 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——预置 localStorage token 后初始化 reducer,token 为该值。
  • 2. 实现最小代码frontend/src/store/slices/authSlice.ts(签名见关键签名)+ frontend/src/store/store.tsconfigureStore,挂 auth reducer,导出 RootState / AppDispatch)。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- authSlice
  • 4. commitfeat(fe-login): authSlice 登录态与 token 持久化 REQ-USR-004

T4 — 登录页布局与区域结构渲染(jsdom 组件测)

  • 1. 写失败测试frontend/tests/unit/LoginPage.layout.test.tsx(用 Redux Provider + MemoryRouter 包裹渲染;mock usrApi.fetchCompanies resolve 空列表避免 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/loginfrontend/src/App.tsx / frontend/src/main.tsx 接入 Provider + Router + ConfigProvider(colorPrimary 对齐 --color-primary);frontend/src/styles/ 入口 import 仓库根 tokens.css。语义色用 var(--color-*),主视觉装饰 scoped。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- LoginPage.layout
  • 4. commitfeat(fe-login): 登录页三段式布局与区域结构 REQ-USR-004

T5 — 版本下拉预加载状态机:loading / idle / empty / 取数失败重试(jsdom 组件测)

  • 1. 写失败测试frontend/tests/unit/LoginPage.companies.test.tsxvi.mock usrApi):
    • ::shows loading placeholder and disabled select while fetching——fetchCompanies pending 时版本 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——fetchCompanies reject,出现「版本加载失败」+ 重试入口;点重试再次调用 fetchCompanies(BR5)。
  • 2. 实现最小代码:在 LoginPage 中加挂载即取数(useEffect)、companies / companiesLoading / companiesError 本地态、label 映射(D8)、单项自动选中、空态与重试逻辑。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- LoginPage.companies
  • 4. commitfeat(fe-login): 版本下拉预加载/空态/重试 REQ-USR-004

T6 — 表单必填校验拦截提交(BR1/BR2/BR4)(jsdom 组件测)

  • 1. 写失败测试frontend/tests/unit/LoginPage.validation.test.tsx(mock usrApi,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 配置 AntD Form rules(三字段 required + 对应 message)、onFinish 组装 LoginPayloadusrApi.login
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- LoginPage.validation
  • 4. commitfeat(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. 实现最小代码LoginPagesubmitting 本地态(提交前置 true、settle 后置 false),按钮 loading={submitting}、字段 disabled,提交时若已 submitting 直接 return。
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- LoginPage.submitting
  • 4. commitfeat(fe-login): 提交中态与防重复提交 REQ-USR-004

T8 — 登录成功:写 authSlice + 持久化 + 跳转 /(success,BR9)(jsdom 组件测)

  • 1. 写失败测试frontend/tests/unit/LoginPage.success.test.tsx(真实 store + useNavigate spy / MemoryRouter 记录跳转;mock usrApi.login resolve { token:'tk', user:{ id:1, sUserName:'admin', sUserType:'超级管理员', sLanguage:'中文' } };mock message):
    • ::dispatches setCredentials and persists token on success——成功后 store auth.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 成功分支 dispatch setCredentials({ token, user })message.success('登录成功')navigate('/', { replace:true })
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- LoginPage.success
  • 4. commitfeat(fe-login): 登录成功落地登录态与跳转 REQ-USR-004

T9 — 登录失败错误码分流文案 + 失败后清空聚焦(error,BR6/BR7/BR8 + D5)(jsdom 组件测)

  • 1. 写失败测试frontend/tests/unit/LoginPage.error.test.tsx(mock usrApi.login reject ApiError;mock message;表单预填合法值):
    • ::40101 shows 用户名或密码错误 and clears+focuses password——reject code:40101message.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.codeLOGIN_ERROR_MESSAGES 表(未命中走网络异常文案)message.error(...)40101/42901form.setFieldValue('password', '') 并聚焦密码框;恢复 submitting=false
  • 3. 子会话验证 PASScd frontend && npm run test:unit -- LoginPage.error
  • 4. commitfeat(fe-login): 登录失败错误码分流文案与失败后处理 REQ-USR-004

T10 — E2E 登录关键旅程(Playwright)

  • 1. 写失败测试frontend/tests/e2e/login.spec.tspage.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.tswebServernpm run devbaseURL 指向 dev server)及任何为可测性需要的最小 data-testid(仅在 RTL 无法稳定定位时添加)。
  • 3. 子会话验证 PASScd frontend && npm run test:e2e -- login(首次需 npx playwright install 装浏览器)。
  • 4. committest(fe-login): 登录页 E2E 关键旅程 REQ-USR-004

T11 — 全量门禁回归 + 收尾(chore)

  • 1. 写失败测试:无新增测试;本任务跑全量验证。
  • 2. 实现最小代码:修复 lint / build / 类型问题(如有);确认语义色全部 var(--color-*)、无硬编码 hex/rgba(主视觉装饰 scoped 除外,D7)。
  • 3. 子会话验证 PASScd frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e 全绿。
  • 4. commitchore(fe-login): FE-01 门禁回归通过 REQ-USR-004

完成判据(Definition of Done)

  1. /login 路由可达,渲染复刻原型 #screen-login 的品牌头 / 深蓝主视觉 + 右侧浮层登录卡 / 页脚三段式(spec § 2 / § 6.1-6.2)。
  2. 状态机 ≥5 态全部覆盖并有测试固化:companiesLoading / idle / empty / submitting / error / success(spec § 3)。
  3. 业务规则 BR1~BR11 在组件层 / E2E 有对应断言(身份真伪 / 禁用 / 限流由后端裁决,前端按返回码渲染,不复制后端逻辑)(spec § 5)。
  4. 消费 GET /api/usr/companies(预加载、空态、重试)与 POST /api/usr/login(成功 / 40001 / 40101 / 40302 / 42901 / 网络异常)按错误码表分流文案(spec § 4),文案逐字一致。
  5. 成功后写 Redux authSlice + 持久化 localStorage[xly_erp_token] + navigate('/', { replace:true })(spec § 6.6 / D3 / D6)。
  6. 统一走 api/request.ts + api/usrApi.ts,不在页面散用 axios(docs/04 § 2.3)。
  7. 语义色只用 var(--color-*),AntD colorPrimary 对齐 --color-primary,主视觉装饰 scoped 不新增全局 token(spec § 7 / D7)。
  8. 全部落点在 frontend/**,无 backend/ / sql/ / scripts/ 改动。
  9. 门禁全绿: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 } 对齐。