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