From c8b82f640ebe5c05b95641b28c8946dfeffc8fd9 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 8 Aug 2026 11:57:08 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=BB=E5=BB=8A=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 ++++++ backend/library/include/adminive/amis.hpp | 18 +++++ backend/service/CMakeLists.txt | 2 +- backend/service/src/example.hpp | 39 +++++++++++ backend/service/src/example_descriptors.hpp | 28 ++++++++ backend/service/src/gallery.cpp | 41 +++++++++++ backend/service/src/gallery.hpp | 16 +++++ backend/service/src/server_app.cpp | 5 +- backend/service/src/server_app.hpp | 2 + backend/service/tests/test.cpp | 77 +++++++++++++++++++++ backend/service/tests/view_schema_test.cpp | 8 +++ frontend/src/App.css | 2 +- frontend/src/App.tsx | 2 +- frontend/vite.config.ts | 45 +++++++----- 14 files changed, 282 insertions(+), 23 deletions(-) create mode 100644 backend/service/src/gallery.cpp create mode 100644 backend/service/src/gallery.hpp diff --git a/README.md b/README.md index fa385f6..09f2fe1 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/backend/library/include/adminive/amis.hpp b/backend/library/include/adminive/amis.hpp index 100e404..ecc559b 100644 --- a/backend/library/include/adminive/amis.hpp +++ b/backend/library/include/adminive/amis.hpp @@ -472,12 +472,21 @@ Json make_amis_view_node(const View_Node& node, const Json& descriptor_json, std Json items = json_array(); for(const auto& child : node.children) { Json item = json_object(); + json_set(item, "type", "container"); json_set(item, "body", make_amis_view_body(child, descriptor_json, permission, prefix)); + Json item_style = json_object(); + 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_set(result, "type", "flex"); + json_set(result, "direction", "row"); json_set(result, "items", std::move(items)); + Json style = json_object(); + 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(); for(const auto& child : node.children) { Json item = json_object(); + json_set(item, "type", "container"); json_set(item, "body", make_amis_composition_body(child, slots)); + Json item_style = json_object(); + 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_set(result, "type", "flex"); + json_set(result, "direction", "row"); json_set(result, "items", std::move(items)); + Json style = json_object(); + 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; } diff --git a/backend/service/CMakeLists.txt b/backend/service/CMakeLists.txt index 7ff6e22..e76cae7 100644 --- a/backend/service/CMakeLists.txt +++ b/backend/service/CMakeLists.txt @@ -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") diff --git a/backend/service/src/example.hpp b/backend/service/src/example.hpp index 7077e57..1d8cfd2 100644 --- a/backend/service/src/example.hpp +++ b/backend/service/src/example.hpp @@ -5,6 +5,8 @@ #include "adminive/adapters/nlohmann_json.hpp" #include #include +#include +#include #include #include 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 number_value{42}; + Validated_Value 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 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 score{50}; + bool enabled{true}; + std::string color{"#2563eb"}; +}; struct Serial_Device_Row { std::string port_name; Range_Value baud_rate{115200}; diff --git a/backend/service/src/example_descriptors.hpp b/backend/service/src/example_descriptors.hpp index 7a1ecd9..090dcaf 100644 --- a/backend/service/src/example_descriptors.hpp +++ b/backend/service/src/example_descriptors.hpp @@ -86,6 +86,34 @@ struct Type_View_Descriptor { } }; template <> +struct Type_Descriptor { + static auto get() { + using T = example::Gallery_Config; + return object("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("跟踪").enum_label("调试").enum_label("信息").enum_label("警告").enum_label("错误"), 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 { + static Form_View edit() { + using T = example::Gallery_Config; + return edit_form(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 { + static auto get() { + using T = example::Gallery_Row; + return object("gallery_row", ADMINIVE_FIELD_LABEL(T, name, "名称").creatable().editable().required().text_input(), ADMINIVE_FIELD_LABEL(T, category, "分类").creatable().editable().required().select_input().enum_label("主要").enum_label("次要").enum_label("实验"), 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 { + static Table_View table() { + using T = example::Gallery_Row; + return table_view(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 { static auto get() { using T = example::Application_Config; diff --git a/backend/service/src/gallery.cpp b/backend/service/src/gallery.cpp new file mode 100644 index 0000000..ccd0588 --- /dev/null +++ b/backend/service/src/gallery.cpp @@ -0,0 +1,41 @@ +#include "gallery.hpp" +#include +#include +#include +namespace adminive::example { +namespace { +Json make_gallery_note(std::string title, std::string text) { + return Json{{"type", "tpl"}, {"tpl", "" + std::move(title) + "
" + std::move(text) + "
"}}; +} +} +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 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(composition, slots); +} +void Component_Gallery::bind(httplib::Server& server) { + config_resource_.bind(server); + rows_.bind(server); +} +std::vector Component_Gallery::make_rows() { + return { + Gallery_Row{"Alpha", Gallery_Category::primary, Range_Value{92}, true, "#2563eb"}, + Gallery_Row{"Beta", Gallery_Category::secondary, Range_Value{76}, true, "#16a34a"}, + Gallery_Row{"Gamma", Gallery_Category::experimental, Range_Value{58}, false, "#d97706"} + }; +} +} diff --git a/backend/service/src/gallery.hpp b/backend/service/src/gallery.hpp new file mode 100644 index 0000000..a4944bf --- /dev/null +++ b/backend/service/src/gallery.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "example_descriptors.hpp" +#include +namespace adminive::example { +class Component_Gallery { +public: + Component_Gallery(); + Json amis_schema() const; + void bind(httplib::Server& server); +private: + static std::vector make_rows(); + Managed_Value config_; + Http_Resource config_resource_; + Http_Collection_Resource rows_; +}; +} diff --git a/backend/service/src/server_app.cpp b/backend/service/src/server_app.cpp index f376bb3..d0f365e 100644 --- a/backend/service/src/server_app.cpp +++ b/backend/service/src/server_app.cpp @@ -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 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()}}); }); diff --git a/backend/service/src/server_app.hpp b/backend/service/src/server_app.hpp index 8bccc46..ca2dd9a 100644 --- a/backend/service/src/server_app.hpp +++ b/backend/service/src/server_app.hpp @@ -1,5 +1,6 @@ #pragma once #include "device_tables.hpp" +#include "gallery.hpp" #include #include #include @@ -20,6 +21,7 @@ private: Http_Resource service_config_resource_; Http_Resource 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 poll_sequence_{}; }; diff --git a/backend/service/tests/test.cpp b/backend/service/tests/test.cpp index 07be628..cac641a 100644 --- a/backend/service/tests/test.cpp +++ b/backend/service/tests/test.cpp @@ -1,4 +1,5 @@ #include "example_descriptors.hpp" +#include "gallery.hpp" #include #include #include @@ -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(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() == label) { + return &value; + } + for(const auto& [key, child] : value.items()) { + static_cast(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() == 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; diff --git a/backend/service/tests/view_schema_test.cpp b/backend/service/tests/view_schema_test.cpp index 308f6fd..1258f9e 100644 --- a/backend/service/tests/view_schema_test.cpp +++ b/backend/service/tests/view_schema_test.cpp @@ -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(); adminive::validate_table_view(table_view); @@ -73,6 +76,11 @@ int main() { const auto composition_amis = adminive::to_amis_composition_schema(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(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("/configs", table_view); assert(table_amis.at("type") == "page"); assert(table_amis.at("body").at(0).at("columns").at(1).at("searchable") == true); diff --git a/frontend/src/App.css b/frontend/src/App.css index d6b862a..b5e7dbf 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,5 +1,5 @@ .app-shell { - width: min(1100px, calc(100% - 32px)); + width: min(1320px, calc(100% - 32px)); margin: 32px auto; } .app-header { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 34eb3f1..9500ced 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,7 +33,7 @@ export default function App() {

Adminive

-

列表、枚举、树状配置、联动条件、状态刷新和输入控件均由后端 JSON 描述驱动。

+

组件画廊、字段控件、布局组合、表格操作、状态刷新和配置页面均由后端 JSON 描述驱动。

diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 6c45813..4830bcc 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -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 - } -}); +}); \ No newline at end of file