diff --git a/build.bat b/build.bat
index c04a6ee..d11b0ea 100644
--- a/build.bat
+++ b/build.bat
@@ -9,8 +9,12 @@ echo ==========================================
echo.
REM Set paths
-set QT_PATH=D:\01.InstallInfo\InstallPath\QT\6.6.2\mingw_64
-set MINGW_PATH=D:\01.InstallInfo\InstallPath\QT\Tools\mingw1120_64
+set QT_PATH=D:\01.Installinfo\InstallPath\QT\6.6.2\mingw_64
+set MINGW_PATH=D:\01.Installinfo\InstallPath\QT\Tools\mingw1120_64
+set CODE_PATH=E:\02.personal\01.source\02.qt\uft30_change_code
+
+REM Switch to project directory
+cd /d "%CODE_PATH%"
REM Check if paths exist
if not exist "%QT_PATH%\bin\qmake.exe" (
@@ -86,6 +90,19 @@ if errorlevel 1 (
)
echo.
+REM Deploy 3rd-party DLLs from 3rd\ElaWidgetTools\bin (only copy when missing in bin)
+echo Deploying ElaWidgetTools DLLs...
+if not exist "bin" mkdir "bin"
+for %%F in ("%CODE_PATH%\3rd\ElaWidgetTools\bin\*.dll") do (
+ if not exist "bin\%%~nxF" (
+ copy "%%F" "bin\" >nul
+ echo [OK] %%~nxF -^> bin
+ ) else (
+ echo [SKIP] bin\%%~nxF already exists
+ )
+)
+echo.
+
REM Deploy default change_code.ini (only copy when missing, to keep user's existing config)
echo Deploying default change_code.ini...
if not exist "bin\uf2touft3\config" mkdir "bin\uf2touft3\config"
diff --git a/docs/使用手册.md b/docs/使用手册.md
index 1c9c95a..618bb1c 100644
--- a/docs/使用手册.md
+++ b/docs/使用手册.md
@@ -86,9 +86,11 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
**说明**:全量转码仅支持 **LS_ 开头**的功能("内存交易"改造范围),按清单对功能进行整体转码。清单保存在 `cust.json` 的 `convert.dir` 节点(见 §6.1)。
-**表格列**:`UF20函数名` | `是否存在` | `操作`(行尾编辑按钮)。
+**表格列**:`UF20函数名` | `是否存在` | `核心` | `是否柜面接口`(**双击函数名所在行**可在弹窗中修改函数名)。
- "是否存在"显示 **否**:说明元数据未更新或该功能在 UF20 元数据中检索不到(见 §7 FAQ);
+- **核心**:下拉选择该功能转码后归属的核心,可选 **ucbp**(默认)/ **uses** / **uarg** / **usms**;SQL 方言由核心自动派生——ucbp/uses 对应 mdb,uarg/usms 对应 db;
+- **是否柜面接口**:下拉选择 **否**(默认)/ **是**,标识该功能是否为柜面接口;
- 每次显示页面时自动刷新"是否存在"列。
**操作步骤:**
@@ -96,7 +98,7 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
1. 切到 **全量转码** Tab;
2. 点击 **添加**,弹窗内**每行输入一个** LS_ 函数名(支持批量粘贴);输入非 LS 函数将被拒绝,并提示切换到"按需转码";
3. 也可在 **UF20功能查询** 中双击或右键选中 LS_ 功能直接加入(见 §5.3.1);
-4. 点击行尾编辑按钮可修改函数名;**删除选中** / **清空列表**(二次确认)维护清单;
+4. **双击函数名所在行**可在弹窗中修改函数名;**删除选中** / **清空列表**(二次确认)维护清单;
5. 点击 **开始转换**,进度条与状态文字显示转换进度;转换中按钮变为 **停止转换**,可随时点击终止任务。
**清单同步**:手工编辑 `cust.json` 保存后约 0.3 秒内界面自动刷新(文件监视 + 防抖),程序与手工编辑双向同步。
@@ -114,14 +116,15 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
- **方式二**:按需转码 Tab 点击 **添加**,弹窗内每行输入一个函数短名(如 `AS_xxx`);误带 `.service_design` 等后缀会自动剥离;新增功能递归模式默认 **否**;若输入 LS_ 开头函数仅给出提示(建议放入全量转码),不会拦截;
- **方式三**:手工编辑配置文件 `cust_nonls.json`(见 §6.2),保存后程序自动检测并刷新界面。
-**表格列**:`UF20函数名` | `是否存在` | `是否递归` | `操作`(行尾编辑按钮)。
+**表格列**:`UF20函数名` | `是否存在` | `是否递归` | `db模式`(**双击函数名所在行**可在弹窗中修改函数名)。
- LS_ 开头的函数名以橙色字体显示提醒(不拦截)。
-**设置递归模式:**
+**设置递归模式与 db 模式:**
1. 在目标行的 **是否递归** 下拉框选择 **否**(单文件)或 **是**(递归);
-2. 选择后立即写入 `cust_nonls.json` 并生效,无需额外点击。
+2. 在 **db模式** 下拉框选择 SQL 语法转换方言:**否** = mdb(默认)、**是** = db;
+3. 选择后立即写入 `cust_nonls.json` 并生效,无需额外点击。
**执行转码:** 点击 **开始转换**,进度显示与停止方式同全量转码;任务互斥规则相同。
@@ -256,19 +259,38 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
| 节点 | 结构 | 用途 |
| --- | --- | --- |
-| `convert.dir` | 数组,元素为 `"函数名.service_design"` | 全量转码清单 |
+| `convert.dir` | 对象数组,元素 `{name, core, viaUmgr, dialect}` | 全量转码清单(含核心、柜面接口配置) |
| `tableCompareUf2_Uf3` | 对象 `{UF20表名: UFT3表名}` | 表替换映射 |
| `functionReplace` | 对象 `{UF20函数名: UFT3函数名}` | 函数替换映射 |
| `noChangeFullNameCode` | 字符串数组 | 不转码功能函数清单,转码时跳过这些函数名的替换 |
+`convert.dir` 元素字段说明(详见 `docs/转码配置格式契约.md`):
+
+| 字段 | 说明 |
+| --- | --- |
+| `name` | 功能名(含 `.service_design` 后缀) |
+| `core` | 归属核心:`ucbp`(默认)/ `uses` / `uarg` / `usms` |
+| `viaUmgr` | 是否柜面接口:`false`(默认)/ `true` |
+| `dialect` | SQL 方言,由 core 派生:`ucbp`/`uses` → `mdb`,`uarg`/`usms` → `db`(程序保存时自动生成,无需手工填写) |
+
**手工编辑示例(以实际文件结构为准):**
```json
{
"convert": {
"dir": [
- "LS_CustQry.service_design",
- "LS_AcctChk.service_design"
+ {
+ "name": "LS_CustQry.service_design",
+ "core": "ucbp",
+ "viaUmgr": false,
+ "dialect": "mdb"
+ },
+ {
+ "name": "LS_AcctChk.service_design",
+ "core": "uarg",
+ "viaUmgr": true,
+ "dialect": "db"
+ }
]
},
"tableCompareUf2_Uf3": {
@@ -288,17 +310,18 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
- 程序内修改(全量转码 / 表替换 / 函数替换 / 不转码功能函数页)会自动写回该文件;
- 手工编辑保存后各页面自动检测并刷新;
-- 全量转码清单函数名可省略 `.service_design` 后缀展示,`dir` 为数组结构,程序保存时保留文件其余内容与格式。
+- 全量转码清单函数名可省略 `.service_design` 后缀展示,`dir` 为对象数组结构,程序保存时保留文件其余内容与格式;
+- 兼容旧版 `dir` 为字符串数组(仅函数名)的格式,旧条目按 core=ucbp、viaUmgr=false、dialect=mdb 加载。
### 6.2 cust_nonls.json(按需转码清单)
- **路径**:`bin\uf2touft3\config\cust_nonls.json`
-- **作用**:保存按需转码功能清单及每项的递归标志,供界面与转码引擎共用。
+- **作用**:保存按需转码功能清单及每项的递归标志与 SQL 方言(db 模式),供界面与转码引擎共用。
| 节点 | 结构 | 说明 |
| --- | --- | --- |
-| `convert.dir` | 对象 `{功能名: 0 或 -1}` | `0` = 否(单文件转码),`-1` = 是(递归转码) |
-| `convert.dir["*"]` | 引擎兜底键 | 非清单功能,界面不展示、保存时自动保留 |
+| `convert.dir[功能名]` | 对象 `{depth, dialect}` | `depth`: `0` = 否(单文件转码)、`-1` = 是(递归转码);`dialect`: `mdb` = 否(默认)、`db` = 是 |
+| `convert.dir["*"]` | 引擎兜底键(数值) | 非清单功能,界面不展示、保存时原样保留 |
| `convert.ext` | 后缀数组 | 引擎识别的功能文件后缀,程序保存时自动维护 |
**手工编辑示例(以实际文件结构为准):**
@@ -308,8 +331,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
"convert": {
"dir": {
"*": -1,
- "AS_CustQry": 0,
- "LF_AcctChk": -1
+ "AS_CustQry": { "depth": 0, "dialect": "mdb" },
+ "LF_AcctChk": { "depth": -1, "dialect": "db" }
},
"ext": [
".service_design",
@@ -325,7 +348,7 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
**编辑注意:**
- 保持 JSON 格式合法(逗号、引号完整);若格式非法,程序将保留内存中的现有清单并在日志中记录警告;
-- 兼容旧版 `dir` 为数组的格式(无递归标志,一律按"否"加载);
+- 兼容旧版格式:`dir` 值为数值(`0`/`-1`,无方言,按"否/mdb"加载)或为数组(无递归标志,一律按"否/mdb"加载);
- 外部编辑保存后约 0.3 秒内界面自动刷新;程序自身写入该文件时不会触发重复刷新,可放心与手工编辑混用。
### 6.3 config.ini 与 change_code.ini
diff --git a/docs/详细设计文档.md b/docs/详细设计文档.md
index 65eaf87..240d722 100644
--- a/docs/详细设计文档.md
+++ b/docs/详细设计文档.md
@@ -32,7 +32,8 @@ Uft30ChangeCode 是一款桌面端 **UF20 → UFT3 代码转码工具**,提供
| --- | --- |
| UF20 / UFT3 | 新旧两代系统版本,转码即将 UF20 风格代码转换为 UFT3 风格 |
| LS 功能 | "内存交易"改造范围的功能类别;**全量转码仅支持 LS_ 开头功能**,按需转码仅针对非 LS 功能 |
-| 递归标志 | `cust_nonls.json` 中 `convert.dir` 的取值:`0`=否(不递归),`-1`=是(递归) |
+| 递归标志 | `cust_nonls.json` 中 `convert.dir` 条目 `depth` 的取值:`0`=否(不递归),`-1`=是(递归) |
+| SQL 方言(dialect) | 转码引擎 SQL 语法转换目标:`mdb` / `db`。全量转码由核心派生(ucbp/uses→mdb,uarg/usms→db);按需转码由"db模式"列选择(否=mdb,是=db) |
| 元数据 | 从源码解析出的功能/函数索引信息,存于 SQLite 缓存库 |
| 调用链路 | 某功能的向上调用方(谁调用了它)与向下被调(它调用了谁)构成的关系树 |
@@ -161,8 +162,8 @@ Uft30ChangeCode/
| --- | --- | --- | --- |
| 主窗口导航 | `MainWindow` | — | ElaWindow 容器、页面注册、**转码任务调度与互斥**、引擎进程管理 |
| 业务转码容器 | `BusinessConvertPage` | — | ElaPivot 承载全量/按需两个工作台 |
-| 全量转码 | `BatchConvertPage` | — | `cust.json` 的 `convert.dir` 清单管理(仅 LS_ 功能)、触发转码 |
-| 按需转码 | `NonLsConvertPage` | — | 非 LS 功能清单管理、递归模式选择、外部修改同步 |
+| 全量转码 | `BatchConvertPage` | — | `cust.json` 的 `convert.dir` 清单管理(仅 LS_ 功能,含核心/是否柜面接口配置)、触发转码 |
+| 按需转码 | `NonLsConvertPage` | — | 非 LS 功能清单管理、递归模式与 db 模式(SQL 方言)选择、外部修改同步 |
| 功能查询 | `FunctionSearchPage`(基类)、`UF20/UFT3FunctionSearchPage` | 模板方法(基类复用) | 元数据检索(多关键词批量查询);UF20 双击/右键一键分流加入转码清单(合并单次提示),UFT3 纯查询 |
| 代码检索 | `CodeSearchPage`、`CallChainAnalyzer` | — | 调用链路查询、索引构建与缓存、导出 |
| 数据映射 | `TableMappingPage`、`FunctionMappingPage`、`NoChangeFullNamePage` | — | `cust.json` 表替换/函数替换映射维护、不转码功能函数清单维护(增删改查) |
@@ -316,8 +317,8 @@ sequenceDiagram
**要点:**
-- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单,对清单内 LS_ 功能整体转码;
-- **按需转码**:`NonLsConvertPage::buildArguments()` 组装引擎参数(含 `-m 2` 模式标识),功能清单与递归标志由 `cust_nonls.json` 提供,引擎按映射关系(`cust.json`)完成表名/函数名替换;
+- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单(V2 对象数组 `{name, core, viaUmgr, dialect}`),对清单内 LS_ 功能整体转码,核心/柜面接口/SQL 方言随清单下发(契约见 `docs/转码配置格式契约.md`);
+- **按需转码**:`NonLsConvertPage::buildArguments()` 组装引擎参数(含 `-m 2` 模式标识),功能清单、递归标志(depth)与 SQL 方言(dialect)由 `cust_nonls.json` 提供,引擎按映射关系(`cust.json`)完成表名/函数名替换;
- **成败判定**:引擎在个别文件转码失败时退出码仍可能为 0,因此 `MainWindow` 在输出解析链最前端检测 `FAIL` 行(`[N/M] FAIL 函数名`)与汇总行(`批量转码完成: X/Y 通过, Z 失败`)累计 `m_failCount`;收尾按 `正常退出 && 退出码==0 && m_failCount==0` 判定成功,失败时页面调用 `onConvertFailed`——进度条**保持当前进度**(不置 100%)、状态文字"转换失败(N 个文件转换失败)"(详见 §8.7);
- **转码后联动**:任务成功后自动静默刷新代码检索索引,保证调用链路反映最新代码。
@@ -334,11 +335,11 @@ sequenceDiagram
### 5.2 全量转码模块(BatchConvertPage)
-**职责**:维护 LS_ 功能转码清单并对功能执行整体转码,表格展示"UF20函数名 / 是否存在 / 操作"。
+**职责**:维护 LS_ 功能转码清单(含核心、是否柜面接口配置)并对功能执行整体转码,表格展示"UF20函数名 / 是否存在 / 核心 / 是否柜面接口"(无操作列,双击函数名所在行触发编辑)。
```mermaid
flowchart TD
- A([进入全量转码页]) --> B["加载清单
cust.json → convert.dir 数组"]
+ A([进入全量转码页]) --> B["加载清单
cust.json → convert.dir 对象数组
(V2: {name, core, viaUmgr, dialect})"]
B --> C[updateTable 渲染 ElaTableView]
C --> D{逐行判断}
D --> E["checkUF20CnameExists(函数名)
查询 DataCache"]
@@ -353,21 +354,22 @@ flowchart TD
**要点:**
- 清单**仅允许 LS_ 开头**的功能(`validateLsFunctionNames` 校验),非 LS 输入被拒绝并提示切换到按需转码;
-- 清单持久化于 `cust.json` 的 `convert.dir` 数组(元素为 `"函数名.service_design"`);保存时用**正则定位数组做局部替换**,保留文件其余内容与格式;
+- 清单持久化于 `cust.json` 的 `convert.dir` 对象数组(V2 元素 `{name, core, viaUmgr, dialect}`,dialect 由 core 派生:ucbp/uses→mdb、uarg/usms→db);保存时用**正则定位数组做局部替换**,保留文件其余内容与格式;加载兼容 V1 字符串数组(按 core=ucbp、viaUmgr=false、dialect=mdb 兜底);
- `QFileSystemWatcher + QTimer`(300ms 防抖)监听 `cust.json` 外部修改,与手工编辑双向同步;
- "是否存在"列依赖元数据缓存;**首次运行若未执行元数据更新,可能整列显示"否"**(详见 §8.2)。
### 5.3 按需转码模块(NonLsConvertPage)
-**职责**:管理"非 LS 功能"的按需转码清单,支持**单文件转码**与**递归转码**两种模式(下拉选择),转码范围不包含核心文件、module 模块文件。清单持久化于 `bin/uf2touft3/config/cust_nonls.json`,并监听外部修改。
+**职责**:管理"非 LS 功能"的按需转码清单,支持**单文件转码**与**递归转码**两种模式(下拉选择),并为每项功能选择 **db 模式**(SQL 方言:否=mdb,是=db),转码范围不包含核心文件、module 模块文件。清单持久化于 `bin/uf2touft3/config/cust_nonls.json`,并监听外部修改。
#### 5.3.1 数据结构
| 成员 | 类型 | 说明 |
| --- | --- | --- |
| `m_funcList` | `QStringList` | 功能名有序清单(与表格行序一致) |
-| `m_recursionMap` | `QMap` | 功能名 → 递归标志(`0`=否,`-1`=是),与 json 的 `convert.dir` 同值 |
-| `m_tableModel` | `QStandardItemModel` | 表格模型,列:`UF20函数名 / 是否存在 / 是否递归 / 操作` |
+| `m_recursionMap` | `QMap` | 功能名 → 递归标志(`0`=否,`-1`=是),对应 json 的 `convert.dir[名].depth` |
+| `m_dialectMap` | `QMap` | 功能名 → SQL 方言(`mdb`=否/默认,`db`=是),对应 json 的 `convert.dir[名].dialect` |
+| `m_tableModel` | `QStandardItemModel` | 表格模型,列:`UF20函数名 / 是否存在 / 是否递归 / db模式`(双击函数名所在行触发编辑) |
| `m_configWatcher` | `QFileSystemWatcher` | 监听 `cust_nonls.json` 文件与其所在目录 |
| `m_reloadTimer` | `QTimer` | 防抖定时器,外部修改后延迟重载 |
| `m_lastFileMtime` | `QDateTime` | 上次已处理的文件修改时间,用于过滤自身写入触发的变更 |
@@ -400,7 +402,7 @@ flowchart TD
B --> C{遍历 m_funcList}
C --> D[构建行: 函数名 / 是否存在
存在性查询 DataCache]
D --> E["是否递归列: setIndexWidget
放置 ElaComboBox(否/是)"]
- E --> F["操作列: setIndexWidget
放置编辑按钮"]
+ E --> F["双击函数名所在行触发编辑
(基类统一接线, 以函数名为稳定键)"]
F --> G{还有下一行?}
G -- 是 --> D
G -- 否 --> H["viewport()->update()
resizeRowsToContents()
scrollToTop() / clearSelection()"]
@@ -448,7 +450,7 @@ flowchart TD
- `fileChanged` 在部分编辑器保存时会**移除监听**,回调中检测到即对路径重新 `addPath`,保证持续监听;
- 目录监听用于捕获"文件被删除重建"的场景;
- mtime 比较过滤掉程序自身写盘引发的回环刷新;
-- **兼容旧版格式**:旧版 `convert.dir` 为数组(无递归标志),加载时自动兼容、一律按"否"处理;新版为对象 `{功能名: 0|-1}`,引擎兜底键 `"*"` 由程序保存时自动保留。
+- **兼容旧版格式**:旧版 `convert.dir` 为数组(无递归标志与方言)或值为数值(`0`/`-1`,无方言)均可自动兼容,均按"否/mdb"兜底加载;新版为对象 `{功能名: {depth, dialect}}`,引擎兜底键 `"*"`(数值)由程序保存时原样保留。
#### 5.3.6 按需转码执行模式
@@ -724,6 +726,7 @@ flowchart TD
| `tableCompareUf2_Uf3` | UF20→UFT3 表名映射 | 全量/按需转码的表替换 |
| `functionReplace` | 函数替换规则 | 转码的函数替换 |
| `noChangeFullNameCode` | 字符串数组(功能函数全名清单) | 转码引擎对清单内功能函数名不做全名替换 |
+| `convert.dir` | 对象数组 `{name, core, viaUmgr, dialect}`(全量转码清单,V2 格式,见 `docs/转码配置格式契约.md`) | 全量转码页维护、引擎读取 |
| 其余键 | 引擎自身配置 | 原样透传 |
- 数据映射三页各自只读写所属顶层键(键名统一引用 `Constants::CustKeys`,见 §6.4),其余内容原样保留;
@@ -734,13 +737,17 @@ flowchart TD
```json
{
"convert": {
- "dir": { "功能名": 0, "另一功能": -1, "*": 0 },
+ "dir": {
+ "功能名": { "depth": 0, "dialect": "mdb" },
+ "另一功能": { "depth": -1, "dialect": "db" },
+ "*": 0
+ },
"ext": ["c", "h"]
}
}
```
-- `dir.<功能名>`:`0` 纳入转码、`-1` 不纳入;`"*"` 为兜底键;
+- `dir.<功能名>`:对象 `{depth, dialect}`——`depth` 为 `0` 纳入转码(单文件)、`-1` 递归转码;`dialect` 为 SQL 方言(`mdb` 默认 / `db`),对应界面"db模式"列(否=mdb、是=db);`"*"` 为兜底键(数值,程序保存时原样透传);
- `ext`:纳入转码的扩展名白名单;
- 兼容旧版数组格式(仅扩展名列表),读取时自动转换。
diff --git a/help/Writerside/topics/业务转码.md b/help/Writerside/topics/业务转码.md
index eee0514..8cf7b9b 100644
--- a/help/Writerside/topics/业务转码.md
+++ b/help/Writerside/topics/业务转码.md
@@ -7,15 +7,24 @@
点击"添加"按钮,输入函数名添加到转码列表
-点击表格操作按钮可编辑函数名,如下:
+双击表格中函数名所在行,可在弹窗中修改函数名(页面顶部有操作提示),如下:
选中行后点击"删除选中"移除函数、点击"清空列表"清空所有函数,如下:
+## 核心 与 是否柜面接口
+
+每行提供"核心"与"是否柜面接口"两个下拉框:
+
+- **核心**:选择该功能转码后归属的核心,可选 ucbp(默认)/ uses / uarg / usms;SQL 方言由核心自动派生——ucbp/uses 对应 mdb,uarg/usms 对应 db;
+- **是否柜面接口**:选择 否(默认)/ 是,标识该功能是否为柜面接口。
+
+切换后立即写入配置并生效。
+
## 配置文件同步
-- 自动读取 bin/uf2touft3/config/cust.json 配置
+- 自动读取 bin/uf2touft3/config/cust.json 配置(convert.dir 为对象数组 {name, core, viaUmgr, dialect},兼容旧版字符串数组格式);
- 配置文件外部修改时自动刷新列表
## 转换操作
diff --git a/help/Writerside/topics/按需转码.md b/help/Writerside/topics/按需转码.md
index aca1ac9..8f479d0 100644
--- a/help/Writerside/topics/按需转码.md
+++ b/help/Writerside/topics/按需转码.md
@@ -13,25 +13,29 @@
对于以 LS_ 开头的函数,界面会以黄色文字提示,并弹窗建议放入"全量转码"工作台转码(不拦截添加)。
-选中行后点击"删除选中"移除函数、点击"清空列表"清空所有函数。
+选中行后点击"删除选中"移除函数、点击"清空列表"清空所有函数。双击函数名所在行,可在弹窗中修改函数名(页面顶部有操作提示)。
-## 递归模式
+## 递归模式 与 db模式
-每行行尾提供"是否递归"下拉框:
+每行提供"是否递归"与"db模式"下拉框:
-- **否**:非递归模式(默认);
-- **是**:递归模式,自动转码关联函数。
+- **是否递归**:
+ - **否**:非递归模式(默认);
+ - **是**:递归模式,自动转码关联函数。
+- **db模式**:选择 SQL 语法转换方言:
+ - **否**:mdb 方言(默认);
+ - **是**:db 方言。
切换后立即生效并保存。
## 配置文件同步
-- 自动读取 `bin/uf2touft3/config/cust_nonls.json` 配置,"是否递归"对应文件中 convert 节点的 dir 值(0=否、-1=是);
-- 兼容旧版清单格式(纯函数名数组,默认按"否"加载);
+- 自动读取 `bin/uf2touft3/config/cust_nonls.json` 配置:"是否递归"对应 convert.dir 条目的 `depth`(0=否、-1=是),"db模式"对应 `dialect`(mdb=否、db=是);
+- 兼容旧版清单格式(纯函数名数组或数值条目,均按"否/mdb"加载);
- 配置文件外部修改时自动刷新列表。
## 转换操作
点击"开始转换"启动转换任务,转换过程中可查看进度条和状态信息;点击"停止转换"可中止当前任务。
-工具以按需转码模式调用引擎,函数清单与递归开关由引擎直接读取 `cust_nonls.json`,无需手动同步。
+工具以按需转码模式调用引擎,函数清单、递归开关与 SQL 方言由引擎直接读取 `cust_nonls.json`,无需手动同步。
diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp
index 4d42876..6ca5f81 100644
--- a/src/pages/batchconvert/batchconvertpage.cpp
+++ b/src/pages/batchconvert/batchconvertpage.cpp
@@ -10,33 +10,59 @@
#include
#include
#include
-#include
#include
+#include
+#include
+#include
#include
+#include "ElaComboBox.h"
#include "ElaTheme.h"
namespace {
+// V2 默认值(契约 docs/转码配置格式契约.md): core 默认 ucbp, 柜面接口默认否;
+// dialect 不落 UI, 保存时由 core 派生(ucbp/uses→mdb, uarg/usms→db)
+
bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames)
{
- QStringList nonLsNames;
+ // 只允许 LS_ 开头的功能进入全量转码清单
+ QStringList invalidNames;
for (const QString &name : functionNames) {
- const QString trimmedName = name.trimmed();
- if (!trimmedName.isEmpty()
- && !trimmedName.startsWith(Constants::FuncName::LsPrefix)
- && !nonLsNames.contains(trimmedName)) {
- nonLsNames.append(trimmedName);
+ if (!name.startsWith("LS_")) {
+ invalidNames.append(name);
}
}
-
- if (nonLsNames.isEmpty()) {
- return true;
+ if (!invalidNames.isEmpty()) {
+ QMessageBox::warning(
+ parent,
+ QStringLiteral("全量转码"),
+ QStringLiteral("以下功能不是 LS_ 开头,无法加入全量转码清单:\n")
+ + invalidNames.join("\n"),
+ QMessageBox::Ok);
+ LogManager::instance()->logWarning(
+ QString("全量转码 - 存在非 LS_ 前缀功能名被拦截: %1")
+ .arg(invalidNames.join(", ")));
+ return false;
}
+ return true;
+}
- QMessageBox::warning(parent, "提示",
- QString("\"全量转码\"仅支持 LS_ 开头的功能,以下功能无法加入:\n%1\n\n非LS功能请切到\"按需转码\"工作台添加。")
- .arg(nonLsNames.join('\n')));
- return false;
+// 清单表格列定义
+enum TableCol {
+ ColName = 0, // UF20函数名
+ ColExists = 1, // 是否存在
+ ColCore = 2, // 核心(ucbp/uses/uarg/usms)
+ ColViaUmgr = 3 // 是否柜面接口
+};
+
+// 核心取值表(顺序即下拉顺序, 首项为默认值)
+const QStringList kCoreList = {Constants::DirEntry::CoreUcbp, Constants::DirEntry::CoreUses,
+ Constants::DirEntry::CoreUarg, Constants::DirEntry::CoreUsms};
+
+int coreIndexOf(const QString &core)
+{
+ const int index = kCoreList.indexOf(core);
+ return index >= 0 ? index : 0; // 未知核心回退默认首项(ucbp)
}
} // namespace
@@ -48,139 +74,300 @@ BatchConvertPage::BatchConvertPage(QWidget *parent)
initialize();
}
+// ---------------------------------------------------------------------------
+// 界面骨架
+// ---------------------------------------------------------------------------
+
void BatchConvertPage::applyPageLayout(QVBoxLayout *layout)
{
- layout->setContentsMargins(30, 30, 30, 30);
- layout->setSpacing(20);
+ // 页首说明条: 无"操作"列, 双击函数名所在行即可修改函数名
+ m_tipLabel = new QLabel(
+ "说明:双击函数名所在行,可在弹窗中修改函数名。");
+ m_tipLabel->setTextFormat(Qt::RichText);
+ m_tipLabel->setWordWrap(true);
+ layout->addWidget(m_tipLabel);
+}
+
+void BatchConvertPage::applyTipStyle(const QString &textColor)
+{
+ if (m_tipLabel) {
+ m_tipLabel->setStyleSheet(QString("color: %1; font-size: 12px;").arg(textColor));
+ }
}
int BatchConvertPage::tableColumnCount() const
{
- return 3;
+ return 4;
}
QStringList BatchConvertPage::tableHeaderLabels() const
{
- return {"UF20函数名", "是否存在", "操作"};
+ return {QStringLiteral("UF20函数名"), QStringLiteral("是否存在"),
+ QStringLiteral("核心"), QStringLiteral("是否柜面接口")};
}
void BatchConvertPage::applyColumnLayout()
{
+ // 4 列按 8:2:2:2 比例分摊、恰好铺满表格:
+ // "核心"/"是否柜面接口"各占 2 份, 保证四/六字表头与行内下拉完整显示不截断;
+ // 取整余数并入函数名列, 确保总宽精确铺满、无横向滚动条
const int totalWidth = m_funcTable->viewport()->width();
- m_funcTable->setColumnWidth(0, (totalWidth - 80) * 8 / 10);
- m_funcTable->setColumnWidth(1, (totalWidth - 80) * 2 / 10);
- m_funcTable->setColumnWidth(2, 80);
+ const int unit = totalWidth / 14;
+ m_funcTable->setColumnWidth(ColName, totalWidth - unit * 6);
+ m_funcTable->setColumnWidth(ColExists, unit * 2);
+ m_funcTable->setColumnWidth(ColCore, unit * 2);
+ m_funcTable->setColumnWidth(ColViaUmgr, unit * 2);
}
+// ---------------------------------------------------------------------------
+// 名称校验
+// ---------------------------------------------------------------------------
+
bool BatchConvertPage::validateFunctionNames(const QStringList &names)
{
return validateLsFunctionNames(this, names);
}
+// ---------------------------------------------------------------------------
+// 表格行: 核心/柜面接口两列
+// ---------------------------------------------------------------------------
+
+void BatchConvertPage::attachRowItems(QList &rowItems, const QString &funcName)
+{
+ Q_UNUSED(funcName)
+ // "核心"/"是否柜面接口"以行内下拉呈现, 占位列仅承载 setIndexWidget 控件
+ for (int i = 0; i < 2; ++i) {
+ QStandardItem *item = new QStandardItem("");
+ item->setFlags(item->flags() & ~Qt::ItemIsEditable);
+ item->setTextAlignment(Qt::AlignCenter);
+ rowItems.append(item);
+ }
+}
+
+void BatchConvertPage::attachRowWidgets(int row, const QString &funcName)
+{
+ // "核心"下拉: ucbp/uses/uarg/usms, 切换即写回并持久化(dialect 由 core 派生, 保存时写入)
+ ElaComboBox *coreCombo = new ElaComboBox;
+ for (const QString &core : kCoreList) {
+ coreCombo->addItem(core);
+ }
+ const QString curCore = m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp);
+ coreCombo->setCurrentIndex(coreIndexOf(curCore));
+ connect(coreCombo, QOverload::of(&QComboBox::currentIndexChanged), this,
+ [this, funcName, coreCombo](int index) {
+ const QString core = kCoreList.value(index, Constants::DirEntry::CoreUcbp);
+ const QString prev = m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp);
+ if (prev == core) {
+ return;
+ }
+ m_coreMap.insert(funcName, core);
+ // 值变化才落盘, 不重建表格; 失败回滚内存与下拉选中项
+ // (否则界面显示新值而磁盘仍是旧值, 重启后配置与界面矛盾)
+ if (!saveConfig(m_funcList)) {
+ m_coreMap.insert(funcName, prev);
+ coreCombo->blockSignals(true);
+ coreCombo->setCurrentIndex(coreIndexOf(prev));
+ coreCombo->blockSignals(false);
+ }
+ });
+ m_funcTable->setIndexWidget(m_tableModel->index(row, ColCore), coreCombo);
+
+ // "是否柜面接口"下拉: 否/是(与 viaUmgr 布尔一致), 交互与"核心"列一致
+ ElaComboBox *umgrCombo = new ElaComboBox;
+ umgrCombo->addItem(QStringLiteral("否"));
+ umgrCombo->addItem(QStringLiteral("是"));
+ umgrCombo->setCurrentIndex(m_viaUmgrMap.value(funcName, false) ? 1 : 0);
+ connect(umgrCombo, QOverload::of(&QComboBox::currentIndexChanged), this,
+ [this, funcName, umgrCombo](int index) {
+ const bool viaUmgr = (index == 1);
+ const bool prev = m_viaUmgrMap.value(funcName, false);
+ if (prev == viaUmgr) {
+ return;
+ }
+ m_viaUmgrMap.insert(funcName, viaUmgr);
+ if (!saveConfig(m_funcList)) {
+ m_viaUmgrMap.insert(funcName, prev);
+ umgrCombo->blockSignals(true);
+ umgrCombo->setCurrentIndex(prev ? 1 : 0);
+ umgrCombo->blockSignals(false);
+ }
+ });
+ m_funcTable->setIndexWidget(m_tableModel->index(row, ColViaUmgr), umgrCombo);
+}
+
+// ---------------------------------------------------------------------------
+// 附加属性映射维护
+// ---------------------------------------------------------------------------
+
+void BatchConvertPage::onFunctionAdded(const QString &name)
+{
+ m_coreMap.insert(name, Constants::DirEntry::CoreUcbp); // 新增功能默认 ucbp
+ m_viaUmgrMap.insert(name, false); // 新增功能默认非柜面接口
+}
+
+void BatchConvertPage::onFunctionsRemoved(const QStringList &names)
+{
+ for (const QString &name : names) {
+ m_coreMap.remove(name);
+ m_viaUmgrMap.remove(name);
+ }
+}
+
+void BatchConvertPage::onFunctionsRenamed(const QString &oldName, const QString &newName)
+{
+ const QString core = m_coreMap.value(oldName, Constants::DirEntry::CoreUcbp);
+ const bool viaUmgr = m_viaUmgrMap.value(oldName, false);
+ m_coreMap.remove(oldName);
+ m_viaUmgrMap.remove(oldName);
+ m_coreMap.insert(newName, core);
+ m_viaUmgrMap.insert(newName, viaUmgr);
+}
+
+void BatchConvertPage::onFunctionListCleared()
+{
+ m_coreMap.clear();
+ m_viaUmgrMap.clear();
+}
+
+// ---------------------------------------------------------------------------
+// 持久化: cust.json
+// ---------------------------------------------------------------------------
+
bool BatchConvertPage::saveConfig(const QStringList &funcList)
{
- QString jsonPath = Constants::custJson();
-
- // 读取原始文件内容(保留全部格式不重序列化)
- QFile file(jsonPath);
+ // 1. 读原文件并解析
+ QFile file(configFilePath());
+ QByteArray original;
if (!file.open(QIODevice::ReadOnly)) {
- LogManager::instance()->logWarning(
- QString("全量转码 - 保存配置失败,无法打开: %1").arg(jsonPath));
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,无法打开原文件: %1").arg(configFilePath()));
return false;
}
-
- QString content = QString::fromUtf8(file.readAll());
+ original = file.readAll();
file.close();
- // 用正则定位 convert 顶层键(兼容任意空格)
- static const QRegularExpression convertRe(
- QString(R"("%1"\s*:)").arg(Constants::CustKeys::ConvertDir));
- auto convertMatch = convertRe.match(content);
- if (!convertMatch.hasMatch()) {
- LogManager::instance()->logWarning("全量转码 - 保存配置失败,未找到 convert 节点");
+ QJsonParseError parseError;
+ QJsonDocument doc = QJsonDocument::fromJson(original, &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,原文件 JSON 解析失败: %1")
+ .arg(parseError.errorString()));
return false;
}
- // 在 convert 之后定位 "dir" 数组起始 [
- static QRegularExpression dirRe(R"("dir"\s*:\s*\[)");
- auto dirMatch = dirRe.match(content, convertMatch.capturedEnd());
+ // 2. 定位 convert.dir 的 [ ] 区间
+ QString content = QString::fromUtf8(original);
+ QRegularExpression dirRe(
+ QStringLiteral("\"%1\"\\s*:\\s*\\[").arg(Constants::CustKeys::ConvertDirSub));
+ QRegularExpressionMatch dirMatch = dirRe.match(content);
if (!dirMatch.hasMatch()) {
- LogManager::instance()->logWarning("全量转码 - 保存配置失败,未找到 dir 数组");
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,未找到 convert.%1 节点").arg(
+ Constants::CustKeys::ConvertDirSub));
return false;
}
- int bracketStart = dirMatch.capturedEnd() - 1; // 指向 '['
-
- // 括号计数找到匹配的 ']'(必须跳过字符串字面量, 否则字符串内的 [ ] 会截断匹配改坏 JSON)
- int bracketCount = 1;
- int pos = bracketStart + 1;
+ const int bracketStart = dirMatch.capturedEnd() - 1; // '[' 位置
+ int bracketEnd = -1;
+ int depth = 0;
bool inString = false;
bool escaped = false;
- while (pos < content.length() && bracketCount > 0) {
- QChar ch = content[pos];
+ for (int i = bracketStart; i < content.size(); ++i) {
+ const QChar c = content[i];
if (inString) {
- if (escaped) { escaped = false; }
- else if (ch == '\\') { escaped = true; }
- else if (ch == '"') { inString = false; }
- } else {
- if (ch == '"') { inString = true; }
- else if (ch == '[') bracketCount++;
- else if (ch == ']') bracketCount--;
+ if (escaped) {
+ escaped = false;
+ } else if (c == '\\') {
+ escaped = true;
+ } else if (c == '"') {
+ inString = false;
+ }
+ continue;
+ }
+ if (c == '"') {
+ inString = true;
+ } else if (c == '[') {
+ ++depth;
+ } else if (c == ']') {
+ --depth;
+ if (depth == 0) {
+ bracketEnd = i;
+ break;
+ }
}
- pos++;
}
- if (bracketCount != 0) {
- LogManager::instance()->logWarning("全量转码 - 保存配置失败,dir 数组括号不匹配");
+ if (bracketEnd < 0) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,convert.%1 数组括号不匹配").arg(
+ Constants::CustKeys::ConvertDirSub));
return false;
}
- int bracketEnd = pos; // 指向 ']' 之后一位
- // 构建新数组(保持原始缩进风格:项 12 空格,闭合 ] 8 空格)
+ // 3. 构建新数组(V2 对象格式, dialect 由 core 派生)
+ // 紧凑格式: 每个功能一行对象(与需求示例一致), 避免逐字段展开占用大量缩进
QString newArray = "[";
for (int i = 0; i < funcList.size(); ++i) {
- if (i > 0) newArray += ",";
- newArray += "\n \"" + CustJsonIo::escapeJsonString(funcList[i]) + Constants::DesignExts::Service + "\"";
+ const QString &name = funcList[i];
+ const QString core = m_coreMap.value(name, Constants::DirEntry::CoreUcbp);
+ const bool viaUmgr = m_viaUmgrMap.value(name, false);
+ const QString dialect = Constants::DirEntry::dialectForCore(core);
+ if (i > 0) {
+ newArray += ",";
+ }
+ newArray += "\n { \"" + Constants::DirEntry::Name + "\": \""
+ + CustJsonIo::escapeJsonString(name + Constants::DesignExts::Service)
+ + "\", \"" + Constants::DirEntry::Core + "\": \"" + core
+ + "\", \"" + Constants::DirEntry::ViaUmgr + "\": ";
+ newArray += viaUmgr ? "true" : "false";
+ newArray += ", \"" + Constants::DirEntry::Dialect + "\": \"" + dialect + "\" }";
}
if (!funcList.isEmpty()) {
newArray += "\n ";
}
newArray += "]";
- // 只替换 [bracketStart, bracketEnd) 区间,其余内容原样保留
- content.replace(bracketStart, bracketEnd - bracketStart, newArray);
+ // 换行风格跟随原文件: 原文件为 CRLF 时, 新数组文本的 \n 同步为 \r\n。
+ // 写出必须用二进制模式(不开 QIODevice::Text)——Text 模式会把 \n 再转成 \r\n,
+ // 而保留区域(fromUtf8 原样保留)已含 \r\n, 每保存一次就多累积一个 \r,
+ // 最终文件里出现成串回车符(表现为编辑器中大量空白)。
+ if (original.contains("\r\n")) {
+ newArray.replace("\n", "\r\n");
+ }
- // 写回前校验替换结果仍是合法 JSON, 防止改坏的配置被原地写回无法恢复
- QJsonParseError parseErr;
- QJsonDocument::fromJson(content.toUtf8(), &parseErr);
- if (parseErr.error != QJsonParseError::NoError) {
- LogManager::instance()->logWarning(
- QString("全量转码 - 保存配置中止, 替换后 JSON 非法: %1").arg(parseErr.errorString()));
+ // 4. 拼装并做整体 JSON 合法性校验
+ QString newContent = content.left(bracketStart) + newArray + content.mid(bracketEnd + 1);
+ QJsonDocument newDoc = QJsonDocument::fromJson(newContent.toUtf8(), &parseError);
+ if (parseError.error != QJsonParseError::NoError) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,新文件 JSON 校验未通过: %1")
+ .arg(parseError.errorString()));
+ return false;
+ }
+ QJsonObject newRoot = newDoc.object();
+ const QJsonArray verifyDir =
+ newRoot[Constants::CustKeys::ConvertDir].toObject()[Constants::CustKeys::ConvertDirSub].toArray();
+ if (verifyDir.size() != funcList.size()) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,新文件数组元素数(%1)与清单(%2)不一致")
+ .arg(verifyDir.size())
+ .arg(funcList.size()));
return false;
}
- // QSaveFile 原子写: 先写临时文件, commit 成功才替换目标文件,
- // 避免写入中途崩溃/断电损坏配置。
- // 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的
- // fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载;
- // 失败路径(打开/提交失败)由组件 2s 兜底自动关窗
- m_configMonitor->beginSaveWindow();
- QSaveFile saveFile(jsonPath);
- if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
- LogManager::instance()->logWarning(
- QString("全量转码 - 保存配置失败,无法写入: %1").arg(jsonPath));
+ // 5. 原子写回(二进制模式: 内容字节原样落盘, 不做任何换行转换)
+ QSaveFile saveFile(configFilePath());
+ if (!saveFile.open(QIODevice::WriteOnly)) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,无法写入: %1").arg(configFilePath()));
return false;
}
-
- saveFile.write(content.toUtf8());
+ saveFile.write(newContent.toUtf8());
if (!saveFile.commit()) {
- LogManager::instance()->logWarning(
- QString("全量转码 - 保存配置失败,写入提交异常: %1").arg(jsonPath));
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,写入提交失败: %1").arg(configFilePath()));
return false;
}
- // 程序自身写入后更新监控 mtime 基准, 避免触发无谓的重载
- m_configMonitor->endSave();
-
return true;
}
@@ -207,15 +394,36 @@ void BatchConvertPage::loadConfig()
}
QJsonObject root = doc.object();
- QJsonArray dir = root[Constants::CustKeys::ConvertDir].toObject()[Constants::CustKeys::ConvertDirSub].toArray();
+ const QJsonArray dir = root[Constants::CustKeys::ConvertDir].toObject()[Constants::CustKeys::ConvertDirSub].toArray();
QStringList items;
+ m_coreMap.clear();
+ m_viaUmgrMap.clear();
for (const QJsonValue &val : dir) {
- QString item = val.toString();
+ // V1 字符串元素 / V2 对象元素双格式兼容: 缺失字段按契约默认值兜底
+ QString item;
+ QString core = Constants::DirEntry::CoreUcbp;
+ bool viaUmgr = false;
+ if (val.isObject()) {
+ const QJsonObject entry = val.toObject();
+ item = entry.value(Constants::DirEntry::Name).toString();
+ const QString c = entry.value(Constants::DirEntry::Core).toString();
+ if (kCoreList.contains(c)) {
+ core = c;
+ }
+ viaUmgr = entry.value(Constants::DirEntry::ViaUmgr).toBool(false);
+ } else {
+ item = val.toString();
+ }
if (item.endsWith(Constants::DesignExts::Service)) {
item.chop(Constants::DesignExts::Service.size());
}
+ if (item.isEmpty()) {
+ continue;
+ }
items.append(item);
+ m_coreMap.insert(item, core);
+ m_viaUmgrMap.insert(item, viaUmgr);
}
m_funcList = items;
@@ -229,20 +437,20 @@ QString BatchConvertPage::configFilePath() const
QString BatchConvertPage::workbenchTitle() const
{
- return "全量转码";
+ return QStringLiteral("全量转码");
}
QString BatchConvertPage::configDisplayName() const
{
- return "cust.json";
+ return QStringLiteral("cust.json");
}
QString BatchConvertPage::addDialogTitle() const
{
- return "添加UF20函数";
+ return QStringLiteral("添加功能");
}
QString BatchConvertPage::addDialogPlaceholder() const
{
- return "每行输入一个函数名";
+ return QStringLiteral("输入 LS_ 开头的功能名,如 LS_证券参数_股票名单增加");
}
diff --git a/src/pages/batchconvert/batchconvertpage.h b/src/pages/batchconvert/batchconvertpage.h
index fcf6952..2831dcc 100644
--- a/src/pages/batchconvert/batchconvertpage.h
+++ b/src/pages/batchconvert/batchconvertpage.h
@@ -2,13 +2,19 @@
#define BATCHCONVERTPAGE_H
#include "src/pages/common/convertworkbenchbase.h"
+#include
class QVBoxLayout;
+class QLabel;
+class QStandardItem;
// 全量转码工作台(业务转码页 LS pivot,仅支持 LS_ 开头的功能)
-// 函数清单(短名)持久化到 config/cust.json,convert.dir 为
-// ["功能名.service_design", ...] 数组,保存时对原文件做文本级区间替换
-// (保留 cust.json 其余全部格式不重序列化)
+// 函数清单(短名)持久化到 config/cust.json,convert.dir 为对象数组
+// [{name, core, viaUmgr, dialect}, ...](V2 格式,兼容读旧字符串数组 V1),
+// 保存时对原文件做文本级区间替换(保留 cust.json 其余全部格式不重序列化)。
+// core 决定目标核心(ucbp/uses/uarg/usms);dialect 由 core 派生、不提供独立 UI
+// (ucbp/uses→mdb,uarg/usms→db);viaUmgr 为是否柜面接口。
+// 格式契约见 docs/转码配置格式契约.md
class BatchConvertPage : public ConvertWorkbenchBase
{
Q_OBJECT
@@ -19,6 +25,7 @@ public:
protected:
// 界面骨架钩子
void applyPageLayout(QVBoxLayout *layout) override;
+ void applyTipStyle(const QString &textColor) override;
int tableColumnCount() const override;
QStringList tableHeaderLabels() const override;
void applyColumnLayout() override;
@@ -26,6 +33,16 @@ protected:
// 名称校验钩子: 非 LS_ 前缀弹窗拦截
bool validateFunctionNames(const QStringList &names) override;
+ // 表格行钩子: 核心/柜面接口两列行内下拉
+ void attachRowItems(QList &rowItems, const QString &funcName) override;
+ void attachRowWidgets(int row, const QString &funcName) override;
+
+ // 附加属性映射维护钩子(新增默认值/删除清理/改名随迁)
+ void onFunctionAdded(const QString &name) override;
+ void onFunctionsRemoved(const QStringList &names) override;
+ void onFunctionsRenamed(const QString &oldName, const QString &newName) override;
+ void onFunctionListCleared() override;
+
// 持久化钩子
bool saveConfig(const QStringList &funcList) override;
void loadConfig() override;
@@ -34,6 +51,12 @@ protected:
QString configDisplayName() const override;
QString addDialogTitle() const override;
QString addDialogPlaceholder() const override;
+
+private:
+ QLabel *m_tipLabel = nullptr; // 页首灰字说明条(需主题联动)
+ // 每个清单功能的核心/柜面接口属性(与 cust.json convert.dir 条目字段一致)
+ QHash m_coreMap; // 功能名 → "ucbp"/"uses"/"uarg"/"usms"
+ QHash m_viaUmgrMap; // 功能名 → 是否柜面接口
};
-#endif
+#endif // BATCHCONVERTPAGE_H
diff --git a/src/pages/codesearch/codesearchpage.cpp b/src/pages/codesearch/codesearchpage.cpp
index 4fcb450..7f30d71 100644
--- a/src/pages/codesearch/codesearchpage.cpp
+++ b/src/pages/codesearch/codesearchpage.cpp
@@ -62,7 +62,11 @@ namespace {
}
for (const QJsonValue &v : dirArray) {
- QString name = v.toString();
+ // V1 字符串元素 / V2 对象元素(取 name 字段)双格式兼容
+ // (全量转码 convert.dir 格式升级, 见 docs/转码配置格式契约.md)
+ QString name = v.isObject()
+ ? v.toObject().value(Constants::DirEntry::Name).toString()
+ : v.toString();
for (const QString &ext : extList) {
if (name.endsWith(ext)) {
name.chop(ext.length());
diff --git a/src/pages/common/convertworkbenchbase.cpp b/src/pages/common/convertworkbenchbase.cpp
index 5f9dfa1..f63a1f1 100644
--- a/src/pages/common/convertworkbenchbase.cpp
+++ b/src/pages/common/convertworkbenchbase.cpp
@@ -22,9 +22,8 @@
namespace {
-// "是否存在"列(两工作台一致)与操作列固定宽度
+// "是否存在"列(两工作台一致)
constexpr int ExistsColumn = 1;
-constexpr int ActionColumnWidth = 80;
// 开始/停止按钮样式: 状态切换仅换底色, 其余属性一致; 常量化防多处字面量漂移
constexpr const char *kStartBtnStyle =
"background-color: #1abc9c; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;";
@@ -129,6 +128,21 @@ void ConvertWorkbenchBase::initUI()
}
m_funcTable->horizontalHeader()->setStretchLastSection(false);
m_funcTable->setItemDelegateForColumn(ExistsColumn, new StatusDelegate(this));
+
+ // 双击函数名所在行触发编辑(替代原"操作"列编辑按钮)。
+ // 两段式投递: 直连段在信号发出瞬间(行号仍有效)按行取出函数名(稳定键),
+ // 再排队执行编辑——onEditRow 会重建表格并弹模态对话框, 不在视图双击事件
+ // 处理栈内同步执行; 接收者绑定 m_funcTable, 表格析构时过期排队请求随之失效
+ connect(m_funcTable, &QAbstractItemView::doubleClicked, m_funcTable,
+ [this](const QModelIndex &index) {
+ if (index.row() < 0 || index.row() >= m_funcList.size()) {
+ return;
+ }
+ const QString funcName = m_funcList.at(index.row());
+ QMetaObject::invokeMethod(m_funcTable, [this, funcName]() {
+ onEditRowByName(funcName);
+ }, Qt::QueuedConnection);
+ });
funcLayout->addWidget(m_funcTable);
QHBoxLayout *tableBtnLayout = new QHBoxLayout;
@@ -206,31 +220,13 @@ void ConvertWorkbenchBase::refreshTable()
styleStatusItem(statusItem, batchExistsReady ? allCnames.contains(funcName)
: checkFunctionExists(funcName));
- QStandardItem *actionItem = new QStandardItem("");
- actionItem->setFlags(actionItem->flags() & ~Qt::ItemIsEditable);
-
QList rowItems;
rowItems.append(nameItem);
rowItems.append(statusItem);
attachRowItems(rowItems, funcName);
- rowItems.append(actionItem);
m_tableModel->appendRow(rowItems);
attachRowWidgets(i, funcName);
-
- QPushButton *editBtn = new QPushButton;
- editBtn->setIcon(QIcon(":/resources/images/bianji.png"));
- editBtn->setIconSize(QSize(20, 20));
- editBtn->setStyleSheet("padding: 2px; border: none; background-color: transparent;");
- editBtn->setToolTip("修改");
- // QueuedConnection: onEditRow 内 refreshTable 会重建表格析构 editBtn 自身,
- // 同步回调返回到按钮内部事件处理时构成 use-after-free, 故延迟到事件循环执行。
- // 捕获函数名而非行号且以按钮为接收者: 排队前表格可能被重建, 行号会指向错误行;
- // 函数名是稳定键, 且按钮析构时过期的排队请求随之失效
- connect(editBtn, &QPushButton::clicked, editBtn, [this, funcName]() {
- onEditRowByName(funcName);
- }, Qt::QueuedConnection);
- m_funcTable->setIndexWidget(m_tableModel->index(i, m_tableModel->columnCount() - 1), editBtn);
}
applyColumnLayout();
diff --git a/src/pages/common/convertworkbenchbase.h b/src/pages/common/convertworkbenchbase.h
index 3d64e0e..8feb699 100644
--- a/src/pages/common/convertworkbenchbase.h
+++ b/src/pages/common/convertworkbenchbase.h
@@ -83,10 +83,10 @@ protected:
// ---- 表格行虚钩子 ----
// 函数名列样式(子类可对特殊前缀着色等)
virtual QStandardItem *createNameItem(const QString &funcName);
- // 在"是否存在"与"操作"列之间插入额外占位项(如"是否递归"列, 默认无)
+ // 在"是否存在"列之后插入额外占位项(如"是否递归"列, 默认无)
virtual void attachRowItems(QList &rowItems, const QString &funcName)
{ Q_UNUSED(rowItems) Q_UNUSED(funcName) }
- // 嵌入额外列控件(如"是否递归"下拉, 默认无); 操作列编辑按钮由基类统一嵌入
+ // 嵌入额外列控件(如"是否递归"下拉, 默认无); 行编辑入口为双击函数名所在行(基类统一接线)
virtual void attachRowWidgets(int row, const QString &funcName)
{ Q_UNUSED(row) Q_UNUSED(funcName) }
diff --git a/src/pages/metadataupdate/metadataupdatepage.cpp b/src/pages/metadataupdate/metadataupdatepage.cpp
index e28d0eb..2f82cb5 100644
--- a/src/pages/metadataupdate/metadataupdatepage.cpp
+++ b/src/pages/metadataupdate/metadataupdatepage.cpp
@@ -490,7 +490,7 @@ void MetadataUpdatePage::onWorkerFinished()
}
// 提示文案按结果区分: 失败时不得谎报"更新完成", 取消时不报"失败"。
- // 具体失败原因由各 process* 写日志, 弹窗引导用户查看日志
+ // 具体失败原因由各 process* 写日志, 结果仅显示在页面底部提示条, 不弹窗
QString tip;
if (r.cancelled) {
tip = QString("%1更新已取消(总耗时: %2)").arg(m_currentTaskName, elapsedStr);
@@ -521,10 +521,6 @@ void MetadataUpdatePage::onUpdateFinished(bool success, const QString& message)
_uft3AllBtn->setEnabled(true);
_uft3CancelBtn->setEnabled(true);
_forceFullCheckBox->setEnabled(true);
-
- if (success) {
- QMessageBox::information(this, "提示", message);
- } else {
- QMessageBox::warning(this, "提示", message);
- }
+
+ // 结果仅在页面底部提示条展示, 不再弹窗打断用户
}
diff --git a/src/pages/nonlsconvert/nonlsconvertpage.cpp b/src/pages/nonlsconvert/nonlsconvertpage.cpp
index 6092852..81123cf 100644
--- a/src/pages/nonlsconvert/nonlsconvertpage.cpp
+++ b/src/pages/nonlsconvert/nonlsconvertpage.cpp
@@ -47,15 +47,20 @@ QString normalizeNonLsName(const QString &raw)
return name;
}
-// cust_nonls.json convert.dir 值语义:0=否(不递归)、-1=是(递归)
+// cust_nonls.json convert.dir 条目 depth 值语义:0=否(不递归)、-1=是(递归)
constexpr int RecursionNo = 0;
constexpr int RecursionYes = -1;
-// convert.dir 中引擎兜底条目键(非清单功能,加载时跳过、保存时保留),默认值与既有配置样例一致
+// convert.dir 中引擎兜底条目键(非清单功能,加载时跳过、保存时原样透传),默认值与既有配置样例一致
constexpr char WildcardDefaultKey[] = "*";
constexpr int WildcardDefaultValue = RecursionYes;
// 清单表格列定义
-enum TableCol { ColName = 0, ColExists = 1, ColRecurse = 2, ColAction = 3 };
+enum TableCol {
+ ColName = 0, // UF20函数名
+ ColExists = 1, // 是否存在
+ ColRecurse = 2, // 是否递归
+ ColDbMode = 3 // db模式(是=db、否=mdb)
+};
} // namespace
@@ -77,7 +82,9 @@ void NonLsConvertPage::applyPageLayout(QVBoxLayout *layout)
"转码范围不包含核心文件、module 模块文件。"
""
"- 非递归模式:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
"
- "- 递归模式:转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码。
"
+ "- 递归模式:转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码;
"
+ "- db模式:决定 SQL 语法转换方言,是=db、否=mdb(默认);
"
+ "- 双击函数名所在行可在弹窗中修改函数名。
"
"
");
m_tipLabel->setTextFormat(Qt::RichText);
m_tipLabel->setWordWrap(true);
@@ -98,7 +105,7 @@ int NonLsConvertPage::tableColumnCount() const
QStringList NonLsConvertPage::tableHeaderLabels() const
{
- return {"UF20函数名", "是否存在", "是否递归", "操作"};
+ return {"UF20函数名", "是否存在", "是否递归", "db模式"};
}
int NonLsConvertPage::tableMinimumHeight() const
@@ -108,15 +115,15 @@ int NonLsConvertPage::tableMinimumHeight() const
void NonLsConvertPage::applyColumnLayout()
{
- // 4 列按 11:3:2:2 比例分摊、恰好铺满表格(约 61%/17%/11%/11%):
- // "是否递归"占 2 份,保证表头四字与"否/是"下拉完整显示不截断;
+ // 4 列按 8:2:2:2 比例分摊、恰好铺满表格(约 57%/14.3%*3):
+ // "是否递归"/"db模式"各占 2 份,保证表头四字与"否/是"下拉完整显示不截断;
// 取整余数并入函数名列,确保总宽精确铺满、无横向滚动条
const int totalWidth = m_funcTable->viewport()->width();
- const int unit = totalWidth / 18;
- m_funcTable->setColumnWidth(ColName, totalWidth - unit * 7);
- m_funcTable->setColumnWidth(ColExists, unit * 3);
+ const int unit = totalWidth / 14;
+ m_funcTable->setColumnWidth(ColName, totalWidth - unit * 6);
+ m_funcTable->setColumnWidth(ColExists, unit * 2);
m_funcTable->setColumnWidth(ColRecurse, unit * 2);
- m_funcTable->setColumnWidth(ColAction, unit * 2);
+ m_funcTable->setColumnWidth(ColDbMode, unit * 2);
}
QStandardItem *NonLsConvertPage::createNameItem(const QString &funcName)
@@ -132,16 +139,19 @@ QStandardItem *NonLsConvertPage::createNameItem(const QString &funcName)
void NonLsConvertPage::attachRowItems(QList &rowItems, const QString &funcName)
{
Q_UNUSED(funcName)
- // “是否递归”以行尾下拉呈现:否=0、是=-1(与 cust_nonls.json convert.dir 值一致)
- QStandardItem *recItem = new QStandardItem("");
- recItem->setFlags(recItem->flags() & ~Qt::ItemIsEditable);
- recItem->setTextAlignment(Qt::AlignCenter);
- rowItems.append(recItem);
+ // "是否递归"/"db模式"以行内下拉呈现:递归 否=0、是=-1(即 depth),
+ // 方言 否=mdb、是=db(即 dialect),占位列仅承载 setIndexWidget 控件
+ for (int i = 0; i < 2; ++i) {
+ QStandardItem *item = new QStandardItem("");
+ item->setFlags(item->flags() & ~Qt::ItemIsEditable);
+ item->setTextAlignment(Qt::AlignCenter);
+ rowItems.append(item);
+ }
}
void NonLsConvertPage::attachRowWidgets(int row, const QString &funcName)
{
- // “是否递归”下拉控件:否/是,切换即写回内存映射并持久化
+ // "是否递归"下拉控件:否/是,切换即写回内存映射并持久化
ElaComboBox *recCombo = new ElaComboBox;
recCombo->addItem("否", RecursionNo);
recCombo->addItem("是", RecursionYes);
@@ -166,6 +176,31 @@ void NonLsConvertPage::attachRowWidgets(int row, const QString &funcName)
}
});
m_funcTable->setIndexWidget(m_tableModel->index(row, ColRecurse), recCombo);
+
+ // "db模式"下拉控件:否/是 → dialect mdb/db(契约:是=db、否=mdb),交互与"是否递归"列一致
+ ElaComboBox *dbCombo = new ElaComboBox;
+ dbCombo->addItem("否", Constants::DirEntry::DialectMdb);
+ dbCombo->addItem("是", Constants::DirEntry::DialectDb);
+ dbCombo->setCurrentIndex(
+ (m_dialectMap.value(funcName, Constants::DirEntry::DialectMdb)
+ == Constants::DirEntry::DialectDb) ? 1 : 0);
+ connect(dbCombo, QOverload::of(&QComboBox::currentIndexChanged),
+ this, [this, funcName, dbCombo](int index) {
+ const QString dialect = (index == 1) ? Constants::DirEntry::DialectDb
+ : Constants::DirEntry::DialectMdb;
+ const QString prev = m_dialectMap.value(funcName, Constants::DirEntry::DialectMdb);
+ if (prev == dialect) {
+ return;
+ }
+ m_dialectMap.insert(funcName, dialect);
+ if (!saveConfig(m_funcList)) {
+ m_dialectMap.insert(funcName, prev);
+ dbCombo->blockSignals(true);
+ dbCombo->setCurrentIndex(prev == Constants::DirEntry::DialectDb ? 1 : 0);
+ dbCombo->blockSignals(false);
+ }
+ });
+ m_funcTable->setIndexWidget(m_tableModel->index(row, ColDbMode), dbCombo);
}
QString NonLsConvertPage::normalizeFunctionName(const QString &raw) const
@@ -176,12 +211,14 @@ QString NonLsConvertPage::normalizeFunctionName(const QString &raw) const
void NonLsConvertPage::onFunctionAdded(const QString &name)
{
m_recursionMap.insert(name, RecursionNo); // 新增功能默认“否”
+ m_dialectMap.insert(name, Constants::DirEntry::DialectMdb); // 新增功能默认 mdb
}
void NonLsConvertPage::onFunctionsRemoved(const QStringList &names)
{
for (const QString &name : names) {
m_recursionMap.remove(name);
+ m_dialectMap.remove(name);
}
}
@@ -190,11 +227,15 @@ void NonLsConvertPage::onFunctionsRenamed(const QString &oldName, const QString
const int oldRecursion = m_recursionMap.value(oldName, RecursionNo);
m_recursionMap.remove(oldName);
m_recursionMap.insert(newName, oldRecursion);
+ const QString oldDialect = m_dialectMap.value(oldName, Constants::DirEntry::DialectMdb);
+ m_dialectMap.remove(oldName);
+ m_dialectMap.insert(newName, oldDialect);
}
void NonLsConvertPage::onFunctionListCleared()
{
m_recursionMap.clear();
+ m_dialectMap.clear();
}
void NonLsConvertPage::adviseAddedFunctions(const QStringList &names)
@@ -234,12 +275,17 @@ bool NonLsConvertPage::saveConfig(const QStringList &funcList)
}
}
- // convert.dir 键值表:{功能名: 0|-1};0=否(不递归)、-1=是(递归),并保留/兜底引擎默认键 "*"
+ // convert.dir 键值表(V2):{功能名: {depth, dialect}};depth 0=否(不递归)、-1=是(递归),
+ // dialect 是=db、否=mdb;引擎兜底键 "*" 保持数值原样透传(不升级为对象、不带 dialect)
const QJsonObject oldDir = root.value(Constants::CustKeys::ConvertDir).toObject()
.value(Constants::CustKeys::ConvertDirSub).toObject();
QJsonObject dirObj;
for (const QString &name : funcList) {
- dirObj.insert(name, m_recursionMap.value(name, RecursionNo));
+ QJsonObject entry;
+ entry.insert(Constants::DirEntry::Depth, m_recursionMap.value(name, RecursionNo));
+ entry.insert(Constants::DirEntry::Dialect,
+ m_dialectMap.value(name, Constants::DirEntry::DialectMdb));
+ dirObj.insert(name, entry);
}
if (oldDir.contains(WildcardDefaultKey)) {
dirObj.insert(WildcardDefaultKey, oldDir.value(WildcardDefaultKey));
@@ -315,8 +361,10 @@ void NonLsConvertPage::loadConfig()
QStringList items;
m_recursionMap.clear();
+ m_dialectMap.clear();
if (dirVal.isObject()) {
- // 新版:dir 为 {功能名: 0|-1},0=否(不递归)、-1=是(递归);"*" 为引擎默认键,不展示
+ // V2:dir 为 {功能名: {depth, dialect}},depth 0=否、-1=是,dialect 是=db、否=mdb;
+ // 兼容 V1 数值条目(无 dialect,按 mdb 兜底);"*" 为引擎默认键,不展示、保存时原样透传
const QJsonObject dirObj = dirVal.toObject();
for (auto it = dirObj.constBegin(); it != dirObj.constEnd(); ++it) {
if (it.key() == WildcardDefaultKey) {
@@ -326,17 +374,31 @@ void NonLsConvertPage::loadConfig()
if (item.isEmpty()) {
continue;
}
+ // V1 数值 / V2 对象条目双格式兼容: 缺失字段按契约默认值兜底
+ int depth = RecursionNo;
+ QString dialect = Constants::DirEntry::DialectMdb;
+ if (it.value().isObject()) {
+ const QJsonObject entry = it.value().toObject();
+ depth = entry.value(Constants::DirEntry::Depth).toInt(RecursionNo);
+ const QString d =
+ entry.value(Constants::DirEntry::Dialect).toString(Constants::DirEntry::DialectMdb);
+ dialect = (d == Constants::DirEntry::DialectDb) ? Constants::DirEntry::DialectDb
+ : Constants::DirEntry::DialectMdb;
+ } else {
+ depth = it.value().toInt(RecursionNo);
+ }
items.append(item);
- const int v = it.value().toInt(RecursionNo);
- m_recursionMap.insert(item, (v == RecursionYes) ? RecursionYes : RecursionNo);
+ m_recursionMap.insert(item, (depth == RecursionYes) ? RecursionYes : RecursionNo);
+ m_dialectMap.insert(item, dialect);
}
} else {
- // 兼容旧版:dir 为 ["功能名", ...],无递归标志,一律按“否”加载
+ // 兼容更早期版本:dir 为 ["功能名", ...],无递归标志,一律按"否/mdb"加载
for (const QJsonValue &val : dirVal.toArray()) {
const QString item = normalizeNonLsName(val.toString());
if (!item.isEmpty()) {
items.append(item);
m_recursionMap.insert(item, RecursionNo);
+ m_dialectMap.insert(item, Constants::DirEntry::DialectMdb);
}
}
}
diff --git a/src/pages/nonlsconvert/nonlsconvertpage.h b/src/pages/nonlsconvert/nonlsconvertpage.h
index 44662d5..0fd1623 100644
--- a/src/pages/nonlsconvert/nonlsconvertpage.h
+++ b/src/pages/nonlsconvert/nonlsconvertpage.h
@@ -9,10 +9,14 @@ class QVBoxLayout;
class QStandardItem;
// 按需转码工作台(业务转码页非 LS pivot,针对非 LS 功能转码)
-// 函数清单(短名)与"是否递归"开关持久化到 config/cust_nonls.json,
-// convert.dir 为 {功能名: 0| -1} 键值表:0=否(不递归)、-1=是(递归),另含引擎默认键 "*"
+// 函数清单(短名)与"是否递归"开关、"db模式"方言持久化到 config/cust_nonls.json,
+// convert.dir 为 {功能名: {depth: 0|-1, dialect: "db"|"mdb"}} 键值表(V2 格式):
+// depth 0=否(不递归)、-1=是(递归),dialect 是=db、否=mdb(默认),
+// 另含引擎默认键 "*"(保持数值原样透传,不展示不修改)
// 调用契约:仅携带 -m 2(按需转码模式);函数清单与递归开关由引擎直接读取 cust_nonls.json,
// 不再需要 -t/-o 等命令行参数
+// 历史格式兼容:旧版条目为数值(无 dialect)按"否/mdb"兜底;更早期字符串数组(无递归
+// 标志)统一按"否/mdb"兜底。格式契约见 docs/转码配置格式契约.md
class NonLsConvertPage : public ConvertWorkbenchBase
{
Q_OBJECT
@@ -55,8 +59,10 @@ protected:
private:
QLabel *m_tipLabel = nullptr; // 页首灰字说明条(需主题联动)
- // 每个清单功能对应的“是否递归”标志:0=否、-1=是(与 cust_nonls.json convert.dir 值一致)
+ // 每个清单功能对应的"是否递归"标志:0=否、-1=是(与 cust_nonls.json convert.dir 条目 depth 字段一致)
QHash m_recursionMap;
+ // 每个清单功能对应的 SQL 方言:"db"/"mdb"(db模式列 是=db、否=mdb,默认 mdb)
+ QHash m_dialectMap;
};
#endif // NONLSCONVERTPAGE_H
diff --git a/src/utils/Constants.h b/src/utils/Constants.h
index 6bf2f4e..3755935 100644
--- a/src/utils/Constants.h
+++ b/src/utils/Constants.h
@@ -178,6 +178,31 @@ namespace DesignExts {
inline const QString AFunction = ".afunction_design";
}
+// ---------------------------------------------------------------------------
+// convert.dir 条目字段与取值(V2 对象格式, 全量/按需转码页与代码检索清单读取共用,
+// 拼错会静默丢字段/错方言; 契约见 docs/转码配置格式契约.md)
+// ---------------------------------------------------------------------------
+namespace DirEntry {
+ inline const QString Name = "name"; // 功能短名+设计器扩展名(全量转码)
+ inline const QString Core = "core"; // 目标核心(全量转码)
+ inline const QString ViaUmgr = "viaUmgr"; // 是否柜面接口(全量转码)
+ inline const QString Dialect = "dialect"; // SQL 方言(两份配置共用)
+ inline const QString Depth = "depth"; // 递归深度标志(按需转码, 语义同 V1 数值)
+
+ inline const QString CoreUcbp = "ucbp"; // 默认核心
+ inline const QString CoreUses = "uses";
+ inline const QString CoreUarg = "uarg";
+ inline const QString CoreUsms = "usms";
+ inline const QString DialectMdb = "mdb"; // ucbp/uses 对应方言(默认)
+ inline const QString DialectDb = "db"; // uarg/usms 对应方言
+
+ // dialect 由 core 派生: ucbp/uses→mdb, uarg/usms→db; UI 不提供独立 dialect 入口
+ inline QString dialectForCore(const QString &core)
+ {
+ return (core == CoreUarg || core == CoreUsms) ? DialectDb : DialectMdb;
+ }
+}
+
// ---------------------------------------------------------------------------
// 功能查询字段选择器(DataCache 搜索 API 的 field 参数;传错字符串会静默
// 落入"任一字段匹配"分支且不报错,故集中于此)