From 1f33f0fbaa5bc3006523caa83487b11c4ebfceda Mon Sep 17 00:00:00 2001 From: taocong Date: Thu, 2 Jul 2026 19:54:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=95=B0=E6=8D=AE=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E9=A1=B5=E9=9D=A2=EF=BC=8C=E8=A1=A8=E6=98=A0=E5=B0=84?= =?UTF-8?q?=E3=80=81=E5=87=BD=E6=95=B0=E6=98=A0=E5=B0=84;=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Uft30ChangeCode.pro | 15 +- help/Writerside/.idea/vcs.xml | 6 + help/Writerside/hi.tree | 2 + help/Writerside/topics/update-1-1-0.md | 3 + help/Writerside/topics/update-1-2-0.md | 5 + help/Writerside/topics/下载.md | 8 +- help/Writerside/下载.html | 16 + help/Writerside/更新记录.html | 16 + src/mainwindow/mainwindow.cpp | 72 ++- src/mainwindow/mainwindow.h | 7 + src/metadataupdate/metadataprocessor.cpp | 3 +- src/pages/about/aboutdialog.cpp | 3 +- src/pages/batchconvert/batchconvertpage.cpp | 260 +++++----- src/pages/batchconvert/batchconvertpage.h | 9 +- src/pages/datamapping/functionmappingpage.cpp | 449 ++++++++++++++++++ src/pages/datamapping/functionmappingpage.h | 46 ++ src/pages/datamapping/tablemappingpage.cpp | 449 ++++++++++++++++++ src/pages/datamapping/tablemappingpage.h | 46 ++ src/pages/help/helpdocpage.cpp | 26 - src/pages/help/helpdocpage.h | 34 -- src/pages/metadatupdate/metadatupdatepage.cpp | 10 +- src/pages/settings/settingspage.cpp | 3 +- src/utils/Constants.h | 72 +++ src/utils/datacache.cpp | 11 +- src/utils/logmanager.cpp | 5 +- src/utils/uf2configreader.cpp | 4 +- src/utils/uft3configreader.cpp | 4 +- src/utils/version.cpp | 25 +- src/utils/version.h | 4 +- src/utils/versionchecker.cpp | 78 +++ src/utils/versionchecker.h | 29 ++ 31 files changed, 1506 insertions(+), 214 deletions(-) create mode 100644 help/Writerside/.idea/vcs.xml create mode 100644 help/Writerside/topics/update-1-1-0.md create mode 100644 help/Writerside/topics/update-1-2-0.md create mode 100644 help/Writerside/下载.html create mode 100644 help/Writerside/更新记录.html create mode 100644 src/pages/datamapping/functionmappingpage.cpp create mode 100644 src/pages/datamapping/functionmappingpage.h create mode 100644 src/pages/datamapping/tablemappingpage.cpp create mode 100644 src/pages/datamapping/tablemappingpage.h delete mode 100644 src/pages/help/helpdocpage.cpp delete mode 100644 src/pages/help/helpdocpage.h create mode 100644 src/utils/Constants.h create mode 100644 src/utils/versionchecker.cpp create mode 100644 src/utils/versionchecker.h diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro index ea56e69..a2d8edd 100644 --- a/Uft30ChangeCode.pro +++ b/Uft30ChangeCode.pro @@ -1,4 +1,4 @@ -QT += core gui widgets sql +QT += core gui widgets sql network TARGET = Uft30ChangeCode TEMPLATE = app @@ -15,9 +15,10 @@ SOURCES += main.cpp \ src/pages/functionsearch/uft3functionsearchpage.cpp \ src/components/elasearchedit.cpp \ src/components/interdialog.cpp \ - src/pages/help/helpdocpage.cpp \ src/pages/settings/settingspage.cpp \ src/pages/metadatupdate/metadatupdatepage.cpp \ + src/pages/datamapping/tablemappingpage.cpp \ + src/pages/datamapping/functionmappingpage.cpp \ src/metadataupdate/filedb.cpp \ src/metadataupdate/uf2interface.cpp \ src/metadataupdate/uft3interface.cpp \ @@ -33,7 +34,8 @@ SOURCES += main.cpp \ src/utils/uft3configreader.cpp \ src/utils/logmanager.cpp \ src/utils/configmanager.cpp \ - src/utils/version.cpp + src/utils/version.cpp \ + src/utils/versionchecker.cpp HEADERS += src/mainwindow/mainwindow.h \ src/pythonrunner/PythonRunner.h \ @@ -44,9 +46,10 @@ HEADERS += src/mainwindow/mainwindow.h \ src/pages/functionsearch/uft3functionsearchpage.h \ src/components/elasearchedit.h \ src/components/interdialog.h \ - src/pages/help/helpdocpage.h \ src/pages/settings/settingspage.h \ src/pages/metadatupdate/metadatupdatepage.h \ + src/pages/datamapping/tablemappingpage.h \ + src/pages/datamapping/functionmappingpage.h \ src/metadataupdate/filedb.h \ src/metadataupdate/uf2interface.h \ src/metadataupdate/uft3interface.h \ @@ -62,7 +65,8 @@ HEADERS += src/mainwindow/mainwindow.h \ src/utils/uft3configreader.h \ src/utils/logmanager.h \ src/utils/configmanager.h \ - src/utils/version.h + src/utils/version.h \ + src/utils/versionchecker.h RESOURCES += resources.qrc @@ -86,4 +90,3 @@ OBJECTS_DIR = $$PWD/build MOC_DIR = $$PWD/build RCC_DIR = $$PWD/build - diff --git a/help/Writerside/.idea/vcs.xml b/help/Writerside/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/help/Writerside/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/help/Writerside/hi.tree b/help/Writerside/hi.tree index 5524af8..51d3e97 100644 --- a/help/Writerside/hi.tree +++ b/help/Writerside/hi.tree @@ -17,6 +17,8 @@ + + diff --git a/help/Writerside/topics/update-1-1-0.md b/help/Writerside/topics/update-1-1-0.md new file mode 100644 index 0000000..7c81337 --- /dev/null +++ b/help/Writerside/topics/update-1-1-0.md @@ -0,0 +1,3 @@ +# 1.1.0 + +1.支持2.0账户功能查询及业务转码 \ No newline at end of file diff --git a/help/Writerside/topics/update-1-2-0.md b/help/Writerside/topics/update-1-2-0.md new file mode 100644 index 0000000..05414d0 --- /dev/null +++ b/help/Writerside/topics/update-1-2-0.md @@ -0,0 +1,5 @@ +# 1.2.0 + +1.新增数据映射页面,表映射、函数映射 + +2.新增版本更新提示功能 \ No newline at end of file diff --git a/help/Writerside/topics/下载.md b/help/Writerside/topics/下载.md index fea3da1..ee04ecd 100644 --- a/help/Writerside/topics/下载.md +++ b/help/Writerside/topics/下载.md @@ -12,7 +12,9 @@ ## 下载 -| 版本 | 更新日期 | 全量包 | 增量包 | -|------|------------|---------------------------------------------------------------------------------|-----| -| 1.0.0 | 2026-06-01 | [Uft30ChangeCode-1.0.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.0.0.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.1.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) | +| 1.0.0 | 2026-06-01 | [Uft30ChangeCode-1.0.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.0.0.zip) | | diff --git a/help/Writerside/下载.html b/help/Writerside/下载.html new file mode 100644 index 0000000..6dd8da1 --- /dev/null +++ b/help/Writerside/下载.html @@ -0,0 +1,16 @@ + +下载 | Uft30ChangeCode

Uft30ChangeCode Help

下载

全量更新

下载全量包,解压即用。

增量更新

下载增量包,解压后替换原文件即可

下载

版本

更新日期

全量包

增量包

1.1.0

2026-07-02

Uft30ChangeCode-1.1.0.zip

Uft30ChangeCode-1.1.0-update.zip

1.0.0

2026-06-01

Uft30ChangeCode-1.0.0.zip

Last modified: 02 七月 2026
\ No newline at end of file diff --git a/help/Writerside/更新记录.html b/help/Writerside/更新记录.html new file mode 100644 index 0000000..04f202a --- /dev/null +++ b/help/Writerside/更新记录.html @@ -0,0 +1,16 @@ + +更新记录 | Uft30ChangeCode

Uft30ChangeCode Help

\ No newline at end of file diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index 4ed715d..29c6e44 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -6,6 +6,8 @@ #include "utils/uf2configreader.h" #include "utils/uft3configreader.h" #include "utils/logmanager.h" +#include "utils/Constants.h" +#include "utils/versionchecker.h" #include #include #include @@ -25,6 +27,12 @@ #include #include +namespace { + const QString UPDATE_JSON = Constants::Urls::updateInfo(); + const QString DOWNLOAD_PAGE = Constants::Urls::downloadPage(); + const QString HELP_PAGE = Constants::Urls::helpPage(); +} + MainWindow::MainWindow(QWidget *parent) : ElaWindow(parent) , m_pythonRunner(new PythonRunner(this)) @@ -38,14 +46,12 @@ MainWindow::MainWindow(QWidget *parent) LogManager::instance()->initLogFile(); LogManager::instance()->logInfo("应用程序启动"); - QString appDir = QCoreApplication::applicationDirPath(); - loadConfigCache(); - + m_pythonRunner->setRunMode(PythonRunner::UsePackagedExe); - QString exePath = appDir + "/uf2touft3/uf2touft3.exe"; - QString workDir = appDir + "/uf2touft3"; + QString exePath = Constants::toolExe(); + QString workDir = Constants::toolDir(); m_pythonRunner->setPackagedExePath(exePath); m_pythonRunner->setWorkingDirectory(workDir); @@ -62,6 +68,9 @@ MainWindow::MainWindow(QWidget *parent) connect(this, &ElaWindow::navigationNodeClicked, this, &MainWindow::onNavigationNodeClicked); QTimer::singleShot(100, this, &MainWindow::updateNavigationButtonIcon); + + // 启动后延迟检查版本更新 + QTimer::singleShot(3000, this, &MainWindow::checkForUpdates); } void MainWindow::loadConfigCache() @@ -126,6 +135,9 @@ void MainWindow::initContent() addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus); expandNavigationNode(m_funcConvertKey); + addExpanderNode("数据映射", m_dataMappingKey, ElaIconType::TableList); + addPageNode("表替换", createTableMappingPage(), m_dataMappingKey, ElaIconType::TableColumns); + addPageNode("函数替换", createFunctionMappingPage(), m_dataMappingKey, ElaIconType::Function); addPageNode("元数据更新", createMetadataUpdatePage(), ElaIconType::ArrowsRotate); addFooterNode("帮助", m_helpKey, 0, ElaIconType::CircleQuestion); @@ -169,6 +181,16 @@ QWidget* MainWindow::createMetadataUpdatePage() return new MetadataUpdatePage(); } +QWidget* MainWindow::createTableMappingPage() +{ + return new TableMappingPage(); +} + +QWidget* MainWindow::createFunctionMappingPage() +{ + return new FunctionMappingPage(); +} + void MainWindow::onBatchConvertStart() { if (m_pythonRunner->isRunning()) { @@ -177,8 +199,7 @@ void MainWindow::onBatchConvertStart() return; } - QString appDir = QCoreApplication::applicationDirPath(); - QString exePath = appDir + "/uf2touft3/uf2touft3.exe"; + QString exePath = Constants::toolExe(); if (!m_pythonRunner->canRun()) { LogManager::instance()->logError("错误: 找不到 uf2touft3/uf2touft3.exe!"); @@ -382,11 +403,46 @@ void MainWindow::onNavigationButtonClicked() m_navigationButton->update(); } +void MainWindow::checkForUpdates() +{ + auto *checker = new VersionChecker(this); + connect(checker, &VersionChecker::newVersionAvailable, this, &MainWindow::onNewVersionFound); + connect(checker, &VersionChecker::checkError, this, [](const QString &msg) { + LogManager::instance()->logInfo("版本检查: " + msg); + }); + connect(checker, &VersionChecker::noUpdateNeeded, this, []() { + LogManager::instance()->logInfo("版本检查: 已是最新版本"); + }); + checker->check(UPDATE_JSON, Version::getVersionString()); +} + +void MainWindow::onNewVersionFound(const QString &latestVersion, const QString &releaseNotes) +{ + LogManager::instance()->logInfo(QString("发现新版本: %1").arg(latestVersion)); + + QString msg = QString("检测到新版本 %1\n当前版本: %2\n") + .arg(latestVersion, Version::getVersionString()); + + if (!releaseNotes.isEmpty()) { + msg += QString("\n──────── 更新说明 ────────\n%1\n").arg(releaseNotes); + } + + msg += "\n是否前往下载最新版本?"; + + QMessageBox::StandardButton reply = QMessageBox::question( + this, "发现新版本", msg, + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + QDesktopServices::openUrl(QUrl(DOWNLOAD_PAGE)); + } +} + void MainWindow::onNavigationNodeClicked(ElaNavigationType::NavigationNodeType nodeType, QString nodeKey) { Q_UNUSED(nodeType); if (nodeKey == m_helpKey) { - QDesktopServices::openUrl(QUrl("http://10.20.163.105:6045/uft3changecode")); + QDesktopServices::openUrl(QUrl(HELP_PAGE)); } else if (nodeKey == m_aboutKey) { std::make_unique(this)->exec(); } diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h index 2ed51fb..deec0c4 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -13,6 +13,8 @@ #include "src/pages/about/aboutdialog.h" #include "src/pages/settings/settingspage.h" #include "src/pages/metadatupdate/metadatupdatepage.h" +#include "src/pages/datamapping/tablemappingpage.h" +#include "src/pages/datamapping/functionmappingpage.h" #include "src/utils/version.h" class MainWindow : public ElaWindow @@ -33,6 +35,8 @@ private: QWidget* createUFT3FunctionSearchPage(); QWidget* createSettingsPage(); QWidget* createMetadataUpdatePage(); + QWidget* createTableMappingPage(); + QWidget* createFunctionMappingPage(); void loadConfigCache(); @@ -42,6 +46,8 @@ private: void onPythonRunnerOutput(const QString &output); void onPythonRunnerError(const QString &error); + void checkForUpdates(); + void onNewVersionFound(const QString &latestVersion, const QString &releaseNotes); void updateNavigationButtonIcon(); bool eventFilter(QObject *obj, QEvent *event) override; void onNavigationButtonClicked(); @@ -61,6 +67,7 @@ private: QString m_settingKey; QString m_funcSearchKey; QString m_funcConvertKey; + QString m_dataMappingKey; }; #endif // MAINWINDOW_H \ No newline at end of file diff --git a/src/metadataupdate/metadataprocessor.cpp b/src/metadataupdate/metadataprocessor.cpp index 12ce046..1d44b56 100644 --- a/src/metadataupdate/metadataprocessor.cpp +++ b/src/metadataupdate/metadataprocessor.cpp @@ -1,4 +1,5 @@ #include "metadataprocessor.h" +#include "utils/Constants.h" #include "uf2interface.h" #include "uft3interface.h" #include "filedb.h" @@ -22,7 +23,7 @@ MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent) void MetadataProcessor::initConfig(const QString& basePath) { m_basePath = basePath; - m_uf2touft3Path = QDir(basePath).filePath("uf2touft3"); + m_uf2touft3Path = Constants::toolDir(); LogManager::instance()->log("========== 初始化配置 =========="); diff --git a/src/pages/about/aboutdialog.cpp b/src/pages/about/aboutdialog.cpp index 05a1087..a65818a 100644 --- a/src/pages/about/aboutdialog.cpp +++ b/src/pages/about/aboutdialog.cpp @@ -1,4 +1,5 @@ #include "aboutdialog.h" +#include "utils/Constants.h" #include "ElaImageCard.h" #include "ElaText.h" @@ -54,7 +55,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent) auto* downloadBtn = new ElaPushButton("下载最新", this); connect(downloadBtn, &ElaPushButton::clicked, [] { - QDesktopServices::openUrl(QUrl("http://10.20.163.105:6045/uft3changecode/下载.html")); + QDesktopServices::openUrl(QUrl(Constants::Urls::downloadPage())); }); addFirstButton(downloadBtn); diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp index c94d996..d8c2277 100644 --- a/src/pages/batchconvert/batchconvertpage.cpp +++ b/src/pages/batchconvert/batchconvertpage.cpp @@ -1,9 +1,16 @@ #include "batchconvertpage.h" +#include "src/utils/Constants.h" #include "src/utils/uf2configreader.h" #include #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -48,19 +55,24 @@ BatchConvertPage::BatchConvertPage(QWidget *parent) { initUI(); loadFromCustJson(); - - m_configWatcher = new QFileSystemWatcher(this); - QString configPath = getConfigFilePath(); - if (!configPath.isEmpty() && QFile::exists(configPath)) { - m_configWatcher->addPath(configPath); - connect(m_configWatcher, &QFileSystemWatcher::fileChanged, this, &BatchConvertPage::onConfigFileChanged); - } + + // 防抖定时器:编辑器保存可能触发多次信号,合并为一次重载 + m_reloadTimer = new QTimer(this); + m_reloadTimer->setSingleShot(true); + m_reloadTimer->setInterval(300); + connect(m_reloadTimer, &QTimer::timeout, this, &BatchConvertPage::onReloadTimeout); + + // 记录初始 mtime + m_lastFileMtime = currentFileMtime(); + + setupConfigWatcher(); } BatchConvertPage::~BatchConvertPage() { if (m_configWatcher) { m_configWatcher->removePaths(m_configWatcher->files()); + m_configWatcher->removePaths(m_configWatcher->directories()); } } @@ -366,140 +378,112 @@ void BatchConvertPage::onClearTable() bool BatchConvertPage::saveToCustJson(const QStringList &funcList) { - QString jsonPath = QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json"; - + QString jsonPath = Constants::custJson(); + + // 读取原始文件内容(保留全部格式不重序列化) QFile file(jsonPath); if (!file.open(QIODevice::ReadOnly)) { + LogManager::instance()->logWarning( + QString("业务转码 - 保存配置失败,无法打开: %1").arg(jsonPath)); return false; } - - QByteArray data = file.readAll(); + + QString content = QString::fromUtf8(file.readAll()); file.close(); - - QString content = QString::fromUtf8(data); - - int convertStart = content.indexOf("\"convert\":"); - if (convertStart < 0) { - convertStart = content.indexOf("\"convert\" :"); - } - if (convertStart < 0) { + + // 用正则定位 "convert" 节点(兼容任意空格) + static QRegularExpression convertRe(R"("convert"\s*:)"); + auto convertMatch = convertRe.match(content); + if (!convertMatch.hasMatch()) { + LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 convert 节点"); return false; } - - int dirStart = content.indexOf("\"dir\":[", convertStart); - if (dirStart < 0) { - dirStart = content.indexOf("\"dir\" : [", convertStart); - } - if (dirStart < 0) { + + // 在 convert 之后定位 "dir" 数组起始 [ + static QRegularExpression dirRe(R"("dir"\s*:\s*\[)"); + auto dirMatch = dirRe.match(content, convertMatch.capturedEnd()); + if (!dirMatch.hasMatch()) { + LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 dir 数组"); return false; } - - int arrayStart = content.indexOf('[', dirStart); + + int bracketStart = dirMatch.capturedEnd() - 1; // 指向 '[' + + // 括号计数找到匹配的 ']' int bracketCount = 1; - int pos = arrayStart + 1; + int pos = bracketStart + 1; while (pos < content.length() && bracketCount > 0) { - if (content[pos] == '[') bracketCount++; - else if (content[pos] == ']') bracketCount--; + QChar ch = content[pos]; + if (ch == '[') bracketCount++; + else if (ch == ']') bracketCount--; pos++; } - int arrayEnd = pos; - - QString newDirArray = "["; + int bracketEnd = pos; // 指向 ']' 之后一位 + + // 构建新数组(保持原始缩进风格:项 12 空格,闭合 ] 8 空格) + QString newArray = "["; for (int i = 0; i < funcList.size(); ++i) { - if (i > 0) newDirArray += ","; - newDirArray += "\n \"" + funcList[i] + ".service_design\""; + if (i > 0) newArray += ","; + newArray += "\n \"" + funcList[i] + ".service_design\""; } - newDirArray += "\n ]"; - - content.replace(arrayStart, arrayEnd - arrayStart, newDirArray); - + if (!funcList.isEmpty()) { + newArray += "\n "; + } + newArray += "]"; + + // 只替换 [bracketStart, bracketEnd) 区间,其余内容原样保留 + content.replace(bracketStart, bracketEnd - bracketStart, newArray); + + // 写回 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LogManager::instance()->logWarning( + QString("业务转码 - 保存配置失败,无法写入: %1").arg(jsonPath)); return false; } - + file.write(content.toUtf8()); file.close(); - + + // 程序自身写入后更新 mtime,避免触发无谓的重载 + m_lastFileMtime = currentFileMtime(); + return true; } void BatchConvertPage::loadFromCustJson() { - QString jsonPath = QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json"; - + QString jsonPath = Constants::custJson(); + QFile file(jsonPath); - int retryCount = 0; - const int maxRetries = 3; - const int retryDelayMs = 100; - - while (!file.open(QIODevice::ReadOnly) && retryCount < maxRetries) { - retryCount++; - QThread::msleep(retryDelayMs); - } - - if (!file.isOpen()) { + if (!file.open(QIODevice::ReadOnly)) { + LogManager::instance()->logWarning( + QString("业务转码 - 加载配置失败,无法打开: %1").arg(jsonPath)); return; } - + QByteArray data = file.readAll(); file.close(); - - QString content = QString::fromUtf8(data); - - int convertStart = content.indexOf("\"convert\":"); - if (convertStart < 0) { - convertStart = content.indexOf("\"convert\" :"); - } - if (convertStart < 0) { + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) { + LogManager::instance()->logWarning( + QString("业务转码 - 加载配置 JSON 解析失败: %1").arg(parseError.errorString())); return; } - - int dirStart = content.indexOf("\"dir\":[", convertStart); - if (dirStart < 0) { - dirStart = content.indexOf("\"dir\" : [", convertStart); - } - if (dirStart < 0) { - return; - } - - int arrayStart = content.indexOf('[', dirStart); - - int bracketCount = 1; - int pos = arrayStart + 1; - while (pos < content.length() && bracketCount > 0) { - if (content[pos] == '[') bracketCount++; - else if (content[pos] == ']') bracketCount--; - pos++; - } - int arrayEnd = pos; - - QString arrayContent = content.mid(arrayStart + 1, arrayEnd - arrayStart - 1).trimmed(); - - if (arrayContent.isEmpty() || arrayContent == "null") { - return; - } - + + QJsonObject root = doc.object(); + QJsonArray dir = root["convert"].toObject()["dir"].toArray(); + QStringList items; - int itemPos = 0; - while (itemPos < arrayContent.length()) { - int itemStart = arrayContent.indexOf('"', itemPos); - if (itemStart < 0) break; - - int itemEnd = arrayContent.indexOf('"', itemStart + 1); - if (itemEnd < 0) break; - - QString item = arrayContent.mid(itemStart + 1, itemEnd - itemStart - 1); - items.append(item); - - itemPos = itemEnd + 1; - } - - for (QString &item : items) { + for (const QJsonValue &val : dir) { + QString item = val.toString(); if (item.endsWith(".service_design")) { - item = item.left(item.length() - static_cast(strlen(".service_design"))); + item.chop(static_cast(strlen(".service_design"))); } + items.append(item); } - + m_funcList = items; updateTable(); } @@ -566,17 +550,71 @@ void BatchConvertPage::addFunction(const QString &funcName) } } +void BatchConvertPage::setupConfigWatcher() +{ + QString configPath = getConfigFilePath(); + if (configPath.isEmpty()) { + return; + } + + m_configWatcher = new QFileSystemWatcher(this); + + // 1. 监视文件本身(覆盖直接编辑场景) + if (QFile::exists(configPath)) { + m_configWatcher->addPath(configPath); + } + + // 2. 监视文件所在目录(覆盖编辑器原子替换:写临时文件→删除→重命名) + // Windows 上 fileChanged 在文件被替换后常失效,directoryChanged 更可靠 + QString dir = QFileInfo(configPath).absolutePath(); + if (QDir(dir).exists()) { + m_configWatcher->addPath(dir); + } + + connect(m_configWatcher, &QFileSystemWatcher::fileChanged, + this, &BatchConvertPage::onConfigFileChanged); + connect(m_configWatcher, &QFileSystemWatcher::directoryChanged, + this, &BatchConvertPage::onConfigDirChanged); +} + +QDateTime BatchConvertPage::currentFileMtime() const +{ + QFileInfo info(getConfigFilePath()); + return info.exists() ? info.lastModified() : QDateTime(); +} + void BatchConvertPage::onConfigFileChanged(const QString &path) -{ - QStringList oldList = m_funcList; - loadFromCustJson(); - - if (m_configWatcher && !m_configWatcher->files().contains(path)) { +{ + // 文件可能被编辑器替换导致监视失效,尝试重新添加 + if (m_configWatcher && !m_configWatcher->files().contains(path) + && QFile::exists(path)) { m_configWatcher->addPath(path); } + m_reloadTimer->start(); // 防抖 +} + +void BatchConvertPage::onConfigDirChanged(const QString & /*dirPath*/) +{ + // 目录变化时重新监视文件(可能之前文件不存在/被替换) + QString configPath = getConfigFilePath(); + if (m_configWatcher && QFile::exists(configPath) + && !m_configWatcher->files().contains(configPath)) { + m_configWatcher->addPath(configPath); + } + m_reloadTimer->start(); // 防抖 +} + +void BatchConvertPage::onReloadTimeout() +{ + // 比较 mtime,避免无谓重载 + QDateTime now = currentFileMtime(); + if (now.isValid() && now != m_lastFileMtime) { + m_lastFileMtime = now; + loadFromCustJson(); + } } QString BatchConvertPage::getConfigFilePath() const { - return QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json"; + return Constants::custJson(); } diff --git a/src/pages/batchconvert/batchconvertpage.h b/src/pages/batchconvert/batchconvertpage.h index fe7060a..87e10bf 100644 --- a/src/pages/batchconvert/batchconvertpage.h +++ b/src/pages/batchconvert/batchconvertpage.h @@ -5,7 +5,8 @@ #include #include #include -#include +#include +#include #include "ElaLineEdit.h" #include "ElaTableView.h" #include "ElaProgressBar.h" @@ -39,6 +40,8 @@ private slots: void onClearTable(); void onEditRow(int row); void onConfigFileChanged(const QString &path); + void onConfigDirChanged(const QString &path); + void onReloadTimeout(); private: void initUI(); @@ -48,6 +51,8 @@ private: void updateTable(); void resizeEvent(QResizeEvent *event) override; QString getConfigFilePath() const; + void setupConfigWatcher(); + QDateTime currentFileMtime() const; ElaTableView *m_funcTable; QStandardItemModel *m_tableModel; @@ -56,6 +61,8 @@ private: ElaText *m_progressLabel; QPushButton *m_startBtn; QFileSystemWatcher *m_configWatcher; + QTimer *m_reloadTimer; + QDateTime m_lastFileMtime; }; #endif diff --git a/src/pages/datamapping/functionmappingpage.cpp b/src/pages/datamapping/functionmappingpage.cpp new file mode 100644 index 0000000..65ab232 --- /dev/null +++ b/src/pages/datamapping/functionmappingpage.cpp @@ -0,0 +1,449 @@ +#include "functionmappingpage.h" +#include "utils/Constants.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "src/utils/logmanager.h" +#include "ElaPushButton.h" +#include "ElaLineEdit.h" +#include "ElaText.h" + +// 自定义代理模型:支持两列同时过滤 +class FuncFilterProxyModel : public QSortFilterProxyModel +{ +public: + explicit FuncFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} + + void setFilterText(const QString &text) + { + m_filterText = text; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + if (m_filterText.isEmpty()) return true; + QString t0 = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent)).toString(); + QString t1 = sourceModel()->data(sourceModel()->index(sourceRow, 1, sourceParent)).toString(); + return t0.contains(m_filterText, Qt::CaseInsensitive) || t1.contains(m_filterText, Qt::CaseInsensitive); + } + +private: + QString m_filterText; +}; + +FunctionMappingPage::FunctionMappingPage(QWidget *parent) + : QWidget(parent) + , m_isLoading(false) + , m_isSaving(false) +{ + initUI(); + loadData(); +} + +FunctionMappingPage::~FunctionMappingPage() +{ +} + +void FunctionMappingPage::initUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(20, 20, 20, 20); + mainLayout->setSpacing(10); + + // 搜索框 + QHBoxLayout *searchLayout = new QHBoxLayout; + ElaLineEdit *searchEdit = new ElaLineEdit(this); + searchEdit->setPlaceholderText("搜索 UF20 或 UFT3 函数名..."); + searchEdit->setClearButtonEnabled(true); + connect(searchEdit, &ElaLineEdit::textChanged, this, &FunctionMappingPage::onSearchTextChanged); + searchLayout->addWidget(searchEdit); + mainLayout->addLayout(searchLayout); + + m_model = new QStandardItemModel(this); + m_model->setColumnCount(2); + m_model->setHorizontalHeaderLabels({"UF20", "UFT3"}); + connect(m_model, &QStandardItemModel::itemChanged, this, &FunctionMappingPage::onModelItemChanged); + + m_proxyModel = new FuncFilterProxyModel(this); + m_proxyModel->setSourceModel(m_model); + + m_tableView = new ElaTableView; + m_tableView->setModel(m_proxyModel); + m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); + m_tableView->verticalHeader()->setVisible(false); + m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mainLayout->addWidget(m_tableView); + + QHBoxLayout *btnLayout = new QHBoxLayout; + ElaPushButton *addBtn = new ElaPushButton("添加", this); + ElaPushButton *delBtn = new ElaPushButton("删除选中", this); + ElaPushButton *saveBtn = new ElaPushButton("保存", this); + + connect(addBtn, &ElaPushButton::clicked, this, &FunctionMappingPage::onAddRow); + connect(delBtn, &ElaPushButton::clicked, this, &FunctionMappingPage::onDeleteRow); + connect(saveBtn, &ElaPushButton::clicked, this, &FunctionMappingPage::onSave); + + btnLayout->addWidget(addBtn); + btnLayout->addWidget(delBtn); + btnLayout->addStretch(); + btnLayout->addWidget(saveBtn); + mainLayout->addLayout(btnLayout); + + QHBoxLayout *tipLayout = new QHBoxLayout; + QLabel *tipLabel = new QLabel("💡 双击表格修改数据"); + tipLabel->setStyleSheet("color: #666; font-size: 13px;"); + tipLayout->addWidget(tipLabel); + tipLayout->addStretch(); + mainLayout->addLayout(tipLayout); + + // 文件监控:检测 cust.json 外部修改后自动刷新 + m_fileWatcher = new QFileSystemWatcher(this); + connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &FunctionMappingPage::onFileChanged); +} + +void FunctionMappingPage::loadData() +{ + m_isLoading = true; + m_model->removeRows(0, m_model->rowCount()); + + QJsonObject custJson = readCustJson(); + if (custJson.contains("functionReplace")) { + QJsonObject dataObj = custJson["functionReplace"].toObject(); + QStringList keys = dataObj.keys(); + for (const QString &key : keys) { + QList rowItems; + rowItems.append(new QStandardItem(key)); + rowItems.append(new QStandardItem(dataObj[key].toString())); + m_model->appendRow(rowItems); + } + } + + m_isLoading = false; + + // 设置文件监控 + QString jsonPath = getCustJsonPath(); + if (!m_fileWatcher->files().contains(jsonPath)) + m_fileWatcher->addPath(jsonPath); +} + +QString FunctionMappingPage::getCustJsonPath() const +{ + return Constants::custJson(); +} + +QJsonObject FunctionMappingPage::readCustJson() +{ + QString jsonPath = getCustJsonPath(); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + LogManager::instance()->logError("无法读取 cust.json: " + jsonPath); + return QJsonObject(); + } + QByteArray data = file.readAll(); + file.close(); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + LogManager::instance()->logError("cust.json 解析失败: " + error.errorString()); + return QJsonObject(); + } + return doc.object(); +} + +// 从原始 JSON 文本中提取顶层 key 的顺序 +static QStringList extractTopLevelKeyOrder(const QString &rawJson) +{ + QStringList keys; + QRegularExpression re("\"(\\w+)\"\\s*:"); + auto it = re.globalMatch(rawJson); + while (it.hasNext()) { + QString key = it.next().captured(1); + if (!keys.contains(key)) + keys.append(key); + } + return keys; +} + +// 对 JSON 字符串进行缩进处理 +static QString indentJsonValue(const QString &json) +{ + if (json.isEmpty()) return json; + QStringList lines = json.split('\n'); + QString result; + for (const QString &line : lines) { + if (!line.trimmed().isEmpty()) + result += " " + line + "\n"; + } + return result; +} + +// 写入 JSON 值,根据类型选择合适的格式 +static QString formatJsonValue(const QJsonValue &value) +{ + if (value.isObject()) { + QJsonDocument doc(value.toObject()); + QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented)); + return indentJsonValue(raw); + } else if (value.isArray()) { + QJsonDocument doc(value.toArray()); + QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented)); + return indentJsonValue(raw); + } else if (value.isString()) { + QString escaped = value.toString(); + escaped.replace("\\", "\\\\"); + escaped.replace("\"", "\\\""); + escaped.replace("\n", "\\n"); + escaped.replace("\r", "\\r"); + escaped.replace("\t", "\\t"); + return "\"" + escaped + "\""; + } else if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } else if (value.isDouble()) { + double d = value.toDouble(); + if (d == static_cast(d)) + return QString::number(static_cast(d)); + return QString::number(d, 'f'); + } + return "null"; +} + +// 按顺序格式化一个 string->string 键值对的嵌套 JSON 对象 +static QString formatOrderedJsonObject(const QList> &pairs) +{ + QString result = "{\n"; + bool first = true; + for (const auto &pair : pairs) { + if (!first) result += ",\n"; + first = false; + QString ek = pair.first; + ek.replace("\\", "\\\\"); ek.replace("\"", "\\\""); + QString ev = pair.second; + ev.replace("\\", "\\\\"); ev.replace("\"", "\\\""); + ev.replace("\n", "\\n"); ev.replace("\r", "\\r"); ev.replace("\t", "\\t"); + result += " \"" + ek + "\": \"" + ev + "\""; + } + result += "\n }"; + return result; +} + +bool FunctionMappingPage::writeCustJson(const QJsonObject &json, const QStringList &keyOrder, + const QString &orderedKey, const QList> &orderedData) +{ + QString jsonPath = getCustJsonPath(); + QFile file(jsonPath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LogManager::instance()->logError("无法写入 cust.json: " + jsonPath); + return false; + } + + QTextStream out(&file); + out << "{\n"; + bool first = true; + for (const QString &key : keyOrder) { + if (!json.contains(key)) continue; + if (!first) out << ",\n"; + first = false; + out << " \"" << key << "\": "; + if (key == orderedKey) { + out << formatOrderedJsonObject(orderedData); + } else { + out << formatJsonValue(json[key]); + } + } + out << "\n}\n"; + file.close(); + return true; +} + +void FunctionMappingPage::onAddRow() +{ + QDialog dialog(this); + dialog.setWindowTitle("添加函数映射"); + dialog.resize(450, 180); + + QVBoxLayout *layout = new QVBoxLayout(&dialog); + layout->setSpacing(12); + + QHBoxLayout *keyLayout = new QHBoxLayout; + ElaText *keyLabel = new ElaText("UF20:", &dialog); + keyLabel->setTextPointSize(10); + keyLayout->addWidget(keyLabel); + ElaLineEdit *keyEdit = new ElaLineEdit(&dialog); + keyEdit->setPlaceholderText("输入UF20函数名"); + keyLayout->addWidget(keyEdit); + layout->addLayout(keyLayout); + + QHBoxLayout *valLayout = new QHBoxLayout; + ElaText *valLabel = new ElaText("UFT3:", &dialog); + valLabel->setTextPointSize(10); + valLayout->addWidget(valLabel); + ElaLineEdit *valEdit = new ElaLineEdit(&dialog); + valEdit->setPlaceholderText("输入UFT3函数名"); + valLayout->addWidget(valEdit); + layout->addLayout(valLayout); + + QHBoxLayout *btnLayout = new QHBoxLayout; + ElaPushButton *okBtn = new ElaPushButton("确定", &dialog); + ElaPushButton *cancelBtn = new ElaPushButton("取消", &dialog); + btnLayout->addStretch(); + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); + layout->addLayout(btnLayout); + + connect(okBtn, &ElaPushButton::clicked, &dialog, &QDialog::accept); + connect(cancelBtn, &ElaPushButton::clicked, &dialog, &QDialog::reject); + + if (dialog.exec() != QDialog::Accepted) return; + + QString key = keyEdit->text().trimmed(); + QString val = valEdit->text().trimmed(); + + if (key.isEmpty()) { + QMessageBox::warning(this, "提示", "UF20 函数名不能为空!"); + return; + } + + for (int row = 0; row < m_model->rowCount(); ++row) { + if (m_model->item(row, 0) && m_model->item(row, 0)->text() == key) { + QMessageBox::warning(this, "提示", QString("UF20 函数名 \"%1\" 已存在!").arg(key)); + return; + } + } + + QList rowItems; + rowItems.append(new QStandardItem(key)); + rowItems.append(new QStandardItem(val)); + m_model->appendRow(rowItems); +} + +void FunctionMappingPage::onDeleteRow() +{ + QModelIndexList selectedRows = m_tableView->selectionModel()->selectedRows(); + if (selectedRows.isEmpty()) { + QMessageBox::warning(this, "提示", "请先选中要删除的行!"); + return; + } + + QStringList keyNames; + QList sourceRows; + for (const QModelIndex &idx : selectedRows) { + QModelIndex srcIdx = m_proxyModel->mapToSource(idx); + QStandardItem *keyItem = m_model->item(srcIdx.row(), 0); + if (keyItem) keyNames.append(keyItem->text()); + sourceRows.append(srcIdx.row()); + } + + QString confirmMsg; + if (selectedRows.size() == 1) { + confirmMsg = QString("确定要删除 \"%1\" 这条映射吗?").arg(keyNames.first()); + } else { + confirmMsg = QString("确定要删除选中的 %1 条映射吗?").arg(selectedRows.size()); + } + + QMessageBox::StandardButton reply = QMessageBox::question(this, "确认删除", + confirmMsg, QMessageBox::Yes | QMessageBox::No); + if (reply != QMessageBox::Yes) return; + + // 从高行号到低行号删除,避免索引偏移问题 + std::sort(sourceRows.begin(), sourceRows.end(), std::greater()); + for (int row : sourceRows) { + m_model->removeRow(row); + } +} + +void FunctionMappingPage::onModelItemChanged(QStandardItem *item) +{ + if (m_isLoading) return; + Q_UNUSED(item); +} + +void FunctionMappingPage::onSave() +{ + // 标记正在保存,忽略文件监控事件 + m_isSaving = true; + + // 读取原始文件内容以保留顶层 key 顺序 + QString jsonPath = getCustJsonPath(); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::critical(this, "错误", "无法读取 cust.json!"); + return; + } + QString rawJson = QString::fromUtf8(file.readAll()); + file.close(); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) { + QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString()); + return; + } + QJsonObject custJson = doc.object(); + + // 提取原始 key 顺序 + QStringList keyOrder = extractTopLevelKeyOrder(rawJson); + + // 构建有序的函数替换数据 + QJsonObject functionReplaceObj; + QList> funcData; + for (int row = 0; row < m_model->rowCount(); ++row) { + QStandardItem *keyItem = m_model->item(row, 0); + QStandardItem *valItem = m_model->item(row, 1); + if (keyItem && !keyItem->text().trimmed().isEmpty()) { + QString k = keyItem->text().trimmed(); + QString v = valItem ? valItem->text().trimmed() : ""; + functionReplaceObj[k] = v; + funcData.append({k, v}); + } + } + custJson["functionReplace"] = functionReplaceObj; + + if (writeCustJson(custJson, keyOrder, "functionReplace", funcData)) { + LogManager::instance()->logInfo("函数映射配置已保存到 cust.json"); + QMessageBox::information(this, "提示", "保存成功!"); + } else { + QMessageBox::critical(this, "错误", "保存到 cust.json 失败!"); + m_isSaving = false; + } + + // 延迟重置保存标记,让文件监控忽略本次写入事件 + QTimer::singleShot(2000, this, [this]() { + m_isSaving = false; + // 重新添加监控路径 + QString path = getCustJsonPath(); + if (!m_fileWatcher->files().contains(path)) + m_fileWatcher->addPath(path); + }); +} + +void FunctionMappingPage::onFileChanged(const QString &path) +{ + Q_UNUSED(path); + if (m_isSaving) return; + LogManager::instance()->logInfo("检测到 cust.json 外部修改,自动刷新数据"); + loadData(); +} + +void FunctionMappingPage::onSearchTextChanged(const QString &text) +{ + static_cast(m_proxyModel)->setFilterText(text); +} diff --git a/src/pages/datamapping/functionmappingpage.h b/src/pages/datamapping/functionmappingpage.h new file mode 100644 index 0000000..c4e5b00 --- /dev/null +++ b/src/pages/datamapping/functionmappingpage.h @@ -0,0 +1,46 @@ +#ifndef FUNCTIONMAPPINGPAGE_H +#define FUNCTIONMAPPINGPAGE_H + +#include +#include +#include +#include +#include +#include +#include "ElaTableView.h" + +class ElaLineEdit; + +class FunctionMappingPage : public QWidget +{ + Q_OBJECT + +public: + explicit FunctionMappingPage(QWidget *parent = nullptr); + ~FunctionMappingPage(); + +private slots: + void onAddRow(); + void onDeleteRow(); + void onModelItemChanged(QStandardItem *item); + void onSave(); + void onFileChanged(const QString &path); + void onSearchTextChanged(const QString &text); + +private: + void initUI(); + void loadData(); + QJsonObject readCustJson(); + bool writeCustJson(const QJsonObject &json, const QStringList &keyOrder, + const QString &orderedKey, const QList> &orderedData); + QString getCustJsonPath() const; + + ElaTableView *m_tableView; + QStandardItemModel *m_model; + QSortFilterProxyModel *m_proxyModel; + QFileSystemWatcher *m_fileWatcher; + bool m_isLoading; + bool m_isSaving; +}; + +#endif // FUNCTIONMAPPINGPAGE_H diff --git a/src/pages/datamapping/tablemappingpage.cpp b/src/pages/datamapping/tablemappingpage.cpp new file mode 100644 index 0000000..7ccff49 --- /dev/null +++ b/src/pages/datamapping/tablemappingpage.cpp @@ -0,0 +1,449 @@ +#include "tablemappingpage.h" +#include "utils/Constants.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "src/utils/logmanager.h" +#include "ElaPushButton.h" +#include "ElaLineEdit.h" +#include "ElaText.h" + +// 自定义代理模型:支持两列同时过滤 +class TableFilterProxyModel : public QSortFilterProxyModel +{ +public: + explicit TableFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} + + void setFilterText(const QString &text) + { + m_filterText = text; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + if (m_filterText.isEmpty()) return true; + QString t0 = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent)).toString(); + QString t1 = sourceModel()->data(sourceModel()->index(sourceRow, 1, sourceParent)).toString(); + return t0.contains(m_filterText, Qt::CaseInsensitive) || t1.contains(m_filterText, Qt::CaseInsensitive); + } + +private: + QString m_filterText; +}; + +TableMappingPage::TableMappingPage(QWidget *parent) + : QWidget(parent) + , m_isLoading(false) + , m_isSaving(false) +{ + initUI(); + loadData(); +} + +TableMappingPage::~TableMappingPage() +{ +} + +void TableMappingPage::initUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(20, 20, 20, 20); + mainLayout->setSpacing(10); + + // 搜索框 + QHBoxLayout *searchLayout = new QHBoxLayout; + ElaLineEdit *searchEdit = new ElaLineEdit(this); + searchEdit->setPlaceholderText("搜索 UF20 或 UFT3 表名..."); + searchEdit->setClearButtonEnabled(true); + connect(searchEdit, &ElaLineEdit::textChanged, this, &TableMappingPage::onSearchTextChanged); + searchLayout->addWidget(searchEdit); + mainLayout->addLayout(searchLayout); + + m_model = new QStandardItemModel(this); + m_model->setColumnCount(2); + m_model->setHorizontalHeaderLabels({"UF20", "UFT3"}); + connect(m_model, &QStandardItemModel::itemChanged, this, &TableMappingPage::onModelItemChanged); + + m_proxyModel = new TableFilterProxyModel(this); + m_proxyModel->setSourceModel(m_model); + + m_tableView = new ElaTableView; + m_tableView->setModel(m_proxyModel); + m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); + m_tableView->verticalHeader()->setVisible(false); + m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mainLayout->addWidget(m_tableView); + + QHBoxLayout *btnLayout = new QHBoxLayout; + ElaPushButton *addBtn = new ElaPushButton("添加", this); + ElaPushButton *delBtn = new ElaPushButton("删除选中", this); + ElaPushButton *saveBtn = new ElaPushButton("保存", this); + + connect(addBtn, &ElaPushButton::clicked, this, &TableMappingPage::onAddRow); + connect(delBtn, &ElaPushButton::clicked, this, &TableMappingPage::onDeleteRow); + connect(saveBtn, &ElaPushButton::clicked, this, &TableMappingPage::onSave); + + btnLayout->addWidget(addBtn); + btnLayout->addWidget(delBtn); + btnLayout->addStretch(); + btnLayout->addWidget(saveBtn); + mainLayout->addLayout(btnLayout); + + QHBoxLayout *tipLayout = new QHBoxLayout; + QLabel *tipLabel = new QLabel("💡 双击表格修改数据"); + tipLabel->setStyleSheet("color: #666; font-size: 13px;"); + tipLayout->addWidget(tipLabel); + tipLayout->addStretch(); + mainLayout->addLayout(tipLayout); + + // 文件监控:检测 cust.json 外部修改后自动刷新 + m_fileWatcher = new QFileSystemWatcher(this); + connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &TableMappingPage::onFileChanged); +} + +void TableMappingPage::loadData() +{ + m_isLoading = true; + m_model->removeRows(0, m_model->rowCount()); + + QJsonObject custJson = readCustJson(); + if (custJson.contains("tableCompareUf2_Uf3")) { + QJsonObject dataObj = custJson["tableCompareUf2_Uf3"].toObject(); + QStringList keys = dataObj.keys(); + for (const QString &key : keys) { + QList rowItems; + rowItems.append(new QStandardItem(key)); + rowItems.append(new QStandardItem(dataObj[key].toString())); + m_model->appendRow(rowItems); + } + } + + m_isLoading = false; + + // 设置文件监控 + QString jsonPath = getCustJsonPath(); + if (!m_fileWatcher->files().contains(jsonPath)) + m_fileWatcher->addPath(jsonPath); +} + +QString TableMappingPage::getCustJsonPath() const +{ + return Constants::custJson(); +} + +QJsonObject TableMappingPage::readCustJson() +{ + QString jsonPath = getCustJsonPath(); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + LogManager::instance()->logError("无法读取 cust.json: " + jsonPath); + return QJsonObject(); + } + QByteArray data = file.readAll(); + file.close(); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + LogManager::instance()->logError("cust.json 解析失败: " + error.errorString()); + return QJsonObject(); + } + return doc.object(); +} + +// 从原始 JSON 文本中提取顶层 key 的顺序 +static QStringList extractTopLevelKeyOrder(const QString &rawJson) +{ + QStringList keys; + QRegularExpression re("\"(\\w+)\"\\s*:"); + auto it = re.globalMatch(rawJson); + while (it.hasNext()) { + QString key = it.next().captured(1); + if (!keys.contains(key)) + keys.append(key); + } + return keys; +} + +// 对 JSON 字符串进行缩进处理 +static QString indentJsonValue(const QString &json) +{ + if (json.isEmpty()) return json; + QStringList lines = json.split('\n'); + QString result; + for (const QString &line : lines) { + if (!line.trimmed().isEmpty()) + result += " " + line + "\n"; + } + return result; +} + +// 写入 JSON 值,根据类型选择合适的格式 +static QString formatJsonValue(const QJsonValue &value) +{ + if (value.isObject()) { + QJsonDocument doc(value.toObject()); + QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented)); + return indentJsonValue(raw); + } else if (value.isArray()) { + QJsonDocument doc(value.toArray()); + QString raw = QString::fromUtf8(doc.toJson(QJsonDocument::Indented)); + return indentJsonValue(raw); + } else if (value.isString()) { + QString escaped = value.toString(); + escaped.replace("\\", "\\\\"); + escaped.replace("\"", "\\\""); + escaped.replace("\n", "\\n"); + escaped.replace("\r", "\\r"); + escaped.replace("\t", "\\t"); + return "\"" + escaped + "\""; + } else if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } else if (value.isDouble()) { + double d = value.toDouble(); + if (d == static_cast(d)) + return QString::number(static_cast(d)); + return QString::number(d, 'f'); + } + return "null"; +} + +// 按顺序格式化一个 string->string 键值对的嵌套 JSON 对象 +static QString formatOrderedJsonObject(const QList> &pairs) +{ + QString result = "{\n"; + bool first = true; + for (const auto &pair : pairs) { + if (!first) result += ",\n"; + first = false; + QString ek = pair.first; + ek.replace("\\", "\\\\"); ek.replace("\"", "\\\""); + QString ev = pair.second; + ev.replace("\\", "\\\\"); ev.replace("\"", "\\\""); + ev.replace("\n", "\\n"); ev.replace("\r", "\\r"); ev.replace("\t", "\\t"); + result += " \"" + ek + "\": \"" + ev + "\""; + } + result += "\n }"; + return result; +} + +bool TableMappingPage::writeCustJson(const QJsonObject &json, const QStringList &keyOrder, + const QString &orderedKey, const QList> &orderedData) +{ + QString jsonPath = getCustJsonPath(); + QFile file(jsonPath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LogManager::instance()->logError("无法写入 cust.json: " + jsonPath); + return false; + } + + QTextStream out(&file); + out << "{\n"; + bool first = true; + for (const QString &key : keyOrder) { + if (!json.contains(key)) continue; + if (!first) out << ",\n"; + first = false; + out << " \"" << key << "\": "; + if (key == orderedKey) { + out << formatOrderedJsonObject(orderedData); + } else { + out << formatJsonValue(json[key]); + } + } + out << "\n}\n"; + file.close(); + return true; +} + +void TableMappingPage::onAddRow() +{ + QDialog dialog(this); + dialog.setWindowTitle("添加表映射"); + dialog.resize(450, 180); + + QVBoxLayout *layout = new QVBoxLayout(&dialog); + layout->setSpacing(12); + + QHBoxLayout *keyLayout = new QHBoxLayout; + ElaText *keyLabel = new ElaText("UF20:", &dialog); + keyLabel->setTextPointSize(10); + keyLayout->addWidget(keyLabel); + ElaLineEdit *keyEdit = new ElaLineEdit(&dialog); + keyEdit->setPlaceholderText("输入UF20表名"); + keyLayout->addWidget(keyEdit); + layout->addLayout(keyLayout); + + QHBoxLayout *valLayout = new QHBoxLayout; + ElaText *valLabel = new ElaText("UFT3:", &dialog); + valLabel->setTextPointSize(10); + valLayout->addWidget(valLabel); + ElaLineEdit *valEdit = new ElaLineEdit(&dialog); + valEdit->setPlaceholderText("输入UFT3表名"); + valLayout->addWidget(valEdit); + layout->addLayout(valLayout); + + QHBoxLayout *btnLayout = new QHBoxLayout; + ElaPushButton *okBtn = new ElaPushButton("确定", &dialog); + ElaPushButton *cancelBtn = new ElaPushButton("取消", &dialog); + btnLayout->addStretch(); + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); + layout->addLayout(btnLayout); + + connect(okBtn, &ElaPushButton::clicked, &dialog, &QDialog::accept); + connect(cancelBtn, &ElaPushButton::clicked, &dialog, &QDialog::reject); + + if (dialog.exec() != QDialog::Accepted) return; + + QString key = keyEdit->text().trimmed(); + QString val = valEdit->text().trimmed(); + + if (key.isEmpty()) { + QMessageBox::warning(this, "提示", "UF20 表名不能为空!"); + return; + } + + for (int row = 0; row < m_model->rowCount(); ++row) { + if (m_model->item(row, 0) && m_model->item(row, 0)->text() == key) { + QMessageBox::warning(this, "提示", QString("UF20 表名 \"%1\" 已存在!").arg(key)); + return; + } + } + + QList rowItems; + rowItems.append(new QStandardItem(key)); + rowItems.append(new QStandardItem(val)); + m_model->appendRow(rowItems); +} + +void TableMappingPage::onDeleteRow() +{ + QModelIndexList selectedRows = m_tableView->selectionModel()->selectedRows(); + if (selectedRows.isEmpty()) { + QMessageBox::warning(this, "提示", "请先选中要删除的行!"); + return; + } + + QStringList keyNames; + QList sourceRows; + for (const QModelIndex &idx : selectedRows) { + QModelIndex srcIdx = m_proxyModel->mapToSource(idx); + QStandardItem *keyItem = m_model->item(srcIdx.row(), 0); + if (keyItem) keyNames.append(keyItem->text()); + sourceRows.append(srcIdx.row()); + } + + QString confirmMsg; + if (selectedRows.size() == 1) { + confirmMsg = QString("确定要删除 \"%1\" 这条映射吗?").arg(keyNames.first()); + } else { + confirmMsg = QString("确定要删除选中的 %1 条映射吗?").arg(selectedRows.size()); + } + + QMessageBox::StandardButton reply = QMessageBox::question(this, "确认删除", + confirmMsg, QMessageBox::Yes | QMessageBox::No); + if (reply != QMessageBox::Yes) return; + + // 从高行号到低行号删除,避免索引偏移问题 + std::sort(sourceRows.begin(), sourceRows.end(), std::greater()); + for (int row : sourceRows) { + m_model->removeRow(row); + } +} + +void TableMappingPage::onModelItemChanged(QStandardItem *item) +{ + if (m_isLoading) return; + Q_UNUSED(item); +} + +void TableMappingPage::onSave() +{ + // 标记正在保存,忽略文件监控事件 + m_isSaving = true; + + // 读取原始文件内容以保留顶层 key 顺序 + QString jsonPath = getCustJsonPath(); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::critical(this, "错误", "无法读取 cust.json!"); + return; + } + QString rawJson = QString::fromUtf8(file.readAll()); + file.close(); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) { + QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString()); + return; + } + QJsonObject custJson = doc.object(); + + // 提取原始 key 顺序 + QStringList keyOrder = extractTopLevelKeyOrder(rawJson); + + // 构建有序的表替换数据 + QJsonObject tableCompareObj; + QList> tableData; + for (int row = 0; row < m_model->rowCount(); ++row) { + QStandardItem *keyItem = m_model->item(row, 0); + QStandardItem *valItem = m_model->item(row, 1); + if (keyItem && !keyItem->text().trimmed().isEmpty()) { + QString k = keyItem->text().trimmed(); + QString v = valItem ? valItem->text().trimmed() : ""; + tableCompareObj[k] = v; + tableData.append({k, v}); + } + } + custJson["tableCompareUf2_Uf3"] = tableCompareObj; + + if (writeCustJson(custJson, keyOrder, "tableCompareUf2_Uf3", tableData)) { + LogManager::instance()->logInfo("表映射配置已保存到 cust.json"); + QMessageBox::information(this, "提示", "保存成功!"); + } else { + QMessageBox::critical(this, "错误", "保存到 cust.json 失败!"); + m_isSaving = false; + } + + // 延迟重置保存标记,让文件监控忽略本次写入事件 + QTimer::singleShot(2000, this, [this]() { + m_isSaving = false; + // 重新添加监控路径 + QString path = getCustJsonPath(); + if (!m_fileWatcher->files().contains(path)) + m_fileWatcher->addPath(path); + }); +} + +void TableMappingPage::onFileChanged(const QString &path) +{ + Q_UNUSED(path); + if (m_isSaving) return; + LogManager::instance()->logInfo("检测到 cust.json 外部修改,自动刷新数据"); + loadData(); +} + +void TableMappingPage::onSearchTextChanged(const QString &text) +{ + static_cast(m_proxyModel)->setFilterText(text); +} diff --git a/src/pages/datamapping/tablemappingpage.h b/src/pages/datamapping/tablemappingpage.h new file mode 100644 index 0000000..0801f02 --- /dev/null +++ b/src/pages/datamapping/tablemappingpage.h @@ -0,0 +1,46 @@ +#ifndef TABLEMAPPINGPAGE_H +#define TABLEMAPPINGPAGE_H + +#include +#include +#include +#include +#include +#include +#include "ElaTableView.h" + +class ElaLineEdit; + +class TableMappingPage : public QWidget +{ + Q_OBJECT + +public: + explicit TableMappingPage(QWidget *parent = nullptr); + ~TableMappingPage(); + +private slots: + void onAddRow(); + void onDeleteRow(); + void onModelItemChanged(QStandardItem *item); + void onSave(); + void onFileChanged(const QString &path); + void onSearchTextChanged(const QString &text); + +private: + void initUI(); + void loadData(); + QJsonObject readCustJson(); + bool writeCustJson(const QJsonObject &json, const QStringList &keyOrder, + const QString &orderedKey, const QList> &orderedData); + QString getCustJsonPath() const; + + ElaTableView *m_tableView; + QStandardItemModel *m_model; + QSortFilterProxyModel *m_proxyModel; + QFileSystemWatcher *m_fileWatcher; + bool m_isLoading; + bool m_isSaving; +}; + +#endif // TABLEMAPPINGPAGE_H diff --git a/src/pages/help/helpdocpage.cpp b/src/pages/help/helpdocpage.cpp deleted file mode 100644 index 2b274ae..0000000 --- a/src/pages/help/helpdocpage.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "helpdocpage.h" -#include -#include -#include -#include - -HelpDocPage::HelpDocPage(QWidget *parent) - : QWidget(parent) -{ - initUI(); -} - -HelpDocPage::~HelpDocPage() -{ -} - -void HelpDocPage::initUI() -{ - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - - QLabel *label = new QLabel("点击此处跳转帮助文档"); - label->setStyleSheet("font-size: 14px; color: #4a90d9;"); - label->setOpenExternalLinks(true); - layout->addWidget(label, 0, Qt::AlignCenter); -} \ No newline at end of file diff --git a/src/pages/help/helpdocpage.h b/src/pages/help/helpdocpage.h deleted file mode 100644 index 47621dc..0000000 --- a/src/pages/help/helpdocpage.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef HELPDOCPAGE_H -#define HELPDOCPAGE_H - -#include -#include -#include -#include -#include "ElaTreeView.h" - -class HelpDocPage : public QWidget -{ - Q_OBJECT - -public: - explicit HelpDocPage(QWidget *parent = nullptr); - ~HelpDocPage(); - -private: - void initUI(); - void initTreeView(); - void initContentPages(); - void loadHelpFile(); - void updateContent(const QString §ion); - QString extractSection(const QString §ionName); - QString markdownToHtml(const QString &markdown); - - ElaTreeView* m_treeView; - QStackedWidget* m_stackedWidget; - QLabel* m_titleLabel; - QTextBrowser* m_contentBrowser; - QString m_helpContent; -}; - -#endif // HELPDOCPAGE_H \ No newline at end of file diff --git a/src/pages/metadatupdate/metadatupdatepage.cpp b/src/pages/metadatupdate/metadatupdatepage.cpp index 36803aa..584c890 100644 --- a/src/pages/metadatupdate/metadatupdatepage.cpp +++ b/src/pages/metadatupdate/metadatupdatepage.cpp @@ -1,4 +1,5 @@ #include "metadatupdatepage.h" +#include "utils/Constants.h" #include #include #include @@ -15,8 +16,7 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent) { initUI(); _processor = new MetadataProcessor(this); - QString basePath = QCoreApplication::applicationDirPath(); - _processor->initConfig(basePath); + _processor->initConfig(Constants::toolDir()); } MetadataUpdatePage::~MetadataUpdatePage() @@ -261,8 +261,7 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items) } else if (item == "标准错误号") { processUf2ErrNumber(); } else if (item == "接口数据") { - QString basePath = QCoreApplication::applicationDirPath(); - processUf2Interface(basePath); + processUf2Interface(Constants::toolDir()); interfaceUpdated = true; } } @@ -298,8 +297,7 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items) } else if (item == "表结构") { processUft3Table(); } else if (item == "接口数据") { - QString basePath = QCoreApplication::applicationDirPath(); - processUft3Interface(basePath); + processUft3Interface(Constants::toolDir()); interfaceUpdated = true; } } diff --git a/src/pages/settings/settingspage.cpp b/src/pages/settings/settingspage.cpp index 2613502..9120d86 100644 --- a/src/pages/settings/settingspage.cpp +++ b/src/pages/settings/settingspage.cpp @@ -1,4 +1,5 @@ #include "settingspage.h" +#include "utils/Constants.h" #include #include #include @@ -142,7 +143,7 @@ void SettingsPage::loadSettings() QString SettingsPage::getConfigIniPath() { - return QCoreApplication::applicationDirPath() + "/uf2touft3/config.ini"; + return Constants::configIni(); } bool SettingsPage::isValidUtf8(const QByteArray &data) diff --git a/src/utils/Constants.h b/src/utils/Constants.h new file mode 100644 index 0000000..660dc6e --- /dev/null +++ b/src/utils/Constants.h @@ -0,0 +1,72 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#include +#include + +// ============================================================================ +// 项目公共常量 — 所有 URL、路径、文件名等配置集中于此,修改只需改一处 +// ============================================================================ + +namespace Constants { + +// --------------------------------------------------------------------------- +// 应用版本号(发版时只需修改此处) +// --------------------------------------------------------------------------- +namespace App { + constexpr char Version[] = "1.2.0"; +} + +// --------------------------------------------------------------------------- +// URL 常量 +// --------------------------------------------------------------------------- +namespace Urls { + constexpr char Base[] = "http://10.20.163.105:6045"; + + inline QString updateInfo() { return QString::fromLatin1(Base) + "/download/uft3changecode/update_info.json"; } + inline QString downloadPage(){ return QString::fromLatin1(Base) + "/uft3changecode/下载.html"; } + inline QString helpPage() { return QString::fromLatin1(Base) + "/uft3changecode"; } +} + +// --------------------------------------------------------------------------- +// 目录 / 文件路径常量(相对于 exe 目录的子路径名称) +// --------------------------------------------------------------------------- +namespace Paths { + inline const QString ToolDir = "uf2touft3"; // 工具主目录 + inline const QString LogDir = "logs"; // 日志目录 + inline const QString CacheDir = "cache"; // 缓存目录 +} + +// --------------------------------------------------------------------------- +// ToolDir 下的文件名 +// --------------------------------------------------------------------------- +namespace ToolFiles { + inline const QString Exe = "uf2touft3.exe"; + inline const QString ConfigIni = "config.ini"; + inline const QString Uf2Json = "uf2.json"; + inline const QString Uft3Json = "uft3.json"; + inline const QString Uft3Trans = "uft3Trans.json"; + inline const QString CustJson = "cust.json"; +} + +// --------------------------------------------------------------------------- +// Cache 相关 +// --------------------------------------------------------------------------- +namespace Cache { + inline const QString DbFile = "config_cache.db"; +} + +// --------------------------------------------------------------------------- +// 运行时路径辅助函数(基于 applicationDirPath 拼接) +// --------------------------------------------------------------------------- +inline QString toolDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::ToolDir; } +inline QString toolExe() { return toolDir() + "/" + ToolFiles::Exe; } +inline QString configIni() { return toolDir() + "/" + ToolFiles::ConfigIni; } +inline QString custJson() { return toolDir() + "/" + ToolFiles::CustJson; } +inline QString logDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::LogDir; } +inline QString cacheDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::CacheDir; } +inline QString cacheDb() { return cacheDir() + "/" + Cache::DbFile; } + +} // namespace Constants + +#endif // CONSTANTS_H diff --git a/src/utils/datacache.cpp b/src/utils/datacache.cpp index b8e0849..92444e7 100644 --- a/src/utils/datacache.cpp +++ b/src/utils/datacache.cpp @@ -1,4 +1,5 @@ #include "datacache.h" +#include "Constants.h" #include "logmanager.h" #include #include @@ -31,14 +32,14 @@ DataCache* DataCache::instance() QString DataCache::getDatabasePath() { - QString appDir = QCoreApplication::applicationDirPath(); - QDir dbDir(appDir + "/cache"); + QString dir = Constants::cacheDir(); + QDir dbDir(dir); if (!dbDir.exists()) { - if (!dbDir.mkpath(dbDir.absolutePath())) { - LogManager::instance()->logError(QString("数据缓存 - 创建缓存目录失败: %1").arg(dbDir.absolutePath())); + if (!dbDir.mkpath(dir)) { + LogManager::instance()->logError(QString("数据缓存 - 创建缓存目录失败: %1").arg(dir)); } } - return dbDir.absoluteFilePath("config_cache.db"); + return dbDir.absoluteFilePath(Constants::Cache::DbFile); } bool DataCache::createTables() diff --git a/src/utils/logmanager.cpp b/src/utils/logmanager.cpp index 57a916c..26745ec 100644 --- a/src/utils/logmanager.cpp +++ b/src/utils/logmanager.cpp @@ -1,4 +1,5 @@ #include "logmanager.h" +#include "Constants.h" #include #include #include @@ -27,8 +28,8 @@ LogManager* LogManager::instance() void LogManager::initLogFile() { - QString appDir = QCoreApplication::applicationDirPath(); - QDir logDir(appDir + "/logs"); + QString dir = Constants::logDir(); + QDir logDir(dir); if (!logDir.exists()) { logDir.mkpath("."); } diff --git a/src/utils/uf2configreader.cpp b/src/utils/uf2configreader.cpp index 3b556bf..7b3e93c 100644 --- a/src/utils/uf2configreader.cpp +++ b/src/utils/uf2configreader.cpp @@ -1,4 +1,5 @@ #include "uf2configreader.h" +#include "Constants.h" #include "datacache.h" #include "logmanager.h" #include @@ -15,8 +16,7 @@ UF2ConfigReader::UF2ConfigReader(QObject *parent) : QObject(parent), m_loaded(fa QString UF2ConfigReader::getBinPath() { - QString appDir = QCoreApplication::applicationDirPath(); - return appDir + "/uf2touft3"; + return Constants::toolDir(); } bool UF2ConfigReader::loadAllUF20Config() diff --git a/src/utils/uft3configreader.cpp b/src/utils/uft3configreader.cpp index 55f06ac..66553b8 100644 --- a/src/utils/uft3configreader.cpp +++ b/src/utils/uft3configreader.cpp @@ -1,4 +1,5 @@ #include "uft3configreader.h" +#include "Constants.h" #include "datacache.h" #include "logmanager.h" #include @@ -15,8 +16,7 @@ UFT3ConfigReader::UFT3ConfigReader(QObject *parent) : QObject(parent), m_loaded( QString UFT3ConfigReader::getBinPath() { - QString appDir = QCoreApplication::applicationDirPath(); - return appDir + "/uf2touft3"; + return Constants::toolDir(); } bool UFT3ConfigReader::loadAllUFT3Config() diff --git a/src/utils/version.cpp b/src/utils/version.cpp index 9ee349d..9d7cf77 100644 --- a/src/utils/version.cpp +++ b/src/utils/version.cpp @@ -1,12 +1,29 @@ #include "version.h" +#include "Constants.h" #include namespace Version { - QString getVersionString() { - return "1.0.0"; + + QString getVersionString() + { + return QString::fromLatin1(Constants::App::Version); } - QString getBuildDateTime() { + QString getBuildDateTime() + { return QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); } -} \ No newline at end of file + + int compare(const QString &v1, const QString &v2) + { + QStringList parts1 = v1.split('.'); + QStringList parts2 = v2.split('.'); + int maxLen = qMax(parts1.size(), parts2.size()); + for (int i = 0; i < maxLen; ++i) { + int n1 = (i < parts1.size()) ? parts1[i].toInt() : 0; + int n2 = (i < parts2.size()) ? parts2[i].toInt() : 0; + if (n1 != n2) return n1 - n2; + } + return 0; + } +} diff --git a/src/utils/version.h b/src/utils/version.h index ac57613..4ab8f0e 100644 --- a/src/utils/version.h +++ b/src/utils/version.h @@ -6,6 +6,8 @@ namespace Version { QString getVersionString(); QString getBuildDateTime(); + // 比较两个语义化版本号,返回: <0 表示 v10 表示 v1>v2 + int compare(const QString &v1, const QString &v2); } -#endif // VERSION_H \ No newline at end of file +#endif // VERSION_H diff --git a/src/utils/versionchecker.cpp b/src/utils/versionchecker.cpp new file mode 100644 index 0000000..52e683b --- /dev/null +++ b/src/utils/versionchecker.cpp @@ -0,0 +1,78 @@ +#include "versionchecker.h" +#include "version.h" +#include +#include +#include +#include + +VersionChecker::VersionChecker(QObject *parent) + : QObject(parent) + , m_manager(new QNetworkAccessManager(this)) +{ +} + +void VersionChecker::check(const QString &updateJsonUrl, const QString ¤tVersion) +{ + m_currentVersion = currentVersion; + + QNetworkRequest request{QUrl(updateJsonUrl)}; + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + QNetworkReply *reply = m_manager->get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + onReplyFinished(reply); + }); +} + +void VersionChecker::onReplyFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + emit checkError(QString("无法连接更新服务器: %1").arg(reply->errorString())); + return; + } + + QByteArray data = reply->readAll(); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { + emit checkError(QString("更新信息格式错误: %1").arg(parseError.errorString())); + return; + } + + QJsonObject obj = doc.object(); + + // 遍历所有版本键(如 "V1.0.0"、"V1.1.0"),找出最新版本 + QString latestKey; + QString latestVersion; // 去掉 V 前缀后的纯版本号,如 "1.0.0" + for (auto it = obj.begin(); it != obj.end(); ++it) { + QString key = it.key(); + // 去掉可能的 "V" 或 "v" 前缀 + QString ver = key; + if (ver.startsWith('V') || ver.startsWith('v')) + ver = ver.mid(1); + + if (latestVersion.isEmpty() || Version::compare(ver, latestVersion) > 0) { + latestVersion = ver; + latestKey = key; + } + } + + if (latestKey.isEmpty()) { + emit checkError("更新信息中未找到任何版本记录"); + return; + } + + int cmp = Version::compare(latestVersion, m_currentVersion); + if (cmp <= 0) { + emit noUpdateNeeded(); + return; + } + + QJsonObject versionObj = obj.value(latestKey).toObject(); + QString releaseNotes = versionObj.value("release_notes").toString(); + + emit newVersionAvailable(latestVersion, releaseNotes); +} diff --git a/src/utils/versionchecker.h b/src/utils/versionchecker.h new file mode 100644 index 0000000..9913f5e --- /dev/null +++ b/src/utils/versionchecker.h @@ -0,0 +1,29 @@ +#ifndef VERSIONCHECKER_H +#define VERSIONCHECKER_H + +#include +#include + +class VersionChecker : public QObject +{ + Q_OBJECT + +public: + explicit VersionChecker(QObject *parent = nullptr); + void check(const QString &updateJsonUrl, const QString ¤tVersion); + +signals: + void newVersionAvailable(const QString &latestVersion, + const QString &releaseNotes); + void noUpdateNeeded(); + void checkError(const QString &errorMessage); + +private slots: + void onReplyFinished(QNetworkReply *reply); + +private: + QNetworkAccessManager *m_manager; + QString m_currentVersion; +}; + +#endif // VERSIONCHECKER_H