Files
Adminive/README.md
T
2026-08-06 20:33:28 +08:00

11 KiB
Raw Blame History

Adminive Backend-Driven Complete Example

Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、字段权限、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 /admin/amis 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。

适配层

核心目标只提供描述器、转换流程和 amis Schema 生成:

target_link_libraries(your_target PRIVATE Adminive::Core)

HTTP 资源层单独链接:

target_link_libraries(your_target PRIVATE Adminive::Http)

项目自带的 nlohmann::jsonmagic_enum 桥接不会被核心头自动包含。需要默认实现时显式包含:

#include "adminive/adapters/nlohmann_json.hpp"
#include "adminive/adapters/magic_enum.hpp"
using Json = nlohmann::json;

JSON 适配

外部 JSON 类型通过 Json_Adapter<Json> 接入。适配器负责对象、数组、标量、字段访问、解析和输出。所有转换入口显式携带 JSON 类型:

auto descriptor = adminive::to_descriptor_json<Json, Config>();
auto encoded = adminive::to_json<Json>(config);
auto result = adminive::apply_frontend_patch<Json>(config, input);

外部值类型适配

外部包装类型不需要继承 Adminive 类型。可以针对全部 JSON 实现提供通用适配,也可以只针对某个 JSON 类型特化:

template <class Json>
struct adminive::Value_Adapter<Copyable_Atomic_Int, Json> {
    using value_type = int;
    static int read(const Copyable_Atomic_Int& value) noexcept {
        return value.load();
    }
    static void write(Copyable_Atomic_Int& target, int value) noexcept {
        target.store(value);
    }
};

需要完全控制某种 JSON 的编码时,适配器可以提供 encode()decode()type_nameappend_schema()

枚举适配

核心仅调用 Enum_Adapter<Enum>adminive/adapters/magic_enum.hpp 是可选桥接,也可以为单个枚举自行实现:

template <>
struct adminive::Enum_Adapter<My_Mode> {
    static std::vector<My_Mode> values();
    static std::string_view name(My_Mode value);
    static std::optional<My_Mode> cast(std::string_view value);
};

PFR 或其他结构体反射适配

核心通过 Reflection_Adapter<T> 接收字段数量、字段名称和按索引访问。外部可以在实现层用 Boost.PFR 完成桥接,Adminive 本身不包含 Boost.PFR。描述器使用 reflected_object()reflected_object_with() 生成字段。

template <>
struct adminive::Reflection_Adapter<Config> {
    static constexpr std::size_t field_count = boost::pfr::tuple_size_v<Config>;
    template <std::size_t Index>
    static decltype(auto) get(Config& value) {
        return boost::pfr::get<Index>(value);
    }
    template <std::size_t Index>
    static decltype(auto) get(const Config& value) {
        return boost::pfr::get<Index>(value);
    }
    template <std::size_t Index>
    static constexpr std::string_view name() {
        return boost::pfr::get_name<Index, Config>();
    }
};

公开且无歧义的基类成员也可以直接注册到派生类型描述器,字段所有者不再要求与最终对象类型完全相同。

目录结构

Adminive/
├── backend/
│   ├── include/adminive/
│   ├── src/
│   │   ├── example.hpp
│   │   └── main.cpp
│   ├── tests/
│   └── third_party/
├── cmake/
├── frontend/
├── scripts/
├── CMakeLists.txt
└── README.md

example.hpp 位于 backend/src,只作为完整示例实现,不属于公共库头文件。

列字段名称、显示名称和排序

ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
    .creatable()
    .editable()
    .required()
    .list_label("Mode")
    .order(5)
    .sortable();
  • name 来自成员名称,用于 JSON 字段和接口参数。
  • label 用于表单标签。
  • list_label 用于列表表头,未设置时回退到 label
  • order 控制列表列顺序,未设置时按字段声明顺序生成默认值。
  • sortable 控制该列能否排序,后端只接受已声明为可排序的字段。

枚举列表项

enum class Radio_Mode {
    receive,
    transmit,
    duplex,
    maintenance
};

业务结构直接使用枚举:

struct Radio_State {
    Radio_Mode mode{Radio_Mode::receive};
};

描述中不需要手写选项:

ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
    .creatable()
    .editable()
    .required()
    .list_label("Mode")
    .order(5)
    .sortable();

示例通过可选的 magic_enum 桥接生成;核心只依赖 Enum_Adapter

{
  "name": "mode",
  "value_type": "enum",
  "options": [
    {"label": "Receive", "value": "receive"},
    {"label": "Transmit", "value": "transmit"},
    {"label": "Duplex", "value": "duplex"},
    {"label": "Maintenance", "value": "maintenance"}
  ]
}

表格列使用 amis mapping,接口数据仍然返回稳定的枚举字符串,例如 "duplex"

树状配置

树结构由嵌套的已描述对象表达,不额外维护一份前端树配置:

struct Endpoint_Config {
    bool enabled{true};
    std::string host{"127.0.0.1"};
    Range_Value<int, 1, 65535> port{9000};
};
struct Appearance_Config {
    std::string panel_title{"Radio Control"};
    std::string effective_date{"2026-08-06"};
    std::string accent_color{"#2563eb"};
};
struct Radio_Service_Config {
    std::string profile_name{"Primary Radio Profile"};
    Radio_Mode mode{Radio_Mode::receive};
    Range_Value<int, 1, 64> worker_count{4};
    double receive_gain{1.25};
    Endpoint_Config primary_endpoint{};
    Endpoint_Config backup_endpoint{};
    Appearance_Config appearance{};
};

界面结构为:

Radio Service Configuration
├── Profile Name
├── Operating Mode
├── Worker Count
├── Receive Gain
├── Primary Endpoint
│   ├── Enable Endpoint
│   ├── Host Address
│   └── Port
├── Backup Endpoint
│   ├── Enable Endpoint
│   ├── Host Address
│   └── Port
└── Appearance
    ├── Panel Title
    ├── Effective Date
    └── Accent Color

嵌套对象在 amis 表单中渲染成可折叠 fieldset,子字段使用点路径,例如:

primary_endpoint.host
appearance.effective_date

提交时仍然形成嵌套 JSON 对象。

配置联动

字段描述通过 visible_on 保存 amis 表达式:

ADMINIVE_FIELD_LABEL(T, backup_endpoint, "Backup Endpoint")
    .editable()
    .visible_on("${mode == 'duplex'}");

mode 不是 duplex 时,整个 Backup Endpoint 子树隐藏。

第二个配置示例使用组合条件:

ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook Endpoint")
    .editable()
    .visible_on("${enabled && channel == 'webhook'}");

只有启用告警且投递通道为 webhook 时,才显示 webhook 子配置。

完整输入控件

后端根据 C++ 类型自动选择基础控件:

std::string        -> input-text
整数类型           -> input-number
浮点类型           -> input-number
bool               -> switch
enum class         -> select

日期和颜色通过后端字段描述指定:

ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date")
    .editable()
    .required()
    .widget("input-date");
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color")
    .editable()
    .required()
    .widget("input-color");

日期控件自动补充:

{
  "type": "input-date",
  "valueFormat": "YYYY-MM-DD",
  "displayFormat": "YYYY-MM-DD",
  "clearable": true
}

一次性确认修改

配置表单不会在单个输入项改变时调用后端。所有修改先保留在表单数据域,点击 Confirm Changes 后一次性提交完整配置:

{
  "actions": [
    {"type": "reset", "label": "Reset"},
    {"type": "submit", "label": "Confirm Changes", "level": "primary"}
  ]
}

后端先复制当前对象,在副本上完成所有字段赋值和校验,全部成功后才替换原对象,因此一次提交具有对象级事务语义。

两组配置示例

页面包含三个后端定义的标签页:

Radio State List
Radio Service Configuration
Alert Configuration

Radio Service Configuration 展示字符串、枚举、整数、浮点数、树状子配置、日期和颜色。

Alert Configuration 展示布尔开关、字符串、两个枚举、数字、日期、颜色和条件显示的 webhook 子配置。

对象状态

对象状态使用独立返回类型描述,不混入 CRUD 对象字段:

struct Radio_Item_Status {
    Status_Value<Radio_Operating_State> operating_state;
    Status_Value<std::uint64_t> refresh_sequence;
    Status_Value<std::string> refresh_time;
};

状态值同时携带颜色:

{
  "operating_state": {
    "value": "running",
    "color": "#16a34a"
  }
}

资源通过成员函数指针注册:

resource.register_status<&Radio_State::status>(2000);

只有用户打开某一行的 View status PopOver 后,该行状态才开始请求和刷新。

API

GET    /admin/amis
GET    /admin/status
GET    /admin/radio_states/descriptor
GET    /admin/radio_states/status/descriptor
GET    /admin/radio_states/amis
GET    /admin/radio_states
GET    /admin/radio_states/{id}
GET    /admin/radio_states/{id}/status
POST   /admin/radio_states
PUT    /admin/radio_states/{id}
PATCH  /admin/radio_states/{id}
DELETE /admin/radio_states/{id}
GET    /admin/config/radio/descriptor
GET    /admin/config/radio/data
GET    /admin/config/radio/amis
POST   /admin/config/radio/data
GET    /admin/config/alerts/descriptor
GET    /admin/config/alerts/data
GET    /admin/config/alerts/amis
POST   /admin/config/alerts/data

前端

创建外部 node_modules 相对符号链接:

pwsh -NoProfile -File .\scripts\link_node_modules.ps1

默认结构:

Adminive/
├── frontend/
│   └── node_modules -> ../node_modules
└── node_modules/

安装和构建:

npm --prefix .\frontend ci
npm --prefix .\frontend run build

后端

cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
.\build\backend\Adminive_Server.exe 9999

访问:

http://127.0.0.1:9999

源码压缩目标

cmake --build build --target package_zip

输出文件位置由第二个参数明确指定,第三个参数传入排除列表变量名:

add_project_zip_target(package_zip "${CMAKE_CURRENT_LIST_DIR}/Adminive.zip" adminive_package_excludes)