mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-28 16:20:56 +08:00
✨ feat(page-template): 添加页面模板服务,支持前端布局组件和数据提交处理
- 新增页面模板服务模块,提供页面模板配置、字段定义和数据验证功能。 - 实现了前端布局组件模型,包括行、列、文本、按钮、卡片等,支持灵活的页面布局。 - 引入 FastAPI 路由,提供统一的API接口以获取模板配置和处理数据提交。 - 注册用户表单模板示例,包含提交、重置和取消按钮的功能。 - 增强了数据验证和处理逻辑,确保提交数据的有效性和安全性。
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||||
|
from nonebot_plugin_uninfo import Uninfo
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from zhenxun.configs.utils import PluginExtraData
|
||||||
|
from zhenxun.services.log import logger
|
||||||
|
from zhenxun.services.page_template import PageTemplateConfig, template_manager
|
||||||
|
from zhenxun.services.page_template.components import (
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
Col,
|
||||||
|
ColProps,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormItemProps,
|
||||||
|
FormProps,
|
||||||
|
Row,
|
||||||
|
RowProps,
|
||||||
|
)
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="web测试",
|
||||||
|
description="想要更加了解真寻吗",
|
||||||
|
usage="""
|
||||||
|
指令:
|
||||||
|
关于
|
||||||
|
""".strip(),
|
||||||
|
extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").to_dict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_matcher = on_alconna(Alconna("test"), priority=5, block=True, rule=to_me())
|
||||||
|
|
||||||
|
|
||||||
|
@_matcher.handle()
|
||||||
|
async def _(session: Uninfo, arparma: Arparma):
|
||||||
|
logger.info("1")
|
||||||
|
|
||||||
|
|
||||||
|
def temp(a: dict[str, Any]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserFormData(BaseModel):
|
||||||
|
username: str = Field(..., min_length=3, max_length=20)
|
||||||
|
email: str
|
||||||
|
age: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def register_user_form_template():
|
||||||
|
# 使用 list[Any] 避免 list 协变导致的类型告警
|
||||||
|
layout: list[Any] = [
|
||||||
|
Row(
|
||||||
|
props=RowProps(gutter=16),
|
||||||
|
children=[
|
||||||
|
Col(
|
||||||
|
props=ColProps(span=12),
|
||||||
|
children=[
|
||||||
|
Form(
|
||||||
|
props=FormProps(label_width="100px", inline=True),
|
||||||
|
children=[
|
||||||
|
FormItem(
|
||||||
|
props=FormItemProps(
|
||||||
|
label="用户名", prop="username"
|
||||||
|
),
|
||||||
|
children=None,
|
||||||
|
bind_field="username",
|
||||||
|
),
|
||||||
|
FormItem(
|
||||||
|
props=FormItemProps(label="邮箱", prop="email"),
|
||||||
|
children=None,
|
||||||
|
bind_field="email",
|
||||||
|
),
|
||||||
|
FormItem(
|
||||||
|
props=FormItemProps(label="年龄", prop="age"),
|
||||||
|
children=None,
|
||||||
|
bind_field="age",
|
||||||
|
),
|
||||||
|
FormItem(
|
||||||
|
props=FormItemProps(label=""),
|
||||||
|
children=[
|
||||||
|
Button(
|
||||||
|
props=ButtonProps(
|
||||||
|
text="提交",
|
||||||
|
type="primary",
|
||||||
|
action="submit",
|
||||||
|
confirm=True,
|
||||||
|
confirm_text="确认提交吗?",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
props=ButtonProps(
|
||||||
|
text="重置",
|
||||||
|
type="default",
|
||||||
|
action="reset", # 前端重置表单
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
props=ButtonProps(
|
||||||
|
text="取消",
|
||||||
|
type="danger",
|
||||||
|
action="cancel", # 前端自行关闭/返回
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bind_field=None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
config = PageTemplateConfig(
|
||||||
|
template_id="user_form",
|
||||||
|
title="用户表单示例",
|
||||||
|
description="包含提交/重置/取消按钮的示例表单",
|
||||||
|
layout=layout,
|
||||||
|
callback_handler=temp,
|
||||||
|
)
|
||||||
|
|
||||||
|
template_manager.register(config, data_model=UserFormData)
|
||||||
@@ -7,6 +7,7 @@ Zhenxun Bot - 核心服务模块
|
|||||||
- LLM服务 (llm): 提供与大语言模型交互的统一API。
|
- LLM服务 (llm): 提供与大语言模型交互的统一API。
|
||||||
- 插件生命周期管理 (plugin_init): 支持插件安装和卸载时的钩子函数。
|
- 插件生命周期管理 (plugin_init): 支持插件安装和卸载时的钩子函数。
|
||||||
- 定时任务调度器 (scheduler): 提供持久化的、可管理的定时任务服务。
|
- 定时任务调度器 (scheduler): 提供持久化的、可管理的定时任务服务。
|
||||||
|
- 页面模板服务 (page_template_service): 用于构建前端页面(表格、表单等)并处理数据提交。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from nonebot import require
|
from nonebot import require
|
||||||
@@ -44,6 +45,15 @@ from .llm import (
|
|||||||
set_global_default_model_name,
|
set_global_default_model_name,
|
||||||
)
|
)
|
||||||
from .log import logger
|
from .log import logger
|
||||||
|
from .page_template import (
|
||||||
|
ColumnAlign,
|
||||||
|
FieldConfig,
|
||||||
|
FieldType,
|
||||||
|
PageTemplateConfig,
|
||||||
|
PageTemplateManager,
|
||||||
|
PageTemplateService,
|
||||||
|
template_manager,
|
||||||
|
)
|
||||||
from .plugin_init import PluginInit, PluginInitManager
|
from .plugin_init import PluginInit, PluginInitManager
|
||||||
from .renderer import renderer_service
|
from .renderer import renderer_service
|
||||||
from .scheduler import (
|
from .scheduler import (
|
||||||
@@ -56,13 +66,19 @@ from .scheduler import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"AI",
|
"AI",
|
||||||
"AIConfig",
|
"AIConfig",
|
||||||
|
"ColumnAlign",
|
||||||
"CommonOverrides",
|
"CommonOverrides",
|
||||||
"ExecutionPolicy",
|
"ExecutionPolicy",
|
||||||
|
"FieldConfig",
|
||||||
|
"FieldType",
|
||||||
"LLMContentPart",
|
"LLMContentPart",
|
||||||
"LLMException",
|
"LLMException",
|
||||||
"LLMGenerationConfig",
|
"LLMGenerationConfig",
|
||||||
"LLMMessage",
|
"LLMMessage",
|
||||||
"Model",
|
"Model",
|
||||||
|
"PageTemplateConfig",
|
||||||
|
"PageTemplateManager",
|
||||||
|
"PageTemplateService",
|
||||||
"PluginInit",
|
"PluginInit",
|
||||||
"PluginInitManager",
|
"PluginInitManager",
|
||||||
"ScheduleContext",
|
"ScheduleContext",
|
||||||
@@ -86,5 +102,6 @@ __all__ = [
|
|||||||
"scheduler_manager",
|
"scheduler_manager",
|
||||||
"search",
|
"search",
|
||||||
"set_global_default_model_name",
|
"set_global_default_model_name",
|
||||||
|
"template_manager",
|
||||||
"with_db_timeout",
|
"with_db_timeout",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
页面模板服务模块
|
||||||
|
|
||||||
|
提供页面模板配置、字段定义和数据验证功能。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .components import (
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
Card,
|
||||||
|
CardProps,
|
||||||
|
Col,
|
||||||
|
ColProps,
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormItemProps,
|
||||||
|
FormProps,
|
||||||
|
Row,
|
||||||
|
RowProps,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
TableProps,
|
||||||
|
Text,
|
||||||
|
TextProps,
|
||||||
|
)
|
||||||
|
from .service import PageTemplateConfig, PageTemplateManager, PageTemplateService
|
||||||
|
|
||||||
|
# 创建全局模板管理器实例
|
||||||
|
template_manager = PageTemplateManager()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Button",
|
||||||
|
"ButtonProps",
|
||||||
|
"Card",
|
||||||
|
"CardProps",
|
||||||
|
"Col",
|
||||||
|
"ColProps",
|
||||||
|
"Component",
|
||||||
|
"ComponentType",
|
||||||
|
"Divider",
|
||||||
|
"Form",
|
||||||
|
"FormItem",
|
||||||
|
"FormItemProps",
|
||||||
|
"FormProps",
|
||||||
|
"PageTemplateConfig",
|
||||||
|
"PageTemplateManager",
|
||||||
|
"PageTemplateService",
|
||||||
|
"Row",
|
||||||
|
"RowProps",
|
||||||
|
"Space",
|
||||||
|
"Table",
|
||||||
|
"TableProps",
|
||||||
|
"Text",
|
||||||
|
"TextProps",
|
||||||
|
"template_manager",
|
||||||
|
]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
前端布局组件模型集合
|
||||||
|
|
||||||
|
用于以数据形式描述页面布局,标签与前端 Element 组件保持一致:
|
||||||
|
- el-row, el-col
|
||||||
|
- el-text
|
||||||
|
- el-button
|
||||||
|
- el-card, el-divider, el-space, el-form, el-form-item, el-table
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .layout import (
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
Card,
|
||||||
|
CardProps,
|
||||||
|
Col,
|
||||||
|
ColProps,
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormItemProps,
|
||||||
|
FormProps,
|
||||||
|
Row,
|
||||||
|
RowProps,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
TableProps,
|
||||||
|
Text,
|
||||||
|
TextProps,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Button",
|
||||||
|
"ButtonProps",
|
||||||
|
"Card",
|
||||||
|
"CardProps",
|
||||||
|
"Col",
|
||||||
|
"ColProps",
|
||||||
|
"Component",
|
||||||
|
"ComponentType",
|
||||||
|
"Divider",
|
||||||
|
"Form",
|
||||||
|
"FormItem",
|
||||||
|
"FormItemProps",
|
||||||
|
"FormProps",
|
||||||
|
"Row",
|
||||||
|
"RowProps",
|
||||||
|
"Space",
|
||||||
|
"Table",
|
||||||
|
"TableProps",
|
||||||
|
"Text",
|
||||||
|
"TextProps",
|
||||||
|
]
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentType(str, Enum):
|
||||||
|
"""组件类型,名称与前端 tag 保持一致"""
|
||||||
|
|
||||||
|
ROW = "row"
|
||||||
|
COL = "col"
|
||||||
|
TEXT = "text"
|
||||||
|
BUTTON = "button"
|
||||||
|
CARD = "card"
|
||||||
|
DIVIDER = "divider"
|
||||||
|
SPACE = "space"
|
||||||
|
FORM = "form"
|
||||||
|
FORM_ITEM = "form_item"
|
||||||
|
TABLE = "table"
|
||||||
|
|
||||||
|
|
||||||
|
class RowProps(BaseModel):
|
||||||
|
"""行组件属性"""
|
||||||
|
|
||||||
|
gutter: int | None = None # 行间距
|
||||||
|
justify: str | None = None # 主轴对齐方式
|
||||||
|
align: str | None = None # 交叉轴对齐方式
|
||||||
|
|
||||||
|
|
||||||
|
class ColProps(BaseModel):
|
||||||
|
"""列组件属性"""
|
||||||
|
|
||||||
|
span: int | None = None # 栅格占比
|
||||||
|
offset: int | None = None # 左侧偏移
|
||||||
|
push: int | None = None # 向右移动
|
||||||
|
pull: int | None = None # 向左移动
|
||||||
|
|
||||||
|
|
||||||
|
class TextProps(BaseModel):
|
||||||
|
"""文本组件属性"""
|
||||||
|
|
||||||
|
content: str = "" # 文本内容
|
||||||
|
tag: str | None = None # HTML 标签,如 h1/h2/p/span
|
||||||
|
type: str | None = None # 文本类型,对应 el-text 的 type
|
||||||
|
size: str | None = None # 文本大小
|
||||||
|
truncated: bool = False # 是否截断
|
||||||
|
line_clamp: int | None = None # 最多显示行数
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonProps(BaseModel):
|
||||||
|
"""按钮组件属性"""
|
||||||
|
|
||||||
|
text: str # 按钮文本(必填)
|
||||||
|
type: str | None = None # 按钮类型 primary/success/warning/danger/info/default
|
||||||
|
size: str | None = None # 按钮尺寸 large/default/small
|
||||||
|
plain: bool = False # 朴素按钮
|
||||||
|
round: bool = False # 圆角按钮
|
||||||
|
circle: bool = False # 圆形按钮
|
||||||
|
link: bool = False # 文字按钮
|
||||||
|
icon: str | None = None # 图标名称
|
||||||
|
action: str | None = None # 按钮行为:submit/reset/cancel/custom
|
||||||
|
confirm: bool = False # 是否需要二次确认
|
||||||
|
confirm_text: str | None = None # 确认提示文案
|
||||||
|
api: str | None = None # 自定义调用的后端 API(action=custom 时使用)
|
||||||
|
api_method: str = "POST" # 自定义 API 的 HTTP 方法
|
||||||
|
|
||||||
|
|
||||||
|
class CardProps(BaseModel):
|
||||||
|
"""卡片组件属性"""
|
||||||
|
|
||||||
|
header: str | None = None # 卡片标题
|
||||||
|
shadow: str | None = None # 阴影类型
|
||||||
|
body_style: dict[str, Any] | None = None # 卡片主体样式
|
||||||
|
|
||||||
|
|
||||||
|
class FormProps(BaseModel):
|
||||||
|
"""表单组件属性"""
|
||||||
|
|
||||||
|
label_width: str | None = None # 标签宽度
|
||||||
|
inline: bool = False # 行内表单
|
||||||
|
size: str | None = None # 表单尺寸
|
||||||
|
|
||||||
|
|
||||||
|
class FormItemProps(BaseModel):
|
||||||
|
"""表单项组件属性"""
|
||||||
|
|
||||||
|
label: str | None = None # 标签文本
|
||||||
|
prop: str | None = None # 绑定字段名
|
||||||
|
required: bool = False # 是否必填
|
||||||
|
|
||||||
|
|
||||||
|
class TableProps(BaseModel):
|
||||||
|
"""表格组件属性"""
|
||||||
|
|
||||||
|
columns: list[dict[str, Any]] | None = None # 列配置
|
||||||
|
data: list[dict[str, Any]] | None = None # 数据源
|
||||||
|
|
||||||
|
|
||||||
|
class BaseComponent(BaseModel):
|
||||||
|
# 这些字段在 BaseModel 中有默认值,调用时都不是必传
|
||||||
|
# 用 Any 以便可以直接传 TextProps / ButtonProps / FormProps 等模型实例
|
||||||
|
props: Any | None
|
||||||
|
# 用 Any 避免 list 协变问题
|
||||||
|
children: list[Any] | None = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
def __init__(self, **data: Any):
|
||||||
|
super().__init__(**data)
|
||||||
|
_validate_children_impl(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_children_impl(values: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
限定哪些组件可以拥有 children:
|
||||||
|
- 允许 children: row, col, card, space, form, form_item, table
|
||||||
|
- 不允许 children: text, button, divider
|
||||||
|
"""
|
||||||
|
t = values.get("type")
|
||||||
|
children = values.get("children") or []
|
||||||
|
no_children = {
|
||||||
|
ComponentType.TEXT.value,
|
||||||
|
ComponentType.BUTTON.value,
|
||||||
|
ComponentType.DIVIDER.value,
|
||||||
|
}
|
||||||
|
if t in no_children and children:
|
||||||
|
raise ValueError(f"组件 '{t}' 不允许包含 children")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class Row(BaseComponent):
|
||||||
|
"""行组件(对应 el-row)"""
|
||||||
|
|
||||||
|
type = ComponentType.ROW.value
|
||||||
|
props: RowProps # 必填
|
||||||
|
|
||||||
|
|
||||||
|
class Col(BaseComponent):
|
||||||
|
"""列组件(对应 el-col)"""
|
||||||
|
|
||||||
|
type = ComponentType.COL.value
|
||||||
|
props: ColProps # 必填
|
||||||
|
|
||||||
|
|
||||||
|
class Text(BaseComponent):
|
||||||
|
"""文本组件(对应 el-text 或基础标签)"""
|
||||||
|
|
||||||
|
type = ComponentType.TEXT.value
|
||||||
|
props: TextProps # 必填
|
||||||
|
|
||||||
|
|
||||||
|
class Button(BaseComponent):
|
||||||
|
"""按钮组件(对应 el-button)"""
|
||||||
|
|
||||||
|
type = ComponentType.BUTTON.value
|
||||||
|
props: ButtonProps # 必填
|
||||||
|
|
||||||
|
|
||||||
|
class Card(BaseComponent):
|
||||||
|
"""卡片组件(对应 el-card)"""
|
||||||
|
|
||||||
|
type = ComponentType.CARD.value
|
||||||
|
props: CardProps # 必填
|
||||||
|
|
||||||
|
|
||||||
|
class Divider(BaseComponent):
|
||||||
|
"""分割线组件(对应 el-divider)"""
|
||||||
|
|
||||||
|
type = ComponentType.DIVIDER.value
|
||||||
|
|
||||||
|
|
||||||
|
class Space(BaseComponent):
|
||||||
|
"""间距组件(对应 el-space)"""
|
||||||
|
|
||||||
|
type = ComponentType.SPACE.value
|
||||||
|
|
||||||
|
|
||||||
|
class Form(BaseComponent):
|
||||||
|
"""表单组件(对应 el-form)"""
|
||||||
|
|
||||||
|
type = ComponentType.FORM.value
|
||||||
|
props: FormProps # 必填
|
||||||
|
|
||||||
|
|
||||||
|
class FormItem(BaseComponent):
|
||||||
|
"""表单项组件(对应 el-form-item)"""
|
||||||
|
|
||||||
|
type = ComponentType.FORM_ITEM.value
|
||||||
|
props: FormItemProps # 必填
|
||||||
|
children: list[Any] | None = None
|
||||||
|
bind_field: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Table(BaseComponent):
|
||||||
|
"""表格组件(对应 el-table)"""
|
||||||
|
|
||||||
|
type = ComponentType.TABLE.value
|
||||||
|
|
||||||
|
|
||||||
|
Component = Row | Col | Text | Button | Card | Divider | Space | Form | FormItem | Table
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
BaseComponent.model_rebuild()
|
||||||
|
except AttributeError:
|
||||||
|
BaseComponent.update_forward_refs()
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
页面模板服务 FastAPI 路由
|
||||||
|
|
||||||
|
提供统一的API接口,通过template_id来获取配置和处理数据提交。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Body, Depends, Query
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from zhenxun.builtin_plugins.web_ui.base_model import Result
|
||||||
|
from zhenxun.builtin_plugins.web_ui.utils import authentication
|
||||||
|
from zhenxun.services.log import logger
|
||||||
|
from zhenxun.services.page_template import template_manager
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/page_template")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/template_config",
|
||||||
|
dependencies=[Depends(authentication())],
|
||||||
|
response_model=Result[dict],
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="获取页面模板配置(表格配置)",
|
||||||
|
)
|
||||||
|
async def get_template_config(template_id: str = Query(..., description="模板ID")):
|
||||||
|
"""获取页面模板配置"""
|
||||||
|
try:
|
||||||
|
config = template_manager.get_template_config(template_id)
|
||||||
|
if config is None:
|
||||||
|
return Result.fail(f"模板ID '{template_id}' 不存在")
|
||||||
|
return Result.ok(config, "获取配置成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{router.prefix}/template_config 调用错误", "PageTemplate", e=e)
|
||||||
|
return Result.fail(f"获取配置失败: {type(e)}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/form_config",
|
||||||
|
dependencies=[Depends(authentication())],
|
||||||
|
response_model=Result[dict],
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="获取表单配置",
|
||||||
|
)
|
||||||
|
async def get_form_config(template_id: str = Query(..., description="模板ID")):
|
||||||
|
"""获取表单配置"""
|
||||||
|
try:
|
||||||
|
config = template_manager.get_form_config(template_id)
|
||||||
|
if config is None:
|
||||||
|
return Result.fail(f"模板ID '{template_id}' 不存在")
|
||||||
|
return Result.ok(config, "获取表单配置成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{router.prefix}/form_config 调用错误", "PageTemplate", e=e)
|
||||||
|
return Result.fail(f"获取表单配置失败: {type(e)}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/submit",
|
||||||
|
dependencies=[Depends(authentication())],
|
||||||
|
response_model=Result,
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="提交数据",
|
||||||
|
)
|
||||||
|
async def submit_data(
|
||||||
|
template_id: str = Query(..., description="模板ID"),
|
||||||
|
data: dict = Body(..., description="提交的数据"),
|
||||||
|
):
|
||||||
|
"""处理数据提交"""
|
||||||
|
try:
|
||||||
|
success, message, result = await template_manager.process_submit(
|
||||||
|
template_id, data
|
||||||
|
)
|
||||||
|
if not success:
|
||||||
|
return Result.fail(message)
|
||||||
|
return Result.ok(info=message, data=result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{router.prefix}/submit 调用错误", "PageTemplate", e=e)
|
||||||
|
return Result.fail(f"提交数据失败: {type(e)}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/list",
|
||||||
|
dependencies=[Depends(authentication())],
|
||||||
|
response_model=Result[list[str]],
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="获取所有已注册的模板ID列表",
|
||||||
|
)
|
||||||
|
async def list_templates():
|
||||||
|
"""获取所有已注册的模板ID列表"""
|
||||||
|
try:
|
||||||
|
template_ids = template_manager.list_templates()
|
||||||
|
return Result.ok(template_ids, "获取模板列表成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{router.prefix}/list 调用错误", "PageTemplate", e=e)
|
||||||
|
return Result.fail(f"获取模板列表失败: {type(e)}: {e}")
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"""
|
||||||
|
页面模板服务
|
||||||
|
|
||||||
|
用于构建前端页面(如表格、表单等),支持字段绑定和数据提交处理。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
|
from zhenxun.services.log import logger
|
||||||
|
from zhenxun.services.page_template.components import Component
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
class PageTemplateConfig(BaseModel):
|
||||||
|
"""页面模板配置"""
|
||||||
|
|
||||||
|
template_id: str = Field(..., description="模板ID")
|
||||||
|
"""模板ID"""
|
||||||
|
title: str = Field(..., description="页面标题")
|
||||||
|
"""页面标题"""
|
||||||
|
description: str | None = Field(None, description="页面描述")
|
||||||
|
"""页面描述"""
|
||||||
|
callback_handler: Callable[[dict[str, Any]], Any] | None = Field(
|
||||||
|
None,
|
||||||
|
description="数据提交后的回调处理方法(异步或同步函数,接收验证后的数据字典)",
|
||||||
|
)
|
||||||
|
"""数据提交后的回调处理方法(异步或同步函数,接收验证后的数据字典)"""
|
||||||
|
layout: list[Component] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="页面布局组件树,使用 row/col/text/button 等组件描述前端结构",
|
||||||
|
)
|
||||||
|
"""页面布局组件树"""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
class PageTemplateService(Generic[T]):
|
||||||
|
"""页面模板服务"""
|
||||||
|
|
||||||
|
def __init__(self, config: PageTemplateConfig, data_model: type[T] | None = None):
|
||||||
|
"""
|
||||||
|
初始化页面模板服务
|
||||||
|
|
||||||
|
参数:
|
||||||
|
config: 页面模板配置
|
||||||
|
data_model: 数据模型类(可选,用于数据验证)
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
self.data_model = data_model
|
||||||
|
|
||||||
|
def get_table_config(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取表格配置(用于前端渲染表格)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
包含表格配置的字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"template_id": self.config.template_id,
|
||||||
|
"title": self.config.title,
|
||||||
|
"description": self.config.description,
|
||||||
|
"layout": self.get_layout_config(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_config(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取表单配置(用于前端渲染表单)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
包含表单配置的字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"template_id": self.config.template_id,
|
||||||
|
"title": self.config.title,
|
||||||
|
"description": self.config.description,
|
||||||
|
"layout": self.get_layout_config(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_layout_config(self) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取页面布局配置(组件树)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
布局组件的列表(字典形式,适合前端直接渲染)
|
||||||
|
"""
|
||||||
|
from zhenxun.utils.pydantic_compat import model_dump
|
||||||
|
|
||||||
|
return [model_dump(node, exclude_none=True) for node in self.config.layout]
|
||||||
|
|
||||||
|
def validate_data(self, data: dict[str, Any]) -> tuple[bool, str | None, T | None]:
|
||||||
|
"""
|
||||||
|
验证提交的数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 待验证的数据字典
|
||||||
|
|
||||||
|
返回:
|
||||||
|
元组 (是否有效, 错误信息, 验证后的数据模型实例)
|
||||||
|
"""
|
||||||
|
# 如果提供了数据模型,使用Pydantic验证
|
||||||
|
if self.data_model:
|
||||||
|
try:
|
||||||
|
validated_data = self.data_model(**data)
|
||||||
|
return True, None, validated_data
|
||||||
|
except ValidationError as e:
|
||||||
|
error_messages = []
|
||||||
|
for error in e.errors():
|
||||||
|
field_name = ".".join(str(loc) for loc in error["loc"])
|
||||||
|
error_messages.append(f"{field_name}: {error['msg']}")
|
||||||
|
return False, "; ".join(error_messages), None
|
||||||
|
|
||||||
|
return True, None, None
|
||||||
|
|
||||||
|
def process_submit_data(
|
||||||
|
self, data: dict[str, Any]
|
||||||
|
) -> tuple[bool, str, dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
处理提交的数据(验证并返回处理后的数据)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: 提交的数据字典
|
||||||
|
|
||||||
|
返回:
|
||||||
|
元组 (是否成功, 消息, 处理后的数据)
|
||||||
|
"""
|
||||||
|
is_valid, error_msg, validated_model = self.validate_data(data)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
return False, error_msg or "数据验证失败", {}
|
||||||
|
|
||||||
|
# 如果验证成功且有模型,返回模型数据
|
||||||
|
if validated_model:
|
||||||
|
from zhenxun.utils.pydantic_compat import model_dump
|
||||||
|
|
||||||
|
return True, "数据验证成功", model_dump(validated_model)
|
||||||
|
|
||||||
|
# 否则返回原始数据(已通过验证)
|
||||||
|
return True, "数据验证成功", data
|
||||||
|
|
||||||
|
|
||||||
|
class PageTemplateManager:
|
||||||
|
"""页面模板管理器(全局单例)"""
|
||||||
|
|
||||||
|
_instance: "PageTemplateManager | None" = None
|
||||||
|
_templates: dict[str, PageTemplateService[Any]]
|
||||||
|
|
||||||
|
def __new__(cls) -> "PageTemplateManager":
|
||||||
|
"""单例模式"""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._templates = {}
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def register(
|
||||||
|
self,
|
||||||
|
config: PageTemplateConfig,
|
||||||
|
data_model: type[T] | None = None,
|
||||||
|
) -> PageTemplateService[T]:
|
||||||
|
"""
|
||||||
|
注册页面模板
|
||||||
|
|
||||||
|
参数:
|
||||||
|
config: 页面模板配置
|
||||||
|
data_model: 数据模型类(可选,用于数据验证)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
PageTemplateService实例
|
||||||
|
|
||||||
|
异常:
|
||||||
|
ValueError: 如果template_id已存在
|
||||||
|
"""
|
||||||
|
if config.template_id in self._templates:
|
||||||
|
raise ValueError(
|
||||||
|
f"模板ID '{config.template_id}' 已存在,请使用不同的template_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
service = PageTemplateService(config, data_model)
|
||||||
|
self._templates[config.template_id] = service
|
||||||
|
logger.info(f"已注册页面模板: {config.template_id} - {config.title}")
|
||||||
|
return service
|
||||||
|
|
||||||
|
def get(self, template_id: str) -> PageTemplateService[Any] | None:
|
||||||
|
"""
|
||||||
|
获取页面模板服务
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template_id: 模板ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
PageTemplateService实例,如果不存在则返回None
|
||||||
|
"""
|
||||||
|
return self._templates.get(template_id)
|
||||||
|
|
||||||
|
def unregister(self, template_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
注销页面模板
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template_id: 模板ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否成功注销
|
||||||
|
"""
|
||||||
|
if template_id in self._templates:
|
||||||
|
del self._templates[template_id]
|
||||||
|
logger.info(f"已注销页面模板: {template_id}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_templates(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
列出所有已注册的模板ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
模板ID列表
|
||||||
|
"""
|
||||||
|
return list(self._templates.keys())
|
||||||
|
|
||||||
|
def get_template_config(self, template_id: str) -> dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
获取模板配置(表格配置)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template_id: 模板ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
表格配置字典,如果模板不存在则返回None
|
||||||
|
"""
|
||||||
|
service = self.get(template_id)
|
||||||
|
return service.get_table_config() if service else None
|
||||||
|
|
||||||
|
def get_form_config(self, template_id: str) -> dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
获取表单配置
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template_id: 模板ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
表单配置字典,如果模板不存在则返回None
|
||||||
|
"""
|
||||||
|
service = self.get(template_id)
|
||||||
|
return service.get_form_config() if service else None
|
||||||
|
|
||||||
|
async def process_submit(
|
||||||
|
self, template_id: str, data: dict[str, Any]
|
||||||
|
) -> tuple[bool, str, dict[str, Any] | None]:
|
||||||
|
"""
|
||||||
|
处理数据提交
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template_id: 模板ID
|
||||||
|
data: 提交的数据字典
|
||||||
|
|
||||||
|
返回:
|
||||||
|
元组 (是否成功, 消息, 处理后的数据或回调结果)
|
||||||
|
"""
|
||||||
|
service = self.get(template_id)
|
||||||
|
if not service:
|
||||||
|
return False, f"模板ID '{template_id}' 不存在", None
|
||||||
|
|
||||||
|
# 验证数据
|
||||||
|
success, message, processed_data = service.process_submit_data(data)
|
||||||
|
if not success:
|
||||||
|
return False, message, None
|
||||||
|
|
||||||
|
# 如果有回调处理器,执行回调
|
||||||
|
if service.config.callback_handler:
|
||||||
|
try:
|
||||||
|
result = await self._call_handler(
|
||||||
|
service.config.callback_handler, processed_data
|
||||||
|
)
|
||||||
|
return True, message, result
|
||||||
|
except Exception as e:
|
||||||
|
handler_name = getattr(
|
||||||
|
service.config.callback_handler, "__name__", "unknown"
|
||||||
|
)
|
||||||
|
logger.error(
|
||||||
|
f"执行回调处理器失败: {handler_name}",
|
||||||
|
"PageTemplate",
|
||||||
|
e=e,
|
||||||
|
)
|
||||||
|
return False, f"执行回调处理器失败: {e!s}", None
|
||||||
|
|
||||||
|
return True, message, processed_data
|
||||||
|
|
||||||
|
async def _call_handler(
|
||||||
|
self, handler: Callable[[dict[str, Any]], Any], data: dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
调用回调处理器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
handler: 回调处理函数
|
||||||
|
data: 要传递的数据
|
||||||
|
|
||||||
|
返回:
|
||||||
|
处理器的返回值
|
||||||
|
"""
|
||||||
|
if not callable(handler):
|
||||||
|
raise ValueError("回调处理器必须是一个可调用对象")
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
# 检查是否是异步函数
|
||||||
|
if inspect.iscoroutinefunction(handler):
|
||||||
|
return await handler(data)
|
||||||
|
else:
|
||||||
|
return handler(data)
|
||||||
Reference in New Issue
Block a user