分离 json magic_enum 依赖
This commit is contained in:
@@ -1,10 +1,146 @@
|
||||
# Adminive Backend-Driven Example
|
||||
# Adminive Backend-Driven Complete Example
|
||||
|
||||
Adminive 使用 C++20 模板、成员指针和 `magic_enum` 生成后端描述。前端只负责渲染 `/admin/amis` 返回的 amis Schema,不包含业务字段、枚举映射、列顺序、排序规则或配置联动。
|
||||
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 {
|
||||
@@ -12,179 +148,260 @@ struct Radio_State {
|
||||
};
|
||||
```
|
||||
|
||||
界面标签、列表表头和枚举显示名称在描述端单独配置:
|
||||
描述中不需要手写选项:
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "运行模式")
|
||||
.list_label("模式")
|
||||
.sortable()
|
||||
.enum_label<Radio_Mode::receive>("接收")
|
||||
.enum_label<Radio_Mode::transmit>("发送")
|
||||
.enum_label<Radio_Mode::duplex>("双工")
|
||||
.enum_label<Radio_Mode::maintenance>("维护");
|
||||
```
|
||||
|
||||
接口仍返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "duplex"
|
||||
}
|
||||
```
|
||||
|
||||
表格显示为“模式:双工”。`magic_enum` 负责枚举值发现和稳定字符串生成,`.enum_label` 只覆盖界面文字。
|
||||
|
||||
## 列顺序与用户排序
|
||||
|
||||
字段的默认列顺序和是否允许按值排序由后端描述:
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD_LABEL(T, buffer_count, "缓冲区数量")
|
||||
.list_label("缓冲区")
|
||||
.order(10)
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
|
||||
.creatable()
|
||||
.editable()
|
||||
.required()
|
||||
.list_label("Mode")
|
||||
.order(5)
|
||||
.sortable();
|
||||
```
|
||||
|
||||
对象描述可以定义默认排序:
|
||||
示例通过可选的 `magic_enum` 桥接生成;核心只依赖 `Enum_Adapter`:
|
||||
|
||||
```cpp
|
||||
object<T>(/* fields */)
|
||||
.default_sort("buffer_count");
|
||||
```json
|
||||
{
|
||||
"name": "mode",
|
||||
"value_type": "enum",
|
||||
"options": [
|
||||
{"label": "Receive", "value": "receive"},
|
||||
{"label": "Transmit", "value": "transmit"},
|
||||
{"label": "Duplex", "value": "duplex"},
|
||||
{"label": "Maintenance", "value": "maintenance"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
用户点击可排序表头时,前端发送 `orderBy` 和 `orderDir`,后端只接受声明为 `.sortable()` 的字段。
|
||||
表格列使用 amis `mapping`,接口数据仍然返回稳定的枚举字符串,例如 `"duplex"`。
|
||||
|
||||
用户也可以拖拽调整行顺序:
|
||||
|
||||
```cpp
|
||||
object<T>(/* fields */)
|
||||
.user_reorderable();
|
||||
```
|
||||
|
||||
该配置会生成 `draggable` 和 `saveOrderApi`,后端提供 `POST <resource>/order` 保存当前运行期顺序。列选择菜单支持拖拽调整列先后,默认由 `.column_reorderable()` 控制。
|
||||
|
||||
## CRUD 中文文字
|
||||
|
||||
CRUD 固定文字也在对象描述中配置:
|
||||
|
||||
```cpp
|
||||
object<T>(/* fields */)
|
||||
.label("无线电状态")
|
||||
.id_label("编号")
|
||||
.create_label("新增")
|
||||
.edit_label("编辑")
|
||||
.delete_label("删除")
|
||||
.actions_label("操作")
|
||||
.confirm_label("确认修改")
|
||||
.view_status_label("查看状态 ▾")
|
||||
.management_label("无线电状态列表");
|
||||
```
|
||||
|
||||
## 树状配置与持久化
|
||||
|
||||
嵌套 C++ 对象直接表达树状配置:
|
||||
## 树状配置
|
||||
|
||||
树结构由嵌套的已描述对象表达,不额外维护一份前端树配置:
|
||||
|
||||
```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{};
|
||||
};
|
||||
```
|
||||
|
||||
嵌套对象在界面中生成 `fieldset`。字符串、整数、浮点数、布尔、枚举、日期和颜色分别生成对应控件。联动条件仍由后端定义:
|
||||
界面结构为:
|
||||
|
||||
```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, "备用端点")
|
||||
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "Backup Endpoint")
|
||||
.editable()
|
||||
.visible_on("${mode == 'duplex'}");
|
||||
```
|
||||
|
||||
配置文件位于:
|
||||
当 `mode` 不是 `duplex` 时,整个 `Backup Endpoint` 子树隐藏。
|
||||
|
||||
```text
|
||||
config/adminive.json
|
||||
```
|
||||
|
||||
文件保存以下持久化内容:
|
||||
|
||||
```text
|
||||
Application_Config
|
||||
├── default_device_type
|
||||
├── radio_service
|
||||
├── alerts
|
||||
├── serial_table
|
||||
└── network_table
|
||||
```
|
||||
|
||||
程序启动时读取该文件。文件不存在时,使用 C++ 默认值自动创建。配置表单点击“确认修改”后,后端先在副本上完成赋值和校验,再写入临时文件并替换正式配置文件,成功后才替换内存对象。
|
||||
|
||||
## 多态类型表格
|
||||
|
||||
不同表格类型通过 C++ 虚函数注册:
|
||||
第二个配置示例使用组合条件:
|
||||
|
||||
```cpp
|
||||
class Device_Table_Base {
|
||||
public:
|
||||
virtual ~Device_Table_Base() = default;
|
||||
virtual Device_Table_Type type() const noexcept = 0;
|
||||
virtual std::string_view type_name() const noexcept = 0;
|
||||
virtual std::string_view type_label() const noexcept = 0;
|
||||
virtual Json amis_schema() const = 0;
|
||||
virtual void bind(httplib::Server& server) = 0;
|
||||
};
|
||||
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook Endpoint")
|
||||
.editable()
|
||||
.visible_on("${enabled && channel == 'webhook'}");
|
||||
```
|
||||
|
||||
示例实现两个子类:
|
||||
只有启用告警且投递通道为 `webhook` 时,才显示 webhook 子配置。
|
||||
|
||||
## 完整输入控件
|
||||
|
||||
后端根据 C++ 类型自动选择基础控件:
|
||||
|
||||
```text
|
||||
Serial_Device_Table
|
||||
Network_Device_Table
|
||||
std::string -> input-text
|
||||
整数类型 -> input-number
|
||||
浮点类型 -> input-number
|
||||
bool -> switch
|
||||
enum class -> select
|
||||
```
|
||||
|
||||
界面切换 `device_type` 后,`service.schemaApi` 重新向后端请求对应子类的 Schema:
|
||||
日期和颜色通过后端字段描述指定:
|
||||
|
||||
```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
|
||||
serial -> 串口、波特率、校验位、启用
|
||||
network -> 地址、协议、超时、TLS
|
||||
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/radio_states/{id}/status
|
||||
POST /admin/device_tables/serial/items/order
|
||||
POST /admin/device_tables/network/items/order
|
||||
GET /admin/device_tables/schema?type=serial
|
||||
GET /admin/device_tables/schema?type=network
|
||||
POST /admin/device_tables/default
|
||||
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
|
||||
POST /admin/device_tables/serial/config/data
|
||||
POST /admin/device_tables/network/config/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
|
||||
@@ -193,46 +410,20 @@ ctest --test-dir build --output-on-failure
|
||||
.\build\backend\Adminive_Server.exe 9999
|
||||
```
|
||||
|
||||
访问:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:9999
|
||||
```
|
||||
|
||||
## 源码压缩目标
|
||||
|
||||
压缩函数参数顺序:
|
||||
|
||||
```cmake
|
||||
add_project_zip_target(
|
||||
target_name
|
||||
output_file
|
||||
root_directory
|
||||
include_list_name
|
||||
exclude_list_name
|
||||
)
|
||||
```powershell
|
||||
cmake --build build --target package_zip
|
||||
```
|
||||
|
||||
调用示例:
|
||||
输出文件位置由第二个参数明确指定,第三个参数传入排除列表变量名:
|
||||
|
||||
```cmake
|
||||
set(adminive_package_includes
|
||||
"/backend/"
|
||||
"/cmake/"
|
||||
"/config/"
|
||||
"/frontend/"
|
||||
"/scripts/"
|
||||
"/CMakeLists.txt"
|
||||
"/README.md"
|
||||
)
|
||||
set(adminive_package_excludes
|
||||
"/.git/"
|
||||
"/.idea/"
|
||||
"/build/"
|
||||
"/cmake-build-*/"
|
||||
"/doc/"
|
||||
"/node_modules/"
|
||||
"/frontend/frontend.zip"
|
||||
"/frontend_dist/"
|
||||
"*.tsbuildinfo"
|
||||
"*.zip"
|
||||
)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PackageZip.cmake")
|
||||
add_project_zip_target(package_zip "${CMAKE_CURRENT_LIST_DIR}/Adminive.zip" "${CMAKE_CURRENT_LIST_DIR}" adminive_package_includes adminive_package_excludes)
|
||||
add_project_zip_target(package_zip "${CMAKE_CURRENT_LIST_DIR}/Adminive.zip" adminive_package_excludes)
|
||||
```
|
||||
|
||||
函数只扫描包含列表指定的目录和文件,然后相对于根目录应用排除列表。
|
||||
|
||||
Reference in New Issue
Block a user