Files
my-v0-server/app/schemas/component_resource.py
T
2026-10-15 03:38:20 -07:00

212 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""前端组件目录及查询结果"""
from typing import Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
from app.schemas.base import ExtensibleModel, JsonObject, NonBlankString
from app.schemas.material_field import MISSING, FieldGroup, lookup
from app.schemas.screen import Layout, PositiveNumber
class ComponentSummary(BaseModel):
key: NonBlankString # 目录编号
type: NonBlankString # 前端实际组件类型
name: NonBlankString # 组件名称
description: NonBlankString # 组件用途
default_width: PositiveNumber # 默认宽度,单位 px
default_height: PositiveNumber # 默认高度,单位 px
class MaterialTemplate(ExtensibleModel):
type: NonBlankString
name: NonBlankString
layout: Layout
props: JsonObject = Field(default_factory=dict)
style: JsonObject | None = None
events: list[JsonObject] = Field(default_factory=list)
class EventOption(ExtensibleModel):
label: NonBlankString
value: NonBlankString = Field(description="事件类型,即节点 events[].type")
class EventGroup(ExtensibleModel):
"""多个物料共用的一组事件,物料按 key 引用;前端接口展开成 eventOptions 数组"""
key: NonBlankString
options: list[EventOption]
class ComponentDefinition(ExtensibleModel):
model_config = ConfigDict(serialize_by_alias=True)
key: NonBlankString = Field(description="目录中的唯一编号,不是 Screen 节点 ID")
type: NonBlankString = Field(
description="前端实际注册的节点 type,不根据图表名称猜测"
)
name: NonBlankString = Field(description="组件名称")
description: NonBlankString = Field(description="组件用途和支持的展示方式")
tags: list[NonBlankString] = Field(
default_factory=list, description="用于检索的分类和关键词"
)
renderer: NonBlankString = Field(description="渲染能力标识,当前从前端实现映射而来")
group: NonBlankString
icon: NonBlankString | None = None
schema_: MaterialTemplate = Field(
alias="schema", description="完整默认物料配置,不包含实例 ID"
)
field_groups: list[NonBlankString | FieldGroup] = Field(
default_factory=list,
description="字段分组:字符串引用目录的共享分组,对象为本物料独有的分组;"
"前端属性面板的 setters 由它派生,不单独保存",
)
event_groups: list[NonBlankString] = Field(
default_factory=list,
description="引用目录的共享事件组,按顺序展开成前端的 eventOptions",
)
rows_path: NonBlankString | None = Field(
default=None,
description="组件数据行(对象数组)的点路径,例如 props.option.dataset.source;"
"节点的 filters 作用在这里,后端检查按它取列;前端各物料自己知道数据行在哪",
)
data_source: bool = Field(
default=False,
description="组件可以绑定数据源(节点 dataId):绑定后运行时的数据行换成数据源的数据,不再读 rows_path;"
"前端按它决定是否显示数据源绑定。需要 rows_path",
)
usage_notes: list[NonBlankString] = Field(
default_factory=list, description="配置、数据绑定及使用限制说明"
)
def template(self) -> JsonObject:
"""默认配置的 JSON;style 为 null 表示未设置,字段默认值按路径不存在处理"""
data = self.schema_.model_dump(mode="json")
if data.get("style") is None:
data.pop("style", None)
return data
@model_validator(mode="after")
def consistent_type(self) -> Self:
if self.type != self.schema_.type:
raise ValueError("目录 type 必须与 schema.type 一致")
if "id" in self.schema_.model_extra:
raise ValueError("默认物料配置不能包含实例 ID")
if self.rows_path is not None:
try:
rows = lookup(self.template(), self.rows_segments)
except ValueError:
rows = MISSING
if not isinstance(rows, list):
raise ValueError(f"rows_path {self.rows_path} 在默认配置里不是数组")
if self.data_source and self.rows_path is None:
raise ValueError("data_source 的物料要声明 rows_path:绑定的数据源替换的是这里的数据行")
return self
@property
def rows_segments(self) -> list[str | int]:
return [
int(part) if part.isdigit() else part for part in self.rows_path.split(".")
]
def _default_problem(field, template: JsonObject) -> str | None:
"""默认值只有一个来源:默认配置有该路径时取它,没有时字段必须声明 default;
跟随主题的颜色由前端按画布主题取,默认配置里不能写死"""
try:
current = lookup(template, field.segments)
except ValueError:
return "的路径与默认配置结构冲突"
if field.themed:
return None if current is MISSING else "跟随主题,默认配置里不能写死颜色"
if field.example and current is MISSING:
return "标为示例内容,默认配置里要有这个值"
if current is MISSING:
if field.default is None:
return "在默认配置中不存在,需要声明 default"
error = field.value_error(field.default)
return f"的 default {error}" if error else None
if field.default is not None:
return "在默认配置中已有值,不能再声明 default"
error = field.value_error(current)
return f"的默认配置值 {error}" if error else None
class ComponentCatalog(ExtensibleModel):
version: NonBlankString = Field(
description="前端组件目录版本,后续用于检查是否与前端匹配"
)
components: list[ComponentDefinition]
field_groups: list[FieldGroup] = Field(
default_factory=list, description="多个物料共用的字段分组,物料按 key 引用"
)
event_groups: list[EventGroup] = Field(
default_factory=list, description="多个物料共用的事件组,物料按 key 引用"
)
def event_options(self, component: ComponentDefinition) -> list[JsonObject]:
"""物料引用的事件组按顺序展开,即前端的 eventOptions"""
groups = {group.key: group for group in self.event_groups}
return [
option.model_dump(mode="json")
for key in component.event_groups
for option in groups[key].options
]
@model_validator(mode="after")
def unique_keys(self) -> Self:
keys = [component.key for component in self.components]
if len(keys) != len(set(keys)):
raise ValueError("组件目录 key 不能重复")
shared = [group.key for group in self.field_groups]
if len(shared) != len(set(shared)):
raise ValueError("共享字段分组 key 不能重复")
event_groups = [group.key for group in self.event_groups]
if len(event_groups) != len(set(event_groups)):
raise ValueError("共享事件组 key 不能重复")
for component in self.components:
groups = [
group.key if isinstance(group, FieldGroup) else group
for group in component.field_groups
]
if len(groups) != len(set(groups)):
raise ValueError(f"物料 {component.key} 的字段分组 key 重复")
if missing := [
group
for group in component.field_groups
if isinstance(group, str) and group not in shared
]:
raise ValueError(
f"物料 {component.key} 引用了不存在的共享分组: {missing}"
)
if shadowed := [
group.key
for group in component.field_groups
if isinstance(group, FieldGroup) and group.key in shared
]:
raise ValueError(
f"物料 {component.key} 的独有分组与共享分组重名: {shadowed}"
)
if missing := [
key for key in component.event_groups if key not in event_groups
]:
raise ValueError(f"物料 {component.key} 引用了不存在的事件组: {missing}")
events = [option["value"] for option in self.event_options(component)]
if len(events) != len(set(events)):
raise ValueError(f"物料 {component.key} 的事件类型重复")
by_key = {group.key: group for group in self.field_groups}
fields = [
field
for group in component.field_groups
for field in (by_key[group] if isinstance(group, str) else group).fields
]
paths = [field.path for field in fields]
if duplicated := sorted({path for path in paths if paths.count(path) > 1}):
raise ValueError(f"物料 {component.key} 的字段路径重复: {duplicated}")
template = component.template()
for field in fields:
problem = _default_problem(field, template)
if problem:
raise ValueError(f"物料 {component.key} 字段 {field.path} {problem}")
return self