feat:新增物料
This commit is contained in:
@@ -9,8 +9,8 @@ from app.schemas.screen import Canvas, Screen
|
||||
|
||||
class UpdateCanvasHandler(BaseActionHandler[UpdateCanvasAction]):
|
||||
def apply(self, screen: Screen, action: UpdateCanvasAction) -> Screen:
|
||||
# 将现有画布转成字典,保留动作没有指定的字段
|
||||
properties = screen.canvas.model_dump()
|
||||
# 将现有画布转成字典,保留动作没有指定的字段;没写过的主题、背景色不补成 null
|
||||
properties = screen.canvas.model_dump(exclude_unset=True)
|
||||
# 顶层字段整体替换,嵌套对象不做深度合并
|
||||
properties.update(deepcopy(action.properties))
|
||||
# 校验合并后的画布属性,尺寸等字段不合法时直接抛出异常
|
||||
|
||||
+103
-28
@@ -1,7 +1,9 @@
|
||||
"""apply_actions 里的 $query:把字段值写成 {"$query": "SELECT ..."},提交时替换成上传数据的查询结果
|
||||
|
||||
模型不用把查询结果逐行抄进动作,又慢又容易抄错;替换发生在 run_batch 之前,替换后照常做字段校验
|
||||
结果是对象数组,键是查询结果的列名,正好对应图表物料的 dataset.source;series 的 encode 按列名引用
|
||||
结果的形状按写入字段的定义:单个值的字段(指标卡的数值等)取一行一列的那个值,元素是数字、文字的数组
|
||||
(指标卡的趋势数据)取唯一一列;其余是对象数组,键是查询结果的列名,正好对应图表物料的 dataset.source,
|
||||
series 的 encode 按列名引用
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -12,42 +14,64 @@ from pydantic import JsonValue
|
||||
from app.actions.batch import BatchError
|
||||
from app.agent.data_tools import DataContext
|
||||
from app.config import settings
|
||||
from app.resources.material_fields import find_material, resolve_field_groups
|
||||
from app.schemas.actions import AddAction, AgentAction, UpdateAction
|
||||
from app.schemas.component_resource import ComponentCatalog
|
||||
from app.schemas.material_field import MaterialField
|
||||
from app.schemas.screen import Screen
|
||||
|
||||
QUERY_KEY = "$query"
|
||||
# 图表物料的数据都在这里,series 的 encode 按它的列名引用;整个换成查询结果时数据就不再是示例数据
|
||||
SOURCE_PATH = "props.option.dataset.source"
|
||||
# 字段是这些类型时,$query 的结果取一行一列的值
|
||||
SCALAR_TYPES = {"string", "number", "integer", "boolean", "color", "enum"}
|
||||
# 数组元素是这些类型时,$query 的结果取唯一一列
|
||||
LIST_ITEM_TYPES = {"string", "number", "integer", "boolean", "color"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmbeddedQuery:
|
||||
"""一处替换:第几个动作、写到哪个字段、结果的列和行数,写进 apply_actions 的结果供模型核对"""
|
||||
"""一处替换:第几个动作、写到哪个字段、结果的列和行数,写进 apply_actions 的结果供模型核对
|
||||
|
||||
写入单个值的字段时 value 是取到的值,模型据此核对数字、在回复里引用
|
||||
"""
|
||||
|
||||
index: int
|
||||
node_id: str
|
||||
path: str
|
||||
columns: list[str]
|
||||
rows: int
|
||||
value: JsonValue = None
|
||||
|
||||
|
||||
Found = list[EmbeddedQuery]
|
||||
|
||||
|
||||
def fill_queries(
|
||||
actions: list[AgentAction], data: DataContext
|
||||
actions: list[AgentAction],
|
||||
data: DataContext,
|
||||
catalog: ComponentCatalog | None = None,
|
||||
screen: Screen | None = None,
|
||||
) -> tuple[list[AgentAction], list[EmbeddedQuery]]:
|
||||
"""替换 add 的 node 和 update 的 value 里所有的 $query;查询失败抛 BatchError,整批不生效
|
||||
|
||||
catalog 和 screen 用来查写入字段的定义,决定结果的形状;没有时都按对象数组
|
||||
add 用了 $query 又没写 sample_data 时设为 false:数据来自用户上传的文件,不是示例数据
|
||||
"""
|
||||
# update 的目标可能是这一批前面新增的组件,类型按执行顺序记下
|
||||
types = {node.id: node.type for node in screen.nodes} if screen else {}
|
||||
filled: list[AgentAction] = []
|
||||
embedded: list[EmbeddedQuery] = []
|
||||
for index, action in enumerate(actions):
|
||||
found: list[tuple[str, list[str], int]] = []
|
||||
node_id = "" # 只有 add 和 update 有字段值,其他动作不会有替换
|
||||
found: Found = []
|
||||
try:
|
||||
if isinstance(action, AddAction):
|
||||
types[action.node.id] = action.node.type
|
||||
node = _fill(
|
||||
action.node.model_dump(mode="json", exclude_unset=True),
|
||||
data,
|
||||
_Target(
|
||||
data, _fields(catalog, action.node.type), index, action.node.id
|
||||
),
|
||||
"",
|
||||
found,
|
||||
)
|
||||
@@ -56,49 +80,70 @@ def fill_queries(
|
||||
fields["node"] = node
|
||||
fields.setdefault("sample_data", False)
|
||||
action = AddAction.model_validate(fields)
|
||||
node_id = action.node.id
|
||||
elif isinstance(action, UpdateAction):
|
||||
value = _fill(action.value, data, action.field_path, found)
|
||||
target = _Target(
|
||||
data,
|
||||
_fields(catalog, types.get(action.target_id)),
|
||||
index,
|
||||
action.target_id,
|
||||
)
|
||||
value = _fill(action.value, target, action.field_path, found)
|
||||
if found:
|
||||
action = action.model_copy(update={"value": value})
|
||||
node_id = action.target_id
|
||||
except ValueError as error: # 含 QueryError
|
||||
raise BatchError(index, action, str(error)) from error
|
||||
embedded.extend(
|
||||
EmbeddedQuery(index, node_id, path, columns, rows)
|
||||
for path, columns, rows in found
|
||||
)
|
||||
embedded.extend(found)
|
||||
filled.append(action)
|
||||
return filled, embedded
|
||||
|
||||
|
||||
def _fill(
|
||||
value: JsonValue,
|
||||
data: DataContext,
|
||||
path: str,
|
||||
found: list[tuple[str, list[str], int]],
|
||||
) -> JsonValue:
|
||||
@dataclass(frozen=True)
|
||||
class _Target:
|
||||
"""一个动作的替换上下文:查询用的数据、组件的字段定义、动作序号和组件 ID"""
|
||||
|
||||
data: DataContext
|
||||
fields: dict[str, MaterialField]
|
||||
index: int
|
||||
node_id: str
|
||||
|
||||
|
||||
def _fields(
|
||||
catalog: ComponentCatalog | None, node_type: str | None
|
||||
) -> dict[str, MaterialField]:
|
||||
"""组件类型的字段定义,按路径索引;没有目录或类型不认识时为空,结果按对象数组处理"""
|
||||
if catalog is None or node_type is None:
|
||||
return {}
|
||||
try:
|
||||
component = find_material(catalog, node_type)
|
||||
except ValueError: # 物料不存在,补全或执行时会报错
|
||||
return {}
|
||||
return {
|
||||
field.path: field
|
||||
for group in resolve_field_groups(catalog, component)
|
||||
for field in group.fields
|
||||
}
|
||||
|
||||
|
||||
def _fill(value: JsonValue, target: _Target, path: str, found: Found) -> JsonValue:
|
||||
if isinstance(value, dict):
|
||||
if QUERY_KEY in value:
|
||||
return _query(value, data, path, found)
|
||||
return _query(value, target, path, found)
|
||||
return {
|
||||
key: _fill(item, data, _join(path, key), found)
|
||||
key: _fill(item, target, _join(path, key), found)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [
|
||||
_fill(item, data, _join(path, position), found)
|
||||
_fill(item, target, _join(path, position), found)
|
||||
for position, item in enumerate(value)
|
||||
]
|
||||
return value
|
||||
|
||||
|
||||
def _query(
|
||||
value: dict[str, Any],
|
||||
data: DataContext,
|
||||
path: str,
|
||||
found: list[tuple[str, list[str], int]],
|
||||
) -> list[dict[str, Any]]:
|
||||
value: dict[str, Any], target: _Target, path: str, found: Found
|
||||
) -> JsonValue:
|
||||
data = target.data
|
||||
sql = value[QUERY_KEY]
|
||||
if len(value) != 1 or not isinstance(sql, str):
|
||||
raise ValueError(
|
||||
@@ -118,7 +163,37 @@ def _query(
|
||||
raise ValueError(
|
||||
f"{path} 的 $query 结果有重名的列 {'、'.join(duplicated)},用 AS 起不同的别名"
|
||||
)
|
||||
found.append((path, result.columns, len(result.rows)))
|
||||
shape = (
|
||||
f"{len(result.rows)} 行 {len(result.columns)} 列({'、'.join(result.columns)})"
|
||||
)
|
||||
field = target.fields.get(path)
|
||||
if field is not None and field.value_type in SCALAR_TYPES:
|
||||
if len(result.rows) != 1 or len(result.columns) != 1:
|
||||
raise ValueError(
|
||||
f"{path}({field.label})是单个值,$query 的结果要一行一列,实际 {shape}"
|
||||
)
|
||||
scalar = result.rows[0][0]
|
||||
found.append(
|
||||
EmbeddedQuery(target.index, target.node_id, path, result.columns, 1, scalar)
|
||||
)
|
||||
return scalar
|
||||
listed = (
|
||||
field is not None
|
||||
and field.value_type == "array"
|
||||
and field.item_type in LIST_ITEM_TYPES
|
||||
)
|
||||
if listed and len(result.columns) != 1:
|
||||
raise ValueError(
|
||||
f"{path}({field.label})是一组值,$query 的结果要只有一列,实际 {shape};"
|
||||
"排序用的列不用选出来,写在 ORDER BY 里"
|
||||
)
|
||||
found.append(
|
||||
EmbeddedQuery(
|
||||
target.index, target.node_id, path, result.columns, len(result.rows)
|
||||
)
|
||||
)
|
||||
if listed:
|
||||
return [row[0] for row in result.rows]
|
||||
return [dict(zip(result.columns, row, strict=True)) for row in result.rows]
|
||||
|
||||
|
||||
|
||||
+9
-3
@@ -131,9 +131,13 @@ def _result_text(
|
||||
lines.append("$query 已替换为查询结果:")
|
||||
for item in queries:
|
||||
note = "(结果为空,组件会没有数据)" if not item.rows else ""
|
||||
result = (
|
||||
f"取值 {item.value}"
|
||||
if item.value is not None
|
||||
else f"{item.rows} 行{note},列 {'、'.join(item.columns)}"
|
||||
)
|
||||
lines.append(
|
||||
f"- 第 {item.index + 1} 个动作 {item.node_id} 的 {item.path}:"
|
||||
f"{item.rows} 行{note},列 {'、'.join(item.columns)}"
|
||||
f"- 第 {item.index + 1} 个动作 {item.node_id} 的 {item.path}:{result}"
|
||||
)
|
||||
|
||||
old_layouts = {node.id: node.layout for node in before.nodes}
|
||||
@@ -246,7 +250,9 @@ def apply_actions(
|
||||
else ""
|
||||
)
|
||||
try:
|
||||
actions, queries = fill_queries(actions, runtime.context.data)
|
||||
actions, queries = fill_queries(
|
||||
actions, runtime.context.data, runtime.context.catalog, before
|
||||
)
|
||||
completed, after = run_batch(before, runtime.context.catalog, actions)
|
||||
# 这一批改过数据或映射的组件都核对,不论数据来自这一批的 $query、之前的提交还是直接写入
|
||||
problems = encode_problems(after, rebound_nodes(before, after))
|
||||
|
||||
+15
-10
@@ -15,20 +15,24 @@ def request_summary(context: InputContext) -> JsonObject:
|
||||
"""用户输入、澄清记录、编辑上下文、附件、数据文件和大屏摘要"""
|
||||
nodes: list[JsonObject] = []
|
||||
for node in context.screen.nodes:
|
||||
# 图表标题帮助判断用户指的是哪个组件;没有 option 或标题格式不符时按无标题处理
|
||||
# 画面上的标题帮助判断用户指的是哪个组件:图表在 option.title,指标卡在 props.title;
|
||||
# 格式不符时按无标题处理
|
||||
option = node.props.get("option")
|
||||
title = option.get("title") if isinstance(option, dict) else None
|
||||
titles = title if isinstance(title, list) else [title]
|
||||
texts = [
|
||||
item["text"]
|
||||
for item in titles
|
||||
if isinstance(item, dict) and isinstance(item.get("text"), str)
|
||||
]
|
||||
if isinstance(node.props.get("title"), str) and node.props["title"]:
|
||||
texts.append(node.props["title"])
|
||||
nodes.append(
|
||||
{
|
||||
"id": node.id,
|
||||
"type": node.type,
|
||||
"name": node.name,
|
||||
"titles": [
|
||||
item["text"]
|
||||
for item in titles
|
||||
if isinstance(item, dict) and isinstance(item.get("text"), str)
|
||||
],
|
||||
"titles": texts,
|
||||
"layout": node.layout.model_dump(mode="json", exclude_unset=True),
|
||||
}
|
||||
)
|
||||
@@ -38,10 +42,11 @@ def request_summary(context: InputContext) -> JsonObject:
|
||||
"clarification_history": context.clarification_history,
|
||||
"context": context.context.model_dump(mode="json", exclude_unset=True),
|
||||
"screen_summary": {
|
||||
"canvas": {
|
||||
"width": context.screen.canvas.width,
|
||||
"height": context.screen.canvas.height,
|
||||
},
|
||||
"canvas": context.screen.canvas.model_dump(
|
||||
mode="json",
|
||||
include={"width", "height", "theme", "backgroundColor"},
|
||||
exclude_none=True,
|
||||
),
|
||||
"nodes": nodes,
|
||||
# 数据源只保留标识、名称和类型
|
||||
"data_sources": [
|
||||
|
||||
@@ -28,7 +28,7 @@ def resolve_field_groups(
|
||||
|
||||
|
||||
def field_default(component: ComponentDefinition, field: MaterialField) -> JsonValue:
|
||||
"""物料默认配置中有该路径时取默认配置的值,否则取字段声明的 default"""
|
||||
"""物料默认配置中有该路径时取默认配置的值,否则取字段声明的 default;跟随主题的颜色为 None"""
|
||||
value = lookup(component.template(), field.segments)
|
||||
return field.default if value is MISSING else value
|
||||
|
||||
@@ -54,6 +54,8 @@ def panel_setters(groups: list[FieldGroup]) -> list[dict]:
|
||||
}
|
||||
if field.span is not None:
|
||||
setter["span"] = field.span
|
||||
if field.themed:
|
||||
setter["themed"] = True
|
||||
setters.append(setter)
|
||||
return setters
|
||||
|
||||
@@ -126,11 +128,14 @@ def describe_field(
|
||||
"""给模型看的字段说明:省略空值和属性面板专用的信息"""
|
||||
data = field.model_dump(
|
||||
mode="json",
|
||||
exclude={"default", "panel", "span", "editable"},
|
||||
exclude={"default", "panel", "span", "editable", "themed"},
|
||||
exclude_none=True,
|
||||
exclude_defaults=True,
|
||||
)
|
||||
data["default"] = field_default(component, field)
|
||||
if field.themed:
|
||||
# 默认为 null:没单独设置时画面上按画布主题取色
|
||||
data["follows_theme"] = True
|
||||
if not field.editable:
|
||||
data["editable"] = False
|
||||
data["group"] = group.key
|
||||
|
||||
@@ -16,7 +16,7 @@ class AddAction(ActionModel):
|
||||
description="新组件。id、type、layout 必填,其余只写需要覆盖物料默认配置的字段;"
|
||||
"后端按 type 找到物料,用默认配置补全:对象逐层覆盖,数组整体替换,null 明确覆盖。"
|
||||
"id 不能与当前大屏上的组件重复(包括之前提交里新增的),先 delete 的 ID 可以再用;type 用 list_components 返回的 type。"
|
||||
'任何字段值都可以写成 {"$query": "SELECT ..."},后端替换成上传数据的查询结果(对象数组,键是结果的列名)'
|
||||
'任何字段值都可以写成 {"$query": "SELECT ..."},后端替换成上传数据的查询结果:写入单个值的字段时结果要一行一列,取那个值;写入数字、文字数组的字段时结果要只有一列;其余是对象数组,键是结果的列名'
|
||||
)
|
||||
sample_data: bool = Field(
|
||||
default=False,
|
||||
|
||||
@@ -23,7 +23,7 @@ class UpdateAction(BaseAction):
|
||||
)
|
||||
value: JsonValue = Field(
|
||||
description="替换该字段的值,必须符合字段定义的类型和取值;"
|
||||
'可以写成 {"$query": "SELECT ..."},后端替换成上传数据的查询结果(对象数组,键是结果的列名)'
|
||||
'可以写成 {"$query": "SELECT ..."},后端替换成上传数据的查询结果:写入单个值的字段时结果要一行一列,取那个值;写入数字、文字数组的字段时结果要只有一列;其余是对象数组,键是结果的列名'
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
||||
@@ -15,5 +15,6 @@ class UpdateCanvasAction(ActionModel):
|
||||
type: Literal["update_canvas"] = "update_canvas"
|
||||
properties: dict[NonBlankString, JsonValue] = Field(
|
||||
min_length=1,
|
||||
description="要替换的 canvas 顶层字段及最终值,如 width、height、backgroundColor;未提供的字段保持不变。改变画布尺寸不会自动缩放或移动组件",
|
||||
description="要替换的 canvas 顶层字段及最终值,如 width、height、theme(light 亮色 / dark 深色)、backgroundColor;未提供的字段保持不变。"
|
||||
"改 theme 会让组件里跟随主题的颜色一起变;backgroundColor 写 null 表示用主题的背景色。改变画布尺寸不会自动缩放或移动组件",
|
||||
)
|
||||
|
||||
@@ -74,11 +74,14 @@ class ComponentDefinition(ExtensibleModel):
|
||||
|
||||
|
||||
def _default_problem(field, template: JsonObject) -> str | None:
|
||||
"""默认值只有一个来源:默认配置有该路径时取它,没有时字段必须声明 default"""
|
||||
"""默认值只有一个来源:默认配置有该路径时取它,没有时字段必须声明 default;
|
||||
跟随主题的颜色由前端按画布主题取,默认配置里不能写死"""
|
||||
try:
|
||||
current = lookup(template, field.segments)
|
||||
except ValueError:
|
||||
return "的路径与默认配置结构冲突"
|
||||
if field.themed:
|
||||
return None if current is MISSING else "跟随主题,默认配置里不能写死颜色"
|
||||
if current is MISSING:
|
||||
if field.default is None:
|
||||
return "在默认配置中不存在,需要声明 default"
|
||||
|
||||
@@ -20,10 +20,11 @@ ValueType = Literal[
|
||||
ItemType = Literal["string", "number", "integer", "boolean", "color", "object"]
|
||||
|
||||
# 值类型对应的前端属性控件;array / object 没有通用控件,只能不在面板显示
|
||||
# number 可以输入小数,integer 只能输入整数
|
||||
PANEL_CONTROLS: dict[str, str] = {
|
||||
"string": "input",
|
||||
"number": "number",
|
||||
"integer": "number",
|
||||
"integer": "integer",
|
||||
"boolean": "checkbox",
|
||||
"color": "color",
|
||||
"enum": "select",
|
||||
@@ -101,6 +102,10 @@ class MaterialField(ExtensibleModel):
|
||||
aliases: list[NonBlankString] = Field(
|
||||
default_factory=list, description="用户可能的叫法,供字段搜索使用"
|
||||
)
|
||||
themed: bool = Field(
|
||||
default=False,
|
||||
description="颜色跟随画布主题:默认配置里不写值,由前端按画布主题(亮 / 暗)取色;写入 null 恢复跟随",
|
||||
)
|
||||
editable: bool = Field(default=True, description="是否允许修改")
|
||||
panel: bool = Field(default=True, description="是否显示在前端属性面板")
|
||||
span: int | None = Field(default=None, description="属性面板栅格宽度,24 为整行")
|
||||
@@ -115,10 +120,15 @@ class MaterialField(ExtensibleModel):
|
||||
raise ValueError(f"只有 array 字段提供 item_type,且 array 必须提供: {self.path}")
|
||||
if self.panel and self.value_type not in PANEL_CONTROLS:
|
||||
raise ValueError(f"{self.value_type} 字段没有属性面板控件,panel 必须为 false: {self.path}")
|
||||
if self.themed and (self.value_type != "color" or self.default is not None):
|
||||
raise ValueError(f"只有 color 字段可以跟随主题,且不能声明 default: {self.path}")
|
||||
return self
|
||||
|
||||
def value_error(self, value: JsonValue) -> str | None:
|
||||
"""检查值是否符合字段类型和取值约束,符合返回 None"""
|
||||
# 跟随主题的颜色写 null 表示去掉单独设置的颜色、恢复跟随
|
||||
if self.themed and value is None:
|
||||
return None
|
||||
if self.value_type == "enum":
|
||||
allowed = [option.value for option in self.options]
|
||||
return None if value in allowed else f"取值必须是 {allowed} 之一"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""大屏公共结构;组件属性、事件与数据源配置保持开放"""
|
||||
|
||||
from typing import Annotated, Self
|
||||
from typing import Annotated, Literal, Self
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
@@ -13,6 +13,13 @@ PositiveNumber = Annotated[Number, Field(gt=0)]
|
||||
class Canvas(ExtensibleModel):
|
||||
width: PositiveNumber
|
||||
height: PositiveNumber
|
||||
theme: Literal["light", "dark"] | None = Field(
|
||||
default=None,
|
||||
description="画布主题:组件里没单独设置的文字、坐标轴、分隔线、卡片底等颜色按主题取;旧大屏没有时前端按背景色推断",
|
||||
)
|
||||
backgroundColor: str | None = Field(
|
||||
default=None, description="画布背景色;为 null 时用主题的背景色"
|
||||
)
|
||||
|
||||
|
||||
class Layout(ExtensibleModel):
|
||||
|
||||
@@ -35,7 +35,8 @@ from app.services.thumbnails import ThumbnailStorage
|
||||
SCRIPT_MAX_CHARS = 64 * 1024
|
||||
COPY_SUFFIX = " 副本"
|
||||
EMPTY_PAGE = {
|
||||
"canvas": {"width": 1920, "height": 1080, "backgroundColor": "#ffffff10"},
|
||||
# 不设背景色:用主题的背景色,组件颜色也跟着主题走
|
||||
"canvas": {"width": 1920, "height": 1080, "theme": "light"},
|
||||
"nodes": [],
|
||||
"dataSource": [],
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ describe_data 为准
|
||||
- 多个系列(例如每月线上、线下各一根柱子)用宽表,每个系列一列:长表(每行一个类别和数值)用 PIVOT(要写 IN 列出取值)或 SUM
|
||||
(CASE WHEN ...) 转成宽表;反过来宽表转长表用 UNPIVOT
|
||||
- 日期按需要的粒度转成文字,例如 strftime (月份, '%Y-%m'),坐标轴上不出现完整的时间
|
||||
- 指标卡、单个数值不用 $query:先用 query_data 查出来,再直接写数字
|
||||
- 指标卡(stat-card)的数值、对比的变化量和迷你趋势线的数据(trend.data)也写 $query:数值和变化量的查询结果要一行一列,
|
||||
环比、同比在 SQL 里算好;趋势数据只选数值一列,按时间 ORDER BY
|
||||
- 用户要分析结论时,加一个文本组件写结论,只写查询结果支持的内容
|
||||
- 用了 $query 的新增组件不用写 sample_data,后端会设为 false;标题按数据的内容写
|
||||
|
||||
## 数据不合适时
|
||||
|
||||
@@ -10,15 +10,19 @@ description: 新建大屏,或在大屏上新增组件、整体重新布局、
|
||||
1. 用户说明了图表类型时,直接用 get_component_materials 查看默认配置;只有不确定选哪种时才用 list_components 查看可用组件,按用户要展示的内容选类型;不编造目录中没有的组件类型
|
||||
2. 需要的查询(get_component_materials、get_nodes 等)互不依赖,在同一轮一起调用
|
||||
3. 新建大屏时,根据主题安排一个标题文本和若干图表;图表标题按要展示的内容写,不沿用物料的示例标题
|
||||
4. 销售额、订单数、转化率这类关键数字用指标卡(stat-card),一个指标一张卡,不用文本拼数字
|
||||
|
||||
## 布局
|
||||
|
||||
- 位置和尺寸参考物料的默认尺寸,按画布大小安排;新组件不与已有组件重叠
|
||||
- 画布上没有空位时,可以移动或缩小已有组件给新组件腾出位置,不改它们的内容
|
||||
- 多个组件对齐排列,之间留出间距
|
||||
- 有指标卡时排在标题下方一行,同一行的指标卡等宽等高;图表放在指标卡下方
|
||||
- 用户没有指定的位置、尺寸、标题和数据采用合理默认
|
||||
|
||||
## 整体调整
|
||||
|
||||
- 重新布局:用 arrange、align、distribute、set_layout 排列已有组件,不删除、不重建;只调整位置和尺寸不需要 get_nodes,大屏摘要里已有每个组件的 layout 和画布尺寸
|
||||
- 统一风格:先用 get_nodes 读取组件配置,用 search_fields 找到颜色等字段,再逐个修改;画布背景用 update_canvas
|
||||
- 换亮色 / 深色风格:用 update_canvas 改 theme(light / dark)。组件里标了 follows_theme 的颜色(文字、坐标轴、分隔线、卡片底等)没单独设置时会跟着主题变,不用逐个改;画布 backgroundColor 设过的写 null 改用主题背景,除非用户指定了背景色
|
||||
- 换主题后,用 get_nodes 检查组件上单独设置过的跟随主题字段(值不是 null),和新主题不协调的写 null 恢复跟随;系列颜色、涨跌颜色等不跟主题的颜色按用户要求修改
|
||||
- 其他统一风格(换配色、统一字号等):用 get_nodes 读取组件配置,用 search_fields 找到字段,再逐个修改
|
||||
|
||||
@@ -18,6 +18,7 @@ description: 修改大屏上已有的组件或画布:改标题、文字、颜
|
||||
一次查多个分组
|
||||
3. 只用查到的字段路径,值要符合字段的类型、可选值和取值范围;查不到对应字段说明物料不支持这项修改,按无法完成处理,不自行编造路径
|
||||
4. 需要基于当前值修改时(如"字号调大一点"),先用 get_nodes 读取当前配置
|
||||
5. 字段标了 follows_theme 时,没单独设置(值为 null)表示按画布主题取色;用户要"恢复默认颜色""跟主题走"时写 null
|
||||
|
||||
## 位置和尺寸
|
||||
|
||||
|
||||
Reference in New Issue
Block a user