更新使用手册;修复滚动条问题;新增表映射关系查询
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// V2 默认值(契约 docs/转码配置格式契约.md): core 默认 ucbp, 柜面接口默认否;
|
||||
// V2 默认值(契约 详细设计文档.md §7.5): core 默认 ucbp, 柜面接口默认否;
|
||||
// dialect 不落 UI, 保存时由 core 派生(ucbp/uses→mdb, uarg/usms→db)
|
||||
|
||||
bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames)
|
||||
@@ -355,6 +355,11 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
|
||||
}
|
||||
|
||||
// 5. 原子写回(二进制模式: 内容字节原样落盘, 不做任何换行转换)
|
||||
// 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的
|
||||
// fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载
|
||||
// (重载会 refreshTable 强制 scrollToTop, 表现为下拉选择后滚动条回顶);
|
||||
// 失败路径(打开/提交失败)由组件 2s 兜底自动关窗
|
||||
m_configMonitor->beginSaveWindow();
|
||||
QSaveFile saveFile(configFilePath());
|
||||
if (!saveFile.open(QIODevice::WriteOnly)) {
|
||||
LogManager::instance()->logError(
|
||||
@@ -368,6 +373,8 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 程序自身写入后更新监控 mtime 基准, 避免触发无谓的重载
|
||||
m_configMonitor->endSave();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class QStandardItem;
|
||||
// 保存时对原文件做文本级区间替换(保留 cust.json 其余全部格式不重序列化)。
|
||||
// core 决定目标核心(ucbp/uses/uarg/usms);dialect 由 core 派生、不提供独立 UI
|
||||
// (ucbp/uses→mdb,uarg/usms→db);viaUmgr 为是否柜面接口。
|
||||
// 格式契约见 docs/转码配置格式契约.md
|
||||
// 格式契约见 详细设计文档.md §7.5
|
||||
class BatchConvertPage : public ConvertWorkbenchBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace {
|
||||
|
||||
for (const QJsonValue &v : dirArray) {
|
||||
// V1 字符串元素 / V2 对象元素(取 name 字段)双格式兼容
|
||||
// (全量转码 convert.dir 格式升级, 见 docs/转码配置格式契约.md)
|
||||
// (全量转码 convert.dir 格式升级, 见 详细设计文档.md §7.5)
|
||||
QString name = v.isObject()
|
||||
? v.toObject().value(Constants::DirEntry::Name).toString()
|
||||
: v.toString();
|
||||
@@ -960,12 +960,29 @@ void CodeSearchPage::onQueryByInput()
|
||||
return;
|
||||
}
|
||||
const QList<FunctionInfo> results = cache->searchUFT3Functions(raw, Constants::FunctionSearch::FieldFunctionNo, true);
|
||||
if (results.isEmpty() || results.first().eName.isEmpty()) {
|
||||
QMessageBox::information(this, "提示",
|
||||
QString("未找到功能号:%1 对应的函数").arg(raw));
|
||||
return;
|
||||
if (!results.isEmpty() && !results.first().eName.isEmpty()) {
|
||||
target = results.first().eName;
|
||||
} else {
|
||||
// 元数据查不到时回退代码索引按 objectId 查找: 元数据接口扫描会排除
|
||||
// cbp/ucbp 目录(moduleGenerationCheck 开启时)且同名中文名接口互相覆盖,
|
||||
// 部分功能号在元数据中缺失; 代码索引全量覆盖检索目录, 无此限制。
|
||||
// 后台任务可能正在重建索引(写 objectId 哈希), 主线程读前须确认无在途任务
|
||||
if (!m_activeWatchers.isEmpty()) {
|
||||
QMessageBox::information(this, "提示", "当前有任务正在执行, 请稍候再试");
|
||||
return;
|
||||
}
|
||||
const QList<QSharedPointer<const CallChainAnalyzer::FunctionInfo>> byId =
|
||||
currentAnalyzer()->lookupByObjectId(raw);
|
||||
if (byId.isEmpty()) {
|
||||
QMessageBox::information(this, "提示",
|
||||
QString("未找到功能号:%1 对应的函数\n"
|
||||
"请确认该功能号属于 UF30 工程接口,且元数据已更新、"
|
||||
"代码检索路径正确并已刷新索引。").arg(raw));
|
||||
return;
|
||||
}
|
||||
// 同功能号多接口时取第一个候选(均在当前检索目录内, 皆可展示调用链)
|
||||
target = byId.first()->chineseName;
|
||||
}
|
||||
target = results.first().eName;
|
||||
}
|
||||
|
||||
runQuery(target);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <QPainter>
|
||||
#include <QShowEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include "ElaPushButton.h"
|
||||
#include "ElaTheme.h"
|
||||
|
||||
@@ -196,6 +198,11 @@ QStandardItem *ConvertWorkbenchBase::createNameItem(const QString &funcName)
|
||||
|
||||
void ConvertWorkbenchBase::refreshTable()
|
||||
{
|
||||
// 记录重建前的滚动像素位置: 编辑/增删/外部重载后恢复原位, 避免全量重建
|
||||
// 后视图跳顶(曾表现为"修改 UF20 函数名后滚动条自动跳回顶部")
|
||||
QScrollBar *vbar = m_funcTable->verticalScrollBar();
|
||||
const int savedScrollPos = vbar->value();
|
||||
|
||||
// 不使用 blockSignals:QTableView 依赖 rowsRemoved/rowsInserted 信号更新内部布局缓存,
|
||||
// 若屏蔽会导致新增行不可见。
|
||||
// 批量重建期间抑制逐行重绘:N 行的 removeRow/appendRow/setIndexWidget 每步都会触发
|
||||
@@ -234,11 +241,24 @@ void ConvertWorkbenchBase::refreshTable()
|
||||
// 恢复视图更新并整体重绘一次
|
||||
m_funcTable->setUpdatesEnabled(true);
|
||||
|
||||
// 视图刷新保险:强制重绘并滚动到顶部,确保新增行可见
|
||||
// 视图刷新保险:强制重绘;恢复滚动位置。
|
||||
// 关键时序: 重建后滚动范围(maximum)要等 Qt 延迟布局事件才最终确定, 直接 setValue
|
||||
// 会被 clamp(表现为位置漂移到中间); scrollTo(PositionAtTop) 按视口相对坐标换算,
|
||||
// 在布局中间态下会错位甚至因"目标已可见"短路不滚。因此先 doItemsLayout() 强制
|
||||
// 同步完成延迟布局(滚动范围就绪), 再按绝对像素值精确恢复
|
||||
m_funcTable->viewport()->update();
|
||||
m_funcTable->resizeRowsToContents();
|
||||
m_funcTable->scrollToTop();
|
||||
m_funcTable->doItemsLayout();
|
||||
vbar->setValue(savedScrollPos);
|
||||
m_funcTable->clearSelection();
|
||||
|
||||
// 延迟一拍兜底补设: 布局/滚动范围在此后才最终确定的场景下校正,
|
||||
// 防止其他代码在重建后改动滚动条导致位置漂移
|
||||
QTimer::singleShot(0, m_funcTable, [vbar, savedScrollPos]() {
|
||||
if (vbar->value() != savedScrollPos) {
|
||||
vbar->setValue(savedScrollPos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConvertWorkbenchBase::refreshExistColumn()
|
||||
@@ -283,6 +303,16 @@ void ConvertWorkbenchBase::showEvent(QShowEvent *event)
|
||||
// 构造期 loadConfig 计算列宽时页面尚未布局, viewport 宽度非真实值,
|
||||
// 首次切入本页时宽度已就绪, 重算一次保证列宽比例正确(幂等, 开销可忽略)
|
||||
applyColumnLayout();
|
||||
// 行高定稿: 页面在启动期(窗口未显示、字体/控件未 polish)构建, 那时 refreshTable
|
||||
// 的 resizeRowsToContents 算出的行高偏小(如 28px); 首次编辑触发的重建在可见
|
||||
// 状态下算出最终行高(如 33px), 行高突变使同一滚动数值下的可视内容整体位移,
|
||||
// 表现为"首次修改后滚动条回滚"。切页显示后补算一次, 让行高在用户操作前定稿
|
||||
// (延迟一拍确保布局事件处理完毕后再测量, 测量结果与重建路径一致)
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (m_funcTable && isVisible()) {
|
||||
m_funcTable->resizeRowsToContents();
|
||||
}
|
||||
});
|
||||
// 元数据更新后 DataCache 可能变化: 切页回来仅轻量刷新"是否存在"列,
|
||||
// 不做全量表格重建(数据变更已由 refreshTable/外部修改监控统一触发)
|
||||
refreshExistColumn();
|
||||
|
||||
@@ -210,6 +210,21 @@ void MappingTablePage::onModelItemChanged(QStandardItem *item)
|
||||
|
||||
void MappingTablePage::onSave()
|
||||
{
|
||||
QString errorMessage;
|
||||
const SaveResult result = saveToDisk(&errorMessage);
|
||||
if (result == SaveResult::Ok) {
|
||||
QMessageBox::information(this, "提示", "保存成功!");
|
||||
} else if (result == SaveResult::ValidationFailed) {
|
||||
QMessageBox::warning(this, "提示", errorMessage);
|
||||
} else {
|
||||
QMessageBox::critical(this, "错误", errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
MappingTablePage::SaveResult MappingTablePage::saveToDisk(QString *errorMessage)
|
||||
{
|
||||
if (errorMessage) errorMessage->clear();
|
||||
|
||||
// 开启保存窗口: 窗口期内监控事件直接忽略
|
||||
m_monitor->beginSaveWindow();
|
||||
|
||||
@@ -218,8 +233,8 @@ void MappingTablePage::onSave()
|
||||
QFile file(jsonPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
m_monitor->endSave(); // 早退必须关闭窗口,否则监控被永久抑制
|
||||
QMessageBox::critical(this, "错误", "无法读取 cust.json!");
|
||||
return;
|
||||
if (errorMessage) *errorMessage = "无法读取 cust.json!";
|
||||
return SaveResult::Error;
|
||||
}
|
||||
const QString rawJson = QString::fromUtf8(file.readAll());
|
||||
file.close();
|
||||
@@ -228,21 +243,21 @@ void MappingTablePage::onSave()
|
||||
QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
m_monitor->endSave();
|
||||
QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString());
|
||||
return;
|
||||
if (errorMessage) *errorMessage = "cust.json 解析失败: " + error.errorString();
|
||||
return SaveResult::Error;
|
||||
}
|
||||
QJsonObject custJson = doc.object();
|
||||
|
||||
// 提取原始 key 顺序
|
||||
const QStringList keyOrder = CustJsonIo::extractTopLevelKeyOrder(rawJson);
|
||||
|
||||
// 子类校验并回填数据: 校验失败置 errorMessage, 弹警告不落盘
|
||||
QString errorMessage;
|
||||
const QHash<QString, QString> overrideRaw = collectMappingData(custJson, errorMessage);
|
||||
if (!errorMessage.isEmpty()) {
|
||||
// 子类校验并回填数据: 校验失败不落盘
|
||||
QString validateError;
|
||||
const QHash<QString, QString> overrideRaw = collectMappingData(custJson, validateError);
|
||||
if (!validateError.isEmpty()) {
|
||||
m_monitor->endSave();
|
||||
QMessageBox::warning(this, "提示", errorMessage);
|
||||
return;
|
||||
if (errorMessage) *errorMessage = validateError;
|
||||
return SaveResult::ValidationFailed;
|
||||
}
|
||||
|
||||
// endSave 以保存后的新 mtime 为基准(writeCustJsonOrdered 同步写完才返回),
|
||||
@@ -254,10 +269,15 @@ void MappingTablePage::onSave()
|
||||
if (writeOk) {
|
||||
m_dirty = false; // 保存成功后与磁盘内容一致
|
||||
LogManager::instance()->logInfo(saveLogText(m_model->rowCount()));
|
||||
QMessageBox::information(this, "提示", "保存成功!");
|
||||
} else {
|
||||
QMessageBox::critical(this, "错误", "保存到 cust.json 失败!");
|
||||
onSavedToDisk(); // 落盘成功钩子(表替换页覆写: 同步 table_mapping.json)
|
||||
return SaveResult::Ok;
|
||||
}
|
||||
if (errorMessage) *errorMessage = "保存到 cust.json 失败!";
|
||||
return SaveResult::Error;
|
||||
}
|
||||
|
||||
void MappingTablePage::onSavedToDisk()
|
||||
{
|
||||
}
|
||||
|
||||
void MappingTablePage::onReloadRequested()
|
||||
|
||||
@@ -73,6 +73,18 @@ protected:
|
||||
// 子类构造体内调用(规避构造期虚函数绑定到基类版本)
|
||||
void load();
|
||||
|
||||
// 保存结果分类: Ok=成功落盘; ValidationFailed=子类校验未过(用户级警告);
|
||||
// Error=读文件/解析/写盘失败(程序级错误)
|
||||
enum class SaveResult { Ok, ValidationFailed, Error };
|
||||
|
||||
// 落盘保存核心(无任何弹窗): onSave 与「映射查询页双击自动添加保存」共用。
|
||||
// 失败原因经 errorMessage 出参带回(ValidationFailed=校验文案, Error=IO/解析文案)
|
||||
SaveResult saveToDisk(QString *errorMessage);
|
||||
|
||||
// 落盘成功后的钩子(默认空实现),子类按需覆写
|
||||
// (表替换页覆写: 把新增映射同步追加到 table_mapping.json)
|
||||
virtual void onSavedToDisk();
|
||||
|
||||
// 虚点:从 custJson 填充模型
|
||||
virtual void populateModel(const QJsonObject &custJson) = 0;
|
||||
// 虚点:保存时校验并回填数据到 custJson;返回 overrideRaw(键名→格式化原文);
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
#include "tablemappingpage.h"
|
||||
#include "utils/Constants.h"
|
||||
#include "src/utils/Constants.h"
|
||||
#include "src/utils/logmanager.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QStandardItem>
|
||||
|
||||
namespace {
|
||||
// JSON 字符串转义: 表名为普通标识符, 实际仅需处理引号与反斜杠
|
||||
QString escapeJsonString(const QString &text)
|
||||
{
|
||||
QString out = text;
|
||||
out.replace('\\', "\\\\");
|
||||
out.replace('"', "\\\"");
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TableMappingPage::TableMappingPage(QWidget *parent)
|
||||
: TwoColumnKVMappingPage(Constants::CustKeys::TableCompare,
|
||||
@@ -7,3 +26,248 @@ TableMappingPage::TableMappingPage(QWidget *parent)
|
||||
{
|
||||
load();
|
||||
}
|
||||
|
||||
void TableMappingPage::onSavedToDisk()
|
||||
{
|
||||
syncTableMappingJson();
|
||||
}
|
||||
|
||||
void TableMappingPage::syncTableMappingJson()
|
||||
{
|
||||
// 模型当前映射(模型序): 刚落盘的 cust.json 与模型一致, 以其为同步源
|
||||
QHash<QString, QString> modelMap;
|
||||
for (int row = 0; row < m_model->rowCount(); ++row) {
|
||||
QStandardItem *keyItem = m_model->item(row, 0);
|
||||
if (!keyItem || keyItem->text().trimmed().isEmpty()) {
|
||||
continue; // 空键行在 collectMappingData 已拦截, 正常不会走到
|
||||
}
|
||||
QStandardItem *valItem = m_model->item(row, 1);
|
||||
modelMap.insert(keyItem->text().trimmed(),
|
||||
valItem ? valItem->text().trimmed() : QString());
|
||||
}
|
||||
|
||||
const QString jsonPath = Constants::tableMappingJson();
|
||||
QFile file(jsonPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LogManager::instance()->logError("表替换同步 - 无法读取 table_mapping.json: " + jsonPath);
|
||||
return;
|
||||
}
|
||||
const QString raw = QString::fromUtf8(file.readAll());
|
||||
file.close();
|
||||
|
||||
QJsonParseError parseErr;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw.toUtf8(), &parseErr);
|
||||
if (parseErr.error != QJsonParseError::NoError) {
|
||||
LogManager::instance()->logError(
|
||||
QString("表替换同步 - table_mapping.json 解析失败: %1 (偏移 %2)")
|
||||
.arg(parseErr.errorString()).arg(parseErr.offset));
|
||||
return;
|
||||
}
|
||||
const QJsonObject tmObj = doc.object()[Constants::CustKeys::TableCompare].toObject();
|
||||
|
||||
// 文本定位 tableCompareUf2_Uf3 对象的开/闭括号, 成员级解析后按原格式重建,
|
||||
// 保持文件其余部分(条目顺序/一对多数组/空行风格)原样不动
|
||||
const QString tmKey = "\"" + Constants::CustKeys::TableCompare + "\"";
|
||||
const qsizetype keyPos = raw.indexOf(tmKey);
|
||||
const qsizetype openPos = keyPos < 0 ? -1 : raw.indexOf('{', keyPos + tmKey.size());
|
||||
if (openPos < 0) {
|
||||
LogManager::instance()->logError(
|
||||
"表替换同步 - table_mapping.json 中未找到 tableCompareUf2_Uf3 对象");
|
||||
return;
|
||||
}
|
||||
|
||||
// 成员描述: key + 原文起止(从 key 引号到 value 结束, 含跨行数组原文)
|
||||
struct Member {
|
||||
QString key;
|
||||
qsizetype start;
|
||||
qsizetype end;
|
||||
};
|
||||
QList<Member> members;
|
||||
|
||||
// 花括号/方括号配对跳过字符串字面量, 从开括号后逐成员解析
|
||||
qsizetype i = openPos + 1;
|
||||
while (i < raw.size()) {
|
||||
while (i < raw.size() && raw.at(i).isSpace()) {
|
||||
++i;
|
||||
}
|
||||
if (i >= raw.size() || raw.at(i) == '}') {
|
||||
break;
|
||||
}
|
||||
if (raw.at(i) != '"') {
|
||||
LogManager::instance()->logError(
|
||||
QString("表替换同步 - tableCompareUf2_Uf3 成员解析异常(偏移 %1), 已放弃同步").arg(i));
|
||||
return;
|
||||
}
|
||||
const qsizetype memberStart = i;
|
||||
// 解析 key 字符串(处理 \" 与 \\ 转义, 与 QJsonDocument 解析口径一致)
|
||||
++i;
|
||||
QString key;
|
||||
while (i < raw.size()) {
|
||||
const QChar ch = raw.at(i++);
|
||||
if (ch == '\\' && i < raw.size()) {
|
||||
const QChar esc = raw.at(i++);
|
||||
if (esc == '"' || esc == '\\') {
|
||||
key += esc;
|
||||
} else {
|
||||
key += '\\';
|
||||
key += esc;
|
||||
}
|
||||
} else if (ch == '"') {
|
||||
break;
|
||||
} else {
|
||||
key += ch;
|
||||
}
|
||||
}
|
||||
while (i < raw.size() && raw.at(i).isSpace()) {
|
||||
++i;
|
||||
}
|
||||
if (i >= raw.size() || raw.at(i) != ':') {
|
||||
LogManager::instance()->logError(
|
||||
QString("表替换同步 - 成员 %1 缺少冒号, 已放弃同步").arg(key));
|
||||
return;
|
||||
}
|
||||
++i;
|
||||
while (i < raw.size() && raw.at(i).isSpace()) {
|
||||
++i;
|
||||
}
|
||||
// 解析 value: 字符串 / 数组 / 对象, 括号配对方括号与花括号都计数
|
||||
int bracketDepth = 0;
|
||||
bool valString = false;
|
||||
const qsizetype valueEnd = [&]() {
|
||||
while (i < raw.size()) {
|
||||
const QChar ch = raw.at(i);
|
||||
if (valString) {
|
||||
if (ch == '\\') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch == '"') {
|
||||
valString = false;
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (ch == '"') {
|
||||
valString = true;
|
||||
} else if (ch == '[' || ch == '{') {
|
||||
++bracketDepth;
|
||||
} else if (ch == ']' || ch == '}') {
|
||||
--bracketDepth;
|
||||
if (bracketDepth < 0) {
|
||||
return i; // 到达本对象闭括号(value 已结束于上一字符)
|
||||
}
|
||||
} else if (ch == ',' && bracketDepth == 0) {
|
||||
return i;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}();
|
||||
// memberEnd 回退 value 尾部空白, 确保原文片段以 value 末字符结尾
|
||||
qsizetype trimmedEnd = valueEnd;
|
||||
while (trimmedEnd > memberStart && raw.at(trimmedEnd - 1).isSpace()) {
|
||||
--trimmedEnd;
|
||||
}
|
||||
members.append({key, memberStart, trimmedEnd});
|
||||
i = valueEnd;
|
||||
if (i < raw.size() && raw.at(i) == ',') {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
// 缩进/换行探测: 以首个既有成员所在行的行首空白为准
|
||||
const QString newline = raw.contains("\r\n") ? "\r\n" : "\n";
|
||||
QString entryIndent(" "); // tm 当前缩进风格兜底
|
||||
QString closeIndent(" ");
|
||||
if (!members.isEmpty()) {
|
||||
const qsizetype lineStart = raw.lastIndexOf('\n', members.first().start) + 1;
|
||||
entryIndent = raw.mid(lineStart, members.first().start - lineStart);
|
||||
}
|
||||
{
|
||||
const qsizetype closePos = raw.indexOf('}', i);
|
||||
if (closePos >= 0) {
|
||||
const qsizetype lineStart = raw.lastIndexOf('\n', closePos) + 1;
|
||||
closeIndent = raw.mid(lineStart, closePos - lineStart);
|
||||
}
|
||||
}
|
||||
|
||||
// 逐成员对账: 同 key 时字符串 value 变了才更新; 数组(一对多)一律保留原文;
|
||||
// 模型中多余的 key 追加到末尾。删除暂不同步(成员不因模型缺失而移除)
|
||||
QStringList blocks;
|
||||
QSet<QString> processedKeys;
|
||||
int updated = 0;
|
||||
int arraySkipped = 0;
|
||||
int emptyValueSkipped = 0;
|
||||
for (const Member &member : members) {
|
||||
processedKeys.insert(member.key);
|
||||
const auto modelIt = modelMap.constFind(member.key);
|
||||
if (modelIt == modelMap.constEnd()) {
|
||||
blocks.append(raw.mid(member.start, member.end - member.start)); // cust 已删: tm 保留
|
||||
continue;
|
||||
}
|
||||
const QJsonValue tmVal = tmObj.value(member.key);
|
||||
if (tmVal.isArray()) {
|
||||
blocks.append(raw.mid(member.start, member.end - member.start)); // 一对多数组不覆盖
|
||||
++arraySkipped;
|
||||
continue;
|
||||
}
|
||||
const QString newVal = modelIt.value();
|
||||
if (newVal.isEmpty()) {
|
||||
blocks.append(raw.mid(member.start, member.end - member.start)); // 空值不同步
|
||||
++emptyValueSkipped;
|
||||
continue;
|
||||
}
|
||||
if (tmVal.isString() && tmVal.toString() != newVal) {
|
||||
blocks.append('"' + escapeJsonString(member.key) + "\": \""
|
||||
+ escapeJsonString(newVal) + '"');
|
||||
++updated;
|
||||
} else {
|
||||
blocks.append(raw.mid(member.start, member.end - member.start));
|
||||
}
|
||||
}
|
||||
int appended = 0;
|
||||
for (auto it = modelMap.constBegin(); it != modelMap.constEnd(); ++it) {
|
||||
if (processedKeys.contains(it.key()) || it.value().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
blocks.append('"' + escapeJsonString(it.key()) + "\": \""
|
||||
+ escapeJsonString(it.value()) + '"');
|
||||
++appended;
|
||||
}
|
||||
if (appended == 0 && updated == 0) {
|
||||
return; // 无新增无更新(数组跳过/空值跳过不产生写入)
|
||||
}
|
||||
|
||||
// 重建对象内部: 每成员统一行首缩进, 成员间逗号分隔; 成员原文若跨行(数组),
|
||||
// 其内部各行已有更大缩进, 保持原样
|
||||
QStringList indented;
|
||||
indented.reserve(blocks.size());
|
||||
for (const QString &block : blocks) {
|
||||
indented.append(entryIndent + block);
|
||||
}
|
||||
const QString interior = newline + indented.join(QString(",") + newline) + newline + closeIndent;
|
||||
const QString updatedRaw = raw.left(openPos + 1) + interior + raw.mid(raw.indexOf('}', i));
|
||||
|
||||
// 写前校验: 拼接结果必须仍是合法 JSON, 防止文本插入破坏文件
|
||||
QJsonParseError verifyErr;
|
||||
QJsonDocument::fromJson(updatedRaw.toUtf8(), &verifyErr);
|
||||
if (verifyErr.error != QJsonParseError::NoError) {
|
||||
LogManager::instance()->logError(
|
||||
QString("表替换同步 - 拼接结果校验失败, 已放弃写入: %1").arg(verifyErr.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile outFile(jsonPath);
|
||||
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
LogManager::instance()->logError("表替换同步 - 无法写入 table_mapping.json: " + jsonPath);
|
||||
return;
|
||||
}
|
||||
outFile.write(updatedRaw.toUtf8());
|
||||
outFile.close();
|
||||
|
||||
LogManager::instance()->logInfo(
|
||||
QString("表替换保存: table_mapping.json 同步完成(新增 %1, 更新 %2%3%4)")
|
||||
.arg(appended).arg(updated)
|
||||
.arg(arraySkipped > 0 ? QString(", 一对多数组跳过 %1").arg(arraySkipped) : QString())
|
||||
.arg(emptyValueSkipped > 0 ? QString(", 空值跳过 %1").arg(emptyValueSkipped) : QString()));
|
||||
}
|
||||
|
||||
@@ -5,13 +5,22 @@
|
||||
|
||||
// 维护 cust.json 顶层键 tableCompareUf2_Uf3(UF20→UFT3 表替换映射):
|
||||
// 两列映射表, 支持 新增(弹窗)/双击修改/删除/搜索过滤, 保存按顶层键顺序保序写回;
|
||||
// 填充/校验/新增弹窗样板由 TwoColumnKVMappingPage 中间基类收编
|
||||
// 填充/校验/新增弹窗样板由 TwoColumnKVMappingPage 中间基类收编。
|
||||
// 保存成功后把模型中 table_mapping.json 缺失的新增 key 追加到该文件
|
||||
// (只增不改: 已有条目含一对多数组一律以 table_mapping.json 为准)
|
||||
class TableMappingPage : public TwoColumnKVMappingPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TableMappingPage(QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void onSavedToDisk() override;
|
||||
|
||||
private:
|
||||
// 把模型中 table_mapping.json 缺失的 key 追加到该文件(文本定位插入, 保序保格式)
|
||||
void syncTableMappingJson();
|
||||
};
|
||||
|
||||
#endif // TABLEMAPPINGPAGE_H
|
||||
|
||||
@@ -73,6 +73,35 @@ QHash<QString, QString> TwoColumnKVMappingPage::collectMappingData(QJsonObject &
|
||||
CustJsonIo::formatOrderedJsonObject(orderedData)}};
|
||||
}
|
||||
|
||||
QString TwoColumnKVMappingPage::appendPairIfAbsent(const QString &key, const QString &value)
|
||||
{
|
||||
// 判重口径与 onAddRow/collectMappingData 一致: trimmed 后比较
|
||||
const QString k = key.trimmed();
|
||||
if (k.isEmpty()) {
|
||||
return QString("UF20 %1为空,无法添加").arg(m_entityName);
|
||||
}
|
||||
for (int row = 0; row < m_model->rowCount(); ++row) {
|
||||
if (m_model->item(row, 0) && m_model->item(row, 0)->text().trimmed() == k) {
|
||||
return QString("UF20 %1 \"%2\" 在表替换中已存在(当前映射: %3),未重复添加")
|
||||
.arg(m_entityName).arg(k)
|
||||
.arg(m_model->item(row, 1) ? m_model->item(row, 1)->text() : QString());
|
||||
}
|
||||
}
|
||||
QList<QStandardItem*> rowItems;
|
||||
rowItems.append(new QStandardItem(k));
|
||||
rowItems.append(new QStandardItem(value.trimmed()));
|
||||
m_model->appendRow(rowItems);
|
||||
// 显式置脏纳入外部文件变更保护, 随后立即落盘保存(自动保存语义,
|
||||
// 无弹窗: 成功静默, 失败把原因带回给调用页 tip 展示)
|
||||
m_dirty = true;
|
||||
QString saveError;
|
||||
const MappingTablePage::SaveResult result = saveToDisk(&saveError);
|
||||
if (result != MappingTablePage::SaveResult::Ok) {
|
||||
return QString("已添加但自动保存失败:%1").arg(saveError);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void TwoColumnKVMappingPage::onAddRow()
|
||||
{
|
||||
QDialog dialog(this);
|
||||
|
||||
@@ -23,6 +23,12 @@ public:
|
||||
const QString &entityName,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
// 追加一对映射(UF20 & 内存表映射查询页双击添加的入口)。
|
||||
// key 按 trimmed 口径判重: 已存在返回提示文案(含当前映射值,不覆盖)。
|
||||
// 追加成功后自动落盘保存(无弹窗),失败时返回"已添加但自动保存失败:原因"文案,
|
||||
// 成功返回空串
|
||||
QString appendPairIfAbsent(const QString &key, const QString &value);
|
||||
|
||||
protected:
|
||||
void populateModel(const QJsonObject &custJson) override;
|
||||
QHash<QString, QString> collectMappingData(QJsonObject &custJson,
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
#include "tablesearchpage.h"
|
||||
#include "src/utils/Constants.h"
|
||||
#include "src/utils/datacache.h"
|
||||
#include "src/utils/logmanager.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QHeaderView>
|
||||
#include <QStandardItem>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <algorithm>
|
||||
#include "ElaTheme.h"
|
||||
|
||||
namespace {
|
||||
// 拆分多关键词: 中英文逗号/分号 + 空白统一视为分隔符(与功能查询页口径一致)
|
||||
// 全角字符用字符串字面量替换, 多字节字符不能写成字符常量(会成为 multichar)
|
||||
const QRegularExpression kWhitespaceRegex(QStringLiteral("\\s+"));
|
||||
|
||||
QStringList splitKeywords(const QString& text)
|
||||
{
|
||||
QString normalized = text;
|
||||
normalized.replace(QStringLiteral(","), QStringLiteral(" "))
|
||||
.replace(QStringLiteral(";"), QStringLiteral(" "))
|
||||
.replace(',', ' ')
|
||||
.replace(';', ' ');
|
||||
return normalized.split(kWhitespaceRegex, Qt::SkipEmptyParts);
|
||||
}
|
||||
}
|
||||
|
||||
TableSearchPage::TableSearchPage(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_tipLabel(nullptr)
|
||||
, m_exactMatch(false)
|
||||
{
|
||||
initUI();
|
||||
}
|
||||
|
||||
void TableSearchPage::initUI()
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(30, 30, 30, 30);
|
||||
layout->setSpacing(20);
|
||||
|
||||
QHBoxLayout *searchLayout = new QHBoxLayout;
|
||||
searchLayout->setSpacing(10);
|
||||
|
||||
m_searchEdit = new ElaSearchEdit;
|
||||
m_searchEdit->setPlaceholderText("搜索 UF20 表名或内存表名...");
|
||||
m_searchEdit->setSearchEditSize(330, 35);
|
||||
connect(m_searchEdit, &ElaSearchEdit::returnPressed, this, &TableSearchPage::onSearch);
|
||||
connect(m_searchEdit, &ElaSearchEdit::searchClicked, this, &TableSearchPage::onSearch);
|
||||
connect(m_searchEdit, &ElaSearchEdit::exactMatchChanged, this, &TableSearchPage::onExactMatchToggled);
|
||||
searchLayout->addWidget(m_searchEdit);
|
||||
searchLayout->addStretch();
|
||||
layout->addLayout(searchLayout);
|
||||
|
||||
m_tableModel = new QStandardItemModel(this);
|
||||
m_tableModel->setColumnCount(4);
|
||||
m_tableModel->setHorizontalHeaderLabels({"UF20表名", "UF20表中文名", "内存表名", "内存表中文名"});
|
||||
|
||||
m_resultTable = new ElaTableView;
|
||||
m_resultTable->setModel(m_tableModel);
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
m_resultTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); // 表头居中
|
||||
m_resultTable->setMinimumHeight(400);
|
||||
m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_resultTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
layout->addWidget(m_resultTable);
|
||||
|
||||
// 双击记录行 → 添加映射到「数据映射 → 表替换」页(经 MainWindow 接线)
|
||||
connect(m_resultTable, &QTableView::doubleClicked, this, &TableSearchPage::onDoubleClicked);
|
||||
|
||||
QHBoxLayout *tipLayout = new QHBoxLayout;
|
||||
m_tipLabel = new QLabel("💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」");
|
||||
// 不开折行: wordWrap(true) 的 QLabel 在布局中会自选偏窄宽度把提示拆成多行,
|
||||
// 与功能查询页保持一致, 提示单行显示
|
||||
m_tipLabel->setWordWrap(false);
|
||||
tipLayout->addWidget(m_tipLabel);
|
||||
tipLayout->addStretch();
|
||||
layout->addLayout(tipLayout);
|
||||
|
||||
// tip 标签为主题感知: 跟随 Ela 主题切换, 并按当前主题初始化
|
||||
connect(ElaTheme::getInstance(), &ElaTheme::themeModeChanged,
|
||||
this, &TableSearchPage::updateTipStyle);
|
||||
updateTipStyle();
|
||||
}
|
||||
|
||||
void TableSearchPage::updateTipStyle()
|
||||
{
|
||||
if (!m_tipLabel) {
|
||||
return;
|
||||
}
|
||||
const ElaThemeType::ThemeMode mode = ElaTheme::getInstance()->getThemeMode();
|
||||
const QString textColor = ElaThemeColor(mode, BasicText).name();
|
||||
m_tipLabel->setStyleSheet(QString("color: %1; font-size: 13px;").arg(textColor));
|
||||
}
|
||||
|
||||
void TableSearchPage::onExactMatchToggled(bool checked)
|
||||
{
|
||||
m_exactMatch = checked;
|
||||
}
|
||||
|
||||
void TableSearchPage::onDoubleClicked(const QModelIndex& index)
|
||||
{
|
||||
// 双击行取第 1 列(UF20表名)与第 3 列(内存表名): 任一为空即无完整映射关系, 不能添加
|
||||
const QString uf20Name = m_tableModel->item(index.row(), 0)
|
||||
? m_tableModel->item(index.row(), 0)->text() : QString();
|
||||
const QString memName = m_tableModel->item(index.row(), 2)
|
||||
? m_tableModel->item(index.row(), 2)->text() : QString();
|
||||
if (uf20Name.isEmpty() || memName.isEmpty()) {
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("⚠️ 该行无完整映射关系(UF20 表名或内存表名为空),无法添加到表替换");
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit requestAddMapping(uf20Name, memName);
|
||||
}
|
||||
|
||||
void TableSearchPage::onMappingAddResult(const QString& error)
|
||||
{
|
||||
// 双击添加的回调结果: error 为空即添加并自动保存成功, 非空为原因(已存在/保存失败等)
|
||||
if (!m_tipLabel) {
|
||||
return;
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
m_tipLabel->setText("✅ 已添加到「数据映射 → 表替换」并自动保存");
|
||||
} else {
|
||||
m_tipLabel->setText("⚠️ " + error);
|
||||
}
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> TableSearchPage::loadTableMapping(bool* readOk,
|
||||
QString* errorOut) const
|
||||
{
|
||||
if (readOk) *readOk = false;
|
||||
if (errorOut) errorOut->clear();
|
||||
QList<QPair<QString, QString>> pairs;
|
||||
const QString jsonPath = Constants::tableMappingJson();
|
||||
QFile file(jsonPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
const QString msg = "无法读取映射文件 table_mapping.json(" + jsonPath + ")";
|
||||
LogManager::instance()->logError("表查询 - " + msg);
|
||||
if (errorOut) *errorOut = msg;
|
||||
return pairs;
|
||||
}
|
||||
const QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
|
||||
if (err.error != QJsonParseError::NoError) {
|
||||
// 字符偏移转行号(从 1 起), 便于手工维护文件时定位(常见手误: 末项尾随逗号)
|
||||
int line = 1;
|
||||
for (qint64 pos = 0; pos < err.offset && pos < raw.size(); ++pos) {
|
||||
if (raw.at(pos) == '\n') {
|
||||
++line;
|
||||
}
|
||||
}
|
||||
const QString msg = QString("映射文件 table_mapping.json 解析失败(第 %1 行附近):%2。"
|
||||
"映射关系暂不可用,请检查该文件(常见原因:最后一项后多了逗号、引号缺失)")
|
||||
.arg(line).arg(err.errorString());
|
||||
LogManager::instance()->logError("表查询 - " + msg);
|
||||
if (errorOut) *errorOut = msg;
|
||||
return pairs;
|
||||
}
|
||||
// 顶层键 tableCompareUf2_Uf3: key=UF20表名, value=内存表名;
|
||||
// value 可为字符串(一对一)或数组(一对多, 拆为多条映射对, 展示时一个 UF20 表对应多行)
|
||||
const QJsonObject mapping = doc.object()[Constants::CustKeys::TableCompare].toObject();
|
||||
for (auto it = mapping.constBegin(); it != mapping.constEnd(); ++it) {
|
||||
if (it.value().isArray()) {
|
||||
const QJsonArray arr = it.value().toArray();
|
||||
for (const QJsonValue& v : arr) {
|
||||
pairs.append({it.key(), v.toString()});
|
||||
}
|
||||
} else {
|
||||
pairs.append({it.key(), it.value().toString()});
|
||||
}
|
||||
}
|
||||
if (readOk) *readOk = true;
|
||||
return pairs;
|
||||
}
|
||||
|
||||
void TableSearchPage::onSearch()
|
||||
{
|
||||
// 多关键词查询: 支持逗号/空格等分隔多个表名, 任一关键词命中即展示(OR 并集)
|
||||
const QStringList keywords = splitKeywords(m_searchEdit->text());
|
||||
|
||||
m_tableModel->removeRows(0, m_tableModel->rowCount());
|
||||
|
||||
if (keywords.isEmpty()) {
|
||||
// 空关键字: 页内 tip 反馈, 不弹模态框打断操作
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("请输入 UF20 表名或内存表名");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据源三件套: 双库"表名→中文名"映射 + table_mapping.json 表映射(key=UF20表名, value=内存表名)。
|
||||
// 双库映射由 DataCache 内存缓存, 重复查询零开销
|
||||
const QMap<QString, QString> uf20Names = DataCache::instance()->getUf20TableNameMap();
|
||||
const QMap<QString, QString> memNames = DataCache::instance()->getUft3TableNameMap();
|
||||
if (uf20Names.isEmpty() && memNames.isEmpty()) {
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("⚠️ 缓存库中暂无表数据,请先在「元数据更新」页面执行更新");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool mappingReadOk = false;
|
||||
QString mappingError;
|
||||
const QList<QPair<QString, QString>> mapping = loadTableMapping(&mappingReadOk, &mappingError);
|
||||
// 映射文件是本页核心数据源: 读取/解析失败时醒目报错并停止检索,
|
||||
// 不再降级展示单侧结果(否则表现为"有表名却查不到映射", 极具误导性)
|
||||
if (!mappingReadOk) {
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("⚠️ " + mappingError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 单行结果: 四列与页面列定义一致, 任一表名侧为空即"无映射关系"行
|
||||
struct TableRow {
|
||||
QString uf20Name, uf20Cname, memName, memCname;
|
||||
};
|
||||
QList<TableRow> rows;
|
||||
// 已覆盖表名集合: 映射整体行里的表不再补单侧行, 防止同一表重复出现
|
||||
QSet<QString> coveredUf20;
|
||||
QSet<QString> coveredMem;
|
||||
// 已输出的映射对(以换行分隔拼接): 多关键词可能重复命中同一映射对, 去重
|
||||
QSet<QString> seenPairs;
|
||||
|
||||
// 映射命中(key 或 value)→ 完整行: 按 key/value 分别取双库中文名
|
||||
auto appendFullRow = [&](const QString& key, const QString& value) {
|
||||
const QString pairKey = key + QLatin1Char('\n') + value;
|
||||
if (seenPairs.contains(pairKey)) {
|
||||
return;
|
||||
}
|
||||
seenPairs.insert(pairKey);
|
||||
rows.append({key, uf20Names.value(key), value, memNames.value(value)});
|
||||
coveredUf20.insert(key);
|
||||
coveredMem.insert(value);
|
||||
};
|
||||
|
||||
QStringList noMatchKeywords;
|
||||
for (const QString& keyword : keywords) {
|
||||
bool hit = false;
|
||||
if (m_exactMatch) {
|
||||
// 精确匹配: 先查映射(key 或 value 全等 → 整行), 再查双库(单侧行);
|
||||
// 映射已覆盖的表不再补单侧行, 否则同一表会多出一条映射列置空的重复记录
|
||||
for (const auto& pair : mapping) {
|
||||
if (pair.first == keyword || pair.second == keyword) {
|
||||
appendFullRow(pair.first, pair.second);
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
if (!coveredUf20.contains(keyword) && uf20Names.contains(keyword)) {
|
||||
rows.append({keyword, uf20Names.value(keyword), QString(), QString()});
|
||||
coveredUf20.insert(keyword);
|
||||
hit = true;
|
||||
}
|
||||
if (!coveredMem.contains(keyword) && memNames.contains(keyword)) {
|
||||
rows.append({QString(), QString(), keyword, memNames.value(keyword)});
|
||||
coveredMem.insert(keyword);
|
||||
hit = true;
|
||||
}
|
||||
} else {
|
||||
// 模糊匹配: 包含即命中(不区分大小写); 映射命中→整行, 库命中→单侧行
|
||||
for (const auto& pair : mapping) {
|
||||
if (pair.first.contains(keyword, Qt::CaseInsensitive) ||
|
||||
pair.second.contains(keyword, Qt::CaseInsensitive)) {
|
||||
appendFullRow(pair.first, pair.second);
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
for (auto it = uf20Names.constBegin(); it != uf20Names.constEnd(); ++it) {
|
||||
if (!coveredUf20.contains(it.key()) &&
|
||||
it.key().contains(keyword, Qt::CaseInsensitive)) {
|
||||
rows.append({it.key(), it.value(), QString(), QString()});
|
||||
coveredUf20.insert(it.key());
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
for (auto it = memNames.constBegin(); it != memNames.constEnd(); ++it) {
|
||||
if (!coveredMem.contains(it.key()) &&
|
||||
it.key().contains(keyword, Qt::CaseInsensitive)) {
|
||||
rows.append({QString(), QString(), it.key(), it.value()});
|
||||
coveredMem.insert(it.key());
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hit) {
|
||||
noMatchKeywords.append(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("⚠️ 未找到匹配的表,请确认表名是否正确(UF20 表与内存表为两套命名)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 无映射行数: UF20 表名或内存表名任一侧为空即视为无映射关系;
|
||||
// 中文名缺失: 表名侧有值但缓存库查不到该表(元数据更新未覆盖其来源工程),
|
||||
// 中文名无从取得, 只能留空并在 tip 中提示数量
|
||||
int unmappedCount = 0;
|
||||
int cnameMissingCount = 0;
|
||||
for (const TableRow& row : rows) {
|
||||
if (row.uf20Name.isEmpty() || row.memName.isEmpty()) {
|
||||
++unmappedCount;
|
||||
}
|
||||
if ((!row.uf20Name.isEmpty() && row.uf20Cname.isEmpty()) ||
|
||||
(!row.memName.isEmpty() && row.memCname.isEmpty())) {
|
||||
++cnameMissingCount;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询成功: tip 显示统计(一对多映射会产生多条记录); 映射读取失败时附加降级说明
|
||||
if (m_tipLabel) {
|
||||
QString tip = QString("已查询 %1 条记录").arg(rows.size());
|
||||
if (unmappedCount > 0) {
|
||||
tip += QString("(其中 %1 个无映射关系").arg(unmappedCount);
|
||||
if (cnameMissingCount > 0) {
|
||||
tip += QString(",%1 个表中文名缺失(未在缓存库收录)").arg(cnameMissingCount);
|
||||
}
|
||||
tip += ")";
|
||||
} else if (cnameMissingCount > 0) {
|
||||
tip += QString("(%1 个表中文名缺失,未在缓存库收录)").arg(cnameMissingCount);
|
||||
}
|
||||
if (!noMatchKeywords.isEmpty()) {
|
||||
tip += QString(",%1 个关键词无匹配").arg(noMatchKeywords.size());
|
||||
}
|
||||
m_tipLabel->setText(tip);
|
||||
}
|
||||
|
||||
// 结果按 UF20 表名排序(不区分大小写); 仅内存表命中的行(UF20 名为空)排末尾;
|
||||
// 同一 UF20 表的一对多记录按内存表名排序, 保证相邻展示
|
||||
std::sort(rows.begin(), rows.end(), [](const TableRow& a, const TableRow& b) {
|
||||
if (a.uf20Name.isEmpty() != b.uf20Name.isEmpty()) {
|
||||
return b.uf20Name.isEmpty(); // a 的 UF20 名非空在前
|
||||
}
|
||||
const QString an = a.uf20Name.isEmpty() ? a.memName : a.uf20Name;
|
||||
const QString bn = b.uf20Name.isEmpty() ? b.memName : b.uf20Name;
|
||||
const int primary = an.compare(bn, Qt::CaseInsensitive);
|
||||
if (primary != 0) {
|
||||
return primary < 0;
|
||||
}
|
||||
return a.memName.compare(b.memName, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
|
||||
// 批量填充期间抑制逐行重绘(与功能查询页一致): 关闭更新后仅在填充完成时整体呈现
|
||||
m_resultTable->setUpdatesEnabled(false);
|
||||
for (const TableRow& row : rows) {
|
||||
const QString cells[4] = {row.uf20Name, row.uf20Cname, row.memName, row.memCname};
|
||||
QList<QStandardItem*> rowItems;
|
||||
for (const QString& cell : cells) {
|
||||
QStandardItem *item = new QStandardItem(cell);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
item->setTextAlignment(Qt::AlignCenter); // 列内容居中
|
||||
rowItems.append(item);
|
||||
}
|
||||
m_tableModel->appendRow(rowItems);
|
||||
}
|
||||
m_resultTable->setUpdatesEnabled(true);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef TABLESEARCHPAGE_H
|
||||
#define TABLESEARCHPAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStandardItemModel>
|
||||
#include "components/elasearchedit.h"
|
||||
#include "ElaTableView.h"
|
||||
|
||||
class QLabel;
|
||||
|
||||
// UF20 & 内存表映射查询页(原"表查询"): 检索 UF20(2.0) 表与内存表(UFT3)之间的映射关系。
|
||||
// 结果四列: UF20表名 / UF20表中文名(uf20_cache.db 按 key 取) /
|
||||
// 内存表名(table_mapping.json 映射的 value) / 内存表中文名(uft3_cache.db 按 value 取)。
|
||||
// 输入 UF20 表名或内存表名均可检索, 支持精确/模糊(ElaSearchEdit 内置开关)两种模式;
|
||||
// 无映射命中的表仅展示数据库侧字段, 映射相关列置空。
|
||||
// 双击有完整映射的记录行可添加到「数据映射 → 表替换」页(经 MainWindow 接线, 追加后自动落盘)。
|
||||
// 不继承 FunctionSearchPageBase: 结果结构为 4 列表信息, 与 FunctionInfo(3 列函数信息)
|
||||
// 结构不同, 数据源与交互(无批量加入)亦不同, 强行复用基类不值
|
||||
class TableSearchPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TableSearchPage(QWidget *parent = nullptr);
|
||||
// 双击添加的回调结果(经 MainWindow 接线后回传): error 为空即添加成功, 非空为原因
|
||||
void onMappingAddResult(const QString& error);
|
||||
|
||||
signals:
|
||||
// 双击有完整映射的记录行 → 请求把 (UF20表名, 内存表名) 添加到表替换页
|
||||
void requestAddMapping(const QString& uf20Name, const QString& memName);
|
||||
|
||||
private slots:
|
||||
void onSearch();
|
||||
void onExactMatchToggled(bool checked);
|
||||
// 双击记录行: 有完整映射发出 requestAddMapping, 单侧行仅 tip 提示
|
||||
void onDoubleClicked(const QModelIndex& index);
|
||||
// tip 标签主题感知(Ela 原生控件颜色不随主题切换, 需手动跟随)
|
||||
void updateTipStyle();
|
||||
|
||||
private:
|
||||
void initUI();
|
||||
// 读取表映射文件(Constants::tableMappingJson(), 即 bin/uf2touft3/config/table_mapping.json,
|
||||
// 顶层键 tableCompareUf2_Uf3: key=UF20表名, value=内存表名; value 可为字符串(一对一)
|
||||
// 或字符串数组(一对多, 拆为多条映射对))。
|
||||
// readOk 出参: 文件读取/解析成功为 true(映射本身可为空), 失败为 false;
|
||||
// errorOut 出参: 失败时带回可展示的错误原因(含行号, 常见手误如末项尾随逗号)
|
||||
QList<QPair<QString, QString>> loadTableMapping(bool* readOk,
|
||||
QString* errorOut = nullptr) const;
|
||||
|
||||
ElaSearchEdit *m_searchEdit;
|
||||
ElaTableView *m_resultTable;
|
||||
QStandardItemModel *m_tableModel;
|
||||
QLabel *m_tipLabel;
|
||||
bool m_exactMatch;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -16,7 +16,7 @@ class QStandardItem;
|
||||
// 调用契约:仅携带 -m 2(按需转码模式);函数清单与递归开关由引擎直接读取 cust_nonls.json,
|
||||
// 不再需要 -t/-o 等命令行参数
|
||||
// 历史格式兼容:旧版条目为数值(无 dialect)按"否/mdb"兜底;更早期字符串数组(无递归
|
||||
// 标志)统一按"否/mdb"兜底。格式契约见 docs/转码配置格式契约.md
|
||||
// 标志)统一按"否/mdb"兜底。格式契约见 详细设计文档.md §7.5
|
||||
class NonLsConvertPage : public ConvertWorkbenchBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Reference in New Issue
Block a user