新增按需转码功能;支持批量查询和批量添加

This commit is contained in:
taocong45644
2026-09-15 11:14:25 +08:00
committed by taocong
parent 644fa8b4ce
commit 7b0a7e1687
66 changed files with 4092 additions and 871 deletions
+138 -12
View File
@@ -5,6 +5,7 @@
#include <QHBoxLayout>
#include <QGroupBox>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QDir>
#include <QJsonDocument>
@@ -25,6 +26,7 @@
#include "ElaPushButton.h"
#include "ElaProgressBar.h"
#include "ElaText.h"
#include "ElaTheme.h"
namespace {
@@ -93,6 +95,11 @@ BatchConvertPage::BatchConvertPage(QWidget *parent)
m_lastFileMtime = currentFileMtime();
setupConfigWatcher();
// 灰字进度标签跟随主题明暗切换(上轮修改实际未落盘, 此处补全)
connect(ElaTheme::getInstance(), &ElaTheme::themeModeChanged,
this, &BatchConvertPage::updateTipStyle);
updateTipStyle();
}
BatchConvertPage::~BatchConvertPage()
@@ -150,7 +157,6 @@ void BatchConvertPage::initUI()
progressLayout->setSpacing(10);
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);
@@ -232,9 +238,13 @@ void BatchConvertPage::updateTable()
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);
});
// QueuedConnection: onEditRow 经 loadFromCustJson 重建表格会析构 editBtn 自身,
// 同步回调返回到按钮内部事件处理时构成 use-after-free, 故延迟到事件循环执行。
// 捕获函数名而非行号且以按钮为接收者: 排队前表格可能被重建, 行号会指向错误行;
// 函数名是稳定键, 且按钮析构时过期的排队请求随之失效
connect(editBtn, &QPushButton::clicked, editBtn, [this, funcName]() {
onEditRowByName(funcName);
}, Qt::QueuedConnection);
m_funcTable->setIndexWidget(m_tableModel->index(i, 2), editBtn);
}
@@ -263,6 +273,16 @@ void BatchConvertPage::showEvent(QShowEvent *event)
updateTable();
}
// 按函数名定位行再编辑: 表格可能在外部文件监控/切页时被全量重建,
// 排队请求中的旧行号会失效指向错误行, 函数名是唯一稳定键
void BatchConvertPage::onEditRowByName(const QString &funcName)
{
const int row = m_funcList.indexOf(funcName);
if (row >= 0) {
onEditRow(row);
}
}
void BatchConvertPage::onEditRow(int row)
{
if (row < 0 || row >= m_funcList.size()) {
@@ -310,6 +330,8 @@ void BatchConvertPage::onEditRow(int row)
updateTable();
if (saveToCustJson(m_funcList)) {
loadFromCustJson();
} else {
QMessageBox::warning(this, "保存失败", "函数名已在内存中修改,但写入 cust.json 失败(文件被占用或权限不足?),重启后修改将丢失!");
}
}
@@ -376,6 +398,14 @@ void BatchConvertPage::onAddFunction()
QString("成功添加 %1 个函数").arg(addedCount));
}
// 落盘: 新增函数持久化到 cust.json, 否则只写内存重启后丢失(与 onEditRow 保持一致)
if (addedCount > 0) {
if (saveToCustJson(m_funcList)) {
loadFromCustJson();
} else {
QMessageBox::warning(this, "保存失败", "新增函数已在内存中生效,但写入 cust.json 失败(文件被占用或权限不足?),重启后将丢失!");
}
}
updateTable();
}
@@ -403,6 +433,8 @@ void BatchConvertPage::onRemoveFunction()
updateTable();
if (saveToCustJson(m_funcList)) {
loadFromCustJson();
} else {
QMessageBox::warning(this, "保存失败", "删除操作已在内存中生效,但写入 cust.json 失败(文件被占用或权限不足?),重启后将恢复原清单!");
}
}
@@ -420,10 +452,21 @@ void BatchConvertPage::onClearTable()
updateTable();
if (saveToCustJson(m_funcList)) {
loadFromCustJson();
} else {
QMessageBox::warning(this, "保存失败", "清空操作已在内存中生效,但写入 cust.json 失败(文件被占用或权限不足?),重启后将恢复原清单!");
}
}
}
void BatchConvertPage::updateTipStyle()
{
if (!m_progressLabel) return;
const ElaThemeType::ThemeMode mode = ElaTheme::getInstance()->getThemeMode();
const QString textColor = ElaThemeColor(mode, BasicText).name();
// 仅联动颜色: ElaText 保持默认字号(与"按需转码"页一致), 强设 12px 过小(用户反馈)
m_progressLabel->setStyleSheet(QString("color: %1;").arg(textColor));
}
bool BatchConvertPage::saveToCustJson(const QStringList &funcList)
{
QString jsonPath = Constants::custJson();
@@ -457,22 +500,42 @@ bool BatchConvertPage::saveToCustJson(const QStringList &funcList)
int bracketStart = dirMatch.capturedEnd() - 1; // 指向 '['
// 括号计数找到匹配的 ']'
// 括号计数找到匹配的 ']'(必须跳过字符串字面量, 否则字符串内的 [ ] 会截断匹配改坏 JSON)
int bracketCount = 1;
int pos = bracketStart + 1;
bool inString = false;
bool escaped = false;
while (pos < content.length() && bracketCount > 0) {
QChar ch = content[pos];
if (ch == '[') bracketCount++;
else if (ch == ']') bracketCount--;
if (inString) {
if (escaped) { escaped = false; }
else if (ch == '\\') { escaped = true; }
else if (ch == '"') { inString = false; }
} else {
if (ch == '"') { inString = true; }
else if (ch == '[') bracketCount++;
else if (ch == ']') bracketCount--;
}
pos++;
}
if (bracketCount != 0) {
LogManager::instance()->logWarning("全量转码 - 保存配置失败,dir 数组括号不匹配");
return false;
}
int bracketEnd = pos; // 指向 ']' 之后一位
// 构建新数组(保持原始缩进风格:项 12 空格,闭合 ] 8 空格)
// 函数名做 JSON 字符串转义, 防止含引号/反斜杠时产生非法 JSON
auto jsonEscape = [](const QString &s) -> QString {
QString out = s;
out.replace('\\', "\\\\");
out.replace('"', "\\\"");
return out;
};
QString newArray = "[";
for (int i = 0; i < funcList.size(); ++i) {
if (i > 0) newArray += ",";
newArray += "\n \"" + funcList[i] + ".service_design\"";
newArray += "\n \"" + jsonEscape(funcList[i]) + ".service_design\"";
}
if (!funcList.isEmpty()) {
newArray += "\n ";
@@ -482,15 +545,30 @@ bool BatchConvertPage::saveToCustJson(const QStringList &funcList)
// 只替换 [bracketStart, bracketEnd) 区间,其余内容原样保留
content.replace(bracketStart, bracketEnd - bracketStart, newArray);
// 写回
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
// 写回前校验替换结果仍是合法 JSON, 防止改坏的配置被原地写回无法恢复
QJsonParseError parseErr;
QJsonDocument::fromJson(content.toUtf8(), &parseErr);
if (parseErr.error != QJsonParseError::NoError) {
LogManager::instance()->logWarning(
QString("全量转码 - 保存配置中止, 替换后 JSON 非法: %1").arg(parseErr.errorString()));
return false;
}
// QSaveFile 原子写: 先写临时文件, commit 成功才替换目标文件,
// 避免写入中途崩溃/断电损坏配置
QSaveFile saveFile(jsonPath);
if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
LogManager::instance()->logWarning(
QString("全量转码 - 保存配置失败,无法写入: %1").arg(jsonPath));
return false;
}
file.write(content.toUtf8());
file.close();
saveFile.write(content.toUtf8());
if (!saveFile.commit()) {
LogManager::instance()->logWarning(
QString("全量转码 - 保存配置失败,写入提交异常: %1").arg(jsonPath));
return false;
}
// 程序自身写入后更新 mtime,避免触发无谓的重载
m_lastFileMtime = currentFileMtime();
@@ -577,6 +655,14 @@ void BatchConvertPage::onConvertFinished()
m_startBtn->setStyleSheet("background-color: #1abc9c; color: white; padding: 10px 30px; font-size: 14px; border: none; border-radius: 4px;");
}
void BatchConvertPage::onConvertFailed(const QString &detail)
{
// 失败收尾: 进度条保持当前进度(不复位 100%, 与成功完成区分), 文字提示转换失败
m_progressLabel->setText(detail.isEmpty() ? "转换失败" : detail);
m_startBtn->setText("开始转换");
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) {
@@ -612,6 +698,46 @@ void BatchConvertPage::addFunction(const QString &funcName)
}
}
QPair<int, int> BatchConvertPage::addFunctionsBatch(const QStringList &funcNames, bool notify)
{
// 批量添加(功能查询页多选双击): 不逐条弹窗;
// notify=true 时结束后一次汇总提示, notify=false 静默(由调用方统一汇总), 返回 {新增数, 跳过数}
QStringList added;
int skipped = 0;
for (const QString &funcName : funcNames) {
if (funcName.isEmpty()) {
continue;
}
if (m_funcList.contains(funcName)) {
++skipped;
continue;
}
m_funcList.append(funcName);
added.append(funcName);
}
if (added.isEmpty()) {
if (notify && skipped > 0) {
QMessageBox::information(nullptr, "提示",
QString("所选 %1 个功能均已在\"全量转码\"列表中").arg(skipped));
}
return qMakePair(0, skipped);
}
updateTable();
if (!saveToCustJson(m_funcList)) {
QMessageBox::warning(nullptr, "警告",
QString("添加成功,但保存到 cust.json 失败(%1 个功能)").arg(added.size()));
return qMakePair(-1, -1);
}
if (notify) {
QMessageBox::information(nullptr, "提示",
QString("已将 %1 个功能加入\"全量转码\"").arg(added.size()) +
(skipped > 0 ? QString("(另有 %1 个已存在,已跳过)").arg(skipped) : QString()));
}
return qMakePair(added.size(), skipped);
}
void BatchConvertPage::setupConfigWatcher()
{
QString configPath = getConfigFilePath();