Преглед изворни кода

feat: 支持HMI多页面管理与导航

main
suyu пре 1 месец
родитељ
комит
decb015d5c
6 измењених фајлова са 394 додато и 6 уклоњено
  1. +240
    -3
      app/src/services/hmi_editor_service.cpp
  2. +14
    -3
      app/src/services/hmi_editor_service.h
  3. +45
    -0
      app/src/services/hmi_navigation_service.cpp
  4. +35
    -0
      app/src/services/hmi_navigation_service.h
  5. +58
    -0
      app/tests/hmi_editor_service_tests.cpp
  6. +2
    -0
      app/tests/hmi_editor_service_tests.pro

+ 240
- 3
app/src/services/hmi_editor_service.cpp Прегледај датотеку

@@ -3,6 +3,8 @@
#include "project_service.h"

#include <algorithm>
#include <cctype>
#include <cstddef>
#include <utility>

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 &current = 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 &current = 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 &current = 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 &current = 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<std::ptrdiff_t>(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<int>(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);


+ 14
- 3
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 待检查页面


+ 45
- 0
app/src/services/hmi_navigation_service.cpp Прегледај датотеку

@@ -0,0 +1,45 @@
#include "hmi_navigation_service.h"

#include "project_service.h"

#include <algorithm>

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_;
}

+ 35
- 0
app/src/services/hmi_navigation_service.h Прегледај датотеку

@@ -0,0 +1,35 @@
#pragma once

#include <string>

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 &currentPageId() const;

private:
ProjectService &project_service_;
std::string current_page_id_;
};

+ 58
- 0
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 <exception>
@@ -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)
{


+ 2
- 0
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

Loading…
Откажи
Сачувај