From 389c6eeecd4d870ecdd4e270849c8861eaeb271f Mon Sep 17 00:00:00 2001 From: taocong Date: Tue, 21 Jul 2026 17:08:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BB=A3=E7=A0=81=E6=A3=80?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD=E3=80=81=E6=8F=90=E5=8D=87=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E8=BD=AC=E7=A0=81=E6=95=88=E7=8E=87=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Uft30ChangeCode.pro | 6 +- build.bat | 10 + help/Writerside/hi.tree | 4 +- help/Writerside/images/转码业务.png | Bin 0 -> 51750 bytes help/Writerside/images/非转码业务.png | Bin 0 -> 33412 bytes .../topics/{1-3-0.md => update-1-3-0.md} | 0 help/Writerside/topics/update-1-4-0.md | 11 + help/Writerside/topics/下载.md | 1 + help/Writerside/topics/代码检索.md | 11 + help/Writerside/topics/功能转码.md | 2 + main.cpp | 87 +- resources.qrc | 2 + resources/images/collapse_all.png | Bin 0 -> 1860 bytes resources/images/expand_all.png | Bin 0 -> 1966 bytes src/mainwindow/mainwindow.cpp | 135 ++- src/mainwindow/mainwindow.h | 4 + src/metadataupdate/metadataprocessor.cpp | 424 +++++-- src/metadataupdate/metadataprocessor.h | 11 +- src/metadataupdate/uf2interface.cpp | 95 ++ src/metadataupdate/uf2interface.h | 20 + src/metadataupdate/uft3interface.cpp | 55 + src/metadataupdate/uft3interface.h | 14 + src/pages/about/aboutdialog.cpp | 4 +- src/pages/batchconvert/batchconvertpage.cpp | 13 +- src/pages/codesearch/codesearchpage.cpp | 1012 +++++++++++++++++ src/pages/codesearch/codesearchpage.h | 127 +++ src/pages/metadatupdate/metadatupdatepage.cpp | 191 +++- src/pages/metadatupdate/metadatupdatepage.h | 37 +- src/utils/Constants.h | 17 +- src/utils/callchainanalyzer.cpp | 698 ++++++++++++ src/utils/callchainanalyzer.h | 101 ++ update_info.json | 7 +- 32 files changed, 2858 insertions(+), 241 deletions(-) create mode 100644 help/Writerside/images/转码业务.png create mode 100644 help/Writerside/images/非转码业务.png rename help/Writerside/topics/{1-3-0.md => update-1-3-0.md} (100%) create mode 100644 help/Writerside/topics/update-1-4-0.md create mode 100644 help/Writerside/topics/代码检索.md create mode 100644 resources/images/collapse_all.png create mode 100644 resources/images/expand_all.png create mode 100644 src/pages/codesearch/codesearchpage.cpp create mode 100644 src/pages/codesearch/codesearchpage.h create mode 100644 src/utils/callchainanalyzer.cpp create mode 100644 src/utils/callchainanalyzer.h 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 0000000000000000000000000000000000000000..d25049b20bc3dbb2abffbdbae2b71626c0523cc3 GIT binary patch literal 51750 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 !F)xL7w5wa{7g~-#*Jjm-cjEh@;NO}FP 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 QyxDi>=Dize&4(}l7n6{X0RR91 literal 0 HcmV?d00001 diff --git a/help/Writerside/images/非转码业务.png b/help/Writerside/images/非转码业务.png new file mode 100644 index 0000000000000000000000000000000000000000..0731f43b39a847a93f7059d3d38ff6bb1c3137aa GIT binary patch literal 33412 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 WiLVPUqW?S_Lis89N!jD)Z~hOQ_bhJ! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cec3a89ce52160413c24de739dbb34e4f863612c GIT binary patch literal 1860 zcmZ8ieLPfY8$NS7bPQ^Im67aejIUZD6cTd~Gd^pUF}8S#5V46eS|8(>SR}{CygoK> zywMx^*otcN$}H|DQ4d2p}e*Z&KGwppQ)Gr0D}HVR9J zu_uwIv3p3=`1P;T^KqZO-RTYERjab@_6Kho#?v4;g~=lA|2-=Q*Qxw$%o>I%#8Khs zE(CqO2|JZ4Nhgk4_4%Ui4nnK|sd{9FpnKArzcSdtA~^(iToXpr7$wi(he)uR8SY(+k$0aV$ z=D(U-nkN#|V&Tw6-g$g#p_;)q30O387GfQ6|8x|Bp5E5*ZRnBxJ1B&^W@-MCE{puc z-+#gZz?oX;&A14$)f;D@hyYwrcx%zShdczmso}SJ6=RX=W)a7B!9IxW1NCuu6i+_t zsX+^tJF|IXaDcqD>OfBZXk7F;+|xrVo@RKz5bH1zE}E!eoyYC&8+!uy|9_QqE{0is zkC|6i{x1l?DYc}90T4QuJ8BBJNFVyioQ8EO8CB2n;KA{DwJ}-m>`#gVSuN#Jzq-i+ zW;l0VG`BksA~Ig2cx%CA>?gO0`~G6<cdw5zccLAmDf{wB=#&6wiW z(sY&_j8v01b$mf)5pi310Fr7n6=O*uYMsM^IZ5UF2t-)KkaY^tf}x{ zjDyp)t=}Hgg^}lIZB#5Ms88GDn!6Aa_x7*)z{rnTouV#~p>ShJZz%#VZ@RU&A<1wU#5 zh{;Y{nqLUv`SRE6>=5)xMsStC>w+uEjfq_30YGuFtO})7dcQ1f5kq*iUTCrrj2LOe z&xeloxsZaAYlbumflwWdW>Fe5Kz}FINrXO+kADT-kZRna2)zU{GCySNFq{M7L+s^D2^~hPH+1MCLnv)5 zM|2r1Fy)ix8VMp61}auh=e`Ei|JU7H{W}PU&sP)*5LDGfO+$`B`jRYCyvHQfg0M}o zvD5rT^U=5dOAWR;nACs2mLmOKO83yPPR%k+ikYI=0CBGUVGPqKt^jWF#-KdcGO8nA@<@_Q=}@4a8xVz-UhWt z-F-G-8FL5WXxBfR2I;KQ2X>fI2<9f=$R(h_PfAj<7{XOEzOQKNIY0}EgMZuXA;c<1 z55Eittu7PJIjUjo34!{)QV4f882)$|*;3|49xQgRaTQ`d1^r75Aal{f${HXd%Y-Bo zkp5r6z%tC|#ALHJg(%EE+I|NJ&$#iCF>v=AM*n+bFa;EiV}DUW4cI>-7TVx!hX+^g z_5c^!Q2xl!XOWL*4Wlh6#05`@%vT{XNwyLf-54(&ZRqyuu8$V9i(HA9!j7kgi09|3 zj@B>M)&$YEYq5fr1=-~D6CMpK3Qw{tlenrFyNhqxmkr_P@*2Hd0V(wBSI>d=Zq48O z5Tx%{(C51qMka|KeLo=R$eSx~!EE$59boRF5KdmaBu52Wh+%Su3*c=+q52y`3Zc|7 z^L_$!>cEZ#W#E^Jk18cFFft2pcxh}z;@>0vzDc_UR z^kBIY(hB-5b3u;j%;g?pOz%kZEsH@lrbnUHa{|I|mq&D4fG+C0=9~n=IC7{dObaBe zc3kYdC@sKNKb##10rdC+Y?3-eBqkWY3Rh0EHxCc852H@$ zcTS4u)(7K;wCwod_L2GA2PM}v=ML|~kKO*yPGlO)+|^~jvP;0F8*5uuffMYKc?JfL z-N>=xMqgmd8vTZugFvll0mlba74cyu0N60ww8UF1K?v_0=T|DgoetCp)J=#5T{+B0 zfICS-s|`^8d${_RR8|zuV3wyp22|PTbmV}J_9$4{gN}+Wy2M-owEiu$Z5_ccw2`%q n5cK);&c)gh@Gq2K{|Rn@+%DN^9_>FX1z#Y}K~JZ8hd|;_XzBbN literal 0 HcmV?d00001 diff --git a/resources/images/expand_all.png b/resources/images/expand_all.png new file mode 100644 index 0000000000000000000000000000000000000000..3451a8348c49ca8ff25dfb9c8e5a084668824dce GIT binary patch literal 1966 zcmZuyX;@QN8or4axGc(r0EHOgA^`+~Y_fyovaewgWw8|ygd$5LA`Nv*E=wXHhCmf* zSOip{f&*x&Q#J)zWs!8CfJH>c4wZ_4S`aEd{WCMqI6v;W&v$;D^M3F5zVEpZ!1CBG zN0oyhXuFrEYY_PN{q-TzV66{rD1#uGXfIbhm_u4D^~tBXsklW%H7XW2!r7%qRRziP zY+bcZ^mn1^k7)IG-R5oc&{-b zvWYTN_{rnfQm`wUc^Kju!RWjkt(|}fwhYG3VtU{W;(-v9?F`{!*zKwvjJopeZ`x^d zF&0tjyNirec9k9|MPC~TDBl+?ltaYYh;WA;I5VF;Ipsuxm&>(xrP8_Bu4hem&y$fA zhReCE&YSQ9`4rAc2Xm1b$7bftSN#*4j;)wK+dWdDfT2+R5B*%Mp3~@WPDVrw_LQ*> z7dzHE{wTKIg^bLZ?(?3>hls@9l0up?6XjnETuFn`82fSYybvO66n+~NrH|!8#GBoN z?b-#vhi!0nsl(GRD{NJ^>VQ$4WVZ^Dh;&>VJ06%7Kbs3bVhW#qmlf(iIVHGvl) zqUPE!k%}FrJj3I(}r}%Xa;tw)$mBl%uSz+jL9;l zd%$hYw&vOvB)DI^zikA73v>1D^M!EpoBm79fO|D1iaCXe4!yCRb|ArKT1ryK!DRg2 zi<8PAqWF@4Y$=Ng^v(`j`9t_2_tdr3HNTyCGV{PlK5`3QA1WB`P4tk&GNQp%|LGLbyT}ZtxBq*5+GNBf}eTRH{~{ z4W@v0X||GMx+J(3?roxhV?2ga!@MBe@O~phhNnx+WB3)c7|=io;J`e9@t$y2AJ#*# zK3GK)p9R*~OJ9@a$&v>M-{5W?M140G%YIOyavEsih+c4v03y`oGJ+NXpwm|a3be^5 zq|D%}L7U2mPT#X=iVMUydJj^?zCdiV7*M367*r=_-3S^o+Y1kT+Q{Agu>EDP>lnrYEBgY)`-YDmHfV60FdwwOVDE^=Q>6bg~+kPmg zHWf~clwTSN7xmLUeUcaND)E*(F08$M#h*A3y(M99ZLY5+ek!SRfaBdnXPp6PVdGe^ z5inE^eIpaZp?383+Cu=Hte4dVZwRl{^NH{SQ>5f2Dt`?Tu6~!>OMoo()Ffk0VxfCKo{RQuX0Z)eKcC@e{{Fc7!**egIYR9W*;{Cv4FEq?_%t){oZGZg; zWmGjrze8(=c1fXJVP5=nQ+3tRB?K+l8O4cX=bOILO2X$vLqCTVzfz+67VR6$0Kr)? zP!u$TtM#>Y(WSBg12BYu$wl2$%1rA3#EO|QJz7YBqM7FqOz zF>f5?>|mUrbM~0@(RUVsp%!>g$neq^d>$#|+b?I6)wh<5zt6GKvZQp!o(`7codP89 zayldfvi1J98*TqCN4log("========================================"); 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导出;" + } }