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)
target_link_libraries(httplib_target PRIVATE Adminive::Httplib)
target_link_libraries(drogon_target PRIVATE Adminive::Drogon)
Adminive::Http 只提供 Resource_Service、HTTP 响应模型和错误结构,不包含服务器头文件。adminive/adapters/httplib.hpp 与 adminive/adapters/drogon.hpp 分别负责路由注册和响应写回。
项目自带的 nlohmann::json 和 magic_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_name 和 append_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> 接收字段数量、字段名称和按索引访问。可选桥接头为:
#include "adminive/adapters/boost_pfr.hpp"
普通聚合类型:
template <>
struct adminive::Reflection_Adapter<Config>
: adminive::Boost_Pfr_Reflection_Adapter<Config> {};
由多个数据基类组成的配置类型可以展开所有基类字段:
template <>
struct adminive::Reflection_Adapter<Mode_ACS_Config>
: adminive::Boost_Pfr_Base_Reflection_Adapter<
Mode_ACS_Config,
Mode_ACS_Config_Base_Data,
Mode_ACS_Config_Data> {};
较旧 Boost.PFR 不提供字段名时,外部为每个数据基类特化 Boost_Pfr_Name_Adapter<T>;较新版本自动使用 boost::pfr::get_name()。
不可复制运行时对象
Object_Adapter<T> 将不可复制、含原子字段或带运行时状态的对象映射到纯值配置模型。Adminive 只修改候选模型,校验和持久化成功后才调用 commit():
template <>
struct adminive::Object_Adapter<Runtime_Config> {
using model_type = Runtime_Config_Model;
static model_type snapshot(const Runtime_Config& value);
static model_type create();
static void commit(Runtime_Config& target, model_type value);
};
外部控件适配
复杂业务类型通过 Control_Adapter<T, Json> 明确决定表单控件和列表列。框架不会把 vector 或 map 隐式决定为表格、列表、分页或标签页;未配置控件的结构类型会在 Schema 生成时给出明确错误。
template <>
struct adminive::Control_Adapter<Color, Json> {
static Json make_control(const Json& field, adminive::Control_Context context);
static Json make_column(const Json& field);
};
多态对象
Polymorphic_Adapter<T, Json> 定义判别字段、各派生类型描述器以及编码解码。默认 amis 表达使用一个类型选择器和按类型联动显示的子表单,外部仍可用 Control_Adapter 完全替换界面表达。
template <>
struct adminive::Polymorphic_Adapter<Device, Json> {
static Json descriptor();
static Json encode(const Device& value);
static void decode(Device& target, const Json& value);
};
嵌套错误和 HTTP 反馈
字段错误使用完整路径,例如 map_resources.tile_sources[2].resource.maximum_level。HTTP 422 响应同时返回 errors 和 field_errors,Drogon 与 cpp-httplib 适配层使用同一响应模型。
描述器驱动状态
整体状态和列表项状态都使用 adminive.status 描述器生成,不再包含固定的服务名、字段名或模板。服务标题、字段标签、颜色和轮询周期由调用端传入的状态描述器决定。
目录结构
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)