画廊添加
This commit is contained in:
@@ -96,6 +96,26 @@ const auto page = composition_view("device_page", compose::vertical(
|
||||
|
||||
`slot` 引用已经生成的 Form/Table/Status 等组件,后端仍然决定语义组合,AMIS Adapter 只负责把组合翻译成具体 AMIS JSON。前端可以进一步决定 CSS、断点、列宽等纯视觉细节。
|
||||
|
||||
### 示例组件画廊
|
||||
|
||||
完整示例首页现在包含“组件画廊”页签。画廊不是前端写死的组件清单,而是后端使用 Adminive 自身 Descriptor、View Schema、Composition View 和 Collection Service 生成,用于直接检查当前支持能力的实际 AMIS 效果。
|
||||
|
||||
字段画廊覆盖 `automatic`、`text`、`multiline_text`、`number`、`boolean`、`select`、`date`、`color`、`std::optional`、只读 static、条件联动和嵌套对象;同一表单同时覆盖 `vertical`、`horizontal`、`grid`、`flow`、`group`、`card` 和 `tabs`。表格画廊提供真实的新增、编辑、删除、刷新、列控制、搜索、筛选、排序和拖拽顺序接口。第三个页签直接展示 Composition 层的 `heading`、`horizontal`、`grid`、`flow`、`group`、`card`、`tabs` 和 `slot`。
|
||||
|
||||
画廊资源同时提供正常 Adminive HTTP 接口:
|
||||
|
||||
```text
|
||||
/admin/gallery/config/descriptor
|
||||
/admin/gallery/config/view
|
||||
/admin/gallery/config/data
|
||||
/admin/gallery/config/amis
|
||||
/admin/gallery/items
|
||||
/admin/gallery/items/{id}
|
||||
/admin/gallery/items/order
|
||||
```
|
||||
|
||||
`flow` 在 AMIS Adapter 中翻译成可换行 Flex,每个 `items` 元素都是显式 `container` renderer。这样 Flow 的子 Card/Form Control 不会以无 `type` 的包装对象进入 AMIS renderer tree。
|
||||
|
||||
## 后端分层
|
||||
|
||||
`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它依赖项目内的 `third_party/Structive`,由 Structive 负责 intrinsic property capability、Schema 和 synchronization;Adminive 自身包含 descriptor、JSON 适配协议、AMIS schema、状态协议、managed adapter 和抽象 `Resource_Service`:
|
||||
|
||||
@@ -472,12 +472,21 @@ Json make_amis_view_node(const View_Node& node, const Json& descriptor_json, std
|
||||
Json items = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "type", "container");
|
||||
json_set(item, "body", make_amis_view_body<Json, T>(child, descriptor_json, permission, prefix));
|
||||
Json item_style = json_object<Json>();
|
||||
json_set(item_style, "flex", "1 1 280px");
|
||||
json_set(item, "style", std::move(item_style));
|
||||
json_append(items, std::move(item));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "flex");
|
||||
json_set(result, "direction", "row");
|
||||
json_set(result, "items", std::move(items));
|
||||
Json style = json_object<Json>();
|
||||
json_set(style, "flexWrap", "wrap");
|
||||
json_set(style, "gap", "12px");
|
||||
json_set(result, "style", std::move(style));
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
@@ -606,12 +615,21 @@ Json make_amis_composition_node(const Composition_Node& node, const std::map<std
|
||||
Json items = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "type", "container");
|
||||
json_set(item, "body", make_amis_composition_body<Json>(child, slots));
|
||||
Json item_style = json_object<Json>();
|
||||
json_set(item_style, "flex", "1 1 280px");
|
||||
json_set(item, "style", std::move(item_style));
|
||||
json_append(items, std::move(item));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "flex");
|
||||
json_set(result, "direction", "row");
|
||||
json_set(result, "items", std::move(items));
|
||||
Json style = json_object<Json>();
|
||||
json_set(style, "flexWrap", "wrap");
|
||||
json_set(style, "gap", "12px");
|
||||
json_set(result, "style", std::move(style));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ add_library(Adminive_Default INTERFACE)
|
||||
set_target_properties(Adminive_Default PROPERTIES EXPORT_NAME Default)
|
||||
add_library(Adminive::Default ALIAS Adminive_Default)
|
||||
target_link_libraries(Adminive_Default INTERFACE Adminive::Httplib Adminive::Nlohmann Adminive::MagicEnum)
|
||||
add_library(Adminive_Example STATIC "${CMAKE_CURRENT_LIST_DIR}/src/example.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/config_store.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/device_tables.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/server_app.cpp")
|
||||
add_library(Adminive_Example STATIC "${CMAKE_CURRENT_LIST_DIR}/src/example.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/config_store.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/device_tables.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/gallery.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/server_app.cpp")
|
||||
target_include_directories(Adminive_Example PUBLIC "${CMAKE_CURRENT_LIST_DIR}/src")
|
||||
target_link_libraries(Adminive_Example PUBLIC Adminive::Default)
|
||||
add_executable(Adminive_Server "${CMAKE_CURRENT_LIST_DIR}/src/main.cpp")
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "adminive/adapters/nlohmann_json.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
namespace adminive::example {
|
||||
@@ -114,6 +116,43 @@ struct Application_Config {
|
||||
Serial_Table_Config serial_table{};
|
||||
Network_Table_Config network_table{};
|
||||
};
|
||||
struct Gallery_Config {
|
||||
struct Even_Validator {
|
||||
static constexpr int multiple_of = 2;
|
||||
static constexpr std::string_view message = "值必须是偶数";
|
||||
void operator()(const int& value) const {
|
||||
if(value % 2 != 0) {
|
||||
throw std::invalid_argument("value must be even");
|
||||
}
|
||||
}
|
||||
};
|
||||
std::string automatic_text{"自动推导为文本输入"};
|
||||
std::string text_value{"单行文本"};
|
||||
std::string multiline_value{"第一行\n第二行"};
|
||||
Range_Value<int, 0, 100> number_value{42};
|
||||
Validated_Value<int, Even_Validator> even_number{4};
|
||||
bool boolean_value{true};
|
||||
Log_Level select_value{Log_Level::info};
|
||||
std::string date_value{"2026-08-08"};
|
||||
std::string color_value{"#2563eb"};
|
||||
std::optional<std::string> optional_text{"这个字段可以清空"};
|
||||
bool show_conditional{true};
|
||||
std::string conditional_text{"这个字段由上面的开关控制显示"};
|
||||
std::string read_only_text{"只读字段使用 static 展示,不生成编辑控件"};
|
||||
Endpoint_Config nested{};
|
||||
};
|
||||
enum class Gallery_Category {
|
||||
primary,
|
||||
secondary,
|
||||
experimental
|
||||
};
|
||||
struct Gallery_Row {
|
||||
std::string name;
|
||||
Gallery_Category category{Gallery_Category::primary};
|
||||
Range_Value<int, 0, 100> score{50};
|
||||
bool enabled{true};
|
||||
std::string color{"#2563eb"};
|
||||
};
|
||||
struct Serial_Device_Row {
|
||||
std::string port_name;
|
||||
Range_Value<int, 1200, 4000000> baud_rate{115200};
|
||||
|
||||
@@ -86,6 +86,34 @@ struct Type_View_Descriptor<example::Network_Table_Config> {
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Gallery_Config> {
|
||||
static auto get() {
|
||||
using T = example::Gallery_Config;
|
||||
return object<T>("gallery_config", ADMINIVE_FIELD_LABEL(T, automatic_text, "自动推导文本").editable().description("不指定 control,由字段类型自动选择输入控件"), ADMINIVE_FIELD_LABEL(T, text_value, "单行文本").editable().text_input().description("显式 text_input"), ADMINIVE_FIELD_LABEL(T, multiline_value, "多行文本").editable().multiline_text().description("textarea 多行输入"), ADMINIVE_FIELD_LABEL(T, number_value, "范围数字").editable().number_input().required().description("Range_Value 自动生成最小值和最大值"), ADMINIVE_FIELD_LABEL(T, even_number, "偶数校验").editable().number_input().required().description("Validated_Value 自动生成 step 和校验提示"), ADMINIVE_FIELD_LABEL(T, boolean_value, "布尔开关").editable().boolean_input(), ADMINIVE_FIELD_LABEL(T, select_value, "枚举选择").editable().select_input().enum_label<example::Log_Level::trace>("跟踪").enum_label<example::Log_Level::debug>("调试").enum_label<example::Log_Level::info>("信息").enum_label<example::Log_Level::warning>("警告").enum_label<example::Log_Level::error>("错误"), ADMINIVE_FIELD_LABEL(T, date_value, "日期选择").editable().date_input(), ADMINIVE_FIELD_LABEL(T, color_value, "颜色选择").editable().color_input(), ADMINIVE_FIELD_LABEL(T, optional_text, "可清空文本").editable().text_input().description("std::optional 自动允许清空"), ADMINIVE_FIELD_LABEL(T, show_conditional, "显示联动字段").editable().boolean_input(), ADMINIVE_FIELD_LABEL(T, conditional_text, "联动文本").editable().text_input().visible_on("${$self.show_conditional}"), ADMINIVE_FIELD_LABEL(T, read_only_text, "只读展示").description("没有 editable,因此编辑表单中使用 static 展示"), ADMINIVE_FIELD_LABEL(T, nested, "嵌套对象").editable().description("对象字段自动展开为 fieldset")).label("Adminive 控件画廊");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Gallery_Config> {
|
||||
static Form_View edit() {
|
||||
using T = example::Gallery_Config;
|
||||
return edit_form<T>(vertical(group("文本输入", horizontal(use<&T::automatic_text>(), use<&T::text_value>()), use<&T::multiline_value>(), use<&T::optional_text>()), group("数字与选择", grid(4, use<&T::number_value>(), use<&T::even_number>(), use<&T::boolean_value>(), use<&T::select_value>())), flow(card("日期", use<&T::date_value>()), card("颜色", use<&T::color_value>())), tabs(tab("条件联动", use<&T::show_conditional>(), use<&T::conditional_text>()), tab("嵌套对象", use<&T::nested>()), tab("只读字段", use<&T::read_only_text>())))).titled("字段控件与表单布局").submit("保存画廊数据");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Gallery_Row> {
|
||||
static auto get() {
|
||||
using T = example::Gallery_Row;
|
||||
return object<T>("gallery_row", ADMINIVE_FIELD_LABEL(T, name, "名称").creatable().editable().required().text_input(), ADMINIVE_FIELD_LABEL(T, category, "分类").creatable().editable().required().select_input().enum_label<example::Gallery_Category::primary>("主要").enum_label<example::Gallery_Category::secondary>("次要").enum_label<example::Gallery_Category::experimental>("实验"), ADMINIVE_FIELD_LABEL(T, score, "评分").creatable().editable().required().number_input(), ADMINIVE_FIELD_LABEL(T, enabled, "启用").creatable().editable().boolean_input(), ADMINIVE_FIELD_LABEL(T, color, "颜色").creatable().editable().color_input()).label("画廊表格项");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Gallery_Row> {
|
||||
static Table_View table() {
|
||||
using T = example::Gallery_Row;
|
||||
return table_view<T>(column<&T::name>("名称").sort().search().fix(Table_Fixed::left), column<&T::category>("分类").sort().filter(), column<&T::score>("评分").sort(), column<&T::enabled>("启用").sort().filter(), column<&T::color>("颜色")).titled("表格与操作画廊").id_title("编号").create_title("新增一行").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("保存修改").delete_confirmation("确定删除这条画廊数据吗?").default_sort("score", true).reorderable();
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Application_Config> {
|
||||
static auto get() {
|
||||
using T = example::Application_Config;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "gallery.hpp"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
namespace adminive::example {
|
||||
namespace {
|
||||
Json make_gallery_note(std::string title, std::string text) {
|
||||
return Json{{"type", "tpl"}, {"tpl", "<strong>" + std::move(title) + "</strong><div class=\"text-muted\" style=\"margin-top:6px\">" + std::move(text) + "</div>"}};
|
||||
}
|
||||
}
|
||||
Component_Gallery::Component_Gallery() : config_resource_(config_, "/admin/gallery/config"), rows_("/admin/gallery/items", make_rows()) {}
|
||||
Json Component_Gallery::amis_schema() const {
|
||||
const Json form = config_resource_.amis_schema();
|
||||
const Json table = rows_.amis_schema();
|
||||
const auto composition = composition_view("component_gallery", compose::tabs(compose::tab("字段控件与布局", compose::slot("form")), compose::tab("表格与操作", compose::slot("table")), compose::tab("组合能力", compose::vertical(compose::heading("Composition View 组合画廊").accent("#2563eb"), compose::group("横向与网格", compose::horizontal(compose::card("Horizontal A", compose::slot("horizontal_a")), compose::card("Horizontal B", compose::slot("horizontal_b"))), compose::grid(3, compose::card("Grid 1", compose::slot("grid_1")), compose::card("Grid 2", compose::slot("grid_2")), compose::card("Grid 3", compose::slot("grid_3")))), compose::group("Flow 自动换行", compose::flow(compose::card("Flow A", compose::slot("flow_a")), compose::card("Flow B", compose::slot("flow_b")), compose::card("Flow C", compose::slot("flow_c")), compose::card("Flow D", compose::slot("flow_d")))).collapsible_when())))).titled("Adminive 组件画廊");
|
||||
std::map<std::string, Json> slots;
|
||||
slots.emplace("form", form);
|
||||
slots.emplace("table", table.at("body"));
|
||||
slots.emplace("horizontal_a", make_gallery_note("horizontal", "两个组件按横向语义组合,具体栅格由 AMIS Adapter 翻译。"));
|
||||
slots.emplace("horizontal_b", make_gallery_note("card", "Card 是组合层容器,不和字段 Presentation 混在一起。"));
|
||||
slots.emplace("grid_1", make_gallery_note("grid", "后端定义逻辑列数。"));
|
||||
slots.emplace("grid_2", make_gallery_note("slot", "Slot 可以嵌入 Form、Table、Status 或其他后端组件。"));
|
||||
slots.emplace("grid_3", make_gallery_note("group", "Group 可以折叠并继续嵌套其他组合。"));
|
||||
slots.emplace("flow_a", make_gallery_note("flow", "Flow 使用可换行 Flex,子项都是合法 container renderer。"));
|
||||
slots.emplace("flow_b", make_gallery_note("tabs", "整个画廊本身就是 Composition tabs。"));
|
||||
slots.emplace("flow_c", make_gallery_note("vertical", "纵向组合只表达顺序,不泄漏具体 CSS。"));
|
||||
slots.emplace("flow_d", make_gallery_note("heading", "标题也是组合节点,可以独立使用。"));
|
||||
return to_amis_composition_schema<Json>(composition, slots);
|
||||
}
|
||||
void Component_Gallery::bind(httplib::Server& server) {
|
||||
config_resource_.bind(server);
|
||||
rows_.bind(server);
|
||||
}
|
||||
std::vector<Gallery_Row> Component_Gallery::make_rows() {
|
||||
return {
|
||||
Gallery_Row{"Alpha", Gallery_Category::primary, Range_Value<int, 0, 100>{92}, true, "#2563eb"},
|
||||
Gallery_Row{"Beta", Gallery_Category::secondary, Range_Value<int, 0, 100>{76}, true, "#16a34a"},
|
||||
Gallery_Row{"Gamma", Gallery_Category::experimental, Range_Value<int, 0, 100>{58}, false, "#d97706"}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "example_descriptors.hpp"
|
||||
#include <vector>
|
||||
namespace adminive::example {
|
||||
class Component_Gallery {
|
||||
public:
|
||||
Component_Gallery();
|
||||
Json amis_schema() const;
|
||||
void bind(httplib::Server& server);
|
||||
private:
|
||||
static std::vector<Gallery_Row> make_rows();
|
||||
Managed_Value<Gallery_Config> config_;
|
||||
Http_Resource<Gallery_Config> config_resource_;
|
||||
Http_Collection_Resource<Gallery_Row> rows_;
|
||||
};
|
||||
}
|
||||
@@ -35,12 +35,14 @@ int Server_App::run() {
|
||||
}
|
||||
Json Server_App::amis_schema() const {
|
||||
Json tabs = Json::array();
|
||||
const Json gallery_schema = gallery_.amis_schema();
|
||||
const Json states_schema = state_resource_.amis_schema();
|
||||
tabs.push_back(Json{{"title", "组件画廊"}, {"body", gallery_schema.at("body")}});
|
||||
tabs.push_back(Json{{"title", "无线电状态"}, {"body", states_schema.at("body")}});
|
||||
tabs.push_back(Json{{"title", "无线电服务配置"}, {"body", service_config_resource_.amis_schema()}});
|
||||
tabs.push_back(Json{{"title", "告警配置"}, {"body", alert_config_resource_.amis_schema()}});
|
||||
tabs.push_back(Json{{"title", "多类型设备"}, {"body", device_tables_.amis_schema()}});
|
||||
return Json{{"type", "page"}, {"title", "Adminive 完整示例"}, {"subTitle", "列表中文映射、后端排序、拖拽顺序、多态类型表格和持久化配置均由后端定义"}, {"body", Json{{"type", "tabs"}, {"tabs", std::move(tabs)}}}};
|
||||
return Json{{"type", "page"}, {"title", "Adminive 完整示例"}, {"subTitle", "组件画廊、字段控件、组合布局、表格操作和持久化配置均由后端定义"}, {"body", Json{{"type", "tabs"}, {"tabs", std::move(tabs)}}}};
|
||||
}
|
||||
std::vector<Radio_State> Server_App::make_radio_states() {
|
||||
return {
|
||||
@@ -54,6 +56,7 @@ void Server_App::bind_routes() {
|
||||
service_config_resource_.bind(server_);
|
||||
alert_config_resource_.bind(server_);
|
||||
device_tables_.bind(server_);
|
||||
gallery_.bind(server_);
|
||||
server_.Get("/admin/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", amis_schema()}});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "device_tables.hpp"
|
||||
#include "gallery.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
@@ -20,6 +21,7 @@ private:
|
||||
Http_Resource<Radio_Service_Config> service_config_resource_;
|
||||
Http_Resource<Alert_Config> alert_config_resource_;
|
||||
Device_Table_Manager device_tables_;
|
||||
Component_Gallery gallery_;
|
||||
std::chrono::steady_clock::time_point started_at_{std::chrono::steady_clock::now()};
|
||||
std::atomic<std::uint64_t> poll_sequence_{};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "example_descriptors.hpp"
|
||||
#include "gallery.hpp"
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -25,6 +26,47 @@ const Json* find_named_node(const Json& value, std::string_view name) {
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void assert_flex_items_renderable(const Json& value) {
|
||||
if(value.is_object()) {
|
||||
if(value.contains("type") && value.at("type") == "flex") {
|
||||
assert(value.contains("items"));
|
||||
for(const auto& item : value.at("items")) {
|
||||
assert(item.is_object());
|
||||
assert(item.contains("type"));
|
||||
}
|
||||
}
|
||||
for(const auto& [key, child] : value.items()) {
|
||||
static_cast<void>(key);
|
||||
assert_flex_items_renderable(child);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(value.is_array()) {
|
||||
for(const auto& child : value) {
|
||||
assert_flex_items_renderable(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
const Json* find_labeled_node(const Json& value, std::string_view label) {
|
||||
if(value.is_object()) {
|
||||
if(value.contains("label") && value.at("label").is_string() && value.at("label").get<std::string>() == label) {
|
||||
return &value;
|
||||
}
|
||||
for(const auto& [key, child] : value.items()) {
|
||||
static_cast<void>(key);
|
||||
if(const Json* result = find_labeled_node(child, label)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if(value.is_array()) {
|
||||
for(const auto& child : value) {
|
||||
if(const Json* result = find_labeled_node(child, label)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
const Json* find_titled_node(const Json& value, std::string_view title) {
|
||||
if(value.is_object()) {
|
||||
if(value.contains("title") && value.at("title").is_string() && value.at("title").get<std::string>() == title) {
|
||||
@@ -217,6 +259,41 @@ int main() {
|
||||
assert(resource_form.at("api").at("method") == "post");
|
||||
assert(resource_form.at("api").at("url") == "/admin/config/radio/data");
|
||||
assert(resource_form.at("actions").at(1).at("label") == "确认修改");
|
||||
adminive::example::Component_Gallery gallery;
|
||||
const auto gallery_schema = gallery.amis_schema();
|
||||
assert(gallery_schema.at("type") == "panel");
|
||||
assert_flex_items_renderable(gallery_schema);
|
||||
const Json* automatic_control = find_named_node(gallery_schema, "automatic_text");
|
||||
const Json* text_control = find_named_node(gallery_schema, "text_value");
|
||||
const Json* multiline_control = find_named_node(gallery_schema, "multiline_value");
|
||||
const Json* number_control = find_named_node(gallery_schema, "number_value");
|
||||
const Json* even_control = find_named_node(gallery_schema, "even_number");
|
||||
const Json* boolean_control = find_named_node(gallery_schema, "boolean_value");
|
||||
const Json* select_control = find_named_node(gallery_schema, "select_value");
|
||||
const Json* date_gallery_control = find_named_node(gallery_schema, "date_value");
|
||||
const Json* color_gallery_control = find_named_node(gallery_schema, "color_value");
|
||||
const Json* optional_control = find_named_node(gallery_schema, "optional_text");
|
||||
const Json* readonly_control = find_named_node(gallery_schema, "read_only_text");
|
||||
assert(automatic_control && automatic_control->at("type") == "input-text");
|
||||
assert(text_control && text_control->at("type") == "input-text");
|
||||
assert(multiline_control && multiline_control->at("type") == "textarea");
|
||||
assert(number_control && number_control->at("type") == "input-number");
|
||||
assert(number_control->at("min") == 0 && number_control->at("max") == 100);
|
||||
assert(even_control && even_control->at("type") == "input-number" && even_control->at("step") == 2);
|
||||
assert(boolean_control && boolean_control->at("type") == "switch");
|
||||
assert(select_control && select_control->at("type") == "select");
|
||||
assert(date_gallery_control && date_gallery_control->at("type") == "input-date");
|
||||
assert(color_gallery_control && color_gallery_control->at("type") == "input-color");
|
||||
assert(optional_control && optional_control->at("clearable") == true);
|
||||
assert(readonly_control && readonly_control->at("type") == "static");
|
||||
const Json* save_gallery_button = find_labeled_node(gallery_schema, "保存画廊数据");
|
||||
const Json* create_gallery_button = find_labeled_node(gallery_schema, "新增一行");
|
||||
const Json* edit_gallery_button = find_labeled_node(gallery_schema, "编辑");
|
||||
const Json* delete_gallery_button = find_labeled_node(gallery_schema, "删除");
|
||||
assert(save_gallery_button && save_gallery_button->at("type") == "submit");
|
||||
assert(create_gallery_button && create_gallery_button->at("type") == "button");
|
||||
assert(edit_gallery_button && edit_gallery_button->at("actionType") == "dialog");
|
||||
assert(delete_gallery_button && delete_gallery_button->at("actionType") == "ajax");
|
||||
adminive::example::Server_Status server_status;
|
||||
server_status.server_time = "2026-08-06T00:00:00Z";
|
||||
server_status.poll_sequence = 7;
|
||||
|
||||
@@ -55,6 +55,9 @@ int main() {
|
||||
assert(form_amis.at("body").at(0).at("type") == "fieldset");
|
||||
assert(form_amis.at("body").at(1).at("type") == "grid");
|
||||
assert(form_amis.at("body").at(2).at("type") == "flex");
|
||||
assert(form_amis.at("body").at(2).at("items").at(0).at("type") == "container");
|
||||
assert(form_amis.at("body").at(2).at("items").at(1).at("type") == "container");
|
||||
assert(form_amis.at("body").at(2).at("items").at(0).at("body").at(0).at("type") == "card");
|
||||
assert(form_amis.at("body").at(3).at("type") == "tabs");
|
||||
const auto table_view = adminive::describe_table_view<Config>();
|
||||
adminive::validate_table_view<Config>(table_view);
|
||||
@@ -73,6 +76,11 @@ int main() {
|
||||
const auto composition_amis = adminive::to_amis_composition_schema<Json>(composition, {{"form", form_amis}, {"table", Json{{"type", "tpl"}, {"tpl", "table"}}}});
|
||||
assert(composition_amis.at("body").at(0).at("type") == "tpl");
|
||||
assert(composition_amis.at("body").at(1).at("type") == "grid");
|
||||
const auto flow_composition = adminive::composition_view("flow_page", adminive::compose::flow(adminive::compose::slot("left"), adminive::compose::slot("right")));
|
||||
const auto flow_amis = adminive::to_amis_composition_schema<Json>(flow_composition, {{"left", Json{{"type", "tpl"}, {"tpl", "left"}}}, {"right", Json{{"type", "tpl"}, {"tpl", "right"}}}});
|
||||
assert(flow_amis.at("body").at(0).at("type") == "flex");
|
||||
assert(flow_amis.at("body").at(0).at("items").at(0).at("type") == "container");
|
||||
assert(flow_amis.at("body").at(0).at("items").at(1).at("type") == "container");
|
||||
const auto table_amis = adminive::to_amis_table_schema<Json, Config>("/configs", table_view);
|
||||
assert(table_amis.at("type") == "page");
|
||||
assert(table_amis.at("body").at(0).at("columns").at(1).at("searchable") == true);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.app-shell {
|
||||
width: min(1100px, calc(100% - 32px));
|
||||
width: min(1320px, calc(100% - 32px));
|
||||
margin: 32px auto;
|
||||
}
|
||||
.app-header {
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function App() {
|
||||
<header className="app-header">
|
||||
<div>
|
||||
<h1>Adminive</h1>
|
||||
<p>列表、枚举、树状配置、联动条件、状态刷新和输入控件均由后端 JSON 描述驱动。</p>
|
||||
<p>组件画廊、字段控件、布局组合、表格操作、状态刷新和配置页面均由后端 JSON 描述驱动。</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => void load_schema()} disabled={loading}>重新读取后端描述</button>
|
||||
</header>
|
||||
|
||||
+26
-19
@@ -1,26 +1,33 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import {fileURLToPath, URL} from 'node:url';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
const output_dir = fileURLToPath(new URL('../frontend_dist', import.meta.url));
|
||||
const cache_dir = fileURLToPath(new URL('./node_modules/.vite-adminive', import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: cache_dir,
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
dedupe: ['mobx', 'mobx-react', 'mobx-react-lite', 'mobx-state-tree', 'react', 'react-dom']
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/admin': {
|
||||
target: 'http://127.0.0.1:9999',
|
||||
changeOrigin: true
|
||||
}
|
||||
cacheDir: cache_dir,
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
dedupe: ['mobx', 'mobx-react', 'mobx-react-lite', 'mobx-state-tree', 'react', 'react-dom']
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/admin': {
|
||||
target: 'http://127.0.0.1:9999',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: output_dir,
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: output_dir,
|
||||
emptyOutDir: true
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user