From 644fa8b4ce4134cb62d1bf396a206a16643e3c0d Mon Sep 17 00:00:00 2001 From: taocong Date: Thu, 10 Sep 2026 16:01:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=85=A8=E9=87=8F=E8=BD=AC?= =?UTF-8?q?=E7=A0=81=E5=92=8C=E6=8C=89=E9=9C=80=E8=BD=AC=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Uft30ChangeCode.pro | 4 + build.bat | 10 +- src/mainwindow/mainwindow.cpp | 135 ++- src/mainwindow/mainwindow.h | 19 +- src/pages/batchconvert/batchconvertpage.cpp | 33 +- src/pages/batchconvert/batchconvertpage.h | 4 +- .../businessconvert/businessconvertpage.cpp | 55 ++ .../businessconvert/businessconvertpage.h | 36 + .../functionsearch/uf20functionsearchpage.cpp | 14 +- .../functionsearch/uf20functionsearchpage.h | 4 +- src/pages/nonlsconvert/nonlsconvertpage.cpp | 809 ++++++++++++++++-- src/pages/nonlsconvert/nonlsconvertpage.h | 72 +- src/utils/Constants.h | 5 + src/utils/configreaderbase.cpp | 16 + src/utils/configreaderbase.h | 3 + src/utils/uf2configreader.cpp | 3 + src/utils/uft3configreader.cpp | 8 + 17 files changed, 1083 insertions(+), 147 deletions(-) create mode 100644 src/pages/businessconvert/businessconvertpage.cpp create mode 100644 src/pages/businessconvert/businessconvertpage.h diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro index 8d1acb1..14a3ab3 100644 --- a/Uft30ChangeCode.pro +++ b/Uft30ChangeCode.pro @@ -9,7 +9,9 @@ SOURCES += main.cpp \ src/mainwindow/mainwindow.cpp \ src/pythonrunner/PythonRunner.cpp \ src/pages/about/aboutdialog.cpp \ + src/pages/businessconvert/businessconvertpage.cpp \ src/pages/batchconvert/batchconvertpage.cpp \ + src/pages/nonlsconvert/nonlsconvertpage.cpp \ src/pages/codesearch/codesearchpage.cpp \ src/pages/functionsearch/functionsearchpage.cpp \ src/pages/functionsearch/uf20functionsearchpage.cpp \ @@ -48,7 +50,9 @@ SOURCES += main.cpp \ HEADERS += src/mainwindow/mainwindow.h \ src/pythonrunner/PythonRunner.h \ src/pages/about/aboutdialog.h \ + src/pages/businessconvert/businessconvertpage.h \ src/pages/batchconvert/batchconvertpage.h \ + src/pages/nonlsconvert/nonlsconvertpage.h \ src/pages/codesearch/codesearchpage.h \ src/pages/functionsearch/functionsearchpage.h \ src/pages/functionsearch/uf20functionsearchpage.h \ diff --git a/build.bat b/build.bat index 48c1e1b..c04a6ee 100644 --- a/build.bat +++ b/build.bat @@ -88,12 +88,12 @@ echo. REM Deploy default change_code.ini (only copy when missing, to keep user's existing config) echo Deploying default change_code.ini... -if not exist "bin\uf2touft3" mkdir "bin\uf2touft3" -if not exist "bin\uf2touft3\change_code.ini" ( - copy "config\change_code.ini" "bin\uf2touft3\change_code.ini" >nul - echo [OK] change_code.ini -> bin\uf2touft3\ (default copied) +if not exist "bin\uf2touft3\config" mkdir "bin\uf2touft3\config" +if not exist "bin\uf2touft3\config\change_code.ini" ( + copy "config\change_code.ini" "bin\uf2touft3\config\change_code.ini" >nul + echo [OK] change_code.ini -> bin\uf2touft3\config (default copied) ) else ( - echo [SKIP] bin\uf2touft3\change_code.ini already exists, keep user config + echo [SKIP] bin\uf2touft3\config\change_code.ini already exists, keep user config ) echo. diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index d10d317..5686732 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -40,6 +40,10 @@ MainWindow::MainWindow(QWidget *parent) , m_iconsSet(false) , m_navigationButton(nullptr) , m_isNavigationExpanded(true) + , m_businessConvertPage(nullptr) + , m_batchConvertPage(nullptr) + , m_nonLsConvertPage(nullptr) + , m_activeKind(ConvertKind::LS) , m_totalFunctions(0) , m_currentFunctionIndex(0) , m_lastProgress(0) @@ -136,7 +140,7 @@ void MainWindow::initWindow() void MainWindow::initContent() { addExpanderNode("功能转码", m_funcConvertKey, ElaIconType::FileCode); - addPageNode("业务转码", createBatchConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat); + addPageNode("业务转码", createBusinessConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat); addPageNode("代码检索", createCodeSearchPage(), m_funcConvertKey, ElaIconType::Cubes); addExpanderNode("功能查询", m_funcSearchKey, ElaIconType::MagnifyingGlass); @@ -154,12 +158,21 @@ void MainWindow::initContent() addFooterNode("设置", createSettingsPage(), m_settingKey, 0, ElaIconType::GearComplex); } -QWidget* MainWindow::createBatchConvertPage() +QWidget* MainWindow::createBusinessConvertPage() { - m_batchConvertPage = new BatchConvertPage; - connect(m_batchConvertPage, &BatchConvertPage::startConvert, this, &MainWindow::onBatchConvertStart); - connect(m_batchConvertPage, &BatchConvertPage::stopConvert, this, &MainWindow::onBatchConvertStop); - return m_batchConvertPage; + m_businessConvertPage = new BusinessConvertPage; + m_batchConvertPage = m_businessConvertPage->lsPage(); + m_nonLsConvertPage = m_businessConvertPage->nonLsPage(); + + connect(m_batchConvertPage, &BatchConvertPage::startConvert, + this, &MainWindow::onLsConvertStart); + connect(m_batchConvertPage, &BatchConvertPage::stopConvert, + this, &MainWindow::onConvertStop); + connect(m_nonLsConvertPage, &NonLsConvertPage::startConvert, + this, &MainWindow::onNonLsConvertStart); + connect(m_nonLsConvertPage, &NonLsConvertPage::stopConvert, + this, &MainWindow::onConvertStop); + return m_businessConvertPage; } QWidget* MainWindow::createCodeSearchPage() @@ -176,8 +189,11 @@ QWidget* MainWindow::createFunctionSearchPage() QWidget* MainWindow::createUF20FunctionSearchPage() { UF20FunctionSearchPage *page = new UF20FunctionSearchPage; - connect(page, &UF20FunctionSearchPage::addFunctionToConvert, + // LS_ 开头功能 → 全量转码工作台;其余功能 → 按需转码工作台 + connect(page, &UF20FunctionSearchPage::addLsFunctionToConvert, m_batchConvertPage, &BatchConvertPage::addFunction); + connect(page, &UF20FunctionSearchPage::addNonLsFunctionToConvert, + m_nonLsConvertPage, &NonLsConvertPage::addFunction); return page; } @@ -206,53 +222,87 @@ QWidget* MainWindow::createFunctionMappingPage() return new FunctionMappingPage(); } -void MainWindow::onBatchConvertStart() +void MainWindow::onLsConvertStart() +{ + beginConvert(ConvertKind::LS); +} + +void MainWindow::onNonLsConvertStart() +{ + beginConvert(ConvertKind::NonLs); +} + +void MainWindow::beginConvert(ConvertKind kind) { if (m_pythonRunner->isRunning()) { LogManager::instance()->logWarning("警告: 上一个任务还在运行中..."); QMessageBox::warning(this, "提示", "上一个转换任务正在运行,请等待完成或停止当前任务。"); return; } - + QString exePath = Constants::toolExe(); - + if (!m_pythonRunner->canRun()) { LogManager::instance()->logError("错误: 找不到 uf2touft3/uf2touft3.exe!"); - QMessageBox::critical(this, "错误", + QMessageBox::critical(this, "错误", QString("无法启动转换任务!\n\n请确保:\nuf2touft3/uf2touft3.exe 存在,且该目录下包含 config/config.ini\n\n期望路径:\n%1") - .arg(exePath)); + .arg(exePath)); return; } - - if (m_batchConvertPage) { - m_totalFunctions = m_batchConvertPage->getFunctionCount(); - m_functionList = m_batchConvertPage->getFunctionList(); - m_currentFunctionIndex = 0; - } else { - m_totalFunctions = 0; - m_currentFunctionIndex = 0; - m_functionList.clear(); - } - // 进度基准:BatchConvertPage 在开始时已将进度条置为 20(配置加载完成) + QStringList engineArgs; + if (kind == ConvertKind::NonLs) { + // 按需转码:参数由页面在 onStartConvert 中拼装完成(含 -t/-o 与 @列表文件生成) + engineArgs = m_nonLsConvertPage ? m_nonLsConvertPage->preparedArguments() : QStringList(); + m_pythonRunner->setArguments(engineArgs); + m_totalFunctions = m_nonLsConvertPage ? m_nonLsConvertPage->getFunctionCount() : 0; + m_functionList = m_nonLsConvertPage ? m_nonLsConvertPage->getFunctionList() : QStringList(); + } else { + // 全量转码:沿用原无参调用,显式清空历史参数 + m_pythonRunner->setArguments(QStringList()); + m_totalFunctions = m_batchConvertPage ? m_batchConvertPage->getFunctionCount() : 0; + m_functionList = m_batchConvertPage ? m_batchConvertPage->getFunctionList() : QStringList(); + } + m_activeKind = kind; + m_currentFunctionIndex = 0; + + // 进度基准:页面在开始时已将进度条置为 20(配置加载完成) m_lastProgress = 20; - + LogManager::instance()->log("========================================"); - LogManager::instance()->log("开始执行 UFT2 到 UFT3 的代码转换..."); + LogManager::instance()->log(QString("开始执行 %1 代码转换...") + .arg(kind == ConvertKind::NonLs ? "按需转码" : "全量转码")); LogManager::instance()->log(QString("工具路径: %1").arg(exePath)); + if (kind == ConvertKind::NonLs && !engineArgs.isEmpty()) { + LogManager::instance()->log(QString("引擎参数: %1").arg(engineArgs.join(' '))); + } LogManager::instance()->log(QString("待转换函数数量: %1").arg(m_totalFunctions)); LogManager::instance()->log("========================================"); - + if (!m_pythonRunner->start()) { LogManager::instance()->logError("错误: 无法启动转换进程!"); + return; + } + + // 本页进入运行态;另一 Tab 禁止开始,避免并发冲突 + if (kind == ConvertKind::NonLs) { + if (m_nonLsConvertPage) { + m_nonLsConvertPage->onConvertStarted(); + } + if (m_batchConvertPage) { + m_batchConvertPage->setTaskBusy(true); + } } else { if (m_batchConvertPage) { m_batchConvertPage->onConvertStarted(); } + if (m_nonLsConvertPage) { + m_nonLsConvertPage->setTaskBusy(true); + } } } -void MainWindow::onBatchConvertStop() +void MainWindow::onConvertStop() { if (m_pythonRunner->isRunning()) { LogManager::instance()->log("正在停止转换进程..."); @@ -262,8 +312,21 @@ void MainWindow::onBatchConvertStop() void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitStatus) { - if (m_batchConvertPage) { - m_batchConvertPage->onConvertFinished(); + // 完成态回到发起任务的 Tab,并解除另一 Tab 的互斥限制 + if (m_activeKind == ConvertKind::NonLs) { + if (m_nonLsConvertPage) { + m_nonLsConvertPage->onConvertFinished(); + } + if (m_batchConvertPage) { + m_batchConvertPage->setTaskBusy(false); + } + } else { + if (m_batchConvertPage) { + m_batchConvertPage->onConvertFinished(); + } + if (m_nonLsConvertPage) { + m_nonLsConvertPage->setTaskBusy(false); + } } LogManager::instance()->log("========================================"); @@ -346,18 +409,20 @@ void MainWindow::onPythonRunnerOutput(const QString &output) } } - if (!m_batchConvertPage) { - return; - } - // 进度单调递增:只有更大的进度才会推进进度条,避免"一下长一下短" if (targetProgress > m_lastProgress) { m_lastProgress = targetProgress; } // 状态文字:仅展示已识别的处理阶段;未识别的日志行不显示(保持上一次状态) - // 空 status 表示当前行无有效状态,BatchConvertPage 会保留上一次文字 - m_batchConvertPage->onProgressUpdate(m_lastProgress, status); + // 空 status 表示当前行无有效状态,页面会保留上一次文字 + if (m_activeKind == ConvertKind::NonLs) { + if (m_nonLsConvertPage) { + m_nonLsConvertPage->onProgressUpdate(m_lastProgress, status); + } + } else if (m_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 7c8b991..6fd938a 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -5,7 +5,9 @@ #include "ElaToolButton.h" #include #include "src/pythonrunner/PythonRunner.h" +#include "src/pages/businessconvert/businessconvertpage.h" #include "src/pages/batchconvert/batchconvertpage.h" +#include "src/pages/nonlsconvert/nonlsconvertpage.h" #include "src/pages/codesearch/codesearchpage.h" #include "src/pages/functionsearch/functionsearchpage.h" #include "src/pages/functionsearch/uf20functionsearchpage.h" @@ -30,7 +32,7 @@ private: void initWindow(); void initContent(); - QWidget* createBatchConvertPage(); + QWidget* createBusinessConvertPage(); QWidget* createCodeSearchPage(); QWidget* createFunctionSearchPage(); QWidget* createUF20FunctionSearchPage(); @@ -42,8 +44,13 @@ private: void loadConfigCache(); - void onBatchConvertStart(); - void onBatchConvertStop(); + // 转码任务类别:全量转码 / 按需转码 + enum class ConvertKind { LS, NonLs }; + + void onLsConvertStart(); + void onNonLsConvertStart(); + void onConvertStop(); + void beginConvert(ConvertKind kind); void onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitStatus); void onPythonRunnerOutput(const QString &output); void onPythonRunnerError(const QString &error); @@ -59,9 +66,13 @@ private: bool m_iconsSet; ElaToolButton* m_navigationButton; bool m_isNavigationExpanded; + BusinessConvertPage *m_businessConvertPage; BatchConvertPage *m_batchConvertPage; + NonLsConvertPage *m_nonLsConvertPage; CodeSearchPage *_codeSearchPage; - + + ConvertKind m_activeKind; + int m_totalFunctions; int m_currentFunctionIndex; int m_lastProgress; // 已上报的最大进度,保证进度单调递增,避免进度条来回跳动 diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp index 45fb120..57f4bf9 100644 --- a/src/pages/batchconvert/batchconvertpage.cpp +++ b/src/pages/batchconvert/batchconvertpage.cpp @@ -45,7 +45,8 @@ bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames) } QMessageBox::warning(parent, "提示", - QString("不支持非LS业务转码:\n%1").arg(nonLsNames.join('\n'))); + QString("\"全量转码\"仅支持 LS_ 开头的功能,以下功能无法加入:\n%1\n\n非LS功能请切到\"按需转码\"工作台添加。") + .arg(nonLsNames.join('\n'))); return false; } @@ -431,7 +432,7 @@ bool BatchConvertPage::saveToCustJson(const QStringList &funcList) QFile file(jsonPath); if (!file.open(QIODevice::ReadOnly)) { LogManager::instance()->logWarning( - QString("业务转码 - 保存配置失败,无法打开: %1").arg(jsonPath)); + QString("全量转码 - 保存配置失败,无法打开: %1").arg(jsonPath)); return false; } @@ -442,7 +443,7 @@ bool BatchConvertPage::saveToCustJson(const QStringList &funcList) static QRegularExpression convertRe(R"("convert"\s*:)"); auto convertMatch = convertRe.match(content); if (!convertMatch.hasMatch()) { - LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 convert 节点"); + LogManager::instance()->logWarning("全量转码 - 保存配置失败,未找到 convert 节点"); return false; } @@ -450,7 +451,7 @@ bool BatchConvertPage::saveToCustJson(const QStringList &funcList) static QRegularExpression dirRe(R"("dir"\s*:\s*\[)"); auto dirMatch = dirRe.match(content, convertMatch.capturedEnd()); if (!dirMatch.hasMatch()) { - LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 dir 数组"); + LogManager::instance()->logWarning("全量转码 - 保存配置失败,未找到 dir 数组"); return false; } @@ -484,7 +485,7 @@ bool BatchConvertPage::saveToCustJson(const QStringList &funcList) // 写回 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { LogManager::instance()->logWarning( - QString("业务转码 - 保存配置失败,无法写入: %1").arg(jsonPath)); + QString("全量转码 - 保存配置失败,无法写入: %1").arg(jsonPath)); return false; } @@ -504,7 +505,7 @@ void BatchConvertPage::loadFromCustJson() QFile file(jsonPath); if (!file.open(QIODevice::ReadOnly)) { LogManager::instance()->logWarning( - QString("业务转码 - 加载配置失败,无法打开: %1").arg(jsonPath)); + QString("全量转码 - 加载配置失败,无法打开: %1").arg(jsonPath)); return; } @@ -515,7 +516,7 @@ void BatchConvertPage::loadFromCustJson() QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) { LogManager::instance()->logWarning( - QString("业务转码 - 加载配置 JSON 解析失败: %1").arg(parseError.errorString())); + QString("全量转码 - 加载配置 JSON 解析失败: %1").arg(parseError.errorString())); return; } @@ -576,6 +577,20 @@ void BatchConvertPage::onConvertFinished() m_startBtn->setStyleSheet("background-color: #1abc9c; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;"); } +void BatchConvertPage::setTaskBusy(bool busy) +{ + if (!m_startBtn) { + return; + } + m_startBtn->setEnabled(!busy); + if (busy) { + m_progressBar->setValue(0); + m_progressLabel->setText("有其他转换任务进行中,请等待完成..."); + } else { + m_progressLabel->setText("等待开始..."); + } +} + void BatchConvertPage::addFunction(const QString &funcName) { if (funcName.isEmpty()) { @@ -591,9 +606,9 @@ void BatchConvertPage::addFunction(const QString &funcName) updateTable(); if (saveToCustJson(m_funcList)) { - QMessageBox::information(nullptr, "提示", QString("已将功能 [%1] 添加到业务转码").arg(funcName)); + QMessageBox::information(nullptr, "提示", QString("已将功能 [%1] 添加到\"全量转码\"").arg(funcName)); } else { - QMessageBox::warning(nullptr, "警告", "添加到业务转码成功,但保存到 cust.json 失败"); + QMessageBox::warning(nullptr, "警告", "添加到\"全量转码\"成功,但保存到 cust.json 失败"); } } diff --git a/src/pages/batchconvert/batchconvertpage.h b/src/pages/batchconvert/batchconvertpage.h index 35012b0..0e81e4d 100644 --- a/src/pages/batchconvert/batchconvertpage.h +++ b/src/pages/batchconvert/batchconvertpage.h @@ -29,7 +29,9 @@ public slots: void onConvertFinished(); void onConvertStarted(); void addFunction(const QString &funcName); - + // 另一工作台任务运行时由 MainWindow 调用,禁用本页开始按钮 + void setTaskBusy(bool busy); + int getFunctionCount() const { return m_funcList.size(); } QStringList getFunctionList() const { return m_funcList; } diff --git a/src/pages/businessconvert/businessconvertpage.cpp b/src/pages/businessconvert/businessconvertpage.cpp new file mode 100644 index 0000000..45c2fa4 --- /dev/null +++ b/src/pages/businessconvert/businessconvertpage.cpp @@ -0,0 +1,55 @@ +#include "businessconvertpage.h" +#include "ElaPivot.h" +#include "src/pages/batchconvert/batchconvertpage.h" +#include "src/pages/nonlsconvert/nonlsconvertpage.h" +#include +#include +#include + +BusinessConvertPage::BusinessConvertPage(QWidget *parent) + : QWidget(parent) + , m_modePivot(nullptr) + , m_stackWidget(nullptr) + , m_lsPage(nullptr) + , m_nonLsPage(nullptr) +{ + initUI(); +} + +void BusinessConvertPage::initUI() +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 12, 0, 0); + layout->setSpacing(8); + + // 顶部模式切换(ElaPivot),与代码检索页风格一致 + QHBoxLayout *modeLayout = new QHBoxLayout; + modeLayout->setContentsMargins(30, 0, 30, 0); + + m_modePivot = new ElaPivot; + m_modePivot->appendPivot("全量转码"); + m_modePivot->appendPivot("按需转码"); + m_modePivot->setTextPixelSize(14); + m_modePivot->setCurrentIndex(0); + connect(m_modePivot, &ElaPivot::pivotClicked, this, &BusinessConvertPage::onModePivotClicked); + + modeLayout->addWidget(m_modePivot); + modeLayout->addStretch(); + layout->addLayout(modeLayout); + + // 两套转码工作台放入堆叠,由 pivot 切换 + m_lsPage = new BatchConvertPage; + m_nonLsPage = new NonLsConvertPage; + + m_stackWidget = new QStackedWidget; + m_stackWidget->addWidget(m_lsPage); + m_stackWidget->addWidget(m_nonLsPage); + layout->addWidget(m_stackWidget, 1); +} + +void BusinessConvertPage::onModePivotClicked(int index) +{ + if (m_stackWidget) { + m_stackWidget->setCurrentIndex(index); + } +} diff --git a/src/pages/businessconvert/businessconvertpage.h b/src/pages/businessconvert/businessconvertpage.h new file mode 100644 index 0000000..a9c96fe --- /dev/null +++ b/src/pages/businessconvert/businessconvertpage.h @@ -0,0 +1,36 @@ +#ifndef BUSINESSCONVERTPAGE_H +#define BUSINESSCONVERTPAGE_H + +#include + +class ElaPivot; +class QStackedWidget; +class BatchConvertPage; +class NonLsConvertPage; + +// 业务转码容器页:用 ElaPivot(类似代码检索的 m_modePivot)切换两种转码工作台 +// Pivot1 全量转码 = BatchConvertPage(整体复用现有实现) +// Pivot2 按需转码 = NonLsConvertPage +class BusinessConvertPage : public QWidget +{ + Q_OBJECT + +public: + explicit BusinessConvertPage(QWidget *parent = nullptr); + + BatchConvertPage *lsPage() const { return m_lsPage; } + NonLsConvertPage *nonLsPage() const { return m_nonLsPage; } + +private slots: + void onModePivotClicked(int index); + +private: + void initUI(); + + ElaPivot *m_modePivot; + QStackedWidget *m_stackWidget; + BatchConvertPage *m_lsPage; + NonLsConvertPage *m_nonLsPage; +}; + +#endif // BUSINESSCONVERTPAGE_H diff --git a/src/pages/functionsearch/uf20functionsearchpage.cpp b/src/pages/functionsearch/uf20functionsearchpage.cpp index 4293a74..a2cd74c 100644 --- a/src/pages/functionsearch/uf20functionsearchpage.cpp +++ b/src/pages/functionsearch/uf20functionsearchpage.cpp @@ -60,7 +60,7 @@ void UF20FunctionSearchPage::initUI() layout->addWidget(m_resultTable); QHBoxLayout *tipLayout = new QHBoxLayout; - QLabel *tipLabel = new QLabel("💡 双击选中行添加转码业务,只支持LS功能添加"); + QLabel *tipLabel = new QLabel("💡 双击选中行加入转码工作台:LS功能 → \"全量转码\";其它功能 → \"按需转码\""); tipLabel->setStyleSheet("color: #666; font-size: 13px;"); tipLayout->addWidget(tipLabel); tipLayout->addStretch(); @@ -113,11 +113,13 @@ void UF20FunctionSearchPage::onSearch() void UF20FunctionSearchPage::onTableDoubleClicked(const QModelIndex &index) { if (index.isValid()) { - QString funcName = m_tableModel->data(m_tableModel->index(index.row(), 0)).toString(); - if (!funcName.startsWith("LS_")) { - QMessageBox::information(this, "提示", "仅支持LS功能添加转码业务"); - return; + const QString funcName = + m_tableModel->data(m_tableModel->index(index.row(), 0)).toString(); + // LS_ 开头 → 全量转码;其余 → 按需转码(由 MainWindow 接到对应工作台) + if (funcName.startsWith("LS_")) { + emit addLsFunctionToConvert(funcName); + } else { + emit addNonLsFunctionToConvert(funcName); } - emit addFunctionToConvert(funcName); } } diff --git a/src/pages/functionsearch/uf20functionsearchpage.h b/src/pages/functionsearch/uf20functionsearchpage.h index 3a72ba4..19d342f 100644 --- a/src/pages/functionsearch/uf20functionsearchpage.h +++ b/src/pages/functionsearch/uf20functionsearchpage.h @@ -16,7 +16,9 @@ public: ~UF20FunctionSearchPage(); signals: - void addFunctionToConvert(const QString& funcName); + // 双击按功能类别分流:LS_ 开头 → 全量转码工作台;其余 → 按需转码工作台 + void addLsFunctionToConvert(const QString& funcName); + void addNonLsFunctionToConvert(const QString& funcName); private slots: void onSearch(); diff --git a/src/pages/nonlsconvert/nonlsconvertpage.cpp b/src/pages/nonlsconvert/nonlsconvertpage.cpp index d510bd4..6863500 100644 --- a/src/pages/nonlsconvert/nonlsconvertpage.cpp +++ b/src/pages/nonlsconvert/nonlsconvertpage.cpp @@ -1,121 +1,776 @@ #include "nonlsconvertpage.h" +#include "src/utils/Constants.h" +#include "src/utils/uf2configreader.h" +#include "src/utils/logmanager.h" #include #include -#include -#include #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ElaPushButton.h" +#include "ElaComboBox.h" + +namespace { + +// 按需转码不做前缀白名单,仅对 LS_ 开头的函数给出“建议放全量转码 Tab”提示 +void adviseAboutLsNames(QWidget *parent, const QStringList &names) +{ + QStringList lsNames; + for (const QString &name : names) { + const QString trimmed = name.trimmed(); + if (trimmed.startsWith("LS_") && !lsNames.contains(trimmed)) { + lsNames.append(trimmed); + } + } + if (!lsNames.isEmpty()) { + QMessageBox::information(parent, "提示", + QString("以下函数以 LS_ 开头,通常属于\"内存交易\"改造范围,\n建议放入\"全量转码\"工作台转码:\n\n%1") + .arg(lsNames.join('\n'))); + } +} + +// 清洗清单项:去首尾空白;若误带 .service_design 后缀则剥离(保持短名形态) +QString normalizeNonLsName(const QString &raw) +{ + QString name = raw.trimmed(); + const QString suffix = ".service_design"; + if (name.endsWith(suffix)) { + name.chop(static_cast(strlen(".service_design"))); + name = name.trimmed(); + } + return name; +} + +// cust_nonls.json convert.dir 值语义:0=否(不递归)、-1=是(递归) +constexpr int RecursionNo = 0; +constexpr int RecursionYes = -1; +// convert.dir 中引擎兜底条目键(非清单功能,加载时跳过、保存时保留),默认值与既有配置样例一致 +constexpr char WildcardDefaultKey[] = "*"; +constexpr int WildcardDefaultValue = RecursionYes; + +// 清单表格列定义 +enum TableCol { ColName = 0, ColExists = 1, ColRecurse = 2, ColAction = 3 }; +constexpr int ActionColumnWidth = 80; +constexpr int RecurseColumnWidth = 100; + +} // namespace + +class NonLsStatusDelegate : public QStyledItemDelegate +{ +public: + explicit NonLsStatusDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {} + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override + { + QVariant bgColor = index.data(Qt::BackgroundRole); + QVariant fgColor = index.data(Qt::ForegroundRole); + + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + + if (bgColor.canConvert()) { + painter->fillRect(opt.rect, bgColor.value()); + } + + if (fgColor.canConvert()) { + painter->setPen(fgColor.value().color()); + } + + painter->drawText(opt.rect, opt.displayAlignment, opt.text); + } +}; NonLsConvertPage::NonLsConvertPage(QWidget *parent) : QWidget(parent) { initUI(); + loadFromCustNonLsJson(); + + m_reloadTimer = new QTimer(this); + m_reloadTimer->setSingleShot(true); + m_reloadTimer->setInterval(300); + connect(m_reloadTimer, &QTimer::timeout, this, &NonLsConvertPage::onReloadTimeout); + + m_lastFileMtime = currentFileMtime(); + setupConfigWatcher(); } NonLsConvertPage::~NonLsConvertPage() { + if (m_configWatcher) { + m_configWatcher->removePaths(m_configWatcher->files()); + m_configWatcher->removePaths(m_configWatcher->directories()); + } } void NonLsConvertPage::initUI() { QVBoxLayout *layout = new QVBoxLayout(this); - layout->setContentsMargins(30, 30, 30, 30); - layout->setSpacing(20); + layout->setContentsMargins(30, 20, 30, 30); + layout->setSpacing(14); - QGroupBox *inputBox = new QGroupBox("输入设置"); - QVBoxLayout *inputLayout = new QVBoxLayout(inputBox); - inputLayout->setSpacing(15); + // 按需转码说明条:非 LS 功能,支持单文件/递归两种模式,不含核心文件与 module 模块文件 + QLabel *modeTipLabel = new QLabel( + "说明:按需转码针对非 LS 功能转码,支持单文件转码与递归转码两种模式," + "转码范围不包含核心文件、module 模块文件。" + "
    " + "
  • 非递归模式:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
  • " + "
  • 递归模式:转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码。
  • " + "
"); + modeTipLabel->setTextFormat(Qt::RichText); + modeTipLabel->setStyleSheet("color: #666; font-size: 12px;"); + modeTipLabel->setWordWrap(true); + layout->addWidget(modeTipLabel); - QHBoxLayout *srcLayout = new QHBoxLayout; - QLabel *srcLabel = new QLabel("源文件:"); - m_srcFileEdit = new QLineEdit; - QPushButton *srcBtn = new QPushButton("浏览"); - connect(srcBtn, &QPushButton::clicked, this, &NonLsConvertPage::onBrowseSrcFile); - srcLayout->addWidget(srcLabel); - srcLayout->addWidget(m_srcFileEdit); - srcLayout->addWidget(srcBtn); - inputLayout->addLayout(srcLayout); + QGroupBox *funcBox = new QGroupBox("转码功能"); + QVBoxLayout *funcLayout = new QVBoxLayout(funcBox); + funcLayout->setSpacing(10); - QHBoxLayout *dstLayout = new QHBoxLayout; - QLabel *dstLabel = new QLabel("输出文件:"); - m_dstFileEdit = new QLineEdit; - QPushButton *dstBtn = new QPushButton("浏览"); - connect(dstBtn, &QPushButton::clicked, this, &NonLsConvertPage::onBrowseDstFile); - dstLayout->addWidget(dstLabel); - dstLayout->addWidget(m_dstFileEdit); - dstLayout->addWidget(dstBtn); - inputLayout->addLayout(dstLayout); + m_tableModel = new QStandardItemModel(this); + m_tableModel->setColumnCount(4); + m_tableModel->setHorizontalHeaderLabels({"UF20函数名", "是否存在", "是否递归", "操作"}); - layout->addWidget(inputBox); + m_funcTable = new ElaTableView; + m_funcTable->setModel(m_tableModel); + m_funcTable->setMinimumHeight(180); + m_funcTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_funcTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); + m_funcTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); + m_funcTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed); + m_funcTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed); + m_funcTable->horizontalHeader()->setStretchLastSection(false); + m_funcTable->setItemDelegateForColumn(ColExists, new NonLsStatusDelegate(this)); + funcLayout->addWidget(m_funcTable); - QGroupBox *optionBox = new QGroupBox("转换选项"); - QVBoxLayout *optionLayout = new QVBoxLayout(optionBox); - optionLayout->setSpacing(15); + QHBoxLayout *tableBtnLayout = new QHBoxLayout; + ElaPushButton *addBtn = new ElaPushButton("添加"); + connect(addBtn, &QPushButton::clicked, this, &NonLsConvertPage::onAddFunction); + ElaPushButton *removeBtn = new ElaPushButton("删除选中"); + connect(removeBtn, &QPushButton::clicked, this, &NonLsConvertPage::onRemoveFunction); + ElaPushButton *clearBtn = new ElaPushButton("清空列表"); + connect(clearBtn, &QPushButton::clicked, this, &NonLsConvertPage::onClearTable); + tableBtnLayout->addWidget(addBtn); + tableBtnLayout->addStretch(); + tableBtnLayout->addWidget(removeBtn); + tableBtnLayout->addWidget(clearBtn); + funcLayout->addLayout(tableBtnLayout); - QHBoxLayout *modeLayout = new QHBoxLayout; - QLabel *modeLabel = new QLabel("转换模式:"); - QComboBox *modeCombo = new QComboBox; - modeCombo->addItems({"非LS转LS", "LS转非LS"}); - modeLayout->addWidget(modeLabel); - modeLayout->addWidget(modeCombo); - optionLayout->addLayout(modeLayout); + layout->addWidget(funcBox); - QHBoxLayout *encodingLayout = new QHBoxLayout; - QLabel *encodingLabel = new QLabel("编码格式:"); - QComboBox *encodingCombo = new QComboBox; - encodingCombo->addItems({"UTF-8", "GBK", "GB2312"}); - encodingLayout->addWidget(encodingLabel); - encodingLayout->addWidget(encodingCombo); - optionLayout->addLayout(encodingLayout); + QGroupBox *progressBox = new QGroupBox("转换进度"); + progressBox->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + QVBoxLayout *progressLayout = new QVBoxLayout(progressBox); + progressLayout->setSpacing(10); - layout->addWidget(optionBox); + m_progressLabel = new ElaText("等待开始..."); + m_progressLabel->setStyleSheet("color: #666; font-size: 12px;"); + m_progressLabel->setFixedHeight(40); + m_progressLabel->setWordWrap(false); + m_progressLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + progressLayout->addWidget(m_progressLabel); + + m_progressBar = new ElaProgressBar; + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + m_progressBar->setFixedHeight(25); + m_progressBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + progressLayout->addWidget(m_progressBar); + + layout->addWidget(progressBox); QHBoxLayout *btnLayout = new QHBoxLayout; - QPushButton *startBtn = new QPushButton("开始转换"); - startBtn->setStyleSheet("background-color: #3498db; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;"); - connect(startBtn, &QPushButton::clicked, this, &NonLsConvertPage::onStartConvert); - QPushButton *previewBtn = new QPushButton("预览"); - connect(previewBtn, &QPushButton::clicked, this, &NonLsConvertPage::onPreview); + m_startBtn = new QPushButton("开始转换"); + m_startBtn->setStyleSheet("background-color: #1abc9c; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;"); + connect(m_startBtn, &QPushButton::clicked, this, &NonLsConvertPage::onStartConvert); btnLayout->addStretch(); - btnLayout->addWidget(startBtn); - btnLayout->addWidget(previewBtn); + btnLayout->addWidget(m_startBtn); + layout->addLayout(btnLayout); +} + +void NonLsConvertPage::onProgressUpdate(int progress, const QString &status) +{ + m_progressBar->setValue(progress); + + if (!status.isEmpty()) { + QString displayStatus = status; + if (displayStatus.length() > 80) { + displayStatus = displayStatus.left(80) + "..."; + } + m_progressLabel->setText(displayStatus); + } +} + +bool NonLsConvertPage::checkFunctionExists(const QString &funcName) +{ + return UF2ConfigReader::checkFuncExistsStatic(funcName); +} + +void NonLsConvertPage::updateTable() +{ + // 不使用 blockSignals:QTableView 依赖 rowsRemoved/rowsInserted 信号更新内部布局缓存, + // 若屏蔽会导致新增行不可见。m_recursionMap 已预填充,值相等时无需写盘。 + m_tableModel->removeRows(0, m_tableModel->rowCount()); + + for (int i = 0; i < m_funcList.size(); ++i) { + const QString funcName = m_funcList[i]; + + QStandardItem *nameItem = new QStandardItem(funcName); + nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); + // LS_ 开头的函数给出黄色提示(不拦截) + if (funcName.startsWith("LS_")) { + nameItem->setData(QBrush(QColor(196, 126, 10)), Qt::ForegroundRole); + } + + bool exists = checkFunctionExists(funcName); + QString statusText = exists ? "是" : "否"; + + QStandardItem *statusItem = new QStandardItem(statusText); + statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable); + statusItem->setTextAlignment(Qt::AlignCenter); + + if (exists) { + statusItem->setData(QBrush(QColor(144, 238, 144)), Qt::BackgroundRole); + statusItem->setData(QBrush(QColor(0, 100, 0)), Qt::ForegroundRole); + } else { + statusItem->setData(QBrush(QColor(255, 182, 193)), Qt::BackgroundRole); + statusItem->setData(QBrush(QColor(139, 0, 0)), Qt::ForegroundRole); + } + + // “是否递归”以行尾下拉呈现:否=0、是=-1(与 cust_nonls.json convert.dir 值一致) + QStandardItem *recItem = new QStandardItem(""); + recItem->setFlags(recItem->flags() & ~Qt::ItemIsEditable); + recItem->setTextAlignment(Qt::AlignCenter); + + QStandardItem *actionItem = new QStandardItem(""); + actionItem->setFlags(actionItem->flags() & ~Qt::ItemIsEditable); + + QList rowItems; + rowItems.append(nameItem); + rowItems.append(statusItem); + rowItems.append(recItem); + rowItems.append(actionItem); + m_tableModel->appendRow(rowItems); + + // “是否递归”下拉控件:否/是,切换即写回内存映射并持久化 + ElaComboBox *recCombo = new ElaComboBox; + recCombo->addItem("否", RecursionNo); + recCombo->addItem("是", RecursionYes); + recCombo->setCurrentIndex( + (m_recursionMap.value(funcName, RecursionNo) == RecursionYes) ? 1 : 0); + connect(recCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, [this, funcName](int index) { + const int recursion = (index == 1) ? RecursionYes : RecursionNo; + if (m_recursionMap.value(funcName, RecursionNo) == recursion) { + return; + } + m_recursionMap.insert(funcName, recursion); + saveToCustNonLsJson(m_funcList); + }); + m_funcTable->setIndexWidget(m_tableModel->index(i, ColRecurse), recCombo); + + QPushButton *editBtn = new QPushButton; + editBtn->setIcon(QIcon(":/resources/images/bianji.png")); + editBtn->setIconSize(QSize(20, 20)); + editBtn->setStyleSheet("padding: 2px; border: none; background-color: transparent;"); + editBtn->setToolTip("修改"); + connect(editBtn, &QPushButton::clicked, this, [this, i]() { + onEditRow(i); + }); + m_funcTable->setIndexWidget(m_tableModel->index(i, ColAction), editBtn); + } + + int totalWidth = m_funcTable->viewport()->width(); + const int fixedWidth = ActionColumnWidth + RecurseColumnWidth; + const int rest = totalWidth - fixedWidth; + m_funcTable->setColumnWidth(ColName, rest * 8 / 10); + m_funcTable->setColumnWidth(ColExists, rest * 2 / 10); + m_funcTable->setColumnWidth(ColRecurse, RecurseColumnWidth); + m_funcTable->setColumnWidth(ColAction, ActionColumnWidth); + + // 视图刷新保险:强制重绘并滚动到顶部,确保新增行可见 + m_funcTable->viewport()->update(); + m_funcTable->resizeRowsToContents(); + m_funcTable->scrollToTop(); + m_funcTable->clearSelection(); +} + +void NonLsConvertPage::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + if (m_funcTable) { + int totalWidth = m_funcTable->width(); + const int fixedWidth = ActionColumnWidth + RecurseColumnWidth; + const int rest = totalWidth - fixedWidth; + m_funcTable->setColumnWidth(ColName, rest * 8 / 10); + m_funcTable->setColumnWidth(ColExists, rest * 2 / 10); + m_funcTable->setColumnWidth(ColRecurse, RecurseColumnWidth); + m_funcTable->setColumnWidth(ColAction, ActionColumnWidth); + } +} + +void NonLsConvertPage::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + updateTable(); +} + +void NonLsConvertPage::onEditRow(int row) +{ + if (row < 0 || row >= m_funcList.size()) { + return; + } + + QString oldName = m_funcList[row]; + QString newName = oldName; + + while (true) { + QInputDialog dialog(this); + dialog.setWindowTitle("修改函数名"); + dialog.setLabelText("请输入新的函数名:"); + dialog.setTextValue(oldName); + dialog.resize(300, 150); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + newName = normalizeNonLsName(dialog.textValue()); + + if (newName.isEmpty()) { + QMessageBox::warning(this, "提示", "函数名不能为空!"); + continue; + } + + if (newName == oldName) { + return; + } + + if (m_funcList.contains(newName)) { + QMessageBox::warning(this, "提示", "该函数名已存在,请重新输入!"); + continue; + } + + break; + } + + const int oldRecursion = m_recursionMap.value(oldName, RecursionNo); + m_recursionMap.remove(oldName); + m_recursionMap.insert(newName, oldRecursion); + + m_funcList[row] = newName; + updateTable(); + if (saveToCustNonLsJson(m_funcList)) { + loadFromCustNonLsJson(); + } + adviseAboutLsNames(this, {newName}); +} + +void NonLsConvertPage::onAddFunction() +{ + QDialog dialog(this); + dialog.setWindowTitle("添加函数"); + dialog.resize(400, 250); + + QVBoxLayout *layout = new QVBoxLayout(&dialog); + + QTextEdit *textEdit = new QTextEdit; + textEdit->setPlaceholderText("每行输入一个函数名(短名,如 AS_xxx)"); + layout->addWidget(textEdit); + + QHBoxLayout *btnLayout = new QHBoxLayout; + QPushButton *okBtn = new QPushButton("确定"); + QPushButton *cancelBtn = new QPushButton("取消"); + btnLayout->addStretch(); + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); layout->addLayout(btnLayout); - m_resultEdit = new QTextEdit; - m_resultEdit->setReadOnly(true); - m_resultEdit->setStyleSheet("background-color: #2c3e50; color: #ecf0f1;"); - m_resultEdit->append("非LS转码工具已准备就绪..."); - layout->addWidget(m_resultEdit); -} + connect(okBtn, &QPushButton::clicked, &dialog, &QDialog::accept); + connect(cancelBtn, &QPushButton::clicked, &dialog, &QDialog::reject); -void NonLsConvertPage::onBrowseSrcFile() -{ - QString file = QFileDialog::getOpenFileName(this, "选择源文件"); - if (!file.isEmpty()) { - m_srcFileEdit->setText(file); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + QString text = textEdit->toPlainText().trimmed(); + if (text.isEmpty()) { + return; + } + + QStringList funcNames = text.split('\n', Qt::SkipEmptyParts); + int addedCount = 0; + int existCount = 0; + QStringList addedNames; + for (const QString &name : funcNames) { + const QString trimmedName = normalizeNonLsName(name); + if (trimmedName.isEmpty()) { + continue; + } + if (m_funcList.contains(trimmedName)) { + existCount++; + continue; + } + m_funcList.append(trimmedName); + m_recursionMap.insert(trimmedName, RecursionNo); // 新增功能默认“否” + addedNames.append(trimmedName); + addedCount++; + } + + if (existCount > 0) { + QMessageBox::information(this, "提示", + QString("成功添加 %1 个函数,%2 个函数已存在").arg(addedCount).arg(existCount)); + } else if (addedCount > 0) { + QMessageBox::information(this, "提示", + QString("成功添加 %1 个函数").arg(addedCount)); + } + + adviseAboutLsNames(this, addedNames); + + updateTable(); + if (addedCount > 0) { + saveToCustNonLsJson(m_funcList); } } -void NonLsConvertPage::onBrowseDstFile() +void NonLsConvertPage::onRemoveFunction() { - QString file = QFileDialog::getSaveFileName(this, "选择输出文件"); - if (!file.isEmpty()) { - m_dstFileEdit->setText(file); + QModelIndexList selectedIndexes = m_funcTable->selectionModel()->selectedRows(); + if (selectedIndexes.isEmpty()) { + QMessageBox::warning(this, "提示", "请先选中要删除的行!"); + return; } + + QList rowsToRemove; + for (const QModelIndex &index : selectedIndexes) { + int row = index.row(); + if (!rowsToRemove.contains(row)) { + rowsToRemove.append(row); + } + } + + std::sort(rowsToRemove.begin(), rowsToRemove.end(), std::greater()); + for (int row : rowsToRemove) { + m_recursionMap.remove(m_funcList.at(row)); + m_funcList.removeAt(row); + } + + updateTable(); + if (saveToCustNonLsJson(m_funcList)) { + loadFromCustNonLsJson(); + } +} + +void NonLsConvertPage::onClearTable() +{ + if (m_funcList.isEmpty()) { + return; + } + + QMessageBox::StandardButton reply; + reply = QMessageBox::question(this, "确认清空", "确定要清空所有函数吗?", + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { + m_funcList.clear(); + m_recursionMap.clear(); + updateTable(); + if (saveToCustNonLsJson(m_funcList)) { + loadFromCustNonLsJson(); + } + } +} + +bool NonLsConvertPage::saveToCustNonLsJson(const QStringList &funcList) +{ + const QString jsonPath = Constants::custNonLsJson(); + + // 保留既有顶层节点,仅更新 convert 节点 + QJsonObject root; + { + QFile file(jsonPath); + if (file.exists() && file.open(QIODevice::ReadOnly)) { + QByteArray data = file.readAll(); + file.close(); + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error == QJsonParseError::NoError && doc.isObject()) { + root = doc.object(); + } else { + LogManager::instance()->logWarning( + QString("按需转码 - 保存前读取既有配置失败(%1),将按当前清单重建: %2") + .arg(parseError.errorString()).arg(jsonPath)); + } + } + } + + // convert.dir 键值表:{功能名: 0|-1};0=否(不递归)、-1=是(递归),并保留/兜底引擎默认键 "*" + const QJsonObject oldDir = root.value("convert").toObject().value("dir").toObject(); + QJsonObject dirObj; + for (const QString &name : funcList) { + dirObj.insert(name, m_recursionMap.value(name, RecursionNo)); + } + if (oldDir.contains(WildcardDefaultKey)) { + dirObj.insert(WildcardDefaultKey, oldDir.value(WildcardDefaultKey)); + } else { + dirObj.insert(WildcardDefaultKey, WildcardDefaultValue); + } + + QJsonArray extArray; + for (const QString &ext : {QStringLiteral(".service_design"), + QStringLiteral(".function_design"), + QStringLiteral(".aservice_design"), + QStringLiteral(".procedure_design"), + QStringLiteral(".afunction_design")}) { + extArray.append(ext); + } + QJsonObject convertObj; + convertObj["dir"] = dirObj; + convertObj["ext"] = extArray; + root["convert"] = convertObj; + + QFile file(jsonPath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LogManager::instance()->logWarning( + QString("按需转码 - 保存配置失败,无法写入: %1").arg(jsonPath)); + return false; + } + QJsonDocument doc(root); + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); + + // 程序自身写入后更新 mtime,避免触发无谓的重载 + m_lastFileMtime = currentFileMtime(); + return true; +} + +void NonLsConvertPage::loadFromCustNonLsJson() +{ + const QString jsonPath = Constants::custNonLsJson(); + if (!QFile::exists(jsonPath)) { + return; // 文件尚不存在(首次使用),保持空清单,不打扰 + } + + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + LogManager::instance()->logWarning( + QString("按需转码 - 加载配置失败,无法打开: %1").arg(jsonPath)); + return; + } + + QByteArray data = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) { + LogManager::instance()->logWarning( + QString("按需转码 - 加载配置 JSON 解析失败: %1").arg(parseError.errorString())); + return; + } + + QJsonObject root = doc.object(); + const QJsonValue dirVal = root["convert"].toObject()["dir"]; + + QStringList items; + m_recursionMap.clear(); + if (dirVal.isObject()) { + // 新版:dir 为 {功能名: 0|-1},0=否(不递归)、-1=是(递归);"*" 为引擎默认键,不展示 + const QJsonObject dirObj = dirVal.toObject(); + for (auto it = dirObj.constBegin(); it != dirObj.constEnd(); ++it) { + if (it.key() == WildcardDefaultKey) { + continue; + } + const QString item = normalizeNonLsName(it.key()); + if (item.isEmpty()) { + continue; + } + items.append(item); + const int v = it.value().toInt(RecursionNo); + m_recursionMap.insert(item, (v == RecursionYes) ? RecursionYes : RecursionNo); + } + } else { + // 兼容旧版:dir 为 ["功能名", ...],无递归标志,一律按“否”加载 + for (const QJsonValue &val : dirVal.toArray()) { + const QString item = normalizeNonLsName(val.toString()); + if (!item.isEmpty()) { + items.append(item); + m_recursionMap.insert(item, RecursionNo); + } + } + } + + m_funcList = items; + updateTable(); } void NonLsConvertPage::onStartConvert() { - m_resultEdit->append("开始转换..."); - m_resultEdit->append("源文件: " + m_srcFileEdit->text()); - m_resultEdit->append("输出文件: " + m_dstFileEdit->text()); - m_resultEdit->append("转换完成!"); + if (m_startBtn->text() == "停止转换") { + m_progressLabel->setText("正在停止转换..."); + emit stopConvert(); + return; + } + + if (m_funcList.isEmpty()) { + QMessageBox::warning(this, "提示", "请先添加需要转码的函数!"); + return; + } + + m_progressBar->setValue(0); + m_progressLabel->setText("正在准备转换..."); + + if (!saveToCustNonLsJson(m_funcList)) { + m_progressLabel->setText("配置保存失败"); + return; + } + + bool ok = false; + m_preparedArgs = buildArguments(&ok); + if (!ok) { + m_progressLabel->setText("参数生成失败"); + return; + } + + m_progressBar->setValue(20); + m_progressLabel->setText("配置加载成功,开始转换..."); + + emit startConvert(); } -void NonLsConvertPage::onPreview() +void NonLsConvertPage::onConvertStarted() { - m_resultEdit->append("预览模式..."); - m_resultEdit->append("源文件: " + m_srcFileEdit->text()); - m_resultEdit->append("预览完成!"); -} \ No newline at end of file + m_startBtn->setText("停止转换"); + m_startBtn->setStyleSheet("background-color: #e74c3c; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;"); +} + +void NonLsConvertPage::onConvertFinished() +{ + m_progressBar->setValue(100); + m_progressLabel->setText("转换完成"); + m_startBtn->setText("开始转换"); + m_startBtn->setStyleSheet("background-color: #1abc9c; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;"); +} + +void NonLsConvertPage::setTaskBusy(bool busy) +{ + if (!m_startBtn) { + return; + } + m_startBtn->setEnabled(!busy); + if (busy) { + m_progressBar->setValue(0); + m_progressLabel->setText("有其他转换任务进行中,请等待完成..."); + } else { + m_progressLabel->setText("等待开始..."); + } +} + +void NonLsConvertPage::addFunction(const QString &funcName) +{ + if (funcName.isEmpty()) { + return; + } + + const QString name = normalizeNonLsName(funcName); + if (m_funcList.contains(name)) { + QMessageBox::information(nullptr, "提示", QString("功能 [%1] 已在列表中").arg(name)); + return; + } + + m_funcList.append(name); + m_recursionMap.insert(name, RecursionNo); // 新增功能默认“否” + updateTable(); + + if (saveToCustNonLsJson(m_funcList)) { + QMessageBox::information(nullptr, "提示", QString("已将功能 [%1] 添加到\"按需转码\"").arg(name)); + } else { + QMessageBox::warning(nullptr, "警告", "添加到\"按需转码\"成功,但保存到 cust_nonls.json 失败"); + } +} + +QStringList NonLsConvertPage::buildArguments(bool *ok) +{ + if (ok) { + *ok = true; + } + + // 引擎契约(新版):仅携带 -m 2 表示“按需转码”模式, + // 函数清单与“是否递归”开关由引擎直接读取 config/cust_nonls.json,无需再传 -t/-o 参数 + return QStringList() << "-m" << "2"; +} + +void NonLsConvertPage::setupConfigWatcher() +{ + const QString configPath = getConfigFilePath(); + + m_configWatcher = new QFileSystemWatcher(this); + + if (QFile::exists(configPath)) { + m_configWatcher->addPath(configPath); + } + + const QString dir = QFileInfo(configPath).absolutePath(); + if (QDir(dir).exists()) { + m_configWatcher->addPath(dir); + } + + connect(m_configWatcher, &QFileSystemWatcher::fileChanged, + this, &NonLsConvertPage::onConfigFileChanged); + connect(m_configWatcher, &QFileSystemWatcher::directoryChanged, + this, &NonLsConvertPage::onConfigDirChanged); +} + +QDateTime NonLsConvertPage::currentFileMtime() const +{ + QFileInfo info(getConfigFilePath()); + return info.exists() ? info.lastModified() : QDateTime(); +} + +void NonLsConvertPage::onConfigFileChanged(const QString &path) +{ + if (m_configWatcher && !m_configWatcher->files().contains(path) + && QFile::exists(path)) { + m_configWatcher->addPath(path); + } + m_reloadTimer->start(); // 防抖 +} + +void NonLsConvertPage::onConfigDirChanged(const QString &dirPath) +{ + Q_UNUSED(dirPath); + const QString configPath = getConfigFilePath(); + if (m_configWatcher && QFile::exists(configPath) + && !m_configWatcher->files().contains(configPath)) { + m_configWatcher->addPath(configPath); + } + m_reloadTimer->start(); // 防抖 +} + +void NonLsConvertPage::onReloadTimeout() +{ + QDateTime now = currentFileMtime(); + if (now.isValid() && now != m_lastFileMtime) { + m_lastFileMtime = now; + loadFromCustNonLsJson(); + } +} + +QString NonLsConvertPage::getConfigFilePath() const +{ + return Constants::custNonLsJson(); +} diff --git a/src/pages/nonlsconvert/nonlsconvertpage.h b/src/pages/nonlsconvert/nonlsconvertpage.h index 4718d57..c752571 100644 --- a/src/pages/nonlsconvert/nonlsconvertpage.h +++ b/src/pages/nonlsconvert/nonlsconvertpage.h @@ -2,9 +2,23 @@ #define NONLSCONVERTPAGE_H #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include "ElaTableView.h" +#include "ElaProgressBar.h" +#include "ElaText.h" +// 按需转码工作台(业务转码页 Pivot2) +// 交互与全量转码页(BatchConvertPage)保持一致: +// 函数清单(短名)与“是否递归”开关 -> 持久化到 config/cust_nonls.json +// convert.dir 为 {功能名: 0| -1} 键值表:0=否(不递归)、-1=是(递归),另含引擎默认键 "*" +// 调用契约:仅携带 -m 2(按需转码模式);函数清单与递归开关由引擎直接读取 cust_nonls.json, +// 不再需要 -t/-o 等命令行参数 class NonLsConvertPage : public QWidget { Q_OBJECT @@ -13,18 +27,58 @@ public: explicit NonLsConvertPage(QWidget *parent = nullptr); ~NonLsConvertPage(); +signals: + void startConvert(); + void stopConvert(); + +public slots: + void onProgressUpdate(int progress, const QString &status); + void onConvertStarted(); + void onConvertFinished(); + void addFunction(const QString &funcName); + // 另一工作台任务运行时由 MainWindow 调用,禁用本页开始按钮 + void setTaskBusy(bool busy); + + int getFunctionCount() const { return m_funcList.size(); } + QStringList getFunctionList() const { return m_funcList; } + // 本次开始转换前已拼装好的引擎参数(由 onStartConvert 生成) + QStringList preparedArguments() const { return m_preparedArgs; } + private slots: - void onBrowseSrcFile(); - void onBrowseDstFile(); void onStartConvert(); - void onPreview(); + void onAddFunction(); + void onRemoveFunction(); + void onClearTable(); + void onEditRow(int row); + void onConfigFileChanged(const QString &path); + void onConfigDirChanged(const QString &path); + void onReloadTimeout(); private: void initUI(); + bool saveToCustNonLsJson(const QStringList &funcList); + void loadFromCustNonLsJson(); + bool checkFunctionExists(const QString &funcName); + void updateTable(); + void resizeEvent(QResizeEvent *event) override; + void showEvent(QShowEvent *event) override; + QString getConfigFilePath() const; + void setupConfigWatcher(); + QDateTime currentFileMtime() const; + QStringList buildArguments(bool *ok); - QLineEdit *m_srcFileEdit; - QLineEdit *m_dstFileEdit; - QTextEdit *m_resultEdit; + ElaTableView *m_funcTable; + QStandardItemModel *m_tableModel; + QStringList m_funcList; + // 每个清单功能对应的“是否递归”标志:0=否、-1=是(与 cust_nonls.json convert.dir 值一致) + QHash m_recursionMap; + ElaProgressBar *m_progressBar; + ElaText *m_progressLabel; + QPushButton *m_startBtn; + QFileSystemWatcher *m_configWatcher; + QTimer *m_reloadTimer; + QDateTime m_lastFileMtime; + QStringList m_preparedArgs; }; -#endif // NONLSCONVERTPAGE_H \ No newline at end of file +#endif // NONLSCONVERTPAGE_H diff --git a/src/utils/Constants.h b/src/utils/Constants.h index 61694d4..8e90232 100644 --- a/src/utils/Constants.h +++ b/src/utils/Constants.h @@ -38,6 +38,7 @@ namespace Paths { inline const QString ConfigDir = "config"; // 配置文件目录(toolDir 下) inline const QString ChangeCodeDir = "change_code"; // 代码检索根目录(toolDir 下) inline const QString FunctionOutput = "function_output"; // 调用链/文件导出目录 + inline const QString TranslateOut = "translate_out"; // 按需转码缺省输出子目录(createPath 下) } // --------------------------------------------------------------------------- @@ -51,6 +52,8 @@ namespace ToolFiles { inline const QString Uft3Trans = "uft3Trans.json"; inline const QString CustJson = "config/cust.json"; inline const QString Cust2Json = "config/cust2.json"; + inline const QString CustNonLsJson = "config/cust_nonls.json"; // 按需转码 UI 清单持久化 + inline const QString NonLsRunList = "config/run_nonls_list.txt"; // 大批量时生成的 @列表文件 } // --------------------------------------------------------------------------- @@ -113,6 +116,8 @@ inline QString toolDir() { return QCoreApplication::applicationDirPath() + " inline QString toolExe() { return toolDir() + "/" + ToolFiles::Exe; } inline QString configIni() { return toolDir() + "/" + ToolFiles::ConfigIni; } inline QString custJson() { return toolDir() + "/" + ToolFiles::CustJson; } +inline QString custNonLsJson() { return toolDir() + "/" + ToolFiles::CustNonLsJson; } +inline QString nonLsRunListFile() { return toolDir() + "/" + ToolFiles::NonLsRunList; } inline QString logDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::LogDir; } inline QString cacheDir() { return toolDir() + "/" + Paths::CacheDir; } // bin/uf2touft3/cache inline QString uf20CacheDb() { return cacheDir() + "/" + Cache::Uf20DbFile; } diff --git a/src/utils/configreaderbase.cpp b/src/utils/configreaderbase.cpp index 8643e7d..15c146c 100644 --- a/src/utils/configreaderbase.cpp +++ b/src/utils/configreaderbase.cpp @@ -68,6 +68,17 @@ bool ConfigReaderBase::doLoadCnameCache(const ConfigReaderTraits& traits, } cache = traits.getAllCnames(); + + // 接口表为空(首次启动或元数据更新尚未落库完成)时数据尚未就绪: + // 此时不标记为“已加载”, 保留 cacheLoaded=false 以便后续查询重新加载, + // 避免把这一时刻的空结果长期固化为“功能不存在”。 + const bool dataReady = traits.hasConfig ? traits.hasConfig(traits.mainFile) : true; + if (cache.isEmpty() && !dataReady) { + LogManager::instance()->logInfo( + QString("%1 Cname缓存暂未加载 - 接口表为空, 数据未就绪, 后续查询将重试").arg(traits.logTag)); + return false; + } + cacheLoaded = true; return true; } @@ -78,6 +89,11 @@ bool ConfigReaderBase::doCheckFuncExists(const ConfigReaderTraits& traits, const if (!cacheLoaded) { doLoadCnameCache(traits, cache, cacheLoaded); } + if (cache.isEmpty()) { + // 内存缓存为空(数据尚未就绪/元数据更新进行中): 直接查库兜底, + // 避免把“缓存没准备好”误判为“功能不存在”。 + return traits.checkExists ? traits.checkExists(funcName) : false; + } return cache.contains(funcName); } diff --git a/src/utils/configreaderbase.h b/src/utils/configreaderbase.h index 6b39cc4..5e2c230 100644 --- a/src/utils/configreaderbase.h +++ b/src/utils/configreaderbase.h @@ -22,6 +22,9 @@ struct ConfigReaderTraits std::function hasConfig; // DataCache 是否已缓存该配置 std::function saveConfig; // DataCache 保存配置 std::function()> getAllCnames; // DataCache 读取全部 cname + // 单行直查(绕过 cname 内存缓存): 用于数据尚未就绪、内存缓存为空时兜底, + // 避免把“缓存没准备好”误判为“功能不存在” + std::function checkExists; }; // 模板方法模式的公共基类:承载 UF2/UFT3 配置读取的共享流程。 diff --git a/src/utils/uf2configreader.cpp b/src/utils/uf2configreader.cpp index 42c2406..5727d92 100644 --- a/src/utils/uf2configreader.cpp +++ b/src/utils/uf2configreader.cpp @@ -24,6 +24,9 @@ const ConfigReaderTraits& UF2ConfigReader::traits() traits.getAllCnames = [] { return DataCache::instance()->getAllUF20Cnames(); }; + traits.checkExists = [](const QString& cname) { + return DataCache::instance()->checkUF20CnameExists(cname); + }; return traits; }(); return t; diff --git a/src/utils/uft3configreader.cpp b/src/utils/uft3configreader.cpp index 16c9125..4e9c1d1 100644 --- a/src/utils/uft3configreader.cpp +++ b/src/utils/uft3configreader.cpp @@ -1,5 +1,6 @@ #include "uft3configreader.h" #include "datacache.h" +#include "Constants.h" QSet UFT3ConfigReader::m_cnameCache; bool UFT3ConfigReader::m_cnameCacheLoaded = false; @@ -27,6 +28,13 @@ const ConfigReaderTraits& UFT3ConfigReader::traits() traits.getAllCnames = [] { return DataCache::instance()->getAllUFT3Cnames(); }; + traits.checkExists = [](const QString& cname) { + DataCache *cache = DataCache::instance(); + // 与 getAllUFT3Cnames 覆盖范围保持一致: interface + trans_code 两张表 + return cache->checkUFT3CnameExists(cname) + || !cache->getRowData(QStringLiteral("uft3"), + Constants::Cache::TblUft3TransCode, cname).isEmpty(); + }; return traits; }(); return t;