feat:接入mcp,为模型提供图表信息

This commit is contained in:
2026-10-01 06:33:44 -07:00
parent 618290d852
commit f1b2a70ad1
44 changed files with 6531 additions and 207 deletions
+2 -2
View File
@@ -165,9 +165,9 @@ def plan_materials(state: QueryState) -> list[dict]:
if not types:
return []
# 按类型提取全部匹配物料,使用协议别名输出 schema 等字段
# 按类型提取全部匹配物料,使用协议别名输出 schema 等字段;字段定义由查询工具按需提供
return [
item.model_dump(mode="json", by_alias=True)
item.model_dump(mode="json", by_alias=True, exclude={"field_groups"})
for item in state["component_resources"].components
if item.type in types
]
+35 -15
View File
@@ -3,9 +3,37 @@
from copy import deepcopy
from app.actions.base import BaseActionHandler
from app.resources.material_fields import editable_fields, known_prop_keys
from app.schemas.actions import UpdateAction
from app.schemas.screen import Screen
from app.schemas.screen import Screen, ScreenNode
def fit_existing_path(node: ScreenNode, action: UpdateAction) -> UpdateAction:
"""字段定义允许的路径在组件中可能还没有中间对象(例如未开启过的数值标签)
前端只按已有路径赋值,这里把缺失的部分折叠进值里:改为更新最近的已有对象,
值为补齐的嵌套对象;缺失的若是数组元素则无法补齐,保持原样交给执行时报错
"""
if action.section == "name":
return action
current = getattr(node, action.section) or {}
for index, segment in enumerate(action.path[:-1]):
if isinstance(current, dict) and isinstance(segment, str) and segment not in current:
rest = action.path[index + 1 :]
if not all(isinstance(item, str) for item in rest):
return action
value = action.value
for item in reversed(rest):
value = {item: value}
return action.model_copy(
update={"path": action.path[: index + 1], "value": value}
)
if isinstance(current, dict) and isinstance(segment, str):
current = current[segment]
elif isinstance(current, list) and type(segment) is int and segment < len(current):
current = current[segment]
else:
return action
return action
class UpdateHandler(BaseActionHandler[UpdateAction]):
@@ -13,23 +41,15 @@ class UpdateHandler(BaseActionHandler[UpdateAction]):
# 确认目标存在,后续路径访问和赋值均在大屏副本中进行
result, target = self.copy_target(screen, action.target_id)
# 组件名称是节点顶层字段,没有后续路径
if action.section == "name":
target.name = action.value
return result
# 未设置的 style 视为空对象,只允许直接添加末级字段
if action.section == "style" and target.style is None:
target.style = {}
# props 第一层不能新造字段:渲染器不会读取约定外的字段,新造只会“成功”却没有效果
if action.section == "props":
known = known_prop_keys(target)
if known is not None and action.path[0] not in known:
labels = "、".join(
f"{field['label']}={field['section']}.{field['path']}"
for field in editable_fields(target.type)
)
raise ValueError(
f"组件 {target.type} 的 props 没有字段 {action.path[0]!r},"
f"不能新造组件级属性;可编辑字段:{labels or '无'}"
)
# 获取动作指定的属性区域,作为路径遍历的起点
current = getattr(target, action.section)
# 逐段访问路径,只有到达末级时才写入新值
+4 -4
View File
@@ -5,15 +5,15 @@ from fastapi import APIRouter
from app.common.response import ApiResponse, success_response
from app.core.exceptions import BizException
from app.resources.component import get_catalog_cache
from app.schemas.component_resource import ComponentCatalog
from app.resources.material_fields import frontend_catalog
router = APIRouter(prefix="/materials", tags=["Material"])
@router.get("")
async def list_materials() -> ApiResponse[ComponentCatalog]:
# 直接返回启动时载入的内存缓存,不查库
async def list_materials() -> ApiResponse[dict]:
# 直接使用启动时载入的内存缓存,不查库;属性面板 setters 由字段定义派生
catalog = get_catalog_cache()
if catalog is None:
raise BizException(503, "物料目录尚未加载")
return success_response(catalog)
return success_response(frontend_catalog(catalog))
+118
View File
@@ -0,0 +1,118 @@
"""物料字段 MCP 客户端:把字段查询工具交给模型使用
服务只监听本机,由应用启动时拉起;不可用时直接报错,不退回其他查询方式
"""
import asyncio
import os
import socket
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlsplit
from langchain_core.tools import BaseTool
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from loguru import logger
from mcp.types import CallToolResult
from app.config import settings
from app.resources.component import get_catalog_cache
SERVER_NAME = "materials"
PROJECT_ROOT = Path(__file__).resolve().parents[2]
class MaterialFieldServiceError(RuntimeError):
"""物料字段服务不可用,或与主进程的物料目录版本不一致"""
async def _check_version(request: MCPToolCallRequest, handler):
"""限制单次查询耗时;返回的目录版本须与主进程一致,否则模型查到的字段和校验依据不同"""
try:
result = await asyncio.wait_for(
handler(request), timeout=settings.mcp_tool_timeout
)
except TimeoutError as error:
raise MaterialFieldServiceError(f"物料字段查询超时: {request.name}") from error
if isinstance(result, CallToolResult) and result.structuredContent:
served = result.structuredContent.get("catalog_version")
catalog = get_catalog_cache()
expected = catalog.version if catalog is not None else None
if served != expected:
raise MaterialFieldServiceError(
f"物料字段服务的目录版本 {served} 与主服务 {expected} 不一致,请重启服务"
)
return result
_client = MultiServerMCPClient(
{
SERVER_NAME: {
"transport": "streamable_http",
"url": settings.mcp_material_url,
}
},
tool_interceptors=[_check_version],
# 未知组件类型、分组等查询错误返回给模型自行修正;连接失败等仍直接抛出
handle_tool_errors=True,
)
_tools: list[BaseTool] | None = None
async def get_material_tools() -> list[BaseTool]:
"""字段查询工具;首次使用或此前连接失败时向服务拉取工具清单"""
global _tools
if _tools is None:
try:
_tools = await asyncio.wait_for(
_client.get_tools(server_name=SERVER_NAME),
timeout=settings.mcp_tool_timeout,
)
except Exception as error:
raise MaterialFieldServiceError(
f"物料字段服务不可用: {settings.mcp_material_url}"
) from error
return _tools
def _port_open(port: int) -> bool:
with socket.socket() as probe:
probe.settimeout(0.2)
return probe.connect_ex(("127.0.0.1", port)) == 0
async def start_material_server() -> subprocess.Popen | None:
"""本机地址且尚未运行时拉起服务,返回进程供退出时关闭;已运行则直接复用"""
target = urlsplit(settings.mcp_material_url)
if target.hostname not in {"localhost", "127.0.0.1"}:
raise MaterialFieldServiceError("物料字段服务只能部署在本机,不对外暴露")
port = target.port or 80
if _port_open(port):
return None
process = subprocess.Popen(
[sys.executable, "-X", "utf8", "-m", "mcp_servers.material_server"],
cwd=PROJECT_ROOT,
env={**os.environ, "PORT": str(port)},
)
for _ in range(100):
if process.poll() is not None:
raise MaterialFieldServiceError("物料字段服务启动失败,请查看终端日志")
if _port_open(port):
logger.info("物料字段服务已启动: {}", settings.mcp_material_url)
return process
await asyncio.sleep(0.1)
await stop_material_server(process)
raise MaterialFieldServiceError("物料字段服务启动超时")
async def stop_material_server(process: subprocess.Popen | None) -> None:
if process is None or process.poll() is not None:
return
process.terminate()
try:
await asyncio.to_thread(process.wait, timeout=5)
except subprocess.TimeoutExpired:
process.kill()
await asyncio.to_thread(process.wait)
+19
View File
@@ -58,6 +58,21 @@ class Settings:
default_factory=lambda: int(os.getenv("MAX_ACTION_RETRIES", "2"))
)
# 物料字段 MCP 服务:只给模型查询字段定义,只监听本机,随应用启动
mcp_material_url: str = field(
default_factory=lambda: (
os.getenv("MCP_MATERIAL_URL") or "http://127.0.0.1:8201/mcp"
)
)
# 单次字段查询的超时(秒)
mcp_tool_timeout: float = field(
default_factory=lambda: float(os.getenv("MCP_TOOL_TIMEOUT", "5"))
)
# 生成动作或修正建议时,最多几轮查询字段,之后必须给出结果
max_field_tool_rounds: int = field(
default_factory=lambda: int(os.getenv("MAX_FIELD_TOOL_ROUNDS", "4"))
)
# 没有任何对话的空会话保留时长(秒),超时后视为不存在
conversation_empty_ttl: int = field(
default_factory=lambda: int(os.getenv("CONVERSATION_EMPTY_TTL", "7200"))
@@ -113,6 +128,10 @@ class Settings:
raise ValueError("CONVERSATION_HISTORY_TURNS 必须是非负整数")
if type(self.max_action_retries) is not int or self.max_action_retries < 0:
raise ValueError("MAX_ACTION_RETRIES 必须是非负整数")
if self.mcp_tool_timeout <= 0:
raise ValueError("MCP_TOOL_TIMEOUT 必须是正数")
if self.max_field_tool_rounds < 0:
raise ValueError("MAX_FIELD_TOOL_ROUNDS 必须是非负整数")
if not 0 <= self.intent_min_probability <= 1:
raise ValueError("INTENT_MIN_PROBABILITY 必须在 0 到 1 之间")
if not 0 <= self.intent_qa_min_probability <= 1:
+19 -6
View File
@@ -18,6 +18,11 @@ from app.core.logging import setup_logging
async def lifespan(application: FastAPI):
setup_logging()
from app.clients.decision import close_decision_engine
from app.clients.material_mcp import (
MaterialFieldServiceError,
start_material_server,
stop_material_server,
)
from app.clients.user_center import close_user_center
from app.observability.tracing import flush_tracing
from app.processor.graph import build_graph
@@ -28,9 +33,15 @@ async def lifespan(application: FastAPI):
# 按配置选择 MongoDB 或进程内存,会话记录与图检查点使用同一种存储
persistence = await open_persistence()
application.state.conversation_store = persistence.store
material_server = None
try:
# 物料目录启动时载入内存,agent 规划与前端接口共用,避免重复查询数据库
await warm_material_cache(persistence.db)
# 物料字段服务给模型查询字段;启动失败只记录,需要查字段的修改执行时会报错
try:
material_server = await start_material_server()
except MaterialFieldServiceError as error:
logger.error("{},修改组件属性和新增组件将无法执行", error)
application.state.agent_service = AgentService(
build_graph(persistence.checkpointer), persistence.store
)
@@ -38,19 +49,21 @@ async def lifespan(application: FastAPI):
finally:
# 共享客户端只在应用退出时关闭,追踪刷新放到线程避免阻塞事件循环
try:
await persistence.close()
await stop_material_server(material_server)
finally:
try:
await close_decision_engine()
await persistence.close()
finally:
try:
await close_user_center()
await close_decision_engine()
finally:
try:
await asyncio.to_thread(flush_tracing)
await close_user_center()
finally:
await logger.complete()
try:
await asyncio.to_thread(flush_tracing)
finally:
await logger.complete()
def create_app() -> FastAPI:
application = FastAPI(title="Screen Agent API", lifespan=lifespan)
+91
View File
@@ -0,0 +1,91 @@
"""需要物料字段的模型调用:模型按需调用字段查询工具,最后按指定结构输出
动作生成和修正建议共用;字段查询走物料字段 MCP 服务,服务不可用时直接报错
"""
from typing import TypeVar
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call
from langchain.agents.structured_output import ToolStrategy
from langchain_core.messages import AIMessage, HumanMessage
from pydantic import BaseModel
from app.clients.llm import get_llm
from app.clients.material_mcp import get_material_tools
from app.config import settings
from app.processor.state import QueryState
from app.resources.component import get_catalog_cache
from app.resources.material_fields import list_field_groups
OutputModel = TypeVar("OutputModel", bound=BaseModel)
@wrap_model_call
async def _limit_field_queries(request, handler):
"""查询轮数用尽后只保留输出工具,模型必须给出结果"""
rounds = sum(isinstance(message, AIMessage) for message in request.messages)
if rounds >= settings.max_field_tool_rounds:
request = request.override(tools=[])
return await handler(request)
async def invoke_with_field_tools(
schema: type[OutputModel], system_prompt: str, content: str
) -> OutputModel | None:
"""返回结构化结果;模型未给出结果时返回 None
结果不符合 schema 时抛出 StructuredOutputValidationError,由调用方转换为校验错误
"""
agent = create_agent(
get_llm(),
await get_material_tools(),
system_prompt=system_prompt,
# 不在调用内自动重试,结构错误交给后续校验和修正流程,与不查字段时一致
response_format=ToolStrategy(schema, handle_errors=False),
middleware=[_limit_field_queries],
name=f"{schema.__name__}_with_fields",
)
result = await agent.ainvoke(
{"messages": [HumanMessage(content=content)]},
# 每轮模型调用和工具执行各占一步,轮数由中间件限制,这里只防止异常循环
config={"recursion_limit": 2 * settings.max_field_tool_rounds + 6},
)
return result.get("structured_response")
def field_group_directory(state: QueryState) -> dict[str, dict]:
"""本次相关组件的字段分组目录,按物料类型列出,省去模型先查目录的一轮调用"""
nodes = {node.id: node.type for node in state["input_context"].screen.nodes}
plan = state.get("screen_plan")
if plan is None:
types = [
nodes[target]
for change in state["requirement"].changes
if change.target_scope == "nodes"
for target in change.target_ids
if target in nodes
]
else:
types = [
item.component_type
if item.disposition in ("add", "replace")
else nodes.get(item.target_id)
for item in plan.components
if item.disposition in ("add", "replace", "modify")
]
catalog = get_catalog_cache()
directory = {}
for component_type in dict.fromkeys(item for item in types if item):
try:
if catalog is None:
raise ValueError("物料目录未载入")
directory[component_type] = {
key: value
for key, value in list_field_groups(catalog, component_type).items()
if key not in ("component_type", "catalog_version")
}
except ValueError as error:
# 没有物料定义的组件不能修改属性,告诉模型原因
directory[component_type] = {"error": str(error)}
return directory
+39 -49
View File
@@ -2,6 +2,10 @@
import json
from langchain.agents.structured_output import (
StructuredOutputError,
StructuredOutputValidationError,
)
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import ValidationError
@@ -9,13 +13,18 @@ from pydantic import ValidationError
from app.actions.plan import assemble_plan_actions, plan_block_reason, plan_materials
from app.clients.llm import get_llm
from app.processor.base import BaseNode
from app.processor.material_agent import field_group_directory, invoke_with_field_tools
from app.processor.state import QueryState
from app.prompt.action import build_action_prompt
from app.resources.material_fields import editable_fields
from app.prompt.action import build_action_prompt, uses_material_fields
from app.schemas.action_validation import ActionValidationError, schema_errors
from app.schemas.actions import ActionBatch, AddAction
from app.schemas.operations import is_direct_operation
GENERATION_ERROR = ActionValidationError(
code="generation_error",
message="模型输出无法解析为 ActionBatch,请按协议重新生成",
)
def can_generate_actions(state: QueryState) -> bool:
# 没有阻塞原因时才允许生成动作,路由和节点入口复用同一套能力检查
@@ -59,28 +68,6 @@ def action_block_reason(state: QueryState) -> str | None:
return None
def update_fields(state: QueryState) -> dict[str, list[dict]]:
"""按组件 ID 汇总可能被属性更新的已有组件的可编辑字段,没有字段的组件省略"""
plan = state.get("screen_plan")
if plan is None:
target_ids = [
target
for change in state["requirement"].changes
if change.target_scope == "nodes"
for target in change.target_ids
]
else:
target_ids = [
item.target_id for item in plan.components if item.disposition == "modify"
]
nodes = {node.id: node for node in state["input_context"].screen.nodes}
result = {}
for target in dict.fromkeys(target_ids):
if target in nodes and (fields := editable_fields(nodes[target].type)):
result[target] = fields
return result
class ActionNode(BaseNode):
name = "action_node"
@@ -115,10 +102,6 @@ class ActionNode(BaseNode):
payload["screen_plan"] = state["screen_plan"].model_dump(mode="json")
payload["component_materials"] = plan_materials(state)
# 提供待修改组件的属性面板字段,让模型把用户说的“标题”“颜色”等对应到真实路径
if fields := update_fields(state):
payload["editable_fields"] = fields
# 如果本轮是动作修正,提供修正建议、上一轮动作和校验错误供模型参考
if state.get("repair_plan") is not None:
payload.update(
@@ -133,7 +116,17 @@ class ActionNode(BaseNode):
],
)
llm_client = get_llm(ActionBatch)
system_prompt = build_action_prompt(
state["requirement"],
state.get("screen_plan"),
repairing=state.get("repair_plan") is not None,
)
# 需要写组件属性时提供相关组件的字段分组,模型再按需查询字段定义
with_fields = uses_material_fields(state["requirement"], state.get("screen_plan"))
if with_fields:
payload["field_groups"] = field_group_directory(state)
# 将本次具体资料放入用户消息,保留中文以便阅读和排查
content = json.dumps(payload, ensure_ascii=False)
# 旧动作及错误已放入模型输入,现在清空输出状态,避免本轮失败后残留旧结果
updated_state["actions"] = None
@@ -146,19 +139,12 @@ class ActionNode(BaseNode):
# 捕获输出结构和解析错误,转换成统一校验结果供后续修正流程使用
try:
# 异步调用模型,根据需求、规划及是否正在修正选择对应的系统规则
result = await llm_client.ainvoke(
[
SystemMessage(
content=build_action_prompt(
state["requirement"],
state.get("screen_plan"),
repairing=state.get("repair_plan") is not None,
)
),
# 将本次具体资料放入用户消息,保留中文以便阅读和排查
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
)
if with_fields:
result = await invoke_with_field_tools(ActionBatch, system_prompt, content)
else:
result = await get_llm(ActionBatch).ainvoke(
[SystemMessage(content=system_prompt), HumanMessage(content=content)]
)
# 响应中没有工具调用时解析器会返回 None
if result is None:
@@ -184,15 +170,19 @@ class ActionNode(BaseNode):
updated_state["validation_passed"] = False
updated_state["validation_errors"] = schema_errors(error)
return updated_state
except OutputParserException:
except StructuredOutputValidationError as error:
# 查询字段后的输出不符合结构,与直接输出时的处理一致
updated_state["validation_passed"] = False
updated_state["validation_errors"] = (
schema_errors(error.source)
if isinstance(error.source, ValidationError)
else [GENERATION_ERROR]
)
return updated_state
except (OutputParserException, StructuredOutputError):
# 如果模型输出无法解析,记录生成错误,提示后续按 ActionBatch 协议重新生成
updated_state["validation_passed"] = False
updated_state["validation_errors"] = [
ActionValidationError(
code="generation_error",
message="模型输出无法解析为 ActionBatch,请按协议重新生成",
)
]
updated_state["validation_errors"] = [GENERATION_ERROR]
return updated_state
# 保存生成的动作,实际目标、规划一致性及模拟执行结果仍由后续校验节点检查
+21 -10
View File
@@ -2,6 +2,7 @@
import json
from langchain.agents.structured_output import StructuredOutputError
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import ValidationError
@@ -9,7 +10,9 @@ from pydantic import ValidationError
from app.actions.plan import plan_materials
from app.clients.llm import get_llm
from app.processor.base import BaseNode
from app.processor.material_agent import field_group_directory, invoke_with_field_tools
from app.processor.state import QueryState
from app.prompt.action import FIELD_QUERY_PROMPT, uses_material_fields
from app.schemas.capabilities import USER_FAILURE_MESSAGE
from app.prompt.reflection import REFLECTION_SYSTEM_PROMPT
from app.schemas.action_repair import ActionRepairPlan
@@ -22,7 +25,6 @@ class ReflectionNode(BaseNode):
# 浅拷贝状态
updated_state: QueryState = state.copy()
llm_client = get_llm(ActionRepairPlan)
# 汇总原始上下文、需求和失败信息,让模型基于实际错误提出修正建议
payload = {
"input_context": state["input_context"].model_dump(mode="json"),
@@ -46,21 +48,30 @@ class ReflectionNode(BaseNode):
payload["screen_plan"] = state["screen_plan"].model_dump(mode="json")
payload["component_materials"] = plan_materials(state)
# 涉及组件属性时,修正建议须基于查询到的字段定义,与动作生成使用同一组工具
with_fields = uses_material_fields(state["requirement"], state.get("screen_plan"))
if with_fields:
payload["field_groups"] = field_group_directory(state)
# 用户消息承载本轮待分析的具体资料,序列化时保留中文
content = json.dumps(payload, ensure_ascii=False)
# 只捕获输出结构和解析错误,配置、网络等异常继续向上抛出
try:
# 异步生成修正建议,本节点不直接修改动作
plan = await llm_client.ainvoke(
[
# 系统消息提供稳定的失败分析和修正规则
SystemMessage(content=REFLECTION_SYSTEM_PROMPT),
# 用户消息承载本轮待分析的具体资料,序列化时保留中文
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
)
if with_fields:
plan = await invoke_with_field_tools(
ActionRepairPlan,
REFLECTION_SYSTEM_PROMPT + "\n" + FIELD_QUERY_PROMPT,
content,
)
else:
plan = await get_llm(ActionRepairPlan).ainvoke(
[SystemMessage(content=REFLECTION_SYSTEM_PROMPT), HumanMessage(content=content)]
)
# 响应中没有工具调用时解析器会返回 None
if plan is None:
raise OutputParserException("模型未返回结构化结果")
except (ValidationError, OutputParserException):
except (ValidationError, OutputParserException, StructuredOutputError):
# 如果未能得到合法建议,清空旧修正计划,避免再次使用已失效的建议
updated_state["repair_plan"] = None
# 记录无法继续修正的原因,交给后续路由和失败处理流程
+36 -4
View File
@@ -4,10 +4,18 @@ from pydantic import ValidationError
from app.actions.plan import validate_plan_actions, validate_plan_result
from app.actions.registry import apply_action
from app.actions.update import fit_existing_path
from app.processor.base import BaseNode
from app.processor.state import QueryState
from app.resources.component import get_catalog_cache
from app.resources.material_fields import check_update_field
from app.schemas.action_validation import ActionValidationError, schema_errors
from app.schemas.actions import ActionBatch, AddAction, UpdateCanvasAction
from app.schemas.actions import (
ActionBatch,
AddAction,
UpdateAction,
UpdateCanvasAction,
)
from app.schemas.operations import is_canvas_operation
@@ -121,8 +129,32 @@ class ValidatorNode(BaseNode):
# 在副本上试执行,不改变用户传入的大屏,也不操作前端画布
screen = state["input_context"].screen.model_copy(deep=True)
catalog = get_catalog_cache()
# 保存补齐路径后的动作,前端按它执行
actions = list(batch.actions)
# 按动作顺序逐项执行,每一步都使用上一步更新后的大屏副本
for index, action in enumerate(batch.actions):
for index, action in enumerate(actions):
# 属性更新先按物料字段定义检查,目标不存在时交给处理器报错
node = (
next((item for item in screen.nodes if item.id == action.target_id), None)
if isinstance(action, UpdateAction)
else None
)
if node is not None:
try:
check_update_field(catalog, node.type, action)
except ValueError as error:
updated_state["validation_passed"] = False
updated_state["validation_errors"] = [
ActionValidationError(
action_index=index, code="field_error", message=str(error)
)
]
updated_state["simulated_screen"] = None
return updated_state
# 字段定义允许、但组件中还没有的中间对象,折叠进值里再执行
action = actions[index] = fit_existing_path(node, action)
try:
# 根据动作类型调用对应处理逻辑,检查目标和参数并返回更新后的大屏
screen = apply_action(screen, action)
@@ -147,8 +179,8 @@ class ValidatorNode(BaseNode):
updated_state["simulated_screen"] = None
return updated_state
# 全部检查通过后,保存重新校验过的动作对象,统一后续读取的数据结构
updated_state["actions"] = batch.actions
# 全部检查通过后,保存重新校验并补齐路径的动作对象,统一后续读取的数据结构
updated_state["actions"] = actions
# 标记整批动作校验通过,并清空之前可能残留的错误
updated_state["validation_passed"] = True
updated_state["validation_errors"] = []
+48 -21
View File
@@ -53,6 +53,46 @@ REPAIR_TASK_PROMPT = """# 当前阶段:修正重试
"""
FIELD_QUERY_PROMPT = """# 查询组件字段
field_groups 按物料类型列出本次相关组件的字段分组,节点的 type 就是物料类型
确定要改的字段:用用户原话中对属性的说法(input_context 中的提问,不用改写后的需求)调用 search_fields;
返回多个字段时,依据字段说明判断用户指的是哪一个;用户看的是大屏画面,没有提到图层列表、属性面板等编辑器位置时,按画面上看得到的内容理解
需要了解一组相关字段时用 get_fields,一次可查多个分组
只能使用查询到的字段路径,值须符合字段的类型、可选值和取值范围;查不到对应字段说明不支持,不自行编造路径
"""
# 这些动作会写组件属性,需要先查物料字段
FIELD_ACTIONS = frozenset({"update", "add"})
def action_types(requirement: Requirement, screen_plan: ScreenPlan | None) -> set[str]:
"""本次需要的动作类型:简单修改按需求变更,规划按各组件的处理方式"""
if screen_plan is None:
return {
"update_canvas" if change.target_scope == "canvas" else change.operation
for change in requirement.changes
}
types = set()
if screen_plan.canvas_changes:
types.add("update_canvas")
for item in screen_plan.components:
if item.disposition in ("add", "replace"):
types.add("add")
elif item.disposition == "remove":
types.add("delete")
elif item.disposition == "modify":
types.update(("move", "resize"))
if item.design:
types.add("update")
return types
def uses_material_fields(
requirement: Requirement, screen_plan: ScreenPlan | None
) -> bool:
return bool(action_types(requirement, screen_plan) & FIELD_ACTIONS)
def build_action_prompt(
requirement: Requirement,
screen_plan: ScreenPlan | None = None,
@@ -60,40 +100,27 @@ def build_action_prompt(
repairing: bool = False,
) -> str:
"""只组合本次任务需要的规则,不改变 Schema 或后端校验范围"""
types = action_types(requirement, screen_plan)
sections = [ACTION_SYSTEM_PROMPT]
if screen_plan is None:
sections.append(SIMPLE_TASK_PROMPT)
action_types = {
"update_canvas" if change.target_scope == "canvas" else change.operation
for change in requirement.changes
}
else:
sections.append(PLAN_TASK_PROMPT)
action_types = set()
dispositions = {item.disposition for item in screen_plan.components}
if screen_plan.canvas_changes:
sections.append(
"## 调整画布\n将 canvas_changes 中的最终属性值转换为 update_canvas,完整覆盖且不添加其他画布字段"
)
action_types.add("update_canvas")
if any(item.disposition == "add" for item in screen_plan.components):
action_types.add("add")
if any(item.disposition == "replace" for item in screen_plan.components):
if "replace" in dispositions:
sections.append(REPLACE_PLAN_PROMPT)
action_types.add("add")
if any(item.disposition == "remove" for item in screen_plan.components):
action_types.add("delete")
modifications = [
item for item in screen_plan.components if item.disposition == "modify"
]
if modifications:
if "modify" in dispositions:
sections.append(MODIFY_PLAN_PROMPT)
action_types.update(("move", "resize"))
if any(item.design for item in modifications):
action_types.add("update")
if types & FIELD_ACTIONS:
sections.append(FIELD_QUERY_PROMPT)
# 排序保证提示词顺序稳定,缺少动作说明时直接报错,避免静默遗漏
sections.append("# 本次可用动作\n" + "、".join(sorted(action_types)))
sections.extend(ACTION_RULES[name] for name in sorted(action_types))
sections.append("# 本次可用动作\n" + "、".join(sorted(types)))
sections.extend(ACTION_RULES[name] for name in sorted(types))
if repairing:
sections.append(REPAIR_TASK_PROMPT)
return "\n\n".join(section.strip() for section in sections)
+4 -6
View File
@@ -21,13 +21,11 @@ properties 只包含需要修改的画布顶层字段,宽高为最终绝对值
不需要改变的尺寸省略,不改变组件位置
""",
"update": """## update:更新已有组件属性
依据快照选择 props / style 下的字段路径,不猜测不存在的嵌套结构
path 中字符串是对象键,整数是数组下标;中间路径必须存在,数组下标不能越界
只替换指定字段,不做深度合并;null 表示赋值,不表示删除
提供 editable_fields 时,它是该组件属性面板的字段:用户说的名称(如名字、标题、颜色)优先对应 label 相符的字段,使用其 section 和 path
props 第一层只能使用组件已有或物料默认配置中的字段,不新造组件级属性;更深层的配置可以按需新增
字段路径的第一段写入 section(name / props / style),其余各段写入 path,数字段写成整数下标;name 的 path 为空列表
例如字段 props.option.title.text 写成 section=props、path=["option", "title", "text"]
layout 字段不用 update:位置用 move,尺寸用 resize
只替换指定字段,不做深度合并;组件中还没有的中间对象由后端补齐
不通过修改属性改变组件类型(如把 series 的 type 改成其他图表类型),组件类型由节点 type 和物料决定
style 为 null 时可用单层路径添加属性,保留未要求修改的配置
""",
"add": """## add:按物料新增组件
每个 add 规划对应一个新增动作,用 plan_id 关联规划,component_key 选择 component_materials 中的物料
+1
View File
@@ -15,6 +15,7 @@ REFLECTION_SYSTEM_PROMPT = f"""## 任务
需要用户补充、外部资源或尚未支持的能力时,retryable=false,说明缺少什么
{DATA_BINDING_GUIDE}
因数据绑定校验失败时,去掉 dataId 并保留物料示例数据即可修正,retryable=true
字段错误(field_error)时,查询字段定义,建议改用查到的字段路径或合法取值;物料没有能满足需求的字段时,retryable=false
## 边界
保持需求和规划范围,不通过遗漏需求、猜测字段或替换用户意图来绕过错误
+38 -6
View File
@@ -1,5 +1,7 @@
"""查询前端组件目录:目录只来自启动时从 MongoDB 载入的进程内存缓存,没有文件回退"""
import hashlib
import json
from pathlib import Path
from app.schemas.component_resource import (
@@ -21,14 +23,44 @@ def get_catalog_cache() -> ComponentCatalog | None:
return _cached_catalog
def load_catalog_file(path: str | Path) -> ComponentCatalog:
"""读取导出的目录文件,只供导入脚本使用,运行时不读取文件"""
# 如果传入的是相对路径,以项目根目录为基准,避免受启动时工作目录影响
target = Path(path)
# 手写的物料定义目录,只供导入脚本读取后写入 MongoDB,运行时不读取
MATERIALS_DIR = Path(__file__).resolve().parent / "materials"
FIELD_GROUPS_FILE = "field_groups.json"
def load_material_definitions(directory: str | Path = MATERIALS_DIR) -> ComponentCatalog:
"""读取物料定义目录:共享字段分组一个文件,每个物料一个文件;不合法直接报错
version 为全部文件内容的摘要,定义有任何改动都会得到新版本
"""
target = Path(directory)
if not target.is_absolute():
target = Path(__file__).resolve().parents[2] / target
# utf-8-sig 同时兼容带 BOM 和不带 BOM 的 UTF-8 文件;损坏或不合法直接报错
return ComponentCatalog.model_validate_json(target.read_text(encoding="utf-8-sig"))
files = sorted(target.glob("*.json"))
if not any(path.name == FIELD_GROUPS_FILE for path in files):
raise FileNotFoundError(f"物料定义目录缺少 {FIELD_GROUPS_FILE}: {target}")
digest = hashlib.sha256()
components, field_groups = [], []
for path in files:
# utf-8-sig 同时兼容带 BOM 和不带 BOM 的 UTF-8 文件
text = path.read_text(encoding="utf-8-sig")
digest.update(path.name.encode() + b"\0" + text.encode())
if path.name == FIELD_GROUPS_FILE:
field_groups = json.loads(text)
else:
components.append(json.loads(text))
# 文件名排序与展示顺序无关,物料按定义中的 order 排列,未写时排在最后
components.sort(key=lambda item: item.get("order", len(files)))
return ComponentCatalog.model_validate(
{
"version": f"sha256:{digest.hexdigest()}",
"components": [
{key: value for key, value in item.items() if key != "order"}
for item in components
],
"field_groups": field_groups,
}
)
class ComponentProvider:
+195 -39
View File
@@ -1,50 +1,206 @@
"""从物料目录读取组件的可编辑字段,供动作生成理解字段含义,并供校验约束属性结构"""
"""按物料展开字段定义:解析共享分组引用、取字段默认值、派生前端属性面板 setters"""
from app.resources.component import ComponentProvider
from app.schemas.component_resource import ComponentDefinition, ComponentQuery
from app.schemas.screen import ScreenNode
from pydantic import JsonValue
# 属性面板的 key 以区域开头,只有这两个区域可以被 update 修改
EDITABLE_SECTIONS = ("props", "style")
from app.schemas.component_resource import ComponentCatalog, ComponentDefinition
from app.schemas.material_field import (
MISSING,
PANEL_CONTROLS,
FieldGroup,
MaterialField,
lookup,
)
def _materials(node_type: str) -> list[ComponentDefinition]:
# 多个物料预设可以共用同一前端 type,按 type 取全部预设
result = ComponentProvider().query(ComponentQuery(type=node_type))
return result.components if result.status == "resolved" else []
def resolve_field_groups(
catalog: ComponentCatalog, component: ComponentDefinition
) -> list[FieldGroup]:
"""按物料声明的顺序展开字段分组,共享分组取目录中的定义;返回副本,不污染目录"""
shared = {group.key: group for group in catalog.field_groups}
return [
(shared[group] if isinstance(group, str) else group).model_copy(deep=True)
for group in component.field_groups
]
def _split_key(key: str) -> tuple[str, list[str | int]] | None:
# 前端点路径中的纯数字段是数组下标,转换为与 UpdateAction.path 一致的整数
section, *path = key.split(".")
if section not in EDITABLE_SECTIONS or not path:
return None
return section, [int(part) if part.isdigit() else part for part in path]
def field_default(component: ComponentDefinition, field: MaterialField) -> JsonValue:
"""物料默认配置中有该路径时取默认配置的值,否则取字段声明的 default"""
value = lookup(component.template(), field.segments)
return field.default if value is MISSING else value
def editable_fields(node_type: str) -> list[dict]:
"""返回组件属性面板中的字段:用户可见名称、区域、路径及可选值,按出现顺序去重"""
fields: dict[str, dict] = {}
for material in _materials(node_type):
for setter in material.setters:
parsed = _split_key(setter.key)
if parsed is None or setter.key in fields:
def panel_setters(groups: list[FieldGroup]) -> list[dict]:
"""派生前端属性面板的 setters,格式与前端 SetterSchema 一致"""
setters = []
for group in groups:
for field in group.fields:
if not field.panel:
continue
section, path = parsed
field = {"label": setter.label, "section": section, "path": path}
if options := setter.props.get("options"):
field["options"] = options
fields[setter.key] = field
return list(fields.values())
props = {}
if field.options:
props["options"] = [
{"label": option.label, "value": option.value} for option in field.options
]
setter = {
"type": PANEL_CONTROLS[field.value_type],
"label": field.label,
"key": field.path,
"props": props,
}
if field.span is not None:
setter["span"] = field.span
setters.append(setter)
return setters
def known_prop_keys(node: ScreenNode) -> set[str] | None:
"""props 第一层是组件与渲染器的约定,只能是节点已有或物料默认配置中的字段;
目录中找不到该类型时约定未知,返回 None 表示不做限制"""
materials = _materials(node.type)
if not materials:
return None
keys = set(node.props)
for material in materials:
keys.update(material.schema_.props)
return keys
def frontend_catalog(catalog: ComponentCatalog) -> dict:
"""前端物料接口的数据:在保存的定义上附加由字段定义派生的 setters"""
data = catalog.model_dump(mode="json", by_alias=True)
for item, component in zip(data["components"], catalog.components, strict=True):
item["setters"] = panel_setters(resolve_field_groups(catalog, component))
return data
# ---- 字段查询:MCP 工具、动作提示词和动作校验共用同一套逻辑 ----
def find_material(catalog: ComponentCatalog, component_type: str) -> ComponentDefinition:
"""按节点 type 找物料;找不到或对应多个物料时报错,不猜测"""
matches = [item for item in catalog.components if item.type == component_type]
if len(matches) != 1:
known = "、".join(item.type for item in catalog.components)
reason = "没有对应的物料" if not matches else "对应多个物料"
raise ValueError(f"组件类型 {component_type} {reason},可用类型:{known}")
return matches[0]
def describe_field(
component: ComponentDefinition, group: FieldGroup, field: MaterialField
) -> dict:
"""给模型看的字段说明:省略空值和属性面板专用的信息"""
data = field.model_dump(
mode="json",
exclude={"default", "panel", "span", "editable"},
exclude_none=True,
exclude_defaults=True,
)
data["default"] = field_default(component, field)
if not field.editable:
data["editable"] = False
data["group"] = group.key
return data
def list_field_groups(catalog: ComponentCatalog, component_type: str) -> dict:
"""物料的字段分组目录:每组的说明和字段名称,模型据此决定查看哪几组"""
component = find_material(catalog, component_type)
return {
"component_type": component.type,
"name": component.name,
"catalog_version": catalog.version,
"groups": [
{
"key": group.key,
"label": group.label,
"description": group.description,
"fields": [field.label for field in group.fields],
}
for group in resolve_field_groups(catalog, component)
],
}
def get_fields(
catalog: ComponentCatalog, component_type: str, group_keys: list[str]
) -> dict:
"""指定分组的完整字段定义"""
component = find_material(catalog, component_type)
groups = {group.key: group for group in resolve_field_groups(catalog, component)}
if unknown := [key for key in group_keys if key not in groups]:
raise ValueError(
f"{component.type} 没有字段分组 {unknown},可用分组:{list(groups)}"
)
return {
"component_type": component.type,
"catalog_version": catalog.version,
"fields": [
describe_field(component, groups[key], field)
for key in dict.fromkeys(group_keys)
for field in groups[key].fields
],
}
def _match_score(field: MaterialField, query: str) -> int:
"""名称、别名完全一致最相关,其次是互相包含、路径末段一致,最后是说明中出现"""
names = [name.casefold() for name in (field.label, *field.aliases)]
if query in names:
return 4
if any(query in name or name in query for name in names):
return 3
if query == field.path.rsplit(".", 1)[-1].casefold():
return 2
if query in field.description.casefold() or query in field.path.casefold():
return 1
return 0
def search_fields(
catalog: ComponentCatalog, component_type: str, query: str, limit: int = 8
) -> dict:
"""按名称、别名、路径和说明搜索字段,按相关度返回"""
component = find_material(catalog, component_type)
keyword = query.strip().casefold()
if not keyword:
raise ValueError("搜索内容不能为空")
scored = [
(score, index, group, field)
for index, (group, field) in enumerate(
(group, field)
for group in resolve_field_groups(catalog, component)
for field in group.fields
)
if (score := _match_score(field, keyword))
]
scored.sort(key=lambda item: (-item[0], item[1]))
return {
"component_type": component.type,
"catalog_version": catalog.version,
"query": query,
"fields": [
describe_field(component, group, field)
for _, _, group, field in scored[:limit]
],
}
def check_update_field(catalog: ComponentCatalog | None, node_type: str, action) -> None:
"""update 只能修改字段定义中的可编辑字段,值须符合字段类型和取值约束
不合法时抛 ValueError,路径不存在时附上可能对应的字段供修正参考
"""
if catalog is None:
raise ValueError("物料目录未载入,无法确认组件有哪些字段")
component = find_material(catalog, node_type)
fields = {
field.path: field
for group in resolve_field_groups(catalog, component)
for field in group.fields
}
path = action.field_path
field = fields.get(path)
if field is None:
message = f"{node_type} 没有字段 {path},只能修改字段定义中的路径"
keyword = next(
(str(item) for item in reversed(action.path) if isinstance(item, str)),
action.section,
)
if similar := search_fields(catalog, node_type, keyword, limit=3)["fields"]:
message += ";可能是:" + "、".join(
f"{item['label']}({item['path']})" for item in similar
)
raise ValueError(message)
if not field.editable:
raise ValueError(f"字段 {path}({field.label})不允许修改")
if error := field.value_error(action.value):
raise ValueError(f"字段 {path}({field.label}){error}")
+207
View File
@@ -0,0 +1,207 @@
{
"order": 2,
"key": "area-chart",
"type": "area-chart",
"name": "面积图",
"description": "折线下方填充面积,展示数值随时间的变化趋势并突出总量",
"tags": [
"charts",
"面积图",
"area-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:graph-new-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
"chart-axis",
"chart-line-series",
{
"key": "area-fill",
"label": "面积",
"description": "折线下方填充区域",
"fields": [
{
"path": "props.option.series.0.areaStyle.color",
"label": "面积色",
"description": "折线下方填充区域的颜色,通常带透明度,例如 rgba(64, 158, 255, 0.25)",
"value_type": "color",
"aliases": [
"填充色",
"面积颜色",
"阴影颜色"
]
}
]
},
"chart-encode-xy"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "area-chart",
"name": "面积图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "成交额趋势",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "axis"
},
"dataset": {
"source": [
{
"date": "周一",
"revenue": 120
},
{
"date": "周二",
"revenue": 200
},
{
"date": "周三",
"revenue": 150
},
{
"date": "周四",
"revenue": 260
},
{
"date": "周五",
"revenue": 330
},
{
"date": "周六",
"revenue": 420
},
{
"date": "周日",
"revenue": 510
}
]
},
"grid": {
"top": 86,
"right": 24,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"boundaryGap": false,
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"yAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"series": [
{
"name": "成交额",
"type": "line",
"smooth": true,
"symbolSize": 8,
"encode": {
"x": "date",
"y": "revenue"
},
"lineStyle": {
"width": 3,
"color": "#409eff"
},
"itemStyle": {
"color": "#409eff"
},
"areaStyle": {
"color": "rgba(64, 158, 255, 0.25)"
}
}
]
}
},
"style": null,
"events": []
}
}
+274
View File
@@ -0,0 +1,274 @@
{
"order": 0,
"key": "bar-chart",
"type": "bar-chart",
"name": "柱状图",
"description": "按类目对比数值大小,适合月度、地区、产品等分类数据的对比",
"tags": [
"charts",
"柱状图",
"bar-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-2-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
"chart-axis",
{
"key": "bar-series",
"label": "柱形",
"description": "柱子的颜色、宽度、圆角、系列名称和数值标签",
"fields": [
{
"path": "props.option.series.0.itemStyle.color",
"label": "柱颜色",
"description": "柱子的填充颜色",
"value_type": "color",
"aliases": [
"颜色",
"柱子颜色",
"主色"
]
},
{
"path": "props.option.series.0.name",
"label": "系列名称",
"description": "这组柱子的名称,显示在图例和鼠标悬停提示中;不是图表标题",
"value_type": "string",
"aliases": [
"图例名称",
"系列名",
"数据名称"
],
"panel": false
},
{
"path": "props.option.series.0.barWidth",
"label": "柱宽",
"description": "柱子宽度,百分比表示占每个类目宽度的比例,例如 45%",
"value_type": "string",
"aliases": [
"柱子宽度",
"柱子粗细"
],
"panel": false
},
{
"path": "props.option.series.0.itemStyle.borderRadius",
"label": "柱圆角",
"description": "柱子四个角的圆角半径,顺序为左上、右上、右下、左下",
"value_type": "array",
"item_type": "number",
"unit": "px",
"aliases": [
"圆角"
],
"panel": false
},
{
"path": "props.option.series.0.label.show",
"label": "数值标签",
"description": "是否在每根柱子上显示数值",
"value_type": "boolean",
"default": false,
"aliases": [
"显示数值",
"数据标签",
"标签"
],
"panel": false
},
{
"path": "props.option.series.0.label.position",
"label": "标签位置",
"description": "数值标签相对柱子的位置,开启数值标签后生效",
"value_type": "enum",
"default": "inside",
"options": [
{
"value": "top",
"label": "柱顶上方"
},
{
"value": "inside",
"label": "柱内居中"
},
{
"value": "insideTop",
"label": "柱内顶端"
},
{
"value": "insideBottom",
"label": "柱内底端"
}
],
"aliases": [
"数值位置"
],
"panel": false
}
]
},
"chart-encode-xy"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "bar-chart",
"name": "柱状图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "月度销售额",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {},
"dataset": {
"source": [
{
"month": "一月",
"sales": 120
},
{
"month": "二月",
"sales": 200
},
{
"month": "三月",
"sales": 150
},
{
"month": "四月",
"sales": 80
},
{
"month": "五月",
"sales": 170
},
{
"month": "六月",
"sales": 240
}
]
},
"grid": {
"top": 86,
"right": 24,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"yAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"series": [
{
"name": "销售额",
"type": "bar",
"barWidth": "45%",
"encode": {
"x": "month",
"y": "sales"
},
"itemStyle": {
"color": "#409eff",
"borderRadius": [
4,
4,
0,
0
]
}
}
]
}
},
"style": null,
"events": []
}
}
+382
View File
@@ -0,0 +1,382 @@
{
"order": 11,
"key": "bar-line-chart",
"type": "bar-line-chart",
"name": "柱线混合图",
"description": "柱子展示数量、折线展示比率,左右两个纵轴分别对应两种数值",
"tags": [
"charts",
"柱线混合图",
"bar-line-chart",
"双轴",
"混合",
"增长率"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-2-linear",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
"柱系列使用左轴,折线系列 yAxisIndex=1 使用右轴",
"两个系列 encode.x 需保持一致"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
"chart-palette",
{
"key": "bar-line-series",
"label": "系列",
"description": "柱系列和折线系列的颜色、名称和样式",
"fields": [
{
"path": "props.option.color.0",
"label": "柱颜色",
"description": "柱子的颜色",
"value_type": "color",
"span": 12,
"aliases": [
"颜色",
"主色"
]
},
{
"path": "props.option.color.1",
"label": "线颜色",
"description": "折线的颜色",
"value_type": "color",
"span": 12,
"aliases": [
"折线颜色"
]
},
{
"path": "props.option.series.1.smooth",
"label": "平滑曲线",
"description": "开启后折线为平滑曲线,关闭后为折角直线",
"value_type": "boolean",
"aliases": [
"曲线",
"平滑"
]
},
{
"path": "props.option.series.0.name",
"label": "柱系列名称",
"description": "柱子系列的名称,显示在图例和鼠标悬停提示中",
"value_type": "string",
"aliases": [
"柱子名称"
],
"panel": false
},
{
"path": "props.option.series.1.name",
"label": "线系列名称",
"description": "折线系列的名称,显示在图例和鼠标悬停提示中",
"value_type": "string",
"aliases": [
"折线名称"
],
"panel": false
},
{
"path": "props.option.series.0.barWidth",
"label": "柱宽",
"description": "柱子宽度,百分比表示占每个类目宽度的比例,例如 40%",
"value_type": "string",
"aliases": [
"柱子宽度"
],
"panel": false
}
]
},
{
"key": "bar-line-axis",
"label": "坐标轴",
"description": "左纵轴对应柱子数值,右纵轴对应折线数值",
"fields": [
{
"path": "props.option.yAxis.0.name",
"label": "左轴名称",
"description": "左侧纵轴(柱子数值)顶部显示的名称",
"value_type": "string",
"span": 12,
"aliases": [
"左轴标题"
]
},
{
"path": "props.option.yAxis.1.name",
"label": "右轴名称",
"description": "右侧纵轴(折线数值)顶部显示的名称",
"value_type": "string",
"span": 12,
"aliases": [
"右轴标题"
]
},
{
"path": "props.option.yAxis.1.axisLabel.formatter",
"label": "右轴格式",
"description": "右侧纵轴刻度的显示格式,{value} 代表刻度值,例如 {value}%",
"value_type": "string",
"aliases": [
"右轴单位"
],
"panel": false
},
{
"path": "props.option.xAxis.axisLabel.color",
"label": "X轴文字色",
"description": "横轴类目文字的颜色",
"value_type": "color",
"aliases": [
"横轴文字颜色"
],
"panel": false
}
]
},
{
"key": "bar-line-data",
"label": "数据映射",
"description": "柱线混合图从数据集(dataset.source)中取哪些列",
"fields": [
{
"path": "props.option.series.0.encode.x",
"label": "X字段",
"description": "横轴类目使用数据集中的哪一列;两个系列必须相同,修改时同时修改折线的X字段",
"value_type": "string",
"aliases": [
"横轴字段",
"类目字段"
]
},
{
"path": "props.option.series.1.encode.x",
"label": "折线X字段",
"description": "须与X字段保持一致,修改X字段时同步修改",
"value_type": "string",
"panel": false
},
{
"path": "props.option.series.0.encode.y",
"label": "柱数值字段",
"description": "柱子高度使用数据集中的哪一列",
"value_type": "string",
"aliases": [
"柱子字段"
]
},
{
"path": "props.option.series.1.encode.y",
"label": "线数值字段",
"description": "折线数值使用数据集中的哪一列",
"value_type": "string",
"aliases": [
"折线字段"
]
}
]
}
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "bar-line-chart",
"name": "柱线混合图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"color": [
"#409eff",
"#e6a23c"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "销售额与增长率",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "axis"
},
"dataset": {
"source": [
{
"month": "一月",
"sales": 120,
"growth": 12.5
},
{
"month": "二月",
"sales": 200,
"growth": 18.2
},
{
"month": "三月",
"sales": 150,
"growth": -4.3
},
{
"month": "四月",
"sales": 80,
"growth": -20.1
},
{
"month": "五月",
"sales": 170,
"growth": 9.6
},
{
"month": "六月",
"sales": 240,
"growth": 21.4
}
]
},
"grid": {
"top": 86,
"right": 48,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"yAxis": [
{
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
},
"name": "销售额",
"nameTextStyle": {
"color": "#a3a6ad"
}
},
{
"type": "value",
"axisLabel": {
"color": "#cfd3dc",
"formatter": "{value}%"
},
"splitLine": {
"show": false
},
"name": "增长率",
"nameTextStyle": {
"color": "#a3a6ad"
}
}
],
"series": [
{
"name": "销售额",
"type": "bar",
"barWidth": "40%",
"encode": {
"x": "month",
"y": "sales"
},
"itemStyle": {
"borderRadius": [
4,
4,
0,
0
]
}
},
{
"name": "增长率",
"type": "line",
"yAxisIndex": 1,
"smooth": true,
"symbolSize": 6,
"encode": {
"x": "month",
"y": "growth"
}
}
]
}
},
"style": null,
"events": []
}
}
+566
View File
@@ -0,0 +1,566 @@
[
{
"key": "node-base",
"label": "基础",
"description": "所有组件共有的名称、位置和尺寸",
"fields": [
{
"path": "name",
"label": "组件名称",
"description": "组件在编辑器图层列表和属性面板标题栏中的名称,用于编辑时识别组件,不显示在大屏画面上;图表画面上显示的名字是标题",
"value_type": "string",
"aliases": [
"组件名",
"图层名",
"图层名称"
],
"panel": false
},
{
"path": "layout.x",
"label": "X",
"description": "组件左上角距画布左边缘的水平距离",
"value_type": "number",
"unit": "px",
"aliases": [
"横坐标",
"水平位置",
"左边距离"
],
"panel": false
},
{
"path": "layout.y",
"label": "Y",
"description": "组件左上角距画布上边缘的垂直距离",
"value_type": "number",
"unit": "px",
"aliases": [
"纵坐标",
"垂直位置",
"上边距离"
],
"panel": false
},
{
"path": "layout.width",
"label": "宽度",
"description": "组件的宽度",
"value_type": "number",
"minimum": 1,
"unit": "px",
"aliases": [
"宽",
"横向尺寸"
],
"panel": false
},
{
"path": "layout.height",
"label": "高度",
"description": "组件的高度",
"value_type": "number",
"minimum": 1,
"unit": "px",
"aliases": [
"高",
"纵向尺寸"
],
"panel": false
}
]
},
{
"key": "chart-title",
"label": "标题",
"description": "图表上方显示的标题文字及其颜色、字号、对齐",
"fields": [
{
"path": "props.option.title.text",
"label": "标题",
"description": "显示在图表上方的标题文字,是用户在大屏上看到的图表名字",
"value_type": "string",
"aliases": [
"名字",
"名称",
"图表名称",
"图表标题",
"标题文字"
]
},
{
"path": "props.option.title.textStyle.color",
"label": "标题色",
"description": "标题文字的颜色",
"value_type": "color",
"aliases": [
"标题颜色",
"标题文字颜色"
]
},
{
"path": "props.option.title.left",
"label": "对齐",
"description": "标题在组件内的水平位置",
"value_type": "enum",
"options": [
{
"value": "left",
"label": "左对齐"
},
{
"value": "center",
"label": "居中"
},
{
"value": "right",
"label": "右对齐"
}
],
"aliases": [
"标题对齐",
"标题位置"
]
},
{
"path": "props.option.title.textStyle.fontSize",
"label": "标题字号",
"description": "标题文字的大小",
"value_type": "number",
"minimum": 8,
"maximum": 72,
"unit": "px",
"aliases": [
"标题大小",
"标题字体大小"
],
"panel": false
},
{
"path": "props.option.title.show",
"label": "显示标题",
"description": "是否显示标题,关闭后整个标题隐藏",
"value_type": "boolean",
"default": true,
"aliases": [
"隐藏标题",
"去掉标题"
],
"panel": false
}
]
},
{
"key": "chart-legend",
"label": "图例",
"description": "标题下方列出各系列名称和颜色的图例",
"fields": [
{
"path": "props.option.legend.show",
"label": "图例显示",
"description": "是否显示图例;图例列出各系列的名称和颜色",
"value_type": "boolean",
"aliases": [
"图例",
"显示图例",
"隐藏图例"
]
},
{
"path": "props.option.legend.textStyle.color",
"label": "图例文字色",
"description": "图例中系列名称文字的颜色",
"value_type": "color",
"aliases": [
"图例颜色",
"图例文字颜色"
],
"panel": false
}
]
},
{
"key": "chart-grid",
"label": "边距",
"description": "绘图区(坐标轴所在区域)与组件四边的距离",
"fields": [
{
"path": "props.option.grid.top",
"label": "上边距",
"description": "绘图区距组件上边缘的距离;默认为标题和图例留出位置,调小可能与它们重叠",
"value_type": "number",
"minimum": 0,
"unit": "px",
"span": 12
},
{
"path": "props.option.grid.right",
"label": "右边距",
"description": "绘图区距组件右边缘的距离",
"value_type": "number",
"minimum": 0,
"unit": "px",
"span": 12
},
{
"path": "props.option.grid.bottom",
"label": "下边距",
"description": "绘图区距组件下边缘的距离,横轴文字显示在这里",
"value_type": "number",
"minimum": 0,
"unit": "px",
"span": 12
},
{
"path": "props.option.grid.left",
"label": "左边距",
"description": "绘图区距组件左边缘的距离,纵轴文字显示在这里",
"value_type": "number",
"minimum": 0,
"unit": "px",
"span": 12
}
]
},
{
"key": "chart-axis",
"label": "坐标轴",
"description": "直角坐标图表的横轴、纵轴和网格线颜色",
"fields": [
{
"path": "props.option.xAxis.axisLabel.color",
"label": "X轴文字色",
"description": "横轴刻度文字的颜色",
"value_type": "color",
"aliases": [
"横轴文字颜色",
"X轴标签颜色"
],
"panel": false
},
{
"path": "props.option.xAxis.axisLine.lineStyle.color",
"label": "X轴线颜色",
"description": "横轴轴线的颜色",
"value_type": "color",
"aliases": [
"横轴颜色",
"坐标轴颜色"
],
"panel": false
},
{
"path": "props.option.yAxis.axisLabel.color",
"label": "Y轴文字色",
"description": "纵轴刻度文字的颜色",
"value_type": "color",
"aliases": [
"纵轴文字颜色",
"Y轴标签颜色"
],
"panel": false
},
{
"path": "props.option.yAxis.splitLine.lineStyle.color",
"label": "网格线颜色",
"description": "绘图区内横向分隔线的颜色",
"value_type": "color",
"aliases": [
"分隔线颜色",
"背景线颜色"
],
"panel": false
}
]
},
{
"key": "chart-palette",
"label": "配色",
"description": "各系列或数据项按顺序依次取用的颜色列表",
"fields": [
{
"path": "props.option.color",
"label": "调色盘",
"description": "颜色列表,第 1 个系列或数据项用第 1 个颜色,依次类推;整体换一套配色时修改它,只改某一项时用对应的单项颜色字段",
"value_type": "array",
"item_type": "color",
"aliases": [
"配色",
"配色方案",
"整体颜色"
],
"panel": false
}
]
},
{
"key": "chart-encode-xy",
"label": "数据映射",
"description": "图表从数据集(dataset.source)中取哪一列作为横轴和纵轴",
"fields": [
{
"path": "props.option.series.0.encode.x",
"label": "X字段",
"description": "横轴使用数据集中的哪一列,必须是 dataset.source 中存在的字段名",
"value_type": "string",
"aliases": [
"横轴字段"
]
},
{
"path": "props.option.series.0.encode.y",
"label": "Y字段",
"description": "纵轴使用数据集中的哪一列,必须是 dataset.source 中存在的字段名",
"value_type": "string",
"aliases": [
"纵轴字段"
]
}
]
},
{
"key": "chart-encode-item",
"label": "数据映射",
"description": "图表从数据集(dataset.source)中取哪一列作为数据项名称和数值",
"fields": [
{
"path": "props.option.series.0.encode.itemName",
"label": "名称字段",
"description": "每个数据项(扇区、层级等)的名称使用数据集中的哪一列,必须是 dataset.source 中存在的字段名",
"value_type": "string",
"aliases": [
"类目字段",
"分类字段"
]
},
{
"path": "props.option.series.0.encode.value",
"label": "数值字段",
"description": "每个数据项的数值使用数据集中的哪一列,必须是 dataset.source 中存在的字段名",
"value_type": "string",
"aliases": [
"值字段"
]
}
]
},
{
"key": "chart-line-series",
"label": "线条",
"description": "折线的颜色、粗细、平滑度、数据点和数值标签",
"fields": [
{
"path": "props.option.series.0.lineStyle.color",
"label": "线颜色",
"description": "折线的颜色",
"value_type": "color",
"aliases": [
"颜色",
"折线颜色",
"主色"
]
},
{
"path": "props.option.series.0.name",
"label": "系列名称",
"description": "这条折线的名称,显示在图例和鼠标悬停提示中;不是图表标题",
"value_type": "string",
"aliases": [
"图例名称",
"系列名",
"数据名称"
],
"panel": false
},
{
"path": "props.option.series.0.smooth",
"label": "平滑曲线",
"description": "开启后折线为平滑曲线,关闭后为折角直线",
"value_type": "boolean",
"aliases": [
"曲线",
"平滑"
],
"panel": false
},
{
"path": "props.option.series.0.lineStyle.width",
"label": "线宽",
"description": "折线的粗细",
"value_type": "number",
"minimum": 1,
"maximum": 12,
"unit": "px",
"aliases": [
"线条粗细",
"线粗细"
],
"panel": false
},
{
"path": "props.option.series.0.itemStyle.color",
"label": "数据点颜色",
"description": "折线上每个数据点(拐点)的颜色",
"value_type": "color",
"aliases": [
"拐点颜色",
"圆点颜色"
],
"panel": false
},
{
"path": "props.option.series.0.symbolSize",
"label": "数据点大小",
"description": "折线上数据点(拐点)的直径",
"value_type": "number",
"minimum": 0,
"maximum": 30,
"unit": "px",
"aliases": [
"拐点大小",
"圆点大小"
],
"panel": false
},
{
"path": "props.option.series.0.label.show",
"label": "数值标签",
"description": "是否在每个数据点上方显示数值",
"value_type": "boolean",
"default": false,
"aliases": [
"显示数值",
"数据标签",
"标签"
],
"panel": false
}
]
},
{
"key": "chart-item-series",
"label": "数据项",
"description": "饼图、漏斗图等按数据项着色的图表的主色、系列名称和数据项标签",
"fields": [
{
"path": "props.option.color.0",
"label": "主色",
"description": "调色盘的第一个颜色,用于第一个数据项(扇区或层级);其余数据项依次取调色盘后面的颜色",
"value_type": "color",
"aliases": [
"颜色",
"第一个颜色"
]
},
{
"path": "props.option.series.0.name",
"label": "系列名称",
"description": "这组数据的名称,显示在鼠标悬停提示中;不是图表标题",
"value_type": "string",
"aliases": [
"系列名",
"数据名称"
],
"panel": false
},
{
"path": "props.option.series.0.label.show",
"label": "数据项标签",
"description": "是否在每个数据项旁显示名称标签",
"value_type": "boolean",
"default": true,
"aliases": [
"标签",
"显示标签",
"扇区标签"
],
"panel": false
},
{
"path": "props.option.series.0.label.color",
"label": "标签文字色",
"description": "数据项名称标签的文字颜色",
"value_type": "color",
"aliases": [
"标签颜色"
],
"panel": false
}
]
},
{
"key": "chart-gauge-value",
"label": "数值",
"description": "仪表类图表的量程、进度颜色和中心数值显示",
"fields": [
{
"path": "props.option.series.0.progress.itemStyle.color",
"label": "进度色",
"description": "进度弧的颜色",
"value_type": "color",
"aliases": [
"颜色",
"主色",
"进度条颜色"
]
},
{
"path": "props.option.series.0.min",
"label": "最小值",
"description": "量程起点,进度从这个值开始计算",
"value_type": "number",
"span": 12,
"aliases": [
"量程最小值",
"起始值"
]
},
{
"path": "props.option.series.0.max",
"label": "最大值",
"description": "量程终点,数值达到它时进度为满",
"value_type": "number",
"span": 12,
"aliases": [
"量程最大值",
"目标值",
"满值"
]
},
{
"path": "props.option.series.0.detail.formatter",
"label": "数值格式",
"description": "中心数值的显示格式,{value} 代表当前值,例如 {value}%",
"value_type": "string",
"aliases": [
"数值单位",
"显示格式"
],
"panel": false
},
{
"path": "props.option.series.0.detail.color",
"label": "数值颜色",
"description": "中心数值文字的颜色",
"value_type": "color",
"aliases": [
"数字颜色"
],
"panel": false
},
{
"path": "props.option.series.0.detail.fontSize",
"label": "数值字号",
"description": "中心数值文字的大小",
"value_type": "number",
"minimum": 8,
"maximum": 96,
"unit": "px",
"aliases": [
"数字大小"
],
"panel": false
}
]
}
]
+201
View File
@@ -0,0 +1,201 @@
{
"order": 8,
"key": "funnel-chart",
"type": "funnel-chart",
"name": "漏斗图",
"description": "展示流程各阶段的数量和逐级转化情况",
"tags": [
"charts",
"漏斗图",
"funnel-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:filter-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-palette",
"chart-item-series",
{
"key": "funnel-shape",
"label": "漏斗",
"description": "漏斗的排序和层间距",
"fields": [
{
"path": "props.option.series.0.sort",
"label": "排序",
"description": "各层按数值排列的方向",
"value_type": "enum",
"options": [
{
"value": "descending",
"label": "从大到小(正漏斗)"
},
{
"value": "ascending",
"label": "从小到大(倒漏斗)"
},
{
"value": "none",
"label": "按数据顺序"
}
],
"aliases": [
"方向",
"顺序"
],
"panel": false
},
{
"path": "props.option.series.0.gap",
"label": "层间距",
"description": "相邻两层之间的间隔",
"value_type": "number",
"minimum": 0,
"maximum": 40,
"unit": "px",
"aliases": [
"间隔",
"间距"
],
"panel": false
}
]
},
"chart-encode-item"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "funnel-chart",
"name": "漏斗图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 280
},
"props": {
"option": {
"color": [
"#409eff",
"#67c23a",
"#e6a23c",
"#f56c6c",
"#909399"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "转化漏斗",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item"
},
"dataset": {
"source": [
{
"stage": "访问",
"value": 100
},
{
"stage": "咨询",
"value": 72
},
{
"stage": "下单",
"value": 48
},
{
"stage": "成交",
"value": 26
}
]
},
"series": [
{
"name": "转化",
"type": "funnel",
"top": 72,
"bottom": 16,
"left": "12%",
"width": "76%",
"minSize": "16%",
"maxSize": "100%",
"sort": "descending",
"gap": 4,
"label": {
"color": "#cfd3dc"
},
"itemStyle": {
"borderColor": "transparent"
},
"encode": {
"itemName": "stage",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
+234
View File
@@ -0,0 +1,234 @@
{
"order": 9,
"key": "gauge-chart",
"type": "gauge-chart",
"name": "仪表盘",
"description": "用指针和进度弧展示单个指标的当前值或完成率",
"tags": [
"charts",
"仪表盘",
"gauge-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:speedometer-middle-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-gauge-value",
{
"key": "gauge-dial",
"label": "表盘",
"description": "仪表盘的指针、刻度和表盘宽度",
"fields": [
{
"path": "props.option.series.0.pointer.show",
"label": "显示指针",
"description": "是否显示指针",
"value_type": "boolean",
"default": true,
"aliases": [
"指针"
],
"panel": false
},
{
"path": "props.option.series.0.pointer.itemStyle.color",
"label": "指针颜色",
"description": "指针的颜色",
"value_type": "color",
"panel": false
},
{
"path": "props.option.series.0.axisLabel.color",
"label": "刻度文字色",
"description": "表盘刻度数字的颜色",
"value_type": "color",
"aliases": [
"刻度颜色"
],
"panel": false
},
{
"path": "props.option.series.0.axisLine.lineStyle.width",
"label": "表盘宽度",
"description": "表盘底弧的粗细",
"value_type": "number",
"minimum": 1,
"maximum": 60,
"unit": "px",
"aliases": [
"底弧宽度"
],
"panel": false
},
{
"path": "props.option.series.0.progress.width",
"label": "进度宽度",
"description": "进度弧的粗细",
"value_type": "number",
"minimum": 1,
"maximum": 60,
"unit": "px",
"aliases": [
"进度条宽度"
],
"panel": false
}
]
},
"chart-encode-item"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "gauge-chart",
"name": "仪表盘",
"layout": {
"x": 0,
"y": 0,
"width": 320,
"height": 280
},
"props": {
"option": {
"title": {
"text": "销售目标完成率",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item"
},
"dataset": {
"source": [
{
"name": "销售目标完成率",
"value": 72
}
]
},
"series": [
{
"type": "gauge",
"min": 0,
"max": 100,
"startAngle": 210,
"endAngle": -30,
"center": [
"50%",
"58%"
],
"radius": "80%",
"progress": {
"show": true,
"width": 12,
"itemStyle": {
"color": "#409eff"
}
},
"axisLine": {
"lineStyle": {
"width": 12,
"color": [
[
1,
"rgba(255, 255, 255, 0.1)"
]
]
}
},
"axisTick": {
"show": false
},
"splitLine": {
"length": 8,
"lineStyle": {
"width": 1,
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc",
"distance": 16
},
"pointer": {
"itemStyle": {
"color": "#e5eaf3"
}
},
"title": {
"offsetCenter": [
0,
"72%"
],
"color": "#cfd3dc",
"fontSize": 13
},
"detail": {
"valueAnimation": true,
"offsetCenter": [
0,
"46%"
],
"color": "#e5eaf3",
"fontSize": 28,
"formatter": "{value}%"
},
"encode": {
"itemName": "name",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
+320
View File
@@ -0,0 +1,320 @@
{
"order": 1,
"key": "hbar-chart",
"type": "hbar-chart",
"name": "条形图",
"description": "横向条形对比各类目数值,类目名称较长或类目较多时比柱状图更易读",
"tags": [
"charts",
"条形图",
"hbar-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:align-left-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
{
"key": "hbar-axis",
"label": "坐标轴",
"description": "条形图的横轴为数值轴、纵轴为类目轴",
"fields": [
{
"path": "props.option.xAxis.axisLabel.color",
"label": "X轴文字色",
"description": "横轴(数值)刻度文字的颜色",
"value_type": "color",
"aliases": [
"横轴文字颜色",
"数值轴文字颜色"
],
"panel": false
},
{
"path": "props.option.xAxis.splitLine.lineStyle.color",
"label": "网格线颜色",
"description": "绘图区内纵向分隔线的颜色",
"value_type": "color",
"aliases": [
"分隔线颜色",
"背景线颜色"
],
"panel": false
},
{
"path": "props.option.yAxis.axisLabel.color",
"label": "Y轴文字色",
"description": "纵轴类目名称文字的颜色",
"value_type": "color",
"aliases": [
"纵轴文字颜色",
"类目文字颜色"
],
"panel": false
},
{
"path": "props.option.yAxis.axisLine.lineStyle.color",
"label": "Y轴线颜色",
"description": "纵轴轴线的颜色",
"value_type": "color",
"aliases": [
"纵轴颜色",
"坐标轴颜色"
],
"panel": false
}
]
},
{
"key": "hbar-series",
"label": "条形",
"description": "条形的颜色、粗细、圆角、系列名称和数值标签",
"fields": [
{
"path": "props.option.series.0.itemStyle.color",
"label": "条颜色",
"description": "条形的填充颜色",
"value_type": "color",
"aliases": [
"颜色",
"条形颜色",
"主色"
]
},
{
"path": "props.option.series.0.name",
"label": "系列名称",
"description": "这组条形的名称,显示在图例和鼠标悬停提示中;不是图表标题",
"value_type": "string",
"aliases": [
"图例名称",
"系列名",
"数据名称"
],
"panel": false
},
{
"path": "props.option.series.0.barWidth",
"label": "条宽",
"description": "条形粗细,百分比表示占每个类目高度的比例,例如 45%",
"value_type": "string",
"aliases": [
"条形宽度",
"条形粗细"
],
"panel": false
},
{
"path": "props.option.series.0.itemStyle.borderRadius",
"label": "条圆角",
"description": "条形四个角的圆角半径,顺序为左上、右上、右下、左下,默认只有右端圆角",
"value_type": "array",
"item_type": "number",
"unit": "px",
"aliases": [
"圆角"
],
"panel": false
},
{
"path": "props.option.series.0.label.show",
"label": "数值标签",
"description": "是否在每个条形上显示数值",
"value_type": "boolean",
"default": false,
"aliases": [
"显示数值",
"数据标签",
"标签"
],
"panel": false
},
{
"path": "props.option.series.0.label.position",
"label": "标签位置",
"description": "数值标签相对条形的位置,开启数值标签后生效",
"value_type": "enum",
"default": "inside",
"options": [
{
"value": "right",
"label": "条形右侧"
},
{
"value": "inside",
"label": "条内居中"
},
{
"value": "insideRight",
"label": "条内右端"
},
{
"value": "insideLeft",
"label": "条内左端"
}
],
"aliases": [
"数值位置"
],
"panel": false
}
]
},
"chart-encode-xy"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "hbar-chart",
"name": "条形图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "地区销售额",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {},
"dataset": {
"source": [
{
"region": "华东",
"sales": 320
},
{
"region": "华南",
"sales": 240
},
{
"region": "华北",
"sales": 210
},
{
"region": "西南",
"sales": 180
},
{
"region": "西北",
"sales": 130
}
]
},
"grid": {
"top": 86,
"right": 24,
"bottom": 24,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"yAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"series": [
{
"name": "销售额",
"type": "bar",
"barWidth": "45%",
"encode": {
"x": "sales",
"y": "region"
},
"itemStyle": {
"color": "#409eff",
"borderRadius": [
0,
4,
4,
0
]
}
}
]
}
},
"style": null,
"events": []
}
}
+516
View File
@@ -0,0 +1,516 @@
{
"order": 14,
"key": "heatmap-chart",
"type": "heatmap-chart",
"name": "热力图",
"description": "用格子颜色深浅展示两个类目维度交叉处的数值大小",
"tags": [
"charts",
"热力图",
"heatmap-chart",
"热力",
"分布",
"时段"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:widget-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
"数据源每行一个格子,三列分别对应 X、Y、数值,数值列需放在最后一列(visualMap 默认映射最后一个维度)",
"visualMap.min / max 需按实际数据范围调整",
"label.formatter 用 {@字段名} 引用数值列,修改数值字段时需同步修改"
],
"field_groups": [
"node-base",
"chart-title",
"chart-grid",
{
"key": "heatmap-axis",
"label": "坐标轴",
"description": "热力图的横轴和纵轴都是类目轴",
"fields": [
{
"path": "props.option.xAxis.axisLabel.color",
"label": "X轴文字色",
"description": "横轴类目文字的颜色",
"value_type": "color",
"aliases": [
"横轴文字颜色"
],
"panel": false
},
{
"path": "props.option.yAxis.axisLabel.color",
"label": "Y轴文字色",
"description": "纵轴类目文字的颜色",
"value_type": "color",
"aliases": [
"纵轴文字颜色"
],
"panel": false
}
]
},
{
"key": "heatmap-visual",
"label": "色阶",
"description": "数值到格子颜色的映射,数值越大颜色越接近高值颜色",
"fields": [
{
"path": "props.option.visualMap.min",
"label": "最小值",
"description": "色阶的最小值,需按实际数据范围调整",
"value_type": "number",
"span": 12
},
{
"path": "props.option.visualMap.max",
"label": "最大值",
"description": "色阶的最大值,需按实际数据范围调整",
"value_type": "number",
"span": 12
},
{
"path": "props.option.visualMap.inRange.color.0",
"label": "低值颜色",
"description": "最小值对应的格子颜色",
"value_type": "color",
"span": 12
},
{
"path": "props.option.visualMap.inRange.color.4",
"label": "高值颜色",
"description": "最大值对应的格子颜色",
"value_type": "color",
"span": 12
},
{
"path": "props.option.visualMap.inRange.color",
"label": "色阶颜色",
"description": "从低到高的 5 个颜色,整体换配色时修改它",
"value_type": "array",
"item_type": "color",
"aliases": [
"配色",
"渐变色"
],
"panel": false
}
]
},
{
"key": "heatmap-label",
"label": "数值标签",
"description": "格子内显示的数值",
"fields": [
{
"path": "props.option.series.0.label.show",
"label": "显示数值",
"description": "是否在每个格子内显示数值",
"value_type": "boolean",
"aliases": [
"数值标签",
"标签"
]
},
{
"path": "props.option.series.0.label.formatter",
"label": "数值格式",
"description": "格子内数值的显示格式,用 {@字段名} 引用数值列,例如 {@value};修改数值字段时需同步修改",
"value_type": "string",
"aliases": [
"标签格式"
]
},
{
"path": "props.option.series.0.label.color",
"label": "数值文字色",
"description": "格子内数值文字的颜色",
"value_type": "color",
"aliases": [
"标签颜色"
],
"panel": false
}
]
},
"chart-encode-xy",
{
"key": "heatmap-value",
"label": "数值映射",
"description": "格子颜色使用的数值列",
"fields": [
{
"path": "props.option.series.0.encode.value",
"label": "数值字段",
"description": "格子数值使用数据集中的哪一列,必须是 dataset.source 中的最后一列;修改后同步修改数值格式",
"value_type": "string",
"aliases": [
"值字段"
]
}
]
}
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "heatmap-chart",
"name": "热力图",
"layout": {
"x": 0,
"y": 0,
"width": 480,
"height": 300
},
"props": {
"option": {
"title": {
"text": "访问时段分布",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"position": "top"
},
"dataset": {
"source": [
{
"slot": "0-4点",
"day": "周一",
"value": 8
},
{
"slot": "4-8点",
"day": "周一",
"value": 7
},
{
"slot": "8-12点",
"day": "周一",
"value": 46
},
{
"slot": "12-16点",
"day": "周一",
"value": 62
},
{
"slot": "16-20点",
"day": "周一",
"value": 76
},
{
"slot": "20-24点",
"day": "周一",
"value": 84
},
{
"slot": "0-4点",
"day": "周二",
"value": 15
},
{
"slot": "4-8点",
"day": "周二",
"value": 14
},
{
"slot": "8-12点",
"day": "周二",
"value": 42
},
{
"slot": "12-16点",
"day": "周二",
"value": 58
},
{
"slot": "16-20点",
"day": "周二",
"value": 72
},
{
"slot": "20-24点",
"day": "周二",
"value": 80
},
{
"slot": "0-4点",
"day": "周三",
"value": 11
},
{
"slot": "4-8点",
"day": "周三",
"value": 10
},
{
"slot": "8-12点",
"day": "周三",
"value": 49
},
{
"slot": "12-16点",
"day": "周三",
"value": 65
},
{
"slot": "16-20点",
"day": "周三",
"value": 68
},
{
"slot": "20-24点",
"day": "周三",
"value": 76
},
{
"slot": "0-4点",
"day": "周四",
"value": 18
},
{
"slot": "4-8点",
"day": "周四",
"value": 6
},
{
"slot": "8-12点",
"day": "周四",
"value": 45
},
{
"slot": "12-16点",
"day": "周四",
"value": 61
},
{
"slot": "16-20点",
"day": "周四",
"value": 75
},
{
"slot": "20-24点",
"day": "周四",
"value": 83
},
{
"slot": "0-4点",
"day": "周五",
"value": 14
},
{
"slot": "4-8点",
"day": "周五",
"value": 13
},
{
"slot": "8-12点",
"day": "周五",
"value": 52
},
{
"slot": "12-16点",
"day": "周五",
"value": 57
},
{
"slot": "16-20点",
"day": "周五",
"value": 71
},
{
"slot": "20-24点",
"day": "周五",
"value": 79
},
{
"slot": "0-4点",
"day": "周六",
"value": 12
},
{
"slot": "4-8点",
"day": "周六",
"value": 11
},
{
"slot": "8-12点",
"day": "周六",
"value": 61
},
{
"slot": "12-16点",
"day": "周六",
"value": 81
},
{
"slot": "16-20点",
"day": "周六",
"value": 98
},
{
"slot": "20-24点",
"day": "周六",
"value": 97
},
{
"slot": "0-4点",
"day": "周日",
"value": 19
},
{
"slot": "4-8点",
"day": "周日",
"value": 7
},
{
"slot": "8-12点",
"day": "周日",
"value": 57
},
{
"slot": "12-16点",
"day": "周日",
"value": 77
},
{
"slot": "16-20点",
"day": "周日",
"value": 94
},
{
"slot": "20-24点",
"day": "周日",
"value": 104
}
]
},
"grid": {
"top": 48,
"right": 24,
"bottom": 64,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
},
"splitArea": {
"show": true
}
},
"yAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
},
"inverse": true,
"splitArea": {
"show": true
}
},
"visualMap": {
"min": 0,
"max": 120,
"calculable": true,
"orient": "horizontal",
"left": "center",
"bottom": 4,
"itemHeight": 120,
"textStyle": {
"color": "#cfd3dc"
},
"inRange": {
"color": [
"#1d2b44",
"#337ecc",
"#79bbff",
"#e6a23c",
"#f56c6c"
]
}
},
"series": [
{
"name": "访问量",
"type": "heatmap",
"label": {
"show": true,
"formatter": "{@value}",
"color": "#e5eaf3",
"fontSize": 11
},
"itemStyle": {
"borderColor": "#141414",
"borderWidth": 2
},
"encode": {
"x": "slot",
"y": "day",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
+186
View File
@@ -0,0 +1,186 @@
{
"order": 3,
"key": "line-chart",
"type": "line-chart",
"name": "折线图",
"description": "展示数值随时间或有序类目的变化趋势",
"tags": [
"charts",
"折线图",
"line-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:graph-up-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
"chart-axis",
"chart-line-series",
"chart-encode-xy"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "line-chart",
"name": "折线图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "访问量趋势",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "axis"
},
"dataset": {
"source": [
{
"date": "周一",
"visits": 820
},
{
"date": "周二",
"visits": 932
},
{
"date": "周三",
"visits": 901
},
{
"date": "周四",
"visits": 934
},
{
"date": "周五",
"visits": 1290
},
{
"date": "周六",
"visits": 1330
},
{
"date": "周日",
"visits": 1320
}
]
},
"grid": {
"top": 86,
"right": 24,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"boundaryGap": false,
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"yAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"series": [
{
"name": "访问量",
"type": "line",
"smooth": true,
"symbolSize": 8,
"encode": {
"x": "date",
"y": "visits"
},
"lineStyle": {
"width": 3,
"color": "#409eff"
},
"itemStyle": {
"color": "#409eff"
}
}
]
}
},
"style": null,
"events": []
}
}
@@ -0,0 +1,272 @@
{
"order": 13,
"key": "pictorial-bar-chart",
"type": "pictorial-bar-chart",
"name": "象形柱图",
"description": "用重复的图形堆叠成柱子对比各类目数值,视觉上更形象",
"tags": [
"charts",
"象形柱图",
"pictorial-bar-chart",
"象形",
"分段",
"装饰"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:battery-charge-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-grid",
"chart-axis",
{
"key": "pictorial-series",
"label": "图形",
"description": "组成柱子的图形样式",
"fields": [
{
"path": "props.option.series.0.itemStyle.color",
"label": "图形颜色",
"description": "图形的颜色",
"value_type": "color",
"aliases": [
"颜色",
"主色"
]
},
{
"path": "props.option.series.0.symbol",
"label": "图形",
"description": "组成柱子的图形形状",
"value_type": "enum",
"options": [
{
"value": "rect",
"label": "矩形"
},
{
"value": "roundRect",
"label": "圆角矩形"
},
{
"value": "circle",
"label": "圆形"
},
{
"value": "triangle",
"label": "三角形"
},
{
"value": "diamond",
"label": "菱形"
}
],
"aliases": [
"形状"
]
},
{
"path": "props.option.series.0.symbolSize.0",
"label": "图形宽度",
"description": "单个图形的宽度",
"value_type": "number",
"span": 12,
"minimum": 1,
"unit": "px"
},
{
"path": "props.option.series.0.symbolSize.1",
"label": "图形高度",
"description": "单个图形的高度",
"value_type": "number",
"span": 12,
"minimum": 1,
"unit": "px"
},
{
"path": "props.option.series.0.symbolMargin",
"label": "图形间距",
"description": "重复图形之间的间隔",
"value_type": "number",
"span": 12,
"minimum": 0,
"unit": "px",
"aliases": [
"间距"
]
},
{
"path": "props.option.series.0.symbolRepeat",
"label": "重复图形",
"description": "开启后用多个小图形重复堆叠成柱子,关闭后用一个拉伸的图形",
"value_type": "boolean",
"span": 12,
"aliases": [
"重复"
]
},
{
"path": "props.option.series.0.name",
"label": "系列名称",
"description": "这组柱子的名称,显示在鼠标悬停提示中;不是图表标题",
"value_type": "string",
"aliases": [
"系列名",
"数据名称"
],
"panel": false
}
]
},
"chart-encode-xy"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "pictorial-bar-chart",
"name": "象形柱图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"title": {
"text": "各仓库库存量",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "axis",
"axisPointer": {
"type": "none"
}
},
"dataset": {
"source": [
{
"warehouse": "上海仓",
"stock": 86
},
{
"warehouse": "广州仓",
"stock": 72
},
{
"warehouse": "北京仓",
"stock": 64
},
{
"warehouse": "成都仓",
"stock": 45
},
{
"warehouse": "武汉仓",
"stock": 38
}
]
},
"grid": {
"top": 48,
"right": 24,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"yAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"series": [
{
"name": "库存",
"type": "pictorialBar",
"symbol": "rect",
"symbolRepeat": true,
"symbolSize": [
24,
6
],
"symbolMargin": 2,
"symbolClip": true,
"itemStyle": {
"color": "#409eff"
},
"encode": {
"x": "warehouse",
"y": "stock"
}
}
]
}
},
"style": null,
"events": []
}
}
+154
View File
@@ -0,0 +1,154 @@
{
"order": 4,
"key": "pie-chart",
"type": "pie-chart",
"name": "饼图",
"description": "展示各部分占整体的比例,适合类目不多的占比数据",
"tags": [
"charts",
"饼图",
"pie-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-2-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-palette",
"chart-item-series",
"chart-encode-item"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "pie-chart",
"name": "饼图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"color": [
"#409eff",
"#67c23a",
"#e6a23c",
"#f56c6c",
"#909399"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "访问来源占比",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item"
},
"dataset": {
"source": [
{
"channel": "搜索引擎",
"value": 1048
},
{
"channel": "直接访问",
"value": 735
},
{
"channel": "社交媒体",
"value": 484
},
{
"channel": "广告投放",
"value": 300
}
]
},
"series": [
{
"name": "访问来源",
"type": "pie",
"center": [
"50%",
"62%"
],
"avoidLabelOverlap": true,
"label": {
"color": "#cfd3dc"
},
"labelLine": {
"lineStyle": {
"color": "#a3a6ad"
}
},
"encode": {
"itemName": "channel",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
@@ -0,0 +1,213 @@
{
"order": 15,
"key": "progress-ring-chart",
"type": "progress-ring-chart",
"name": "进度环",
"description": "用环形进度条展示单个指标的完成百分比",
"tags": [
"charts",
"进度环",
"progress-ring-chart",
"进度",
"完成率",
"百分比"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-3-linear",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
"基于 gauge 系列,数据源取第一行,前端按 encode 转成 series.data"
],
"field_groups": [
"node-base",
"chart-title",
"chart-gauge-value",
{
"key": "progress-ring-shape",
"label": "环形",
"description": "进度环和底环的粗细与端点",
"fields": [
{
"path": "props.option.series.0.progress.width",
"label": "环宽度",
"description": "进度环的粗细",
"value_type": "number",
"span": 12,
"minimum": 1,
"maximum": 60,
"unit": "px",
"aliases": [
"进度环宽度",
"环的粗细"
]
},
{
"path": "props.option.series.0.axisLine.lineStyle.width",
"label": "底环宽度",
"description": "进度环下方灰色底环的粗细,通常与环宽度相同",
"value_type": "number",
"span": 12,
"minimum": 1,
"maximum": 60,
"unit": "px"
},
{
"path": "props.option.series.0.progress.roundCap",
"label": "圆角端点",
"description": "开启后进度环两端为圆角",
"value_type": "boolean",
"aliases": [
"圆角"
]
}
]
},
"chart-encode-item"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "progress-ring-chart",
"name": "进度环",
"layout": {
"x": 0,
"y": 0,
"width": 260,
"height": 260
},
"props": {
"option": {
"title": {
"text": "年度目标完成率",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item",
"formatter": "{b}: {c}%"
},
"dataset": {
"source": [
{
"name": "完成率",
"value": 68
}
]
},
"series": [
{
"type": "gauge",
"min": 0,
"max": 100,
"startAngle": 90,
"endAngle": -270,
"center": [
"50%",
"56%"
],
"radius": "72%",
"pointer": {
"show": false
},
"progress": {
"show": true,
"roundCap": true,
"width": 14,
"itemStyle": {
"color": "#409eff"
}
},
"axisLine": {
"lineStyle": {
"width": 14,
"color": [
[
1,
"rgba(255, 255, 255, 0.1)"
]
]
}
},
"axisTick": {
"show": false
},
"splitLine": {
"show": false
},
"axisLabel": {
"show": false
},
"title": {
"offsetCenter": [
0,
"28%"
],
"color": "#cfd3dc",
"fontSize": 13
},
"detail": {
"valueAnimation": true,
"offsetCenter": [
0,
"-8%"
],
"color": "#e5eaf3",
"fontSize": 30,
"fontWeight": "bold",
"formatter": "{value}%"
},
"encode": {
"itemName": "name",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
+308
View File
@@ -0,0 +1,308 @@
{
"order": 7,
"key": "radar-chart",
"type": "radar-chart",
"name": "雷达图",
"description": "在多个维度上对比一个或多个对象的得分",
"tags": [
"charts",
"雷达图",
"radar-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:radar-2-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-palette",
{
"key": "radar-series",
"label": "线条",
"description": "各对象雷达线的颜色和填充",
"fields": [
{
"path": "props.option.color.0",
"label": "第一条线颜色",
"description": "第一个对象(数据集第一行)的雷达线颜色",
"value_type": "color",
"aliases": [
"颜色",
"主色"
]
},
{
"path": "props.option.color.1",
"label": "第二条线颜色",
"description": "第二个对象(数据集第二行)的雷达线颜色",
"value_type": "color"
},
{
"path": "props.option.series.0.lineStyle.width",
"label": "线宽",
"description": "雷达线的粗细",
"value_type": "number",
"minimum": 1,
"maximum": 12,
"unit": "px",
"aliases": [
"线条粗细"
],
"panel": false
},
{
"path": "props.option.series.0.areaStyle.opacity",
"label": "填充透明度",
"description": "雷达线围成区域的填充透明度,0 为不填充,1 为不透明",
"value_type": "number",
"minimum": 0,
"maximum": 1,
"aliases": [
"填充",
"透明度"
],
"panel": false
}
]
},
{
"key": "radar-axis",
"label": "维度",
"description": "雷达图的各个维度及其文字样式",
"fields": [
{
"path": "props.option.radar.indicator",
"label": "维度",
"description": "雷达图的维度列表,每项为 {name: 维度名称, max: 满分},顺序与数值字段一一对应",
"value_type": "array",
"item_type": "object",
"aliases": [
"指标",
"维度名称",
"满分"
],
"panel": false
},
{
"path": "props.option.radar.axisName.color",
"label": "维度文字色",
"description": "维度名称文字的颜色",
"value_type": "color",
"aliases": [
"维度颜色",
"指标文字颜色"
],
"panel": false
}
]
},
{
"key": "radar-data",
"label": "数据映射",
"description": "雷达图从数据集(dataset.source)中取哪些列",
"fields": [
{
"path": "props.option.series.0.encode.itemName",
"label": "名称字段",
"description": "每个对象(每条雷达线)的名称使用数据集中的哪一列",
"value_type": "string",
"aliases": [
"对象字段",
"类目字段"
]
},
{
"path": "props.option.series.0.encode.value",
"label": "数值字段",
"description": "各维度的得分依次使用数据集中的哪些列,顺序与维度列表一一对应",
"value_type": "array",
"item_type": "string",
"aliases": [
"维度字段",
"得分字段"
],
"panel": false
}
]
}
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "radar-chart",
"name": "雷达图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 280
},
"props": {
"option": {
"color": [
"#409eff",
"#67c23a",
"#e6a23c",
"#f56c6c",
"#909399"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "店铺服务评分",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {},
"radar": {
"center": [
"50%",
"62%"
],
"radius": "58%",
"indicator": [
{
"name": "商品质量",
"max": 100
},
{
"name": "物流速度",
"max": 100
},
{
"name": "服务态度",
"max": 100
},
{
"name": "描述相符",
"max": 100
},
{
"name": "售后保障",
"max": 100
}
],
"axisName": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.12)"
}
},
"splitArea": {
"areaStyle": {
"color": [
"rgba(255, 255, 255, 0.03)",
"rgba(255, 255, 255, 0.06)"
]
}
},
"axisLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.16)"
}
}
},
"dataset": {
"source": [
{
"name": "店铺A",
"sales": 80,
"market": 70,
"develop": 90,
"support": 85,
"tech": 75
},
{
"name": "店铺B",
"sales": 65,
"market": 88,
"develop": 72,
"support": 78,
"tech": 92
}
]
},
"series": [
{
"name": "评分",
"type": "radar",
"encode": {
"itemName": "name",
"value": [
"sales",
"market",
"develop",
"support",
"tech"
]
},
"lineStyle": {
"width": 2
},
"areaStyle": {
"opacity": 0.18
}
}
]
}
},
"style": null,
"events": []
}
}
+178
View File
@@ -0,0 +1,178 @@
{
"order": 5,
"key": "ring-chart",
"type": "ring-chart",
"name": "环形图",
"description": "中间留空的饼图,展示各部分占整体的比例",
"tags": [
"charts",
"环形图",
"ring-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-3-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-palette",
"chart-item-series",
{
"key": "ring-shape",
"label": "环形",
"description": "环形的内外半径",
"fields": [
{
"path": "props.option.series.0.radius",
"label": "内外半径",
"description": "两个百分比依次为内半径和外半径,相对组件较短边的一半,例如 [\"42%\", \"68%\"];内半径越大环越细",
"value_type": "array",
"item_type": "string",
"aliases": [
"环宽",
"半径",
"环的粗细"
],
"panel": false
}
]
},
"chart-encode-item"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "ring-chart",
"name": "环形图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"color": [
"#409eff",
"#67c23a",
"#e6a23c",
"#f56c6c",
"#909399"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "设备占比",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item"
},
"dataset": {
"source": [
{
"device": "桌面端",
"value": 1048
},
{
"device": "移动端",
"value": 735
},
{
"device": "平板",
"value": 380
},
{
"device": "其他",
"value": 184
}
]
},
"series": [
{
"name": "设备",
"type": "pie",
"radius": [
"42%",
"68%"
],
"center": [
"50%",
"62%"
],
"avoidLabelOverlap": true,
"label": {
"color": "#cfd3dc"
},
"labelLine": {
"lineStyle": {
"color": "#a3a6ad"
}
},
"encode": {
"itemName": "device",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
+211
View File
@@ -0,0 +1,211 @@
{
"order": 12,
"key": "rose-chart",
"type": "rose-chart",
"name": "玫瑰图",
"description": "用扇区半径展示各类目数值大小的饼图变体,差异比普通饼图更醒目",
"tags": [
"charts",
"玫瑰图",
"rose-chart",
"南丁格尔",
"占比"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:pie-chart-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-palette",
"chart-item-series",
{
"key": "rose-shape",
"label": "玫瑰",
"description": "玫瑰图的类型和半径",
"fields": [
{
"path": "props.option.series.0.roseType",
"label": "玫瑰类型",
"description": "扇区如何体现数值",
"value_type": "enum",
"options": [
{
"value": "area",
"label": "面积模式",
"description": "各扇区圆心角相同,半径体现数值"
},
{
"value": "radius",
"label": "半径模式",
"description": "圆心角和半径都体现数值"
}
],
"aliases": [
"模式"
]
},
{
"path": "props.option.series.0.radius",
"label": "内外半径",
"description": "两个百分比依次为内半径和外半径,相对组件较短边的一半,例如 [\"12%\", \"62%\"]",
"value_type": "array",
"item_type": "string",
"aliases": [
"半径"
],
"panel": false
}
]
},
"chart-encode-item"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "rose-chart",
"name": "玫瑰图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"color": [
"#409eff",
"#67c23a",
"#e6a23c",
"#f56c6c",
"#909399",
"#b37feb"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "区域销售分布",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item"
},
"dataset": {
"source": [
{
"region": "华东",
"value": 40
},
{
"region": "华南",
"value": 33
},
{
"region": "华北",
"value": 28
},
{
"region": "西南",
"value": 22
},
{
"region": "华中",
"value": 20
},
{
"region": "西北",
"value": 15
}
]
},
"series": [
{
"name": "区域",
"type": "pie",
"roseType": "area",
"radius": [
"12%",
"62%"
],
"center": [
"50%",
"62%"
],
"itemStyle": {
"borderRadius": 4
},
"label": {
"color": "#cfd3dc"
},
"labelLine": {
"lineStyle": {
"color": "#a3a6ad"
}
},
"encode": {
"itemName": "region",
"value": "value"
}
}
]
}
},
"style": null,
"events": []
}
}
+232
View File
@@ -0,0 +1,232 @@
{
"order": 6,
"key": "scatter-chart",
"type": "scatter-chart",
"name": "散点图",
"description": "用点的横纵位置展示两个数值指标之间的关系和分布",
"tags": [
"charts",
"散点图",
"scatter-chart"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-square-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
"chart-axis",
{
"key": "scatter-series",
"label": "散点",
"description": "散点的颜色、大小和系列名称",
"fields": [
{
"path": "props.option.series.0.itemStyle.color",
"label": "点颜色",
"description": "散点的颜色",
"value_type": "color",
"aliases": [
"颜色",
"散点颜色",
"主色"
]
},
{
"path": "props.option.series.0.symbolSize",
"label": "点大小",
"description": "散点的直径",
"value_type": "number",
"minimum": 1,
"maximum": 60,
"unit": "px",
"aliases": [
"散点大小",
"点的大小"
],
"panel": false
},
{
"path": "props.option.series.0.name",
"label": "系列名称",
"description": "这组散点的名称,显示在图例和鼠标悬停提示中;不是图表标题",
"value_type": "string",
"aliases": [
"图例名称",
"系列名",
"数据名称"
],
"panel": false
}
]
},
"chart-encode-xy"
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "scatter-chart",
"name": "散点图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "访客数与销售额",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "item"
},
"dataset": {
"source": [
{
"users": 12,
"sales": 32
},
{
"users": 28,
"sales": 48
},
{
"users": 35,
"sales": 41
},
{
"users": 47,
"sales": 72
},
{
"users": 58,
"sales": 66
},
{
"users": 69,
"sales": 88
},
{
"users": 81,
"sales": 94
},
{
"users": 90,
"sales": 80
}
]
},
"grid": {
"top": 86,
"right": 24,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "value",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"yAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"series": [
{
"name": "店铺",
"type": "scatter",
"symbolSize": 12,
"encode": {
"x": "users",
"y": "sales"
},
"itemStyle": {
"color": "#409eff"
}
}
]
}
},
"style": null,
"events": []
}
}
@@ -0,0 +1,348 @@
{
"order": 10,
"key": "stacked-bar-chart",
"type": "stacked-bar-chart",
"name": "堆叠柱状图",
"description": "多个系列在同一柱子上堆叠,同时展示总量和各部分构成",
"tags": [
"charts",
"堆叠柱状图",
"stacked-bar-chart",
"堆叠",
"构成"
],
"renderer": "echarts",
"group": "charts",
"icon": "solar:chart-bold",
"sample_data": true,
"usage_notes": [
"默认图表数据仅用于演示,不是真实业务数据",
"通过顶层 dataId 关联已有数据源,当前后端尚未实现绑定操作",
"三个系列共用 stack: total,X 字段需在三个系列的 encode.x 中保持一致"
],
"field_groups": [
"node-base",
"chart-title",
"chart-legend",
"chart-grid",
"chart-axis",
"chart-palette",
{
"key": "stacked-series",
"label": "系列",
"description": "三个堆叠系列的颜色和名称,从下往上依次为系列1、2、3",
"fields": [
{
"path": "props.option.color.0",
"label": "系列1颜色",
"description": "第 1 个系列(从下往上第 1 段)的颜色",
"value_type": "color",
"span": 12,
"aliases": [
"颜色",
"主色"
]
},
{
"path": "props.option.color.1",
"label": "系列2颜色",
"description": "第 2 个系列(从下往上第 2 段)的颜色",
"value_type": "color",
"span": 12
},
{
"path": "props.option.color.2",
"label": "系列3颜色",
"description": "第 3 个系列(从下往上第 3 段)的颜色",
"value_type": "color",
"span": 12
},
{
"path": "props.option.series.0.name",
"label": "系列1名称",
"description": "第 1 个系列的名称,显示在图例和鼠标悬停提示中",
"value_type": "string",
"aliases": [
"图例名称"
],
"panel": false
},
{
"path": "props.option.series.1.name",
"label": "系列2名称",
"description": "第 2 个系列的名称,显示在图例和鼠标悬停提示中",
"value_type": "string",
"aliases": [
"图例名称"
],
"panel": false
},
{
"path": "props.option.series.2.name",
"label": "系列3名称",
"description": "第 3 个系列的名称,显示在图例和鼠标悬停提示中",
"value_type": "string",
"aliases": [
"图例名称"
],
"panel": false
}
]
},
{
"key": "stacked-data",
"label": "数据映射",
"description": "堆叠柱状图从数据集(dataset.source)中取哪些列",
"fields": [
{
"path": "props.option.series.0.encode.x",
"label": "X字段",
"description": "横轴类目使用数据集中的哪一列;三个系列必须相同,修改时同时修改系列2、系列3的X字段",
"value_type": "string",
"aliases": [
"横轴字段",
"类目字段"
]
},
{
"path": "props.option.series.1.encode.x",
"label": "系列2X字段",
"description": "须与X字段保持一致,修改X字段时同步修改",
"value_type": "string",
"panel": false
},
{
"path": "props.option.series.2.encode.x",
"label": "系列3X字段",
"description": "须与X字段保持一致,修改X字段时同步修改",
"value_type": "string",
"panel": false
},
{
"path": "props.option.series.0.encode.y",
"label": "系列1字段",
"description": "第 1 个系列的数值使用数据集中的哪一列",
"value_type": "string",
"aliases": [
"系列1数值字段"
]
},
{
"path": "props.option.series.1.encode.y",
"label": "系列2字段",
"description": "第 2 个系列的数值使用数据集中的哪一列",
"value_type": "string",
"aliases": [
"系列2数值字段"
]
},
{
"path": "props.option.series.2.encode.y",
"label": "系列3字段",
"description": "第 3 个系列的数值使用数据集中的哪一列",
"value_type": "string",
"aliases": [
"系列3数值字段"
]
}
]
}
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "stacked-bar-chart",
"name": "堆叠柱状图",
"layout": {
"x": 0,
"y": 0,
"width": 400,
"height": 260
},
"props": {
"option": {
"color": [
"#409eff",
"#67c23a",
"#e6a23c",
"#f56c6c",
"#909399"
],
"legend": {
"top": 38,
"left": "center",
"itemWidth": 12,
"itemHeight": 8,
"show": true,
"textStyle": {
"color": "#cfd3dc"
}
},
"title": {
"text": "各渠道月度销售额",
"top": 8,
"left": "center",
"textStyle": {
"color": "#e5eaf3",
"fontSize": 16
}
},
"tooltip": {
"trigger": "axis",
"axisPointer": {
"type": "shadow"
}
},
"dataset": {
"source": [
{
"month": "一月",
"online": 120,
"offline": 220,
"agent": 150
},
{
"month": "二月",
"online": 132,
"offline": 182,
"agent": 212
},
{
"month": "三月",
"online": 101,
"offline": 191,
"agent": 201
},
{
"month": "四月",
"online": 134,
"offline": 234,
"agent": 154
},
{
"month": "五月",
"online": 90,
"offline": 290,
"agent": 190
},
{
"month": "六月",
"online": 230,
"offline": 330,
"agent": 330
}
]
},
"grid": {
"top": 86,
"right": 24,
"bottom": 32,
"left": 48,
"containLabel": true
},
"xAxis": {
"type": "category",
"axisLine": {
"lineStyle": {
"color": "#4c4d4f"
}
},
"axisLabel": {
"color": "#cfd3dc"
}
},
"yAxis": {
"type": "value",
"axisLabel": {
"color": "#cfd3dc"
},
"splitLine": {
"lineStyle": {
"color": "rgba(255, 255, 255, 0.08)"
}
}
},
"series": [
{
"name": "线上",
"type": "bar",
"stack": "total",
"barWidth": "45%",
"encode": {
"x": "month",
"y": "online"
},
"itemStyle": {}
},
{
"name": "门店",
"type": "bar",
"stack": "total",
"barWidth": "45%",
"encode": {
"x": "month",
"y": "offline"
},
"itemStyle": {}
},
{
"name": "代理",
"type": "bar",
"stack": "total",
"barWidth": "45%",
"encode": {
"x": "month",
"y": "agent"
},
"itemStyle": {
"borderRadius": [
4,
4,
0,
0
]
}
}
]
}
},
"style": null,
"events": []
}
}
+213
View File
@@ -0,0 +1,213 @@
{
"order": 16,
"key": "text",
"type": "text",
"name": "文本",
"description": "显示一段文字,常用作大屏标题、说明或标签",
"tags": [
"info",
"文本",
"text"
],
"renderer": "text",
"group": "info",
"icon": "solar:text-bold",
"sample_data": false,
"usage_notes": [
"events 中代码只作为资料保存,不由后端执行"
],
"field_groups": [
"node-base",
{
"key": "text-content",
"label": "内容",
"description": "文本组件显示的文字",
"fields": [
{
"path": "props.content",
"label": "内容",
"description": "文本组件显示的文字",
"value_type": "string",
"aliases": [
"文字",
"文本内容",
"标题文字",
"名字"
]
}
]
},
{
"key": "text-style",
"label": "文字样式",
"description": "文字的颜色、大小、粗细和对齐",
"fields": [
{
"path": "style.color",
"label": "颜色",
"description": "文字的颜色",
"value_type": "color",
"aliases": [
"文字颜色",
"字体颜色"
]
},
{
"path": "style.fontSize",
"label": "字号",
"description": "文字的大小",
"value_type": "number",
"minimum": 8,
"maximum": 200,
"unit": "px",
"aliases": [
"字体大小",
"文字大小"
]
},
{
"path": "style.textAlign",
"label": "水平对齐",
"description": "文字在组件内的水平位置",
"value_type": "enum",
"options": [
{
"value": "left",
"label": "左对齐"
},
{
"value": "center",
"label": "居中"
},
{
"value": "right",
"label": "右对齐"
}
],
"aliases": [
"对齐"
]
},
{
"path": "style.alignItems",
"label": "垂直对齐",
"description": "文字在组件内的垂直位置",
"value_type": "enum",
"options": [
{
"value": "flex-start",
"label": "顶部"
},
{
"value": "center",
"label": "居中"
},
{
"value": "flex-end",
"label": "底部"
}
]
},
{
"path": "style.fontWeight",
"label": "字重",
"description": "文字粗细",
"value_type": "enum",
"default": "normal",
"options": [
{
"value": "normal",
"label": "常规"
},
{
"value": "bold",
"label": "加粗"
}
],
"aliases": [
"加粗",
"粗细"
],
"panel": false
},
{
"path": "style.backgroundColor",
"label": "背景色",
"description": "文本组件的背景颜色,默认透明",
"value_type": "color",
"default": "transparent",
"aliases": [
"背景颜色",
"底色"
],
"panel": false
}
]
}
],
"eventOptions": [
{
"label": "点击事件",
"value": "click"
},
{
"label": "双击事件",
"value": "dblclick"
},
{
"label": "鼠标进入事件",
"value": "mouseenter"
},
{
"label": "鼠标离开事件",
"value": "mouseleave"
},
{
"label": "鼠标按下事件",
"value": "mousedown"
},
{
"label": "鼠标抬起事件",
"value": "mouseup"
},
{
"label": "鼠标移动事件",
"value": "mousemove"
},
{
"label": "鼠标滚轮事件",
"value": "mousewheel"
},
{
"label": "生命周期事件",
"value": "vnodeMounted"
}
],
"schema": {
"type": "text",
"name": "文本",
"layout": {
"x": 0,
"y": 0,
"width": 200,
"height": 50
},
"props": {
"content": "电商运营数据看板"
},
"style": {
"color": "#e5eaf3",
"fontSize": 16,
"textAlign": "center",
"alignItems": "center"
},
"events": [
{
"type": "click",
"title": "点击事件",
"name": "fn",
"code": "console.log(123)"
}
]
}
}
+3 -1
View File
@@ -9,7 +9,9 @@ class ActionValidationError(BaseModel):
action_index: int | None = Field(
default=None, ge=0, description="失败动作的下标,从 0 开始,整批错误为 null"
)
code: Literal["schema_error", "generation_error", "scope_error", "execution_error"]
code: Literal[
"schema_error", "generation_error", "scope_error", "field_error", "execution_error"
]
message: str
+19 -8
View File
@@ -1,8 +1,8 @@
"""精确更新组件 props 或 style 下的一个字段"""
"""精确更新组件名称,或 props / style 下的一个字段"""
from typing import Annotated, Literal
from typing import Annotated, Literal, Self
from pydantic import Field, JsonValue
from pydantic import Field, JsonValue, model_validator
from app.schemas.base import NonBlankString
@@ -13,10 +13,21 @@ PathSegment = NonBlankString | Annotated[int, Field(ge=0)]
class UpdateAction(BaseAction):
type: Literal["update"] = "update"
section: Literal["props", "style"] = Field(description="允许更新的组件属性区域")
section: Literal["name", "props", "style"] = Field(
description="字段路径的第一段:name 为组件名称,props / style 为组件属性区域"
)
path: list[PathSegment] = Field(
min_length=1, description="区域内的字段路径,字符串为对象键,整数为数组下标"
)
value: JsonValue = Field(
description="替换该路径的值,支持任意 JSON;null 是赋值,不表示删除"
description="字段路径第一段之后的各段,字符串为对象键,整数为数组下标;name 没有后续路径,传空列表"
)
value: JsonValue = Field(description="替换该字段的值,必须符合字段定义的类型和取值")
@model_validator(mode="after")
def path_matches_section(self) -> Self:
if (self.section == "name") != (not self.path):
raise ValueError("section 为 name 时 path 为空,props / style 至少一段路径")
return self
@property
def field_path(self) -> str:
"""与物料字段定义一致的点路径,例如 props.option.title.text"""
return ".".join([self.section, *map(str, self.path)])
+69 -17
View File
@@ -5,6 +5,7 @@ from typing import Literal, 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
@@ -26,15 +27,6 @@ class MaterialTemplate(ExtensibleModel):
events: list[JsonObject] = Field(default_factory=list)
class MaterialSetter(ExtensibleModel):
type: NonBlankString = Field(description="前端属性控件类型,不是完整的值类型约束")
label: NonBlankString
key: NonBlankString = Field(
description="前端原始点路径,例如 props.option.series.0.itemStyle.color"
)
props: JsonObject = Field(default_factory=dict)
class ComponentDefinition(ExtensibleModel):
model_config = ConfigDict(serialize_by_alias=True)
key: NonBlankString = Field(description="目录中的唯一编号,不是 Screen 节点 ID")
@@ -52,22 +44,26 @@ class ComponentDefinition(ExtensibleModel):
schema_: MaterialTemplate = Field(
alias="schema", description="完整默认物料配置,不包含实例 ID"
)
setters: list[MaterialSetter] = Field(default_factory=list)
field_groups: list[NonBlankString | FieldGroup] = Field(
default_factory=list,
description="字段分组:字符串引用目录的共享分组,对象为本物料独有的分组;"
"前端属性面板的 setters 由它派生,不单独保存",
)
eventOptions: list[JsonObject] = Field(default_factory=list)
sample_data: bool = Field(
default=False, description="默认配置是否含示例数据,不能视为真实业务数据"
)
props_schema: JsonObject | None = Field(
default=None,
description="可选的 props JSON Schema,未提供表示未知,不表示无限制",
)
style_schema: JsonObject | None = Field(
default=None, description="可选的 style JSON Schema,仅保存定义,当前不执行校验"
)
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:
@@ -77,17 +73,73 @@ class ComponentDefinition(ExtensibleModel):
return self
def _default_problem(field, template: JsonObject) -> str | None:
"""默认值只有一个来源:默认配置有该路径时取它,没有时字段必须声明 default"""
try:
current = lookup(template, field.segments)
except ValueError:
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 引用"
)
@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 不能重复")
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}")
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
+155
View File
@@ -0,0 +1,155 @@
"""物料字段定义:分组 → 字段,每个字段带路径、含义、值类型和取值约束
字段定义是“组件有哪些可改属性”的唯一来源:前端属性面板的 setters、
agent 查询字段、动作校验都由它派生,不再各自维护一份
"""
from typing import Literal, Self
from pydantic import Field, JsonValue, model_validator
from app.schemas.base import ExtensibleModel, NonBlankString
# 字段路径的第一段:节点名称、布局、组件属性、组件样式
FIELD_ROOTS = ("name", "layout", "props", "style")
ValueType = Literal[
"string", "number", "integer", "boolean", "color", "enum", "array", "object"
]
# 数组元素只用基础类型,枚举和嵌套数组没有需要的场景
ItemType = Literal["string", "number", "integer", "boolean", "color", "object"]
# 值类型对应的前端属性控件;array / object 没有通用控件,只能不在面板显示
PANEL_CONTROLS: dict[str, str] = {
"string": "input",
"number": "number",
"integer": "number",
"boolean": "checkbox",
"color": "color",
"enum": "select",
}
# 默认配置中没有该路径时的标记,区别于值为 null
MISSING = object()
# 值类型对应的 JSON 类型,用于检查默认配置里的现值
_JSON_TYPES: dict[str, type | tuple[type, ...]] = {
"string": str,
"color": str,
"number": (int, float),
"integer": int,
"boolean": bool,
"array": list,
"object": dict,
}
def _type_error(value_type: str, value: JsonValue) -> str | None:
# bool 是 int 的子类,数值类型需要单独排除
if not isinstance(value, _JSON_TYPES[value_type]) or (
value_type in ("number", "integer") and isinstance(value, bool)
):
return f"类型必须是 {value_type}"
return None
def lookup(value: JsonValue, segments: list[str | int]):
"""按路径取值;路径不存在返回 MISSING,路径中途遇到标量时抛 ValueError"""
for segment in segments:
if isinstance(value, dict) and isinstance(segment, str):
if segment not in value:
return MISSING
value = value[segment]
elif isinstance(value, list) and isinstance(segment, int):
if segment >= len(value):
return MISSING
value = value[segment]
else:
raise ValueError(f"路径无法继续访问: {segments}")
return value
class FieldOption(ExtensibleModel):
value: JsonValue
label: NonBlankString
description: NonBlankString | None = None
class MaterialField(ExtensibleModel):
path: NonBlankString = Field(
description="点路径,与前端 setter key 一致,数字段为数组下标,例如 props.option.series.0.itemStyle.color"
)
label: NonBlankString = Field(description="字段名称,属性面板显示的文字")
description: NonBlankString = Field(
description="字段含义、在画面上的效果,以及用户通常怎么称呼它"
)
value_type: ValueType
options: list[FieldOption] = Field(
default_factory=list, description="enum 的可选值,每个值可带说明"
)
item_type: ItemType | None = Field(
default=None, description="array 的元素类型"
)
minimum: int | float | None = None
maximum: int | float | None = None
unit: NonBlankString | None = Field(default=None, description="数值单位,例如 px")
default: JsonValue = Field(
default=None,
description="物料默认配置中没有该路径时的默认值;有该路径时以默认配置为准,不在这里重复",
)
aliases: list[NonBlankString] = Field(
default_factory=list, description="用户可能的叫法,供字段搜索使用"
)
editable: bool = Field(default=True, description="是否允许修改")
panel: bool = Field(default=True, description="是否显示在前端属性面板")
span: int | None = Field(default=None, description="属性面板栅格宽度,24 为整行")
@model_validator(mode="after")
def consistent(self) -> Self:
if self.path.split(".")[0] not in FIELD_ROOTS:
raise ValueError(f"字段路径必须以 {'/'.join(FIELD_ROOTS)} 开头: {self.path}")
if (self.value_type == "enum") != bool(self.options):
raise ValueError(f"只有 enum 字段提供 options,且 enum 必须提供: {self.path}")
if (self.value_type == "array") != (self.item_type is not None):
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}")
return self
def value_error(self, value: JsonValue) -> str | None:
"""检查值是否符合字段类型和取值约束,符合返回 None"""
if self.value_type == "enum":
allowed = [option.value for option in self.options]
return None if value in allowed else f"取值必须是 {allowed} 之一"
if error := _type_error(self.value_type, value):
return error
if self.item_type is not None:
for element in value:
if error := _type_error(self.item_type, element):
return f"数组元素{error}"
if self.value_type in ("number", "integer"):
if self.minimum is not None and value < self.minimum:
return f"不能小于 {self.minimum}"
if self.maximum is not None and value > self.maximum:
return f"不能大于 {self.maximum}"
return None
@property
def segments(self) -> list[str | int]:
# 数字段视为数组下标,与前端 setter key 的解析方式一致
return [int(part) if part.isdigit() else part for part in self.path.split(".")]
class FieldGroup(ExtensibleModel):
key: NonBlankString = Field(description="分组编号,共享分组在整个目录内唯一")
label: NonBlankString
description: NonBlankString = Field(description="这一组字段管什么,供模型先看目录再查字段")
fields: list[MaterialField]
@model_validator(mode="after")
def unique_paths(self) -> Self:
paths = [field.path for field in self.fields]
if len(paths) != len(set(paths)):
raise ValueError(f"分组 {self.key} 中字段路径重复")
return self
+1 -1
View File
@@ -30,7 +30,7 @@ class RequirementChange(ExtensibleModel):
operation: NonBlankString = Field(description=f"需求操作。{OPERATION_GUIDE}")
parameters: JsonObject = Field(
default_factory=dict,
description="需求参数,例如 delta_x=50;保留用户要求的单位与相对变化语义",
description="需求参数,例如 delta_x=50;保留用户要求的单位与相对变化语义;属性修改用用户的说法描述改什么、改成什么,例如 {\"名字\": \"月度销售\"},不推断字段路径",
)
@field_validator("operation")
+29 -18
View File
@@ -1,4 +1,5 @@
"""物料目录的 MongoDB 存储:每个组件一条记录,目录版本放在元数据记录中"""
"""物料目录的 MongoDB 存储:每个组件一条记录,目录版本放在元数据记录中,
多个物料共用的字段分组单独存放,每个分组一条记录"""
from loguru import logger
from pymongo.asynchronous.database import AsyncDatabase
@@ -14,6 +15,7 @@ class MongoMaterialRepository:
def __init__(self, db: AsyncDatabase, prefix: str = ""):
# 集合名前缀用于测试隔离
self._materials = db.get_collection(f"{prefix}materials")
self._field_groups = db.get_collection(f"{prefix}material_field_groups")
async def load_all(self) -> ComponentCatalog | None:
# 没有元数据记录视为尚未导入,由调用方决定回退方式
@@ -21,33 +23,42 @@ class MongoMaterialRepository:
if meta is None:
return None
documents = await self._materials.find({"_id": {"$ne": META_ID}}).to_list()
components = [
{key: value for key, value in item.items() if key not in ("_id", "order")}
for item in sorted(documents, key=lambda item: item.get("order", 0))
]
groups = await self._field_groups.find({}).to_list()
return ComponentCatalog.model_validate(
{
"version": meta["version"],
"components": components,
"components": _ordered(documents),
"field_groups": _ordered(groups),
}
)
async def replace_all(self, catalog: ComponentCatalog) -> None:
# 整体替换目录,供导入脚本使用;order 保持导出文件里的组件顺序
documents = [
{
**component.model_dump(mode="json", by_alias=True),
"_id": component.key,
"order": index,
}
for index, component in enumerate(catalog.components)
]
await self._materials.delete_many({})
if documents:
await self._materials.insert_many(documents)
# 整体替换目录,供导入脚本使用;order 保持源文件中的顺序
for collection, items in (
(self._materials, catalog.components),
(self._field_groups, catalog.field_groups),
):
await collection.delete_many({})
if items:
await collection.insert_many(_documents(items))
await self._materials.insert_one({"_id": META_ID, "version": catalog.version})
def _documents(items) -> list[dict]:
# 记录 _id 为物料或分组的 key
return [
{**item.model_dump(mode="json", by_alias=True), "_id": item.key, "order": index}
for index, item in enumerate(items)
]
def _ordered(documents: list[dict]) -> list[dict]:
return [
{key: value for key, value in item.items() if key not in ("_id", "order")}
for item in sorted(documents, key=lambda item: item.get("order", 0))
]
async def warm_material_cache(db: AsyncDatabase | None) -> ComponentCatalog | None:
"""启动时把物料目录从 MongoDB 载入进程缓存;未启用或尚未导入时目录为空,不回退读文件"""
catalog = await MongoMaterialRepository(db).load_all() if db is not None else None