feat:图表交互
This commit is contained in:
@@ -10,6 +10,7 @@ from .delete import DeleteAction
|
||||
from .layout import AlignAction, ArrangeAction, DistributeAction, SetLayoutAction
|
||||
from .move import MoveAction
|
||||
from .resize import ResizeAction
|
||||
from .set_interaction import SetInteractionAction
|
||||
from .update import UpdateAction
|
||||
from .update_canvas import UpdateCanvasAction
|
||||
|
||||
@@ -20,7 +21,8 @@ Action = Annotated[
|
||||
| MoveAction
|
||||
| ResizeAction
|
||||
| UpdateAction
|
||||
| UpdateCanvasAction,
|
||||
| UpdateCanvasAction
|
||||
| SetInteractionAction,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
@@ -33,6 +35,7 @@ AgentAction = Annotated[
|
||||
| ResizeAction
|
||||
| UpdateAction
|
||||
| UpdateCanvasAction
|
||||
| SetInteractionAction
|
||||
| ArrangeAction
|
||||
| AlignAction
|
||||
| DistributeAction
|
||||
@@ -53,6 +56,7 @@ __all__ = [
|
||||
"DeleteAction",
|
||||
"MoveAction",
|
||||
"ResizeAction",
|
||||
"SetInteractionAction",
|
||||
"UpdateAction",
|
||||
"UpdateCanvasAction",
|
||||
]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""新增组件动作"""
|
||||
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from app.schemas.actions.base import ActionModel
|
||||
from app.schemas.screen import ScreenNode
|
||||
from app.schemas.screen import ScreenNode, json_equal
|
||||
|
||||
|
||||
class AddAction(ActionModel):
|
||||
@@ -14,11 +14,43 @@ class AddAction(ActionModel):
|
||||
type: Literal["add"] = "add"
|
||||
node: ScreenNode = Field(
|
||||
description="新组件。id、type、layout 必填,其余只写需要覆盖物料默认配置的字段;"
|
||||
"显示条件、数据筛选、事件(visibleWhen、filters、events)也写在 node 里,不写在动作这一层,也可以之后用 set_interaction 设置;"
|
||||
"后端按 type 找到物料,用默认配置补全:对象逐层覆盖,数组整体替换,null 明确覆盖。"
|
||||
"写出的字段和 update 一样按字段定义检查类型和取值(跟随主题的颜色可以写 null)。"
|
||||
"id 不能与当前大屏上的组件重复(包括之前提交里新增的),先 delete 的 ID 可以再用;type 用 list_components 返回的 type。"
|
||||
'任何字段值都可以写成 {"$query": "SELECT ..."},后端替换成上传数据的查询结果:写入单个值的字段时结果要一行一列,取那个值;写入数字、文字数组的字段时结果要只有一列;其余是对象数组,键是结果的列名'
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def node_fields_into_node(cls, data: Any) -> Any:
|
||||
"""动作这一层写了节点字段时挪进 node:模型常照 set_interaction 的样子把 visibleWhen、filters、events
|
||||
写在 node 旁边,严格校验只报"Extra inputs are not permitted",它看不懂,反复提交直到放弃。
|
||||
按 ScreenNode 的字段判断,不列举;交付的动作里这些字段只在 node 里,前端协议不变;其他未知键照样报错
|
||||
"""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("node"), dict):
|
||||
return data
|
||||
misplaced = [
|
||||
key
|
||||
for key in data
|
||||
if key in ScreenNode.model_fields and key not in cls.model_fields
|
||||
]
|
||||
if not misplaced:
|
||||
return data
|
||||
data = dict(data)
|
||||
node = dict(data["node"])
|
||||
for key in misplaced:
|
||||
value = data.pop(key)
|
||||
# 按 JSON 比较:Python 的 != 把 true 和 1 当成一样,会让外层的值静默覆盖
|
||||
if key not in node:
|
||||
node[key] = value
|
||||
elif not json_equal(node[key], value):
|
||||
raise ValueError(
|
||||
f"{key} 是组件的字段,同时写在 node 里和动作这一层且值不同;只写在 node 里"
|
||||
)
|
||||
data["node"] = node
|
||||
return data
|
||||
|
||||
sample_data: bool = Field(
|
||||
default=False,
|
||||
description="true 表示组件展示的是自己编写的数据(不是来自上传的文件或大屏上已有的组件),交付时标为示例数据。"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""设置组件的交互:显示条件、数据筛选、事件;变量本身在画布上,用 update_canvas 写 variables"""
|
||||
|
||||
from typing import Literal, Self
|
||||
|
||||
from pydantic import Field, model_serializer, model_validator
|
||||
|
||||
from app.schemas.base import ExtensibleModel, NonBlankString
|
||||
from app.schemas.screen import EventAction, RowFilter, VisibleWhen
|
||||
|
||||
from .base import BaseAction
|
||||
|
||||
# 这三个键只处理写出的:没写保持原样,写 null 删除节点上的该字段
|
||||
INTERACTION_KEYS = ("visibleWhen", "filters", "events")
|
||||
|
||||
|
||||
class NodeEvent(ExtensibleModel):
|
||||
"""组件事件;写变量的事件只写 type 和 action,保留已有的代码事件时把它原样带上"""
|
||||
|
||||
type: NonBlankString = Field(
|
||||
description="事件类型,取物料 eventOptions 里的值:click 点击组件;itemClick 点击图表的某个数据项(只有图表有)"
|
||||
)
|
||||
action: EventAction | None = Field(
|
||||
default=None, description="写变量;有 action 时不执行事件代码"
|
||||
)
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def without_empty_action(self, handler):
|
||||
# 代码事件没有 action,不补出 null
|
||||
data = handler(self)
|
||||
if self.action is None:
|
||||
data.pop("action", None)
|
||||
return data
|
||||
|
||||
|
||||
class SetInteractionAction(BaseAction):
|
||||
"""设置组件的显示条件、数据筛选和事件。只改写出的键,整体替换该键;写 null 删除该键;至少写一个键
|
||||
|
||||
变量要先在画布 variables 里声明(update_canvas),同一批里先后顺序不限,整批执行完再检查引用
|
||||
"""
|
||||
|
||||
type: Literal["set_interaction"] = "set_interaction"
|
||||
visibleWhen: VisibleWhen | None = Field(
|
||||
default=None,
|
||||
description="变量等于 equals 时才显示该组件,用于整页切换;同一变量取不同值的组件不会同时出现,可以放在同一位置",
|
||||
)
|
||||
filters: list[RowFilter] | None = Field(
|
||||
default=None,
|
||||
description="按变量筛选组件的数据行,多条为且,变量为 null 时不过滤;只用于有数据行的组件(图表的 dataset.source、指标卡的 rows),"
|
||||
"field 必须是数据行里的列。被筛选的图表的数据要带上这一列,查询时不要对它加 WHERE",
|
||||
)
|
||||
events: list[NodeEvent] | None = Field(
|
||||
default=None,
|
||||
description="组件的全部事件,整体替换:组件上已有的代码事件要保留时原样带上。"
|
||||
'例如 [{"type": "click", "action": {"setVar": "page", "value": "stock"}}]、'
|
||||
'[{"type": "itemClick", "action": {"setVar": "model", "fromField": "model"}}]',
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def some_key(self) -> Self:
|
||||
if not self.model_fields_set & set(INTERACTION_KEYS):
|
||||
raise ValueError(f"至少写出 {'、'.join(INTERACTION_KEYS)} 中的一个")
|
||||
return self
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def written_keys_only(self, handler):
|
||||
# 交给前端时 null 表示删除,没写的键不能序列化成 null;检查点按序列化结果重建,也靠它保留"写了哪些键"
|
||||
data = handler(self)
|
||||
for key in INTERACTION_KEYS:
|
||||
if key not in self.model_fields_set:
|
||||
data.pop(key, None)
|
||||
return data
|
||||
@@ -16,5 +16,7 @@ class UpdateCanvasAction(ActionModel):
|
||||
properties: dict[NonBlankString, JsonValue] = Field(
|
||||
min_length=1,
|
||||
description="要替换的 canvas 顶层字段及最终值,如 width、height、theme(light 亮色 / dark 深色)、backgroundColor;未提供的字段保持不变。"
|
||||
"改 theme 会让组件里跟随主题的颜色一起变;backgroundColor 写 null 表示用主题的背景色。改变画布尺寸不会自动缩放或移动组件",
|
||||
"改 theme 会让组件里跟随主题的颜色一起变;backgroundColor 写 null 表示用主题的背景色。改变画布尺寸不会自动缩放或移动组件。"
|
||||
'variables 是大屏变量的完整列表(整组替换,保留已有的),例如 [{"id": "page", "name": "当前页", "default": "sales"}];'
|
||||
"组件怎么读写变量用 set_interaction 设置",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user