支持全量转码和按需转码

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