diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index 7e32b7d..aa0e8aa 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -3,6 +3,8 @@ #include "project_service.h" #include +#include +#include #include namespace { @@ -36,10 +38,17 @@ std::string controlPrefix(HmiControlType type) return "numeric-input"; } case HmiControlType::Label: - default: { return "label"; } + case HmiControlType::PageJump: + { + return "page-jump"; + } + default: + { + return "control"; + } } } @@ -64,10 +73,17 @@ std::string defaultText(HmiControlType type) return "数值输入"; } case HmiControlType::Label: - default: { return "文本"; } + case HmiControlType::PageJump: + { + return "页面跳转"; + } + default: + { + return "控件"; + } } } @@ -76,6 +92,31 @@ HmiEditorResult failure(HmiEditorError error, const std::string &message) return {false, error, message, {}}; } +bool isBlank(const std::string &value) +{ + return value.empty() + || std::all_of( + value.cbegin(), value.cend(), + [](unsigned char character) { return std::isspace(character) != 0; }); +} + +std::string makeUniquePageId(const Project &project) +{ + int suffix = 1; + while (true) + { + const std::string candidate = "page-" + std::to_string(suffix); + const bool found = std::any_of( + project.hmiPages.cbegin(), project.hmiPages.cend(), + [&candidate](const HmiPage &page) { return page.id == candidate; }); + if (!found) + { + return candidate; + } + ++suffix; + } +} + } // namespace HmiEditorService::HmiEditorService(ProjectService &project_service) @@ -128,6 +169,11 @@ HmiEditorResult HmiEditorService::ensureDefaultPage() { if (!project_service_.project().hmiPages.empty()) { + if (project_service_.project().initialHmiPageId.empty()) + { + Project &project = project_service_.editProject(); + project.initialHmiPageId = project.hmiPages.front().id; + } return {true, HmiEditorError::None, {}, firstPageId()}; } @@ -138,9 +184,157 @@ HmiEditorResult HmiEditorService::ensureDefaultPage() Project &project = project_service_.editProject(); project.hmiPages.push_back(std::move(page)); + project.initialHmiPageId = project.hmiPages.back().id; + return {true, HmiEditorError::None, {}, project.hmiPages.back().id}; +} + +HmiEditorResult HmiEditorService::addPage(const std::string &name) +{ + if (isBlank(name)) + { + return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空"); + } + const Project ¤t = project_service_.project(); + const bool duplicate = std::any_of( + current.hmiPages.cbegin(), current.hmiPages.cend(), + [&name](const HmiPage &page) { return page.name == name; }); + if (duplicate) + { + return failure(HmiEditorError::DuplicateName, "HMI 页面名称必须唯一"); + } + + HmiPage page; + page.id = makeUniquePageId(current); + page.name = name; + Project &project = project_service_.editProject(); + project.hmiPages.push_back(std::move(page)); + if (project.initialHmiPageId.empty()) + { + project.initialHmiPageId = project.hmiPages.back().id; + } return {true, HmiEditorError::None, {}, project.hmiPages.back().id}; } +HmiEditorResult HmiEditorService::renamePage( + const std::string &page_id, const std::string &name) +{ + if (isBlank(name)) + { + return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空"); + } + const Project ¤t = project_service_.project(); + const auto existing = std::find_if( + current.hmiPages.cbegin(), current.hmiPages.cend(), + [&page_id](const HmiPage &page) { return page.id == page_id; }); + if (existing == current.hmiPages.cend()) + { + return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); + } + const bool duplicate = std::any_of( + current.hmiPages.cbegin(), current.hmiPages.cend(), + [&page_id, &name](const HmiPage &page) + { + return page.id != page_id && page.name == name; + }); + if (duplicate) + { + return failure(HmiEditorError::DuplicateName, "HMI 页面名称必须唯一"); + } + + Project &project = project_service_.editProject(); + auto page = std::find_if( + project.hmiPages.begin(), project.hmiPages.end(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); + page->name = name; + return {true, HmiEditorError::None, {}, page_id}; +} + +HmiEditorResult HmiEditorService::removePage(const std::string &page_id) +{ + const Project ¤t = project_service_.project(); + const auto page = std::find_if( + current.hmiPages.cbegin(), current.hmiPages.cend(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); + if (page == current.hmiPages.cend()) + { + return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); + } + if (current.hmiPages.size() <= 1) + { + return failure(HmiEditorError::LastPageRequired, "工程至少需要保留一个 HMI 页面"); + } + if (current.initialHmiPageId == page_id) + { + return failure( + HmiEditorError::InitialPageCannotBeRemoved, + "请先设置其他初始页面,再删除当前页面"); + } + for (const HmiPage &source_page : current.hmiPages) + { + const auto reference = std::find_if( + source_page.controls.cbegin(), source_page.controls.cend(), + [&page_id](const HmiControl &control) + { + return control.type == HmiControlType::PageJump + && control.pageJump.has_value() + && control.pageJump->targetPageId == page_id; + }); + if (reference != source_page.controls.cend()) + { + return failure( + HmiEditorError::PageReferenced, + "页面仍被跳转控件 " + reference->id + " 引用"); + } + } + + Project &project = project_service_.editProject(); + project.hmiPages.erase( + std::remove_if( + project.hmiPages.begin(), project.hmiPages.end(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }), + project.hmiPages.end()); + return {true, HmiEditorError::None, {}, page_id}; +} + +HmiEditorResult HmiEditorService::movePage(const std::string &page_id, int offset) +{ + if (offset != -1 && offset != 1) + { + return failure(HmiEditorError::InvalidOperation, "页面每次只能上移或下移一位"); + } + const Project ¤t = project_service_.project(); + const auto page = std::find_if( + current.hmiPages.cbegin(), current.hmiPages.cend(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); + if (page == current.hmiPages.cend()) + { + return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); + } + const auto index = std::distance(current.hmiPages.cbegin(), page); + const std::ptrdiff_t target_index = index + offset; + if (target_index < 0 + || target_index >= static_cast(current.hmiPages.size())) + { + return failure(HmiEditorError::InvalidOperation, "HMI 页面已经位于目标边界"); + } + + Project &project = project_service_.editProject(); + std::iter_swap( + project.hmiPages.begin() + index, + project.hmiPages.begin() + target_index); + return {true, HmiEditorError::None, {}, page_id}; +} + +HmiEditorResult HmiEditorService::setInitialPage(const std::string &page_id) +{ + if (findPage(page_id) == nullptr) + { + return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); + } + project_service_.editProject().initialHmiPageId = page_id; + return {true, HmiEditorError::None, {}, page_id}; +} + HmiEditorResult HmiEditorService::addControl( const std::string &page_id, HmiControlType type) { @@ -280,8 +474,21 @@ HmiEditorResult HmiEditorService::updateControl( } bool HmiEditorService::validateEditableControl( - const HmiPage &page, const HmiControl &control, std::string *error) + const HmiPage &page, const HmiControl &control, std::string *error) const { + switch (control.type) + { + case HmiControlType::Button: + case HmiControlType::Indicator: + case HmiControlType::NumericDisplay: + case HmiControlType::NumericInput: + case HmiControlType::Label: + case HmiControlType::PageJump: + break; + default: + setError(error, "不支持的 HMI 控件类型"); + return false; + } if (control.id.empty()) { setError(error, "HMI 控件 ID 不能为空"); @@ -325,6 +532,32 @@ bool HmiEditorService::validateEditableControl( setError(error, "数值控件必须绑定 D 区地址"); return false; } + if ((control.type == HmiControlType::Label + || control.type == HmiControlType::PageJump) + && control.binding.has_value()) + { + setError(error, "文本和页面跳转控件不能绑定寄存器"); + return false; + } + if (control.type == HmiControlType::PageJump) + { + if (!control.pageJump.has_value()) + { + setError(error, "页面跳转控件缺少跳转配置"); + return false; + } + if (!control.pageJump->targetPageId.empty() + && findPage(control.pageJump->targetPageId) == nullptr) + { + setError(error, "页面跳转控件的目标页面不存在"); + return false; + } + } + else if (control.pageJump.has_value()) + { + setError(error, "非页面跳转控件不能包含跳转配置"); + return false; + } return true; } @@ -351,6 +584,10 @@ HmiControl HmiEditorService::makeControl( control.text = defaultText(type); control.bounds.width = type == HmiControlType::Indicator ? 64 : 120; control.bounds.height = type == HmiControlType::Indicator ? 64 : 40; + if (type == HmiControlType::PageJump) + { + control.pageJump = HmiPageJumpConfig{}; + } const int offset = static_cast(page.controls.size()) * 16; control.bounds.x = std::min(20 + offset, page.width - control.bounds.width); control.bounds.y = std::min(20 + offset, page.height - control.bounds.height); diff --git a/app/src/services/hmi_editor_service.h b/app/src/services/hmi_editor_service.h index 48d6ae5..ba739ec 100644 --- a/app/src/services/hmi_editor_service.h +++ b/app/src/services/hmi_editor_service.h @@ -22,8 +22,13 @@ enum class HmiEditorError PageNotFound, ControlNotFound, DuplicateId, + DuplicateName, InvalidPage, - InvalidControl + InvalidControl, + LastPageRequired, + InitialPageCannotBeRemoved, + PageReferenced, + InvalidOperation }; /** @@ -74,6 +79,12 @@ public: * @return 已有或新建页面的成功结果及其页面标识 */ HmiEditorResult ensureDefaultPage(); + HmiEditorResult addPage(const std::string &name); + HmiEditorResult renamePage( + const std::string &page_id, const std::string &name); + HmiEditorResult removePage(const std::string &page_id); + HmiEditorResult movePage(const std::string &page_id, int offset); + HmiEditorResult setInitialPage(const std::string &page_id); /** * @brief 向指定页面添加待配置的基础控件 * @param page_id 目标页面唯一标识 @@ -125,10 +136,10 @@ private: * @param error 校验失败时写入原因,可为 nullptr * @return 控件标识、尺寸、位置和寄存器绑定区域均有效时返回 true */ - static bool validateEditableControl( + bool validateEditableControl( const HmiPage &page, const HmiControl &control, - std::string *error); + std::string *error) const; /** * @brief 检查候选控件标识是否与页面内其他控件重复 * @param page 待检查页面 diff --git a/app/src/services/hmi_navigation_service.cpp b/app/src/services/hmi_navigation_service.cpp new file mode 100644 index 0000000..f99c5cf --- /dev/null +++ b/app/src/services/hmi_navigation_service.cpp @@ -0,0 +1,45 @@ +#include "hmi_navigation_service.h" + +#include "project_service.h" + +#include + +HmiNavigationService::HmiNavigationService(ProjectService &project_service) + : project_service_(project_service) +{ +} + +HmiNavigationResult HmiNavigationService::start() +{ + const Project &project = project_service_.project(); + if (project.hmiPages.empty()) + { + current_page_id_.clear(); + return {false, HmiNavigationError::NoPages, "工程没有 HMI 页面", {}}; + } + return navigateTo(project.initialHmiPageId); +} + +HmiNavigationResult HmiNavigationService::navigateTo(const std::string &page_id) +{ + const Project &project = project_service_.project(); + const auto page = std::find_if( + project.hmiPages.cbegin(), project.hmiPages.cend(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); + if (page == project.hmiPages.cend()) + { + return {false, HmiNavigationError::PageNotFound, "目标 HMI 页面不存在", {}}; + } + current_page_id_ = page_id; + return {true, HmiNavigationError::None, {}, current_page_id_}; +} + +void HmiNavigationService::stop() +{ + current_page_id_.clear(); +} + +const std::string &HmiNavigationService::currentPageId() const +{ + return current_page_id_; +} diff --git a/app/src/services/hmi_navigation_service.h b/app/src/services/hmi_navigation_service.h new file mode 100644 index 0000000..c7793a8 --- /dev/null +++ b/app/src/services/hmi_navigation_service.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +class ProjectService; + +enum class HmiNavigationError +{ + None, + NoPages, + PageNotFound +}; + +struct HmiNavigationResult +{ + bool succeeded = false; + HmiNavigationError error = HmiNavigationError::None; + std::string message; + std::string pageId; +}; + +class HmiNavigationService +{ +public: + explicit HmiNavigationService(ProjectService &project_service); + + HmiNavigationResult start(); + HmiNavigationResult navigateTo(const std::string &page_id); + void stop(); + const std::string ¤tPageId() const; + +private: + ProjectService &project_service_; + std::string current_page_id_; +}; diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index af84044..2ae9961 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -2,6 +2,7 @@ #include "domain/register_repository.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" +#include "services/hmi_navigation_service.h" #include "services/project_service.h" #include @@ -148,6 +149,62 @@ void testRuntimeUsesRegisterRepository() "numeric display must read D values through the repository"); } +void testPageLifecycleAndNavigation() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService service(project_service); + const std::string main_id = service.ensureDefaultPage().id; + require(project_service.project().initialHmiPageId == main_id, + "the first page must become the initial HMI page"); + + const HmiEditorResult settings = service.addPage("Settings"); + const HmiEditorResult maintenance = service.addPage("Maintenance"); + require(settings.succeeded && maintenance.succeeded, + "multiple HMI pages must be creatable"); + require(service.renamePage(settings.id, "Parameters").succeeded, + "an HMI page must be renamable by stable id"); + require(service.renamePage(maintenance.id, "Parameters").error + == HmiEditorError::DuplicateName, + "HMI page names must remain unique"); + require(service.movePage(maintenance.id, -1).succeeded + && project_service.project().hmiPages.at(1).id == maintenance.id, + "HMI page order must be editable independently from page ids"); + + const HmiEditorResult label = service.addControl(main_id, HmiControlType::Label); + require(label.succeeded + && service.findControl(main_id, label.id)->isConfigured(), + "the editor service must expose a configured static Label control"); + const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump); + require(jump.succeeded, "the editor service must expose PageJump creation"); + HmiControl jump_control = *service.findControl(main_id, jump.id); + jump_control.pageJump = HmiPageJumpConfig{maintenance.id}; + require(service.updateControl(main_id, jump.id, jump_control).succeeded, + "PageJump target updates must resolve stable page ids"); + require(service.removePage(maintenance.id).error == HmiEditorError::PageReferenced, + "a page referenced by PageJump must not be deletable"); + + require(service.setInitialPage(settings.id).succeeded, + "the user must be able to select a different initial page"); + require(service.removePage(settings.id).error + == HmiEditorError::InitialPageCannotBeRemoved, + "the initial page must not be deleted implicitly"); + + HmiNavigationService navigation(project_service); + const HmiNavigationResult start = navigation.start(); + require(start.succeeded && start.pageId == settings.id, + "runtime navigation must start from the persisted initial page"); + require(navigation.navigateTo(maintenance.id).succeeded + && navigation.currentPageId() == maintenance.id, + "runtime navigation must switch to an existing page"); + require(navigation.navigateTo("missing").error + == HmiNavigationError::PageNotFound, + "runtime navigation must reject an unknown page id"); + navigation.stop(); + require(navigation.currentPageId().empty(), + "stopping runtime navigation must clear session state"); +} + } // namespace int main() @@ -157,6 +214,7 @@ int main() // 编辑和运行场景分别验证服务层两条独立职责 testControlEditing(); testRuntimeUsesRegisterRepository(); + testPageLifecycleAndNavigation(); } catch (const std::exception &error) { diff --git a/app/tests/hmi_editor_service_tests.pro b/app/tests/hmi_editor_service_tests.pro index 814ae7a..59c928a 100644 --- a/app/tests/hmi_editor_service_tests.pro +++ b/app/tests/hmi_editor_service_tests.pro @@ -15,6 +15,7 @@ SOURCES += \ ../src/domain/project_model.cpp \ ../src/services/project_service.cpp \ ../src/services/hmi_editor_service.cpp \ + ../src/services/hmi_navigation_service.cpp \ ../src/services/hmi_runtime_service.cpp HEADERS += \ @@ -26,4 +27,5 @@ HEADERS += \ ../src/domain/project_storage.h \ ../src/services/project_service.h \ ../src/services/hmi_editor_service.h \ + ../src/services/hmi_navigation_service.h \ ../src/services/hmi_runtime_service.h