20260726225220
This commit is contained in:
1 parent
e3c7c6f8da
commit
a8478d60ed
170 files changed
+6786
-1678
No files matched your search
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: FmsTable 内置单元格编辑
|
||||
overview: "将当前散落在 ModulePowersPanel.vue 的“单行/单元格编辑”逻辑(row._editing 协议 + handleAreaSelectionChange + col._onEditEnd)下沉到 FmsTable 内,通过 editable: boolean prop 控制启用,组件自闭环;现有唯一调用方 ModulePowersPanel 同步改写为新 API。"
|
||||
todos:
|
||||
- id: refactor-utils
|
||||
content: 重构 createEditableCellRenderer.js 与 createCellRenderer.js,使 createInputCell 通过闭包读取 editingState,resolveCustomCell 根据 ctx.editable 决定是否注入 input customCell
|
||||
status: completed
|
||||
- id: enhance-fmstable
|
||||
content: 在 FmsTable.vue 新增 editable prop、editingState 内部 shallowRef、internalData + watch、handleAreaSelectionChange 处理器与 change 事件 emit,将 StkTable 的 dataSource 指向 internalData
|
||||
status: completed
|
||||
dependencies:
|
||||
- refactor-utils
|
||||
- id: refactor-powers-panel
|
||||
content: 改写 ModulePowersPanel.vue:移除 handleEditEnd / handleAreaSelectionChange 函数,列定义移除 _onEditEnd 与 onChange,模板改为 :editable="true" + @change="onCellChange",清理无效的 :area-selection 与 @area-selection-change
|
||||
status: completed
|
||||
dependencies:
|
||||
- enhance-fmstable
|
||||
---
|
||||
|
||||
## Product Overview
|
||||
|
||||
将当前散落在 `ModulePowersPanel.vue` 的"单元格编辑"逻辑(row._editing 协议 + handleAreaSelectionChange + col._onEditEnd)下沉到 `FmsTable` 内部。组件自闭环,通过 `editable: boolean` prop 控制是否启用单元格编辑。
|
||||
|
||||
## Core Features
|
||||
|
||||
- 新增 `FmsTable.editable: boolean` prop(默认 `false`),`type:"input"` 列在该 prop 为 `true` 时才进入可编辑态
|
||||
- `FmsTable` 内部维护 `editingState: { rowKey, col } | null`(shallowRef),**不挂在 row 上**,零数据污染,父组件保存前无需清理
|
||||
- `FmsTable` 内部监听 `@area-selection-change`(鼠标点击 / 键盘方向键统一入口),自动把命中单元格设为编辑格
|
||||
- 内部 watch `editingState` → 浅拷贝内部 `dataSource` → 触发 `stk-table` 重渲染让 `customCell` 重新求值
|
||||
- 退出编辑(Enter / Esc / Blur)时 `emit("change", row, col, newValue)` 给父组件
|
||||
- 沿用现有 `createInputCell` 的"非编辑零 VNode / 原生 input / 自动 focus.select()" 行为
|
||||
- `ModulePowersPanel.vue` 同步改写为新 API:删除 `handleEditEnd` / `handleAreaSelectionChange` / 列定义中的 `_onEditEnd` / `onChange`,模板改为 `:editable="true"` + `@change="onCellChange"`
|
||||
- 其他 7 个 `FmsTable` 调用方(dashboard / TableDemo / i18n-type / I18nPanel / Fields / ListConfig / EditConfig / QueryConfig)**完全不受影响**——`editable` 默认 `false`,与现状一致
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Vue 3 `<script setup>` + `shallowRef` + `watch`
|
||||
- 复用 `stk-table-vue` 已有的 `area-selection` 与 `customCell` 机制
|
||||
- 无新增依赖、无新增组件库、无 composable 文件
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
**核心策略**:把"编辑态"从 `row` 对象上的 `_editing` 字段迁移到 `FmsTable` 内部的 `shallowRef`;`customCell` 通过闭包读取 `editingState`;通过内部 `dataSource` 浅拷贝触发 `stk-table` 重渲染。
|
||||
|
||||
### 数据流(一次完整编辑)
|
||||
|
||||
```
|
||||
stk-table area-selection-change(ranges)
|
||||
↓
|
||||
FmsTable.handleAreaSelectionChange(ranges)
|
||||
├─ !props.editable → return
|
||||
├─ ranges 为空 / 列非 input / 行越界 → editingState = null
|
||||
├─ editingState 已指向同一 row+col → return(避免方向键连按重渲染)
|
||||
└─ editingState = { rowKey, col } → watch → internalData = [...internalData] → 重渲染
|
||||
↓
|
||||
customCell({ row, cellValue }) 重跑
|
||||
├─ editing 不匹配 → 返回纯文本字符串(零 VNode 开销)
|
||||
└─ 匹配 → 返回原生 <input ref=focus.select()>
|
||||
├─ onInput: row[col.dataIndex] = e.target.value
|
||||
└─ onBlur / Enter / Esc → notifyChange(row, col, newValue)
|
||||
├─ emit("change", row, col, newValue) → 父 onCellChange → emitUpdate
|
||||
└─ editingState = null → watch → 重渲染 → 文本回退
|
||||
```
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **`editable: boolean`(表格级)**:`type:"input"` 列在 `editable=true` 时由 `resolveCustomCell` 注入自定义 `customCell`;`false` 时回退到 `stk-table` 默认文本渲染,零运行时开销
|
||||
2. **`editingState` 用 `shallowRef`,不挂 row**:避免"保存前清理 `_editing` 字段"的脏数据风险;customCell 通过闭包读 `ctx.getEditing()`
|
||||
3. **只 emit `change(row, col, newValue)`**:进入编辑不发事件(语义冗余);退出编辑统一把"新值"冒泡给父组件用于脏数据判定
|
||||
4. **内部 `internalData` 浅拷贝触发重渲染**:复用 `stk-table` 的 `dataSource` 响应式机制;`editingState` 变更 → watch → 引用变更 → 重渲染
|
||||
5. **复用现有 `createInputCell` 行为**:Enter=不退出(允许多行编辑后续扩展)/ Esc=退出 / Blur=退出 / 自动 `focus().select()`
|
||||
6. **沿用硬编码 `:area-selection`**(keyboard:true / ctrl:false / shift:false),不动——本次删除 `ModulePowersPanel` 上无效的覆盖赋值属清理
|
||||
|
||||
### 性能保证
|
||||
|
||||
- non-editing 单元格仍然"返回字符串,零 VNode"(`createInputCell` 早返回)
|
||||
- `editingState` 用 `shallowRef`,watch 仅在状态变更时触发
|
||||
- 方向键连按同一目标时早 return,避免每按一次重渲染整表
|
||||
- `checkbox` / `actions` 渲染器继续用 `WeakMap` 缓存,不受影响
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `props.dataSource` 外部引用变更(含父组件 `localPowers = [...localPowers]`)会同步触发 watch;`editingState` 内的 `rowKey` 是字符串,与引用解耦,引用变更不影响编辑态判定
|
||||
- `props.editable` 由 `true` → `false` 动态切换时,`displayColumns` 重算 → `resolveCustomCell` 返回无 `customCell` 的 col → `stk-table` 回退到默认文本渲染
|
||||
- `ModulePowersPanel` 的 `localPowers` ref / `addRow` / `deleteRow` / `batchDelete` 行为不变(仍是父组件编辑副本),仅事件链路从"列 `onChange`"改为"`@change`"
|
||||
- 删除 `:area-selection="true"`(被硬编码覆盖,无效)和 `@area-selection-change`(下沉的入口)属"现状无效 prop 清理",不改变运行时行为
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
fms-vue/src/
|
||||
├── components/fms-table/
|
||||
│ ├── FmsTable.vue [MODIFY] 新增 editable prop + editingState 内部状态 +
|
||||
│ │ handleAreaSelectionChange + change emit,
|
||||
│ │ dataSource 改为内部 shallowRef 触发重渲染
|
||||
│ └── utils/
|
||||
│ ├── createCellRenderer.js [MODIFY] resolveCustomCell(col, ctx) 接 ctx,
|
||||
│ │ editable=false 时不注入 input customCell
|
||||
│ └── createEditableCellRenderer.js [MODIFY] createInputCell(col, ctx) 通过闭包读
|
||||
│ editingState;onExit→ctx.onExit(row, val)
|
||||
└── views/module-management/components/
|
||||
└── ModulePowersPanel.vue [MODIFY] 删 handleEditEnd/handleAreaSelectionChange;
|
||||
列定义移除 _onEditEnd / onChange;
|
||||
模板加 editable + @change="onCellChange",
|
||||
删 :area-selection / @area-selection-change
|
||||
```
|
||||
|
||||
### 关键代码契约
|
||||
|
||||
**FmsTable 关键改动点**
|
||||
|
||||
| 位置 | 改动 |
|
||||
| --- | --- |
|
||||
| `props` (L9-22) | 新增 `editable: { type: Boolean, default: false }` |
|
||||
| `defineEmits` (L24) | 新增 `"change"` |
|
||||
| 新增 `editingState = shallowRef(null)` | 编辑态内部 ref |
|
||||
| 新增 `internalData = shallowRef([])` + 两个 watch | dataSource 同步 + 编辑态触发浅拷贝重渲染 |
|
||||
| `displayColumns` (L41-55) | 改为 `props.columns.map(col => resolveCustomCell(col, ctx))`,ctx 每次重算以追踪 `props.editable` |
|
||||
| 新增 `handleAreaSelectionChange(ranges)` | 校验 editable / ranges / row / col,写入 editingState |
|
||||
| 新增 `notifyChange(row, col, val)` | emit change + 清空 editingState |
|
||||
| `<StkTable>` `:data-source` (L120) | 由 `dataSource` 改为 `internalData` |
|
||||
|
||||
|
||||
**utils 新签名**
|
||||
|
||||
```js
|
||||
// createEditableCellRenderer.js
|
||||
createInputCell(col, ctx)
|
||||
// ctx = { getEditing, onExit(row, col, val), getRowKey }
|
||||
|
||||
// createCellRenderer.js
|
||||
resolveCustomCell(col, ctx)
|
||||
// editable=false → type:"input" 列不注入 customCell(stk-table 默认文本)
|
||||
// editable=true → 注入 createInputCell(col, ctx)
|
||||
```
|
||||
|
||||
**ModulePowersPanel 改动要点**
|
||||
|
||||
- 删除 `handleEditEnd`、`handleAreaSelectionChange` 两个函数(约 L100-141)
|
||||
- 列定义移除 `_onEditEnd: handleEditEnd` 与 `onChange: () => emitUpdate()`(L200-203 共 4 处)
|
||||
- 模板 `<FmsTable>` 加 `editable` 与 `@change="onCellChange"`,移除无效的 `:area-selection` 与 `@area-selection-change`
|
||||
- 新增 `function onCellChange(row, col, val) { emitUpdate() }`
|
||||
- `localPowers` ref / `addRow` / `deleteRow` / `batchDelete` 逻辑不变
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: 登录日志功能(前后端)
|
||||
overview: 后端在登录时统一采集客户端信息(IP、IP归属地、浏览器/版本、操作系统/版本、设备类型、原始UA),复用通用 saveobjt 机制写入新表 s_log_login(成功与失败都记录);前端新增只读列表页展示登录日志。表名 s_log_login,权限暂不接入。
|
||||
design:
|
||||
styleKeywords:
|
||||
- Enterprise
|
||||
- Clean
|
||||
- Audit Table
|
||||
- Teal Accent
|
||||
- Light Theme
|
||||
fontSystem:
|
||||
fontFamily: PingFang SC
|
||||
heading:
|
||||
size: 20px
|
||||
weight: 600
|
||||
subheading:
|
||||
size: 14px
|
||||
weight: 500
|
||||
body:
|
||||
size: 13px
|
||||
weight: 400
|
||||
colorSystem:
|
||||
primary:
|
||||
- "#15998E"
|
||||
- "#173A42"
|
||||
background:
|
||||
- "#F5F7F8"
|
||||
- "#FFFFFF"
|
||||
text:
|
||||
- "#19323A"
|
||||
- "#5B6B70"
|
||||
functional:
|
||||
- "#52C41A"
|
||||
- "#FF4D4F"
|
||||
- "#15998E"
|
||||
todos:
|
||||
- id: add-deps-xdb
|
||||
content: pom.xml 新增 ip2region/uap-java 依赖并下载 ip2region.xdb 到 resources
|
||||
status: completed
|
||||
- id: client-info-utils
|
||||
content: 实现 IpUtils、ClientInfo、IpRegionSearcher、ClientInfoExtractor 采集工具
|
||||
status: completed
|
||||
dependencies:
|
||||
- add-deps-xdb
|
||||
- id: login-log-service
|
||||
content: 实现 LoginLogService 并在 AuthController/AuthService 登录成功与失败分支串联记录
|
||||
status: completed
|
||||
dependencies:
|
||||
- client-info-utils
|
||||
- id: migration-table
|
||||
content: 新增 003_create_login_log.sql 建表 s_log_login 及索引
|
||||
status: completed
|
||||
- id: frontend-log-page
|
||||
content: 用 [skill:antdv-next] 新增路由、侧边栏菜单与登录日志只读列表页(筛选+分页)
|
||||
status: completed
|
||||
dependencies:
|
||||
- migration-table
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
统一在登录环节采集客户端信息,落地「登录日志」功能,覆盖登录成功与失败(账号不存在、密码错误)两类行为,供后续操作日志与系统审计复用。前端提供只读的登录日志列表页。
|
||||
|
||||
## 产品概述
|
||||
|
||||
登录时由后端统一采集客户端环境信息并落库,形成可审计的登录轨迹;管理员可在前端以表格形式分页查看每次登录的客户端画像与结果。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 后端登录流程中采集:IP 地址、IP 归属地(国家/省/市/运营商)、浏览器及版本、操作系统及版本、设备类型、原始 User-Agent。
|
||||
- 登录成功与失败均写登录日志(表 `s_log_login`),失败记录失败原因,写日志失败不影响登录。
|
||||
- 前端新增「登录日志」只读列表页:分页展示全部采集字段,成功/失败以标签区分,支持按账号、结果、时间筛选。
|
||||
- 权限按用户要求暂不做限制(路由不带 moduleCode,登录用户均可访问)。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 后端:Java 21 + Spring Boot 4.1,沿用 `DbUtils` 原生 JDBC 与多机构 `OrgRoutingDataSource`;新增依赖 `org.lionsoul:ip2region`(本地 xdb 解析)、`com.github.ua-parser:uap-java`(User-Agent 解析)。
|
||||
- 前端:Vue 3 + Vite + antdv-next + Pinia,复用现有 `pageDataApi` 通用分页接口。
|
||||
|
||||
## 实现思路
|
||||
|
||||
- 客户端信息全部在后端登录时采集:IP 与 UA 本就是 HTTP 请求头,服务端获取更可靠、不可被前端伪造,也契合规范中「优先通用接口、不新增业务接口」。
|
||||
- `IpUtils`:按 `X-Real-IP` → `X-Forwarded-For`(取首个有效 IP)→ `request.getRemoteAddr()` 顺序取值,兼容 Nginx 代理。
|
||||
- `IpRegionSearcher`:以 `@Component` 单例在启动时把 `ip2region.xdb` 读入内存(`Searcher.newWithBuffer(bytes)`),避免每次文件 IO;`search(ip)` 返回 `国家|省|市|运营商`,异常时返回「未知」。
|
||||
- `ClientInfoExtractor`:`@Component`,注入 `IpRegionSearcher` 与 uap-java `Parser`(单例),`extract(HttpServletRequest)` 解析 UA 得到浏览器/版本、系统/版本、设备类型(依据 `device.family` 映射 Mobile/Tablet/Desktop/Bot/Unknown)。
|
||||
- `LoginLogService.recordLogin(...)`:在方法内 `OrgContext.setOrgId(orgId)` 后,组装 `s_log_login` 行(`b_id = IdGenerator.nextId()`),以通用保存结构 `{table:'s_log_login', key_field:'b_id', inserts:[row]}` 调用 `DataSaveService.save(...)`(即 `saveobjt` 后端实现),整段 `try/catch` 包裹、失败仅记录日志、`finally` 中 `OrgContext.clear()`,保证登录绝不因写日志失败而中断;机构无效(无数据源)时自然抛错被捕获跳过落库。
|
||||
- `AuthController.login` 将 `HttpServletRequest` 透传给 `AuthService.login`;在成功(含 `sessionId`)与失败(账号不存在/密码错误)分支均调用 `recordLogin`。
|
||||
- 前端:新增路由(无 `moduleCode`)+ 侧边栏菜单项(无 `moduleCode` 经 `canModule` 恒为可见)+ 只读 `a-table` 列表页,调用 `pageDataApi({viewName:'s_log_login', orderBy:'b_login_time DESC'})`,分页依据响应 `rows/total`。
|
||||
|
||||
## 性能与可靠性
|
||||
|
||||
- ip2region 内存检索为 O(1) 级、uap-java 解析为微秒级,登录非高频操作,无性能瓶颈;不引入每请求文件 IO 或额外 SQL。
|
||||
- 写日志与登录主流程解耦(try/catch + 独立事务回滚不影响登录),`OrgContext` 严格 `finally` 清理,避免线程上下文污染。
|
||||
- 不改动 `saveobjt`/通用查询等既有接口与共享组件,向后兼容。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[AuthController.login HttpServletRequest] --> B[AuthService.login]
|
||||
B -->|成功/失败| C[LoginLogService.recordLogin]
|
||||
C --> D[ClientInfoExtractor.extract]
|
||||
D --> E[IpUtils 取IP]
|
||||
D --> F[IpRegionSearcher 归属地]
|
||||
D --> G[uap-java 解析UA]
|
||||
C -->|OrgContext.setOrgId| H[DataSaveService.save 通用saveobjt]
|
||||
H --> I[(s_log_login 机构库)]
|
||||
subgraph 前端
|
||||
J[LoginLogView] --> K[pageDataApi /data/page]
|
||||
K --> L[(s_log_login)]
|
||||
end
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
fms-api/
|
||||
├── pom.xml # [MODIFY] 新增 ip2region、uap-java 依赖
|
||||
├── src/main/resources/ip2region.xdb # [NEW] 官方 ip2region 二进制库(约11MB,不入库/可 gitignore)
|
||||
├── src/main/resources/application.yaml # [MODIFY/可选] 如需配置 xdb 路径占位
|
||||
├── config/migrations/003_create_login_log.sql # [NEW] 建表 s_log_login + 索引
|
||||
└── src/main/java/cn/g3soft/fmsapi/
|
||||
├── utils/
|
||||
│ ├── IpUtils.java # [NEW] 从请求头/远程地址提取客户端 IP
|
||||
│ ├── ClientInfo.java # [NEW] 客户端信息载体(ip/location/browser/os/deviceType/ua)
|
||||
│ ├── IpRegionSearcher.java # [NEW] 启动初始化 xdb Searcher,提供 search(ip)
|
||||
│ └── ClientInfoExtractor.java # [NEW] 组合 IpUtils+IpRegionSearcher+uap-java,extract(request)
|
||||
├── service/
|
||||
│ ├── LoginLogService.java # [NEW] recordLogin:组装并调用 DataSaveService.save
|
||||
│ └── AuthService.java # [MODIFY] 注入 LoginLogService,login 增 HttpServletRequest 参数并在成功/失败分支记录
|
||||
└── controller/AuthController.java # [MODIFY] login 透传 HttpServletRequest 给 service
|
||||
|
||||
fms-vue/
|
||||
├── src/services/api.js # [MODIFY] 新增 pageLoginLogApi 包装 pageDataApi
|
||||
├── src/router/routes.js # [MODIFY] 新增 /system/login-log 路由(无 moduleCode)
|
||||
├── src/layouts/components/AppSidebar.vue # [MODIFY] systemPages 增加「登录日志」菜单项(无 moduleCode)
|
||||
└── src/views/system/login-log/
|
||||
└── index.vue # [NEW] 只读列表页:筛选栏 + a-table 分页展示
|
||||
```
|
||||
|
||||
## 关键代码结构
|
||||
|
||||
```java
|
||||
// ClientInfo:客户端环境信息载体
|
||||
public record ClientInfo(
|
||||
String ip, String ipLocation,
|
||||
String browser, String browserVersion,
|
||||
String os, String osVersion,
|
||||
String deviceType, String userAgent
|
||||
) {}
|
||||
|
||||
// LoginLogService 写日志入口(成功/失败均调用)
|
||||
void recordLogin(String orgId, String account, Map<String,Object> user,
|
||||
boolean success, String failReason, String sessionId,
|
||||
HttpServletRequest request);
|
||||
```
|
||||
|
||||
## 设计风格
|
||||
|
||||
沿用系统现有的企业级货运物流后台风格:浅色画布、左侧深色(`#173a42`)侧栏、青绿主色(`#15998e`)点缀。登录日志页为「筛选栏 + 只读数据表格」的经典审计列表布局,保持与模块管理页一致的视觉语言与栅格间距,整体克制、清晰、专业。
|
||||
|
||||
## 页面规划(1 屏)
|
||||
|
||||
- 顶部区块:页标题「登录日志」+ 筛选栏(账号输入框、登录结果下拉「成功/失败」、登录时间范围选择器、查询按钮)。
|
||||
- 主体区块:antdv `a-table` 只读表格,列含 登录时间、账号、用户姓名、IP、IP 归属地、浏览器(版本)、操作系统(版本)、设备类型、登录结果(标签)、原始 UA(溢出省略,tooltip 全显);底部分页。
|
||||
- 结果列用 `a-tag` 区分:成功=绿色、失败=红色。
|
||||
|
||||
## 交互与响应式
|
||||
|
||||
- 筛选条件变更触发分页查询(`pageDataApi`),加载态用 `a-spin`/表格 `loading`。
|
||||
- 表格列在窄屏下横向滚动(`scroll.x`),筛选栏在移动端自动换行。
|
||||
- 行 hover 高亮;UA 等超长文本 `ellipsis` + `tooltip`,避免撑破布局。
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### Skill
|
||||
|
||||
- **antdv-next**
|
||||
- Purpose: 在实现登录日志只读列表页时,获取 antdv-next 表格(a-table)、筛选控件(a-input / a-select / a-range-picker)、a-tag 等组件的准确 API、事件与插槽用法。
|
||||
- Expected outcome: 前端列表页使用符合项目自动导入约定的 antdv-next 组件,分页/筛选/标签渲染正确且无 API 误用。
|
||||
Reference in new issue
Block a user