| @@ -29,21 +29,19 @@ void AlarmService::refresh() | |||
| records_.begin(), records_.end(), | |||
| [&definition](const AlarmRecord &record) | |||
| { | |||
| return record.definitionId == definition.id && record.active; | |||
| return record.definitionId == definition.id; | |||
| }); | |||
| if (*active && current == records_.end()) | |||
| { | |||
| records_.insert( | |||
| records_.begin(), | |||
| AlarmRecord{definition.id, definition.message, true, false, now, {}}); | |||
| AlarmRecord{definition.id, definition.message, false, now}); | |||
| } | |||
| else if (!*active && current != records_.end()) | |||
| { | |||
| current->active = false; | |||
| current->clearedAt = now; | |||
| records_.erase(current); | |||
| } | |||
| } | |||
| trimHistory(); | |||
| } | |||
| bool AlarmService::acknowledge(const std::string &definition_id) | |||
| @@ -52,7 +50,7 @@ bool AlarmService::acknowledge(const std::string &definition_id) | |||
| records_.begin(), records_.end(), | |||
| [&definition_id](const AlarmRecord &candidate) | |||
| { | |||
| return candidate.definitionId == definition_id && candidate.active; | |||
| return candidate.definitionId == definition_id; | |||
| }); | |||
| if (record == records_.end()) | |||
| { | |||
| @@ -95,20 +93,3 @@ std::optional<bool> AlarmService::evaluate( | |||
| } | |||
| return std::nullopt; | |||
| } | |||
| void AlarmService::trimHistory() | |||
| { | |||
| std::size_t history_count = static_cast<std::size_t>(std::count_if( | |||
| records_.cbegin(), records_.cend(), | |||
| [](const AlarmRecord &record) { return !record.active; })); | |||
| for (auto record = records_.end(); | |||
| record != records_.begin() && history_count > kHistoryLimit;) | |||
| { | |||
| --record; | |||
| if (!record->active) | |||
| { | |||
| record = records_.erase(record); | |||
| --history_count; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,7 +1,6 @@ | |||
| #pragma once | |||
| #include <chrono> | |||
| #include <cstddef> | |||
| #include <optional> | |||
| #include <string> | |||
| #include <vector> | |||
| @@ -14,10 +13,8 @@ struct AlarmRecord | |||
| { | |||
| std::string definitionId; | |||
| std::string message; | |||
| bool active = true; | |||
| bool acknowledged = false; | |||
| std::chrono::system_clock::time_point occurredAt; | |||
| std::optional<std::chrono::system_clock::time_point> clearedAt; | |||
| }; | |||
| class AlarmService | |||
| @@ -34,9 +31,6 @@ public: | |||
| private: | |||
| std::optional<bool> evaluate(const AlarmDefinition &definition) const; | |||
| void trimHistory(); | |||
| static constexpr std::size_t kHistoryLimit = 20; | |||
| const ProjectService &project_service_; | |||
| RegisterRepository &repository_; | |||
| @@ -224,7 +224,7 @@ public: | |||
| rect.height() - kAlarmHeaderHeight), | |||
| Qt::AlignCenter, | |||
| runtime_active_ ? QObject::tr("暂无报警") | |||
| : QObject::tr("运行时显示报警记录")); | |||
| : QObject::tr("运行时显示当前报警")); | |||
| break; | |||
| } | |||
| for (int row = 0; row < visible_rows; ++row) | |||
| @@ -237,18 +237,12 @@ public: | |||
| kAlarmRowHeight); | |||
| painter->fillRect( | |||
| row_rect, | |||
| record.active | |||
| ? QColor(QStringLiteral("#fde8e8")) | |||
| : QColor(QStringLiteral("#f0f2f3"))); | |||
| QColor(QStringLiteral("#fde8e8"))); | |||
| painter->setPen(QColor(QStringLiteral("#d3d8dc"))); | |||
| painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight()); | |||
| painter->setPen(record.active | |||
| ? QColor(QStringLiteral("#8d1f1f")) | |||
| : QColor(QStringLiteral("#667078"))); | |||
| const QString state = record.active | |||
| ? (record.acknowledged ? QObject::tr("已确认") | |||
| : QObject::tr("未确认")) | |||
| : QObject::tr("已清除"); | |||
| painter->setPen(QColor(QStringLiteral("#8d1f1f"))); | |||
| const QString state = record.acknowledged | |||
| ? QObject::tr("已确认") : QObject::tr("未确认"); | |||
| painter->drawText( | |||
| QRectF(row_rect.left() + 5, row_rect.top(), 58, row_rect.height()), | |||
| Qt::AlignVCenter | Qt::AlignLeft, | |||
| @@ -341,6 +335,7 @@ public: | |||
| page_hovered_ = false; | |||
| button_pressed_ = false; | |||
| } | |||
| updateAlarmVisibility(); | |||
| update(); | |||
| } | |||
| @@ -356,6 +351,7 @@ public: | |||
| void setAlarmRecords(const std::vector<AlarmRecord> &records) | |||
| { | |||
| alarm_records_ = records; | |||
| updateAlarmVisibility(); | |||
| update(); | |||
| } | |||
| @@ -394,7 +390,7 @@ protected: | |||
| if (row >= 0 && row < static_cast<int>(alarm_records_.size())) | |||
| { | |||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||
| if (record.active && !record.acknowledged && alarm_acknowledge_) | |||
| if (!record.acknowledged && alarm_acknowledge_) | |||
| { | |||
| alarm_acknowledge_(record.definitionId); | |||
| } | |||
| @@ -501,6 +497,17 @@ protected: | |||
| } | |||
| private: | |||
| void updateAlarmVisibility() | |||
| { | |||
| if (control_.type != HmiControlType::AlarmList) | |||
| { | |||
| return; | |||
| } | |||
| const bool has_active_alarm = !alarm_records_.empty(); | |||
| setVisible(editing_enabled_ || !runtime_active_ || has_active_alarm); | |||
| setZValue(runtime_active_ && has_active_alarm ? 1000.0 : 0.0); | |||
| } | |||
| // 控件本体范围保持领域坐标和尺寸语义不变 | |||
| QRectF controlRect() const | |||
| { | |||
| @@ -631,6 +638,7 @@ void HmiEditorWidget::setRuntimeActive(bool active) | |||
| if (control_item != nullptr) | |||
| { | |||
| control_item->setRuntimeValue(false, 0, false); | |||
| control_item->setAlarmRecords({}); | |||
| } | |||
| } | |||
| } | |||
| @@ -34,7 +34,7 @@ void require(bool condition, const std::string &message) | |||
| } | |||
| } | |||
| const AlarmRecord *findActiveRecord( | |||
| const AlarmRecord *findRecord( | |||
| const AlarmService &service, const std::string &definition_id) | |||
| { | |||
| const auto &records = service.records(); | |||
| @@ -42,7 +42,7 @@ const AlarmRecord *findActiveRecord( | |||
| records.cbegin(), records.cend(), | |||
| [&definition_id](const AlarmRecord &candidate) | |||
| { | |||
| return candidate.definitionId == definition_id && candidate.active; | |||
| return candidate.definitionId == definition_id; | |||
| }); | |||
| return record == records.cend() ? nullptr : &*record; | |||
| } | |||
| @@ -92,29 +92,27 @@ void testAlarmDefinitionsAndRuntimeLifecycle() | |||
| repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 80); | |||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 10); | |||
| alarm_service.refresh(); | |||
| require(findActiveRecord(alarm_service, m_result.id) != nullptr | |||
| && findActiveRecord(alarm_service, high_result.id) != nullptr | |||
| && findActiveRecord(alarm_service, low_result.id) != nullptr, | |||
| require(findRecord(alarm_service, m_result.id) != nullptr | |||
| && findRecord(alarm_service, high_result.id) != nullptr | |||
| && findRecord(alarm_service, low_result.id) != nullptr, | |||
| "all three alarm conditions must create active records"); | |||
| require(alarm_service.acknowledge(m_result.id) | |||
| && findActiveRecord(alarm_service, m_result.id)->acknowledged, | |||
| && findRecord(alarm_service, m_result.id)->acknowledged, | |||
| "an active alarm must be locally acknowledged"); | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | |||
| repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 79); | |||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 11); | |||
| alarm_service.refresh(); | |||
| require(std::none_of( | |||
| alarm_service.records().cbegin(), | |||
| alarm_service.records().cend(), | |||
| [](const AlarmRecord &record) { return record.active; }), | |||
| "cleared conditions must move active alarms into history"); | |||
| require(alarm_service.records().empty(), | |||
| "cleared conditions must immediately remove active alarms"); | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||
| alarm_service.refresh(); | |||
| require(findActiveRecord(alarm_service, m_result.id) != nullptr | |||
| && alarm_service.records().size() == 4, | |||
| "a repeated alarm must create a new occurrence and keep history"); | |||
| require(findRecord(alarm_service, m_result.id) != nullptr | |||
| && alarm_service.records().size() == 1 | |||
| && !alarm_service.records().front().acknowledged, | |||
| "a repeated alarm must create a new unacknowledged occurrence"); | |||
| alarm_service.reset(); | |||
| require(alarm_service.records().empty(), | |||
| @@ -428,14 +428,20 @@ void testRuntimeAlarmListInteraction() | |||
| require(alarm_result.succeeded, | |||
| "runtime alarm test must create an M alarm definition"); | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||
| alarm_service.refresh(); | |||
| require(alarm_service.records().size() == 1 | |||
| && alarm_service.records().front().active, | |||
| "an active M alarm must be available to AlarmList"); | |||
| HmiEditorWidget view(editor_service, runtime_service, alarm_service); | |||
| view.setPageId(page_id); | |||
| QGraphicsItem *alarm_item = nullptr; | |||
| for (QGraphicsItem *item : view.scene()->items()) | |||
| { | |||
| if (item->zValue() >= 0.0) | |||
| { | |||
| alarm_item = item; | |||
| break; | |||
| } | |||
| } | |||
| require(alarm_item != nullptr && alarm_item->isVisible(), | |||
| "AlarmList must remain visible while editing"); | |||
| view.setEditingEnabled(false); | |||
| view.setRuntimeActive(true); | |||
| view.setRuntimeWriteEnabled(false); | |||
| @@ -443,6 +449,17 @@ void testRuntimeAlarmListInteraction() | |||
| view.resize(900, 560); | |||
| view.show(); | |||
| QApplication::processEvents(); | |||
| require(!alarm_item->isVisible(), | |||
| "AlarmList must be hidden when runtime has no active alarm"); | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||
| alarm_service.refresh(); | |||
| view.refreshRuntimeValues(); | |||
| QApplication::processEvents(); | |||
| require(alarm_service.records().size() == 1, | |||
| "an active M alarm must be available to AlarmList"); | |||
| require(alarm_item->isVisible() && alarm_item->zValue() > 0.0, | |||
| "AlarmList must appear above the page for an active alarm"); | |||
| const QPoint header_position = view.mapFromScene(QPointF( | |||
| alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | |||
| @@ -461,9 +478,16 @@ void testRuntimeAlarmListInteraction() | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | |||
| alarm_service.refresh(); | |||
| view.refreshRuntimeValues(); | |||
| require(!alarm_service.records().front().active | |||
| && alarm_service.records().front().clearedAt.has_value(), | |||
| "clearing the M condition must retain a cleared history record"); | |||
| QApplication::processEvents(); | |||
| require(alarm_service.records().empty(), | |||
| "clearing the M condition must remove the alarm immediately"); | |||
| require(!alarm_item->isVisible(), | |||
| "AlarmList must hide after the final alarm recovers"); | |||
| view.setRuntimeActive(false); | |||
| view.setEditingEnabled(true); | |||
| require(alarm_item->isVisible(), | |||
| "AlarmList must become visible again after returning to editing"); | |||
| } | |||
| void testModeActionsControlEditingAvailability() | |||