新增数据映射页面,表映射、函数映射;新增版本更新提示功能
This commit is contained in:
parent
d4d607dc6a
commit
1f33f0fbaa
|
|
@ -1,4 +1,4 @@
|
||||||
QT += core gui widgets sql
|
QT += core gui widgets sql network
|
||||||
|
|
||||||
TARGET = Uft30ChangeCode
|
TARGET = Uft30ChangeCode
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
@ -15,9 +15,10 @@ SOURCES += main.cpp \
|
||||||
src/pages/functionsearch/uft3functionsearchpage.cpp \
|
src/pages/functionsearch/uft3functionsearchpage.cpp \
|
||||||
src/components/elasearchedit.cpp \
|
src/components/elasearchedit.cpp \
|
||||||
src/components/interdialog.cpp \
|
src/components/interdialog.cpp \
|
||||||
src/pages/help/helpdocpage.cpp \
|
|
||||||
src/pages/settings/settingspage.cpp \
|
src/pages/settings/settingspage.cpp \
|
||||||
src/pages/metadatupdate/metadatupdatepage.cpp \
|
src/pages/metadatupdate/metadatupdatepage.cpp \
|
||||||
|
src/pages/datamapping/tablemappingpage.cpp \
|
||||||
|
src/pages/datamapping/functionmappingpage.cpp \
|
||||||
src/metadataupdate/filedb.cpp \
|
src/metadataupdate/filedb.cpp \
|
||||||
src/metadataupdate/uf2interface.cpp \
|
src/metadataupdate/uf2interface.cpp \
|
||||||
src/metadataupdate/uft3interface.cpp \
|
src/metadataupdate/uft3interface.cpp \
|
||||||
|
|
@ -33,7 +34,8 @@ SOURCES += main.cpp \
|
||||||
src/utils/uft3configreader.cpp \
|
src/utils/uft3configreader.cpp \
|
||||||
src/utils/logmanager.cpp \
|
src/utils/logmanager.cpp \
|
||||||
src/utils/configmanager.cpp \
|
src/utils/configmanager.cpp \
|
||||||
src/utils/version.cpp
|
src/utils/version.cpp \
|
||||||
|
src/utils/versionchecker.cpp
|
||||||
|
|
||||||
HEADERS += src/mainwindow/mainwindow.h \
|
HEADERS += src/mainwindow/mainwindow.h \
|
||||||
src/pythonrunner/PythonRunner.h \
|
src/pythonrunner/PythonRunner.h \
|
||||||
|
|
@ -44,9 +46,10 @@ HEADERS += src/mainwindow/mainwindow.h \
|
||||||
src/pages/functionsearch/uft3functionsearchpage.h \
|
src/pages/functionsearch/uft3functionsearchpage.h \
|
||||||
src/components/elasearchedit.h \
|
src/components/elasearchedit.h \
|
||||||
src/components/interdialog.h \
|
src/components/interdialog.h \
|
||||||
src/pages/help/helpdocpage.h \
|
|
||||||
src/pages/settings/settingspage.h \
|
src/pages/settings/settingspage.h \
|
||||||
src/pages/metadatupdate/metadatupdatepage.h \
|
src/pages/metadatupdate/metadatupdatepage.h \
|
||||||
|
src/pages/datamapping/tablemappingpage.h \
|
||||||
|
src/pages/datamapping/functionmappingpage.h \
|
||||||
src/metadataupdate/filedb.h \
|
src/metadataupdate/filedb.h \
|
||||||
src/metadataupdate/uf2interface.h \
|
src/metadataupdate/uf2interface.h \
|
||||||
src/metadataupdate/uft3interface.h \
|
src/metadataupdate/uft3interface.h \
|
||||||
|
|
@ -62,7 +65,8 @@ HEADERS += src/mainwindow/mainwindow.h \
|
||||||
src/utils/uft3configreader.h \
|
src/utils/uft3configreader.h \
|
||||||
src/utils/logmanager.h \
|
src/utils/logmanager.h \
|
||||||
src/utils/configmanager.h \
|
src/utils/configmanager.h \
|
||||||
src/utils/version.h
|
src/utils/version.h \
|
||||||
|
src/utils/versionchecker.h
|
||||||
|
|
||||||
RESOURCES += resources.qrc
|
RESOURCES += resources.qrc
|
||||||
|
|
||||||
|
|
@ -86,4 +90,3 @@ OBJECTS_DIR = $$PWD/build
|
||||||
MOC_DIR = $$PWD/build
|
MOC_DIR = $$PWD/build
|
||||||
RCC_DIR = $$PWD/build
|
RCC_DIR = $$PWD/build
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="元数据更新.md"/>
|
<toc-element topic="元数据更新.md"/>
|
||||||
<toc-element topic="更新记录.md">
|
<toc-element topic="更新记录.md">
|
||||||
|
<toc-element topic="update-1-2-0.md"/>
|
||||||
|
<toc-element topic="update-1-1-0.md"/>
|
||||||
<toc-element topic="update-1-0-0.md"/>
|
<toc-element topic="update-1-0-0.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="下载.md"/>
|
<toc-element topic="下载.md"/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 1.1.0
|
||||||
|
|
||||||
|
1.支持2.0账户功能查询及业务转码
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 1.2.0
|
||||||
|
|
||||||
|
1.新增数据映射页面,表映射、函数映射
|
||||||
|
|
||||||
|
2.新增版本更新提示功能
|
||||||
|
|
@ -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) | |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html SYSTEM "about:legacy-compat">
|
||||||
|
<html lang="en-US" data-preset="contrast" data-primary-color="#307FFF"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta charset="UTF-8"><meta name="robots" content="noindex"><meta name="built-on" content="2026-07-02T10:44:03.5859325"><title>下载 | Uft30ChangeCode</title><script type="application/json" id="virtual-toc-data">[{"id":"qm9v8k_3","level":0,"title":"全量更新","anchor":"#qm9v8k_3"},{"id":"qm9v8k_4","level":0,"title":"增量更新","anchor":"#qm9v8k_4"},{"id":"qm9v8k_5","level":0,"title":"下载","anchor":"#qm9v8k_5"}]</script><script type="application/json" id="topic-shortcuts"></script><link href="https://resources.jetbrains.com/writerside/apidoc/6.10.0-b419/app.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="16x16" href="images/ChangeCode.png"><meta name="image" content=""><!-- Open Graph --><meta property="og:title" content="下载 | Uft30ChangeCode"><meta property="og:description" content=""><meta property="og:image" content=""><meta property="og:site_name" content="Uft30ChangeCode Help"><meta property="og:type" content="website"><meta property="og:locale" content="en_US"><meta property="og:url" content="writerside-documentation/下载.html"><!-- End Open Graph --><!-- Twitter Card --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:site" content=""><meta name="twitter:title" content="下载 | Uft30ChangeCode"><meta name="twitter:description" content=""><meta name="twitter:creator" content=""><meta name="twitter:image:src" content=""><!-- End Twitter Card --><!-- Schema.org WebPage --><script type="application/ld+json">{
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "writerside-documentation/下载.html#webpage",
|
||||||
|
"url": "writerside-documentation/下载.html",
|
||||||
|
"name": "下载 | Uft30ChangeCode",
|
||||||
|
"description": "",
|
||||||
|
"image": "",
|
||||||
|
"inLanguage":"en-US"
|
||||||
|
}</script><!-- End Schema.org --><!-- Schema.org WebSite --><script type="application/ld+json">{
|
||||||
|
"@type": "WebSite",
|
||||||
|
"@id": "writerside-documentation/#website",
|
||||||
|
"url": "writerside-documentation/",
|
||||||
|
"name": "Uft30ChangeCode Help"
|
||||||
|
}</script><!-- End Schema.org --></head><body data-id="下载" data-main-title="下载" data-article-props="{"seeAlsoStyle":"links"}" data-template="article" data-breadcrumbs=""><div class="wrapper"><main class="panel _main"><header class="panel__header"><div class="container"><h3>Uft30ChangeCode Help</h3><div class="panel-trigger"></div></div></header><section class="panel__content"><div class="container"><article class="article" data-shortcut-switcher="inactive"><h1 data-toc="下载" id="下载.md">下载</h1><section class="chapter"><h2 id="qm9v8k_3" data-toc="qm9v8k_3">全量更新</h2><p id="qm9v8k_6">下载全量包,解压即用。</p><aside class="prompt" data-type="note" data-title="" id="qm9v8k_7"><p id="qm9v8k_8">1.0.0版本是基础版本,包含所需功能,如果是第一次下载,请选择全量包。</p></aside></section><section class="chapter"><h2 id="qm9v8k_4" data-toc="qm9v8k_4">增量更新</h2><p id="qm9v8k_9">下载增量包,解压后替换原文件即可</p><aside class="prompt" data-type="note" data-title="" id="qm9v8k_10"><p id="qm9v8k_11">1.0.0版本是基础版本,如若要从旧版升级到1.0.0及以上版本,可以选择增量版本下载,并将旧版根目录下的uf2touft3和当前目录替换覆盖到新版根目录。</p></aside></section><section class="chapter"><h2 id="qm9v8k_5" data-toc="qm9v8k_5">下载</h2><div class="table-wrapper"><table class="wide" id="qm9v8k_12"><thead><tr class="ijRowHead" id="qm9v8k_13"><th id="qm9v8k_16"><p>版本</p></th><th id="qm9v8k_17"><p>更新日期</p></th><th id="qm9v8k_18"><p>全量包</p></th><th id="qm9v8k_19"><p>增量包</p></th></tr></thead><tbody><tr id="qm9v8k_14"><td id="qm9v8k_20"><p>1.1.0</p></td><td id="qm9v8k_21"><p>2026-07-02</p></td><td id="qm9v8k_22"><p><a href="http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0.zip" id="qm9v8k_24" data-external="true" rel="noopener noreferrer">Uft30ChangeCode-1.1.0.zip</a></p></td><td id="qm9v8k_23"><p><a href="http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0-update.zip" id="qm9v8k_25" data-external="true" rel="noopener noreferrer">Uft30ChangeCode-1.1.0-update.zip</a></p></td></tr><tr id="qm9v8k_15"><td id="qm9v8k_26"><p>1.0.0</p></td><td id="qm9v8k_27"><p>2026-06-01</p></td><td id="qm9v8k_28"><p><a href="http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.0.0.zip" id="qm9v8k_30" data-external="true" rel="noopener noreferrer">Uft30ChangeCode-1.0.0.zip</a></p></td><td id="qm9v8k_29"></td></tr></tbody></table></div></section><div class="last-modified">Last modified: 02 七月 2026</div><div data-feedback-placeholder="true"></div><div class="navigation-links _bottom"><a href="update-1-0-0.html" class="navigation-links__prev">1.0.0</a></div></article><div id="disqus_thread"></div></div></section></main></div><script src="https://resources.jetbrains.com/writerside/apidoc/6.10.0-b419/app.js"></script></body></html>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html SYSTEM "about:legacy-compat">
|
||||||
|
<html lang="en-US" data-preset="contrast" data-primary-color="#307FFF"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta charset="UTF-8"><meta name="robots" content="noindex"><meta name="built-on" content="2026-07-02T10:44:03.5744178"><title>更新记录 | Uft30ChangeCode</title><script type="application/json" id="virtual-toc-data">[]</script><script type="application/json" id="topic-shortcuts"></script><link href="https://resources.jetbrains.com/writerside/apidoc/6.10.0-b419/app.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="16x16" href="images/ChangeCode.png"><meta name="image" content=""><!-- Open Graph --><meta property="og:title" content="更新记录 | Uft30ChangeCode"><meta property="og:description" content=""><meta property="og:image" content=""><meta property="og:site_name" content="Uft30ChangeCode Help"><meta property="og:type" content="website"><meta property="og:locale" content="en_US"><meta property="og:url" content="writerside-documentation/更新记录.html"><!-- End Open Graph --><!-- Twitter Card --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:site" content=""><meta name="twitter:title" content="更新记录 | Uft30ChangeCode"><meta name="twitter:description" content=""><meta name="twitter:creator" content=""><meta name="twitter:image:src" content=""><!-- End Twitter Card --><!-- Schema.org WebPage --><script type="application/ld+json">{
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "writerside-documentation/更新记录.html#webpage",
|
||||||
|
"url": "writerside-documentation/更新记录.html",
|
||||||
|
"name": "更新记录 | Uft30ChangeCode",
|
||||||
|
"description": "",
|
||||||
|
"image": "",
|
||||||
|
"inLanguage":"en-US"
|
||||||
|
}</script><!-- End Schema.org --><!-- Schema.org WebSite --><script type="application/ld+json">{
|
||||||
|
"@type": "WebSite",
|
||||||
|
"@id": "writerside-documentation/#website",
|
||||||
|
"url": "writerside-documentation/",
|
||||||
|
"name": "Uft30ChangeCode Help"
|
||||||
|
}</script><!-- End Schema.org --></head><body data-id="更新记录" data-main-title="更新记录" data-article-props="{"seeAlsoStyle":"links"}" data-template="article" data-breadcrumbs=""><div class="wrapper"><main class="panel _main"><header class="panel__header"><div class="container"><h3>Uft30ChangeCode Help</h3><div class="panel-trigger"></div></div></header><section class="panel__content"><div class="container"><article class="article" data-shortcut-switcher="inactive"><h1 data-toc="更新记录" id="更新记录.md">更新记录</h1><p id="r4m9bp_3">Start typing here...</p><div class="last-modified">Last modified: 02 七月 2026</div><div data-feedback-placeholder="true"></div><div class="navigation-links _bottom"><a href="元数据更新.html" class="navigation-links__prev">元数据更新</a><a href="update-1-1-0.html" class="navigation-links__next">1.1.0</a></div></article><div id="disqus_thread"></div></div></section></main></div><script src="https://resources.jetbrains.com/writerside/apidoc/6.10.0-b419/app.js"></script></body></html>
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
#include "utils/uf2configreader.h"
|
#include "utils/uf2configreader.h"
|
||||||
#include "utils/uft3configreader.h"
|
#include "utils/uft3configreader.h"
|
||||||
#include "utils/logmanager.h"
|
#include "utils/logmanager.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
|
#include "utils/versionchecker.h"
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -25,6 +27,12 @@
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
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)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
: ElaWindow(parent)
|
: ElaWindow(parent)
|
||||||
, m_pythonRunner(new PythonRunner(this))
|
, m_pythonRunner(new PythonRunner(this))
|
||||||
|
|
@ -38,14 +46,12 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
LogManager::instance()->initLogFile();
|
LogManager::instance()->initLogFile();
|
||||||
LogManager::instance()->logInfo("应用程序启动");
|
LogManager::instance()->logInfo("应用程序启动");
|
||||||
|
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
|
||||||
|
|
||||||
loadConfigCache();
|
loadConfigCache();
|
||||||
|
|
||||||
m_pythonRunner->setRunMode(PythonRunner::UsePackagedExe);
|
m_pythonRunner->setRunMode(PythonRunner::UsePackagedExe);
|
||||||
|
|
||||||
QString exePath = appDir + "/uf2touft3/uf2touft3.exe";
|
QString exePath = Constants::toolExe();
|
||||||
QString workDir = appDir + "/uf2touft3";
|
QString workDir = Constants::toolDir();
|
||||||
|
|
||||||
m_pythonRunner->setPackagedExePath(exePath);
|
m_pythonRunner->setPackagedExePath(exePath);
|
||||||
m_pythonRunner->setWorkingDirectory(workDir);
|
m_pythonRunner->setWorkingDirectory(workDir);
|
||||||
|
|
@ -62,6 +68,9 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
connect(this, &ElaWindow::navigationNodeClicked, this, &MainWindow::onNavigationNodeClicked);
|
connect(this, &ElaWindow::navigationNodeClicked, this, &MainWindow::onNavigationNodeClicked);
|
||||||
|
|
||||||
QTimer::singleShot(100, this, &MainWindow::updateNavigationButtonIcon);
|
QTimer::singleShot(100, this, &MainWindow::updateNavigationButtonIcon);
|
||||||
|
|
||||||
|
// 启动后延迟检查版本更新
|
||||||
|
QTimer::singleShot(3000, this, &MainWindow::checkForUpdates);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadConfigCache()
|
void MainWindow::loadConfigCache()
|
||||||
|
|
@ -126,6 +135,9 @@ void MainWindow::initContent()
|
||||||
addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
||||||
expandNavigationNode(m_funcConvertKey);
|
expandNavigationNode(m_funcConvertKey);
|
||||||
|
|
||||||
|
addExpanderNode("数据映射", m_dataMappingKey, ElaIconType::TableList);
|
||||||
|
addPageNode("表替换", createTableMappingPage(), m_dataMappingKey, ElaIconType::TableColumns);
|
||||||
|
addPageNode("函数替换", createFunctionMappingPage(), m_dataMappingKey, ElaIconType::Function);
|
||||||
addPageNode("元数据更新", createMetadataUpdatePage(), ElaIconType::ArrowsRotate);
|
addPageNode("元数据更新", createMetadataUpdatePage(), ElaIconType::ArrowsRotate);
|
||||||
|
|
||||||
addFooterNode("帮助", m_helpKey, 0, ElaIconType::CircleQuestion);
|
addFooterNode("帮助", m_helpKey, 0, ElaIconType::CircleQuestion);
|
||||||
|
|
@ -169,6 +181,16 @@ QWidget* MainWindow::createMetadataUpdatePage()
|
||||||
return new MetadataUpdatePage();
|
return new MetadataUpdatePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QWidget* MainWindow::createTableMappingPage()
|
||||||
|
{
|
||||||
|
return new TableMappingPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* MainWindow::createFunctionMappingPage()
|
||||||
|
{
|
||||||
|
return new FunctionMappingPage();
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::onBatchConvertStart()
|
void MainWindow::onBatchConvertStart()
|
||||||
{
|
{
|
||||||
if (m_pythonRunner->isRunning()) {
|
if (m_pythonRunner->isRunning()) {
|
||||||
|
|
@ -177,8 +199,7 @@ void MainWindow::onBatchConvertStart()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
QString exePath = Constants::toolExe();
|
||||||
QString exePath = appDir + "/uf2touft3/uf2touft3.exe";
|
|
||||||
|
|
||||||
if (!m_pythonRunner->canRun()) {
|
if (!m_pythonRunner->canRun()) {
|
||||||
LogManager::instance()->logError("错误: 找不到 uf2touft3/uf2touft3.exe!");
|
LogManager::instance()->logError("错误: 找不到 uf2touft3/uf2touft3.exe!");
|
||||||
|
|
@ -382,11 +403,46 @@ void MainWindow::onNavigationButtonClicked()
|
||||||
m_navigationButton->update();
|
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)
|
void MainWindow::onNavigationNodeClicked(ElaNavigationType::NavigationNodeType nodeType, QString nodeKey)
|
||||||
{
|
{
|
||||||
Q_UNUSED(nodeType);
|
Q_UNUSED(nodeType);
|
||||||
if (nodeKey == m_helpKey) {
|
if (nodeKey == m_helpKey) {
|
||||||
QDesktopServices::openUrl(QUrl("http://10.20.163.105:6045/uft3changecode"));
|
QDesktopServices::openUrl(QUrl(HELP_PAGE));
|
||||||
} else if (nodeKey == m_aboutKey) {
|
} else if (nodeKey == m_aboutKey) {
|
||||||
std::make_unique<AboutDialog>(this)->exec();
|
std::make_unique<AboutDialog>(this)->exec();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#include "src/pages/about/aboutdialog.h"
|
#include "src/pages/about/aboutdialog.h"
|
||||||
#include "src/pages/settings/settingspage.h"
|
#include "src/pages/settings/settingspage.h"
|
||||||
#include "src/pages/metadatupdate/metadatupdatepage.h"
|
#include "src/pages/metadatupdate/metadatupdatepage.h"
|
||||||
|
#include "src/pages/datamapping/tablemappingpage.h"
|
||||||
|
#include "src/pages/datamapping/functionmappingpage.h"
|
||||||
#include "src/utils/version.h"
|
#include "src/utils/version.h"
|
||||||
|
|
||||||
class MainWindow : public ElaWindow
|
class MainWindow : public ElaWindow
|
||||||
|
|
@ -33,6 +35,8 @@ private:
|
||||||
QWidget* createUFT3FunctionSearchPage();
|
QWidget* createUFT3FunctionSearchPage();
|
||||||
QWidget* createSettingsPage();
|
QWidget* createSettingsPage();
|
||||||
QWidget* createMetadataUpdatePage();
|
QWidget* createMetadataUpdatePage();
|
||||||
|
QWidget* createTableMappingPage();
|
||||||
|
QWidget* createFunctionMappingPage();
|
||||||
|
|
||||||
void loadConfigCache();
|
void loadConfigCache();
|
||||||
|
|
||||||
|
|
@ -42,6 +46,8 @@ private:
|
||||||
void onPythonRunnerOutput(const QString &output);
|
void onPythonRunnerOutput(const QString &output);
|
||||||
void onPythonRunnerError(const QString &error);
|
void onPythonRunnerError(const QString &error);
|
||||||
|
|
||||||
|
void checkForUpdates();
|
||||||
|
void onNewVersionFound(const QString &latestVersion, const QString &releaseNotes);
|
||||||
void updateNavigationButtonIcon();
|
void updateNavigationButtonIcon();
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
void onNavigationButtonClicked();
|
void onNavigationButtonClicked();
|
||||||
|
|
@ -61,6 +67,7 @@ private:
|
||||||
QString m_settingKey;
|
QString m_settingKey;
|
||||||
QString m_funcSearchKey;
|
QString m_funcSearchKey;
|
||||||
QString m_funcConvertKey;
|
QString m_funcConvertKey;
|
||||||
|
QString m_dataMappingKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MAINWINDOW_H
|
#endif // MAINWINDOW_H
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "metadataprocessor.h"
|
#include "metadataprocessor.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
#include "uf2interface.h"
|
#include "uf2interface.h"
|
||||||
#include "uft3interface.h"
|
#include "uft3interface.h"
|
||||||
#include "filedb.h"
|
#include "filedb.h"
|
||||||
|
|
@ -22,7 +23,7 @@ MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent)
|
||||||
void MetadataProcessor::initConfig(const QString& basePath)
|
void MetadataProcessor::initConfig(const QString& basePath)
|
||||||
{
|
{
|
||||||
m_basePath = basePath;
|
m_basePath = basePath;
|
||||||
m_uf2touft3Path = QDir(basePath).filePath("uf2touft3");
|
m_uf2touft3Path = Constants::toolDir();
|
||||||
|
|
||||||
LogManager::instance()->log("========== 初始化配置 ==========");
|
LogManager::instance()->log("========== 初始化配置 ==========");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "aboutdialog.h"
|
#include "aboutdialog.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
|
|
||||||
#include "ElaImageCard.h"
|
#include "ElaImageCard.h"
|
||||||
#include "ElaText.h"
|
#include "ElaText.h"
|
||||||
|
|
@ -54,7 +55,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
|
||||||
auto* downloadBtn = new ElaPushButton("下载最新", this);
|
auto* downloadBtn = new ElaPushButton("下载最新", this);
|
||||||
connect(downloadBtn, &ElaPushButton::clicked, []
|
connect(downloadBtn, &ElaPushButton::clicked, []
|
||||||
{
|
{
|
||||||
QDesktopServices::openUrl(QUrl("http://10.20.163.105:6045/uft3changecode/下载.html"));
|
QDesktopServices::openUrl(QUrl(Constants::Urls::downloadPage()));
|
||||||
});
|
});
|
||||||
addFirstButton(downloadBtn);
|
addFirstButton(downloadBtn);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
#include "batchconvertpage.h"
|
#include "batchconvertpage.h"
|
||||||
|
#include "src/utils/Constants.h"
|
||||||
#include "src/utils/uf2configreader.h"
|
#include "src/utils/uf2configreader.h"
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
|
@ -48,19 +55,24 @@ BatchConvertPage::BatchConvertPage(QWidget *parent)
|
||||||
{
|
{
|
||||||
initUI();
|
initUI();
|
||||||
loadFromCustJson();
|
loadFromCustJson();
|
||||||
|
|
||||||
m_configWatcher = new QFileSystemWatcher(this);
|
// 防抖定时器:编辑器保存可能触发多次信号,合并为一次重载
|
||||||
QString configPath = getConfigFilePath();
|
m_reloadTimer = new QTimer(this);
|
||||||
if (!configPath.isEmpty() && QFile::exists(configPath)) {
|
m_reloadTimer->setSingleShot(true);
|
||||||
m_configWatcher->addPath(configPath);
|
m_reloadTimer->setInterval(300);
|
||||||
connect(m_configWatcher, &QFileSystemWatcher::fileChanged, this, &BatchConvertPage::onConfigFileChanged);
|
connect(m_reloadTimer, &QTimer::timeout, this, &BatchConvertPage::onReloadTimeout);
|
||||||
}
|
|
||||||
|
// 记录初始 mtime
|
||||||
|
m_lastFileMtime = currentFileMtime();
|
||||||
|
|
||||||
|
setupConfigWatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchConvertPage::~BatchConvertPage()
|
BatchConvertPage::~BatchConvertPage()
|
||||||
{
|
{
|
||||||
if (m_configWatcher) {
|
if (m_configWatcher) {
|
||||||
m_configWatcher->removePaths(m_configWatcher->files());
|
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)
|
bool BatchConvertPage::saveToCustJson(const QStringList &funcList)
|
||||||
{
|
{
|
||||||
QString jsonPath = QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json";
|
QString jsonPath = Constants::custJson();
|
||||||
|
|
||||||
|
// 读取原始文件内容(保留全部格式不重序列化)
|
||||||
QFile file(jsonPath);
|
QFile file(jsonPath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
LogManager::instance()->logWarning(
|
||||||
|
QString("业务转码 - 保存配置失败,无法打开: %1").arg(jsonPath));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray data = file.readAll();
|
QString content = QString::fromUtf8(file.readAll());
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
QString content = QString::fromUtf8(data);
|
// 用正则定位 "convert" 节点(兼容任意空格)
|
||||||
|
static QRegularExpression convertRe(R"("convert"\s*:)");
|
||||||
int convertStart = content.indexOf("\"convert\":");
|
auto convertMatch = convertRe.match(content);
|
||||||
if (convertStart < 0) {
|
if (!convertMatch.hasMatch()) {
|
||||||
convertStart = content.indexOf("\"convert\" :");
|
LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 convert 节点");
|
||||||
}
|
|
||||||
if (convertStart < 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int dirStart = content.indexOf("\"dir\":[", convertStart);
|
// 在 convert 之后定位 "dir" 数组起始 [
|
||||||
if (dirStart < 0) {
|
static QRegularExpression dirRe(R"("dir"\s*:\s*\[)");
|
||||||
dirStart = content.indexOf("\"dir\" : [", convertStart);
|
auto dirMatch = dirRe.match(content, convertMatch.capturedEnd());
|
||||||
}
|
if (!dirMatch.hasMatch()) {
|
||||||
if (dirStart < 0) {
|
LogManager::instance()->logWarning("业务转码 - 保存配置失败,未找到 dir 数组");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int arrayStart = content.indexOf('[', dirStart);
|
int bracketStart = dirMatch.capturedEnd() - 1; // 指向 '['
|
||||||
|
|
||||||
|
// 括号计数找到匹配的 ']'
|
||||||
int bracketCount = 1;
|
int bracketCount = 1;
|
||||||
int pos = arrayStart + 1;
|
int pos = bracketStart + 1;
|
||||||
while (pos < content.length() && bracketCount > 0) {
|
while (pos < content.length() && bracketCount > 0) {
|
||||||
if (content[pos] == '[') bracketCount++;
|
QChar ch = content[pos];
|
||||||
else if (content[pos] == ']') bracketCount--;
|
if (ch == '[') bracketCount++;
|
||||||
|
else if (ch == ']') bracketCount--;
|
||||||
pos++;
|
pos++;
|
||||||
}
|
}
|
||||||
int arrayEnd = pos;
|
int bracketEnd = pos; // 指向 ']' 之后一位
|
||||||
|
|
||||||
QString newDirArray = "[";
|
// 构建新数组(保持原始缩进风格:项 12 空格,闭合 ] 8 空格)
|
||||||
|
QString newArray = "[";
|
||||||
for (int i = 0; i < funcList.size(); ++i) {
|
for (int i = 0; i < funcList.size(); ++i) {
|
||||||
if (i > 0) newDirArray += ",";
|
if (i > 0) newArray += ",";
|
||||||
newDirArray += "\n \"" + funcList[i] + ".service_design\"";
|
newArray += "\n \"" + funcList[i] + ".service_design\"";
|
||||||
}
|
}
|
||||||
newDirArray += "\n ]";
|
if (!funcList.isEmpty()) {
|
||||||
|
newArray += "\n ";
|
||||||
content.replace(arrayStart, arrayEnd - arrayStart, newDirArray);
|
}
|
||||||
|
newArray += "]";
|
||||||
|
|
||||||
|
// 只替换 [bracketStart, bracketEnd) 区间,其余内容原样保留
|
||||||
|
content.replace(bracketStart, bracketEnd - bracketStart, newArray);
|
||||||
|
|
||||||
|
// 写回
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
LogManager::instance()->logWarning(
|
||||||
|
QString("业务转码 - 保存配置失败,无法写入: %1").arg(jsonPath));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.write(content.toUtf8());
|
file.write(content.toUtf8());
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
|
// 程序自身写入后更新 mtime,避免触发无谓的重载
|
||||||
|
m_lastFileMtime = currentFileMtime();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BatchConvertPage::loadFromCustJson()
|
void BatchConvertPage::loadFromCustJson()
|
||||||
{
|
{
|
||||||
QString jsonPath = QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json";
|
QString jsonPath = Constants::custJson();
|
||||||
|
|
||||||
QFile file(jsonPath);
|
QFile file(jsonPath);
|
||||||
int retryCount = 0;
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
const int maxRetries = 3;
|
LogManager::instance()->logWarning(
|
||||||
const int retryDelayMs = 100;
|
QString("业务转码 - 加载配置失败,无法打开: %1").arg(jsonPath));
|
||||||
|
|
||||||
while (!file.open(QIODevice::ReadOnly) && retryCount < maxRetries) {
|
|
||||||
retryCount++;
|
|
||||||
QThread::msleep(retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.isOpen()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray data = file.readAll();
|
QByteArray data = file.readAll();
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
QString content = QString::fromUtf8(data);
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||||
int convertStart = content.indexOf("\"convert\":");
|
if (parseError.error != QJsonParseError::NoError) {
|
||||||
if (convertStart < 0) {
|
LogManager::instance()->logWarning(
|
||||||
convertStart = content.indexOf("\"convert\" :");
|
QString("业务转码 - 加载配置 JSON 解析失败: %1").arg(parseError.errorString()));
|
||||||
}
|
|
||||||
if (convertStart < 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int dirStart = content.indexOf("\"dir\":[", convertStart);
|
QJsonObject root = doc.object();
|
||||||
if (dirStart < 0) {
|
QJsonArray dir = root["convert"].toObject()["dir"].toArray();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList items;
|
QStringList items;
|
||||||
int itemPos = 0;
|
for (const QJsonValue &val : dir) {
|
||||||
while (itemPos < arrayContent.length()) {
|
QString item = val.toString();
|
||||||
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) {
|
|
||||||
if (item.endsWith(".service_design")) {
|
if (item.endsWith(".service_design")) {
|
||||||
item = item.left(item.length() - static_cast<int>(strlen(".service_design")));
|
item.chop(static_cast<int>(strlen(".service_design")));
|
||||||
}
|
}
|
||||||
|
items.append(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_funcList = items;
|
m_funcList = items;
|
||||||
updateTable();
|
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)
|
void BatchConvertPage::onConfigFileChanged(const QString &path)
|
||||||
{
|
{
|
||||||
QStringList oldList = m_funcList;
|
// 文件可能被编辑器替换导致监视失效,尝试重新添加
|
||||||
loadFromCustJson();
|
if (m_configWatcher && !m_configWatcher->files().contains(path)
|
||||||
|
&& QFile::exists(path)) {
|
||||||
if (m_configWatcher && !m_configWatcher->files().contains(path)) {
|
|
||||||
m_configWatcher->addPath(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
|
QString BatchConvertPage::getConfigFilePath() const
|
||||||
{
|
{
|
||||||
return QCoreApplication::applicationDirPath() + "/uf2touft3/cust.json";
|
return Constants::custJson();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QFileSystemWatcher>
|
#include <QFileSystemWatcher>
|
||||||
#include <QThread>
|
#include <QTimer>
|
||||||
|
#include <QDateTime>
|
||||||
#include "ElaLineEdit.h"
|
#include "ElaLineEdit.h"
|
||||||
#include "ElaTableView.h"
|
#include "ElaTableView.h"
|
||||||
#include "ElaProgressBar.h"
|
#include "ElaProgressBar.h"
|
||||||
|
|
@ -39,6 +40,8 @@ private slots:
|
||||||
void onClearTable();
|
void onClearTable();
|
||||||
void onEditRow(int row);
|
void onEditRow(int row);
|
||||||
void onConfigFileChanged(const QString &path);
|
void onConfigFileChanged(const QString &path);
|
||||||
|
void onConfigDirChanged(const QString &path);
|
||||||
|
void onReloadTimeout();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initUI();
|
void initUI();
|
||||||
|
|
@ -48,6 +51,8 @@ private:
|
||||||
void updateTable();
|
void updateTable();
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
QString getConfigFilePath() const;
|
QString getConfigFilePath() const;
|
||||||
|
void setupConfigWatcher();
|
||||||
|
QDateTime currentFileMtime() const;
|
||||||
|
|
||||||
ElaTableView *m_funcTable;
|
ElaTableView *m_funcTable;
|
||||||
QStandardItemModel *m_tableModel;
|
QStandardItemModel *m_tableModel;
|
||||||
|
|
@ -56,6 +61,8 @@ private:
|
||||||
ElaText *m_progressLabel;
|
ElaText *m_progressLabel;
|
||||||
QPushButton *m_startBtn;
|
QPushButton *m_startBtn;
|
||||||
QFileSystemWatcher *m_configWatcher;
|
QFileSystemWatcher *m_configWatcher;
|
||||||
|
QTimer *m_reloadTimer;
|
||||||
|
QDateTime m_lastFileMtime;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,449 @@
|
||||||
|
#include "functionmappingpage.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QStandardItem>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <QTimer>
|
||||||
|
#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<QStandardItem*> 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<int>(d))
|
||||||
|
return QString::number(static_cast<int>(d));
|
||||||
|
return QString::number(d, 'f');
|
||||||
|
}
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按顺序格式化一个 string->string 键值对的嵌套 JSON 对象
|
||||||
|
static QString formatOrderedJsonObject(const QList<QPair<QString, QString>> &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<QPair<QString, QString>> &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<QStandardItem*> 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<int> 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<int>());
|
||||||
|
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<QPair<QString, QString>> 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<FuncFilterProxyModel*>(m_proxyModel)->setFilterText(text);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#ifndef FUNCTIONMAPPINGPAGE_H
|
||||||
|
#define FUNCTIONMAPPINGPAGE_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QList>
|
||||||
|
#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<QPair<QString, QString>> &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
|
||||||
|
|
@ -0,0 +1,449 @@
|
||||||
|
#include "tablemappingpage.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QStandardItem>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <QTimer>
|
||||||
|
#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<QStandardItem*> 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<int>(d))
|
||||||
|
return QString::number(static_cast<int>(d));
|
||||||
|
return QString::number(d, 'f');
|
||||||
|
}
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按顺序格式化一个 string->string 键值对的嵌套 JSON 对象
|
||||||
|
static QString formatOrderedJsonObject(const QList<QPair<QString, QString>> &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<QPair<QString, QString>> &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<QStandardItem*> 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<int> 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<int>());
|
||||||
|
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<QPair<QString, QString>> 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<TableFilterProxyModel*>(m_proxyModel)->setFilterText(text);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#ifndef TABLEMAPPINGPAGE_H
|
||||||
|
#define TABLEMAPPINGPAGE_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QList>
|
||||||
|
#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<QPair<QString, QString>> &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
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#include "helpdocpage.h"
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QDesktopServices>
|
|
||||||
#include <QUrl>
|
|
||||||
|
|
||||||
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("<a href='https://github.com'>点击此处跳转帮助文档</a>");
|
|
||||||
label->setStyleSheet("font-size: 14px; color: #4a90d9;");
|
|
||||||
label->setOpenExternalLinks(true);
|
|
||||||
layout->addWidget(label, 0, Qt::AlignCenter);
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#ifndef HELPDOCPAGE_H
|
|
||||||
#define HELPDOCPAGE_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QStackedWidget>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QTextBrowser>
|
|
||||||
#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
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "metadatupdatepage.h"
|
#include "metadatupdatepage.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
|
|
@ -15,8 +16,7 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent)
|
||||||
{
|
{
|
||||||
initUI();
|
initUI();
|
||||||
_processor = new MetadataProcessor(this);
|
_processor = new MetadataProcessor(this);
|
||||||
QString basePath = QCoreApplication::applicationDirPath();
|
_processor->initConfig(Constants::toolDir());
|
||||||
_processor->initConfig(basePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MetadataUpdatePage::~MetadataUpdatePage()
|
MetadataUpdatePage::~MetadataUpdatePage()
|
||||||
|
|
@ -261,8 +261,7 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items)
|
||||||
} else if (item == "标准错误号") {
|
} else if (item == "标准错误号") {
|
||||||
processUf2ErrNumber();
|
processUf2ErrNumber();
|
||||||
} else if (item == "接口数据") {
|
} else if (item == "接口数据") {
|
||||||
QString basePath = QCoreApplication::applicationDirPath();
|
processUf2Interface(Constants::toolDir());
|
||||||
processUf2Interface(basePath);
|
|
||||||
interfaceUpdated = true;
|
interfaceUpdated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,8 +297,7 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
|
||||||
} else if (item == "表结构") {
|
} else if (item == "表结构") {
|
||||||
processUft3Table();
|
processUft3Table();
|
||||||
} else if (item == "接口数据") {
|
} else if (item == "接口数据") {
|
||||||
QString basePath = QCoreApplication::applicationDirPath();
|
processUft3Interface(Constants::toolDir());
|
||||||
processUft3Interface(basePath);
|
|
||||||
interfaceUpdated = true;
|
interfaceUpdated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "settingspage.h"
|
#include "settingspage.h"
|
||||||
|
#include "utils/Constants.h"
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -142,7 +143,7 @@ void SettingsPage::loadSettings()
|
||||||
|
|
||||||
QString SettingsPage::getConfigIniPath()
|
QString SettingsPage::getConfigIniPath()
|
||||||
{
|
{
|
||||||
return QCoreApplication::applicationDirPath() + "/uf2touft3/config.ini";
|
return Constants::configIni();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SettingsPage::isValidUtf8(const QByteArray &data)
|
bool SettingsPage::isValidUtf8(const QByteArray &data)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
#ifndef CONSTANTS_H
|
||||||
|
#define CONSTANTS_H
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 项目公共常量 — 所有 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
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "datacache.h"
|
#include "datacache.h"
|
||||||
|
#include "Constants.h"
|
||||||
#include "logmanager.h"
|
#include "logmanager.h"
|
||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
|
|
@ -31,14 +32,14 @@ DataCache* DataCache::instance()
|
||||||
|
|
||||||
QString DataCache::getDatabasePath()
|
QString DataCache::getDatabasePath()
|
||||||
{
|
{
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
QString dir = Constants::cacheDir();
|
||||||
QDir dbDir(appDir + "/cache");
|
QDir dbDir(dir);
|
||||||
if (!dbDir.exists()) {
|
if (!dbDir.exists()) {
|
||||||
if (!dbDir.mkpath(dbDir.absolutePath())) {
|
if (!dbDir.mkpath(dir)) {
|
||||||
LogManager::instance()->logError(QString("数据缓存 - 创建缓存目录失败: %1").arg(dbDir.absolutePath()));
|
LogManager::instance()->logError(QString("数据缓存 - 创建缓存目录失败: %1").arg(dir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dbDir.absoluteFilePath("config_cache.db");
|
return dbDir.absoluteFilePath(Constants::Cache::DbFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DataCache::createTables()
|
bool DataCache::createTables()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "logmanager.h"
|
#include "logmanager.h"
|
||||||
|
#include "Constants.h"
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
|
@ -27,8 +28,8 @@ LogManager* LogManager::instance()
|
||||||
|
|
||||||
void LogManager::initLogFile()
|
void LogManager::initLogFile()
|
||||||
{
|
{
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
QString dir = Constants::logDir();
|
||||||
QDir logDir(appDir + "/logs");
|
QDir logDir(dir);
|
||||||
if (!logDir.exists()) {
|
if (!logDir.exists()) {
|
||||||
logDir.mkpath(".");
|
logDir.mkpath(".");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "uf2configreader.h"
|
#include "uf2configreader.h"
|
||||||
|
#include "Constants.h"
|
||||||
#include "datacache.h"
|
#include "datacache.h"
|
||||||
#include "logmanager.h"
|
#include "logmanager.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
|
@ -15,8 +16,7 @@ UF2ConfigReader::UF2ConfigReader(QObject *parent) : QObject(parent), m_loaded(fa
|
||||||
|
|
||||||
QString UF2ConfigReader::getBinPath()
|
QString UF2ConfigReader::getBinPath()
|
||||||
{
|
{
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
return Constants::toolDir();
|
||||||
return appDir + "/uf2touft3";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UF2ConfigReader::loadAllUF20Config()
|
bool UF2ConfigReader::loadAllUF20Config()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "uft3configreader.h"
|
#include "uft3configreader.h"
|
||||||
|
#include "Constants.h"
|
||||||
#include "datacache.h"
|
#include "datacache.h"
|
||||||
#include "logmanager.h"
|
#include "logmanager.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
|
@ -15,8 +16,7 @@ UFT3ConfigReader::UFT3ConfigReader(QObject *parent) : QObject(parent), m_loaded(
|
||||||
|
|
||||||
QString UFT3ConfigReader::getBinPath()
|
QString UFT3ConfigReader::getBinPath()
|
||||||
{
|
{
|
||||||
QString appDir = QCoreApplication::applicationDirPath();
|
return Constants::toolDir();
|
||||||
return appDir + "/uf2touft3";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UFT3ConfigReader::loadAllUFT3Config()
|
bool UFT3ConfigReader::loadAllUFT3Config()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,29 @@
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
#include "Constants.h"
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
|
||||||
namespace Version {
|
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");
|
return QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
namespace Version {
|
namespace Version {
|
||||||
QString getVersionString();
|
QString getVersionString();
|
||||||
QString getBuildDateTime();
|
QString getBuildDateTime();
|
||||||
|
// 比较两个语义化版本号,返回: <0 表示 v1<v2, 0 表示相等, >0 表示 v1>v2
|
||||||
|
int compare(const QString &v1, const QString &v2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // VERSION_H
|
#endif // VERSION_H
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
#include "versionchecker.h"
|
||||||
|
#include "version.h"
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
#ifndef VERSIONCHECKER_H
|
||||||
|
#define VERSIONCHECKER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
|
||||||
|
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
|
||||||
Loading…
Reference in New Issue