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. **多表关联**:不支持跨表字段的搜索条件
+68 -436
View File
@@ -17,7 +17,7 @@
<script type="text/javascript" src="js/datagrid-drag/datagrid-dnd.js"></script>
<script type="text/javascript" src="js/datagrid-drag/datagrid-head.js"></script>
<script type="text/javascript" src="jsys/chinesetopy.js"></script>
<script type="text/javascript" src="js/draft-page-manager.js"></script>
<script type="text/javascript" src="js/feegrid-manager.js"></script>
<style>
.g3-label-width {
width: 80px;
@@ -230,8 +230,7 @@
</div>
</div>
<div id="subgridPager" class="easyui-pagination"
style="margin: 6px 0px;background:#efefef;border:1px solid #ccc;"></div>
<div class="g3-form-row" style="height: 30px; width: 99%; display: flex; align-items: center; gap: 20px;">
<div class="g3-form-col" id="ed_div_total" style="flex: 1; min-width: 200px;">
@@ -280,36 +279,14 @@
var subdata_zdfees = new Array();
var mainData = {};
/**
* 子表草稿态 + 分页管理模块
* 使用独立模块 draft-page-manager.js 管理
*
* 职责:
* - 草稿池:insertRows / updateRows / deleteRows(保存提交用)
* - 行展示态:行对象上的 _rowState(insert | update | delete | '')
* - 分页:remoteTotal(服务端)+ insertRows 混排
* - UI 勾选:selectedRows 唯一真相源(跨页)
*/
var draftManager = window.DraftPageManager.createDraftManager({
keyField: 'subid',
pageSize: 50
});
/**
* 修剪当前页选中状态(配合 clearChecked 使用)
*/
function pruneSelectedRowsForCurrentPage() {
var rows = $('#feegrid').datagrid('getRows');
draftManager.removeSelectionForRows(rows);
}
let subSummary = {
// 分币种
moneyTypes: [],
// 折合总金额
totalAmount: 0
};
// DataGrid 管理器实例
var gridManager;
$(function () {
SetEnterFocus();
@@ -389,8 +366,7 @@
// configColumn('feegrid',subviewtable,'local');
if (strid != "") {
loaddata(strid);
// loaddata_detail('feegrid', subviewtable, strid, '');
loaddata_sub()
gridManager.loadPage(1);
calcw_fee();
} else {
console.log("走到了这里");
@@ -494,148 +470,34 @@
field: 'bcheck'
});
$('#feegrid').datagrid({
idField: 'subid',
fit: true,
remoteSort: false,
multiSort: true,
//multiSort :true,
singleSelect: true,
//ctrlSelect:true,
selectOnCheck: false,
checkOnSelect: false,
rownumbers: true,
// onDblClickRow:function(rowIndex, rowData){openDatayw(rowData);},
//onClickRow : onClickRow_con,
// onBeginEdit:onBeginEdit_con,
toolbar: '#tb_con',
columns: [columnfield],
// pagination : false,
// 创建 DataGridManager(自动初始化 datagrid 和分页器)
gridManager = createDataGridManager({
gridId: 'feegrid',
keyField: 'subid',
pageSize: 50,
autoRowHeight: false,
//pageList:[50,100,200,500],
LoadSelkeyID: undefined,
rowStyler: function (index, row) {
if (row._rowState === 'insert') {
return 'background-color: #f6ffed;';
} else if (row._rowState === 'update') {
return 'background-color: #fffbe6;';
} else if (row._rowState === 'delete') {
return 'background-color: #fff1f0;';
}
return '';
},
onLoadSuccess: function (data) {
$.messager.progress('close');
if ($('#feegrid').datagrid('options').LoadSelkeyID != undefined) {
var rows = $('#feegrid').datagrid("getRows");
if (rows.length > 0) {
$('#b_dw_id').val(rows[0].mx_dw_id);
$('#v_dwname').val(rows[0].v_mx_dw_name);
if (rows[0].b_mycompany_id != undefined && rows[0].b_mycompany_id != '') {
$('#b_mycompany_id').val(rows[0].b_mycompany_id);
}
if (rows[0].v_mycompanyname != undefined && rows[0].v_mycompanyname != '') {
$('#v_mycompanyname').val(rows[0].v_mycompanyname);
}
}
$('#feegrid').datagrid('options').LoadSelkeyID = undefined;
}
$('.datagrid-body tr').each(function () {
var rowIndex = $(this).attr('datagrid-row-index');
if (rowIndex !== undefined) {
var row = $('#feegrid').datagrid('getRows')[parseInt(rowIndex)];
if (row && row._rowState === 'delete') {
$(this).addClass('datagrid-row-delete');
} else {
$(this).removeClass('datagrid-row-delete');
}
}
});
syncFeeGridChecksFromSubState();
viewName: subviewtable,
orderField: 'father ASC, mx_xh ASC, b_id ASC',
baseSearchCondition: "mx_zdorderid='" + strid + "'",
selectAllBtn: '#btn_select_all',
selectAllText: '全部选中',
unselectAllText: '全部反选',
userid: '',
onLoadSuccess: function (rows) {
calcw_selfee();
},
onAfterEdit: function (rowIndex, row, changes) {
if (Object.keys(changes).length === 0) {
return;
}
if (draftManager.isInsertRow(row) || draftManager.isInInsertRows(row)) {
if (!row._rowState) {
row._rowState = 'insert';
}
} else if (!draftManager.isDeleteRow(row)) {
row._rowState = 'update';
draftManager.addToUpdateRows(row);
}
calcw_fee();
},
onCheckAll: function () {
draftManager.setAllChecked(true);
draftManager.clearExcludedRows();
calcw_selfee()
},
onUncheckAll: function () {
draftManager.setAllChecked(false);
draftManager.clearExcludedRows();
draftManager.clearSelectedRows();
calcw_selfee()
},
onCheck: function (index, row) {
if (draftManager.isAllChecked()) {
draftManager.removeExcludedRow(row);
} else {
draftManager.addSelectedRow(row);
}
calcw_selfee()
},
onUncheck: function (index, row) {
if (draftManager.isAllChecked()) {
draftManager.addExcludedRow(row);
} else {
draftManager.removeSelectedRow(row);
}
calcw_selfee()
}
}).datagrid("columnMoving");
$('#feegrid').datagrid('getPanel').find('.datagrid-body').on('scroll.datagrid', function (e) {
if (editIndex_con != undefined) {
$('#feegrid').datagrid('endEdit', editIndex_con);
editIndex_con = undefined;
}
})
$('#subgridPager').pagination({
total: draftManager.getPagination().total,
pageSize: draftManager.getPagination().pageSize,
pageNumber: draftManager.getPagination().pageNumber,
pageList: [50, 100, 200],
onSelectPage: function (pageNumber, pageSize) {
draftManager.setPageNumber(pageNumber);
draftManager.setPageSize(pageSize);
loaddata_sub();
},
onChangePageSize: function (pageSize) {
draftManager.setPageNumber(1);
draftManager.setPageSize(pageSize);
if (!draftManager.isAllChecked()) {
$('#feegrid').datagrid('clearChecked');
draftManager.clearSelectedRows();
}
calcw_selfee()
loaddata_sub();
onSelectionChange: function () {
calcw_selfee();
}
});
// configColumn('feegrid',subviewtable,'local');
gridManager.setColumns(columnfield);
}
// configColumn('feegrid',subviewtable,'local');
function openData(id, hstype) {
var e = window.event || e;
if (e.stopPropagation) {
@@ -649,227 +511,8 @@
}
}
// ========== 子表:分页请求、与 insertRows 合并、行态恢复、刷新 ==========
// 拉取当前页:纯 insert 尾段走本地切片;否则 Ajax 后 merge 尾段 insert
function loaddata_sub() {
$('#feegrid').datagrid('loading');
$('#subgridPager').css({
'pointer-events': 'none',
'opacity': '0.6'
});
$('#subgridPager').pagination('loading');
var pagination = draftManager.getPagination();
var pageNumber = pagination.pageNumber;
var pageSize = pagination.pageSize;
var remoteTotal = pagination.remoteTotal;
var insertRows = draftManager.getInsertRows();
if (!strid) {
var pageData = [];
if (insertRows.length > 0) {
var startInInsert = (pageNumber - 1) * pageSize;
var endInInsert = startInInsert + pageSize;
pageData = insertRows.slice(startInInsert, endInInsert);
}
$('#feegrid').datagrid('loadData', pageData);
$('#subgridPager').pagination('refresh', {
total: insertRows.length,
pageNumber: pageNumber,
pageSize: pageSize
});
$('#feegrid').datagrid('loaded');
$('#subgridPager').css({
'pointer-events': '',
'opacity': ''
});
$('#subgridPager').pagination('loaded');
return;
}
if (remoteTotal > 0 && draftManager.isInsertOnlyPage(pageNumber)) {
var startInInsert = (pageNumber - 1) * pageSize - remoteTotal;
var endInInsert = startInInsert + pageSize;
var pageData = insertRows.slice(startInInsert, endInInsert);
$('#feegrid').datagrid('loadData', pageData);
$('#subgridPager').pagination('refresh', {
total: draftManager.getActualTotal(),
pageNumber: pageNumber,
pageSize: pageSize
});
$('#feegrid').datagrid('loaded');
$('#subgridPager').css({
'pointer-events': '',
'opacity': ''
});
$('#subgridPager').pagination('loaded');
return;
}
var searchcondition = "mx_zdorderid='" + strid + "'";
var searchparam = {
viewname: subviewtable,
searchcondition: searchcondition,
userid: '',
orderfield: 'father ASC, mx_xh ASC, b_id ASC',
keyfield: 'subid',
page: pagination.pageNumber,
rows: pagination.pageSize
};
$.ajax({
type: 'post',
url: 'jsp/searchdatapagenew.jsp',
cache: false,
data: searchparam,
dataType: 'json',
success: function (result) {
draftManager.setRemoteTotal(result.total);
draftManager.setCurrentRemoteRows(result.rows);
draftManager.restoreRowStates(result.rows);
var pageData = draftManager.buildPageData(result.rows, pageNumber);
$('#feegrid').datagrid('loadData', pageData);
var newPagination = draftManager.getPagination();
$('#subgridPager').pagination('refresh', {
total: newPagination.total,
pageNumber: newPagination.pageNumber,
pageSize: newPagination.pageSize
});
},
error: function () {
console.log('分页加载失败');
},
complete: function () {
$('#feegrid').datagrid('loaded');
$('#subgridPager').css({
'pointer-events': '',
'opacity': ''
});
$('#subgridPager').pagination('loaded');
}
});
}
// buildCurrentPageData 和 restoreRowStates 已移至 draft-page-manager.js 模块中
// 新增费用等后:若页码越界则纠页,否则重载
function refreshCurrentPage() {
var currentPage = draftManager.getPagination().pageNumber;
var totalPages = draftManager.getTotalPages();
if (currentPage > totalPages) {
draftManager.setPageNumber(totalPages);
$('#subgridPager').pagination('select', totalPages);
} else {
loaddata_sub();
}
}
// 仅重绘当前内存行(保存行软删后),会 clearChecked,并同步修剪 selectedRows
function refreshCurrentPageDisplay() {
var currentRows = $('#feegrid').datagrid('getRows');
$('#feegrid').datagrid('loadData', currentRows);
$('#feegrid').datagrid('clearChecked');
pruneSelectedRowsForCurrentPage();
calcw_selfee();
}
// 撤销删除后:重绘并恢复勾选与业务侧选中金额
function refreshCurrentPageAfterUndo() {
var currentRows = $('#feegrid').datagrid('getRows');
$('#feegrid').datagrid('loadData', currentRows);
syncFeeGridChecksFromSubState();
calcw_selfee();
}
// 物理删除 insert 行等后:按展示条数纠正页码并重载
function refreshCurrentPageAfterDelete() {
var pagination = draftManager.getPagination();
var currentPage = pagination.pageNumber;
var pageSize = pagination.pageSize;
var displayTotal = draftManager.getDisplayTotal();
var totalPages = Math.ceil(displayTotal / pageSize);
if (currentPage > totalPages && totalPages > 0) {
draftManager.setPageNumber(totalPages);
$('#subgridPager').pagination('select', totalPages);
} else if (displayTotal === 0) {
$('#feegrid').datagrid('loadData', []);
$('#subgridPager').pagination('refresh', {
total: 0,
pageNumber: 1,
pageSize: pageSize
});
} else {
loaddata_sub();
}
$('#feegrid').datagrid('clearChecked');
pruneSelectedRowsForCurrentPage();
calcw_selfee();
}
// 根据 selectedRows 主键集合,打勾/取消当前可见行(跨页真相源)
function syncFeeGridChecksFromSubState() {
var rows = $('#feegrid').datagrid('getRows');
var selKeys = {};
var selectedRows = draftManager.getSelectedRows();
for (var s = 0; s < selectedRows.length; s++) {
selKeys[draftManager.getRowKey(selectedRows[s])] = true;
}
var excludedKeys = {};
var excludedRows = draftManager.getExcludedRows();
for (var e = 0; e < excludedRows.length; e++) {
excludedKeys[draftManager.getRowKey(excludedRows[e])] = true;
}
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var key = draftManager.getRowKey(row);
if (draftManager.isAllChecked()) {
if (excludedKeys[key]) {
$('#feegrid').datagrid('uncheckRow', i);
} else {
$('#feegrid').datagrid('checkRow', i);
}
} else {
if (selKeys[key]) {
$('#feegrid').datagrid('checkRow', i);
} else {
$('#feegrid').datagrid('uncheckRow', i);
}
}
}
}
function toggleSelectAll() {
if (draftManager.isAllChecked()) {
draftManager.setAllChecked(false);
draftManager.clearExcludedRows();
draftManager.clearSelectedRows();
$('#feegrid').datagrid('clearChecked');
$('#btn_select_all').val('全部选中');
} else {
draftManager.setAllChecked(true);
draftManager.clearExcludedRows();
var rows = $('#feegrid').datagrid('getRows');
for (var i = 0; i < rows.length; i++) {
$('#feegrid').datagrid('checkRow', i);
}
$('#btn_select_all').val('全部反选');
}
gridManager.toggleSelectAll();
}
//////////-------feegrid-----------
@@ -920,8 +563,8 @@
// 草稿:已保存行标记删除 → 仍计入 DB 汇总,需扣回;本地新增 insertRows → 尚未进 DB,需加上
var dIdx;
var deleteRows = draftManager.getDeleteRows();
var insertRows = draftManager.getInsertRows();
var deleteRows = gridManager.getDeleteRows();
var insertRows = gridManager.getInsertRows();
for (dIdx = 0; dIdx < deleteRows.length; dIdx++) {
applyFeeRowToMoneyTypeGroups(groupMap, deleteRows[dIdx], -1);
}
@@ -981,7 +624,7 @@
}
function calcDeleteFee() {
var deleteRows = draftManager.getDeleteRows();
var deleteRows = gridManager.getDeleteRows();
var deleteGroupMap = {};
for (var i = 0; i < deleteRows.length; i++) {
@@ -1025,7 +668,7 @@
// 计算选中行的金额(仅业务有效行)
function calcw_selfee() {
var rows = draftManager.getValidSelectedRows();
var rows = gridManager.getValidSelectedRows();
var irow = rows.length;
@@ -1254,9 +897,8 @@
loaddata(strid);
endEditing_con();
draftManager.reset();
loaddata_sub();
gridManager.reset();
gridManager.loadPage(1);
calcw_fee();
var formobj = app.serializeObject2($('#zdskform'));
@@ -1374,7 +1016,7 @@
var ywidMap = {};
var deleteRows = draftManager.getDeleteRows();
var deleteRows = gridManager.getDeleteRows();
if (deleteRows) {
for (let d of deleteRows) {
if (!d || !d.subid) {
@@ -1387,7 +1029,7 @@
}
}
var insertRows = draftManager.getInsertRows();
var insertRows = gridManager.getInsertRows();
if (insertRows) {
for (let r of insertRows) {
if (!r || !r.subid) {
@@ -1647,7 +1289,7 @@
console.log('主表变化');
return true;
} else {
if (draftManager.getInsertRows().length > 0 || draftManager.getUpdateRows().length > 0 || draftManager.getDeleteRows().length > 0) {
if (gridManager.getInsertRows().length > 0 || gridManager.getUpdateRows().length > 0 || gridManager.getDeleteRows().length > 0) {
return true;
}
return false;
@@ -1711,19 +1353,19 @@
for (var i = 0; i < result.length; i++) {
var r = result[i];
var rk = draftManager.getRowKey(r);
var dup = draftManager.getInsertRows().some(function (x) {
return draftManager.getRowKey(x) === rk;
var rk = gridManager.getDraftManager().getRowKey(r);
var dup = gridManager.getInsertRows().some(function (x) {
return gridManager.getDraftManager().getRowKey(x) === rk;
});
if (!dup) {
if (!r._rowState) {
r._rowState = 'insert';
}
draftManager.addInsertRow(r);
gridManager.addInsertRows(r);
}
}
refreshCurrentPage();
gridManager.refreshCurrentPage();
calcw_fee();
}
},
@@ -1763,30 +1405,30 @@
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
var rk = draftManager.getRowKey(r);
var dup = draftManager.getInsertRows().some(function (x) {
return draftManager.getRowKey(x) === rk;
var rk = gridManager.getDraftManager().getRowKey(r);
var dup = gridManager.getInsertRows().some(function (x) {
return gridManager.getDraftManager().getRowKey(x) === rk;
});
if (!dup) {
if (!r._rowState) {
r._rowState = 'insert';
}
draftManager.addInsertRow(r);
gridManager.addInsertRows(r);
}
}
refreshCurrentPage();
gridManager.refreshCurrentPage();
calcw_fee();
}
////////////////////////////////////////////////////////////
// 子表草稿:标记删除或移除 insert(数据源 getActualSelectedRows)
// 子表草稿:标记删除或移除 insert(数据源 getValidSelectedRows)
function removeit() {
$.messager.confirm("操作提示", '您确定要删除勾选的费用吗?', function (r) {
if (r) {
endEditing_con();
var selectedRows = draftManager.getValidSelectedRows();
var selectedRows = gridManager.getValidSelectedRows();
if (selectedRows.length === 0) {
$.messager.alert('提示', '请选择要删除的行(已标记删除的行请使用「撤销删除」)', 'warning');
@@ -1806,33 +1448,30 @@
var hasInsertDelete = false;
var dm = gridManager.getDraftManager();
for (var i = selectedRows.length - 1; i >= 0; i--) {
var row = selectedRows[i];
if (draftManager.isInsertRow(row) || draftManager.isInInsertRows(row)) {
draftManager.removeInsertRow(row);
draftManager.removeFromUpdateRows(row);
if (dm.isInsertRow(row) || dm.isInInsertRows(row)) {
gridManager.removeInsertRow(row);
hasInsertDelete = true;
} else {
var delKey = draftManager.getRowKey(row);
var wasUpdate = draftManager.getUpdateRows().some(function (rr) {
return draftManager.getRowKey(rr) === delKey;
var delKey = dm.getRowKey(row);
var wasUpdate = gridManager.getUpdateRows().some(function (rr) {
return dm.getRowKey(rr) === delKey;
});
row._undoRowState = wasUpdate ? 'update' : '';
row._rowState = 'delete';
draftManager.addToDeleteRows(row);
draftManager.removeFromUpdateRows(row);
gridManager.addDeleteRow(row);
}
draftManager.removeSelectedRow(row);
}
calcw_fee();
if (hasInsertDelete) {
refreshCurrentPageAfterDelete();
gridManager.refreshAfterDelete();
} else {
refreshCurrentPageDisplay();
gridManager.refreshDisplay();
}
}
})
@@ -1844,13 +1483,14 @@
var pageRows = $('#feegrid').datagrid('getRows');
var targets = [];
var selKeys = {};
var selectedRows = draftManager.getSelectedRows();
var dm = gridManager.getDraftManager();
var selectedRows = gridManager.getSelectedRows();
for (var sk = 0; sk < selectedRows.length; sk++) {
selKeys[draftManager.getRowKey(selectedRows[sk])] = true;
selKeys[dm.getRowKey(selectedRows[sk])] = true;
}
var hasSelectionOnPage = false;
for (var hp = 0; hp < pageRows.length; hp++) {
if (selKeys[draftManager.getRowKey(pageRows[hp])]) {
if (selKeys[dm.getRowKey(pageRows[hp])]) {
hasSelectionOnPage = true;
break;
}
@@ -1862,14 +1502,14 @@
}
}
} else {
var delSel = draftManager.getDeletedSelectedRows();
var delSel = dm.getDeletedSelectedRows();
var delKeys = {};
for (var dv = 0; dv < delSel.length; dv++) {
delKeys[draftManager.getRowKey(delSel[dv])] = true;
delKeys[dm.getRowKey(delSel[dv])] = true;
}
for (var v = 0; v < pageRows.length; v++) {
var pr = pageRows[v];
if (pr._rowState === 'delete' && delKeys[draftManager.getRowKey(pr)]) {
if (pr._rowState === 'delete' && delKeys[dm.getRowKey(pr)]) {
targets.push(pr);
}
}
@@ -1883,18 +1523,10 @@
if (row._rowState !== 'delete') {
continue;
}
var prev = row._undoRowState;
draftManager.removeFromDeleteRows(row);
if (prev === 'update') {
row._rowState = 'update';
draftManager.addToUpdateRows(row);
} else {
row._rowState = '';
}
delete row._undoRowState;
gridManager.removeDeleteRow(row);
}
calcw_fee();
refreshCurrentPageAfterUndo();
gridManager.refreshAfterUndo();
}
// 批量删除
@@ -1930,7 +1562,7 @@
var zdorderid = $('#b_id').val();
var url;
var i = 0;
var rows = draftManager.getValidSelectedRows();
var rows = gridManager.getValidSelectedRows();
if (rows.length <= 0) {
var moneyid = $('#b_money_id').val();
url = "jcwzd/editskd.jsp?zdorderid=" + zdorderid + "&moneyid=" + moneyid;
@@ -2005,7 +1637,7 @@
}
var i = 0;
var url;
var rows = draftManager.getValidSelectedRows();
var rows = gridManager.getValidSelectedRows();
/* if (rows.length<=0){
return false;
};
@@ -2340,7 +1972,7 @@
});
}
function locked_ys() {
var rows = draftManager.getValidSelectedRows();
var rows = gridManager.getValidSelectedRows();
var locked_idarr = new Array();
var updaterows = new Array();
for (var i in rows) {
@@ -2359,7 +1991,7 @@
}
}
function locked_cw() {
var rows = draftManager.getValidSelectedRows();
var rows = gridManager.getValidSelectedRows();
var locked_idarr = new Array();
var updaterows = new Array();
for (var i in rows) {
@@ -2378,7 +2010,7 @@
}
}
function unlocked_cw() {
var rows = draftManager.getValidSelectedRows();
var rows = gridManager.getValidSelectedRows();
var locked_idarr = new Array();
var updaterows = new Array();
for (var i in rows) {
-439
View File
@@ -1,439 +0,0 @@
/**
* 草稿态分页管理模块
*
* 职责:
* 1. 草稿池管理:insertRows / updateRows / deleteRows
* 2. 分页管理:远程数据与本地数据混排
* 3. 选中状态管理:跨页勾选状态同步
* 4. 行状态恢复:_rowState 字段维护
*
* 使用方式:
* var draftManager = window.DraftPageManager.createDraftManager({
* keyField: 'subid',
* pageSize: 50
* });
*/
(function(window) {
'use strict';
/**
* 创建草稿态管理器实例
* @param {Object} config 配置参数
* @param {string} config.keyField 主键字段名,默认 'id'
* @param {number} config.pageSize 每页条数,默认 50
* @returns {Object} 管理器实例
*/
function createDraftManager(config) {
var keyField = config && config.keyField || 'id';
var defaultPageSize = config && config.pageSize || 50;
var state = {
pagination: {
pageSize: defaultPageSize,
pageNumber: 1,
total: 0,
remoteTotal: 0
},
currentRemoteRows: [],
insertRows: [],
updateRows: [],
deleteRows: [],
selectedRows: [],
isAllChecked: false,
excludedRows: []
};
// ==================== 状态查询方法 ====================
function getPagination() {
return {
pageNumber: state.pagination.pageNumber,
pageSize: state.pagination.pageSize,
total: state.pagination.total,
remoteTotal: state.pagination.remoteTotal
};
}
function getInsertRows() {
return state.insertRows;
}
function getUpdateRows() {
return state.updateRows;
}
function getDeleteRows() {
return state.deleteRows;
}
function getSelectedRows() {
return state.selectedRows;
}
function getValidSelectedRows() {
return state.selectedRows.filter(function(row) {
return row._rowState !== 'delete';
});
}
function getDeletedSelectedRows() {
return state.selectedRows.filter(function(row) {
return row._rowState === 'delete';
});
}
function isAllChecked() {
return state.isAllChecked;
}
function getExcludedRows() {
return state.excludedRows;
}
// ==================== 草稿池操作方法 ====================
function addInsertRow(row) {
if (!row._rowState) {
row._rowState = 'insert';
}
var key = getRowKey(row);
var exists = state.insertRows.some(function(r) {
return getRowKey(r) === key;
});
if (!exists) {
state.insertRows.push(row);
}
}
function removeInsertRow(row) {
var key = getRowKey(row);
state.insertRows = state.insertRows.filter(function(r) {
return getRowKey(r) !== key;
});
}
function addToUpdateRows(row) {
var key = getRowKey(row);
var exists = state.updateRows.some(function(r) {
return getRowKey(r) === key;
});
if (!exists) {
state.updateRows.push(row);
}
}
function removeFromUpdateRows(row) {
var key = getRowKey(row);
state.updateRows = state.updateRows.filter(function(r) {
return getRowKey(r) !== key;
});
}
function addToDeleteRows(row) {
var key = getRowKey(row);
var exists = state.deleteRows.some(function(r) {
return getRowKey(r) === key;
});
if (!exists) {
state.deleteRows.push(row);
}
}
function removeFromDeleteRows(row) {
var key = getRowKey(row);
state.deleteRows = state.deleteRows.filter(function(r) {
return getRowKey(r) !== key;
});
}
// ==================== 选中状态操作方法 ====================
function addSelectedRow(row) {
var key = getRowKey(row);
for (var i = 0; i < state.selectedRows.length; i++) {
if (getRowKey(state.selectedRows[i]) === key) {
state.selectedRows[i] = row;
return;
}
}
state.selectedRows.push(row);
}
function removeSelectedRow(row) {
var key = getRowKey(row);
state.selectedRows = state.selectedRows.filter(function(item) {
return getRowKey(item) !== key;
});
}
function clearSelectedRows() {
state.selectedRows = [];
}
function setAllChecked(value) {
state.isAllChecked = value;
if (value) {
clearSelectedRows();
}
}
function addExcludedRow(row) {
if (isRowExcluded(row)) {
return;
}
state.excludedRows.push(row);
}
function removeExcludedRow(row) {
var key = getRowKey(row);
state.excludedRows = state.excludedRows.filter(function(r) {
return getRowKey(r) !== key;
});
}
function clearExcludedRows() {
state.excludedRows = [];
}
function isRowExcluded(row) {
var key = getRowKey(row);
return state.excludedRows.some(function(r) {
return getRowKey(r) === key;
});
}
// ==================== 分页操作方法 ====================
function setPageNumber(pageNumber) {
state.pagination.pageNumber = pageNumber;
}
function setPageSize(pageSize) {
state.pagination.pageSize = pageSize;
}
function setRemoteTotal(remoteTotal) {
state.pagination.remoteTotal = remoteTotal;
state.pagination.total = getActualTotal();
}
function setCurrentRemoteRows(rows) {
state.currentRemoteRows = rows;
}
function buildPageData(remoteRows, pageNumber) {
var pageSize = state.pagination.pageSize;
var remoteTotal = state.pagination.remoteTotal;
var insertRows = state.insertRows;
var pageStart = (pageNumber - 1) * pageSize;
var pageEnd = pageStart + pageSize;
remoteRows.forEach(function(row) {
if (!row._rowState) {
row._rowState = '';
}
});
insertRows.forEach(function(row) {
if (!row._rowState) {
row._rowState = 'insert';
}
});
var result = [];
result = result.concat(remoteRows);
var insertStartIdx = Math.max(0, pageStart - remoteTotal);
var insertEndIdx = pageEnd - remoteTotal;
if (insertStartIdx < insertRows.length) {
var insertSlice = insertRows.slice(insertStartIdx, insertEndIdx);
result = result.concat(insertSlice);
}
if (result.length > pageSize) {
result = result.slice(0, pageSize);
}
return result;
}
function restoreRowStates(rows) {
rows.forEach(function(row) {
var key = getRowKey(row);
var deleteMatch = state.deleteRows.find(function(r) {
return getRowKey(r) === key;
});
if (deleteMatch) {
row._rowState = 'delete';
if (deleteMatch._undoRowState !== undefined) {
row._undoRowState = deleteMatch._undoRowState;
}
return;
}
var insertMatch = state.insertRows.find(function(r) {
return getRowKey(r) === key;
});
if (insertMatch) {
row._rowState = insertMatch._rowState || 'insert';
return;
}
var updateMatch = state.updateRows.find(function(r) {
return getRowKey(r) === key;
});
if (updateMatch) {
row._rowState = 'update';
return;
}
row._rowState = '';
});
}
// ==================== 工具方法 ====================
function getRowKey(row) {
return row[keyField];
}
function isInsertRow(row) {
return row._rowState === 'insert';
}
function isDeleteRow(row) {
return row._rowState === 'delete';
}
function isUpdateRow(row) {
return row._rowState === 'update';
}
function isInInsertRows(row) {
var key = getRowKey(row);
return state.insertRows.some(function(r) {
return getRowKey(r) === key;
});
}
function isSavedRow(row) {
return !isInsertRow(row) && !isInInsertRows(row);
}
function getActualTotal() {
return state.pagination.remoteTotal + state.insertRows.length;
}
function getDisplayTotal() {
var insertDisplayCount = state.insertRows.filter(function(row) {
return row._rowState !== 'delete';
}).length;
return state.pagination.remoteTotal + insertDisplayCount;
}
function getTotalPages() {
return Math.ceil(getActualTotal() / state.pagination.pageSize);
}
function hasInsertRowsInPage(pageNumber) {
var range = getPageRange(pageNumber);
var remoteTotal = state.pagination.remoteTotal;
return range.start < remoteTotal + state.insertRows.length &&
range.end > remoteTotal;
}
function isInsertOnlyPage(pageNumber) {
var range = getPageRange(pageNumber);
return range.start >= state.pagination.remoteTotal;
}
function getPageRange(pageNumber) {
var pageSize = state.pagination.pageSize;
var startIdx = (pageNumber - 1) * pageSize;
var endIdx = startIdx + pageSize;
return { start: startIdx, end: endIdx };
}
function removeSelectionForRows(gridRows) {
var drop = {};
for (var i = 0; i < gridRows.length; i++) {
drop[getRowKey(gridRows[i])] = true;
}
state.selectedRows = state.selectedRows.filter(function(item) {
return !drop[getRowKey(item)];
});
}
function reset() {
state.pagination = {
pageSize: defaultPageSize,
pageNumber: 1,
total: 0,
remoteTotal: 0
};
state.currentRemoteRows = [];
state.insertRows = [];
state.updateRows = [];
state.deleteRows = [];
state.selectedRows = [];
state.isAllChecked = false;
state.excludedRows = [];
}
return {
getPagination: getPagination,
getInsertRows: getInsertRows,
getUpdateRows: getUpdateRows,
getDeleteRows: getDeleteRows,
getSelectedRows: getSelectedRows,
getValidSelectedRows: getValidSelectedRows,
getDeletedSelectedRows: getDeletedSelectedRows,
isAllChecked: isAllChecked,
getExcludedRows: getExcludedRows,
addInsertRow: addInsertRow,
removeInsertRow: removeInsertRow,
addToUpdateRows: addToUpdateRows,
removeFromUpdateRows: removeFromUpdateRows,
addToDeleteRows: addToDeleteRows,
removeFromDeleteRows: removeFromDeleteRows,
addSelectedRow: addSelectedRow,
removeSelectedRow: removeSelectedRow,
clearSelectedRows: clearSelectedRows,
setAllChecked: setAllChecked,
addExcludedRow: addExcludedRow,
removeExcludedRow: removeExcludedRow,
clearExcludedRows: clearExcludedRows,
isRowExcluded: isRowExcluded,
setPageNumber: setPageNumber,
setPageSize: setPageSize,
setRemoteTotal: setRemoteTotal,
setCurrentRemoteRows: setCurrentRemoteRows,
buildPageData: buildPageData,
restoreRowStates: restoreRowStates,
getRowKey: getRowKey,
isInsertRow: isInsertRow,
isDeleteRow: isDeleteRow,
isUpdateRow: isUpdateRow,
isInInsertRows: isInInsertRows,
isSavedRow: isSavedRow,
getActualTotal: getActualTotal,
getDisplayTotal: getDisplayTotal,
getTotalPages: getTotalPages,
hasInsertRowsInPage: hasInsertRowsInPage,
isInsertOnlyPage: isInsertOnlyPage,
removeSelectionForRows: removeSelectionForRows,
reset: reset
};
}
window.DraftPageManager = {
createDraftManager: createDraftManager
};
})(window);
+937
View File
@@ -0,0 +1,937 @@
/**
* DataGrid 公共管理模块
*
* 核心设计原则:
* 1. 草稿态维护最终状态,而非操作过程
* 2. delete 是临时挂起状态,保留 backup 用于恢复
* 3. 本地数据不参与条件搜索,搜索完全依赖远程接口
* 4. 搜索与展示分离:搜索=远程,展示=本地状态覆盖
* 5. baseSearchCondition 作为永久根条件
*
* 使用方式:
* var gridManager = createDataGridManager({
* gridId: 'feegrid',
* keyField: 'subid',
* viewName: 'v_cwzd_orderfee_s',
* baseSearchCondition: "mx_zdorderid='xxx'",
* onLoadSuccess: function(rows) { ... },
* onAfterEdit: function(rowIndex, row, changes) { ... },
* onSelectionChange: function() { ... }
* });
*/
(function (window) {
"use strict";
// ==================== DraftStateManager(内部模块)====================
function createDraftStateManager(config) {
var keyField = (config && config.keyField) || "id";
var defaultPageSize = (config && config.pageSize) || 50;
var draftStateMap = {};
var isAllChecked = false;
var pagination = {
pageSize: defaultPageSize,
pageNumber: 1,
remoteTotal: 0,
};
function initOrUpdateDraftState(rowId) {
if (!draftStateMap[rowId]) {
draftStateMap[rowId] = {
state: null,
data: null,
backup: null,
selected: false,
excluded: false,
};
}
return draftStateMap[rowId];
}
function setDraftState(rowId, state, data, backup) {
var draftState = initOrUpdateDraftState(rowId);
draftState.state = state;
draftState.data = data;
draftState.backup = backup;
}
function getDraftState(rowId) {
return draftStateMap[rowId];
}
function removeDraftState(rowId) {
delete draftStateMap[rowId];
}
function hasDraftState(rowId) {
return draftStateMap.hasOwnProperty(rowId);
}
function setRowSelected(rowId, selected) {
var draftState = initOrUpdateDraftState(rowId);
draftState.selected = selected;
}
function setRowExcluded(rowId, excluded) {
var draftState = initOrUpdateDraftState(rowId);
draftState.excluded = excluded;
}
function addInsertRow(row) {
if (!row._rowState) row._rowState = "insert";
var key = getRowKey(row);
setDraftState(key, "insert", row, null);
}
function removeInsertRow(row) {
var key = getRowKey(row);
removeDraftState(key);
}
function addUpdateRow(row) {
var key = getRowKey(row);
var existingState = getDraftState(key);
if (existingState && existingState.state === "delete") {
return;
}
row._rowState = "update";
setDraftState(key, "update", row, null);
}
function addDeleteRow(row) {
var key = getRowKey(row);
var existingState = getDraftState(key);
var backupData = existingState ? existingState.data : row;
row._rowState = "delete";
setDraftState(key, "delete", row, backupData);
setRowSelected(key, false);
}
function recoverRow(row) {
var key = getRowKey(row);
var state = getDraftState(key);
if (state && state.state === "delete") {
if (state.backup) {
var backupState = state.backup._rowState || "";
setDraftState(key, backupState, state.backup, null);
row._rowState = backupState;
} else {
removeDraftState(key);
row._rowState = "";
}
}
}
function addSelectedRow(row) {
var key = getRowKey(row);
setRowSelected(key, true);
}
function removeSelectedRow(row) {
var key = getRowKey(row);
setRowSelected(key, false);
}
function clearSelectedRows() {
for (var key in draftStateMap) {
draftStateMap[key].selected = false;
}
}
function addExcludedRow(row) {
var key = getRowKey(row);
setRowExcluded(key, true);
}
function removeExcludedRow(row) {
var key = getRowKey(row);
setRowExcluded(key, false);
}
function clearExcludedRows() {
for (var key in draftStateMap) {
draftStateMap[key].excluded = false;
}
}
function isRowExcluded(row) {
var key = getRowKey(row);
var state = getDraftState(key);
return state ? state.excluded : false;
}
function cloneRow(row) {
if (!row) return null;
return JSON.parse(JSON.stringify(row));
}
function applyDraftState(remoteRows) {
var displayRows = [];
for (var i = 0; i < remoteRows.length; i++) {
var row = remoteRows[i];
var key = getRowKey(row);
var draftState = getDraftState(key);
if (draftState) {
switch (draftState.state) {
case "delete":
continue;
case "update":
case "insert":
displayRows.push(cloneRow(draftState.data));
break;
default:
displayRows.push(cloneRow(row));
}
} else {
displayRows.push(cloneRow(row));
}
}
return displayRows;
}
function reset() {
draftStateMap = {};
isAllChecked = false;
pagination = {
pageSize: defaultPageSize,
pageNumber: 1,
remoteTotal: 0,
};
}
function getRowKey(row) {
return row[keyField];
}
function getRowIdsByState(stateFilter) {
var ids = [];
for (var key in draftStateMap) {
var state = draftStateMap[key];
if (state.state === stateFilter) {
ids.push(key);
}
}
return ids;
}
return {
keyField: keyField,
setDraftState: setDraftState,
getDraftState: getDraftState,
removeDraftState: removeDraftState,
hasDraftState: hasDraftState,
addInsertRow: addInsertRow,
removeInsertRow: removeInsertRow,
addUpdateRow: addUpdateRow,
addDeleteRow: addDeleteRow,
recoverRow: recoverRow,
getInsertRows: function () {
var inserts = [];
for (var key in draftStateMap) {
if (draftStateMap[key].state === "insert") {
inserts.push(draftStateMap[key].data);
}
}
return inserts;
},
getInsertRowIds: function () {
return getRowIdsByState("insert");
},
getUpdateRows: function () {
var updates = [];
for (var key in draftStateMap) {
if (draftStateMap[key].state === "update") {
updates.push(draftStateMap[key].data);
}
}
return updates;
},
getDeleteRows: function () {
var deletes = [];
for (var key in draftStateMap) {
if (draftStateMap[key].state === "delete") {
deletes.push(draftStateMap[key].data);
}
}
return deletes;
},
addSelectedRow: addSelectedRow,
removeSelectedRow: removeSelectedRow,
getSelectedRowIds: function () {
var ids = [];
for (var key in draftStateMap) {
if (
draftStateMap[key].selected &&
(!draftStateMap[key].state || draftStateMap[key].state !== "delete")
) {
ids.push(key);
}
}
return ids;
},
getSelectedRows: function () {
var rows = [];
for (var key in draftStateMap) {
var state = draftStateMap[key];
if (state.selected && state.data) {
rows.push(state.data);
}
}
return rows;
},
getValidSelectedRows: function () {
var rows = [];
for (var key in draftStateMap) {
var state = draftStateMap[key];
if (
state.selected &&
state.data &&
(!state.state || state.state !== "delete")
) {
rows.push(state.data);
}
}
return rows;
},
clearSelectedRows: clearSelectedRows,
isAllChecked: function () {
return isAllChecked;
},
setAllChecked: function (value) {
isAllChecked = value;
if (value) clearSelectedRows();
},
addExcludedRow: addExcludedRow,
removeExcludedRow: removeExcludedRow,
getExcludedRowIds: function () {
var ids = [];
for (var key in draftStateMap) {
if (draftStateMap[key].excluded) {
ids.push(key);
}
}
return ids;
},
getExcludedRows: function () {
var rows = [];
for (var key in draftStateMap) {
var state = draftStateMap[key];
if (state.excluded && state.data) {
rows.push(state.data);
}
}
return rows;
},
clearExcludedRows: clearExcludedRows,
isRowExcluded: isRowExcluded,
applyDraftState: applyDraftState,
pagination: {
get: function () {
return pagination;
},
setPageNumber: function (pageNumber) {
pagination.pageNumber = pageNumber;
},
setPageSize: function (pageSize) {
pagination.pageSize = pageSize;
},
setRemoteTotal: function (remoteTotal) {
pagination.remoteTotal = remoteTotal;
},
getPageSize: function () {
return pagination.pageSize;
},
getPageNumber: function () {
return pagination.pageNumber;
},
getRemoteTotal: function () {
return pagination.remoteTotal;
},
getTotalPages: function () {
return Math.ceil(pagination.remoteTotal / pagination.pageSize);
},
},
getRowKey: getRowKey,
reset: reset,
};
}
// ==================== createDataGridManager(导出)====================
function createDataGridManager(config) {
var $ = window.jQuery;
var DEFAULT_CONFIG = {
gridId: "feegrid",
keyField: "subid",
pageSize: 50,
viewName: "",
searchUrl: "jsp/searchdatapagenew.jsp",
orderField: "b_id",
baseSearchCondition: null,
searchCondition: null,
pageList: [50, 100, 200],
rowStateStyles: {
insert: "background-color: #f6ffed;",
update: "background-color: #fffbe6;",
delete: "background-color: #fff1f0;",
},
selectAllBtn: "#btn_select_all",
selectAllText: "全部选中",
unselectAllText: "全部反选",
userid: "",
customDatagridOptions: null,
ajaxTimeout: 30000,
onLoadError: null,
onLoadSuccess: null,
onAfterEdit: null,
onSelectionChange: null,
};
var options = $.extend(true, {}, DEFAULT_CONFIG, config);
var state = {
draftManager: null,
editIndex: undefined,
isLoading: false,
pagerId: null,
requestVersion: 0,
};
_initDraftManager();
_initGrid();
function _initDraftManager() {
state.draftManager = createDraftStateManager({
keyField: options.keyField,
pageSize: options.pageSize,
});
}
function _getPagerId() {
if (!state.pagerId) {
state.pagerId = options.gridId + "_pager";
}
return state.pagerId;
}
function _setLoading() {
state.isLoading = true;
$("#" + options.gridId).datagrid("loading");
$("#" + _getPagerId())
.css({
"pointer-events": "none",
opacity: "0.6",
})
.pagination("loading");
}
function _clearLoading() {
state.isLoading = false;
$("#" + options.gridId).datagrid("loaded");
$("#" + _getPagerId())
.css({
"pointer-events": "",
opacity: "",
})
.pagination("loaded");
}
function _syncGridChecks() {
var rows = $("#" + options.gridId).datagrid("getRows");
var selectedKeys = {};
var selectedRowIds = state.draftManager.getSelectedRowIds();
for (var s = 0; s < selectedRowIds.length; s++) {
selectedKeys[selectedRowIds[s]] = true;
}
var excludedKeys = {};
var excludedRowIds = state.draftManager.getExcludedRowIds();
for (var e = 0; e < excludedRowIds.length; e++) {
excludedKeys[excludedRowIds[e]] = true;
}
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var key = state.draftManager.getRowKey(row);
if (state.draftManager.isAllChecked()) {
if (excludedKeys[key]) {
$("#" + options.gridId).datagrid("uncheckRow", i);
} else {
$("#" + options.gridId).datagrid("checkRow", i);
}
} else {
if (selectedKeys[key]) {
$("#" + options.gridId).datagrid("checkRow", i);
} else {
$("#" + options.gridId).datagrid("uncheckRow", i);
}
}
}
}
function _handleRowStyles() {
$(".datagrid-body tr").each(function () {
var rowIndex = $(this).attr("datagrid-row-index");
if (rowIndex !== undefined) {
var row = $("#" + options.gridId).datagrid("getRows")[
parseInt(rowIndex)
];
if (row && row._rowState === "delete") {
$(this).addClass("datagrid-row-delete");
} else {
$(this).removeClass("datagrid-row-delete");
}
}
});
}
function buildFinalCondition() {
var parts = [];
if (options.baseSearchCondition) {
parts.push(options.baseSearchCondition);
}
if (options.searchCondition) {
parts.push(options.searchCondition);
}
return parts.join(" AND ");
}
function loadPage(pageNumber) {
_setLoading();
var pageSize = state.draftManager.pagination.getPageSize();
var searchCondition = buildFinalCondition();
var currentRequestVersion = ++state.requestVersion;
if (!searchCondition) {
var insertRows = state.draftManager.getInsertRows();
var pageData = [];
if (insertRows.length > 0) {
var startInInsert = (pageNumber - 1) * pageSize;
var endInInsert = startInInsert + pageSize;
pageData = insertRows.slice(startInInsert, endInInsert);
}
_renderPage(pageData, insertRows.length, pageNumber, pageSize);
_clearLoading();
return;
}
var searchParam = {
viewname: options.viewName,
searchcondition: searchCondition,
userid: options.userid,
orderfield: options.orderField,
keyfield: options.keyField,
page: pageNumber,
rows: pageSize,
};
$.ajax({
type: "post",
url: options.searchUrl,
cache: false,
data: searchParam,
dataType: "json",
timeout: options.ajaxTimeout,
success: function (result) {
if (currentRequestVersion !== state.requestVersion) {
return;
}
state.draftManager.pagination.setRemoteTotal(result.total);
var displayRows = state.draftManager.applyDraftState(result.rows);
_renderPage(displayRows, result.total, pageNumber, pageSize);
},
error: function (xhr, status, error) {
if (currentRequestVersion !== state.requestVersion) {
return;
}
if (typeof options.onLoadError === "function") {
options.onLoadError(xhr, status, error);
} else {
console.log("分页加载失败: " + status);
}
},
complete: function () {
if (currentRequestVersion === state.requestVersion) {
_clearLoading();
}
},
});
}
function _renderPage(pageData, total, pageNumber, pageSize) {
$("#" + options.gridId).datagrid("loadData", pageData);
$("#" + _getPagerId()).pagination("refresh", {
total: total,
pageNumber: pageNumber,
pageSize: pageSize,
});
}
function refreshCurrentPage() {
var currentPage = state.draftManager.pagination.getPageNumber();
loadPage(currentPage);
}
function refreshDisplay() {
var currentRows = $("#" + options.gridId).datagrid("getRows");
$("#" + options.gridId).datagrid("loadData", currentRows);
$("#" + options.gridId).datagrid("clearChecked");
_triggerCallback("onSelectionChange");
}
function refreshAfterDelete() {
var pageSize = state.draftManager.pagination.getPageSize();
var remoteTotal = state.draftManager.pagination.getRemoteTotal();
var totalPages = Math.ceil(remoteTotal / pageSize);
var currentPage = state.draftManager.pagination.getPageNumber();
if (currentPage > totalPages && totalPages > 0) {
state.draftManager.pagination.setPageNumber(totalPages);
$("#" + _getPagerId()).pagination("select", totalPages);
} else if (remoteTotal === 0) {
$("#" + options.gridId).datagrid("loadData", []);
$("#" + _getPagerId()).pagination("refresh", {
total: 0,
pageNumber: 1,
pageSize: pageSize,
});
} else {
loadPage(currentPage);
}
$("#" + options.gridId).datagrid("clearChecked");
_triggerCallback("onSelectionChange");
}
function refreshAfterUndo() {
var currentPage = state.draftManager.pagination.getPageNumber();
loadPage(currentPage);
}
function addInsertRows(rows) {
if (!$.isArray(rows)) rows = [rows];
for (var i = 0; i < rows.length; i++) {
state.draftManager.addInsertRow(rows[i]);
}
}
function removeInsertRow(row) {
state.draftManager.removeInsertRow(row);
}
function addDeleteRow(row) {
state.draftManager.addDeleteRow(row);
}
function removeDeleteRow(row) {
state.draftManager.recoverRow(row);
refreshAfterUndo();
}
function addUpdateRow(row) {
state.draftManager.addUpdateRow(row);
}
function toggleSelectAll() {
var btn = $(options.selectAllBtn);
if (state.draftManager.isAllChecked()) {
state.draftManager.setAllChecked(false);
state.draftManager.clearExcludedRows();
state.draftManager.clearSelectedRows();
$("#" + options.gridId).datagrid("clearChecked");
if (btn.length) btn.val(options.selectAllText);
} else {
state.draftManager.setAllChecked(true);
state.draftManager.clearExcludedRows();
var rows = $("#" + options.gridId).datagrid("getRows");
for (var i = 0; i < rows.length; i++) {
$("#" + options.gridId).datagrid("checkRow", i);
}
if (btn.length) btn.val(options.unselectAllText);
}
_triggerCallback("onSelectionChange");
}
function clearSelection() {
state.draftManager.setAllChecked(false);
state.draftManager.clearExcludedRows();
state.draftManager.clearSelectedRows();
$("#" + options.gridId).datagrid("clearChecked");
_triggerCallback("onSelectionChange");
}
function setAllChecked(value) {
state.draftManager.setAllChecked(value);
if (value) state.draftManager.clearExcludedRows();
_syncGridChecks();
_triggerCallback("onSelectionChange");
}
function setSearchCondition(condition) {
options.searchCondition = condition;
}
function clearSearch() {
options.searchCondition = null;
}
function reload(newSearchCondition) {
if (newSearchCondition !== undefined) {
options.searchCondition = newSearchCondition;
}
state.draftManager.pagination.setPageNumber(1);
loadPage(1);
}
function resetSearch() {
options.searchCondition = null;
reload();
}
function _triggerCallback(callbackName) {
var callback = options[callbackName];
if (typeof callback === "function") {
var args = Array.prototype.slice.call(arguments, 1);
callback.apply(null, args);
}
}
function handleAfterEdit(rowIndex, row, changes) {
if (Object.keys(changes).length === 0) return;
var key = state.draftManager.getRowKey(row);
var draftState = state.draftManager.getDraftState(key);
if (draftState && draftState.state === "delete") {
return;
}
if (draftState && draftState.state === "insert") {
row._rowState = "insert";
state.draftManager.setDraftState(key, "insert", row, null);
} else if (draftState && draftState.state === "update") {
row._rowState = "update";
state.draftManager.setDraftState(key, "update", row, null);
} else {
row._rowState = "update";
state.draftManager.addUpdateRow(row);
}
_triggerCallback("onAfterEdit", rowIndex, row, changes);
}
function handleRowCheck(index, row) {
if (state.draftManager.isAllChecked()) {
state.draftManager.removeExcludedRow(row);
} else {
state.draftManager.addSelectedRow(row);
}
_triggerCallback("onSelectionChange");
}
function handleRowUncheck(index, row) {
if (state.draftManager.isAllChecked()) {
state.draftManager.addExcludedRow(row);
} else {
state.draftManager.removeSelectedRow(row);
}
_triggerCallback("onSelectionChange");
}
function handleCheckAll() {
state.draftManager.setAllChecked(true);
state.draftManager.clearExcludedRows();
_triggerCallback("onSelectionChange");
}
function handleUncheckAll() {
state.draftManager.setAllChecked(false);
state.draftManager.clearExcludedRows();
state.draftManager.clearSelectedRows();
_triggerCallback("onSelectionChange");
}
function handleLoadSuccess() {
$.messager.progress("close");
_syncGridChecks();
_handleRowStyles();
var rows = $("#" + options.gridId).datagrid("getRows");
_triggerCallback("onLoadSuccess", rows);
}
function reset() {
state.editIndex = undefined;
state.isLoading = false;
if (state.draftManager) state.draftManager.reset();
}
function _initGrid() {
var baseDatagridOptions = {
idField: options.keyField,
pageSize: options.pageSize,
rowStyler: function (index, row) {
var stateStyle = options.rowStateStyles[row._rowState];
return stateStyle || "";
},
onLoadSuccess: handleLoadSuccess,
onAfterEdit: handleAfterEdit,
onCheckAll: handleCheckAll,
onUncheckAll: handleUncheckAll,
onCheck: handleRowCheck,
onUncheck: handleRowUncheck,
};
var datagridOptions = options.customDatagridOptions
? $.extend(true, {}, baseDatagridOptions, options.customDatagridOptions)
: baseDatagridOptions;
$("#" + options.gridId)
.datagrid(datagridOptions)
.datagrid("columnMoving");
$("#" + options.gridId)
.datagrid("getPanel")
.find(".datagrid-body")
.on("scroll.datagrid", function () {
if (state.editIndex !== undefined) {
$("#" + options.gridId).datagrid("endEdit", state.editIndex);
state.editIndex = undefined;
}
});
var pagerId = _getPagerId();
var pagerSelector = "#" + pagerId;
if ($(pagerSelector).length === 0) {
var gridPanel = $("#" + options.gridId).datagrid("getPanel");
var pagerDiv = $(
'<div id="' +
pagerId +
'" class="pagination" style="margin: 6px 0px;background:#efefef;border:1px solid #ccc;"></div>',
);
gridPanel.after(pagerDiv);
}
$(pagerSelector).pagination({
total: 0,
pageSize: options.pageSize,
pageNumber: 1,
pageList: options.pageList,
onSelectPage: function (pageNumber, pageSize) {
state.draftManager.pagination.setPageNumber(pageNumber);
state.draftManager.pagination.setPageSize(pageSize);
loadPage(pageNumber);
},
onChangePageSize: function (pageSize) {
state.draftManager.pagination.setPageNumber(1);
state.draftManager.pagination.setPageSize(pageSize);
if (!state.draftManager.isAllChecked()) {
$("#" + options.gridId).datagrid("clearChecked");
state.draftManager.clearSelectedRows();
}
_triggerCallback("onSelectionChange");
loadPage(1);
},
});
}
function setColumns(columns) {
$("#" + options.gridId).datagrid({
columns: [columns],
});
}
function isLoading() {
return state.isLoading;
}
function getPager() {
return $("#" + _getPagerId());
}
function getGrid() {
return $("#" + options.gridId);
}
return {
setColumns: setColumns,
loadPage: loadPage,
refreshCurrentPage: refreshCurrentPage,
refreshDisplay: refreshDisplay,
refreshAfterDelete: refreshAfterDelete,
refreshAfterUndo: refreshAfterUndo,
addInsertRows: addInsertRows,
removeInsertRow: removeInsertRow,
addDeleteRow: addDeleteRow,
removeDeleteRow: removeDeleteRow,
addUpdateRow: addUpdateRow,
getValidSelectedRows: function () {
return state.draftManager.getValidSelectedRows();
},
getSelectedRows: function () {
return state.draftManager.getSelectedRows();
},
toggleSelectAll: toggleSelectAll,
clearSelection: clearSelection,
setAllChecked: setAllChecked,
getInsertRows: function () {
return state.draftManager.getInsertRows();
},
getUpdateRows: function () {
return state.draftManager.getUpdateRows();
},
getDeleteRows: function () {
return state.draftManager.getDeleteRows();
},
getDraftManager: function () {
return state.draftManager;
},
setSearchCondition: setSearchCondition,
clearSearch: clearSearch,
reload: reload,
resetSearch: resetSearch,
isLoading: isLoading,
getPager: getPager,
getGrid: getGrid,
reset: reset,
};
}
window.createDataGridManager = createDataGridManager;
})(window);