#include "mappingtablepage.h" #include "utils/Constants.h" #include "src/utils/logmanager.h" #include "src/utils/custjsonio.h" #include #include #include #include #include #include #include #include #include #include #include #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 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()); 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 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); }