优化若干性能问题

This commit is contained in:
taocong
2026-09-22 15:15:22 +08:00
committed by taocong
parent afb8161648
commit 43e184bffb
118 changed files with 5518 additions and 6372 deletions
+315
View File
@@ -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);
}