430 lines
11 KiB
Markdown
430 lines
11 KiB
Markdown
# Adminive Backend-Driven Complete Example
|
||
|
||
Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、字段权限、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。
|
||
|
||
## 适配层
|
||
|
||
核心目标只提供描述器、转换流程和 amis Schema 生成:
|
||
|
||
```cmake
|
||
target_link_libraries(your_target PRIVATE Adminive::Core)
|
||
```
|
||
|
||
HTTP 资源层单独链接:
|
||
|
||
```cmake
|
||
target_link_libraries(your_target PRIVATE Adminive::Http)
|
||
```
|
||
|
||
项目自带的 `nlohmann::json` 和 `magic_enum` 桥接不会被核心头自动包含。需要默认实现时显式包含:
|
||
|
||
```cpp
|
||
#include "adminive/adapters/nlohmann_json.hpp"
|
||
#include "adminive/adapters/magic_enum.hpp"
|
||
using Json = nlohmann::json;
|
||
```
|
||
|
||
### JSON 适配
|
||
|
||
外部 JSON 类型通过 `Json_Adapter<Json>` 接入。适配器负责对象、数组、标量、字段访问、解析和输出。所有转换入口显式携带 JSON 类型:
|
||
|
||
```cpp
|
||
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 类型特化:
|
||
|
||
```cpp
|
||
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` 是可选桥接,也可以为单个枚举自行实现:
|
||
|
||
```cpp
|
||
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()` 生成字段。
|
||
|
||
```cpp
|
||
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>();
|
||
}
|
||
};
|
||
```
|
||
|
||
公开且无歧义的基类成员也可以直接注册到派生类型描述器,字段所有者不再要求与最终对象类型完全相同。
|
||
|
||
## 目录结构
|
||
|
||
```text
|
||
Adminive/
|
||
├── backend/
|
||
│ ├── include/adminive/
|
||
│ ├── src/
|
||
│ │ ├── example.hpp
|
||
│ │ └── main.cpp
|
||
│ ├── tests/
|
||
│ └── third_party/
|
||
├── cmake/
|
||
├── frontend/
|
||
├── scripts/
|
||
├── CMakeLists.txt
|
||
└── README.md
|
||
```
|
||
|
||
`example.hpp` 位于 `backend/src`,只作为完整示例实现,不属于公共库头文件。
|
||
|
||
## 列字段名称、显示名称和排序
|
||
|
||
```cpp
|
||
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
|
||
.creatable()
|
||
.editable()
|
||
.required()
|
||
.list_label("Mode")
|
||
.order(5)
|
||
.sortable();
|
||
```
|
||
|
||
- `name` 来自成员名称,用于 JSON 字段和接口参数。
|
||
- `label` 用于表单标签。
|
||
- `list_label` 用于列表表头,未设置时回退到 `label`。
|
||
- `order` 控制列表列顺序,未设置时按字段声明顺序生成默认值。
|
||
- `sortable` 控制该列能否排序,后端只接受已声明为可排序的字段。
|
||
|
||
## 枚举列表项
|
||
|
||
```cpp
|
||
enum class Radio_Mode {
|
||
receive,
|
||
transmit,
|
||
duplex,
|
||
maintenance
|
||
};
|
||
```
|
||
|
||
业务结构直接使用枚举:
|
||
|
||
```cpp
|
||
struct Radio_State {
|
||
Radio_Mode mode{Radio_Mode::receive};
|
||
};
|
||
```
|
||
|
||
描述中不需要手写选项:
|
||
|
||
```cpp
|
||
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
|
||
.creatable()
|
||
.editable()
|
||
.required()
|
||
.list_label("Mode")
|
||
.order(5)
|
||
.sortable();
|
||
```
|
||
|
||
示例通过可选的 `magic_enum` 桥接生成;核心只依赖 `Enum_Adapter`:
|
||
|
||
```json
|
||
{
|
||
"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"`。
|
||
|
||
## 树状配置
|
||
|
||
树结构由嵌套的已描述对象表达,不额外维护一份前端树配置:
|
||
|
||
```cpp
|
||
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{};
|
||
};
|
||
```
|
||
|
||
界面结构为:
|
||
|
||
```text
|
||
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`,子字段使用点路径,例如:
|
||
|
||
```text
|
||
primary_endpoint.host
|
||
appearance.effective_date
|
||
```
|
||
|
||
提交时仍然形成嵌套 JSON 对象。
|
||
|
||
## 配置联动
|
||
|
||
字段描述通过 `visible_on` 保存 amis 表达式:
|
||
|
||
```cpp
|
||
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "Backup Endpoint")
|
||
.editable()
|
||
.visible_on("${mode == 'duplex'}");
|
||
```
|
||
|
||
当 `mode` 不是 `duplex` 时,整个 `Backup Endpoint` 子树隐藏。
|
||
|
||
第二个配置示例使用组合条件:
|
||
|
||
```cpp
|
||
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook Endpoint")
|
||
.editable()
|
||
.visible_on("${enabled && channel == 'webhook'}");
|
||
```
|
||
|
||
只有启用告警且投递通道为 `webhook` 时,才显示 webhook 子配置。
|
||
|
||
## 完整输入控件
|
||
|
||
后端根据 C++ 类型自动选择基础控件:
|
||
|
||
```text
|
||
std::string -> input-text
|
||
整数类型 -> input-number
|
||
浮点类型 -> input-number
|
||
bool -> switch
|
||
enum class -> select
|
||
```
|
||
|
||
日期和颜色通过后端字段描述指定:
|
||
|
||
```cpp
|
||
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");
|
||
```
|
||
|
||
日期控件自动补充:
|
||
|
||
```json
|
||
{
|
||
"type": "input-date",
|
||
"valueFormat": "YYYY-MM-DD",
|
||
"displayFormat": "YYYY-MM-DD",
|
||
"clearable": true
|
||
}
|
||
```
|
||
|
||
## 一次性确认修改
|
||
|
||
配置表单不会在单个输入项改变时调用后端。所有修改先保留在表单数据域,点击 `Confirm Changes` 后一次性提交完整配置:
|
||
|
||
```json
|
||
{
|
||
"actions": [
|
||
{"type": "reset", "label": "Reset"},
|
||
{"type": "submit", "label": "Confirm Changes", "level": "primary"}
|
||
]
|
||
}
|
||
```
|
||
|
||
后端先复制当前对象,在副本上完成所有字段赋值和校验,全部成功后才替换原对象,因此一次提交具有对象级事务语义。
|
||
|
||
## 两组配置示例
|
||
|
||
页面包含三个后端定义的标签页:
|
||
|
||
```text
|
||
Radio State List
|
||
Radio Service Configuration
|
||
Alert Configuration
|
||
```
|
||
|
||
`Radio Service Configuration` 展示字符串、枚举、整数、浮点数、树状子配置、日期和颜色。
|
||
|
||
`Alert Configuration` 展示布尔开关、字符串、两个枚举、数字、日期、颜色和条件显示的 webhook 子配置。
|
||
|
||
## 对象状态
|
||
|
||
对象状态使用独立返回类型描述,不混入 CRUD 对象字段:
|
||
|
||
```cpp
|
||
struct Radio_Item_Status {
|
||
Status_Value<Radio_Operating_State> operating_state;
|
||
Status_Value<std::uint64_t> refresh_sequence;
|
||
Status_Value<std::string> refresh_time;
|
||
};
|
||
```
|
||
|
||
状态值同时携带颜色:
|
||
|
||
```json
|
||
{
|
||
"operating_state": {
|
||
"value": "running",
|
||
"color": "#16a34a"
|
||
}
|
||
}
|
||
```
|
||
|
||
资源通过成员函数指针注册:
|
||
|
||
```cpp
|
||
resource.register_status<&Radio_State::status>(2000);
|
||
```
|
||
|
||
只有用户打开某一行的 `View status` PopOver 后,该行状态才开始请求和刷新。
|
||
|
||
## API
|
||
|
||
```text
|
||
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` 相对符号链接:
|
||
|
||
```powershell
|
||
pwsh -NoProfile -File .\scripts\link_node_modules.ps1
|
||
```
|
||
|
||
默认结构:
|
||
|
||
```text
|
||
Adminive/
|
||
├── frontend/
|
||
│ └── node_modules -> ../node_modules
|
||
└── node_modules/
|
||
```
|
||
|
||
安装和构建:
|
||
|
||
```powershell
|
||
npm --prefix .\frontend ci
|
||
npm --prefix .\frontend run build
|
||
```
|
||
|
||
## 后端
|
||
|
||
```powershell
|
||
cmake -S . -B build -G Ninja
|
||
cmake --build build
|
||
ctest --test-dir build --output-on-failure
|
||
.\build\backend\Adminive_Server.exe 9999
|
||
```
|
||
|
||
访问:
|
||
|
||
```text
|
||
http://127.0.0.1:9999
|
||
```
|
||
|
||
## 源码压缩目标
|
||
|
||
```powershell
|
||
cmake --build build --target package_zip
|
||
```
|
||
|
||
输出文件位置由第二个参数明确指定,第三个参数传入排除列表变量名:
|
||
|
||
```cmake
|
||
add_project_zip_target(package_zip "${CMAKE_CURRENT_LIST_DIR}/Adminive.zip" adminive_package_excludes)
|
||
```
|