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 下载 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 更新记录 Start typing here...
Last modified: 02 七月 2026
\ 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