20260511214503

This commit is contained in:
oneao committed 2026-05-11 21:45:04 +08:00
1 parent 364d29dbaa
commit e98740192d
9 files changed
+2451 -875

No files matched your search

@@ -0,0 +1,274 @@
# 草稿态核心架构重构计划
## 一、问题分析
当前草稿态管理存在以下核心问题:
| 问题 | 影响 |
|------|------|
| 维护操作过程而非最终状态 | recover/search/pagination/merge 逻辑复杂 |
| delete 直接覆盖状态 | recover 后编辑数据丢失 |
| 本地数据参与搜索 | total/page 不准确,分页错乱 |
| 搜索与展示混在一起 | 搜索结果不稳定 |
| 缺少基础搜索条件 | 业务层重复拼接条件 |
---
## 二、核心设计调整
### 2.1 草稿态数据模型
**新模型结构:**
```js
// 草稿状态映射 - 只维护最终状态
draftStateMap = {
[rowId]: {
state: 'insert' | 'update' | 'delete',
data: {}, // 本地修改后的数据
backup: {} // delete 时的备份(用于 recover)
}
};
// 本地新增数据(尚未保存到数据库)
insertRows = [];
// 选中状态(独立于草稿状态)
selectedRows = [];
```
**状态转换规则:**
```text
insert → delete → recover → insert(恢复本地新增态)
update → delete → recover → update(恢复编辑态)
delete → recover → 恢复 backup,如果没有则恢复远程原始态
```
### 2.2 搜索与展示分离
| 阶段 | 数据源 | 说明 |
|------|--------|------|
| 搜索 | 远程接口 | 完全依赖服务器,本地数据不参与 |
| 展示 | 远程 + 本地覆盖 | 使用本地状态覆盖远程数据展示 |
### 2.3 baseSearchCondition 机制
```js
// 基础条件(永久保留)
baseSearchCondition: "mx_zdorderid='xxx'"
// 动态条件(可清空)
searchCondition: "name like '%abc%'"
// 最终条件
finalCondition = baseSearchCondition + " AND " + searchCondition
```
---
## 三、实施步骤
### 3.1 步骤一:重构 DraftPageManager
**修改文件:** `js/feegrid-manager.js`
**核心改动:**
1. **新增状态映射结构**:
```js
var draftStateMap = {}; // { rowId: { state, data, backup } }
var insertRows = []; // 本地新增行
```
2. **简化状态操作方法**:
```js
function setDraftState(rowId, state, data, backup) {
draftStateMap[rowId] = { state, data, backup };
}
function getDraftState(rowId) {
return draftStateMap[rowId];
}
function removeDraftState(rowId) {
delete draftStateMap[rowId];
}
```
3. **实现 delete backup 机制**:
```js
function addDeleteRow(row) {
var rowId = getRowKey(row);
var existingState = getDraftState(rowId);
setDraftState(rowId, 'delete', row, existingState ? existingState.data : row);
}
function recoverRow(rowId) {
var state = getDraftState(rowId);
if (state && state.state === 'delete') {
setDraftState(rowId, state.backup._rowState || '', state.backup, null);
}
}
```
### 3.2 步骤二:重构 loadPage 方法
**核心改动:**
1. **搜索阶段**:只调用远程接口,本地数据不参与
2. **展示阶段**:使用本地状态覆盖远程数据
```js
function loadPage(pageNumber) {
_setLoading();
var searchCondition = buildFinalCondition();
$.ajax({
url: options.searchUrl,
data: {
viewname: options.viewName,
searchcondition: searchCondition,
page: pageNumber,
rows: options.pageSize
},
success: function(result) {
// 应用本地状态覆盖
var displayRows = applyDraftState(result.rows);
_renderPage(displayRows, result.total, pageNumber);
},
complete: _clearLoading
});
}
function applyDraftState(remoteRows) {
var displayRows = [];
for (var i = 0; i < remoteRows.length; i++) {
var row = remoteRows[i];
var rowId = getRowKey(row);
var draftState = getDraftState(rowId);
if (draftState) {
switch (draftState.state) {
case 'delete':
// 删除状态不展示
continue;
case 'update':
// 展示更新后的数据
displayRows.push(draftState.data);
break;
default:
displayRows.push(row);
}
} else {
displayRows.push(row);
}
}
// 本地新增行单独处理(不参与分页)
return displayRows;
}
```
### 3.3 步骤三:添加 baseSearchCondition 支持
**配置新增:**
```js
var DEFAULT_CONFIG = {
baseSearchCondition: null,
searchCondition: null,
// ... 其他配置
};
```
**条件构建:**
```js
function buildFinalCondition() {
var parts = [];
if (options.baseSearchCondition) {
parts.push(options.baseSearchCondition);
}
if (options.searchCondition) {
parts.push(options.searchCondition);
}
return parts.join(' AND ');
}
function setSearchCondition(condition) {
options.searchCondition = condition;
}
function clearSearch() {
options.searchCondition = null;
}
function reload() {
loadPage(1);
}
```
### 3.4 步骤四:更新 JSP 调用
**修改文件:** `editorder_s_cw.jsp`
**简化调用:**
```js
var gridManager = createDataGridManager({
gridId: 'feegrid',
keyField: 'subid',
viewName: subviewtable,
baseSearchCondition: "mx_zdorderid='" + strid + "'",
onLoadSuccess: calcw_selfee,
onAfterEdit: calcw_fee,
onSelectionChange: calcw_selfee
});
```
---
## 四、文件修改清单
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `js/feegrid-manager.js` | 重构 | 核心数据模型与搜索逻辑 |
| `editorder_s_cw.jsp` | 更新 | 使用新的 baseSearchCondition 配置 |
---
## 五、风险与注意事项
1. **数据一致性**:确保 draftStateMap 与 insertRows 同步
2. **性能考虑**:draftStateMap 使用对象查找,O(1) 复杂度
3. **状态清理**:保存成功后及时清理 draftStateMap
4. **边界情况**:处理 rowId 为 null/undefined 的情况
---
## 六、最终架构图
```
┌─────────────────────────────────────────────────────────────┐
│ createDataGridManager │
├─────────────────────────────────────────────────────────────┤
│ Configuration │
│ ├── gridId, keyField, viewName │
│ ├── baseSearchCondition (永久) │
│ └── searchCondition (动态) │
├─────────────────────────────────────────────────────────────┤
│ Draft State Management │
│ ├── draftStateMap: { id → { state, data, backup } } │
│ ├── insertRows: [] │
│ └── selectedRows: [] │
├─────────────────────────────────────────────────────────────┤
│ Search & Display │
│ ├── loadPage() → 远程搜索 → applyDraftState() → 展示 │
│ └── buildFinalCondition() = base + dynamic │
└─────────────────────────────────────────────────────────────┘
```
@@ -0,0 +1,230 @@
# FeeGrid Manager 重构计划
## 一、问题分析总结
### 核心问题清单
| 问题类型 | 严重程度 | 影响范围 | 根因 |
|---------|---------|---------|------|
| 状态来源不唯一 | **高** | 所有状态操作 | `draftStateMap`、`insertRows`、`selectedRows`、`excludedRows` 并行维护 |
| Selection 基于对象引用 | **高** | 分页/刷新后选择状态 | `selectedRows` 存储完整行对象 |
| applyDraftState 副作用 | **高** | 展示层数据 | 直接使用 `draftState.data` 引用 |
| 请求无版本化 | **高** | 异步分页切换 | 旧请求会污染新状态 |
| recover 后展示不同步 | **中** | 删除恢复场景 | 状态恢复但未触发刷新 |
| insertRows 与 draftStateMap 不同步 | **中** | 插入删除场景 | `addDeleteRow` 未清理 `insertRows` |
### 架构层面的根本缺陷
1. **状态模型未真正单源化** - 多个状态源持有同一行引用,修改时容易不同步
2. **状态修改具有副作用** - `applyDraftState` 直接返回修改过的引用,导致状态污染链
3. **缺乏请求版本控制** - 无法区分旧请求和新请求,导致状态回滚
4. **UI 状态与数据状态耦合** - selection 基于对象引用而非 id
---
## 二、重构目标
### 核心原则
1. **状态单源化** - 仅保留 `draftStateMap` 作为唯一状态源
2. **状态 id 化** - 所有 UI 状态(selection、excluded)基于 rowId 而非对象引用
3. **纯函数化** - `applyDraftState` 不产生副作用,返回新对象
4. **请求版本化** - 仅处理最后一次请求,防止旧请求污染
### 预期改进
- 彻底消除请求竞态导致的状态回滚
- 消除 reload/refresh/page switch 后的 selection 丢失
- 消除 insert → delete → display 的不一致
- 消除 recover 后展示层与数据层不一致
---
## 三、修改方案
### 3.1 状态结构重构
**原结构:**
```javascript
var draftStateMap = {}; // {rowId: {state, data, backup}}
var insertRows = []; // 插入行数组
var selectedRows = []; // 选中行数组
var excludedRows = []; // 排除行数组
var isAllChecked = false; // 全选状态
```
**新结构:**
```javascript
var draftStateMap = {}; // {rowId: {state, data, backup, selected, excluded}}
var isAllChecked = false; // 全选状态
```
**状态对象结构:**
```javascript
{
state: 'insert' | 'update' | 'delete' | null, // 数据状态
data: {...}, // 当前数据
backup: {...} | null, // 删除时的备份数据
selected: boolean, // 是否选中
excluded: boolean // 是否排除(全选时)
}
```
### 3.2 核心函数修改
#### 3.2.1 applyDraftState - 纯函数化
- 创建行的深拷贝
- 不直接使用 draftState.data 引用
- 返回全新的 displayRows
#### 3.2.2 loadPage - 请求版本化
- 引入 `requestVersion` 计数器
- 每次请求递增版本号
- 响应时检查版本号,只处理最新请求
#### 3.2.3 recoverRow - 触发刷新
- 恢复状态后调用 `refreshCurrentPage()`
- 确保展示层同步更新
#### 3.2.4 所有 selection 操作 - id 化
- `addSelectedRow(row)` → `selectRow(rowId)`
- `removeSelectedRow(row)` → `deselectRow(rowId)`
- `getSelectedRows()` → 返回当前页中被选中的行(通过 id 匹配)
---
## 四、修改步骤
### 步骤 1:重构 DraftStateManager 状态结构
**文件:** `js/feegrid-manager.js`(第 27-302 行)
**修改内容:**
1. 移除 `insertRows`、`selectedRows`、`excludedRows` 数组
2. 修改 `draftStateMap` 结构,增加 `selected` 和 `excluded` 字段
3. 修改相关函数:
- `addInsertRow` - 不再维护 `insertRows`
- `addSelectedRow` - 改为通过 rowId 设置 `selected` 字段
- `removeSelectedRow` - 改为通过 rowId 清除 `selected` 字段
- `addExcludedRow` - 改为通过 rowId 设置 `excluded` 字段
- `removeExcludedRow` - 改为通过 rowId 清除 `excluded` 字段
- `getInsertRows` - 从 `draftStateMap` 中筛选
- `getSelectedRows` - 从 `draftStateMap` 中筛选
- `getExcludedRows` - 从 `draftStateMap` 中筛选
- `getValidSelectedRows` - 从 `draftStateMap` 中筛选
### 步骤 2:applyDraftState 纯函数化
**文件:** `js/feegrid-manager.js`(第 161-185 行)
**修改内容:**
- 使用深拷贝创建新对象
- 不修改原始输入参数
- 返回全新的 displayRows 数组
### 步骤 3:请求版本化
**文件:** `js/feegrid-manager.js`(第 441-490 行)
**修改内容:**
- 在 `createDataGridManager` 中添加 `requestVersion` 变量
- 修改 `loadPage` 函数,递增版本号并在响应时检查
### 步骤 4:recoverRow 触发刷新
**文件:** `js/feegrid-manager.js`(第 100-114 行)
**修改内容:**
- 在 `recoverRow` 函数末尾调用 `refreshCurrentPage()`
### 步骤 5:修复 insertRows 与 draftStateMap 同步问题
**文件:** `js/feegrid-manager.js`(第 89-98 行)
**修改内容:**
- 在 `addDeleteRow` 中同步清理 insert 状态(已通过单源化解决)
### 步骤 6:修改 _syncGridChecks 逻辑
**文件:** `js/feegrid-manager.js`(第 377-409 行)
**修改内容:**
- 根据 `draftStateMap` 中的 `selected` 和 `excluded` 字段同步 UI
- 不再依赖 `selectedRows` 和 `excludedRows` 数组
---
## 五、风险处理
### 5.1 兼容性风险
**风险:** 外部调用者可能依赖 `getSelectedRows()` 返回行对象数组
**处理:**
- 保持 API 接口不变,内部实现改为从 `draftStateMap` 中筛选
- 返回的行对象从当前 grid 数据中获取(保持引用一致性)
### 5.2 性能风险
**风险:** 深拷贝可能影响性能
**处理:**
- 只在必要时(applyDraftState)进行深拷贝
- 考虑使用结构化克隆或 JSON 序列化(视浏览器支持)
### 5.3 测试风险
**风险:** 重构可能引入新的 bug
**处理:**
- 按步骤逐步修改,每步进行功能验证
- 重点测试以下场景:
- 快速分页切换
- insert → update → delete → recover
- 搜索后 recover
- reload 后 selection 保持
---
## 六、测试用例
### 场景 1:快速分页切换
1. 快速点击第1页→第2页→第3页
2. 验证最终显示正确页面数据
3. 验证分页控件显示正确页码
### 场景 2:状态流转
1. update → delete → recover
2. 验证 recover 后状态正确(应为原始状态或 update 状态)
3. 验证页面显示恢复的行
### 场景 3:insert → delete
1. insert 行 → delete 该行
2. 验证 insertRows 不包含已删除行
3. 验证页面不显示已删除行
### 场景 4:reload 后 selection 保持
1. 选中某行 → reload
2. 验证选中状态保持
### 场景 5:搜索后 recover
1. update → delete → search → recover
2. 验证 recover 后页面显示恢复的行
---
## 七、预期代码变更量
| 修改类型 | 预估行数 | 说明 |
|---------|---------|------|
| 删除 | ~50 | 移除 insertRows/selectedRows/excludedRows 相关代码 |
| 修改 | ~100 | 修改状态管理函数 |
| 新增 | ~50 | 请求版本化、深拷贝等 |
| 总计 | ~200 | 核心逻辑重构 |
---
## 八、实施顺序
1. **步骤 1** - 状态结构重构(核心)
2. **步骤 2** - applyDraftState 纯函数化
3. **步骤 3** - 请求版本化
4. **步骤 4** - recoverRow 触发刷新
5. **步骤 5** - 已通过步骤 1 解决
6. **步骤 6** - 修改 _syncGridChecks 逻辑
> 注:步骤 1 是基础,必须首先完成。后续步骤可并行或按顺序进行。
@@ -0,0 +1,400 @@
# Feegrid 模块深度重构计划
## 1. 现状分析
### 1.1 现有代码结构
| 文件 | 职责 | 问题 |
| -------------------------- | ---------------------- | --------------- |
| `editorder_s_cw.jsp` | 业务页面,包含大量 feegrid 操作代码 | 通用逻辑与业务代码混杂 |
| `js/draft-page-manager.js` | 草稿态分页管理核心模块 | 已完成基础封装,但仍需上层整合 |
### 1.2 需要封装的通用逻辑
通过分析 `editorder_s_cw.jsp`,以下代码模式重复出现且属于通用行为:
**Loading/Loaded 状态控制(散落在多个函数中):**
```js
$('#feegrid').datagrid('loading');
$('#subgridPager').css({ 'pointer-events': 'none', 'opacity': '0.6' });
$('#subgridPager').pagination('loading');
// ... 数据加载完成后
$('#feegrid').datagrid('loaded');
$('#subgridPager').css({ 'pointer-events': '', 'opacity': '' });
$('#subgridPager').pagination('loaded');
```
**分页刷新逻辑:**
```js
$('#feegrid').datagrid('loadData', pageData);
$('#subgridPager').pagination('refresh', { total, pageNumber, pageSize });
```
**选中状态同步:**
```js
syncFeeGridChecksFromSubState(); // 分页切换后同步勾选状态
```
**行状态样式处理:**
```js
// rowStyler 根据 _rowState 设置背景色
// onLoadSuccess 中处理删除行样式
```
### 1.3 当前问题总结
1. **内聚性不足**:loading、pager、datagrid 状态控制分散在业务代码中
2. **耦合度高**:业务页面直接操作 DOM 和 datagrid API
3. **复用性差**:相同的状态管理逻辑在多个地方重复
4. **初始化暴露**:业务代码需要显式创建 draftManager
***
## 2. 重构目标
### 2.1 架构设计
```
┌─────────────────────────────────────────────────────────────┐
│ 业务页面 (editorder_s_cw.jsp) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 配置项 + 业务回调(仅关心业务逻辑) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ 配置/回调
▼
┌─────────────────────────────────────────────────────────────┐
│ FeegridManager (feegrid-manager.js) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 高阶封装层:datagrid + pager + state 的统一管理 │ │
│ │ - 初始化整合 │ │
│ │ - loading/loaded 状态自动管理 │ │
│ │ - 分页切换与选中状态同步 │ │
│ │ - 行状态样式自动处理 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ 内部调用
▼
┌─────────────────────────────────────────────────────────────┐
│ DraftPageManager (draft-page-manager.js) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 低阶封装层:草稿池 + 分页 + 选中状态核心逻辑 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 模块划分
| 模块 | 职责 | 文件 |
| -------------------- | -------------------------------- | ------------------------------ |
| **FeegridManager** | 高阶整合层,管理 datagrid + pager + 状态同步 | `js/feegrid-manager.js` |
| **DraftPageManager** | 低阶核心层,草稿态分页核心逻辑 | `js/draft-page-manager.js`(现有) |
***
## 3. 重构方案
### 3.1 创建 FeegridManager 模块
**文件路径**:`js/feegrid-manager.js`
**核心职责**:
* 统一管理 datagrid 和 pager 的初始化
* 自动处理 loading/loaded 状态
* 封装分页数据加载逻辑
* 自动同步选中状态
* 处理行状态样式
**API 设计**:
| 方法 | 说明 | 参数 |
| ------------------------ | -------------------- | ------------------------------------------------------------------------------------------- |
| `create(config)` | 创建 FeegridManager 实例 | `config.gridId`, `config.pagerId`, `config.keyField`, `config.pageSize`, `config.callbacks` |
| `loadPage(pageNumber)` | 加载指定页码数据 | `pageNumber`: 页码 |
| `refreshCurrentPage()` | 刷新当前页 | 无 |
| `refreshDisplay()` | 仅刷新当前页显示 | 无 |
| `refreshAfterDelete()` | 删除后刷新 | 无 |
| `refreshAfterUndo()` | 撤销删除后刷新 | 无 |
| `addInsertRows(rows)` | 添加新行到草稿池 | `rows`: 行数据数组 |
| `removeInsertRow(row)` | 从草稿池移除行 | `row`: 行数据 |
| `addDeleteRow(row)` | 标记行为删除状态 | `row`: 行数据 |
| `removeDeleteRow(row)` | 撤销删除标记 | `row`: 行数据 |
| `addUpdateRow(row)` | 添加到更新队列 | `row`: 行数据 |
| `getValidSelectedRows()` | 获取有效的选中行 | 无 |
| `toggleSelectAll()` | 全选/反选切换 | 无 |
| `clearSelection()` | 清除选中状态 | 无 |
| `setAllChecked(value)` | 设置全选状态 | `value`: boolean |
| `reset()` | 重置所有状态 | 无 |
**配置项结构**:
```js
{
gridId: 'feegrid', // datagrid DOM ID
pagerId: 'subgridPager', // pagination DOM ID
keyField: 'subid', // 主键字段名
pageSize: 50, // 每页条数
viewName: '', // 查询视图名
callbacks: {
// 数据加载前回调
beforeLoad: function() {},
// 数据加载成功回调
onLoadSuccess: function(rows) {},
// 行编辑完成回调
onAfterEdit: function(rowIndex, row, changes) {},
// 选中状态变化回调(用于金额计算等)
onSelectionChange: function() {}
},
// 行样式配置
rowStateStyles: {
insert: 'background-color: #f6ffed;',
update: 'background-color: #fffbe6;',
delete: 'background-color: #fff1f0;'
}
}
```
### 3.2 修改业务页面
**目标**:业务页面仅保留配置和业务回调,移除所有通用逻辑代码。
**重构后的业务页面结构**:
```html
<script type="text/javascript">
// 1. 定义配置
var feegridConfig = {
gridId: 'feegrid',
pagerId: 'subgridPager',
keyField: 'subid',
pageSize: 50,
viewName: 'v_cwzd_orderfee_s',
callbacks: {
onLoadSuccess: function(rows) {
// 业务逻辑:计算金额
calcw_selfee();
},
onAfterEdit: function(rowIndex, row, changes) {
// 业务逻辑:重新计算费用
calcw_fee();
},
onSelectionChange: function() {
// 业务逻辑:更新选中金额显示
calcw_selfee();
}
}
};
// 2. 初始化(业务层无需感知内部实现)
var feeGrid = createFeegridManager(feegridConfig);
// 3. 业务操作中调用
function saveData() {
// 获取草稿数据用于保存
var insertRows = feeGrid.getInsertRows();
var updateRows = feeGrid.getUpdateRows();
var deleteRows = feeGrid.getDeleteRows();
// ... 保存逻辑
}
function removeit() {
// 删除选中行
feeGrid.deleteSelectedRows();
}
</script>
```
***
## 4. 具体实施步骤
### 4.1 步骤一:创建 FeegridManager 模块
**文件**:`js/feegrid-manager.js`
封装内容:
1. datagrid 初始化配置
2. pager 初始化配置
3. loading/loaded 状态自动管理
4. 分页数据加载封装
5. 选中状态同步
6. 行状态样式处理
### 4.2 步骤二:修改 DraftPageManager(可选优化)
* 保持现有 API 稳定
* 可能需要增加一些内部方法供 FeegridManager 调用
### 4.3 步骤三:重构业务页面 `editorder_s_cw.jsp`
**需要移除的代码模式**:
1. **Loading 状态控制**:
* `$('#feegrid').datagrid('loading')`
* `$('#feegrid').datagrid('loaded')`
* `$('#subgridPager').pagination('loading')`
* `$('#subgridPager').pagination('loaded')`
* `$('#subgridPager').css({...})`
2. **分页数据加载**:
* `loaddata_sub()` 函数整体移除
* `refreshCurrentPage()` 函数移除
* `refreshCurrentPageDisplay()` 函数移除
* `refreshCurrentPageAfterDelete()` 函数移除
* `refreshCurrentPageAfterUndo()` 函数移除
3. **选中状态同步**:
* `syncFeeGridChecksFromSubState()` 函数移除
* `pruneSelectedRowsForCurrentPage()` 函数移除
4. **初始化代码**:
* `var draftManager = window.DraftPageManager.createDraftManager(...)` 移除
* datagrid 初始化配置移到 FeegridManager
* pager 初始化配置移到 FeegridManager
### 4.4 步骤四:测试验证
1. 测试基本功能:加载、分页、选中、删除、撤销
2. 测试状态同步:分页切换后选中状态保持
3. 测试 loading 状态:加载过程中按钮禁用
4. 测试行样式:新增/修改/删除行样式正确
***
## 5. 代码规范
### 5.1 命名规范
| 类型 | 规则 | 示例 |
| ---- | -------- | -------------------------------------- |
| 模块名 | 小写 + 连字符 | `feegrid-manager.js` |
| 函数名 | 驼峰式 | `createFeegridManager`, `loadPageData` |
| 变量名 | 驼峰式 | `gridId`, `rowStateStyles` |
| 常量名 | 大写 + 下划线 | `DEFAULT_PAGE_SIZE` |
| 内部方法 | 下划线开头 | `_handleLoading`, `_syncCheckStates` |
### 5.2 注释规范
* 模块开头:说明模块职责和使用方式
* 函数/方法:说明功能、参数、返回值
* 关键逻辑:简要说明设计意图
* 避免冗余注释:不注释显而易见的代码
### 5.3 文件结构
```
js/
├── feegrid-manager.js # 高阶整合模块(新增)
├── draft-page-manager.js # 低阶核心模块(现有)
└── draft-page-manager.js # 保持不变
```
***
## 6. 风险与应对
### 6.1 风险识别
| 风险 | 影响 | 应对措施 |
| -------------- | -------------- | -------------------------- |
| API 变更导致业务代码出错 | 功能异常 | 保持 DraftPageManager API 兼容 |
| 状态同步逻辑复杂 | 选中状态错误 | 单元测试覆盖核心场景 |
| loading 状态管理不当 | UI 卡顿或按钮可用状态错误 | 统一状态管理机制 |
| 浏览器兼容性 | 部分功能不生效 | 使用 jQuery 标准 API |
### 6.2 测试要点
1. **分页加载**:远程数据 + 本地插入数据混合展示
2. **选中状态**:跨页选中、全选后取消个别、分页切换后状态保持
3. **删除/撤销**:标记删除、撤销删除、物理删除
4. **loading 状态**:加载过程中按钮禁用、加载完成后恢复
5. **行样式**:新增(绿色)、修改(黄色)、删除(红色)样式正确
***
## 7. 预期成果
### 7.1 业务页面变化
**重构前**:约 800+ 行通用逻辑代码(loaddata\_sub、refresh 系列函数等)
**重构后**:仅保留配置项和业务回调(约 50 行)
### 7.2 代码对比
**重构前业务代码**:
```js
function loaddata_sub() {
$('#feegrid').datagrid('loading');
$('#subgridPager').css({ 'pointer-events': 'none', 'opacity': '0.6' });
$('#subgridPager').pagination('loading');
var pagination = draftManager.getPagination();
// ... 大量分页逻辑
$.ajax({
// ...
complete: function() {
$('#feegrid').datagrid('loaded');
$('#subgridPager').css({ 'pointer-events': '', 'opacity': '' });
$('#subgridPager').pagination('loaded');
}
});
}
```
**重构后业务代码**:
```js
// 配置
var feeGrid = createFeegridManager({
gridId: 'feegrid',
viewName: 'v_cwzd_orderfee_s',
onLoadSuccess: calcw_selfee
});
// 使用
feeGrid.loadPage(1); // 加载第一页
feeGrid.refreshCurrentPage(); // 刷新当前页
```
### 7.3 复用性提升
重构后,其他页面如需使用类似功能,只需:
1. 引入 `feegrid-manager.js`
2. 配置 gridId、pagerId、viewName
3. 设置业务回调
无需重复编写分页、选中、loading 等通用逻辑。
@@ -0,0 +1,162 @@
# Feegrid 模块重构计划
## 1. 修改内容
### 1.1 JS 文件修改
| 修改项 | 原内容 | 新内容 | 说明 |
|--------|--------|--------|------|
| 函数名 | `createFeegridManager` | `createDataGridManager` | 更通用 |
| callbacks 配置 | `callbacks: { onLoadSuccess, onAfterEdit, onSelectionChange }` | 直接展开 `onLoadSuccess, onAfterEdit, onSelectionChange` | 简化配置 |
| pagerId 配置 | `pagerId: 'subgridPager'` | 移除 | 自动创建,无需配置 |
| 全部选中/反选 | JSP 中有 wrapper 函数 | JS 中自动管理按钮文字 | 进一步解耦 |
### 1.2 JSP 文件修改
| 修改项 | 说明 |
|--------|------|
| 删除 `subgridPager` div | 由 JS 自动创建 |
| 简化 `toggleSelectAll` 函数 | 直接调用 JS 方法 |
| 变量名可保持或改为更通用的名字 | 可选 |
---
## 2. 重构后的 API
```js
// 之前
var feeGrid = createFeegridManager({
gridId: 'feegrid',
pagerId: 'subgridPager',
keyField: 'subid',
pageSize: 50,
viewName: subviewtable,
callbacks: {
onLoadSuccess: function(rows) { calcw_selfee(); },
onAfterEdit: function(rowIndex, row, changes) { calcw_fee(); },
onSelectionChange: function() { calcw_selfee(); }
}
});
// 之后
var gridManager = createDataGridManager({
gridId: 'feegrid',
keyField: 'subid',
pageSize: 50,
viewName: subviewtable,
searchUrl: 'jsp/searchdatapagenew.jsp',
orderField: 'father ASC, mx_xh ASC, b_id ASC',
onLoadSuccess: function(rows) { calcw_selfee(); },
onAfterEdit: function(rowIndex, row, changes) { calcw_fee(); },
onSelectionChange: function() { calcw_selfee(); }
});
```
---
## 3. JSP 中需要修改的部分
### 3.1 删除的分页器 HTML(自动创建)
```html
<!-- 删除这行 -->
<div id="subgridPager" class="easyui-pagination" style="margin: 6px 0px;background:#efefef;border:1px solid #ccc;"></div>
```
### 3.2 简化的 `toggleSelectAll` 函数
```js
// 之前:复杂的判断逻辑
function toggleSelectAll() {
if (feeGridManager.getDraftManager().isAllChecked()) {
feeGridManager.toggleSelectAll();
$('#btn_select_all').val('全部选中');
} else {
feeGridManager.toggleSelectAll();
$('#btn_select_all').val('全部反选');
}
}
// 之后:直接调用
function toggleSelectAll() {
gridManager.toggleSelectAll();
}
```
### 3.3 更新的初始化代码
```js
// 之前
feeGridManager = createFeegridManager({
gridId: 'feegrid',
pagerId: 'subgridPager',
keyField: 'subid',
pageSize: 50,
viewName: subviewtable,
callbacks: {
onLoadSuccess: function(rows) { calcw_selfee(); },
onAfterEdit: function(rowIndex, row, changes) { calcw_fee(); },
onSelectionChange: function() { calcw_selfee(); }
}
});
// 之后
gridManager = createDataGridManager({
gridId: 'feegrid',
keyField: 'subid',
pageSize: 50,
viewName: subviewtable,
orderField: 'father ASC, mx_xh ASC, b_id ASC',
onLoadSuccess: function(rows) { calcw_selfee(); },
onAfterEdit: function(rowIndex, row, changes) { calcw_fee(); },
onSelectionChange: function() { calcw_selfee(); }
});
```
---
## 4. 检查 JSP 中未使用的代码
| 函数/变量 | 状态 | 说明 |
|-----------|------|------|
| `subdata_zdfees` | 未使用 | 可以删除 |
| `feegridjson` | 未使用 | 可以删除 |
| `loaddata_subdata_zdfees` | 未使用 | 可以删除 |
| `loaddata_detail` | 部分使用 | 在 `newSKData`/`newIVData` 的 onClose 中调用 |
| `removeit_all` | 注释掉 | 已废弃 |
| `getSelections` | 未使用 | 可以删除 |
---
## 5. searchCondition 功能
代码中已有 `searchCondition` 配置项和 `setSearchCondition` 方法,JSP 中通过 `setMasterId` 设置主表ID后,会自动生成 `mx_zdorderid='xxx'` 的查询条件。
如需自定义查询条件,可使用:
```js
gridManager.setSearchCondition("mx_dw_id='xxx'");
```
---
## 6. 实施步骤
1. 修改 `feegrid-manager.js`:
- 重命名函数为 `createDataGridManager`
- 移除 `callbacks` 包装,直接展开回调配置
- 移除 `pagerId` 配置
- 完善 `toggleSelectAll` 自动管理按钮文字
- 更新默认值配置
2. 修改 `editorder_s_cw.jsp`:
- 删除 `subgridPager` div
- 简化 `toggleSelectAll` 函数
- 更新初始化代码
- 删除未使用的变量和函数
- 变量名改为更通用的 `gridManager`
---
## 7. 预期结果
- **API 简化**:从 `createFeegridManager({ callbacks: {...} })` 变为 `createDataGridManager({ onLoadSuccess, onAfterEdit, onSelectionChange })`
- **配置减少**:移除 `pagerId`,无需在 HTML 中定义分页器
- **代码清理**:删除未使用的函数和变量
- **更好的复用性**:通用名称可在其他页面直接使用
@@ -0,0 +1,155 @@
# Feegrid 模块合并重构计划
## 1. 现状分析
### 1.1 当前文件结构
| 文件 | 行数 | 职责 |
|------|------|------|
| `js/draft-page-manager.js` | 439行 | 草稿态分页核心模块(底层) |
| `js/feegrid-manager.js` | 772行 | Feegrid 高阶管理模块(依赖上层) |
**问题**:两个文件分离,DraftPageManager 被 FeegridManager 依赖,但业务页面需要同时引入两个文件,增加了复杂性。
### 1.2 用户需求
- 将公共代码统一到一个 JS 文件中
- 不拆分多个 JS 文件
- 代码放在一起方便维护
- 保持单一文件的简单结构
---
## 2. 重构目标
### 2.1 合并方案
将 `draft-page-manager.js` 和 `feegrid-manager.js` 合并为一个文件 `js/feegrid-manager.js`。
合并后文件结构:
```
js/feegrid-manager.js
├── DraftPageManager (内部模块,对外不可见)
│ ├── 状态管理
│ ├── 草稿池操作
│ ├── 选中状态操作
│ ├── 分页操作
│ └── 工具方法
│
└── createFeegridManager (导出函数)
├── 初始化方法
├── 数据加载方法
├── 草稿池操作方法
├── 选中状态方法
└── 配置方法
```
### 2.2 优势
1. **单一文件**:只需引入一个 JS 文件
2. **内聚性**:DraftPageManager 作为内部实现细节,不暴露给业务代码
3. **简化引用**:业务页面只需 `createFeegridManager`
4. **命名空间干净**:不再污染 window.DraftPageManager
---
## 3. 重构步骤
### 3.1 步骤一:合并文件
将 `draft-page-manager.js` 的内容作为 `js/feegrid-manager.js` 的内部模块,移除 `window.DraftPageManager` 挂载。
### 3.2 步骤二:修改 DraftPageManager 引用
将 `feegrid-manager.js` 中所有 `window.DraftPageManager.createDraftManager` 调用改为内部引用。
### 3.3 步骤三:清理导出
移除 `window.createFeegridManager` 的 UMD 封装,改为直接 `window.createFeegridManager = createFeegridManager`。
### 3.4 步骤四:更新 JSP 引用
将 JSP 页面中的引用从两个文件改为单一文件:
```html
<!-- 之前 -->
<script src="js/draft-page-manager.js"></script>
<script src="js/feegrid-manager.js"></script>
<!-- 之后 -->
<script src="js/feegrid-manager.js"></script>
```
---
## 4. 文件结构设计
### 4.1 合并后的文件
文件路径:`js/feegrid-manager.js`
```js
/**
* Feegrid 公共管理模块
*
* 包含:
* 1. DraftPageManager - 草稿态分页核心(内部模块)
* 2. createFeegridManager - Feegrid 高阶管理器(导出)
*/
(function(window) {
'use strict';
// ==================== DraftPageManager(内部模块)====================
function createDraftManager(config) {
// ... draft-page-manager.js 的全部逻辑
}
// ==================== createFeegridManager(导出)====================
function createFeegridManager(config) {
// ... feegrid-manager.js 的全部逻辑
// 内部调用 createDraftManager 而非 window.DraftPageManager
}
// 导出到 window
window.createFeegridManager = createFeegridManager;
})(window);
```
---
## 5. 预期成果
### 5.1 文件变化
| 操作 | 文件 |
|------|------|
| 删除 | `js/draft-page-manager.js` |
| 合并+替换 | `js/feegrid-manager.js` |
| 更新引用 | `editorder_s_cw.jsp` |
### 5.2 引用简化
**之前(JSP 中)**:
```html
<script src="js/draft-page-manager.js"></script>
<script src="js/feegrid-manager.js"></script>
```
**之后(JSP 中)**:
```html
<script src="js/feegrid-manager.js"></script>
```
### 5.3 业务代码不变
`createFeegridManager` 的 API 保持不变,业务代码无需修改。
---
## 6. 风险与注意事项
1. **保持 API 兼容**:合并后 `createFeegridManager` 的接口不变
2. **删除源文件**:确认合并成功后删除 `draft-page-manager.js`
3. **测试验证**:合并后需要测试分页、选中、删除、撤销等功能
@@ -0,0 +1,225 @@
# 搜索场景下本地数据处理方案
## 场景分析
### 场景一:新增数据后的搜索
**问题**:用户新增了10条数据(仅在本地,未保存到数据库),执行 LIKE 搜索时,这些本地数据无法被搜索到。
**期望**:搜索结果应包含本地新增的行,且支持 LIKE 模糊匹配。
### 场景二:更新数据后的搜索
**问题**:用户更新了10条数据(本地已修改),执行搜索时,远程返回的是旧数据,本地修改的数据可能不符合搜索条件。
**期望**:搜索结果应使用本地更新后的数据进行匹配,而不是远程的旧数据。
***
## 解决方案
### 方案核心思路
1. **本地数据也参与搜索过滤**:在搜索时,对本地的 `insertRows` 和 `updateRows` 也应用搜索条件过滤
2. **合并展示**:将远程搜索结果与本地过滤后的结果合并展示
3. **保持状态**:保留本地数据的 `_rowState` 标记
### 修改内容
#### 1. JS 文件修改 (`js/feegrid-manager.js`)
**新增方法:**
* `_filterLocalRows(condition)` - 根据搜索条件过滤本地数据
* 修改 `loadPage()` - 在加载远程数据后,合并本地过滤后的数据
**搜索条件解析**:
* 支持 `LIKE '%xxx%'` 模糊匹配
* 支持 `=`, `!=`, `<`, `>`, `<=`, `>=` 比较
* 支持 `AND`, `OR` 逻辑(简化版)
#### 2. 修改 `loadPage()` 方法
```js
function loadPage(pageNumber) {
_setLoading();
var pagination = state.draftManager.getPagination();
var pageSize = pagination.pageSize;
var remoteTotal = pagination.remoteTotal;
var insertRows = state.draftManager.getInsertRows();
var updateRows = state.draftManager.getUpdateRows();
// 获取搜索条件
var searchCondition = options.searchCondition ||
options.keyField.replace('subid', 'mx_') + "='" + state.masterId + "'";
// 过滤本地数据
var filteredInserts = _filterLocalRows(insertRows, searchCondition);
var filteredUpdates = _filterLocalRows(updateRows, searchCondition);
// ... 原有远程加载逻辑 ...
// 合并结果时,使用本地过滤后的数据
}
```
#### 3. 新增条件解析工具函数
```js
function _filterLocalRows(rows, condition) {
if (!condition || condition.trim() === '') {
return rows;
}
var filtered = [];
var conditions = _parseCondition(condition);
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
if (_matchesConditions(row, conditions)) {
filtered.push(row);
}
}
return filtered;
}
function _parseCondition(condition) {
var result = [];
var parts = condition.split(/\s+(AND|OR)\s+/i);
for (var i = 0; i < parts.length; i += 2) {
var expr = parts[i].trim();
var operator = parts[i + 1] ? parts[i + 1].toUpperCase() : 'AND';
var match;
// LIKE 匹配
if ((match = expr.match(/^\s*(\w+)\s+LIKE\s+['"](.*)['"]\s*$/i))) {
result.push({
field: match[1],
operator: 'LIKE',
value: match[2],
logic: operator
});
}
// 等于匹配
else if ((match = expr.match(/^\s*(\w+)\s*=\s*['"](.*)['"]\s*$/))) {
result.push({
field: match[1],
operator: '=',
value: match[2],
logic: operator
});
}
// 不等于匹配
else if ((match = expr.match(/^\s*(\w+)\s*(!=|<>)=\s*['"](.*)['"]\s*$/))) {
result.push({
field: match[1],
operator: '!=',
value: match[2],
logic: operator
});
}
}
return result;
}
function _matchesConditions(row, conditions) {
if (conditions.length === 0) return true;
var result = true;
for (var i = 0; i < conditions.length; i++) {
var cond = conditions[i];
var fieldValue = row[cond.field];
if (fieldValue === undefined || fieldValue === null) {
fieldValue = '';
}
var fieldStr = String(fieldValue).toLowerCase();
var valueStr = String(cond.value).toLowerCase();
var matches = false;
switch (cond.operator) {
case 'LIKE':
var pattern = valueStr
.replace(/%/g, '.*')
.replace(/_/g, '.');
matches = new RegExp('^' + pattern + '$').test(fieldStr);
break;
case '=':
matches = fieldStr === valueStr;
break;
case '!=':
matches = fieldStr !== valueStr;
break;
}
if (cond.logic === 'AND') {
result = result && matches;
} else {
result = result || matches;
}
if (!result && cond.logic === 'AND') {
break;
}
}
return result;
}
```
***
## 实施步骤
1. **修改** **`js/feegrid-manager.js`**:
* 新增 `_filterLocalRows()` 函数
* 新增 `_parseCondition()` 函数
* 新增 `_matchesConditions()` 函数
* 修改 `loadPage()` 方法,在加载时过滤本地数据
2. **修改** **`reload()`** **方法**:
* 确保 `reload()` 时也应用本地数据过滤
3. **测试验证**:
* 测试新增数据后的 LIKE 搜索
* 测试更新数据后的搜索匹配
***
## 预期效果
| 场景 | 操作 | 预期结果 |
| --- | ------------------------- | -------------------------------- |
| 场景一 | 新增10条数据 → 搜索 LIKE '%xxx%' | 本地新增且匹配的行会显示在搜索结果中 |
| 场景二 | 更新10条数据 → 搜索更新后的字段 | 使用本地更新后的数据进行匹配 |
| 场景三 | 删除数据后搜索 | 已标记删除的行仍然出现在搜索结果中,但是status显示删除即可 |
***
## 风险与注意事项
1. **条件解析复杂度**:只支持简单的 LIKE、=、!= 操作符,复杂条件可能解析失败
2. **性能考虑**:本地数据量大时,过滤可能有性能问题(建议限制本地数据量)
3. **日期格式**:日期字段的比较需要注意格式统一
4. **多表关联**:不支持跨表字段的搜索条件