diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro
index a2d8edd..e873c51 100644
--- a/Uft30ChangeCode.pro
+++ b/Uft30ChangeCode.pro
@@ -1,4 +1,4 @@
-QT += core gui widgets sql network
+QT += core gui widgets sql network concurrent
TARGET = Uft30ChangeCode
TEMPLATE = app
@@ -10,6 +10,7 @@ SOURCES += main.cpp \
src/pythonrunner/PythonRunner.cpp \
src/pages/about/aboutdialog.cpp \
src/pages/batchconvert/batchconvertpage.cpp \
+ src/pages/codesearch/codesearchpage.cpp \
src/pages/functionsearch/functionsearchpage.cpp \
src/pages/functionsearch/uf20functionsearchpage.cpp \
src/pages/functionsearch/uft3functionsearchpage.cpp \
@@ -34,6 +35,7 @@ SOURCES += main.cpp \
src/utils/uft3configreader.cpp \
src/utils/logmanager.cpp \
src/utils/configmanager.cpp \
+ src/utils/callchainanalyzer.cpp \
src/utils/version.cpp \
src/utils/versionchecker.cpp
@@ -41,6 +43,7 @@ HEADERS += src/mainwindow/mainwindow.h \
src/pythonrunner/PythonRunner.h \
src/pages/about/aboutdialog.h \
src/pages/batchconvert/batchconvertpage.h \
+ src/pages/codesearch/codesearchpage.h \
src/pages/functionsearch/functionsearchpage.h \
src/pages/functionsearch/uf20functionsearchpage.h \
src/pages/functionsearch/uft3functionsearchpage.h \
@@ -65,6 +68,7 @@ HEADERS += src/mainwindow/mainwindow.h \
src/utils/uft3configreader.h \
src/utils/logmanager.h \
src/utils/configmanager.h \
+ src/utils/callchainanalyzer.h \
src/utils/version.h \
src/utils/versionchecker.h
diff --git a/build.bat b/build.bat
index d5c8a2a..e592080 100644
--- a/build.bat
+++ b/build.bat
@@ -76,6 +76,16 @@ if errorlevel 1 (
)
echo.
+REM Deploy Qt runtime dependencies (auto-copy required Qt DLLs/plugins)
+echo Deploying Qt runtime dependencies...
+windeployqt --release bin\Uft30ChangeCode.exe
+if errorlevel 1 (
+ echo [WARN] windeployqt failed, please deploy Qt DLLs manually
+) else (
+ echo [OK] Qt dependencies deployed
+)
+echo.
+
echo ==========================================
echo Build SUCCESSFUL!
echo ==========================================
diff --git a/help/Writerside/hi.tree b/help/Writerside/hi.tree
index 05f2b97..614304b 100644
--- a/help/Writerside/hi.tree
+++ b/help/Writerside/hi.tree
@@ -10,6 +10,7 @@
+
@@ -21,7 +22,8 @@
-
+
+
diff --git a/help/Writerside/images/转码业务.png b/help/Writerside/images/转码业务.png
new file mode 100644
index 0000000..d25049b
Binary files /dev/null and b/help/Writerside/images/转码业务.png differ
diff --git a/help/Writerside/images/非转码业务.png b/help/Writerside/images/非转码业务.png
new file mode 100644
index 0000000..0731f43
Binary files /dev/null and b/help/Writerside/images/非转码业务.png differ
diff --git a/help/Writerside/topics/1-3-0.md b/help/Writerside/topics/update-1-3-0.md
similarity index 100%
rename from help/Writerside/topics/1-3-0.md
rename to help/Writerside/topics/update-1-3-0.md
diff --git a/help/Writerside/topics/update-1-4-0.md b/help/Writerside/topics/update-1-4-0.md
new file mode 100644
index 0000000..c466ab6
--- /dev/null
+++ b/help/Writerside/topics/update-1-4-0.md
@@ -0,0 +1,11 @@
+# 1.4.0
+
+1.提升业务转码性能;
+
+2.元数据更新支持增量更新和强制全量更新;
+
+3.新增代码检索功能;
+
+4.支持函数调用链路分析;
+
+5.支持全局文字可选中复制及表格Ctrl+C导出;
\ No newline at end of file
diff --git a/help/Writerside/topics/下载.md b/help/Writerside/topics/下载.md
index fe2ae4a..f11528d 100644
--- a/help/Writerside/topics/下载.md
+++ b/help/Writerside/topics/下载.md
@@ -14,6 +14,7 @@
| 版本 | 更新日期 | 全量包 | 增量包 |
|-------|------------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
+| 1.4.0 | 2026-07-21 | [Uft30ChangeCode-1.4.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.4.0.zip) | [Uft30ChangeCode-1.4.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.4.0-update.zip) |
| 1.3.0 | 2026-07-07 | [Uft30ChangeCode-1.3.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.3.0.zip) | [Uft30ChangeCode-1.3.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.3.0-update.zip) |
| 1.2.0 | 2026-07-02 | [Uft30ChangeCode-1.2.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.2.0.zip) | [Uft30ChangeCode-1.2.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.2.0-update.zip) |
| 1.1.0 | 2026-07-01 | [Uft30ChangeCode-1.1.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0.zip) | [Uft30ChangeCode-1.1.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0-update.zip) |
diff --git a/help/Writerside/topics/代码检索.md b/help/Writerside/topics/代码检索.md
new file mode 100644
index 0000000..048ca11
--- /dev/null
+++ b/help/Writerside/topics/代码检索.md
@@ -0,0 +1,11 @@
+# 代码检索
+
+支持"转码业务"、"非转码业务"调用链路查询和导出
+
+## 转码业务
+支持查询调用链路和导出查询函数所涉及的文件以及调用链路导出
+
+
+## 非转码业务
+支持查询调用链路和调用链路导出
+
\ No newline at end of file
diff --git a/help/Writerside/topics/功能转码.md b/help/Writerside/topics/功能转码.md
index 4bb1c25..1ee15db 100644
--- a/help/Writerside/topics/功能转码.md
+++ b/help/Writerside/topics/功能转码.md
@@ -30,3 +30,5 @@
## 快速跳转
[业务转码](http://10.20.163.105:6045/uft3changecode/业务转码.html)
+
+[代码检索](http://10.20.163.105:6045/uft3changecode/业务转码.html)
diff --git a/main.cpp b/main.cpp
index 1646edc..a5c374f 100644
--- a/main.cpp
+++ b/main.cpp
@@ -4,11 +4,96 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// 全局文字可复制过滤器:
+// 1) 所有 QLabel(含 ElaText)文字可用鼠标选中并复制;
+// 2) 表格/树(QAbstractItemView)选中的单元格支持 Ctrl+C 复制,制表符分隔列、换行分隔行。
+class TextSelectableFilter : public QObject
+{
+public:
+ using QObject::QObject;
+
+protected:
+ bool eventFilter(QObject *watched, QEvent *event) override
+ {
+ if (event->type() == QEvent::Polish) {
+ if (QLabel *label = qobject_cast(watched)) {
+ // 跳过用作图标按钮的 QLabel(有 pixmap 无文本),
+ // 否则会被强制设为竖线(IBeamCursor),影响点击类图标控件
+ if (label->pixmap().isNull()) {
+ label->setTextInteractionFlags(label->textInteractionFlags()
+ | Qt::TextSelectableByMouse);
+ label->setCursor(Qt::IBeamCursor);
+ }
+ }
+ } else if (event->type() == QEvent::KeyPress) {
+ if (auto *view = qobject_cast(watched)) {
+ auto *ke = static_cast(event);
+ if (ke->matches(QKeySequence::Copy)) {
+ return copySelectedCells(view);
+ }
+ }
+ }
+ return QObject::eventFilter(watched, event);
+ }
+
+private:
+ static bool copySelectedCells(QAbstractItemView *view)
+ {
+ if (!view->selectionModel()) {
+ return false;
+ }
+ QModelIndexList idxs = view->selectionModel()->selectedIndexes();
+ if (idxs.isEmpty()) {
+ return false;
+ }
+ // 按行、列排序,保证复制顺序与显示一致
+ std::sort(idxs.begin(), idxs.end(),
+ [](const QModelIndex &a, const QModelIndex &b) {
+ if (a.row() != b.row()) {
+ return a.row() < b.row();
+ }
+ return a.column() < b.column();
+ });
+
+ QString text;
+ int lastRow = -1;
+ for (const QModelIndex &idx : idxs) {
+ if (idx.row() != lastRow) {
+ if (lastRow != -1) {
+ text += QLatin1Char('\n');
+ }
+ lastRow = idx.row();
+ } else {
+ text += QLatin1Char('\t');
+ }
+ text += idx.data(Qt::DisplayRole).toString();
+ }
+ if (!text.isEmpty()) {
+ QApplication::clipboard()->setText(text);
+ return true; // 已处理,消费该事件
+ }
+ return false;
+ }
+};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setWindowIcon(QIcon(":resources/images/ChangeCode.png"));
+
+ // 安装全局过滤器:所有页面文字 / 表格树单元格允许用户复制
+ TextSelectableFilter textSelectableFilter;
+ a.installEventFilter(&textSelectableFilter);
+
eApp->init();
QString basePath = QCoreApplication::applicationDirPath();
@@ -17,4 +102,4 @@ int main(int argc, char *argv[])
MainWindow w;
w.showMaximized();
return a.exec();
-}
\ No newline at end of file
+}
diff --git a/resources.qrc b/resources.qrc
index 4dc47a1..287bbd3 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -7,5 +7,7 @@
resources/images/max.png
resources/images/min.png
resources/images/update.png
+ resources/images/expand_all.png
+ resources/images/collapse_all.png
\ No newline at end of file
diff --git a/resources/images/collapse_all.png b/resources/images/collapse_all.png
new file mode 100644
index 0000000..cec3a89
Binary files /dev/null and b/resources/images/collapse_all.png differ
diff --git a/resources/images/expand_all.png b/resources/images/expand_all.png
new file mode 100644
index 0000000..3451a83
Binary files /dev/null and b/resources/images/expand_all.png differ
diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp
index ff0c0b2..4558696 100644
--- a/src/mainwindow/mainwindow.cpp
+++ b/src/mainwindow/mainwindow.cpp
@@ -40,6 +40,9 @@ MainWindow::MainWindow(QWidget *parent)
, m_iconsSet(false)
, m_navigationButton(nullptr)
, m_isNavigationExpanded(true)
+ , m_totalFunctions(0)
+ , m_currentFunctionIndex(0)
+ , m_lastProgress(0)
{
initWindow();
initContent();
@@ -133,7 +136,8 @@ void MainWindow::initContent()
{
addExpanderNode("功能转码", m_funcConvertKey, ElaIconType::FileCode);
addPageNode("业务转码", createBatchConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat);
-
+ addPageNode("代码检索", createCodeSearchPage(), m_funcConvertKey, ElaIconType::Cubes);
+
addExpanderNode("功能查询", m_funcSearchKey, ElaIconType::MagnifyingGlass);
addPageNode("UF20功能查询", createUF20FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
@@ -157,6 +161,12 @@ QWidget* MainWindow::createBatchConvertPage()
return m_batchConvertPage;
}
+QWidget* MainWindow::createCodeSearchPage()
+{
+ _codeSearchPage = new CodeSearchPage;
+ return _codeSearchPage;
+}
+
QWidget* MainWindow::createFunctionSearchPage()
{
return new FunctionSearchPage;
@@ -222,6 +232,9 @@ void MainWindow::onBatchConvertStart()
m_currentFunctionIndex = 0;
m_functionList.clear();
}
+
+ // 进度基准:BatchConvertPage 在开始时已将进度条置为 20(配置加载完成)
+ m_lastProgress = 20;
LogManager::instance()->log("========================================");
LogManager::instance()->log("开始执行 UFT2 到 UFT3 的代码转换...");
@@ -255,6 +268,9 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS
LogManager::instance()->log("========================================");
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
LogManager::instance()->log("✓ 代码转换完成!");
+ if (_codeSearchPage) {
+ _codeSearchPage->refreshIndexSilent();
+ }
QMessageBox::information(this, "完成", "代码转换任务已成功完成!");
} else {
LogManager::instance()->logError(QString("✗ 转换失败! 退出码: %1").arg(exitCode));
@@ -265,59 +281,82 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS
void MainWindow::onPythonRunnerOutput(const QString &output)
{
- if (!output.trimmed().isEmpty()) {
- LogManager::instance()->log(output);
-
- QString trimmedOutput = output.trimmed();
-
- QRegularExpression progressRegExp(R"(progress\s*[:=]\s*(\d+))");
- QRegularExpressionMatch match = progressRegExp.match(trimmedOutput);
- if (match.hasMatch()) {
- int progress = match.captured(1).toInt();
- QString status = trimmedOutput;
- if (status.contains("progress", Qt::CaseInsensitive)) {
- status = status.replace(progressRegExp, "").trimmed();
- if (status.isEmpty()) {
- status = QString("转换中 %1%").arg(progress);
- }
+ if (output.trimmed().isEmpty()) {
+ return;
+ }
+
+ LogManager::instance()->log(output);
+
+ const QString text = output.trimmed();
+
+ // 计算本次输出对应的目标进度;-1 表示本次输出不含可识别的进度标记
+ int targetProgress = -1;
+ QString status;
+
+ // 1) 优先解析工具显式输出的 progress:xx / progress=xx
+ static const QRegularExpression progressRegExp(
+ R"(progress\s*[:=]\s*(\d+))", QRegularExpression::CaseInsensitiveOption);
+ QRegularExpressionMatch pm = progressRegExp.match(text);
+ if (pm.hasMatch()) {
+ targetProgress = pm.captured(1).toInt();
+ status = QString("转换中 %1%").arg(targetProgress);
+ } else {
+ // 2) 解析并行翻译进度 [ n/m](翻译阶段映射到 55~78)
+ // 注意时间戳 [2026-07-17 ...] 不含 '/' 不会误匹配
+ static const QRegularExpression stepRegExp(R"(\[\s*(\d+)\s*/\s*(\d+)\s*\])");
+ QRegularExpressionMatch stepMatch;
+ auto it = stepRegExp.globalMatch(text);
+ while (it.hasNext()) {
+ stepMatch = it.next(); // 取本段输出里最后一个 [n/m]
+ }
+ if (stepMatch.hasMatch()) {
+ int cur = stepMatch.captured(1).toInt();
+ int tot = stepMatch.captured(2).toInt();
+ if (tot > 0) {
+ targetProgress = 55 + (cur * 23) / tot;
+ status = QString("正在翻译文件 %1/%2").arg(cur).arg(tot);
}
- if (m_batchConvertPage) {
- m_batchConvertPage->onProgressUpdate(progress, status);
- }
- } else {
- if (m_batchConvertPage) {
- QString currentFunction;
- QRegularExpression funcRegExp(R"(([\w_]+))");
- QRegularExpressionMatch funcMatch = funcRegExp.match(trimmedOutput);
- if (funcMatch.hasMatch()) {
- currentFunction = funcMatch.captured(1);
- }
-
- if (!currentFunction.isEmpty() && m_totalFunctions > 0) {
- int index = m_functionList.indexOf(currentFunction);
- if (index >= 0 && index >= m_currentFunctionIndex) {
- m_currentFunctionIndex = index + 1;
- int progress = 20 + (m_currentFunctionIndex * 80) / m_totalFunctions;
- QString status = QString("正在转换: %1").arg(currentFunction);
- m_batchConvertPage->onProgressUpdate(progress, status);
- return;
+ }
+
+ // 3) 解析流水线阶段标记(关键字命中取最大值,保证进度稳定推进)
+ if (targetProgress < 0) {
+ struct Marker { const char* key; int prog; const char* label; };
+ static const Marker markers[] = {
+ {"开始加载数据", 25, "正在加载数据..."},
+ {"配置文件加载完成", 30, "配置文件加载完成"},
+ {"元数据加载完成", 35, "元数据加载完成"},
+ {"全部数据加载完成", 42, "数据加载完成"},
+ {"数据初始化完成", 45, "数据初始化完成"},
+ {"校验完成", 52, "数据校验完成"},
+ {"翻译完成", 80, "代码翻译完成"},
+ {"后处理完成", 90, "后处理完成"},
+ {"输出完成", 96, "结果输出完成"},
+ {"转换流水线执行成功", 98, "转换流水线执行成功"},
+ {"转码完成", 98, "转码完成"},
+ };
+ for (const auto &m : markers) {
+ if (text.contains(QString::fromUtf8(m.key))) {
+ if (m.prog > targetProgress) {
+ targetProgress = m.prog;
+ status = QString::fromUtf8(m.label);
}
}
-
- int currentProgress = 20;
- if (trimmedOutput.contains("开始转换", Qt::CaseInsensitive)) {
- currentProgress = 25;
- } else if (trimmedOutput.contains("处理文件", Qt::CaseInsensitive)) {
- currentProgress = 30;
- } else if (trimmedOutput.contains("生成代码", Qt::CaseInsensitive)) {
- currentProgress = 75;
- } else if (trimmedOutput.contains("完成", Qt::CaseInsensitive)) {
- currentProgress = 95;
- }
- m_batchConvertPage->onProgressUpdate(currentProgress, trimmedOutput);
}
}
}
+
+ if (!m_batchConvertPage) {
+ return;
+ }
+
+ // 进度单调递增:只有更大的进度才会推进进度条,避免"一下长一下短"
+ if (targetProgress > m_lastProgress) {
+ m_lastProgress = targetProgress;
+ }
+
+ // 状态文字:仅展示已识别的处理阶段;未识别的日志行不显示(保持上一次状态)
+ // 空 status 表示当前行无有效状态,BatchConvertPage 会保留上一次文字
+ m_batchConvertPage->onProgressUpdate(m_lastProgress, status);
}
void MainWindow::onPythonRunnerError(const QString &error)
diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h
index deec0c4..7c8b991 100644
--- a/src/mainwindow/mainwindow.h
+++ b/src/mainwindow/mainwindow.h
@@ -6,6 +6,7 @@
#include
#include "src/pythonrunner/PythonRunner.h"
#include "src/pages/batchconvert/batchconvertpage.h"
+#include "src/pages/codesearch/codesearchpage.h"
#include "src/pages/functionsearch/functionsearchpage.h"
#include "src/pages/functionsearch/uf20functionsearchpage.h"
#include "src/pages/functionsearch/uft3functionsearchpage.h"
@@ -30,6 +31,7 @@ private:
void initContent();
QWidget* createBatchConvertPage();
+ QWidget* createCodeSearchPage();
QWidget* createFunctionSearchPage();
QWidget* createUF20FunctionSearchPage();
QWidget* createUFT3FunctionSearchPage();
@@ -58,9 +60,11 @@ private:
ElaToolButton* m_navigationButton;
bool m_isNavigationExpanded;
BatchConvertPage *m_batchConvertPage;
+ CodeSearchPage *_codeSearchPage;
int m_totalFunctions;
int m_currentFunctionIndex;
+ int m_lastProgress; // 已上报的最大进度,保证进度单调递增,避免进度条来回跳动
QStringList m_functionList;
QString m_helpKey;
QString m_aboutKey;
diff --git a/src/metadataupdate/metadataprocessor.cpp b/src/metadataupdate/metadataprocessor.cpp
index 003e6fb..2c176cf 100644
--- a/src/metadataupdate/metadataprocessor.cpp
+++ b/src/metadataupdate/metadataprocessor.cpp
@@ -13,13 +13,20 @@
#include "utils/configmanager.h"
#include
#include
+#include
#include
#include
+#include
MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent)
{
}
+void MetadataProcessor::setForceFull(bool force)
+{
+ m_forceFull = force;
+}
+
void MetadataProcessor::initConfig(const QString& basePath)
{
m_basePath = basePath;
@@ -181,107 +188,194 @@ void MetadataProcessor::setBasePath(const QString& path)
m_basePath = path;
}
+QJsonObject MetadataProcessor::interfaceInfoToUF2Json(const InterfaceInfo& info)
+{
+ QJsonObject interfaceJson;
+ interfaceJson["cname"] = info.cname;
+ interfaceJson["eName"] = info.eName;
+ interfaceJson["functionNo"] = info.functionNo;
+ interfaceJson["flag"] = info.flag;
+ interfaceJson["path"] = info.path;
+ interfaceJson["code"] = info.code;
+ interfaceJson["moudle"] = info.moudle;
+ interfaceJson["moudleEName"] = info.moudleEName;
+ interfaceJson["serverType"] = info.serverType;
+ interfaceJson["returnResultSet"] = info.returnResultSet;
+ interfaceJson["needTransMonitor"] = info.needTransMonitor;
+ interfaceJson["checkLicence"] = info.checkLicence;
+
+ QJsonObject inputFieldsJson;
+ for (const QString& fieldName : info.inputFields.keys()) {
+ const FieldInfo& field = info.inputFields[fieldName];
+ QJsonObject fieldJson;
+ fieldJson["name"] = field.name;
+ fieldJson["flag"] = field.flag;
+ fieldJson["desc"] = field.desc;
+ inputFieldsJson[fieldName] = fieldJson;
+ }
+ interfaceJson["inputFileds"] = inputFieldsJson;
+
+ QJsonObject outputFieldsJson;
+ for (const QString& fieldName : info.outputFields.keys()) {
+ const FieldInfo& field = info.outputFields[fieldName];
+ QJsonObject fieldJson;
+ fieldJson["name"] = field.name;
+ fieldJson["flag"] = field.flag;
+ fieldJson["desc"] = field.desc;
+ outputFieldsJson[fieldName] = fieldJson;
+ }
+ interfaceJson["outputFileds"] = outputFieldsJson;
+
+ QJsonObject variableFieldsJson;
+ for (const QString& fieldName : info.variableFields.keys()) {
+ const FieldInfo& field = info.variableFields[fieldName];
+ QJsonObject fieldJson;
+ fieldJson["name"] = field.name;
+ fieldJson["cname"] = field.cname;
+ fieldJson["hsType"] = field.hsType;
+ fieldJson["desc"] = field.desc;
+ variableFieldsJson[fieldName] = fieldJson;
+ }
+ interfaceJson["variableFileds"] = variableFieldsJson;
+
+ return interfaceJson;
+}
+
+QJsonObject MetadataProcessor::readCache()
+{
+ QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json");
+ if (!QFile::exists(cachePath)) {
+ return QJsonObject();
+ }
+ return readJson(cachePath);
+}
+
+bool MetadataProcessor::writeCache(const QJsonObject& cache)
+{
+ QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json");
+ return writeJson(cachePath, cache);
+}
+
bool MetadataProcessor::processUf2Interfaces(const QString& basePath)
{
LogManager::instance()->log("========== 开始处理 UF2.0 接口 ==========");
LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath));
-
- QString configPath = QDir(m_uf2touft3Path).filePath("config.ini");
- LogManager::instance()->log(QString("配置文件路径: %1").arg(configPath));
-
+
QStringList completeNameList = {"module.xml", ".project", "冲突检测.wlua"};
QStringList containNameList = {".vbusiness", "/公共资源/", "/数据库/", "/脚本/", "/通用数据/", "/.svn/"};
-
+
QMap filterDict;
filterDict["completeNameList"] = completeNameList;
filterDict["containNameList"] = containNameList;
-
+
QString dirPath = m_uf20Path;
QString ufAcct20ProjectDir = m_ufAcct20Path;
QString uftDbSett = m_uftDbSettPath;
-
+
QString supportUftdbSett = m_supportUftdbSett;
if (supportUftdbSett == "0") {
uftDbSett.clear();
}
-
+
LogManager::instance()->log(QString("UF20 项目路径: %1").arg(dirPath));
LogManager::instance()->log(QString("UF20 会计项目路径: %1").arg(ufAcct20ProjectDir));
LogManager::instance()->log(QString("UFT 数据库设置路径: %1").arg(uftDbSett));
-
+
QString custJsonPath = QDir(m_uf2touft3Path).filePath("cust.json");
- LogManager::instance()->log(QString("客户配置文件路径: %1").arg(custJsonPath));
QJsonObject custJson = readJson(custJsonPath);
-
- Uf2Interface uf2;
- QMap resultMap = uf2.scanDir(dirPath, ufAcct20ProjectDir, uftDbSett, filterDict, custJson);
-
- QJsonObject resultJson;
- for (const QString& key : resultMap.keys()) {
- const InterfaceInfo& info = resultMap[key];
-
- QJsonObject interfaceJson;
- interfaceJson["cname"] = info.cname;
- interfaceJson["eName"] = info.eName;
- interfaceJson["functionNo"] = info.functionNo;
- interfaceJson["flag"] = info.flag;
- interfaceJson["path"] = info.path;
- interfaceJson["code"] = info.code;
- interfaceJson["moudle"] = info.moudle;
- interfaceJson["moudleEName"] = info.moudleEName;
- interfaceJson["serverType"] = info.serverType;
- interfaceJson["returnResultSet"] = info.returnResultSet;
- interfaceJson["needTransMonitor"] = info.needTransMonitor;
- interfaceJson["checkLicence"] = info.checkLicence;
-
- QJsonObject inputFieldsJson;
- for (const QString& fieldName : info.inputFields.keys()) {
- const FieldInfo& field = info.inputFields[fieldName];
- QJsonObject fieldJson;
- fieldJson["name"] = field.name;
- fieldJson["flag"] = field.flag;
- fieldJson["desc"] = field.desc;
- inputFieldsJson[fieldName] = fieldJson;
- }
- interfaceJson["inputFileds"] = inputFieldsJson;
-
- QJsonObject outputFieldsJson;
- for (const QString& fieldName : info.outputFields.keys()) {
- const FieldInfo& field = info.outputFields[fieldName];
- QJsonObject fieldJson;
- fieldJson["name"] = field.name;
- fieldJson["flag"] = field.flag;
- fieldJson["desc"] = field.desc;
- outputFieldsJson[fieldName] = fieldJson;
- }
- interfaceJson["outputFileds"] = outputFieldsJson;
-
- QJsonObject variableFieldsJson;
- for (const QString& fieldName : info.variableFields.keys()) {
- const FieldInfo& field = info.variableFields[fieldName];
- QJsonObject fieldJson;
- fieldJson["name"] = field.name;
- fieldJson["cname"] = field.cname;
- fieldJson["hsType"] = field.hsType;
- fieldJson["desc"] = field.desc;
- variableFieldsJson[fieldName] = fieldJson;
- }
- interfaceJson["variableFileds"] = variableFieldsJson;
-
- resultJson[key] = interfaceJson;
- }
-
+
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2.json");
-
+
+ Uf2Interface uf2;
+ QJsonObject oldJson = readJson(outputPath);
+ QJsonObject oldCache = readCache();
+
+ // 仅收集文件路径与修改时间(不解析内容),用于增量判定
+ QList entries;
+ entries << uf2.collectFiles(dirPath, "trade", filterDict);
+ if (!ufAcct20ProjectDir.isEmpty()) entries << uf2.collectFiles(ufAcct20ProjectDir, "acct", filterDict);
+ if (!uftDbSett.isEmpty()) entries << uf2.collectFiles(uftDbSett, "dbsett", filterDict);
+
+ QMap moduleCodeMap = uf2.buildModuleCodeMap(dirPath, ufAcct20ProjectDir, uftDbSett, custJson);
+
+ bool full = m_forceFull || oldJson.isEmpty()
+ || !oldCache.contains("uf2") || oldCache["uf2"].toObject().isEmpty();
+
+ QJsonObject resultJson;
+ QJsonObject newUf2Cache;
+
+ if (full) {
+ LogManager::instance()->log("UF2.0 全量扫描模式");
+ QElapsedTimer timer;
+ timer.start();
+
+ QMap mtimeByPath;
+ for (const Uf2FileEntry& e : entries) mtimeByPath[e.path] = e.mtime;
+
+ QMap resultMap = uf2.scanDir(dirPath, ufAcct20ProjectDir, uftDbSett, filterDict, custJson);
+ for (const QString& key : resultMap.keys()) {
+ const InterfaceInfo& info = resultMap[key];
+ resultJson[key] = interfaceInfoToUF2Json(info);
+ QJsonObject rec;
+ rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0));
+ rec["key"] = key;
+ newUf2Cache[info.path] = rec;
+ }
+
+ LogManager::instance()->log(QString("UF2.0 全量扫描耗时: %1ms").arg(timer.elapsed()));
+ } else {
+ LogManager::instance()->log("UF2.0 增量扫描模式");
+ QElapsedTimer timer;
+ timer.start();
+ QJsonObject oldUf2 = oldCache["uf2"].toObject();
+ QSet currentPaths;
+ resultJson = oldJson; // 以旧结果为基准,仅更新变化的文件
+
+ for (const Uf2FileEntry& e : entries) {
+ currentPaths.insert(e.path);
+ QJsonObject rec = oldUf2.value(e.path).toObject();
+ bool changed = rec.isEmpty() || rec["mtime"].toString().toLongLong() != e.mtime;
+ if (changed) {
+ InterfaceInfo info = uf2.assemble(e.path, e.serverType, moduleCodeMap);
+ QString newKey = info.cname;
+ QString oldKey = rec["key"].toString();
+ if (!oldKey.isEmpty() && oldKey != newKey && resultJson.contains(oldKey)) {
+ resultJson.remove(oldKey);
+ }
+ resultJson[newKey] = interfaceInfoToUF2Json(info);
+ QJsonObject crec;
+ crec["mtime"] = QString::number(e.mtime);
+ crec["key"] = newKey;
+ newUf2Cache[e.path] = crec;
+ } else {
+ newUf2Cache[e.path] = rec; // 未变化:直接沿用旧缓存记录
+ }
+ }
+
+ // 删除已从磁盘移除的文件对应的条目
+ for (auto it = oldUf2.begin(); it != oldUf2.end(); ++it) {
+ if (!currentPaths.contains(it.key())) {
+ QString oldKey = it.value().toObject()["key"].toString();
+ if (!oldKey.isEmpty() && resultJson.contains(oldKey)) {
+ resultJson.remove(oldKey);
+ }
+ }
+ }
+
+ LogManager::instance()->log(QString("UF2.0 增量扫描耗时: %1ms").arg(timer.elapsed()));
+ }
+
if (!writeJson(outputPath, resultJson)) {
return false;
}
-
- LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath));
- if (!resultMap.isEmpty()) {
- QString firstKey = resultMap.keys().first();
+
+ QJsonObject merged = oldCache;
+ merged["uf2"] = newUf2Cache;
+ if (!writeCache(merged)) {
+ LogManager::instance()->logWarning("UF2.0 缓存写入失败(不影响本次结果)");
}
-
+
+ LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath));
return true;
}
@@ -289,62 +383,148 @@ bool MetadataProcessor::processUft3Interfaces(const QString& basePath)
{
LogManager::instance()->log("========== 开始处理 UFT3.0 接口 ==========");
LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath));
-
- QString configPath = QDir(m_uf2touft3Path).filePath("config.ini");
- LogManager::instance()->log(QString("配置文件路径: %1").arg(configPath));
-
+
QString moduleGenerationCheck = m_moduleGenerationCheck;
LogManager::instance()->log(QString("模块生成检查: %1").arg(moduleGenerationCheck));
-
+
QStringList completeNameList = {"module.xml", "project.xml", "冲突检测.wlua"};
QStringList containNameList = {".extinterface", ".uftatomfunction", ".uftatomservice", ".uftfunction", ".uftservice"};
-
+
QMap filterDict;
filterDict["completeNameList"] = completeNameList;
filterDict["containNameList"] = containNameList;
-
+
QString dirPath = m_uft30Path;
LogManager::instance()->log(QString("UFT30 项目路径: %1").arg(dirPath));
-
- Uft3Interface uft3;
- auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck);
-
- QMap resultMap = result.first;
- QMap transCodeMap = result.second;
-
-
- QJsonObject resultJson;
- for (const QString& key : resultMap.keys()) {
- const InterfaceInfo& info = resultMap[key];
- resultJson[key] = interfaceInfoToUFT3Json(info);
- }
-
+
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3.json");
-
+ QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json");
+
+ Uft3Interface uft3;
+ QJsonObject oldMainJson = readJson(outputPath);
+ QJsonObject oldTransJson = readJson(transOutputPath);
+ QJsonObject oldCache = readCache();
+
+ QList entries = uft3.collectFiles(dirPath, filterDict, moduleGenerationCheck);
+
+ bool full = m_forceFull || oldMainJson.isEmpty()
+ || !oldCache.contains("uft3") || oldCache["uft3"].toObject().isEmpty();
+
+ QJsonObject resultJson;
+ QJsonObject transCodeJson;
+ QJsonObject newUft3Cache;
+
+ if (full) {
+ LogManager::instance()->log("UFT3.0 全量扫描模式");
+ QElapsedTimer timer;
+ timer.start();
+
+ QMap mtimeByPath;
+ for (const Uft3FileEntry& e : entries) mtimeByPath[e.path] = e.mtime;
+
+ auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck);
+ QMap resultMap = result.first;
+ QMap transCodeMap = result.second;
+
+ for (const QString& key : resultMap.keys()) {
+ const InterfaceInfo& info = resultMap[key];
+ resultJson[key] = interfaceInfoToUFT3Json(info);
+ QJsonObject rec;
+ rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0));
+ rec["key"] = key;
+ rec["trans"] = false;
+ newUft3Cache[info.path] = rec;
+ }
+ for (const QString& key : transCodeMap.keys()) {
+ const InterfaceInfo& info = transCodeMap[key];
+ transCodeJson[key] = interfaceInfoToUFT3Json(info);
+ QJsonObject rec;
+ rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0));
+ rec["key"] = key;
+ rec["trans"] = true;
+ newUft3Cache[info.path] = rec;
+ }
+
+ LogManager::instance()->log(QString("UFT3.0 全量扫描耗时: %1ms").arg(timer.elapsed()));
+ } else {
+ LogManager::instance()->log("UFT3.0 增量扫描模式");
+ QElapsedTimer timer;
+ timer.start();
+ QJsonObject oldUft3 = oldCache["uft3"].toObject();
+ QSet currentPaths;
+ resultJson = oldMainJson; // 以旧结果为基准
+ transCodeJson = oldTransJson;
+
+ for (const Uft3FileEntry& e : entries) {
+ currentPaths.insert(e.path);
+ QJsonObject rec = oldUft3.value(e.path).toObject();
+ bool changed = rec.isEmpty() || rec["mtime"].toString().toLongLong() != e.mtime;
+ if (changed) {
+ InterfaceInfo info = uft3.assemble(e.path);
+ QString newKey = info.cname;
+ QString oldKey = rec["key"].toString();
+ bool oldTrans = rec["trans"].toBool();
+ bool newTrans = e.isTrans;
+ // 若文件归属(主接口/交易码)或名称发生变化,先移除旧条目
+ if (!oldKey.isEmpty()) {
+ if (oldTrans) {
+ if (transCodeJson.contains(oldKey)) transCodeJson.remove(oldKey);
+ } else {
+ if (resultJson.contains(oldKey)) resultJson.remove(oldKey);
+ }
+ }
+ QJsonObject val = interfaceInfoToUFT3Json(info);
+ if (newTrans) transCodeJson[newKey] = val;
+ else resultJson[newKey] = val;
+ QJsonObject crec;
+ crec["mtime"] = QString::number(e.mtime);
+ crec["key"] = newKey;
+ crec["trans"] = newTrans;
+ newUft3Cache[e.path] = crec;
+ } else {
+ newUft3Cache[e.path] = rec;
+ }
+ }
+
+ // 删除已从磁盘移除的文件对应的条目
+ for (auto it = oldUft3.begin(); it != oldUft3.end(); ++it) {
+ if (!currentPaths.contains(it.key())) {
+ QJsonObject rec = it.value().toObject();
+ QString oldKey = rec["key"].toString();
+ bool oldTrans = rec["trans"].toBool();
+ if (oldTrans) {
+ if (transCodeJson.contains(oldKey)) transCodeJson.remove(oldKey);
+ } else {
+ if (resultJson.contains(oldKey)) resultJson.remove(oldKey);
+ }
+ }
+ }
+
+ LogManager::instance()->log(QString("UFT3.0 增量扫描耗时: %1ms").arg(timer.elapsed()));
+ }
+
if (!writeJson(outputPath, resultJson)) {
return false;
}
-
- QJsonObject transCodeJson;
- for (const QString& key : transCodeMap.keys()) {
- const InterfaceInfo& info = transCodeMap[key];
- transCodeJson[key] = interfaceInfoToUFT3Json(info);
- }
-
- QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json");
-
if (!writeJson(transOutputPath, transCodeJson)) {
return false;
}
-
+
+ QJsonObject merged = oldCache;
+ merged["uft3"] = newUft3Cache;
+ if (!writeCache(merged)) {
+ LogManager::instance()->logWarning("UFT3.0 缓存写入失败(不影响本次结果)");
+ }
+
LogManager::instance()->log(QString("UFT3.0 接口处理完成,结果已保存到: %1").arg(outputPath));
-
return true;
}
bool MetadataProcessor::processUf2DataType()
{
LogManager::instance()->log("========== 开始处理 UF2.0 数据类型 ==========");
+ QElapsedTimer timer;
+ timer.start();
QString dirPath = m_uf20Path;
@@ -364,10 +544,11 @@ bool MetadataProcessor::processUf2DataType()
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json");
if (!writeJson(outputPath, resultJson)) {
+ LogManager::instance()->log(QString("UF2.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
- LogManager::instance()->log(QString("UF2.0 数据类型处理完成,结果已保存到: %1").arg(outputPath));
+ LogManager::instance()->log(QString("UF2.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
return true;
}
@@ -375,6 +556,8 @@ bool MetadataProcessor::processUf2DataType()
bool MetadataProcessor::processUf2StdFields()
{
LogManager::instance()->log("========== 开始处理 UF2.0 标准字段 ==========");
+ QElapsedTimer timer;
+ timer.start();
QString dirPath = m_uf20Path;
@@ -406,10 +589,11 @@ bool MetadataProcessor::processUf2StdFields()
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2stdfield.json");
if (!writeJson(outputPath, resultJson)) {
+ LogManager::instance()->log(QString("UF2.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
- LogManager::instance()->log(QString("UF2.0 标准字段处理完成,结果已保存到: %1").arg(outputPath));
+ LogManager::instance()->log(QString("UF2.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
return true;
}
@@ -417,6 +601,8 @@ bool MetadataProcessor::processUf2StdFields()
bool MetadataProcessor::processUf2ErrNumber()
{
LogManager::instance()->log("========== 开始处理 UF2.0 标准错误号 ==========");
+ QElapsedTimer timer;
+ timer.start();
QString dirPath = m_uf20Path;
@@ -436,10 +622,11 @@ bool MetadataProcessor::processUf2ErrNumber()
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json");
if (!writeJson(outputPath, resultJson)) {
+ LogManager::instance()->log(QString("UF2.0 标准错误号处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
- LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,结果已保存到: %1").arg(outputPath));
+ LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
return true;
}
@@ -447,6 +634,8 @@ bool MetadataProcessor::processUf2ErrNumber()
bool MetadataProcessor::processUft3DataType()
{
LogManager::instance()->log("========== 开始处理 UFT3.0 数据类型 ==========");
+ QElapsedTimer timer;
+ timer.start();
QString dirPath = m_uft30PubPath;
@@ -466,10 +655,11 @@ bool MetadataProcessor::processUft3DataType()
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json");
if (!writeJson(outputPath, resultJson)) {
+ LogManager::instance()->log(QString("UFT3.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
- LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,结果已保存到: %1").arg(outputPath));
+ LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
return true;
}
@@ -477,6 +667,8 @@ bool MetadataProcessor::processUft3DataType()
bool MetadataProcessor::processUft3StdFields()
{
LogManager::instance()->log("========== 开始处理 UFT3.0 标准字段 ==========");
+ QElapsedTimer timer;
+ timer.start();
QString dirPath = m_uft30PubPath;
@@ -508,10 +700,11 @@ bool MetadataProcessor::processUft3StdFields()
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3stdfields.json");
if (!writeJson(outputPath, resultJson)) {
+ LogManager::instance()->log(QString("UFT3.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
- LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,结果已保存到: %1").arg(outputPath));
+ LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
return true;
}
@@ -519,6 +712,8 @@ bool MetadataProcessor::processUft3StdFields()
bool MetadataProcessor::processUft3Table()
{
LogManager::instance()->log("========== 开始处理 UFT3.0 表结构 ==========");
+ QElapsedTimer timer;
+ timer.start();
QString dirPath = m_uft30PubPath;
@@ -541,10 +736,11 @@ bool MetadataProcessor::processUft3Table()
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3Table.json");
if (!writeJson(outputPath, resultJson)) {
+ LogManager::instance()->log(QString("UFT3.0 表结构处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
- LogManager::instance()->log(QString("UFT3.0 表结构处理完成,结果已保存到: %1").arg(outputPath));
+ LogManager::instance()->log(QString("UFT3.0 表结构处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
return true;
}
\ No newline at end of file
diff --git a/src/metadataupdate/metadataprocessor.h b/src/metadataupdate/metadataprocessor.h
index affacd5..07da95c 100644
--- a/src/metadataupdate/metadataprocessor.h
+++ b/src/metadataupdate/metadataprocessor.h
@@ -17,7 +17,9 @@ public:
bool processUf2Interfaces(const QString& basePath);
bool processUft3Interfaces(const QString& basePath);
-
+
+ void setForceFull(bool force); // 强制全量更新(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为)
+
bool processUf2StdFields();
bool processUf2DataType();
bool processUf2ErrNumber();
@@ -38,6 +40,11 @@ private:
bool writeJson(const QString& filePath, const QJsonObject& jsonObject);
QJsonObject readJson(const QString& filePath);
QJsonObject interfaceInfoToUFT3Json(const InterfaceInfo& info);
+ QJsonObject interfaceInfoToUF2Json(const InterfaceInfo& info);
+
+ // 轻量增量缓存(仅记录 文件路径->修改时间(mtime)+该文件在 JSON 中的 key)
+ QJsonObject readCache();
+ bool writeCache(const QJsonObject& cache);
QString m_basePath;
QString m_uf2touft3Path;
@@ -51,6 +58,8 @@ private:
QString m_supportUftdbSett;
QString m_moduleGenerationCheck;
+
+ bool m_forceFull = false; // 强制全量更新标志(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为)
};
#endif // METADATAPROCESSOR_H
\ No newline at end of file
diff --git a/src/metadataupdate/uf2interface.cpp b/src/metadataupdate/uf2interface.cpp
index dd8956a..d6d382f 100644
--- a/src/metadataupdate/uf2interface.cpp
+++ b/src/metadataupdate/uf2interface.cpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
Uf2Interface::Uf2Interface(QObject *parent) : QObject(parent)
{
@@ -294,3 +295,97 @@ QMap Uf2Interface::scanDir(const QString& dirPath,
return resultMap;
}
+
+QMap Uf2Interface::buildModuleCodeMap(const QString& dirPath,
+ const QString& ufAcct20ProjectDir,
+ const QString& uftDbSett,
+ const QJsonObject& custJson)
+{
+ QMap moduleCodeMap;
+ moduleCodeMap["转融通"] = "ref";
+
+ // 预提取cust.json中的模块英文名映射
+ QMap custMoudleENameMap;
+ if (!custJson.isEmpty() && custJson.contains("moudleEName")) {
+ QJsonObject moudleENameObj = custJson["moudleEName"].toObject();
+ for (const QString& key : moudleENameObj.keys()) {
+ custMoudleENameMap[key] = moudleENameObj[key].toString();
+ }
+ }
+
+ std::function scanModuleXmlRecursive = [&](const QString& path) {
+ if (path.isEmpty()) return;
+ QDir dir(path);
+ QStringList filters;
+ filters << "module.xml";
+ QFileInfoList fileList = dir.entryInfoList(filters, QDir::Files);
+ for (const QFileInfo& fileInfo : fileList) {
+ QString filePath = fileInfo.absoluteFilePath();
+ filePath.replace('\\', '/');
+ if (filePath.contains("/数据库/")) continue;
+ QString modName = fileInfo.dir().dirName();
+ QString modEName = getModuleEName(filePath);
+ if (filePath.contains("外部接口(不用编译)") && modEName.trimmed().isEmpty()) {
+ if (custMoudleENameMap.contains(modName)) modEName = custMoudleENameMap[modName];
+ }
+ moduleCodeMap[modName] = modEName;
+ }
+ QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QFileInfo& dirInfo : dirList) scanModuleXmlRecursive(dirInfo.absoluteFilePath());
+ };
+ scanModuleXmlRecursive(dirPath);
+ scanModuleXmlRecursive(ufAcct20ProjectDir);
+ scanModuleXmlRecursive(uftDbSett);
+ return moduleCodeMap;
+}
+
+InterfaceInfo Uf2Interface::assemble(const QString& filePath,
+ const QString& serverType,
+ const QMap& moduleCodeMap)
+{
+ QString sFilePath = filePath;
+ sFilePath.replace('\\', '/');
+ int lastSlash = sFilePath.lastIndexOf('/');
+ QString pathWithoutFile = sFilePath.left(lastSlash);
+ int secondLastSlash = pathWithoutFile.lastIndexOf('/');
+ QString pathWithoutLastDir = pathWithoutFile.left(secondLastSlash);
+ int thirdLastSlash = pathWithoutLastDir.lastIndexOf('/');
+ QString modName = pathWithoutLastDir.mid(thirdLastSlash + 1);
+
+ InterfaceInfo interfaceInfo = loadInterface(filePath);
+ interfaceInfo.moudle = modName;
+ interfaceInfo.moudleEName = moduleCodeMap.value(modName);
+ interfaceInfo.serverType = serverType;
+ return interfaceInfo;
+}
+
+QList Uf2Interface::collectFiles(const QString& dirPath,
+ const QString& serverType,
+ const QMap& fileFilterDict)
+{
+ QList result;
+ QStringList completeNameList = fileFilterDict.value("completeNameList");
+ QStringList containNameList = fileFilterDict.value("containNameList");
+
+ std::function scanInterfaceFilesRecursive =
+ [&](const QString& path, const QString& st) {
+ if (path.isEmpty()) return;
+ QDir dir(path);
+ if (!dir.exists()) return;
+ QFileInfoList fileList = dir.entryInfoList(QStringList() << "*.*", QDir::Files);
+ for (const QFileInfo& fileInfo : fileList) {
+ QString filePath = fileInfo.absoluteFilePath();
+ filePath.replace('\\', '/');
+ if (shouldFilter(filePath, completeNameList, containNameList)) continue;
+ Uf2FileEntry entry;
+ entry.path = filePath;
+ entry.serverType = st;
+ entry.mtime = fileInfo.lastModified().toMSecsSinceEpoch();
+ result.append(entry);
+ }
+ QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QFileInfo& dirInfo : dirList) scanInterfaceFilesRecursive(dirInfo.absoluteFilePath(), st);
+ };
+ scanInterfaceFilesRecursive(dirPath, serverType);
+ return result;
+}
diff --git a/src/metadataupdate/uf2interface.h b/src/metadataupdate/uf2interface.h
index e118abb..c5790f1 100644
--- a/src/metadataupdate/uf2interface.h
+++ b/src/metadataupdate/uf2interface.h
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
#include
@@ -41,6 +42,13 @@ struct InterfaceInfo {
QMap internalFields;
};
+// 仅记录文件路径、归属类型、修改时间,用于轻量增量缓存
+struct Uf2FileEntry {
+ QString path;
+ QString serverType; // trade / acct / dbsett
+ qint64 mtime = 0;
+};
+
class Uf2Interface : public QObject
{
Q_OBJECT
@@ -54,6 +62,18 @@ public:
const QMap& fileFilterDict,
const QJsonObject& custJson = QJsonObject());
+ // 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致)
+ QMap buildModuleCodeMap(const QString& dirPath,
+ const QString& ufAcct20ProjectDir,
+ const QString& uftDbSett,
+ const QJsonObject& custJson = QJsonObject());
+ InterfaceInfo assemble(const QString& filePath,
+ const QString& serverType,
+ const QMap& moduleCodeMap);
+ QList collectFiles(const QString& dirPath,
+ const QString& serverType,
+ const QMap& fileFilterDict);
+
private:
QString getModuleEName(const QString& moduleXmlPath);
bool shouldFilter(const QString& filePath,
diff --git a/src/metadataupdate/uft3interface.cpp b/src/metadataupdate/uft3interface.cpp
index 2ee07d7..33e7e73 100644
--- a/src/metadataupdate/uft3interface.cpp
+++ b/src/metadataupdate/uft3interface.cpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
Uft3Interface::Uft3Interface(QObject *parent) : QObject(parent)
{
@@ -243,3 +244,57 @@ Uft3Interface::scanDir(const QString& dirPath,
return qMakePair(resultMap, transCodeMap);
}
+
+InterfaceInfo Uft3Interface::assemble(const QString& filePath)
+{
+ QFileInfo fileInfo(filePath);
+ fileInfo.setFile(fileInfo.absoluteFilePath()); // 标准化路径
+ QString modName = fileInfo.dir().dirName();
+ InterfaceInfo interfaceInfo = loadInterface(filePath);
+ interfaceInfo.moudle = modName;
+ return interfaceInfo;
+}
+
+QList Uft3Interface::collectFiles(const QString& dirPath,
+ const QMap& fileFilterDict,
+ const QString& moduleGenerationCheck)
+{
+ QList result;
+ QStringList completeNameList = fileFilterDict.value("completeNameList");
+ QStringList containNameList = fileFilterDict.value("containNameList");
+
+ std::function scanInterfaceFilesRecursive =
+ [&](const QString& path) {
+ if (path.isEmpty())
+ return;
+
+ QDir dir(path);
+ QFileInfoList fileList = dir.entryInfoList(QStringList() << "*.*", QDir::Files);
+ for (const QFileInfo& fileInfo : fileList) {
+ QString filePath = fileInfo.absoluteFilePath();
+ filePath.replace('\\', '/');
+
+ if (shouldFilter(filePath, dirPath, completeNameList, containNameList, moduleGenerationCheck)) {
+ continue;
+ }
+
+ bool isTransCode = (filePath.contains("/cbptrans/") || filePath.contains("/mgrcbptrans/")
+ || filePath.contains("/converttrans/") || filePath.contains("/ses/"))
+ && !filePath.contains("/cbptrans/cbptranspub/");
+
+ Uft3FileEntry entry;
+ entry.path = filePath;
+ entry.mtime = fileInfo.lastModified().toMSecsSinceEpoch();
+ entry.isTrans = isTransCode;
+ result.append(entry);
+ }
+
+ QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QFileInfo& dirInfo : dirList) {
+ scanInterfaceFilesRecursive(dirInfo.absoluteFilePath());
+ }
+ };
+
+ scanInterfaceFilesRecursive(dirPath);
+ return result;
+}
diff --git a/src/metadataupdate/uft3interface.h b/src/metadataupdate/uft3interface.h
index b7bb69a..9e056eb 100644
--- a/src/metadataupdate/uft3interface.h
+++ b/src/metadataupdate/uft3interface.h
@@ -6,9 +6,17 @@
#include
#include
#include
+#include
#include "uf2interface.h"
+// 仅记录文件路径、修改时间、是否属于交易码,用于轻量增量缓存
+struct Uft3FileEntry {
+ QString path;
+ qint64 mtime = 0;
+ bool isTrans = false;
+};
+
class Uft3Interface : public QObject
{
Q_OBJECT
@@ -21,6 +29,12 @@ public:
const QMap& fileFilterDict,
const QString& moduleGenerationCheck);
+ // 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致)
+ InterfaceInfo assemble(const QString& filePath);
+ QList collectFiles(const QString& dirPath,
+ const QMap& fileFilterDict,
+ const QString& moduleGenerationCheck);
+
private:
bool shouldFilter(const QString& filePath,
const QString& dirPath,
diff --git a/src/pages/about/aboutdialog.cpp b/src/pages/about/aboutdialog.cpp
index a65818a..a602b4e 100644
--- a/src/pages/about/aboutdialog.cpp
+++ b/src/pages/about/aboutdialog.cpp
@@ -12,7 +12,7 @@
AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
{
- setFixedSize(354, 180);
+ setFixedSize(360, 210);
setWindowTitle("关于");
setStandardButtons(QDialogButtonBox::Ok);
@@ -34,7 +34,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
QFont nameTextFont = nameText->font();
nameTextFont.setWeight(QFont::Bold);
nameText->setFont(nameTextFont);
- nameText->setTextPixelSize(17);
+ nameText->setTextPixelSize(15);
nameText->setText("UFT30 ChangeCode");
auto* versionText = new ElaText(this);
diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp
index 8a2a142..79b8eb1 100644
--- a/src/pages/batchconvert/batchconvertpage.cpp
+++ b/src/pages/batchconvert/batchconvertpage.cpp
@@ -151,12 +151,15 @@ void BatchConvertPage::initUI()
void BatchConvertPage::onProgressUpdate(int progress, const QString& status)
{
m_progressBar->setValue(progress);
-
- QString displayStatus = status;
- if (displayStatus.length() > 80) {
- displayStatus = displayStatus.left(80) + "...";
+
+ // 仅当传入有效状态文字时才更新标签,避免无意义的日志行覆盖当前处理状态
+ if (!status.isEmpty()) {
+ QString displayStatus = status;
+ if (displayStatus.length() > 80) {
+ displayStatus = displayStatus.left(80) + "...";
+ }
+ m_progressLabel->setText(displayStatus);
}
- m_progressLabel->setText(displayStatus);
}
bool BatchConvertPage::checkFunctionExists(const QString &funcName)
diff --git a/src/pages/codesearch/codesearchpage.cpp b/src/pages/codesearch/codesearchpage.cpp
new file mode 100644
index 0000000..89e75a2
--- /dev/null
+++ b/src/pages/codesearch/codesearchpage.cpp
@@ -0,0 +1,1012 @@
+#include "codesearchpage.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "ElaText.h"
+#include "utils/datacache.h"
+#include "utils/logmanager.h"
+#include "utils/Constants.h"
+#include
+
+namespace {
+ QStringList loadConvertFunctions()
+ {
+ QStringList result;
+ const QString custPath = Constants::custJson();
+
+ QFile file(custPath);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return result;
+ }
+
+ QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
+ if (!doc.isObject()) {
+ return result;
+ }
+
+ QJsonObject convertObj = doc.object().value("convert").toObject();
+ QJsonArray dirArray = convertObj.value("dir").toArray();
+ QJsonArray extArray = convertObj.value("ext").toArray();
+
+ QStringList extList;
+ for (const QJsonValue &v : extArray) {
+ extList.append(v.toString());
+ }
+
+ for (const QJsonValue &v : dirArray) {
+ QString name = v.toString();
+ for (const QString &ext : extList) {
+ if (name.endsWith(ext)) {
+ name.chop(ext.length());
+ break;
+ }
+ }
+ if (!name.isEmpty()) {
+ result.append(name);
+ }
+ }
+
+ return result;
+ }
+
+ // "原代码已支持"行高亮角色。Ela 主题会覆盖 item 的 BackgroundRole,
+ // 因此用自定义 delegate 强制绘制绿色背景,保证两种模式都生效。
+ const int RowHighlightRole = Qt::UserRole + 100;
+ // 当前查询的函数/功能行高亮角色:强制绘制蓝色背景,突出查询节点
+ const int RowHighlightBlueRole = Qt::UserRole + 101;
+
+ class CallTreeDelegate : public QStyledItemDelegate
+ {
+ public:
+ explicit CallTreeDelegate(QObject *parent = nullptr)
+ : QStyledItemDelegate(parent) {}
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override
+ {
+ const bool blue = index.data(RowHighlightBlueRole).toBool();
+ const bool green = index.data(RowHighlightRole).toBool();
+ if (blue || green) {
+ // 高亮行:直接绘制背景 + 深字(支持节点为叶子,无展开箭头,可整格重绘)
+ const QColor bg = blue ? QColor(189, 215, 238) : QColor(198, 239, 206);
+ const QColor fg = blue ? QColor(10, 60, 120) : QColor(0, 97, 0);
+ painter->save();
+ painter->fillRect(option.rect, bg);
+ painter->setPen(fg);
+ // 第0列(函数名)左对齐并留出图标位置;"目录"、"模块"列居中
+ QRect textRect = option.rect;
+ int align = Qt::AlignCenter;
+ if (index.column() == 0) {
+ textRect = option.rect.adjusted(
+ option.decorationSize.width() + 4, 0, 0, 0);
+ align = Qt::AlignLeft | Qt::AlignVCenter;
+ }
+ painter->drawText(textRect, align,
+ index.data(Qt::DisplayRole).toString());
+ painter->restore();
+ return;
+ }
+ QStyledItemDelegate::paint(painter, option, index);
+ }
+ };
+}
+
+CodeSearchPage::CodeSearchPage(QWidget *parent)
+ : QWidget(parent)
+ , m_analyzers{new CallChainAnalyzer(this), new CallChainAnalyzer(this)}
+{
+ // 两种模式的代码检索路径:优先读取 change_code.ini 配置;为空(键不存在或值为空)时回退到默认路径
+ // (不使用 config.ini,避免程序更新被覆盖导致配置丢失)
+ const QString iniPath = Constants::changeCodeIni();
+ QSettings cc(iniPath, QSettings::IniFormat);
+ QString convertPath = cc.value(Constants::CodeSearch::ConvertPathKey).toString();
+ QString searchPath = cc.value(Constants::CodeSearch::SearchPathKey).toString();
+ if (convertPath.isEmpty()) {
+ convertPath = Constants::changeCodeDir();
+ }
+ if (searchPath.isEmpty()) {
+ searchPath = Constants::changeCodeDir();
+ }
+ m_pathByMode[ConvertMode] = convertPath;
+ m_pathByMode[SearchMode] = searchPath;
+ // 将(可能为默认的)路径写回配置,确保空配置也会被落盘,下次直接命中
+ cc.setValue(Constants::CodeSearch::ConvertPathKey, QDir::fromNativeSeparators(convertPath));
+ cc.setValue(Constants::CodeSearch::SearchPathKey, QDir::fromNativeSeparators(searchPath));
+ cc.sync();
+ initUI();
+}
+
+CallChainAnalyzer *CodeSearchPage::currentAnalyzer() const
+{
+ return m_analyzers[m_mode];
+}
+
+CodeSearchPage::~CodeSearchPage() = default;
+
+void CodeSearchPage::initUI()
+{
+ QVBoxLayout *mainLayout = new QVBoxLayout(this);
+ mainLayout->setSpacing(10);
+ mainLayout->setContentsMargins(15, 15, 15, 15);
+
+ // 业务模式切换:转码业务 / 非转码业务(Tab 形式),置于路径选择栏上方
+ QHBoxLayout *modeLayout = new QHBoxLayout();
+ m_modePivot = new ElaPivot();
+ m_modePivot->appendPivot("转码业务");
+ m_modePivot->appendPivot("非转码业务");
+ m_modePivot->setTextPixelSize(14);
+ m_modePivot->setCurrentIndex(0);
+ connect(m_modePivot, &ElaPivot::pivotClicked, this, &CodeSearchPage::onPivotClicked);
+ modeLayout->addWidget(m_modePivot);
+ modeLayout->addStretch();
+
+ mainLayout->addLayout(modeLayout);
+
+ // 代码检索路径选择栏(位于模式切换下方,两种模式共用)
+ QHBoxLayout *pathLayout = new QHBoxLayout();
+ pathLayout->setSpacing(8);
+
+ ElaText *pathLabel = new ElaText("代码检索路径:");
+ pathLabel->setTextPixelSize(14);
+ pathLayout->addWidget(pathLabel);
+
+ m_pathEdit = new ElaLineEdit();
+ m_pathEdit->setText(m_pathByMode[ConvertMode]);
+ m_pathEdit->setPlaceholderText("请选择需要查询的目录");
+ pathLayout->addWidget(m_pathEdit, 1);
+
+ m_browseBtn = new ElaPushButton("浏览");
+ connect(m_browseBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onBrowsePath);
+ pathLayout->addWidget(m_browseBtn);
+
+ m_refreshBtn = new ElaPushButton("刷新索引");
+ connect(m_refreshBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onRefreshIndex);
+ pathLayout->addWidget(m_refreshBtn);
+
+ // 转码业务中"导出调用文件"紧随"刷新索引"同一行(非转码业务隐藏)
+ m_importBtn = new ElaPushButton("导出调用文件");
+ m_importBtn->setMinimumWidth(140);
+ m_importBtn->setMinimumHeight(38);
+ {
+ QFont f = m_importBtn->font();
+ f.setPointSize(11);
+ m_importBtn->setFont(f);
+ }
+ m_importBtn->setEnabled(false);
+ connect(m_importBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onImportChainFiles);
+ pathLayout->addWidget(m_importBtn);
+
+ mainLayout->addLayout(pathLayout);
+
+ // 非转码业务查询行:函数名/功能号 + 查询链路 + 全部展开/收起(同一行)
+ QHBoxLayout *queryLayout = new QHBoxLayout();
+ queryLayout->setSpacing(8);
+
+ m_queryCombo = new ElaComboBox();
+ m_queryCombo->addItem("函数名");
+ m_queryCombo->addItem("功能号");
+ m_queryCombo->setCurrentText("函数名");
+ queryLayout->addWidget(m_queryCombo);
+
+ m_queryEdit = new ElaLineEdit();
+ m_queryEdit->setPlaceholderText("输入函数名或功能号,回车查询");
+ queryLayout->addWidget(m_queryEdit, 1);
+
+ m_queryBtn = new ElaPushButton("查询链路");
+ connect(m_queryBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onQueryByInput);
+ connect(m_queryEdit, &ElaLineEdit::returnPressed, this, &CodeSearchPage::onQueryByInput);
+ queryLayout->addWidget(m_queryBtn);
+
+ // 非转码业务:导出调用链路(查询生成链路后可用)
+ m_exportChainBtn = new ElaPushButton("导出调用链路");
+ m_exportChainBtn->setMinimumWidth(140);
+ m_exportChainBtn->setMinimumHeight(38);
+ {
+ QFont f = m_exportChainBtn->font();
+ f.setPointSize(11);
+ m_exportChainBtn->setFont(f);
+ }
+ m_exportChainBtn->setEnabled(false);
+ connect(m_exportChainBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onExportChain);
+ queryLayout->addWidget(m_exportChainBtn);
+
+ mainLayout->addLayout(queryLayout);
+
+ // 左右分割:左侧函数列表(转码业务),右侧调用链路树
+ QSplitter *splitter = new QSplitter(Qt::Horizontal);
+
+ // 左侧函数列表(仅转码业务)
+ m_leftPanel = new QWidget();
+ QVBoxLayout *leftLayout = new QVBoxLayout(m_leftPanel);
+ leftLayout->setContentsMargins(0, 0, 0, 0);
+ leftLayout->addWidget(new ElaText("业务转码函数列表"));
+
+ m_funcModel = new QStandardItemModel(this);
+ m_funcModel->setHorizontalHeaderLabels(QStringList() << "函数名");
+
+ m_funcTable = new ElaTableView();
+ m_funcTable->setModel(m_funcModel);
+ m_funcTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_funcTable->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_funcTable->horizontalHeader()->setStretchLastSection(true);
+ m_funcTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ connect(m_funcTable, &ElaTableView::doubleClicked, this, &CodeSearchPage::onFunctionDoubleClicked);
+ leftLayout->addWidget(m_funcTable);
+
+ // 右侧调用链路树(每个模式各自独立的树,互不干扰)
+ QWidget *rightPanel = new QWidget();
+ QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel);
+ rightLayout->setContentsMargins(0, 0, 0, 0);
+
+ // "调用链路"标题行:左侧标题,右侧"全部展开/全部折叠"图标按钮(两种模式共用,居右)
+ QHBoxLayout *chainTitleLayout = new QHBoxLayout();
+ chainTitleLayout->setSpacing(8);
+ ElaText *chainTitle = new ElaText("调用链路");
+ chainTitle->setTextPixelSize(14);
+ chainTitleLayout->addWidget(chainTitle);
+ chainTitleLayout->addStretch();
+
+ m_expandBtn = new IconButton(":/resources/images/expand_all.png", "全部展开", this);
+ connect(m_expandBtn, &IconButton::clicked, this, &CodeSearchPage::onExpandAll);
+ chainTitleLayout->addWidget(m_expandBtn);
+
+ m_collapseBtn = new IconButton(":/resources/images/collapse_all.png", "全部折叠", this);
+ connect(m_collapseBtn, &IconButton::clicked, this, &CodeSearchPage::onCollapseAll);
+ chainTitleLayout->addWidget(m_collapseBtn);
+
+ rightLayout->addLayout(chainTitleLayout);
+
+ for (int i = 0; i < 2; ++i) {
+ m_treeModel[i] = new QStandardItemModel(this);
+ m_treeModel[i]->setHorizontalHeaderLabels(QStringList() << "函数名" << "目录" << "模块");
+ // 表头列名居中显示
+ for (int c = 0; c < 3; ++c) {
+ m_treeModel[i]->setHeaderData(c, Qt::Horizontal,
+ QVariant(Qt::AlignCenter), Qt::TextAlignmentRole);
+ }
+
+ m_callTree[i] = new ElaTreeView();
+ m_callTree[i]->setModel(m_treeModel[i]);
+ m_callTree[i]->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ // 自定义 delegate:强制绘制"原代码已支持"行的绿色背景(绕过 Ela 主题覆盖)
+ m_callTree[i]->setItemDelegate(new CallTreeDelegate(m_callTree[i]));
+ // 初始按内容自适应列宽,之后允许用户拖拽自定义调整列宽
+ m_callTree[i]->header()->setSectionResizeMode(0, QHeaderView::Interactive);
+ m_callTree[i]->header()->setSectionResizeMode(1, QHeaderView::Interactive);
+ m_callTree[i]->header()->setSectionResizeMode(2, QHeaderView::Interactive);
+ m_callTree[i]->header()->setStretchLastSection(false);
+ m_callTree[i]->setAlternatingRowColors(true);
+ rightLayout->addWidget(m_callTree[i]);
+ }
+
+ splitter->addWidget(m_leftPanel);
+ splitter->addWidget(rightPanel);
+ splitter->setSizes(QList() << 300 << 700);
+ mainLayout->addWidget(splitter, 1);
+
+ // 底部静态提示(仿 UF20 功能查询页最下方提示)
+ QHBoxLayout *tipLayout = new QHBoxLayout();
+ m_bottomTip = new QLabel("💡 双击左侧业务转码函数查看调用链路");
+ m_bottomTip->setStyleSheet("color: #666; font-size: 13px;");
+ tipLayout->addWidget(m_bottomTip);
+ tipLayout->addStretch();
+ mainLayout->addLayout(tipLayout);
+
+ // 初始化为转码业务模式
+ applyMode();
+}
+
+void CodeSearchPage::applyMode()
+{
+ const bool convert = (m_mode == ConvertMode);
+
+ // 转码业务:左侧函数列表 + 导出调用文件
+ // 非转码业务:手动查询行 + 全部展开/收起
+ m_leftPanel->setVisible(convert);
+ m_importBtn->setVisible(convert);
+ m_funcTable->setVisible(convert);
+
+ // 各自显示各自的调用链结果
+ m_callTree[ConvertMode]->setVisible(convert);
+ m_callTree[SearchMode]->setVisible(!convert);
+
+ m_queryCombo->setVisible(!convert);
+ m_queryEdit->setVisible(!convert);
+ m_queryBtn->setVisible(!convert);
+ m_exportChainBtn->setVisible(!convert);
+
+ // 底部静态提示:转码业务显示操作提示,非转码业务隐藏
+ m_bottomTip->setText("💡 双击左侧业务转码函数查看调用链路");
+ m_bottomTip->setVisible(convert);
+}
+
+void CodeSearchPage::onPivotClicked(int index)
+{
+ // 0:转码业务;1:非转码业务
+ m_mode = (index == 1) ? SearchMode : ConvertMode;
+ // 切回该页面对应的检索路径,并按需自动构建索引
+ m_pathEdit->setText(m_pathByMode[m_mode]);
+ applyMode();
+ autoBuildIndexIfNeeded();
+}
+
+void CodeSearchPage::showEvent(QShowEvent *event)
+{
+ QWidget::showEvent(event);
+
+ // 每次显示页面都重新读取 cust.json 的 convert 列表,
+ // 以同步「业务转码」中新增/删除的转码函数
+ loadFunctionList();
+
+ // 默认自动构建索引(仅首次/路径未变更且未索引时);
+ // 若用户手动改过路径,则需其点"刷新索引"手动触发
+ autoBuildIndexIfNeeded();
+}
+
+void CodeSearchPage::loadFunctionList()
+{
+ m_funcModel->removeRows(0, m_funcModel->rowCount());
+
+ const QStringList functions = loadConvertFunctions();
+ for (const QString &func : functions) {
+ QStandardItem *item = new QStandardItem(func);
+ item->setToolTip(func);
+ item->setData(func, Qt::UserRole + 1);
+ m_funcModel->appendRow(item);
+ }
+
+ // 业务转码函数列表加载完成(结果直接体现在左侧表格,无需顶部状态栏提示)
+}
+
+void CodeSearchPage::onBrowsePath()
+{
+ const QString dir = QFileDialog::getExistingDirectory(this, "选择代码检索目录", m_pathEdit->text());
+ if (!dir.isEmpty()) {
+ m_pathByMode[m_mode] = QDir::toNativeSeparators(dir);
+ m_pathEdit->setText(m_pathByMode[m_mode]);
+ // 用户重新选择路径:标记需要手动刷新索引
+ m_userReSelectedByMode[m_mode] = true;
+ // 持久化保存该模式路径,下次启动恢复
+ savePathsToConfig();
+ // 顶部状态栏已移除,改用弹窗提示用户需手动刷新索引
+ QMessageBox::information(this, "提示", "路径已更改,请点击\"刷新索引\"重新加载代码目录");
+ }
+}
+
+void CodeSearchPage::onRefreshIndex()
+{
+ const QString path = m_pathEdit->text().trimmed();
+ if (path.isEmpty()) {
+ QMessageBox::warning(this, "提示", "请先选择代码检索路径");
+ return;
+ }
+ if (m_indexing) {
+ return;
+ }
+
+ m_pathByMode[m_mode] = path;
+
+ // 刷新索引时使用的路径也持久化保存,下次启动恢复
+ savePathsToConfig();
+
+ CallChainAnalyzer *ca = currentAnalyzer();
+ ca->setRootPath(path);
+
+ // 后台线程构建,期间显示进度框,避免界面卡死
+ m_indexing = true;
+ runIndexBuild(ca, "正在刷新代码索引,请稍候...",
+ [this, path](bool ok, const QString &message) {
+ m_indexing = false;
+ if (ok) {
+ currentAnalyzer()->saveToCache(); // 写入缓存,后续直接读取
+ m_indexedPathByMode[m_mode] = path;
+ m_userReSelectedByMode[m_mode] = false;
+ LogManager::instance()->log(QString("[代码检索] %1").arg(message));
+ QMessageBox::information(this, "刷新成功",
+ message.isEmpty() ? QString("索引刷新成功") : message);
+ } else {
+ m_indexedPathByMode[m_mode] = path; // 避免重复自动重试
+ QMessageBox::warning(this, "刷新失败",
+ message.isEmpty() ? QString("索引刷新失败") : message);
+ }
+ });
+}
+
+void CodeSearchPage::savePathsToConfig()
+{
+ // 路径持久化到独立的 change_code.ini(与 config.ini 分离,避免更新被覆盖)
+ const QString iniPath = Constants::changeCodeIni();
+ QSettings cc(iniPath, QSettings::IniFormat);
+ // 统一用正斜杠写入,避免 Ini 格式把反斜杠当转义符导致出现多余反斜杠
+ cc.setValue(Constants::CodeSearch::ConvertPathKey, QDir::fromNativeSeparators(m_pathByMode[ConvertMode]));
+ cc.setValue(Constants::CodeSearch::SearchPathKey, QDir::fromNativeSeparators(m_pathByMode[SearchMode]));
+ cc.sync();
+}
+
+void CodeSearchPage::refreshIndexSilent()
+{
+ if (m_indexing) {
+ return;
+ }
+ // 收集已配置路径的模式,后台线程依次异步刷新,避免阻塞转换完成后的界面
+ QList modes;
+ for (int mode = 0; mode < 2; ++mode) {
+ if (!m_pathByMode[mode].isEmpty() && m_analyzers[mode]) {
+ modes.append(mode);
+ }
+ }
+ if (modes.isEmpty()) {
+ return;
+ }
+ m_indexing = true;
+ buildSilentChain(modes, 0);
+}
+
+void CodeSearchPage::buildSilentChain(const QList &modes, int idx)
+{
+ if (idx >= modes.size()) {
+ m_indexing = false;
+ return;
+ }
+ const int mode = modes[idx];
+ const QString path = m_pathByMode[mode];
+ CallChainAnalyzer *ca = m_analyzers[mode];
+ ca->setRootPath(path);
+
+ // 后台线程构建,复用进度框;无弹窗(静默),完成后继续下一模式
+ runIndexBuild(ca, "正在刷新代码索引,请稍候...",
+ [this, modes, idx, mode, path, ca](bool ok, const QString &message) {
+ if (ok) {
+ ca->saveToCache();
+ m_indexedPathByMode[mode] = path;
+ m_userReSelectedByMode[mode] = false;
+ LogManager::instance()->log(QString("[代码检索] 转码后自动刷新索引成功 %1: %2")
+ .arg(mode == 0 ? "转码业务" : "非转码业务", message));
+ } else {
+ m_indexedPathByMode[mode] = path;
+ LogManager::instance()->logWarning(QString("[代码检索] 转码后自动刷新索引失败 %1: %2")
+ .arg(mode == 0 ? "转码业务" : "非转码业务", message));
+ }
+ buildSilentChain(modes, idx + 1);
+ });
+}
+
+void CodeSearchPage::onFunctionDoubleClicked(const QModelIndex &index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ if (!currentAnalyzer()->isIndexed()) {
+ QMessageBox::information(this, "提示", "请先点击\"刷新索引\"加载代码目录");
+ return;
+ }
+
+ const QString displayName = m_funcModel->item(index.row())->data(Qt::UserRole + 1).toString();
+ runQuery(displayName);
+}
+
+void CodeSearchPage::populateTree(const QList &rootNodes)
+{
+ QStandardItemModel *model = m_treeModel[m_mode];
+ ElaTreeView *tree = m_callTree[m_mode];
+
+ model->removeRows(0, model->rowCount());
+
+ QStandardItem *rootItem = model->invisibleRootItem();
+ for (const CallChainAnalyzer::TreeNode &r : rootNodes) {
+ addTreeItem(rootItem, r);
+ }
+
+ tree->expandToDepth(0);
+
+ // 渲染后根据实际内容(含字体度量)自适应列宽,用户仍可自行拖拽调整
+ autoResizeColumns(tree);
+}
+
+void CodeSearchPage::autoResizeColumns(ElaTreeView *tree)
+{
+ // 由于 resizeColumnToContents 只测量"可见"项,被折叠的深层节点不会参与计算,
+ // 导致第3级及以后的函数名/目录/模块列宽无法自适应。
+ // 这里改为遍历整个模型(含未展开节点),按字体度量取各列最大内容宽度。
+ QStandardItemModel *model = qobject_cast(tree->model());
+ if (!model) {
+ return;
+ }
+
+ QFontMetrics fm(tree->font());
+ int colMax[3] = {0, 0, 0};
+
+ // 递归遍历所有行(level 为相对调用树根的层级,用于累加缩进占位)
+ std::function walk = [&](QStandardItem *parent, int level) {
+ for (int r = 0; r < parent->rowCount(); ++r) {
+ QStandardItem *cells[3] = {
+ parent->child(r, 0),
+ parent->child(r, 1),
+ parent->child(r, 2)
+ };
+ // 第0列(函数名)需加上各级缩进与展开箭头占位
+ const int indent = tree->indentation() * (level + 1);
+ if (cells[0]) {
+ colMax[0] = qMax(colMax[0],
+ fm.horizontalAdvance(cells[0]->text()) + indent);
+ }
+ if (cells[1]) {
+ colMax[1] = qMax(colMax[1], fm.horizontalAdvance(cells[1]->text()));
+ }
+ if (cells[2]) {
+ colMax[2] = qMax(colMax[2], fm.horizontalAdvance(cells[2]->text()));
+ }
+ // 以第0列单元格为父,继续向下遍历更深层
+ if (cells[0]) {
+ walk(cells[0], level + 1);
+ }
+ }
+ };
+ walk(model->invisibleRootItem(), 0);
+
+ // 表头列名宽度也应纳入,避免列名比数据更宽时被截断
+ for (int c = 0; c < 3; ++c) {
+ const QString header = model->headerData(c, Qt::Horizontal,
+ Qt::DisplayRole).toString();
+ colMax[c] = qMax(colMax[c], fm.horizontalAdvance(header));
+ }
+
+ // 补充边距避免文字紧贴边界,用户仍可自行拖拽调整
+ for (int c = 0; c < 3; ++c) {
+ tree->setColumnWidth(c, colMax[c] + 16);
+ }
+}
+
+void CodeSearchPage::addTreeItem(QStandardItem *parentItem, const CallChainAnalyzer::TreeNode &node)
+{
+ QString nameText = node.displayName;
+ if (node.state == "notfound") {
+ nameText += " [未找到]";
+ } else if (node.state == "cycle") {
+ nameText += " [循环调用]";
+ } else if (node.state == "depthlimit") {
+ nameText += " [层级截断]";
+ } else if (node.state == "supported") {
+ // 未在转码代码中找到:说明该函数无需转码,原代码已支持
+ // 功能号通过 uft3 功能查询(按功能名称)获取
+ const QString funcNo = lookupUFT3FunctionNo(node.displayName);
+ nameText += " [原代码已支持]";
+ if (!funcNo.isEmpty()) {
+ nameText = QString("%1-%2").arg(funcNo, nameText);
+ }
+ }
+
+ QStandardItem *nameItem = new QStandardItem(nameText);
+ nameItem->setToolTip(nameText);
+ nameItem->setEditable(false);
+
+ QStandardItem *dirItem = new QStandardItem();
+ QStandardItem *moduleItem = new QStandardItem();
+
+ // "目录"、"模块"列数据居中显示
+ dirItem->setTextAlignment(Qt::AlignCenter);
+ moduleItem->setTextAlignment(Qt::AlignCenter);
+
+ if (node.info) {
+ dirItem->setText(node.info->relDir);
+ dirItem->setToolTip(node.info->filePath);
+ moduleItem->setText(node.info->module);
+ }
+
+ // 不同状态用不同颜色提示
+ if (node.state == "notfound") {
+ nameItem->setForeground(QBrush(QColor(180, 0, 0)));
+ } else if (node.state == "cycle") {
+ nameItem->setForeground(QBrush(QColor(180, 100, 0)));
+ } else if (node.state == "supported") {
+ // 原代码已支持:整行绿色背景高亮;功能号置于函数名前,格式:功能号-函数名[原代码已支持]
+ const QString funcNo = lookupUFT3FunctionNo(node.displayName);
+ if (!funcNo.isEmpty()) {
+ nameText = QString("%1-%2[原代码已支持]").arg(funcNo, node.displayName);
+ } else {
+ nameText += " [原代码已支持]";
+ }
+ const QBrush greenBg(QColor(198, 239, 206));
+ nameItem->setBackground(greenBg);
+ dirItem->setBackground(greenBg);
+ moduleItem->setBackground(greenBg);
+ nameItem->setForeground(QBrush(QColor(0, 97, 0)));
+ // 标记高亮角色,交由自定义 delegate 强制绘制绿色背景
+ nameItem->setData(true, RowHighlightRole);
+ dirItem->setData(true, RowHighlightRole);
+ moduleItem->setData(true, RowHighlightRole);
+ }
+
+ dirItem->setEditable(false);
+ moduleItem->setEditable(false);
+
+ // 查询的目标函数/功能:整行蓝色背景高亮,突出当前查询节点(仅对非"原代码已支持"节点生效)
+ if (!m_highlightName.isEmpty() && node.displayName == m_highlightName
+ && node.state != "supported") {
+ const QBrush blueBg(QColor(189, 215, 238));
+ nameItem->setBackground(blueBg);
+ dirItem->setBackground(blueBg);
+ moduleItem->setBackground(blueBg);
+ nameItem->setForeground(QBrush(QColor(10, 60, 120)));
+ // 标记蓝色高亮角色,交由自定义 delegate 强制绘制蓝色背景
+ nameItem->setData(true, RowHighlightBlueRole);
+ dirItem->setData(true, RowHighlightBlueRole);
+ moduleItem->setData(true, RowHighlightBlueRole);
+ }
+
+ QList row;
+ row << nameItem << dirItem << moduleItem;
+ parentItem->appendRow(row);
+
+ for (const CallChainAnalyzer::TreeNode &child : node.children) {
+ addTreeItem(nameItem, child);
+ }
+}
+
+QString CodeSearchPage::lookupUFT3FunctionNo(const QString &funcName)
+{
+ DataCache *cache = DataCache::instance();
+ if (!cache || !cache->isDataLoaded()) {
+ return QString();
+ }
+
+ // 先按"功能名称"精确匹配(cname)
+ QList results = cache->searchUFT3Functions(funcName, "cname", true);
+ if (results.isEmpty()) {
+ // 退化为模糊匹配(cname/英文名/功能号)
+ results = cache->searchUFT3Functions(funcName, "", false);
+ }
+ if (!results.isEmpty()) {
+ return results.first().functionNo;
+ }
+ return QString();
+}
+
+QString CodeSearchPage::resolveChineseName(const QString &displayName) const
+{
+ // 支持两种格式:
+ // 1) LS_xxx / LF_xxx(cust.json 中去掉后缀后的格式)
+ // 2) objectId-LS_xxx / objectId-LF_xxx(带功能号前缀的格式)
+ const int dashIndex = displayName.indexOf('-');
+ if (dashIndex > 0) {
+ return displayName.mid(dashIndex + 1);
+ }
+ return displayName;
+}
+
+void CodeSearchPage::autoBuildIndexIfNeeded()
+{
+ const QString path = m_pathByMode[m_mode].trimmed();
+ // 仅当:路径有效、尚未索引、非用户手动改过路径、且未正在构建时,才默认自动加载
+ if (path.isEmpty() || m_userReSelectedByMode[m_mode]
+ || currentAnalyzer()->isIndexed() || m_indexing) {
+ return;
+ }
+
+ CallChainAnalyzer *ca = currentAnalyzer();
+ ca->setRootPath(path);
+
+ // 1) 优先从磁盘缓存读取(程序重启 / 页面切换时秒开,无需重新扫描目录)
+ QString message;
+ if (ca->loadFromCache(path, &message)) {
+ m_indexedPathByMode[m_mode] = path;
+ LogManager::instance()->log(QString("[代码检索-缓存] %1").arg(message));
+ return;
+ }
+
+ // 2) 缓存不存在或已失效:后台线程扫描目录构建索引,期间显示进度框,避免界面卡死;
+ // 构建完成后写入缓存,供下次直接读取。
+ m_indexing = true;
+ runIndexBuild(ca, "正在构建代码索引,请稍候...",
+ [this, path](bool ok, const QString &message) {
+ m_indexing = false;
+ if (ok) {
+ currentAnalyzer()->saveToCache();
+ m_indexedPathByMode[m_mode] = path;
+ LogManager::instance()->log(QString("[代码检索-自动] %1").arg(message));
+ } else {
+ m_indexedPathByMode[m_mode] = path; // 标记已尝试,避免每次显示重复重试
+ }
+ });
+}
+
+void CodeSearchPage::runIndexBuild(CallChainAnalyzer *ca, const QString &title,
+ std::function onDone)
+{
+ // 模态进度框(不确定进度),后台构建期间维持界面响应
+ auto *dlg = new QProgressDialog(title, QString(), 0, 0, this);
+ dlg->setWindowTitle("请稍候");
+ dlg->setWindowModality(Qt::WindowModal);
+ dlg->setCancelButton(nullptr);
+ dlg->setRange(0, 0); // 不确定进度
+ dlg->setMinimumDuration(0);
+ dlg->show();
+
+ using BuildResult = QPair;
+ auto *watcher = new QFutureWatcher(this);
+ connect(watcher, &QFutureWatcher::finished, this, [=]() {
+ const BuildResult r = watcher->result();
+ watcher->deleteLater();
+ dlg->close();
+ dlg->deleteLater();
+ if (onDone) {
+ onDone(r.first, r.second);
+ }
+ });
+
+ // 在后台线程执行实际扫描,避免阻塞 UI 线程
+ auto future = QtConcurrent::run([=]() -> BuildResult {
+ QString msg;
+ const bool ok = ca->buildIndex(&msg);
+ return {ok, msg};
+ });
+ watcher->setFuture(future);
+}
+
+void CodeSearchPage::onQueryByInput()
+{
+ const QString raw = m_queryEdit->text().trimmed();
+ if (raw.isEmpty()) {
+ QMessageBox::information(this, "提示", "请输入要查询的内容");
+ return;
+ }
+
+ QString target = raw;
+ if (m_queryCombo->currentText() == "功能号") {
+ // 按功能号解析为对应的函数名(英文名 eName)后再查询
+ DataCache *cache = DataCache::instance();
+ if (!cache || !cache->isDataLoaded()) {
+ QMessageBox::information(this, "提示", "功能数据未加载,无法按功能号查询");
+ return;
+ }
+ const QList results = cache->searchUFT3Functions(raw, "function_no", true);
+ if (results.isEmpty() || results.first().eName.isEmpty()) {
+ QMessageBox::information(this, "提示",
+ QString("未找到功能号:%1 对应的函数").arg(raw));
+ return;
+ }
+ target = results.first().eName;
+ }
+
+ runQuery(target);
+}
+
+void CodeSearchPage::runQuery(const QString &displayName)
+{
+ if (!currentAnalyzer()->isIndexed()) {
+ QMessageBox::information(this, "提示", "请先点击\"刷新索引\"加载代码目录");
+ return;
+ }
+
+ const QString chineseName = resolveChineseName(displayName);
+
+ // 记录当前查询的函数/功能名,供调用链树中对应行蓝色高亮。
+ // 注意:树节点 displayName 由 resolver 拼成 "objectId-chineseName" 形式,
+ // 因此这里要用与 CallChainAnalyzer::resolveNode 一致的公式还原展示名,
+ // 否则功能号查询(target=eName)或带功能号前缀的查询会匹配不上,导致目标行不高亮。
+ m_highlightName = chineseName;
+ if (QSharedPointer hi =
+ currentAnalyzer()->lookupBest(chineseName)) {
+ m_highlightName = hi->objectId.isEmpty()
+ ? hi->chineseName
+ : QString("%1-%2").arg(hi->objectId, hi->chineseName);
+ }
+
+ QApplication::setOverrideCursor(Qt::WaitCursor);
+ // buildCallTree 内部使用多策略查找,如果未找到根节点会标记为 "notfound"
+ CallChainAnalyzer::TreeNode downTree = currentAnalyzer()->buildCallTree(chineseName);
+
+ if (downTree.state != "ok") {
+ QApplication::restoreOverrideCursor();
+ QMessageBox::information(this, "未找到",
+ QString("未在索引中找到函数:%1\n请确认代码检索路径正确并已刷新索引。").arg(displayName));
+ return;
+ }
+
+ // 往前查:构造上游调用方森林(每条链以 downTree 收尾),无上游时退化为单根 downTree
+ QList roots =
+ currentAnalyzer()->buildCallerForest(chineseName, downTree);
+
+ populateTree(roots);
+ QApplication::restoreOverrideCursor();
+
+ m_callTree[m_mode]->expandToDepth(2);
+
+ // 收集调用链路文件(排除"原代码已支持"),并记录导出基准路径与查询名
+ QSet collected;
+ for (const CallChainAnalyzer::TreeNode &r : roots) {
+ collectChainFiles(r, collected);
+ }
+ m_currentChainRoots = roots;
+ m_currentChainFilePaths = collected.values();
+ m_searchRootPath = m_pathEdit->text().trimmed();
+ m_currentQueryName = displayName;
+ m_importBtn->setEnabled(!m_currentChainFilePaths.isEmpty());
+ m_exportChainBtn->setEnabled(!m_currentChainFilePaths.isEmpty());
+}
+
+void CodeSearchPage::onExpandAll()
+{
+ // 展开/收起操作当前模式对应的调用链路树
+ ElaTreeView *tree = m_callTree[m_mode];
+ tree->expandAll();
+ // 展开后内容宽度可能变化,重新自适应列宽
+ autoResizeColumns(tree);
+}
+
+void CodeSearchPage::onCollapseAll()
+{
+ m_callTree[m_mode]->collapseAll();
+}
+
+void CodeSearchPage::collectChainFiles(const CallChainAnalyzer::TreeNode &node, QSet &out)
+{
+ // 仅收集有文件且非"原代码已支持"(supported)的节点
+ if (node.info && node.state != "supported") {
+ out.insert(node.info->filePath);
+ }
+ for (const CallChainAnalyzer::TreeNode &child : node.children) {
+ collectChainFiles(child, out);
+ }
+}
+
+void CodeSearchPage::collectChainLines(const CallChainAnalyzer::TreeNode &node, int depth, QStringList &lines)
+{
+ if (depth >= 64) return; // 防无限递归
+
+ QString indent(depth * 4, QChar(' '));
+ QString stateTag;
+ if (node.state == "supported") stateTag = " [原代码已支持]";
+ else if (node.state == "notfound") stateTag = " [未找到]";
+
+ QString funcLabel = node.displayName;
+ // 为 supported / notfound 节点补充功能号前缀(若可解析且名称中尚未包含)
+ if ((node.state == "supported" || node.state == "notfound")
+ && !node.objectId.isEmpty() && !funcLabel.startsWith(node.objectId)) {
+ funcLabel = node.objectId + "-" + funcLabel;
+ }
+
+ QString line = indent;
+ if (depth > 0) line += "|- ";
+ line += funcLabel;
+ line += stateTag;
+ lines.append(line);
+
+ for (const CallChainAnalyzer::TreeNode &child : node.children) {
+ collectChainLines(child, depth + 1, lines);
+ }
+}
+
+void CodeSearchPage::onImportChainFiles()
+{
+ if (m_currentChainFilePaths.isEmpty()) {
+ QMessageBox::information(this, "提示", "请先双击函数生成调用链路");
+ return;
+ }
+ if (m_searchRootPath.isEmpty() || !QFile::exists(m_searchRootPath)) {
+ QMessageBox::warning(this, "提示", "无法定位代码检索路径");
+ return;
+ }
+
+ // function_output / <查询函数名> /
+ const QString outRoot = QDir(m_searchRootPath).filePath(Constants::functionOutput());
+ const QString queryDir = QDir(outRoot).filePath(sanitizeDirName(m_currentQueryName));
+
+ QDir().mkpath(queryDir);
+
+ QDir rootDir(m_searchRootPath);
+ int copied = 0;
+ QStringList failed;
+
+ for (const QString &src : m_currentChainFilePaths) {
+ QFileInfo fi(src);
+ if (!fi.exists()) {
+ failed.append(src);
+ continue;
+ }
+ // 保持相对搜索根的路径结构:b/c/d/a.function
+ const QString relPath = rootDir.relativeFilePath(src);
+ const QString dest = QDir(queryDir).filePath(relPath);
+ QDir().mkpath(QFileInfo(dest).absolutePath());
+ if (QFile::exists(dest) && !QFile::remove(dest)) {
+ failed.append(src);
+ continue;
+ }
+ if (QFile::copy(src, dest)) {
+ copied++;
+ } else {
+ failed.append(src);
+ }
+ }
+
+ // 生成调用链路文本文件(遍历整片森林,含往前查的上游链)
+ QStringList chainLines;
+ for (const CallChainAnalyzer::TreeNode &r : m_currentChainRoots) {
+ collectChainLines(r, 0, chainLines);
+ }
+ const QString txtPath = QDir(queryDir).filePath("_call_chain.txt");
+ QFile txtFile(txtPath);
+ if (txtFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ QTextStream ts(&txtFile);
+ for (const QString &line : chainLines) {
+ ts << line << "\n";
+ }
+ }
+
+ QString msg = QString("已将 %1 个调用文件导出到:\n%2").arg(copied).arg(QDir::toNativeSeparators(queryDir));
+ if (!failed.isEmpty()) {
+ msg += QString("\n\n%1 个文件导出失败:\n%2").arg(failed.size()).arg(failed.join("\n"));
+ }
+ QMessageBox::information(this, "导出完成", msg);
+}
+
+void CodeSearchPage::onExportChain()
+{
+ if (m_currentChainFilePaths.isEmpty()) {
+ QMessageBox::information(this, "提示", "请先查询函数生成调用链路");
+ return;
+ }
+ if (m_searchRootPath.isEmpty() || !QFile::exists(m_searchRootPath)) {
+ QMessageBox::warning(this, "提示", "无法定位代码检索路径");
+ return;
+ }
+
+ // function_output / <查询函数名> /
+ const QString outRoot = QDir(m_searchRootPath).filePath(Constants::functionOutput());
+ const QString queryDir = QDir(outRoot).filePath(sanitizeDirName(m_currentQueryName));
+ QDir().mkpath(queryDir);
+
+ // 非转码业务:仅导出调用链路文本,不复制源文件
+ QStringList chainLines;
+ for (const CallChainAnalyzer::TreeNode &r : m_currentChainRoots) {
+ collectChainLines(r, 0, chainLines);
+ }
+ const QString txtPath = QDir(queryDir).filePath("_call_chain.txt");
+ QFile txtFile(txtPath);
+ if (!txtFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ QMessageBox::warning(this, "提示", "调用链路文件写入失败");
+ return;
+ }
+ QTextStream ts(&txtFile);
+ for (const QString &line : chainLines) {
+ ts << line << "\n";
+ }
+ txtFile.close();
+
+ QString msg = QString("已导出调用链路(%1 行)到:\n%2")
+ .arg(chainLines.size()).arg(QDir::toNativeSeparators(queryDir));
+ QMessageBox::information(this, "导出完成", msg);
+}
+
+QString CodeSearchPage::sanitizeDirName(const QString &name)
+{
+ // 去除 Windows 非法目录字符
+ QString out = name;
+ static const QRegularExpression re(R"([\\/:*?"<>|])");
+ out.replace(re, "_");
+ out = out.trimmed();
+ if (out.isEmpty()) {
+ out = "function";
+ }
+ return out;
+}
diff --git a/src/pages/codesearch/codesearchpage.h b/src/pages/codesearch/codesearchpage.h
new file mode 100644
index 0000000..73a6753
--- /dev/null
+++ b/src/pages/codesearch/codesearchpage.h
@@ -0,0 +1,127 @@
+#ifndef CODESEARCHPAGE_H
+#define CODESEARCHPAGE_H
+
+#include
+#include
+#include
+#include
+#include
+#include "ElaTableView.h"
+#include "ElaText.h"
+#include "ElaLineEdit.h"
+#include "ElaPushButton.h"
+#include "ElaTreeView.h"
+#include "ElaComboBox.h"
+#include "ElaPivot.h"
+#include "utils/callchainanalyzer.h"
+
+// 图标按钮(基于 QLabel):显示图标 + 悬停提示,鼠标点击触发信号
+class IconButton : public QLabel
+{
+ Q_OBJECT
+public:
+ explicit IconButton(const QString &pixmapPath, const QString &tip,
+ QWidget *parent = nullptr)
+ : QLabel(parent)
+ {
+ const QPixmap pm(pixmapPath);
+ setPixmap(pm.scaled(22, 22, Qt::KeepAspectRatio, Qt::SmoothTransformation));
+ setToolTip(tip);
+ setCursor(Qt::ArrowCursor);
+ setFixedSize(28, 28);
+ setAlignment(Qt::AlignCenter);
+ }
+
+signals:
+ void clicked();
+
+protected:
+ void mousePressEvent(QMouseEvent *event) override
+ {
+ emit clicked();
+ QLabel::mousePressEvent(event);
+ }
+};
+
+class CodeSearchPage : public QWidget
+{
+ Q_OBJECT
+
+public:
+ enum Mode { ConvertMode, SearchMode };
+
+ explicit CodeSearchPage(QWidget *parent = nullptr);
+ ~CodeSearchPage();
+
+ /// 静默刷新索引(无弹窗,转码完成后自动调用)
+ void refreshIndexSilent();
+ /// 转码后静默异步刷新:依次(后台线程)刷新各模式索引,全部完成后清除 m_indexing
+ void buildSilentChain(const QList &modes, int idx);
+
+protected:
+ void showEvent(QShowEvent *event) override;
+
+private slots:
+ void onBrowsePath();
+ void onRefreshIndex();
+ void onFunctionDoubleClicked(const QModelIndex &index);
+ void onExpandAll();
+ void onCollapseAll();
+ void onImportChainFiles();
+ void onExportChain();
+ void onQueryByInput();
+ void savePathsToConfig();
+ void onPivotClicked(int index);
+
+private:
+ void initUI();
+ void loadFunctionList();
+ void populateTree(const QList &rootNodes);
+ void autoResizeColumns(ElaTreeView *tree);
+ void addTreeItem(QStandardItem *parentItem, const CallChainAnalyzer::TreeNode &node);
+ QString resolveChineseName(const QString &displayName) const;
+ QString lookupUFT3FunctionNo(const QString &funcName);
+ void collectChainFiles(const CallChainAnalyzer::TreeNode &node, QSet &out);
+ static void collectChainLines(const CallChainAnalyzer::TreeNode &node, int depth, QStringList &lines);
+ static QString sanitizeDirName(const QString &name);
+ void runQuery(const QString &displayName);
+ void autoBuildIndexIfNeeded();
+ void applyMode();
+ CallChainAnalyzer *currentAnalyzer() const;
+
+ /// 后台线程异步构建索引(避免界面卡顿),构建期间显示进度框;完成后回调 onDone(ok, message)
+ void runIndexBuild(CallChainAnalyzer *ca, const QString &title,
+ std::function onDone);
+
+ Mode m_mode = ConvertMode;
+
+ ElaLineEdit *m_pathEdit;
+ ElaPushButton *m_browseBtn;
+ ElaPushButton *m_refreshBtn;
+ ElaPivot *m_modePivot;
+ IconButton *m_expandBtn;
+ IconButton *m_collapseBtn;
+ ElaPushButton *m_importBtn;
+ ElaPushButton *m_exportChainBtn;
+ ElaComboBox *m_queryCombo;
+ ElaLineEdit *m_queryEdit;
+ ElaPushButton *m_queryBtn;
+ ElaTableView *m_funcTable;
+ QWidget *m_leftPanel;
+ ElaTreeView *m_callTree[2];
+ QStandardItemModel *m_funcModel;
+ QStandardItemModel *m_treeModel[2];
+ QLabel *m_bottomTip; // 底部静态提示(仿 UF20 功能查询页)
+ CallChainAnalyzer *m_analyzers[2]; // 两种模式各自的分析器(独立索引)
+ QString m_pathByMode[2]; // 两种模式各自的代码检索路径
+ QString m_indexedPathByMode[2]; // 两种模式各自已构建索引的路径
+ bool m_userReSelectedByMode[2] = {false, false}; // 各自改路径后需手动刷新
+ QStringList m_currentChainFilePaths; // 当前调用链路中所有非"原代码已支持"的文件
+ QList m_currentChainRoots; // 当前调用链路森林(支持往前查),供导出文本用
+ QString m_searchRootPath; // 当前索引根路径(导出基准)
+ QString m_currentQueryName; // 当前查询的函数名称(导出目录名)
+ QString m_highlightName; // 当前查询的函数/功能名(调用链树中对应行蓝色高亮)
+ bool m_indexing = false; // 后台索引构建进行中,防止重复触发
+};
+
+#endif // CODESEARCHPAGE_H
diff --git a/src/pages/metadatupdate/metadatupdatepage.cpp b/src/pages/metadatupdate/metadatupdatepage.cpp
index 515d72c..8105da7 100644
--- a/src/pages/metadatupdate/metadatupdatepage.cpp
+++ b/src/pages/metadatupdate/metadatupdatepage.cpp
@@ -1,5 +1,6 @@
#include "metadatupdatepage.h"
#include "utils/Constants.h"
+#include "utils/logmanager.h"
#include
#include
#include
@@ -8,6 +9,7 @@
#include
#include
#include
+#include
const int UPDATE_ITEM_SHOW_COLUMN_COUNT = 3;
@@ -17,13 +19,18 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent)
initUI();
_processor = new MetadataProcessor(this);
_processor->initConfig(Constants::toolDir());
+
+ _watcher = new QFutureWatcher(this);
+ connect(_watcher, &QFutureWatcher::finished,
+ this, &MetadataUpdatePage::onWorkerFinished);
+ connect(this, &MetadataUpdatePage::sigProgress,
+ this, &MetadataUpdatePage::onUpdateProgress, Qt::QueuedConnection);
}
MetadataUpdatePage::~MetadataUpdatePage()
{
- if (_workerThread && _workerThread->isRunning()) {
- _workerThread->quit();
- _workerThread->wait();
+ if (_watcher && _watcher->isRunning()) {
+ _watcher->waitForFinished();
}
}
@@ -72,6 +79,13 @@ void MetadataUpdatePage::initUI()
gridLayout->addWidget(_uft3CancelBtn, 1, 3, 1, 1);
_uft3UpdateBtn = new ElaPushButton("更新UFT30", contentWidget);
+ _uft3UpdateBtn->setMinimumWidth(140);
+ _uft3UpdateBtn->setMinimumHeight(38);
+ {
+ QFont f = _uft3UpdateBtn->font();
+ f.setPointSize(11);
+ _uft3UpdateBtn->setFont(f);
+ }
connect(_uft3UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUFT3);
gridLayout->addWidget(_uft3UpdateBtn, 1, 4, 1, 1);
@@ -105,17 +119,42 @@ void MetadataUpdatePage::initUI()
gridLayout->addWidget(_uf2CancelBtn, 1, 8, 1, 1);
_uf2UpdateBtn = new ElaPushButton("更新UF20", contentWidget);
+ _uf2UpdateBtn->setMinimumWidth(140);
+ _uf2UpdateBtn->setMinimumHeight(38);
+ {
+ QFont f = _uf2UpdateBtn->font();
+ f.setPointSize(11);
+ _uf2UpdateBtn->setFont(f);
+ }
connect(_uf2UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUF20);
gridLayout->addWidget(_uf2UpdateBtn, 1, 9, 1, 1);
gridLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum), 0, 10, 2, 1);
+ _forceFullCheckBox = new ElaCheckBox("强制全量更新", contentWidget);
+ _forceFullCheckBox->setToolTip("勾选后本次将重新扫描并解析全部接口文件、重建增量缓存;不勾选时仅处理有变化的文件");
+ connect(_forceFullCheckBox, &QCheckBox::toggled, this, [this](bool checked){
+ if (checked) {
+ auto ret = QMessageBox::question(this, "提示",
+ "全量更新接口耗时较久,请确认是否继续?",
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if (ret != QMessageBox::Yes) {
+ _forceFullCheckBox->setChecked(false);
+ }
+ }
+ });
+
centerLayout->addWidget(contentWidget);
centerLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding));
+ // 进度标签行:左侧显示状态文字,右侧放置强制全量更新复选框
+ QHBoxLayout* processHeaderLayout = new QHBoxLayout();
_processLabel = new ElaText(this);
- centerLayout->addWidget(_processLabel);
+ _processLabel->setTextPixelSize(14);
+ processHeaderLayout->addWidget(_processLabel, 1);
+ processHeaderLayout->addWidget(_forceFullCheckBox);
+ centerLayout->addLayout(processHeaderLayout);
_progressBar = new ElaProgressBar(this);
_progressBar->setValue(0);
@@ -209,8 +248,9 @@ void MetadataUpdatePage::onUpdateUF20()
_uf2AllBtn->setEnabled(false);
_uf2CancelBtn->setEnabled(false);
_progressBar->setValue(0);
-
- updateUf2Interfaces(selectedItems);
+ _processor->setForceFull(_forceFullCheckBox->isChecked());
+
+ startUpdate(selectedItems, "UF20");
}
void MetadataUpdatePage::onUFT3SelectAll()
@@ -245,22 +285,22 @@ void MetadataUpdatePage::onUpdateUFT3()
_uft3AllBtn->setEnabled(false);
_uft3CancelBtn->setEnabled(false);
_progressBar->setValue(0);
-
- updateUft3Interfaces(selectedItems);
+ _processor->setForceFull(_forceFullCheckBox->isChecked());
+
+ startUpdate(selectedItems, "UFT30");
}
-void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items)
+UpdateResult MetadataUpdatePage::runUpdateUf2(const QStringList& items)
{
- int itemCount = items.size();
+ bool success = true;
bool interfaceUpdated = false;
-
- foreach (const QString& item, items) {
- _processLabel->setText(QString("正在更新UF20 - %1").arg(item));
- int currentIndex = items.indexOf(item);
- int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount);
- _progressBar->setValue(progress);
- QCoreApplication::processEvents();
-
+ int itemCount = items.size();
+ int idx = 0;
+
+ for (const QString& item : items) {
+ emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount),
+ QString("正在更新UF20 - %1").arg(item));
+
if (item == "标准字段") {
processUf2StdFields();
} else if (item == "数据类型") {
@@ -268,35 +308,29 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items)
} else if (item == "标准错误号") {
processUf2ErrNumber();
} else if (item == "接口数据") {
- processUf2Interface(Constants::toolDir());
+ if (!processUf2Interface(Constants::toolDir())) {
+ success = false;
+ }
interfaceUpdated = true;
}
+ ++idx;
}
-
- if (interfaceUpdated) {
- _processLabel->setText("正在更新数据缓存...");
- QCoreApplication::processEvents();
- UF2ConfigReader::reloadUF20Config();
- }
-
- _progressBar->setValue(100);
- _processLabel->setText("UF20更新完成");
-
- onUpdateFinished(true, "UF20更新完成");
+
+ // 数据缓存重载放到主线程执行(DataCache 单例非线程安全),见 onWorkerFinished
+ return UpdateResult{success, interfaceUpdated};
}
-void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
+UpdateResult MetadataUpdatePage::runUpdateUft3(const QStringList& items)
{
- int itemCount = items.size();
+ bool success = true;
bool interfaceUpdated = false;
-
- foreach (const QString& item, items) {
- _processLabel->setText(QString("正在更新UFT30 - %1").arg(item));
- int currentIndex = items.indexOf(item);
- int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount);
- _progressBar->setValue(progress);
- QCoreApplication::processEvents();
-
+ int itemCount = items.size();
+ int idx = 0;
+
+ for (const QString& item : items) {
+ emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount),
+ QString("正在更新UFT30 - %1").arg(item));
+
if (item == "标准字段") {
processUft3StdFields();
} else if (item == "数据类型") {
@@ -304,21 +338,15 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
} else if (item == "表结构") {
processUft3Table();
} else if (item == "接口数据") {
- processUft3Interface(Constants::toolDir());
+ if (!processUft3Interface(Constants::toolDir())) {
+ success = false;
+ }
interfaceUpdated = true;
}
+ ++idx;
}
-
- if (interfaceUpdated) {
- _processLabel->setText("正在更新数据缓存...");
- QCoreApplication::processEvents();
- UFT3ConfigReader::reloadUFT3Config();
- }
-
- _progressBar->setValue(100);
- _processLabel->setText("UFT30更新完成");
-
- onUpdateFinished(true, "UFT30更新完成");
+
+ return UpdateResult{success, interfaceUpdated};
}
void MetadataUpdatePage::processUf2StdFields()
@@ -336,9 +364,9 @@ void MetadataUpdatePage::processUf2ErrNumber()
_processor->processUf2ErrNumber();
}
-void MetadataUpdatePage::processUf2Interface(const QString& basePath)
+bool MetadataUpdatePage::processUf2Interface(const QString& basePath)
{
- _processor->processUf2Interfaces(basePath);
+ return _processor->processUf2Interfaces(basePath);
}
void MetadataUpdatePage::processUft3StdFields()
@@ -356,9 +384,60 @@ void MetadataUpdatePage::processUft3Table()
_processor->processUft3Table();
}
-void MetadataUpdatePage::processUft3Interface(const QString& basePath)
+bool MetadataUpdatePage::processUft3Interface(const QString& basePath)
{
- _processor->processUft3Interfaces(basePath);
+ return _processor->processUft3Interfaces(basePath);
+}
+
+void MetadataUpdatePage::startUpdate(const QStringList& items, const QString& taskName)
+{
+ m_currentTaskName = taskName;
+ _updateTimer.start();
+ _progressBar->setValue(0);
+ _processLabel->setText(QString("正在准备更新%1...").arg(taskName));
+
+ // 防御:若上一个任务仍在运行则不再启动(按钮已禁用,正常情况下不会触发)
+ if (_watcher->isRunning()) {
+ return;
+ }
+
+ if (taskName == "UF20") {
+ _watcher->setFuture(QtConcurrent::run([this, items]() {
+ return runUpdateUf2(items);
+ }));
+ } else {
+ _watcher->setFuture(QtConcurrent::run([this, items]() {
+ return runUpdateUft3(items);
+ }));
+ }
+}
+
+void MetadataUpdatePage::onWorkerFinished()
+{
+ UpdateResult r = _watcher->result();
+
+ // 计算总耗时
+ qint64 elapsedMs = _updateTimer.elapsed();
+ QString elapsedStr;
+ if (elapsedMs < 1000) {
+ elapsedStr = QString::number(elapsedMs) + "ms";
+ } else {
+ elapsedStr = QString("%1s (%2ms)").arg(elapsedMs / 1000.0, 0, 'f', 1).arg(elapsedMs);
+ }
+ LogManager::instance()->log(QString("%1 元数据更新总耗时: %2").arg(m_currentTaskName, elapsedStr));
+
+ // 数据缓存重载必须在主线程执行(DataCache 单例与静态 cnameCache 非线程安全)
+ if (r.needReload) {
+ _processLabel->setText("正在更新数据缓存...");
+ if (m_currentTaskName == "UF20") {
+ UF2ConfigReader::reloadUF20Config();
+ } else {
+ UFT3ConfigReader::reloadUFT3Config();
+ }
+ }
+
+ onUpdateFinished(r.success, QString("%1更新完成").arg(m_currentTaskName));
+ _processLabel->setText(QString("%1更新完成(总耗时: %2)").arg(m_currentTaskName, elapsedStr));
}
void MetadataUpdatePage::onUpdateProgress(int progress, const QString& message)
diff --git a/src/pages/metadatupdate/metadatupdatepage.h b/src/pages/metadatupdate/metadatupdatepage.h
index 239d27c..b9d7dfe 100644
--- a/src/pages/metadatupdate/metadatupdatepage.h
+++ b/src/pages/metadatupdate/metadatupdatepage.h
@@ -12,6 +12,14 @@
#include
#include
#include
+#include
+#include
+
+// 后台线程执行结果:success 表示整体是否成功,needReload 表示是否需要重新加载数据缓存
+struct UpdateResult {
+ bool success;
+ bool needReload;
+};
class MetadataUpdatePage : public ElaScrollPage
{
@@ -35,22 +43,30 @@ private slots:
private slots:
void onUpdateProgress(int progress, const QString& message);
void onUpdateFinished(bool success, const QString& message);
+ void onWorkerFinished(); // QFutureWatcher::finished 的回调(运行在主线程)
+
+signals:
+ void sigProgress(int progress, const QString& message); // 后台线程 -> 主线程回传进度
private:
void initUI();
void showUpdateItems();
- void updateUf2Interfaces(const QStringList& items);
- void updateUft3Interfaces(const QStringList& items);
-
+
+ // 后台线程执行体(由 QtConcurrent 调用,禁止直接操作 UI)
+ UpdateResult runUpdateUf2(const QStringList& items);
+ UpdateResult runUpdateUft3(const QStringList& items);
+ // 在主线程启动后台任务
+ void startUpdate(const QStringList& items, const QString& taskName);
+
void processUf2StdFields();
void processUf2DataType();
void processUf2ErrNumber();
- void processUf2Interface(const QString& basePath);
-
+ bool processUf2Interface(const QString& basePath);
+
void processUft3StdFields();
void processUft3DataType();
void processUft3Table();
- void processUft3Interface(const QString& basePath);
+ bool processUft3Interface(const QString& basePath);
QGridLayout* _updateGridLayout{nullptr};
QGridLayout* _updateUf2GridLayout{nullptr};
@@ -60,14 +76,17 @@ private:
ElaPushButton* _uft3CancelBtn{nullptr};
ElaPushButton* _uf2AllBtn{nullptr};
ElaPushButton* _uft3UpdateBtn{nullptr};
+ ElaCheckBox* _forceFullCheckBox{nullptr};
ElaText* _processLabel{nullptr};
ElaProgressBar* _progressBar{nullptr};
QList uft3CheckBoxes;
QList uf20CheckBoxes;
-
+
MetadataProcessor* _processor{nullptr};
- QThread* _workerThread{nullptr};
+ QFutureWatcher* _watcher{nullptr};
+ QElapsedTimer _updateTimer;
+ QString m_currentTaskName; // "UF20" / "UFT30",用于区分当前任务
};
-#endif
\ No newline at end of file
+#endif
diff --git a/src/utils/Constants.h b/src/utils/Constants.h
index 6dc8cec..5c051b9 100644
--- a/src/utils/Constants.h
+++ b/src/utils/Constants.h
@@ -14,7 +14,7 @@ namespace Constants {
// 应用版本号(发版时只需修改此处)
// ---------------------------------------------------------------------------
namespace App {
- constexpr char Version[] = "1.3.0";
+ constexpr char Version[] = "1.4.0";
}
// ---------------------------------------------------------------------------
@@ -35,6 +35,8 @@ namespace Paths {
inline const QString ToolDir = "uf2touft3"; // 工具主目录
inline const QString LogDir = "logs"; // 日志目录
inline const QString CacheDir = "cache"; // 缓存目录
+ inline const QString ChangeCodeDir = "change_code"; // 代码检索根目录(toolDir 下)
+ inline const QString FunctionOutput = "function_output"; // 调用链/文件导出目录
}
// ---------------------------------------------------------------------------
@@ -67,6 +69,19 @@ inline QString logDir() { return QCoreApplication::applicationDirPath() + "
inline QString cacheDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::CacheDir; }
inline QString cacheDb() { return cacheDir() + "/" + Cache::DbFile; }
+// ---------------------------------------------------------------------------
+// 代码检索(CodeSearch)相关常量与路径
+// ---------------------------------------------------------------------------
+namespace CodeSearch {
+ constexpr char ChangeCodeIni[] = "change_code.ini"; // 路径持久化配置文件名
+ constexpr char ConvertPathKey[] = "codeSearch/convertPath"; // QSettings 键:转码业务路径
+ constexpr char SearchPathKey[] = "codeSearch/searchPath"; // QSettings 键:非转码业务路径
+}
+
+inline QString changeCodeDir() { return toolDir() + "/" + Paths::ChangeCodeDir; } // 默认代码检索根目录
+inline QString changeCodeIni() { return toolDir() + "/" + QString::fromLatin1(CodeSearch::ChangeCodeIni); }
+inline QString functionOutput() { return Paths::FunctionOutput; } // 调用链/文件导出目录名
+
} // namespace Constants
#endif // CONSTANTS_H
diff --git a/src/utils/callchainanalyzer.cpp b/src/utils/callchainanalyzer.cpp
new file mode 100644
index 0000000..084d237
--- /dev/null
+++ b/src/utils/callchainanalyzer.cpp
@@ -0,0 +1,698 @@
+#include "callchainanalyzer.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+CallChainAnalyzer::CallChainAnalyzer(QObject *parent)
+ : QObject(parent)
+{
+}
+
+CallChainAnalyzer::~CallChainAnalyzer() = default;
+
+// 同名冲突时优先保留“真正的实现”而非外部接口声明:
+// 外部接口(.extinterface)通常无 ,若同名覆盖实现会让调用链路在叶子处断掉。
+static bool shouldPrefer(const CallChainAnalyzer::FunctionInfo *existing,
+ const CallChainAnalyzer::FunctionInfo *candidate)
+{
+ if (existing->isExternal != candidate->isExternal) {
+ return !candidate->isExternal; // 候选是实现则替换声明
+ }
+ // 同类型时,优先有内部调用的(更能展开链路)
+ if (existing->calledFunctions.isEmpty() != candidate->calledFunctions.isEmpty()) {
+ return !candidate->calledFunctions.isEmpty();
+ }
+ return false; // 同优先级,保留先入者
+}
+
+// 索引时排除的目录(框架/平台目录,无需纳入调用链分析,且体量大、扫描慢)
+static bool isExcludedDir(const QString &filePath)
+{
+ const QStringList parts = QDir::fromNativeSeparators(filePath)
+ .toLower().split('/', Qt::SkipEmptyParts);
+ for (const QString &part : parts) {
+ if (part == "toolset" || part == "ldp_platform") {
+ return true;
+ }
+ }
+ return false;
+}
+
+void CallChainAnalyzer::setRootPath(const QString &path)
+{
+ m_rootPath = path;
+}
+
+QString CallChainAnalyzer::rootPath() const
+{
+ return m_rootPath;
+}
+
+bool CallChainAnalyzer::isIndexed() const
+{
+ return m_indexed;
+}
+
+int CallChainAnalyzer::functionCount() const
+{
+ return m_chineseNameIndex.size();
+}
+
+bool CallChainAnalyzer::buildIndex(QString *errorMessage)
+{
+ m_chineseNameIndex.clear();
+ m_fullNameIndex.clear();
+ m_canonicalNameIndex.clear();
+ m_indexed = false;
+
+ QDir rootDir(m_rootPath);
+ if (!rootDir.exists()) {
+ if (errorMessage) {
+ *errorMessage = QString("代码目录不存在: %1").arg(m_rootPath);
+ }
+ return false;
+ }
+
+ // 同名冲突时优先保留“真正的实现”而非外部接口声明:
+ // 外部接口(.extinterface)通常无 ,若同名覆盖实现会让调用链路在叶子处断掉。
+ auto insertPrefer = [&](QHash> &idx,
+ const QString &key, QSharedPointer info) {
+ auto it = idx.find(key);
+ if (it == idx.end()) {
+ idx.insert(key, info);
+ } else if (shouldPrefer(it.value().data(), info.data())) {
+ it.value() = info;
+ }
+ };
+
+ // 扫描 .uftfunction、.uftservice、.uftatomfunction、.uftatomservice、.extinterface 五种格式
+ static const QStringList kScanExtensions = {
+ "*.uftfunction", "*.uftservice",
+ "*.uftatomfunction", "*.uftatomservice",
+ "*.extinterface"
+ };
+
+ int count = 0;
+ for (const QString &ext : kScanExtensions) {
+ QDirIterator it(m_rootPath, QStringList() << ext, QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ const QString filePath = it.next();
+ if (isExcludedDir(filePath)) {
+ continue;
+ }
+ QSharedPointer info = parseFile(filePath);
+ if (!info || info->chineseName.isEmpty()) {
+ continue;
+ }
+
+ // 按 chineseName 索引
+ insertPrefer(m_chineseNameIndex, info->chineseName, info);
+ // 按 objectId-chineseName 索引
+ if (!info->objectId.isEmpty()) {
+ insertPrefer(m_fullNameIndex, info->objectId + "-" + info->chineseName, info);
+ }
+ // 按文件 basename(不含扩展名)索引,方便从 cust.json 名称匹配
+ if (!info->canonicalName.isEmpty()) {
+ insertPrefer(m_canonicalNameIndex, info->canonicalName, info);
+ }
+
+ ++count;
+ }
+ }
+
+ m_indexed = true;
+ rebuildReverseIndex();
+ if (errorMessage) {
+ *errorMessage = QString("已索引 %1 个函数/服务文件").arg(count);
+ }
+ return true;
+}
+
+QString CallChainAnalyzer::computeFingerprint(const QString &rootPath)
+{
+ // 目录内容指纹:文件数量 + 总字节数 + 最新修改时间,用于判断缓存是否失效。
+ // 仅读取元数据,不解析内容,开销远小于重新构建索引。
+ static const QStringList kScanExtensions = {
+ "*.uftfunction", "*.uftservice",
+ "*.uftatomfunction", "*.uftatomservice",
+ "*.extinterface"
+ };
+
+ qint64 totalSize = 0;
+ qint64 latestMtime = 0;
+ qint64 count = 0;
+
+ QDirIterator it(rootPath, kScanExtensions, QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ const QFileInfo fi(it.next());
+ if (isExcludedDir(fi.filePath())) {
+ continue;
+ }
+ totalSize += fi.size();
+ const qint64 mtime = fi.lastModified().toSecsSinceEpoch();
+ if (mtime > latestMtime) {
+ latestMtime = mtime;
+ }
+ ++count;
+ }
+ return QString("%1:%2:%3").arg(count).arg(totalSize).arg(latestMtime);
+}
+
+QString CallChainAnalyzer::cacheFilePathFor(const QString &rootPath)
+{
+ // 缓存目录:程序目录(bin)下的 cache 子目录
+ const QString dir = QDir(QCoreApplication::applicationDirPath()).filePath("cache");
+ // 以根路径最后一个目录名作为缓存文件名,便于识别(如 A/B/C/D -> D.json)。
+ // 不同路径同名目录的冲突由 loadFromCache 中的 rootPath 校验兜底(失效则重建)。
+ const QString base = QDir(rootPath).dirName();
+ const QString safe = base.isEmpty() ? QStringLiteral("index") : base;
+ return QDir(dir).filePath(safe + ".json");
+}
+
+void CallChainAnalyzer::saveToCache() const
+{
+ if (!m_indexed || m_rootPath.isEmpty()) {
+ return;
+ }
+
+ const QString cacheFile = cacheFilePathFor(m_rootPath);
+ QDir().mkpath(QFileInfo(cacheFile).absolutePath());
+
+ QJsonArray funcs;
+ for (auto it = m_chineseNameIndex.constBegin(); it != m_chineseNameIndex.constEnd(); ++it) {
+ const FunctionInfo *info = it.value().data();
+ QJsonObject o;
+ o["objectId"] = info->objectId;
+ o["chineseName"] = info->chineseName;
+ o["canonicalName"] = info->canonicalName;
+ o["filePath"] = info->filePath;
+ o["relDir"] = info->relDir;
+ o["module"] = info->module;
+ o["isExternal"] = info->isExternal;
+ QJsonArray calls;
+ for (const QString &c : info->calledFunctions) {
+ calls.append(c);
+ }
+ o["calledFunctions"] = calls;
+ funcs.append(o);
+ }
+
+ QJsonObject meta;
+ meta["rootPath"] = m_rootPath;
+ meta["fingerprint"] = computeFingerprint(m_rootPath);
+ meta["version"] = 1;
+
+ QJsonObject root;
+ root["meta"] = meta;
+ root["functions"] = funcs;
+
+ QSaveFile file(cacheFile);
+ if (file.open(QIODevice::WriteOnly)) {
+ file.write(QJsonDocument(root).toJson(QJsonDocument::Compact));
+ file.commit();
+ }
+}
+
+bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message)
+{
+ m_chineseNameIndex.clear();
+ m_fullNameIndex.clear();
+ m_canonicalNameIndex.clear();
+ m_indexed = false;
+
+ if (path.isEmpty() || !QDir(path).exists()) {
+ return false;
+ }
+
+ const QString cacheFile = cacheFilePathFor(path);
+ QFile file(cacheFile);
+ if (!file.exists() || !file.open(QIODevice::ReadOnly)) {
+ return false;
+ }
+
+ const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
+ if (!doc.isObject()) {
+ return false;
+ }
+
+ const QJsonObject root = doc.object();
+ const QJsonObject meta = root.value("meta").toObject();
+ // 路径或目录内容发生变化则视为缓存失效,交由调用方重建
+ if (meta.value("rootPath").toString() != path) {
+ return false;
+ }
+ if (meta.value("fingerprint").toString() != computeFingerprint(path)) {
+ return false;
+ }
+
+ const QJsonArray funcs = root.value("functions").toArray();
+ int count = 0;
+ for (const QJsonValue &v : funcs) {
+ const QJsonObject o = v.toObject();
+ QSharedPointer info(new FunctionInfo());
+ info->objectId = o.value("objectId").toString();
+ info->chineseName = o.value("chineseName").toString();
+ info->canonicalName = o.value("canonicalName").toString();
+ info->filePath = o.value("filePath").toString();
+ info->relDir = o.value("relDir").toString();
+ info->module = o.value("module").toString();
+ info->isExternal = o.value("isExternal").toBool();
+ const QJsonArray calls = o.value("calledFunctions").toArray();
+ for (const QJsonValue &c : calls) {
+ info->calledFunctions.append(c.toString());
+ }
+ if (info->chineseName.isEmpty()) {
+ continue;
+ }
+
+ // 同名冲突同样优先“真正的实现”
+ auto itCn = m_chineseNameIndex.find(info->chineseName);
+ if (itCn == m_chineseNameIndex.end()) {
+ m_chineseNameIndex.insert(info->chineseName, info);
+ } else if (shouldPrefer(itCn.value().data(), info.data())) {
+ itCn.value() = info;
+ }
+ if (!info->objectId.isEmpty()) {
+ const QString fullKey = info->objectId + "-" + info->chineseName;
+ auto itFull = m_fullNameIndex.find(fullKey);
+ if (itFull == m_fullNameIndex.end()) {
+ m_fullNameIndex.insert(fullKey, info);
+ } else if (shouldPrefer(itFull.value().data(), info.data())) {
+ itFull.value() = info;
+ }
+ }
+ if (!info->canonicalName.isEmpty()) {
+ auto itCan = m_canonicalNameIndex.find(info->canonicalName);
+ if (itCan == m_canonicalNameIndex.end()) {
+ m_canonicalNameIndex.insert(info->canonicalName, info);
+ } else if (shouldPrefer(itCan.value().data(), info.data())) {
+ itCan.value() = info;
+ }
+ }
+ ++count;
+ }
+
+ m_rootPath = path;
+ m_indexed = true;
+ rebuildReverseIndex();
+ if (message) {
+ *message = QString("已从缓存加载 %1 个函数/服务文件").arg(count);
+ }
+ return true;
+}
+
+QSharedPointer
+CallChainAnalyzer::lookupBest(const QString &name) const
+{
+ // 1) 精确匹配 chineseName
+ {
+ auto it = m_chineseNameIndex.find(name);
+ if (it != m_chineseNameIndex.end()) {
+ return it.value();
+ }
+ }
+
+ // 2) 精确匹配 canonicalName(文件 basename)
+ {
+ auto it = m_canonicalNameIndex.find(name);
+ if (it != m_canonicalNameIndex.end()) {
+ return it.value();
+ }
+ }
+
+ // 3) 尝试添加 "_转码" 后缀匹配(部分 .uftservice 的 chineseName 带有 _转码)
+ {
+ const QString withSuffix = name + QString::fromUtf8("_转码");
+ auto it = m_chineseNameIndex.find(withSuffix);
+ if (it != m_chineseNameIndex.end()) {
+ return it.value();
+ }
+ }
+
+ // 4) 尝试去除 "_转码" 后缀匹配
+ if (name.endsWith(QString::fromUtf8("_转码"))) {
+ const QString withoutSuffix = name.chopped(3); // "_转码" = 3 UTF-8 chars → but QString::chopped counts QChars
+ auto it = m_chineseNameIndex.find(withoutSuffix);
+ if (it != m_chineseNameIndex.end()) {
+ return it.value();
+ }
+ }
+
+ // 5) 在 chineseName 中做前缀匹配(如 LS_证券周边_大约可买获取 匹配 LS_证券周边_大约可买获取_转码)
+ // 注意:name 作为前缀只在它是完整函数名(非已调用子函数名)时才合理,
+ // 所以这里只做开头前缀匹配避免误匹配
+ if (name.length() >= 4) {
+ // name 必须至少是一个有效函数名的前半部分
+ for (auto it = m_chineseNameIndex.constBegin();
+ it != m_chineseNameIndex.constEnd(); ++it) {
+ if (it.key().startsWith(name)) {
+ // 只接受 chineseName 在 name 后紧跟 "_转码" 或其他已知后缀的情况
+ const QString suffix = it.key().mid(name.length());
+ if (suffix == QString::fromUtf8("_转码") || suffix == "_转码") {
+ return it.value();
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+QSharedPointer
+CallChainAnalyzer::findByChineseName(const QString &name) const
+{
+ auto it = m_chineseNameIndex.find(name);
+ if (it != m_chineseNameIndex.end()) {
+ return it.value();
+ }
+ return nullptr;
+}
+
+QSharedPointer
+CallChainAnalyzer::findByFullName(const QString &fullName) const
+{
+ auto it = m_fullNameIndex.find(fullName);
+ if (it != m_fullNameIndex.end()) {
+ return it.value();
+ }
+ return nullptr;
+}
+
+CallChainAnalyzer::TreeNode
+CallChainAnalyzer::buildCallTree(const QString &startChineseName, int maxDepth) const
+{
+ TreeNode root;
+ QSet visited;
+ buildNode(startChineseName, root, visited, 0, maxDepth, true);
+ return root;
+}
+
+void CallChainAnalyzer::resolveNode(const QString &name, TreeNode &node, bool isRoot) const
+{
+ // 使用多策略查找,而非仅 chineseName 精确匹配
+ QSharedPointer info = lookupBest(name);
+ if (info) {
+ node.info = info.data();
+ node.objectId = info->objectId;
+ node.displayName = info->objectId.isEmpty()
+ ? info->chineseName
+ : QString("%1-%2").arg(info->objectId, info->chineseName);
+ node.state = "ok";
+ } else {
+ node.displayName = name;
+ // 根节点未找到:整个函数不在索引中
+ // 子调用未找到:说明该函数无需转码,原代码已支持
+ node.state = isRoot ? "notfound" : "supported";
+ // 若调用名称本身以 "功能号-中文名" 形式传入,解析出功能号用于导出展示
+ int dash = name.indexOf('-');
+ if (dash > 0) {
+ node.objectId = name.left(dash);
+ }
+ }
+}
+
+CallChainAnalyzer::TreeNode CallChainAnalyzer::makeNode(const QString &name, bool isRoot) const
+{
+ TreeNode node;
+ resolveNode(name, node, isRoot);
+ return node;
+}
+
+void CallChainAnalyzer::buildNode(const QString &name, TreeNode &node,
+ QSet &visited, int depth,
+ int maxDepth, bool isRoot) const
+{
+ resolveNode(name, node, isRoot);
+
+ if (depth >= maxDepth) {
+ node.state = "depthlimit";
+ return;
+ }
+
+ if (visited.contains(name)) {
+ node.state = "cycle";
+ return;
+ }
+
+ visited.insert(name);
+
+ QStringList calls;
+ if (node.info) {
+ calls = node.info->calledFunctions;
+ }
+
+ QSet seen;
+ for (const QString &call : calls) {
+ if (seen.contains(call)) {
+ continue;
+ }
+ seen.insert(call);
+
+ TreeNode child;
+ buildNode(call, child, visited, depth + 1, maxDepth);
+ node.children.append(child);
+ }
+
+ visited.remove(name);
+}
+
+void CallChainAnalyzer::rebuildReverseIndex()
+{
+ m_reverseIndex.clear();
+ for (auto it = m_chineseNameIndex.constBegin(); it != m_chineseNameIndex.constEnd(); ++it) {
+ QSharedPointer info = it.value();
+ // 同一“被调用名”下,按调用方 canonicalName 去重,避免同一函数多次调用产生重复链
+ QHash> seenByKey;
+ for (const QString &call : info->calledFunctions) {
+ // 用与正向查找一致的方式解析被调用名,使“往前查”时能正确命中
+ QSharedPointer target = lookupBest(call);
+ // 以多种方式建键,最大化“往前查”命中率:
+ // 解析到的 chineseName / 解析到的 canonicalName / 原始调用名
+ QStringList keys;
+ if (target) {
+ keys.append(target->chineseName);
+ if (!target->canonicalName.isEmpty()) {
+ keys.append(target->canonicalName);
+ }
+ }
+ keys.append(call);
+ for (const QString &k : keys) {
+ if (k.isEmpty() || seenByKey[k].contains(info->canonicalName)) {
+ continue;
+ }
+ seenByKey[k].insert(info->canonicalName);
+ m_reverseIndex[k].append(info);
+ }
+ }
+ }
+}
+
+QList
+CallChainAnalyzer::buildCallerForest(const QString &startName, const TreeNode &downTree,
+ int maxDepth) const
+{
+ QList roots;
+ QSet visited;
+
+ // 将 startName 归一化为其在索引中的 chineseName,保证与反向索引键一致。
+ // 否则当用户以 basename / 功能号前缀等形式查询(正向可命中)时,反向查找会因键不一致而失效。
+ QString startKey = startName;
+ {
+ QSharedPointer startInfo = lookupBest(startName);
+ if (startInfo) {
+ startKey = startInfo->chineseName;
+ }
+ }
+
+ // 递归构造“上游”链:children 为调用方(caller),链末端为 startName
+ auto recurse = [&](auto &self, const QString &name, QSet vis, int depth) -> QList {
+ QList out;
+ const auto it = m_reverseIndex.find(name);
+ QList> callers;
+ if (it != m_reverseIndex.end()) {
+ callers = it.value();
+ }
+
+ if (callers.isEmpty() || depth >= maxDepth) {
+ // 顶层调用方(无人再调用它):作为返回链的根
+ TreeNode n = makeNode(name, name == startKey);
+ if (name == startKey) {
+ n = downTree; // 起始函数节点直接用下游树(自带内部调用)
+ }
+ out.append(n);
+ return out;
+ }
+
+ for (const QSharedPointer &C : callers) {
+ const QString cName = C->chineseName;
+ if (vis.contains(cName)) {
+ // 循环调用:标记并收尾
+ TreeNode cn = makeNode(cName, false);
+ cn.state = "cycle";
+ if (name == startKey) {
+ cn.children.append(downTree);
+ } else {
+ cn.children.append(makeNode(name, false));
+ }
+ out.append(cn);
+ continue;
+ }
+ QSet nextVis = vis;
+ nextVis.insert(name);
+ QList sub = self(self, cName, nextVis, depth + 1);
+ for (TreeNode &s : sub) {
+ if (name == startKey) {
+ s.children.append(downTree);
+ } else {
+ s.children.append(makeNode(name, false));
+ }
+ out.append(s);
+ }
+ }
+ return out;
+ };
+
+ return recurse(recurse, startKey, visited, 0);
+}
+
+QSharedPointer
+CallChainAnalyzer::parseFile(const QString &filePath) const
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return nullptr;
+ }
+
+ QSharedPointer info(new FunctionInfo());
+ info->filePath = filePath;
+
+ const QFileInfo fi(filePath);
+ info->canonicalName = fi.completeBaseName(); // 不含扩展名的文件名
+
+ QDir rootDir(m_rootPath);
+ info->relDir = rootDir.relativeFilePath(fi.path());
+ info->relDir = QDir::fromNativeSeparators(info->relDir);
+ if (info->relDir.isEmpty()) {
+ info->relDir = ".";
+ }
+
+ const QStringList dirParts = info->relDir.split('/', Qt::SkipEmptyParts);
+ info->module = dirParts.isEmpty() ? QString() : dirParts.first();
+
+ QXmlStreamReader reader(&file);
+ bool inCodeElement = false;
+ bool isRoot = true;
+ QString codeText;
+
+ while (!reader.atEnd() && !reader.hasError()) {
+ reader.readNext();
+
+ if (reader.isStartElement()) {
+ const QString elementName = reader.name().toString();
+ if (isRoot) {
+ // 优先从根元素属性读取对象名/中文名,
+ // 兼容 / 以及 .extinterface 等其它带属性格式
+ if (reader.attributes().hasAttribute("chineseName")) {
+ info->chineseName = reader.attributes().value("chineseName").toString();
+ }
+ if (reader.attributes().hasAttribute("objectId")) {
+ info->objectId = reader.attributes().value("objectId").toString();
+ }
+ } else if (elementName == "Function" || elementName == "Service") {
+ // 兜底:对象信息写在嵌套元素的情况
+ if (info->chineseName.isEmpty()) {
+ info->chineseName = reader.attributes().value("chineseName").toString();
+ }
+ if (info->objectId.isEmpty()) {
+ info->objectId = reader.attributes().value("objectId").toString();
+ }
+ } else if (elementName == "code") {
+ inCodeElement = true;
+ }
+ isRoot = false;
+ } else if (reader.isCharacters() && inCodeElement) {
+ codeText += reader.text().toString();
+ } else if (reader.isEndElement()) {
+ if (reader.name().toString() == "code") {
+ inCodeElement = false;
+ }
+ }
+ }
+
+ if (reader.hasError()) {
+ return nullptr;
+ }
+
+ info->calledFunctions = extractFunctionCalls(codeText);
+ // 标记是否为外部接口声明文件(.extinterface 无 ,仅作为调用声明)
+ info->isExternal = (fi.suffix().compare("extinterface", Qt::CaseInsensitive) == 0);
+ return info;
+}
+
+QStringList CallChainAnalyzer::extractFunctionCalls(const QString &code)
+{
+ QStringList result;
+ QSet seen;
+
+ // 先去除注释(// 行注释、/* */ 块注释),避免把注释里提到的函数当成真实调用
+ const QString cleaned = stripComments(code);
+
+ // 格式 1: [函数名] 或 [函数名](.uftfunction 中的调用格式)
+ {
+ static const QRegularExpression callRe1(R"(<(I|MI)>\s*\[([^\]]+)\])");
+ QRegularExpressionMatchIterator it = callRe1.globalMatch(cleaned);
+ while (it.hasNext()) {
+ const QRegularExpressionMatch match = it.next();
+ const QString name = match.captured(2).trimmed();
+ if (!name.isEmpty() && !seen.contains(name)) {
+ if (name.startsWith("LS_") || name.startsWith("LF_") ||
+ name.startsWith("AS_") || name.startsWith("AF_")) {
+ seen.insert(name);
+ result.append(name);
+ }
+ }
+ }
+ }
+
+ // 格式 2: [LS_xxx] / [LF_xxx] / [AS_xxx] / [AF_xxx](.uftservice 中的调用格式)
+ // 如 [LF_证券周边_大约可买获取4][...参数...]
+ {
+ static const QRegularExpression callRe2(
+ R"(\[(L[SF]_[^\]]+|A[SF]_[^\]]+)\])");
+ QRegularExpressionMatchIterator it = callRe2.globalMatch(cleaned);
+ while (it.hasNext()) {
+ const QRegularExpressionMatch match = it.next();
+ const QString name = match.captured(1).trimmed();
+ if (!name.isEmpty() && !seen.contains(name)) {
+ if (name.startsWith("LS_") || name.startsWith("LF_") ||
+ name.startsWith("AS_") || name.startsWith("AF_")) {
+ seen.insert(name);
+ result.append(name);
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+QString CallChainAnalyzer::stripComments(const QString &code)
+{
+ QString cleaned = code;
+ // 去除 /* ... */ 块注释(跨行、非贪婪)
+ cleaned.remove(QRegularExpression(R"(/\*.*?\*/)",
+ QRegularExpression::DotMatchesEverythingOption));
+ // 去除 // 行注释(到行尾)
+ cleaned.remove(QRegularExpression(R"(//[^\n]*)"));
+ return cleaned;
+}
diff --git a/src/utils/callchainanalyzer.h b/src/utils/callchainanalyzer.h
new file mode 100644
index 0000000..17e1a1a
--- /dev/null
+++ b/src/utils/callchainanalyzer.h
@@ -0,0 +1,101 @@
+#ifndef CALLCHAINANALYZER_H
+#define CALLCHAINANALYZER_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class CallChainAnalyzer : public QObject
+{
+ Q_OBJECT
+
+public:
+ struct FunctionInfo
+ {
+ QString objectId; // XML 中的 objectId
+ QString chineseName; // XML 中的 chineseName,如 LF_证券公用_大约可买获取2
+ QString canonicalName; // 文件 basename(不含扩展名),如 LS_SESEXT_ALMOSTBUY_REQ
+ QString filePath; // 文件绝对路径
+ QString relDir; // 文件所在目录(相对于搜索根目录)
+ QString module; // 模块:取 relDir 的第一级目录,如 ucbp
+ QStringList calledFunctions; // 本函数代码中调用的函数中文名列表
+ bool isExternal = false; // 是否为外部接口声明文件(.extinterface)
+ };
+
+ struct TreeNode
+ {
+ const FunctionInfo *info = nullptr;
+ QString displayName;
+ QString objectId; // 功能号(supported 节点尽量从名称中解析)
+ QString state; // ok / notfound / cycle / depthlimit
+ QList children;
+ };
+
+ explicit CallChainAnalyzer(QObject *parent = nullptr);
+ ~CallChainAnalyzer();
+
+ void setRootPath(const QString &path);
+ QString rootPath() const;
+
+ bool isIndexed() const;
+ int functionCount() const;
+
+ // 构建索引:扫描根目录下所有 .uftfunction 文件
+ bool buildIndex(QString *errorMessage = nullptr);
+
+ // 缓存:将已构建的索引持久化到磁盘,避免每次打开都重新扫描目录
+ // 加载时按 rootPath + 目录指纹校验,命中则直接反序列化,否则返回 false 由调用方重建
+ bool loadFromCache(const QString &path, QString *message = nullptr);
+ void saveToCache() const;
+
+ // 根据函数中文名查找
+ QSharedPointer findByChineseName(const QString &name) const;
+ // 根据 objectId-chineseName 查找
+ QSharedPointer findByFullName(const QString &fullName) const;
+
+ // 多策略查找:先精确匹配 chineseName,再尝试 basename、去后缀等。
+ // 与 resolveNode 使用同一解析入口,供外部还原节点的展示名(如蓝底高亮匹配)。
+ QSharedPointer lookupBest(const QString &name) const;
+
+ // 构建从 startChineseName 出发的调用链路树(下游:被其调用的函数)
+ TreeNode buildCallTree(const QString &startChineseName, int maxDepth = 32) const;
+
+ // 构建“往前/反向”调用链路森林:返回所有上游调用方链的根节点集合,
+ // 每条链都以 startName 收尾,并将 downTree(startName 的下游树)挂在其末端。
+ // 例:A 被 AA/AB/AC 调用,AA 被 AAA 调用,则返回 AAA→AA→A、AB→A、AC→A 三条链。
+ QList buildCallerForest(const QString &startName, const TreeNode &downTree,
+ int maxDepth = 32) const;
+
+private:
+ QSharedPointer parseFile(const QString &filePath) const;
+ void buildNode(const QString &name, TreeNode &node, QSet &visited,
+ int depth, int maxDepth, bool isRoot = false) const;
+ // 仅解析单个节点(定位函数并填充 info/displayName/state),不展开子调用
+ void resolveNode(const QString &name, TreeNode &node, bool isRoot) const;
+ // 由函数名构造一个已解析的节点
+ TreeNode makeNode(const QString &name, bool isRoot) const;
+ // 由正向索引反推调用方索引:calledName -> 调用它的函数列表
+ void rebuildReverseIndex();
+
+ static QStringList extractFunctionCalls(const QString &code);
+ static QString stripComments(const QString &code);
+
+ // 缓存辅助:根路径 -> 缓存文件路径(按路径哈希命名),以及目录内容指纹(用于失效判断)
+ static QString cacheFilePathFor(const QString &rootPath);
+ static QString computeFingerprint(const QString &rootPath);
+
+ QString m_rootPath;
+ QHash> m_chineseNameIndex;
+ QHash> m_fullNameIndex;
+ QHash> m_canonicalNameIndex;
+ // 反向索引:被调用函数名 -> 调用它的函数列表(用于“往前查”)
+ QHash>> m_reverseIndex;
+ bool m_indexed = false;
+};
+
+#endif // CALLCHAINANALYZER_H
diff --git a/update_info.json b/update_info.json
index 20bf812..3956c0e 100644
--- a/update_info.json
+++ b/update_info.json
@@ -1,6 +1,6 @@
{
"V1.0.0": {
- "release_notes": "1. 支持业务转码;\n2. 支持代码uf20、uft3功能查找;\n3.支持帮助菜单和在线帮助文档;\n4.支持接口元数据更新;\n5.设置菜单UFT30项目环境路径、UF20项目环境路径、UF20账户环境路径、转码生成路径、支持弹窗选择目录。"
+ "release_notes": "1.支持业务转码;\n2.支持代码uf20、uft3功能查找;\n3.支持帮助菜单和在线帮助文档;\n4.支持接口元数据更新;\n5.设置菜单UFT30项目环境路径、UF20项目环境路径、UF20账户环境路径、转码生成路径、支持弹窗选择目录。"
},
"V1.1.0": {
"release_notes": "1.支持2.0账户功能查询及业务转码"
@@ -10,5 +10,8 @@
},
"V1.3.0": {
"release_notes": "1.修复设置路径不生效;\n2.修复元数据更新后业务转码模块对函数检查状态异常问题;"
- }
+ },
+ "V1.4.0": {
+ "release_notes": "1.提升业务转码性能;\n2.元数据更新支持增量更新和强制全量更新;\n3.新增代码检索功能;\n4.支持函数调用链路分析;\n5.支持全局文字可选中复制及表格Ctrl+C导出;"
+ }
}