Files
change_code/src/pages/batchconvert/batchconvertpage.cpp
T

464 lines
17 KiB
C++

#include "batchconvertpage.h"
#include "src/utils/Constants.h"
#include "src/utils/logmanager.h"
#include "src/utils/custjsonio.h"
#include <QVBoxLayout>
#include <QFile>
#include <QSaveFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QJsonParseError>
#include <QMessageBox>
#include <QRegularExpression>
#include <QLabel>
#include <QStandardItem>
#include <cstring>
#include "ElaComboBox.h"
#include "ElaTheme.h"
namespace {
// V2 默认值(契约 详细设计文档.md §7.5): core 默认 ucbp, 柜面接口默认否;
// dialect 不落 UI, 保存时由 core 派生(ucbp/uses→mdb, uarg/usms→db)
bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames)
{
// 只允许 LS_ 开头的功能进入全量转码清单
QStringList invalidNames;
for (const QString &name : functionNames) {
if (!name.startsWith("LS_")) {
invalidNames.append(name);
}
}
if (!invalidNames.isEmpty()) {
QMessageBox::warning(
parent,
QStringLiteral("全量转码"),
QStringLiteral("以下功能不是 LS_ 开头,无法加入全量转码清单:\n")
+ invalidNames.join("\n"),
QMessageBox::Ok);
LogManager::instance()->logWarning(
QString("全量转码 - 存在非 LS_ 前缀功能名被拦截: %1")
.arg(invalidNames.join(", ")));
return false;
}
return true;
}
// 清单表格列定义
enum TableCol {
ColName = 0, // UF20函数名
ColExists = 1, // 是否存在
ColCore = 2, // 核心(ucbp/uses/uarg/usms)
ColViaUmgr = 3 // 是否柜面接口
};
// 核心取值表(顺序即下拉顺序, 首项为默认值)
const QStringList kCoreList = {Constants::DirEntry::CoreUcbp, Constants::DirEntry::CoreUses,
Constants::DirEntry::CoreUarg, Constants::DirEntry::CoreUsms};
int coreIndexOf(const QString &core)
{
const int index = kCoreList.indexOf(core);
return index >= 0 ? index : 0; // 未知核心回退默认首项(ucbp)
}
} // namespace
BatchConvertPage::BatchConvertPage(QWidget *parent)
: ConvertWorkbenchBase(parent)
{
// 两阶段初始化: 构造体内调用, 确保界面钩子(loadConfig/applyPageLayout 等)派发到本类
initialize();
}
// ---------------------------------------------------------------------------
// 界面骨架
// ---------------------------------------------------------------------------
void BatchConvertPage::applyPageLayout(QVBoxLayout *layout)
{
// 页首说明条: 无"操作"列, 双击函数名所在行即可修改函数名
m_tipLabel = new QLabel(
"<b>说明:</b>双击函数名所在行,可在弹窗中修改函数名。");
m_tipLabel->setTextFormat(Qt::RichText);
m_tipLabel->setWordWrap(true);
layout->addWidget(m_tipLabel);
}
void BatchConvertPage::applyTipStyle(const QString &textColor)
{
if (m_tipLabel) {
m_tipLabel->setStyleSheet(QString("color: %1; font-size: 12px;").arg(textColor));
}
}
int BatchConvertPage::tableColumnCount() const
{
return 4;
}
QStringList BatchConvertPage::tableHeaderLabels() const
{
return {QStringLiteral("UF20函数名"), QStringLiteral("是否存在"),
QStringLiteral("核心"), QStringLiteral("是否柜面接口")};
}
void BatchConvertPage::applyColumnLayout()
{
// 4 列按 8:2:2:2 比例分摊、恰好铺满表格:
// "核心"/"是否柜面接口"各占 2 份, 保证四/六字表头与行内下拉完整显示不截断;
// 取整余数并入函数名列, 确保总宽精确铺满、无横向滚动条
const int totalWidth = m_funcTable->viewport()->width();
const int unit = totalWidth / 14;
m_funcTable->setColumnWidth(ColName, totalWidth - unit * 6);
m_funcTable->setColumnWidth(ColExists, unit * 2);
m_funcTable->setColumnWidth(ColCore, unit * 2);
m_funcTable->setColumnWidth(ColViaUmgr, unit * 2);
}
// ---------------------------------------------------------------------------
// 名称校验
// ---------------------------------------------------------------------------
bool BatchConvertPage::validateFunctionNames(const QStringList &names)
{
return validateLsFunctionNames(this, names);
}
// ---------------------------------------------------------------------------
// 表格行: 核心/柜面接口两列
// ---------------------------------------------------------------------------
void BatchConvertPage::attachRowItems(QList<QStandardItem *> &rowItems, const QString &funcName)
{
Q_UNUSED(funcName)
// "核心"/"是否柜面接口"以行内下拉呈现, 占位列仅承载 setIndexWidget 控件
for (int i = 0; i < 2; ++i) {
QStandardItem *item = new QStandardItem("");
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
item->setTextAlignment(Qt::AlignCenter);
rowItems.append(item);
}
}
void BatchConvertPage::attachRowWidgets(int row, const QString &funcName)
{
// "核心"下拉: ucbp/uses/uarg/usms, 切换即写回并持久化(dialect 由 core 派生, 保存时写入)
ElaComboBox *coreCombo = new ElaComboBox;
for (const QString &core : kCoreList) {
coreCombo->addItem(core);
}
const QString curCore = m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp);
coreCombo->setCurrentIndex(coreIndexOf(curCore));
connect(coreCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this, funcName, coreCombo](int index) {
const QString core = kCoreList.value(index, Constants::DirEntry::CoreUcbp);
const QString prev = m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp);
if (prev == core) {
return;
}
m_coreMap.insert(funcName, core);
// 值变化才落盘, 不重建表格; 失败回滚内存与下拉选中项
// (否则界面显示新值而磁盘仍是旧值, 重启后配置与界面矛盾)
if (!saveConfig(m_funcList)) {
m_coreMap.insert(funcName, prev);
coreCombo->blockSignals(true);
coreCombo->setCurrentIndex(coreIndexOf(prev));
coreCombo->blockSignals(false);
}
});
m_funcTable->setIndexWidget(m_tableModel->index(row, ColCore), coreCombo);
// "是否柜面接口"下拉: 否/是(与 viaUmgr 布尔一致), 交互与"核心"列一致
ElaComboBox *umgrCombo = new ElaComboBox;
umgrCombo->addItem(QStringLiteral("否"));
umgrCombo->addItem(QStringLiteral("是"));
umgrCombo->setCurrentIndex(m_viaUmgrMap.value(funcName, false) ? 1 : 0);
connect(umgrCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this, funcName, umgrCombo](int index) {
const bool viaUmgr = (index == 1);
const bool prev = m_viaUmgrMap.value(funcName, false);
if (prev == viaUmgr) {
return;
}
m_viaUmgrMap.insert(funcName, viaUmgr);
if (!saveConfig(m_funcList)) {
m_viaUmgrMap.insert(funcName, prev);
umgrCombo->blockSignals(true);
umgrCombo->setCurrentIndex(prev ? 1 : 0);
umgrCombo->blockSignals(false);
}
});
m_funcTable->setIndexWidget(m_tableModel->index(row, ColViaUmgr), umgrCombo);
}
// ---------------------------------------------------------------------------
// 附加属性映射维护
// ---------------------------------------------------------------------------
void BatchConvertPage::onFunctionAdded(const QString &name)
{
m_coreMap.insert(name, Constants::DirEntry::CoreUcbp); // 新增功能默认 ucbp
m_viaUmgrMap.insert(name, false); // 新增功能默认非柜面接口
}
void BatchConvertPage::onFunctionsRemoved(const QStringList &names)
{
for (const QString &name : names) {
m_coreMap.remove(name);
m_viaUmgrMap.remove(name);
}
}
void BatchConvertPage::onFunctionsRenamed(const QString &oldName, const QString &newName)
{
const QString core = m_coreMap.value(oldName, Constants::DirEntry::CoreUcbp);
const bool viaUmgr = m_viaUmgrMap.value(oldName, false);
m_coreMap.remove(oldName);
m_viaUmgrMap.remove(oldName);
m_coreMap.insert(newName, core);
m_viaUmgrMap.insert(newName, viaUmgr);
}
void BatchConvertPage::onFunctionListCleared()
{
m_coreMap.clear();
m_viaUmgrMap.clear();
}
// ---------------------------------------------------------------------------
// 持久化: cust.json
// ---------------------------------------------------------------------------
bool BatchConvertPage::saveConfig(const QStringList &funcList)
{
// 1. 读原文件并解析
QFile file(configFilePath());
QByteArray original;
if (!file.open(QIODevice::ReadOnly)) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,无法打开原文件: %1").arg(configFilePath()));
return false;
}
original = file.readAll();
file.close();
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(original, &parseError);
if (parseError.error != QJsonParseError::NoError) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,原文件 JSON 解析失败: %1")
.arg(parseError.errorString()));
return false;
}
// 2. 定位 convert.dir 的 [ ] 区间
QString content = QString::fromUtf8(original);
QRegularExpression dirRe(
QStringLiteral("\"%1\"\\s*:\\s*\\[").arg(Constants::CustKeys::ConvertDirSub));
QRegularExpressionMatch dirMatch = dirRe.match(content);
if (!dirMatch.hasMatch()) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,未找到 convert.%1 节点").arg(
Constants::CustKeys::ConvertDirSub));
return false;
}
const int bracketStart = dirMatch.capturedEnd() - 1; // '[' 位置
int bracketEnd = -1;
int depth = 0;
bool inString = false;
bool escaped = false;
for (int i = bracketStart; i < content.size(); ++i) {
const QChar c = content[i];
if (inString) {
if (escaped) {
escaped = false;
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
inString = false;
}
continue;
}
if (c == '"') {
inString = true;
} else if (c == '[') {
++depth;
} else if (c == ']') {
--depth;
if (depth == 0) {
bracketEnd = i;
break;
}
}
}
if (bracketEnd < 0) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,convert.%1 数组括号不匹配").arg(
Constants::CustKeys::ConvertDirSub));
return false;
}
// 3. 构建新数组(V2 对象格式, dialect 由 core 派生)
// 紧凑格式: 每个功能一行对象(与需求示例一致), 避免逐字段展开占用大量缩进
QString newArray = "[";
for (int i = 0; i < funcList.size(); ++i) {
const QString &name = funcList[i];
const QString core = m_coreMap.value(name, Constants::DirEntry::CoreUcbp);
const bool viaUmgr = m_viaUmgrMap.value(name, false);
const QString dialect = Constants::DirEntry::dialectForCore(core);
if (i > 0) {
newArray += ",";
}
newArray += "\n { \"" + Constants::DirEntry::Name + "\": \""
+ CustJsonIo::escapeJsonString(name + Constants::DesignExts::Service)
+ "\", \"" + Constants::DirEntry::Core + "\": \"" + core
+ "\", \"" + Constants::DirEntry::ViaUmgr + "\": ";
newArray += viaUmgr ? "true" : "false";
newArray += ", \"" + Constants::DirEntry::Dialect + "\": \"" + dialect + "\" }";
}
if (!funcList.isEmpty()) {
newArray += "\n ";
}
newArray += "]";
// 换行风格跟随原文件: 原文件为 CRLF 时, 新数组文本的 \n 同步为 \r\n。
// 写出必须用二进制模式(不开 QIODevice::Text)——Text 模式会把 \n 再转成 \r\n,
// 而保留区域(fromUtf8 原样保留)已含 \r\n, 每保存一次就多累积一个 \r,
// 最终文件里出现成串回车符(表现为编辑器中大量空白)。
if (original.contains("\r\n")) {
newArray.replace("\n", "\r\n");
}
// 4. 拼装并做整体 JSON 合法性校验
QString newContent = content.left(bracketStart) + newArray + content.mid(bracketEnd + 1);
QJsonDocument newDoc = QJsonDocument::fromJson(newContent.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,新文件 JSON 校验未通过: %1")
.arg(parseError.errorString()));
return false;
}
QJsonObject newRoot = newDoc.object();
const QJsonArray verifyDir =
newRoot[Constants::CustKeys::ConvertDir].toObject()[Constants::CustKeys::ConvertDirSub].toArray();
if (verifyDir.size() != funcList.size()) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,新文件数组元素数(%1)与清单(%2)不一致")
.arg(verifyDir.size())
.arg(funcList.size()));
return false;
}
// 5. 原子写回(二进制模式: 内容字节原样落盘, 不做任何换行转换)
// 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的
// fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载
// (重载会 refreshTable 强制 scrollToTop, 表现为下拉选择后滚动条回顶);
// 失败路径(打开/提交失败)由组件 2s 兜底自动关窗
m_configMonitor->beginSaveWindow();
QSaveFile saveFile(configFilePath());
if (!saveFile.open(QIODevice::WriteOnly)) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,无法写入: %1").arg(configFilePath()));
return false;
}
saveFile.write(newContent.toUtf8());
if (!saveFile.commit()) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,写入提交失败: %1").arg(configFilePath()));
return false;
}
// 程序自身写入后更新监控 mtime 基准, 避免触发无谓的重载
m_configMonitor->endSave();
return true;
}
void BatchConvertPage::loadConfig()
{
QString jsonPath = Constants::custJson();
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 QJsonArray dir = root[Constants::CustKeys::ConvertDir].toObject()[Constants::CustKeys::ConvertDirSub].toArray();
QStringList items;
m_coreMap.clear();
m_viaUmgrMap.clear();
for (const QJsonValue &val : dir) {
// V1 字符串元素 / V2 对象元素双格式兼容: 缺失字段按契约默认值兜底
QString item;
QString core = Constants::DirEntry::CoreUcbp;
bool viaUmgr = false;
if (val.isObject()) {
const QJsonObject entry = val.toObject();
item = entry.value(Constants::DirEntry::Name).toString();
const QString c = entry.value(Constants::DirEntry::Core).toString();
if (kCoreList.contains(c)) {
core = c;
}
viaUmgr = entry.value(Constants::DirEntry::ViaUmgr).toBool(false);
} else {
item = val.toString();
}
if (item.endsWith(Constants::DesignExts::Service)) {
item.chop(Constants::DesignExts::Service.size());
}
if (item.isEmpty()) {
continue;
}
items.append(item);
m_coreMap.insert(item, core);
m_viaUmgrMap.insert(item, viaUmgr);
}
m_funcList = items;
refreshTable();
}
QString BatchConvertPage::configFilePath() const
{
return Constants::custJson();
}
QString BatchConvertPage::workbenchTitle() const
{
return QStringLiteral("全量转码");
}
QString BatchConvertPage::configDisplayName() const
{
return QStringLiteral("cust.json");
}
QString BatchConvertPage::addDialogTitle() const
{
return QStringLiteral("添加功能");
}
QString BatchConvertPage::addDialogPlaceholder() const
{
return QStringLiteral("输入 LS_ 开头的功能名,如 LS_证券参数_股票名单增加");
}