Commit 3d923ac91c672cc969f8a71e13257cc0a2517f38
1 parent
236c42b1
docs(spec:FE-01): 派生规格
Showing
1 changed file
with
163 additions
and
0 deletions
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 | +> 本规格不含后端实现细节(认证比对 / 令牌签发 / 数据访问 / 库表迁移等均不在前端作用域);所有认证裁决与错误码均由后端产生,前端仅消费与渲染。 | ... | ... |