优化若干性能问题
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
#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()
|
||||
{
|
||||
// 开启保存窗口: 窗口期内监控事件直接忽略
|
||||
m_monitor->beginSaveWindow();
|
||||
|
||||
// 读取原始文件内容以保留顶层 key 顺序
|
||||
const QString jsonPath = getCustJsonPath();
|
||||
QFile file(jsonPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
m_monitor->endSave(); // 早退必须关闭窗口,否则监控被永久抑制
|
||||
QMessageBox::critical(this, "错误", "无法读取 cust.json!");
|
||||
return;
|
||||
}
|
||||
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();
|
||||
QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString());
|
||||
return;
|
||||
}
|
||||
QJsonObject custJson = doc.object();
|
||||
|
||||
// 提取原始 key 顺序
|
||||
const QStringList keyOrder = CustJsonIo::extractTopLevelKeyOrder(rawJson);
|
||||
|
||||
// 子类校验并回填数据: 校验失败置 errorMessage, 弹警告不落盘
|
||||
QString errorMessage;
|
||||
const QHash<QString, QString> overrideRaw = collectMappingData(custJson, errorMessage);
|
||||
if (!errorMessage.isEmpty()) {
|
||||
m_monitor->endSave();
|
||||
QMessageBox::warning(this, "提示", errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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()));
|
||||
QMessageBox::information(this, "提示", "保存成功!");
|
||||
} else {
|
||||
QMessageBox::critical(this, "错误", "保存到 cust.json 失败!");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user