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

430 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```