Files
change_code/src/pages/datamapping/mappingtablepage.cpp
T

336 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "mappingtablepage.h"
#include "utils/Constants.h"
#include "src/utils/logmanager.h"
#include "src/utils/custjsonio.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QStandardItem>
#include <QLabel>
#include <algorithm>
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaTableView.h"
#include "ElaTheme.h"
// 数据映射三页(表替换/函数替换/不转码功能函数)的公共基类:
// 收编 UI 骨架/删除/编辑置脏/保存保序写/外部修改重载/主题联动等同构样板;
// 数据结构与校验差异由子类通过 populateModel/collectMappingData/onAddRow 实现
MappingTablePage::MappingTablePage(int columnCount,
const QStringList &headers,
const QString &searchPlaceholder,
const QString &tipText,
QWidget *parent)
: QWidget(parent)
, m_isLoading(false)
, m_dirty(false)
{
initUI(columnCount, headers, searchPlaceholder, tipText);
}
MappingTablePage::~MappingTablePage()
{
}
void MappingTablePage::initUI(int columnCount, const QStringList &headers,
const QString &searchPlaceholder, const QString &tipText)
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 20, 20, 20);
mainLayout->setSpacing(10);
// 搜索框
QHBoxLayout *searchLayout = new QHBoxLayout;
m_searchEdit = new ElaLineEdit(this);
m_searchEdit->setPlaceholderText(searchPlaceholder);
m_searchEdit->setClearButtonEnabled(true);
connect(m_searchEdit, &ElaLineEdit::textChanged,
this, &MappingTablePage::onSearchTextChanged);
searchLayout->addWidget(m_searchEdit);
mainLayout->addLayout(searchLayout);
m_model = new QStandardItemModel(this);
m_model->setColumnCount(columnCount);
m_model->setHorizontalHeaderLabels(headers);
connect(m_model, &QStandardItemModel::itemChanged,
this, &MappingTablePage::onModelItemChanged);
m_proxyModel = new MappingFilterProxyModel(columnCount, this);
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
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);
for (int c = 0; c < columnCount; ++c)
m_tableView->horizontalHeader()->setSectionResizeMode(c, 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);
// 虚函数经 this 指针调用, 运行时正确分派到子类实现
connect(addBtn, &ElaPushButton::clicked, this, &MappingTablePage::onAddRow);
connect(delBtn, &ElaPushButton::clicked, this, &MappingTablePage::onDeleteRow);
connect(saveBtn, &ElaPushButton::clicked, this, &MappingTablePage::onSave);
btnLayout->addWidget(addBtn);
btnLayout->addWidget(delBtn);
btnLayout->addStretch();
btnLayout->addWidget(saveBtn);
mainLayout->addLayout(btnLayout);
QHBoxLayout *tipLayout = new QHBoxLayout;
m_tipLabel = new QLabel(tipText);
tipLayout->addWidget(m_tipLabel);
tipLayout->addStretch();
mainLayout->addLayout(tipLayout);
// 外部修改监控: 双监控+防抖+mtime 回环过滤+保存窗口统一由 JsonFileMonitor 处理
m_monitor = new JsonFileMonitor(this);
connect(m_monitor, &JsonFileMonitor::reloadRequested,
this, &MappingTablePage::onReloadRequested);
// tip 标签为主题感知: 跟随 Ela 主题切换, 并按当前主题初始化
connect(ElaTheme::getInstance(), &ElaTheme::themeModeChanged,
this, &MappingTablePage::updateTipStyle);
updateTipStyle();
}
// 由子类构造体内调用(规避构造期虚函数绑定到基类版本): 先读后清空,
// 读取/解析失败时保留现有模型内容并不清脏, 避免用户随手保存把空表写回磁盘
void MappingTablePage::load()
{
m_isLoading = true;
bool readOk = false;
QJsonObject custJson = readCustJson(&readOk).object();
if (readOk) {
// 批量重建期间抑制逐行重绘: 映射表可达数百~上千行, 三个子页共用此路径,
// 基类统一加抑制子页同时受益(populateModel 为虚函数)
m_tableView->setUpdatesEnabled(false);
m_model->removeRows(0, m_model->rowCount());
}
if (readOk) {
populateModel(custJson);
m_tableView->setUpdatesEnabled(true);
}
m_isLoading = false;
// 仅读取/解析成功时清脏: 失败时模型保留了旧内容(可能含未保存编辑),
// 清脏会让外部变更保护失效, 且"保存"会把旧表写回损坏的文件
if (readOk) {
m_dirty = false;
}
// 设置监控并记录 mtime 基准, 供防抖回调过滤自身写入回环
m_monitor->watch(getCustJsonPath());
}
QString MappingTablePage::getCustJsonPath() const
{
return Constants::custJson();
}
QJsonDocument MappingTablePage::readCustJson(bool *ok)
{
if (ok) *ok = false;
const QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
LogManager::instance()->logError("无法读取 cust.json: " + jsonPath);
return QJsonDocument();
}
const QByteArray data = file.readAll();
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
LogManager::instance()->logError(
QString("cust.json 解析失败: %1 (行 %2)").arg(error.errorString()).arg(error.offset));
return QJsonDocument();
}
if (ok) *ok = true;
return doc;
}
void MappingTablePage::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());
}
// 单选但键名 item 缺失(空单元格/排序中列被隐藏)时 keyNames 可能为空,
// 退化为按数量确认, 避免 QList::first() 对空列表断言崩溃
const QString confirmMsg = (selectedRows.size() == 1 && !keyNames.isEmpty())
? deleteConfirmSingle(keyNames.first())
: deleteConfirmMulti(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);
}
m_dirty = true; // 删除行同样视为未保存修改,纳入外部文件变更保护
}
void MappingTablePage::onModelItemChanged(QStandardItem *item)
{
if (m_isLoading) return;
m_dirty = true; // 用户编辑了映射表,标记有未保存修改
Q_UNUSED(item);
}
void MappingTablePage::onSave()
{
QString errorMessage;
const SaveResult result = saveToDisk(&errorMessage);
if (result == SaveResult::Ok) {
QMessageBox::information(this, "提示", "保存成功!");
} else if (result == SaveResult::ValidationFailed) {
QMessageBox::warning(this, "提示", errorMessage);
} else {
QMessageBox::critical(this, "错误", errorMessage);
}
}
MappingTablePage::SaveResult MappingTablePage::saveToDisk(QString *errorMessage)
{
if (errorMessage) errorMessage->clear();
// 开启保存窗口: 窗口期内监控事件直接忽略
m_monitor->beginSaveWindow();
// 读取原始文件内容以保留顶层 key 顺序
const QString jsonPath = getCustJsonPath();
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
m_monitor->endSave(); // 早退必须关闭窗口,否则监控被永久抑制
if (errorMessage) *errorMessage = "无法读取 cust.json!";
return SaveResult::Error;
}
const QString rawJson = QString::fromUtf8(file.readAll());
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
m_monitor->endSave();
if (errorMessage) *errorMessage = "cust.json 解析失败: " + error.errorString();
return SaveResult::Error;
}
QJsonObject custJson = doc.object();
// 提取原始 key 顺序
const QStringList keyOrder = CustJsonIo::extractTopLevelKeyOrder(rawJson);
// 子类校验并回填数据: 校验失败不落盘
QString validateError;
const QHash<QString, QString> overrideRaw = collectMappingData(custJson, validateError);
if (!validateError.isEmpty()) {
m_monitor->endSave();
if (errorMessage) *errorMessage = validateError;
return SaveResult::ValidationFailed;
}
// endSave 以保存后的新 mtime 为基准(writeCustJsonOrdered 同步写完才返回),
// 成功/失败都要关闭窗口, 且必须先于模态弹窗:
// 弹窗阻塞期间若发生外部修改, 迟到的 endSave 会把外部修改的 mtime
// 当作自身保存基准, 导致外部变更被静默吞掉
const bool writeOk = CustJsonIo::writeCustJsonOrdered(jsonPath, custJson, keyOrder, overrideRaw);
m_monitor->endSave();
if (writeOk) {
m_dirty = false; // 保存成功后与磁盘内容一致
LogManager::instance()->logInfo(saveLogText(m_model->rowCount()));
onSavedToDisk(); // 落盘成功钩子(表替换页覆写: 同步 table_mapping.json)
return SaveResult::Ok;
}
if (errorMessage) *errorMessage = "保存到 cust.json 失败!";
return SaveResult::Error;
}
void MappingTablePage::onSavedToDisk()
{
}
void MappingTablePage::onReloadRequested()
{
// 有未保存编辑时询问,避免外部修改静默覆盖用户的编辑
if (m_dirty) {
QMessageBox::StandardButton reply = QMessageBox::question(this, "文件已变更",
"cust.json 已被其他页面或外部程序修改,重新加载将丢失当前未保存的编辑。是否重新加载?",
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) {
return; // 监控组件已记住本次版本,避免同一份内容反复弹窗询问
}
}
LogManager::instance()->logInfo(reloadLogText());
load();
}
void MappingTablePage::onSearchTextChanged(const QString &text)
{
// setFilterFixedString 自动转义正则元字符, 按不区分大小写的包含匹配
m_proxyModel->setFilterFixedString(text);
}
void MappingTablePage::updateTipStyle()
{
if (!m_tipLabel) return;
const ElaThemeType::ThemeMode mode = ElaTheme::getInstance()->getThemeMode();
const QString textColor = ElaThemeColor(mode, BasicText).name();
m_tipLabel->setStyleSheet(QString("color: %1; font-size: 13px;").arg(textColor));
}
// ---------------------------------------------------------------------------
// 默认文案虚点实现(子类按需覆写)
// ---------------------------------------------------------------------------
QString MappingTablePage::saveLogText(int rowCount) const
{
Q_UNUSED(rowCount);
return "配置已保存到 cust.json";
}
QString MappingTablePage::reloadLogText() const
{
return "检测到 cust.json 外部修改,自动刷新数据";
}
QString MappingTablePage::deleteConfirmSingle(const QString &name) const
{
return QString("确定要删除 \"%1\" 这条映射吗?").arg(name);
}
QString MappingTablePage::deleteConfirmMulti(int count) const
{
return QString("确定要删除选中的 %1 条映射吗?").arg(count);
}