Commit 3d923ac91c672cc969f8a71e13257cc0a2517f38

Authored by zichun
1 parent 236c42b1

docs(spec:FE-01): 派生规格

docs/superpowers/specs/2026-06-01-FE-01.md 0 → 100644
  1 +# FE-01 登录页 — 实现规格(前端)
  2 +
  3 +> 阶段:前端(frontend)。作用域限定 `frontend/` 下的页面 / 组件 / 路由 / store / api / 样式。
  4 +> SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-004.md`;原型 `prototype/erp.html`(`#screen-login` 区域,布局/交互权威);API 契约 `docs/05-API接口契约.md` § REQ-USR-004;技术规范 `docs/04-技术规范.md` § 零 / § 二;Design Tokens `src/styles/tokens.css`。
  5 +> 本规格只消费已锁定事实。后端身份认证、BCrypt 比对、JWT 签发、限流等业务逻辑全部在后端(见 REQ-USR-004 后端规格),前端只负责采集输入、提交、依据响应/错误码渲染状态与文案。
  6 +
  7 +---
  8 +
  9 +## 1. 关联 REQ + 关联原型
  10 +
  11 +| 维度 | 内容 |
  12 +|---|---|
  13 +| 业务功能 | FE-01 登录页(用户名/密码/版本下拉登录) |
  14 +| 关联 REQ | REQ-USR-004 登录用户(主);其「版本」下拉数据依赖后端 `GET /api/usr/companies`(REQ-USR-004 后端补齐的配套只读端点) |
  15 +| 关联原型 | `prototype/erp.html` → `<section id="screen-login">`(含 `.login-wrap` / `.login-head` / `.login-hero` / `.login-card` / `.login-foot`) |
  16 +| 路由 | `/login`(React Router v6)。登录成功后跳转主页落地路由(属 FE-02 范畴,本页只负责导航跳转动作,目标路径默认 `/`,见 § 7 决策 D3) |
  17 +| 落地组件目录 | `frontend/src/pages/usr/Login/`(页面);登录态写入 `frontend/src/store/slices/authSlice`;接口走 `frontend/src/api/usrApi.ts` + `frontend/src/api/request.ts` |
  18 +
  19 +> 原型 `#screen-login` 用纯静态 HTML + 内联 demo 脚本(`goTo('login')` 默认进登录页、`.submit[data-go=main]` 点击直接切主页、`#ver-drop` 点击切换 `.open` 展开版本项)模拟交互。本规格按 React + AntD 5 复刻其**布局与交互语义**,但表单校验、提交、下拉取数、错误反馈改为真实对接后端。
  20 +
  21 +---
  22 +
  23 +## 2. 组件树(按区域分块,推导自 prototype DOM)
  24 +
  25 +页面根 `LoginPage`(路由 `/login` 挂载),结构对应原型 `.login-wrap`(占满视口、纵向 flex:头部 / 主视觉 / 页脚):
  26 +
  27 +```
  28 +LoginPage (容器,对应 .login-wrap:position 占满、flex column、背景 --color-bg-base)
  29 +├── LoginHeader (对应 .login-head:左 Logo SVG + 品牌名「Antler ERP」+ 副标题「欢迎登录EBC平台」)
  30 +│ ├── BrandLogo (鹿角 SVG,复用原型 inline svg path)
  31 +│ ├── BrandName ("Antler ERP")
  32 +│ └── BrandSub ("欢迎登录EBC平台")
  33 +├── LoginHero (对应 .login-hero:占满剩余高度的主视觉区,深蓝渐变 + 网格透视背景)
  34 +│ ├── HeroText (对应 .login-text:英文标语 / 中文「企业业务能力平台」/ 巨型 "ERP")
  35 +│ └── LoginCard (对应 .login-card:右侧浮层登录卡片,AntD <Card> 或 <Form> 容器)
  36 +│ ├── CardTitle ("用户登录")
  37 +│ └── LoginForm (AntD <Form>,提交触发认证)
  38 +│ ├── Form.Item[sUserName] → <Input prefix={用户图标}> 占位「请输入你的用户名」
  39 +│ ├── Form.Item[password] → <Input.Password prefix={锁图标}> 占位「请输入你的密码」(输入显示星号)
  40 +│ ├── Form.Item[companyId] → <Select>(版本下拉,options 来自 GET /api/usr/companies)
  41 +│ └── SubmitButton → <Button type="primary" htmlType="submit" block loading={submitting}> "登 录"
  42 +└── LoginFooter (对应 .login-foot:版权 / 备案号文本条,置底)
  43 +```
  44 +
  45 +- 控件选型(依据 `docs/04 § 零` `frontend.ui_lib = Ant Design 5.x`):
  46 + - 用户名 → `Input`,带 `prefix` 用户图标(`@ant-design/icons` `UserOutlined`)。
  47 + - 密码 → `Input.Password`,带 `prefix` 锁图标(`LockOutlined`),AntD 默认掩码显示,满足卡片「输入显示星号」。
  48 + - 版本 → `Select`(单选),`options` 由 `GET /api/usr/companies` 返回项映射(`label=sCompanyName`(含 `sVersion` 时拼接展示,见 § 6 规则)`value=id`)。原型用自定义 `.lf.dropdown` 模拟,本规格以 AntD `Select` 等价复刻下拉交互。
  49 + - 登录按钮 → `Button type="primary" block`,提交中置 `loading`。
  50 +- 整页用 `Form`(`onFinish` 触发提交,`onFinishFailed` 不阻断——校验失败 AntD 就近红字提示)。
  51 +- 页面顶栏(`#topbar`)在登录态隐藏(原型 `goTo('login')` 即 `topbar.display='none'`);本规格中登录页是独立路由,不渲染应用顶栏 / 导航壳(顶栏属 FE-02)。
  52 +
  53 +---
  54 +
  55 +## 3. 页面状态机(≥5 态)
  56 +
  57 +| 状态 | 触发时机 | UI 表现 |
  58 +|---|---|---|
  59 +| `companiesLoading`(版本下拉加载中) | 页面挂载即调 `GET /api/usr/companies` 拉版本项(卡片「预加载=页面加载时」) | 版本 `Select` 置 `loading` 且禁用,`placeholder="加载版本中…"`;用户名/密码可先填 |
  60 +| `idle`(正常待输入) | 版本项加载完成、表单未提交 | 三字段可编辑,版本 `Select` 展示选项;登录按钮可点;无错误提示 |
  61 +| `empty`(版本列表为空) | `GET /api/usr/companies` 返回 `data` 为空数组 | 版本 `Select` 显示空态 `notFoundContent="暂无可用版本"`;版本必填校验仍生效,无法提交(见 § 5);下方轻量提示「未获取到可登录版本,请联系管理员」 |
  62 +| `submitting`(表单提交中) | 点击「登录」且前端校验通过,`POST /api/usr/login` 进行中 | 登录按钮 `loading` 且禁用,三字段禁用(防重复提交);拦截重复回车提交 |
  63 +| `error`(登录失败 / 取数失败) | 登录接口返回非 0 `code`(40001/40101/40302/42901)或网络异常;或版本接口取数失败 | 按 § 4 文案规则在卡片内/全局 `message.error` 展示对应文案;按钮恢复可点;密码框清空并聚焦(见 § 6 规则 5);版本取数失败时给「版本加载失败,点击重试」入口 |
  64 +| `success`(登录成功) | 登录接口返回 `code=0`,拿到 `token` + `user` | 写入 `authSlice`(token + user)→ 持久化 token(见 § 6 规则 6)→ `message.success("登录成功")` → `navigate(目标路由, { replace:true })`(默认 `/`) |
  65 +
  66 +> 状态以本地组件态 + RTK `authSlice` 表达:`submitting` 用本地 `useState` / `Form` 提交态;`companiesLoading` / `empty` 用本地态;`success` 时 dispatch `authSlice` 的 `setCredentials`。
  67 +
  68 +---
  69 +
  70 +## 4. 消费的后端端点(对齐 docs/05)
  71 +
  72 +| 端点 | 方法 | 触发 | 请求 | 成功响应(取用字段) | 失败处理 |
  73 +|---|---|---|---|---|---|
  74 +| `/api/usr/login` | POST | 点击「登录」且校验通过 | JSON `{ sUserName, password, companyId }` | `Result<{ token, user:{ id, sUserName, sUserType, sLanguage } }>`(`code=0`) | 见下错误码表 |
  75 +| `/api/usr/companies` | GET | 页面挂载时(版本下拉预加载) | 无参 | `Result<List<{ id, sCompanyName, sVersion }>>`(`code=0`) | 取数失败 → 版本 `Select` 空 + 重试入口 + `message.error("版本加载失败")` |
  76 +
  77 +> `/api/usr/companies` 在 docs/05 主清单未单列,但 REQ-USR-004 后端规格(`docs/superpowers/specs/2026-06-01-REQ-USR-004.md` § 8 D1)明确补齐该放行只读端点专供登录「版本」下拉取数;前端据此消费(见 § 7 决策 D1)。
  78 +
  79 +请求 / 响应约定(依据 `docs/04 § 1.4 / § 2.3 / § 2.4`):
  80 +- 统一走 `frontend/src/api/request.ts` 的 Axios 实例(`baseURL` 指向 `/api`,端口取 `config-vars.yaml backend.http_port=5172`,开发期经 Vite proxy 转发,见 § 7 决策 D2)。
  81 +- 响应拦截器拆 `Result`:`code=0` 取 `data`;非 0 `code` 抛出供页面按错误码分流文案;登录端点放行、不带 `Authorization` 头(请求拦截器仅对已登录态注入 token)。
  82 +
  83 +### 错误码 → 前端文案(对齐 docs/05 § REQ-USR-004)
  84 +
  85 +| code | 含义(后端) | 前端文案 | 展示方式 |
  86 +|---|---|---|---|
  87 +| `0` | 成功 | 「登录成功」 | `message.success` 后跳转 |
  88 +| `40001` | 参数校验失败(缺用户名/密码/版本,或 companyId 非法) | 「请填写用户名、密码并选择版本」 | 卡片内 `Alert`/`message.error`;正常情况下前端必填校验已拦截,此为兜底 |
  89 +| `40101` | 认证失败(用户名或密码错误,不区分以防枚举) | 「用户名或密码错误」 | `message.error` + 密码框清空聚焦 |
  90 +| `40302` | 账号已禁用(iIsVoid=1) | 「该账号已被禁用,请联系管理员」 | `message.error` |
  91 +| `42901` | 登录过于频繁(连续失败超阈值被限流) | 「登录尝试过于频繁,请稍后再试」 | `message.error` |
  92 +| 网络/超时/5xx | 请求异常 | 「网络异常,请稍后重试」 | 响应拦截器兜底 `message.error` |
  93 +
  94 +> `40101` 严格沿用后端「不区分账号不存在/密码错误」的统一提示,前端**不得**自行细化为「该用户不存在」等可枚举文案(卡片边界 + REQ-USR-004 后端规格规则 2/3)。
  95 +
  96 +---
  97 +
  98 +## 5. 业务规则前端复刻清单(逐条)
  99 +
  100 +| # | 规则 | 触发时机 | 前端报错文案 | 来源 |
  101 +|---|---|---|---|---|
  102 +| BR1 | 用户名必填 | 失焦 / 提交时 AntD `Form` 校验 | 「请输入用户名」 | REQ-USR-004 输入表(用户名 必填=是)/ 原型占位「请输入你的用户名」 |
  103 +| BR2 | 密码必填 | 失焦 / 提交时校验 | 「请输入密码」 | REQ-USR-004 输入表(密码 必填=是)/ 原型占位「请输入你的密码」 |
  104 +| BR3 | 密码输入掩码显示(星号) | 输入时 | —(无报错,`Input.Password` 默认掩码) | REQ-USR-004 输入表(密码 业务规则=「输入显示星号」) |
  105 +| BR4 | 版本必填(下拉单选) | 提交时校验 | 「请选择版本」 | REQ-USR-004 输入表(版本 必填=是,输入方式=下拉单选) |
  106 +| BR5 | 版本选项来自后端公司表,页面加载时预取 | 页面挂载 | 取数失败:「版本加载失败」 | REQ-USR-004 输入表(版本 显示来源=`公司表`、预加载=页面加载时)+ 依赖接口注记 |
  107 +| BR6 | 认证失败统一提示(防账号枚举),不区分账号不存在/密码错误 | 登录接口返回 `40101` | 「用户名或密码错误」 | REQ-USR-004 边界 + 后端规格规则 2/3 |
  108 +| BR7 | 禁用用户禁止登录 | 登录接口返回 `40302` | 「该账号已被禁用,请联系管理员」 | REQ-USR-004 边界(已禁用用户禁止登录) |
  109 +| BR8 | 连续失败限流提示 | 登录接口返回 `42901` | 「登录尝试过于频繁,请稍后再试」 | REQ-USR-004 跨字段规则(连续失败需限流)+ 后端规格规则 8 |
  110 +| BR9 | 登录成功签发 token,前端据此进入受保护区 | 登录接口返回 `code=0` | —(成功,`message.success("登录成功")`) | REQ-USR-004 跨字段规则(登录成功签发访问令牌)+ docs/04 § 2.2 登录态进 store |
  111 +| BR10 | 防重复提交 | `submitting` 期间 | —(按钮 loading + 字段禁用拦截重复提交) | 通用交互安全(提交中态)+ docs/04 § 2.4 错误就近处理 |
  112 +| BR11 | 前端不做密码强度/明文处理,仅原样提交(明文经 HTTPS,后端 BCrypt 比对) | 提交时 | — | REQ-USR-004 后端契约(password 提交明文经 HTTPS)/ docs/04 § 1.7 |
  113 +
  114 +> 前端只做**输入完整性校验(必填/格式)**;身份真伪、禁用判定、限流计数均由后端裁决,前端按返回码渲染文案,不复制后端认证逻辑。
  115 +
  116 +---
  117 +
  118 +## 6. 交互与实现要点(复刻原型语义)
  119 +
  120 +1. **页面布局**:复刻 `.login-wrap` 三段式——顶部品牌头(`.login-head`)、中部深蓝主视觉 + 右侧浮层登录卡(`.login-hero` + `.login-card` 绝对定位居右垂直居中)、底部版权条(`.login-foot`)。主视觉的网格透视 / 径向渐变背景按原型 `::before`/`::after` 装饰性复刻(纯展示,无交互)。
  121 +2. **登录卡定位**:卡片宽约 380px,右侧 8% 处垂直居中(原型 `.login-card{right:8%;top:50%;transform:translateY(-50%)}`),带阴影浮层。响应式收窄时允许卡片回流居中(默认行为,见 § 7 决策 D4)。
  122 +3. **版本下拉**:原型默认值「标准版」、点击 `#ver-drop` 展开 `.opt` 列表。本规格用 AntD `Select`,options 全部来自 `GET /api/usr/companies`,**不硬编码「标准版」**(原型为静态 demo 值);若返回项含 `sVersion` 则下拉 label 展示为 `sCompanyName(sVersion)`,否则仅 `sCompanyName`;`value` 一律取 `id`,提交时作为 `companyId`。仅一项时默认选中该项。
  123 +4. **提交触发**:原型按钮 `data-go="main"` 是直接切屏的 demo 行为;本规格改为 `Form.onFinish` → 调 `POST /api/usr/login`,成功才跳转,失败留在登录页并提示。支持回车提交(AntD `Form` 默认)。
  124 +5. **失败后处理**:`40101`/`42901` 等失败后清空密码字段并聚焦密码框,用户名与版本保留,便于重试(通用登录交互;不属硬业务规则,登记于 § 7 决策 D5)。
  125 +6. **登录态落地**(docs/04 § 2.2 / § 2.3):成功后 `token` + `user` 经 `authSlice.setCredentials` 进 Redux store;token 同时持久化(默认 `localStorage`,键名 `xly_erp_token`,供刷新后 `request.ts` 注入 `Authorization: Bearer`,见 § 7 决策 D6);随后 `navigate('/', { replace:true })`。
  126 +7. **路由守卫协作**:登录页本身为放行路由;已登录用户访问 `/login` 时直接重定向到主页(避免重复登录)。守卫逻辑实现细节属路由壳(FE-02),本页只在 `success` 后触发导航。
  127 +
  128 +---
  129 +
  130 +## 7. Design Tokens 引用清单(`src/styles/tokens.css`,仅 `var(--color-*)`)
  131 +
  132 +> 约束:组件样式只用 `var(--color-*)`,禁止硬编码 hex/rgba;色值冲突时 `tokens.css` 优先于 `prototype/`(原型内联 `:root` 变量为 demo 私有,不作为色值 SSoT)。AntD 主题色经 `ConfigProvider` 对齐 `--color-primary`。
  133 +
  134 +| 用途 | Token | 备注 |
  135 +|---|---|---|
  136 +| 登录按钮 / 主操作 / 链接强调 | `var(--color-primary)` | 对应原型 `.submit` 蓝色按钮;同时作为 AntD 主题 `colorPrimary` |
  137 +| 登录卡背景 / 输入框背景 | `var(--color-form-bg-edit)` | 卡片与可编辑输入框底色(白) |
  138 +| 输入框字体色 | `var(--color-form-fg)` | 输入文本色 |
  139 +| 下拉项 hover 背景 | `var(--color-form-bg-hover)` | 版本 `Select` 选项 hover(原型 `.opt .o:hover`) |
  140 +| 通用文字 / 标题 | `var(--color-text)` | 卡片标题「用户登录」、品牌副标题 |
  141 +| 次要文字 / 占位 / 页脚版权 | `var(--color-text-secondary)` | 输入占位、`.login-foot` 版权文本、备案号 |
  142 +| 边框 / 分隔线 / 输入框描边 | `var(--color-border)` | 输入框边框(原型 `.lf` 描边)、卡片描边 |
  143 +| 页面基础背景 | `var(--color-bg-base)` | `.login-wrap` / `.login-head` / `.login-foot` 浅灰底(原型用 `#eaedf2`,统一映射到基础底色 token) |
  144 +| 错误 / 失败提示文字 | `var(--color-error)` | 校验红字、`message.error` 强调(AntD 默认已用主题 error,显式登记以备自定义文案样式) |
  145 +
  146 +> 主视觉 `.login-hero` 的深蓝渐变 / 网格透视为纯装饰,`tokens.css` 未定义对应深色品牌色 token;本规格将其作为登录页**局部装饰样式**保留在 `Login` 的 scoped 样式里,不引入新全局 token,也不挪用语义 token(见 § 8 决策 D7)。该装饰不承载状态语义,不违反「语义色只用 token」约束。
  147 +
  148 +---
  149 +
  150 +## 8. 自主决策记录(decisions)
  151 +
  152 +| # | 问题 | 选择 | 依据 | 置信度 |
  153 +|---|---|---|---|---|
  154 +| D1 | 版本下拉数据从哪个端点取(docs/05 主清单只列 `POST /api/usr/login`) | 消费 `GET /api/usr/companies`(放行只读端点) | REQ-USR-004 后端规格 § 8 D1 已补齐该端点专供登录「版本」预加载;卡片输入表标版本「显示来源=公司表、预加载=页面加载时」 | high |
  155 +| D2 | 开发期前端如何到达后端(跨端口 5173→5172) | `request.ts` baseURL=`/api`,Vite dev proxy 把 `/api` 转发到 `http://localhost:5172`(取 config-vars `backend.http_port`) | docs/04 § 2.3「baseURL 指向后端 /api」;config-vars 锁定 dev_port=5173 / http_port=5172,proxy 是 Vite 标准跨端口方案 | high |
  156 +| D3 | 登录成功后跳转目标路由 | 默认跳主页 `/`(应用落地路由,属 FE-02 范畴);用 `replace:true` 防回退到登录页 | 原型登录按钮 `data-go="main"` 即进主页;主页路由壳由 FE-02 定义,本页仅触发导航到根路由 | high |
  157 +| D4 | 小屏 / 窄视口下登录卡布局 | 默认随容器回流(卡片可居中),不专门设计移动端断点 | 目标用户为「企业内部管理人员」桌面端 ERP(CLAUDE.md),原型为定宽桌面布局;移动适配非本 REQ 验收项 | medium |
  158 +| D5 | 登录失败后是否清空密码 / 聚焦 | 失败(40101/42901 等)后清空密码框并聚焦,保留用户名与版本 | 通用安全登录交互(避免残留密码、便于重试),不与任何业务规则冲突;非硬性需求,故登记 | medium |
  159 +| D6 | token 持久化方式 | `localStorage`(键 `xly_erp_token`),供刷新后 `request.ts` 注入;登录态同时进 Redux `authSlice` | docs/04 § 2.2「登录态/token 用 Redux 管理」+ § 2.3「请求拦截器注入 Authorization」;持久化需跨刷新留存,localStorage 为常见取舍(无 SSoT 明确指定,故登记;如后续要求更严可换 sessionStorage/HttpOnly) | medium |
  160 +| D7 | 主视觉深蓝渐变 / 网格背景的色值来源(tokens.css 无对应深色品牌 token) | 作为登录页局部装饰样式保留(scoped),不新增全局 token、不挪用语义 token;语义色(按钮/文字/边框/错误)严格走 token | tokens.css 仅定义语义/状态色,无品牌主视觉深色;该背景纯装饰无状态语义,局部化最小侵入;符合「语义色只用 var(--color-*)」约束 | medium |
  161 +| D8 | 版本下拉 label 显示格式(含 sVersion 时) | `sCompanyName(sVersion)`,无 sVersion 时仅 `sCompanyName`;value 恒取 id | 后端返回 `{id, sCompanyName, sVersion}`,sVersion 可为 null;拼接展示更可辨识账套,value 用 id 对齐 login 入参 companyId | high |
  162 +
  163 +> 本规格不含后端实现细节(认证比对 / 令牌签发 / 数据访问 / 库表迁移等均不在前端作用域);所有认证裁决与错误码均由后端产生,前端仅消费与渲染。