Restore AI workflow design nodes

This commit is contained in:
2026-06-09 21:18:33 +08:00
parent fbd726ebdf
commit cb45c0a068
71 changed files with 22676 additions and 21 deletions
+28
View File
@@ -204,12 +204,38 @@ web/apps/web-ele/src/router/routes/modules/online-dev.ts
web/apps/web-ele/src/router/routes/modules/zq-smart-table.ts
web/apps/web-ele/src/components/report-design/
backend-fastapi/online_dev/
!backend-fastapi/online_dev/
backend-fastapi/online_dev/*
!backend-fastapi/online_dev/__init__.py
!backend-fastapi/online_dev/form_manager/
!backend-fastapi/online_dev/form_manager/**
!backend-fastapi/online_dev/form_data_manager/
!backend-fastapi/online_dev/form_data_manager/**
!backend-fastapi/online_dev/page_manager/
!backend-fastapi/online_dev/page_manager/**
backend-fastapi/zq_demo/
backend-fastapi/zq_smart_table/
backend-fastapi/ai_platform/nodes/builtin/app_*_node.py
backend-fastapi/ai_platform/nodes/builtin/dashboard_*_node.py
backend-fastapi/ai_platform/nodes/builtin/form_*_node.py
backend-fastapi/ai_platform/nodes/builtin/system_summary_node.py
!backend-fastapi/ai_platform/nodes/builtin/app_create_node.py
!backend-fastapi/ai_platform/nodes/builtin/app_design_node.py
!backend-fastapi/ai_platform/nodes/builtin/app_settings_node.py
!backend-fastapi/ai_platform/nodes/builtin/app_update_node.py
!backend-fastapi/ai_platform/nodes/builtin/dashboard_basic_info_node.py
!backend-fastapi/ai_platform/nodes/builtin/dashboard_create_node.py
!backend-fastapi/ai_platform/nodes/builtin/dashboard_design_node.py
!backend-fastapi/ai_platform/nodes/builtin/dashboard_publish_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_basic_info_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_create_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_data_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_database_create_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_database_design_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_list_design_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_publish_node.py
!backend-fastapi/ai_platform/nodes/builtin/form_ui_design_node.py
!backend-fastapi/ai_platform/nodes/builtin/system_summary_node.py
web/apps/web-ele/src/api/core/database-monitor.ts
web/apps/web-ele/src/api/core/demo.ts
web/apps/web-ele/src/api/core/dingtalk-sync.ts
@@ -240,6 +266,8 @@ web/apps/web-ele/src/views/_core/system-config/modules/feishu-sync-form.vue
web/apps/web-ele/src/views/_core/system-config/modules/wecom-sync-form.vue
web/apps/web-ele/src/views/ai-platform/workflow/editor/nodes/SystemSummaryNode.vue
web/apps/web-ele/src/views/ai-platform/workflow/editor/panels/SystemSummaryPanel.vue
!web/apps/web-ele/src/views/ai-platform/workflow/editor/nodes/SystemSummaryNode.vue
!web/apps/web-ele/src/views/ai-platform/workflow/editor/panels/SystemSummaryPanel.vue
web/apps/web-ele/src/views/wiki/
web/apps/web-ele/src/components/zq-form/table-selector/types.ts
@@ -0,0 +1,96 @@
"""
Lightweight form metadata API for AI workflow panels.
"""
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_schema import PaginatedResponse
from app.database import get_db
from core.application.model import Application
from online_dev.form_manager.model import FormMeta
router = APIRouter(prefix="/forms", tags=["AI-Forms"])
def _build_form_list_out(form, application_name: str = "", application_code: str = "") -> dict:
return {
"id": str(form.id),
"application_id": form.application_id,
"application_name": application_name or "Main App",
"application_code": application_code or "",
"name": form.name,
"code": form.code,
"status": form.status,
}
@router.get("/list", summary="List form metadata for AI workflow form-data nodes")
async def list_forms(
application_id: Optional[str] = Query(None, alias="applicationId"),
name: Optional[str] = Query(None),
code: Optional[str] = Query(None),
status: Optional[str] = Query(None),
include_globally_visible: bool = Query(False, alias="includeGloballyVisible"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100, alias="pageSize"),
db: AsyncSession = Depends(get_db),
):
conditions = [FormMeta.is_deleted.is_(False)]
if application_id:
app_condition = FormMeta.application_id == application_id
if include_globally_visible:
app_condition = or_(
app_condition,
and_(
FormMeta.globally_visible.is_(True),
FormMeta.status == "published",
),
)
conditions.append(app_condition)
elif not include_globally_visible:
conditions.append(FormMeta.application_id.is_(None))
else:
conditions.append(
or_(
FormMeta.application_id.is_(None),
and_(
FormMeta.globally_visible.is_(True),
FormMeta.status == "published",
),
)
)
if name:
conditions.append(FormMeta.name.ilike(f"%{name}%"))
if code:
conditions.append(FormMeta.code.ilike(f"%{code}%"))
if status:
conditions.append(FormMeta.status == status)
count_stmt = select(func.count(FormMeta.id)).where(and_(*conditions))
total = (await db.execute(count_stmt)).scalar() or 0
offset = (page - 1) * page_size
stmt = (
select(
FormMeta,
Application.name.label("application_name"),
Application.code.label("application_code"),
)
.outerjoin(Application, FormMeta.application_id == Application.id)
.where(and_(*conditions))
.order_by(FormMeta.sort, FormMeta.sys_create_datetime.desc())
.offset(offset)
.limit(page_size)
)
result = await db.execute(stmt)
items = [
_build_form_list_out(form, application_name, application_code)
for form, application_name, application_code in result
]
return PaginatedResponse(items=items, total=total)
@@ -0,0 +1,258 @@
"""
应用创建节点
创建一个新的应用(子应用)
"""
import logging
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class AppCreateNode(BaseNode):
"""
应用创建节点
创建一个新的应用(低代码平台的顶层容器)
"""
node_type = 'app_create'
node_name = '创建应用'
node_category = 'application'
node_icon = 'app-window'
node_description = '创建一个新的应用(子应用)'
inputs = [
{
'name': 'name',
'type': 'string',
'description': '应用名称',
'required': True,
},
{
'name': 'code',
'type': 'string',
'description': '应用编码(唯一标识,用于URL路由,只能包含字母、数字、下划线和连字符,必须以字母开头)',
'required': True,
},
{
'name': 'description',
'type': 'string',
'description': '应用描述',
'required': False,
},
{
'name': 'icon',
'type': 'string',
'description': '应用图标',
'required': False,
},
{
'name': 'app_type',
'type': 'string',
'description': '应用类型: form-表单应用, workflow-流程应用, dashboard-数据应用, screen-大屏应用, mixed-混合应用',
'required': False,
},
]
outputs = [
{
'name': 'app_id',
'type': 'string',
'description': '创建的应用ID',
},
{
'name': 'app_code',
'type': 'string',
'description': '应用编码',
},
{
'name': 'app_name',
'type': 'string',
'description': '应用名称',
},
{
'name': 'success',
'type': 'boolean',
'description': '是否创建成功',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点(同步方法,通过运行异步方法实现)"""
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
else:
return loop.run_until_complete(self.execute_async(context))
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 获取输入参数并解析变量引用
name = context.resolve_template(self.config.get('name', ''))
code_base = context.resolve_template(self.config.get('code', ''))
description = context.resolve_template(self.config.get('description', ''))
icon = context.resolve_template(self.config.get('icon', ''))
app_type = context.resolve_template(self.config.get('app_type', 'mixed'))
# 配置选项
update_if_exists = self.config.get('update_if_exists', False)
logger.info(f'AppCreateNode - 开始创建应用: name={name}, code={code_base}')
# 验证必要参数
if not name:
return NodeResult(
success=False,
error='应用名称不能为空',
)
if not code_base:
return NodeResult(
success=False,
error='应用编码不能为空',
)
# 验证编码格式(字母开头,只能包含字母、数字、下划线和连字符)
import re
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', code_base):
return NodeResult(
success=False,
error='应用编码格式不正确,必须以字母开头,只能包含字母、数字、下划线和连字符',
)
# 导入服务和数据库会话
from app.database import AsyncSessionLocal
from core.application.service import ApplicationService
from core.application.schema import ApplicationCreate, ApplicationUpdate
async with AsyncSessionLocal() as db:
# 生成唯一的 code(如果已存在则自动添加后缀)
code = await self._get_unique_code(db, code_base, ApplicationService)
logger.info(f'AppCreateNode - 使用 code: {code} (原始: {code_base})')
# 检查应用编码是否已存在(理论上不应该存在,因为已经生成了唯一 code)
existing_app = await ApplicationService.get_by_code(db, code)
if existing_app:
if update_if_exists:
# 更新已存在的应用
update_data = ApplicationUpdate(
name=name,
description=description or None,
icon=icon or None,
app_type=app_type or None,
)
updated_app = await ApplicationService.update(
db,
record_id=existing_app.id,
data=update_data,
auto_commit=True
)
if updated_app:
logger.info(f'AppCreateNode - 应用已存在,更新成功: id={updated_app.id}')
return NodeResult(
success=True,
output=f'应用 {name} 更新成功',
output_variables={
'app_id': updated_app.id,
'app_code': updated_app.code,
'app_name': updated_app.name,
'success': True,
}
)
else:
return NodeResult(
success=False,
error=f'更新应用失败',
)
else:
# 应用已存在,返回已存在的应用信息
logger.info(f'AppCreateNode - 应用编码已存在: code={code}, id={existing_app.id}')
return NodeResult(
success=True,
output=f'应用编码 {code} 已存在',
output_variables={
'app_id': existing_app.id,
'app_code': existing_app.code,
'app_name': existing_app.name,
'success': False, # 标记为未创建新应用
}
)
# 创建新应用
create_data = ApplicationCreate(
name=name,
code=code,
description=description or '',
icon=icon or '',
app_type=app_type or 'mixed',
)
new_app = await ApplicationService.create(db, data=create_data, auto_commit=True)
if new_app:
logger.info(f'AppCreateNode - 应用创建成功: id={new_app.id}, code={new_app.code}')
return NodeResult(
success=True,
output=f'应用 {name} 创建成功',
output_variables={
'app_id': new_app.id,
'app_code': new_app.code,
'app_name': new_app.name,
'success': True,
}
)
else:
return NodeResult(
success=False,
error='创建应用失败',
)
except Exception as e:
logger.exception(f'AppCreateNode - 执行异常: {e}')
return NodeResult(
success=False,
error=f'创建应用异常: {str(e)}',
)
async def _get_unique_code(self, db, base_code: str, service) -> str:
"""
生成唯一的 code,如果已存在则自动添加数字后缀
Args:
db: 数据库会话
base_code: 基础 code
service: 服务类(需要有 get_by_code 方法)
Returns:
唯一的 code
"""
code = base_code
counter = 1
while True:
try:
existing = await service.get_by_code(db, code)
if not existing:
# code 不存在,可以使用
break
# code 已存在,添加后缀
code = f"{base_code}_{counter}"
counter += 1
except Exception:
# 查询出错,认为 code 可用
break
return code
@@ -0,0 +1,205 @@
"""
应用设计节点
接收LLM生成的应用设计方案,展示给用户确认或编辑
"""
import json
import logging
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class AppDesignNode(BaseNode):
"""
应用设计节点
接收LLM生成的应用设计方案(Markdown格式),展示给用户确认或编辑
用户可以查看设计方案,编辑后确认继续
"""
node_type = 'app_design'
node_name = '应用设计'
node_category = 'application'
node_icon = 'layout'
node_description = '展示应用设计方案,支持用户确认或编辑'
inputs = [
{
'name': 'design_content',
'type': 'string',
'description': '设计方案内容(Markdown格式)',
'required': True,
},
{
'name': 'design_title',
'type': 'string',
'description': '设计方案标题',
'required': False,
},
]
outputs = [
{
'name': 'design_content',
'type': 'string',
'description': '确认后的设计方案内容',
},
{
'name': 'confirmed',
'type': 'boolean',
'description': '是否已确认',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info(f'AppDesignNode - 使用用户编辑的数据')
# 使用编辑后的数据,清除用户输入
context.variables.pop('__user_input__', None)
return self._build_result(edited_data, require_confirmation=False)
# 获取输入参数
design_content = context.resolve_template(self.config.get('design_content', ''))
design_title = context.resolve_template(self.config.get('design_title', '应用设计方案'))
logger.info(f'AppDesignNode - 输入参数: title={design_title}, content_length={len(design_content)}')
# 验证必填字段
if not design_content or not design_content.strip():
return NodeResult(
success=False,
error='设计方案内容不能为空',
)
design_content = design_content.strip()
design_title = design_title.strip() if design_title else '应用设计方案'
# 构建设计数据
design_data = {
'title': design_title,
'content': design_content,
}
logger.info(f'AppDesignNode - 输出设计方案: title={design_title}')
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
return self._build_result(design_data, require_confirmation)
except Exception as e:
logger.exception(f'应用设计节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点(直接调用同步方法)"""
return self.execute(context)
def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户编辑的数据"""
if isinstance(user_input, dict):
# 检查是否包含设计内容字段
if 'content' in user_input:
return user_input
return None
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict) and 'content' in data:
return data
except (json.JSONDecodeError, TypeError):
# 如果不是JSON,可能直接是编辑后的内容
return {'content': user_input, 'title': '应用设计方案'}
return None
def _build_result(self, design_data: Dict[str, Any], require_confirmation: bool = False) -> NodeResult:
"""构建节点结果"""
title = design_data.get('title', '应用设计方案')
content = design_data.get('content', '')
# 构建预览数据
preview_data = {
'type': 'app_design',
'title': title,
'data': design_data,
'editable': True,
} if require_confirmation else None
return NodeResult(
success=True,
output=design_data,
output_variables={
'design_content': content,
'design_title': title,
'confirmed': not require_confirmation,
},
preview=preview_data,
waiting_for_input=require_confirmation,
waiting_config={
'type': 'design_preview',
'preview_type': 'app_design',
'title': title,
'message': '请确认或编辑应用设计方案',
'data': design_data,
} if require_confirmation else {},
)
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""
获取节点配置 Schema(供前端表单渲染)
Returns:
配置 Schema
"""
return {
'type': 'object',
'properties': {
'design_content': {
'type': 'string',
'title': '设计方案内容',
'description': '设计方案内容(Markdown格式),支持变量引用如 {{llm_output}}',
'required': True,
'x-component': 'Textarea',
'x-component-props': {
'placeholder': '请输入设计方案内容或使用变量 {{llm_output}}',
'rows': 6,
},
},
'design_title': {
'type': 'string',
'title': '设计方案标题',
'description': '设计方案的标题,支持变量引用',
'required': False,
'default': '应用设计方案',
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入标题,默认为"应用设计方案"',
},
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '设计完成后是否暂停等待用户确认或编辑',
'default': True,
'x-component': 'Switch',
},
},
'required': ['design_content'],
}
@@ -0,0 +1,432 @@
"""
应用设置节点
接收应用设置配置,展示给用户确认或编辑
包括:应用配置、Logo配置、内置主题和布局
"""
import json
import logging
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class AppSettingsNode(BaseNode):
"""
应用设置节点
接收应用设置配置,展示给用户确认或编辑
包括:应用配置、Logo配置、内置主题和布局
"""
node_type = 'app_settings'
node_name = '应用设置'
node_category = 'application'
node_icon = 'settings'
node_description = '配置应用设置,包括应用信息、Logo、主题和布局'
inputs = [
{
'name': 'app_name',
'type': 'string',
'description': '应用名称',
'required': False,
},
{
'name': 'home_path',
'type': 'string',
'description': '首页路径',
'required': False,
},
{
'name': 'logo_source',
'type': 'string',
'description': 'Logo图片URL或文件ID',
'required': False,
},
{
'name': 'theme_builtin_type',
'type': 'string',
'description': '内置主题类型',
'required': False,
},
{
'name': 'theme_color_primary',
'type': 'string',
'description': '主题主色调',
'required': False,
},
{
'name': 'app_layout',
'type': 'string',
'description': '应用布局类型',
'required': False,
},
]
outputs = [
{
'name': 'settings',
'type': 'object',
'description': '应用设置配置',
},
{
'name': 'confirmed',
'type': 'boolean',
'description': '是否已确认',
},
]
# 默认配置
DEFAULT_SETTINGS = {
'app': {
'name': '',
'locale': 'zh-CN',
'dynamicTitle': True,
'watermark': False,
'watermarkContent': '',
'enableCheckUpdates': True,
'defaultHomePath': '/analytics',
'enablePreferences': True,
},
'logo': {
'enable': True,
'source': '',
'fit': 'contain',
},
'theme': {
'colorPrimary': 'hsl(212 100% 45%)',
'builtinType': 'default',
'mode': 'light',
'radius': '0.5',
'semiDarkSidebar': False,
'semiDarkHeader': False,
},
'sidebar': {
'enable': True,
'width': 230,
'collapsed': False,
'collapsedShowTitle': False,
'autoActivateChild': False,
'expandOnHover': True,
'collapsedButton': True,
'fixedButton': True,
},
'header': {
'enable': True,
'mode': 'fixed',
'menuAlign': 'start',
},
'footer': {
'enable': True,
'fixed': True,
},
'copyright': {
'enable': True,
'companyName': '',
'companySiteLink': '',
'date': '',
'icp': '',
'icpLink': '',
},
'navigation': {
'styleType': 'rounded',
'split': True,
'accordion': True,
},
'tabbar': {
'enable': True,
'showIcon': True,
'showMore': True,
'showMaximize': True,
'persist': True,
'draggable': True,
'wheelable': True,
'styleType': 'chrome',
'maxCount': 0,
'middleClickToClose': True,
},
'breadcrumb': {
'enable': True,
'showIcon': True,
'showHome': True,
'styleType': 'normal',
'hideOnlyOne': False,
},
'transition': {
'progress': True,
'name': 'fade-slide',
'loading': True,
'enable': True,
},
'widget': {
'globalSearch': True,
'fullscreen': True,
'languageToggle': True,
'notification': True,
'themeToggle': True,
'sidebarToggle': True,
'lockScreen': True,
'refresh': True,
},
'shortcutKeys': {
'enable': True,
'globalSearch': True,
'globalLogout': True,
'globalLockScreen': True,
},
}
# 布局类型选项
LAYOUT_OPTIONS = [
{'label': '侧边导航', 'value': 'sidebar-nav'},
{'label': '侧边混合导航', 'value': 'sidebar-mixed-nav'},
{'label': '顶部导航', 'value': 'header-nav'},
{'label': '混合导航', 'value': 'mixed-nav'},
{'label': '全屏内容', 'value': 'full-content'},
]
# 内置主题选项
BUILTIN_THEME_OPTIONS = [
{'label': '默认', 'value': 'default'},
{'label': '紫罗兰', 'value': 'violet'},
{'label': '粉红', 'value': 'pink'},
{'label': '玫瑰', 'value': 'rose'},
{'label': '天蓝', 'value': 'sky'},
{'label': '青色', 'value': 'cyan'},
{'label': '绿色', 'value': 'green'},
{'label': '橙色', 'value': 'orange'},
{'label': '黄色', 'value': 'yellow'},
{'label': '锌色', 'value': 'zinc'},
{'label': '中性', 'value': 'neutral'},
{'label': '石板', 'value': 'slate'},
{'label': '灰色', 'value': 'gray'},
{'label': '深灰', 'value': 'deep-blue'},
{'label': '深绿', 'value': 'deep-green'},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info(f'AppSettingsNode - 使用用户编辑的数据')
# 使用编辑后的数据,清除用户输入
context.variables.pop('__user_input__', None)
return self._build_result(edited_data, require_confirmation=False)
# 获取输入参数
app_name = context.resolve_template(self.config.get('app_name', ''))
home_path = context.resolve_template(self.config.get('home_path', ''))
logo_source = context.resolve_template(self.config.get('logo_source', ''))
theme_builtin_type = context.resolve_template(self.config.get('theme_builtin_type', 'default'))
theme_color_primary = context.resolve_template(self.config.get('theme_color_primary', ''))
app_layout = context.resolve_template(self.config.get('app_layout', 'sidebar-nav'))
logger.info(f'AppSettingsNode - 输入参数: app_name={app_name}, home_path={home_path}, layout={app_layout}')
# 构建设置数据(合并默认值和输入值)
settings = self._build_settings(
app_name=app_name,
home_path=home_path,
logo_source=logo_source,
theme_builtin_type=theme_builtin_type,
theme_color_primary=theme_color_primary,
app_layout=app_layout,
)
logger.info(f'AppSettingsNode - 输出设置配置')
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
return self._build_result(settings, require_confirmation)
except Exception as e:
logger.exception(f'应用设置节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点(直接调用同步方法)"""
return self.execute(context)
def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户编辑的数据"""
if isinstance(user_input, dict):
# 检查是否包含设置字段
if 'app' in user_input or 'theme' in user_input or 'logo' in user_input:
return user_input
return None
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict) and ('app' in data or 'theme' in data or 'logo' in data):
return data
except (json.JSONDecodeError, TypeError):
pass
return None
def _build_settings(
self,
app_name: str = '',
home_path: str = '',
logo_source: str = '',
theme_builtin_type: str = 'default',
theme_color_primary: str = '',
app_layout: str = 'sidebar-nav',
) -> Dict[str, Any]:
"""构建设置配置"""
import copy
settings = copy.deepcopy(self.DEFAULT_SETTINGS)
# 应用配置
if app_name:
settings['app']['name'] = app_name
# 首页路径配置
if home_path:
settings['app']['defaultHomePath'] = home_path
# 布局配置
if app_layout:
settings['app']['layout'] = app_layout
# Logo配置
if logo_source:
settings['logo']['source'] = logo_source
# 主题配置
if theme_builtin_type:
settings['theme']['builtinType'] = theme_builtin_type
if theme_color_primary:
settings['theme']['colorPrimary'] = theme_color_primary
return settings
def _build_result(self, settings: Dict[str, Any], require_confirmation: bool = False) -> NodeResult:
"""构建节点结果"""
# 构建预览数据
preview_data = {
'type': 'app_settings',
'title': '应用设置',
'data': settings,
'editable': True,
'layoutOptions': self.LAYOUT_OPTIONS,
'themeOptions': self.BUILTIN_THEME_OPTIONS,
} if require_confirmation else None
return NodeResult(
success=True,
output=settings,
output_variables={
'settings': settings,
'confirmed': not require_confirmation,
},
preview=preview_data,
waiting_for_input=require_confirmation,
waiting_config={
'type': 'design_preview',
'preview_type': 'app_settings',
'title': '应用设置',
'message': '请确认或编辑应用设置',
'data': settings,
'layoutOptions': self.LAYOUT_OPTIONS,
'themeOptions': self.BUILTIN_THEME_OPTIONS,
} if require_confirmation else {},
)
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""
获取节点配置 Schema(供前端表单渲染)
Returns:
配置 Schema
"""
return {
'type': 'object',
'properties': {
'app_name': {
'type': 'string',
'title': '应用名称',
'description': '应用的显示名称,支持变量引用如 {{llm_output}}',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入应用名称',
},
},
'home_path': {
'type': 'string',
'title': '首页路径',
'description': '应用首页路径,如 /dashboard 或 /analytics',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '如 /dashboard',
},
},
'logo_source': {
'type': 'string',
'title': 'Logo图片',
'description': 'Logo图片URL或文件ID,支持变量引用',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入Logo图片URL',
},
},
'theme_builtin_type': {
'type': 'string',
'title': '内置主题',
'description': '选择内置主题类型',
'default': 'default',
'enum': ['default', 'violet', 'pink', 'rose', 'sky', 'cyan', 'green', 'orange', 'yellow', 'zinc', 'neutral', 'slate', 'gray', 'deep-blue', 'deep-green'],
'x-component': 'Select',
'x-component-props': {
'placeholder': '请选择内置主题',
},
},
'theme_color_primary': {
'type': 'string',
'title': '主题主色调',
'description': '主题主色调,如 hsl(212 100% 45%)',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '如 hsl(212 100% 45%)',
},
},
'app_layout': {
'type': 'string',
'title': '应用布局',
'description': '应用布局类型',
'default': 'sidebar-nav',
'enum': ['sidebar-nav', 'sidebar-mixed-nav', 'header-nav', 'mixed-nav', 'full-content'],
'x-component': 'Select',
'x-component-props': {
'placeholder': '请选择布局类型',
},
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '设置完成后是否暂停等待用户确认或编辑',
'default': True,
'x-component': 'Switch',
},
},
'required': [],
}
@@ -0,0 +1,302 @@
"""
应用更新节点
执行应用配置的更新操作,将设置保存到数据库
"""
import json
import logging
from typing import Any, Dict, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class AppUpdateNode(BaseNode):
"""
应用更新节点
接收应用设置配置,执行实际的更新操作
"""
node_type = 'app_update'
node_name = '应用更新'
node_category = 'application'
node_icon = 'save'
node_description = '执行应用配置的更新操作,将设置保存到数据库'
inputs = [
{
'name': 'settings',
'type': 'object',
'description': '应用设置配置(来自应用设置节点)',
'required': True,
},
{
'name': 'application_id',
'type': 'string',
'description': '应用ID,不传则更新主应用配置',
'required': False,
},
]
outputs = [
{
'name': 'success',
'type': 'boolean',
'description': '更新是否成功',
},
{
'name': 'config_id',
'type': 'string',
'description': '配置记录ID',
},
{
'name': 'message',
'type': 'string',
'description': '更新结果消息',
},
]
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 获取输入参数
settings = self._get_settings(context)
application_id = self._get_application_id(context)
if not settings:
return NodeResult(
success=False,
error='缺少应用设置配置',
)
logger.info(f'AppUpdateNode - 开始更新应用配置, application_id={application_id}')
# 获取数据库会话
db: AsyncSession = context.db_session
if not db:
return NodeResult(
success=False,
error='数据库会话不可用',
)
# 执行更新
config_id = await self._update_preferences(db, settings, application_id)
if config_id:
logger.info(f'AppUpdateNode - 应用配置更新成功, config_id={config_id}')
return NodeResult(
success=True,
output={
'success': True,
'config_id': config_id,
'message': '应用配置更新成功',
},
output_variables={
'update_success': True,
'config_id': config_id,
'update_message': '应用配置更新成功',
},
)
else:
return NodeResult(
success=False,
error='应用配置更新失败',
)
except Exception as e:
logger.exception(f'应用更新节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
def execute(self, context: NodeContext) -> NodeResult:
"""同步执行节点(不支持,需要数据库操作)"""
return NodeResult(
success=False,
error='应用更新节点需要异步执行',
)
def _get_settings(self, context: NodeContext) -> Optional[Dict[str, Any]]:
"""获取应用设置配置"""
import re
# 优先从配置中获取(支持变量引用)
settings_config = self.config.get('settings', '')
logger.info(f'AppUpdateNode - settings_config from config: {settings_config}')
if settings_config:
# 检查是否是变量引用格式 {{node_id.key}}
var_pattern = r'\{\{([^}]+)\}\}'
match = re.match(var_pattern, settings_config.strip())
if match:
# 是变量引用,直接从上下文获取对象
var_ref = match.group(1).strip()
logger.info(f'AppUpdateNode - 解析变量引用: {var_ref}')
# 解析 node_id.key 格式
# 注意:node_id 可能包含连字符,如 app_settings-1769009671187
# 所以只按最后一个点分割
if '.' in var_ref:
parts = var_ref.rsplit('.', 1) # 从右边分割,只分割一次
node_id = parts[0]
key = parts[1] if len(parts) > 1 else None
logger.info(f'AppUpdateNode - 查找节点输出: node_id={node_id}, key={key}')
# 从节点输出命名空间获取
node_outputs = context.variables.get(f'_node_{node_id}')
logger.info(f'AppUpdateNode - node_outputs type: {type(node_outputs)}, keys: {node_outputs.keys() if isinstance(node_outputs, dict) else "N/A"}')
if isinstance(node_outputs, dict) and key and key in node_outputs:
value = node_outputs[key]
logger.info(f'AppUpdateNode - 找到值, type: {type(value)}')
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError):
logger.warning(f'AppUpdateNode - JSON解析失败: {value[:100]}')
pass
else:
logger.warning(f'AppUpdateNode - 未找到节点输出或key不存在')
else:
# 直接变量名
value = context.variables.get(var_ref)
logger.info(f'AppUpdateNode - 直接变量引用, type: {type(value)}')
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError):
pass
else:
# 不是变量引用,尝试解析为 JSON
try:
return json.loads(settings_config)
except (json.JSONDecodeError, TypeError):
pass
# 从上下文变量获取
settings = context.variables.get('settings')
logger.info(f'AppUpdateNode - 从上下文获取settings, type: {type(settings)}')
if isinstance(settings, dict):
return settings
if isinstance(settings, str):
try:
return json.loads(settings)
except (json.JSONDecodeError, TypeError):
pass
logger.warning(f'AppUpdateNode - 未能获取有效的settings配置')
return None
def _get_application_id(self, context: NodeContext) -> Optional[str]:
"""获取应用ID"""
# 优先从配置中获取
app_id = self.config.get('application_id', '')
if app_id:
resolved = context.resolve_template(app_id)
if resolved and resolved != 'main':
return resolved
# 从上下文变量获取
app_id = context.variables.get('application_id')
if app_id and app_id != 'main':
return app_id
return None
async def _update_preferences(
self,
db: AsyncSession,
settings: Dict[str, Any],
application_id: Optional[str] = None,
) -> Optional[str]:
"""更新偏好配置"""
from core.ui_config.model import UIConfig
# 配置键:使用与前端 API 一致的键名
config_key = 'frontend_preferences'
# 查找现有配置
stmt = select(UIConfig).where(
UIConfig.config_key == config_key,
UIConfig.is_deleted == False,
)
# 根据 application_id 过滤
if application_id:
stmt = stmt.where(UIConfig.application_id == application_id)
else:
stmt = stmt.where(UIConfig.application_id.is_(None))
result = await db.execute(stmt)
config = result.scalar_one_or_none()
# 配置值
config_value = json.dumps(settings, ensure_ascii=False)
if config:
# 更新现有配置
config.config_value = config_value
await db.commit()
await db.refresh(config)
return config.id
else:
# 创建新配置
new_config = UIConfig(
application_id=application_id,
config_key=config_key,
config_value=config_value,
config_type='preferences',
description='前端UI偏好配置' if not application_id else '子应用UI偏好配置',
status=True,
sort=0,
)
db.add(new_config)
await db.commit()
await db.refresh(new_config)
return new_config.id
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""
获取节点配置 Schema(供前端表单渲染)
"""
return {
'type': 'object',
'properties': {
'settings': {
'type': 'string',
'title': '应用设置',
'description': '应用设置配置,支持变量引用如 {{app_settings-1.settings}}',
'required': True,
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入应用设置或使用变量引用',
},
},
'application_id': {
'type': 'string',
'title': '应用ID',
'description': '目标应用ID,不填则更新主应用配置。支持变量引用如 {{application_id}}',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '留空则更新主应用配置',
},
},
},
'required': ['settings'],
}
@@ -0,0 +1,232 @@
"""
仪表盘基础信息处理节点
接收LLM提取的基础信息,进行验证和补充
"""
import logging
import re
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class DashboardBasicInfoNode(BaseNode):
"""
仪表盘基础信息处理节点
接收LLM提取的基础信息,进行验证和补充
"""
node_type = 'dashboard_basic_info'
node_name = '仪表盘基础信息'
node_category = 'dashboard'
node_icon = 'layout-dashboard'
node_description = '处理仪表盘基础信息,验证和补充名称、编码等'
inputs = [
{
'name': 'name',
'type': 'string',
'description': '仪表盘名称',
'required': True,
},
{
'name': 'code',
'type': 'string',
'description': '仪表盘编码',
'required': False,
},
{
'name': 'category',
'type': 'string',
'description': '分类(dashboard/portal/databoard/other',
'required': False,
},
{
'name': 'description',
'type': 'string',
'description': '仪表盘描述',
'required': False,
},
{
'name': 'sort',
'type': 'number',
'description': '排序',
'required': False,
},
]
outputs = [
{
'name': 'dashboard_basic_info',
'type': 'object',
'description': '处理后的仪表盘基础信息',
},
{
'name': 'dashboard_name',
'type': 'string',
'description': '仪表盘名称',
},
{
'name': 'dashboard_code',
'type': 'string',
'description': '仪表盘编码',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info(f'DashboardBasicInfoNode - 使用用户编辑的数据: {edited_data}')
# 使用编辑后的数据,清除用户输入
context.variables.pop('__user_input__', None)
return self._build_result(edited_data, require_confirmation=False)
# 获取输入参数
name = context.resolve_template(self.config.get('name', ''))
code = context.resolve_template(self.config.get('code', ''))
category = context.resolve_template(self.config.get('category', 'dashboard'))
description = context.resolve_template(self.config.get('description', ''))
# 处理sort(可能是字符串或数字)
sort_value = self.config.get('sort', 0)
sort_str = context.resolve_template(str(sort_value))
try:
sort = int(sort_str) if sort_str else 0
except (ValueError, TypeError):
sort = 0
logger.info(f'DashboardBasicInfoNode - 输入参数: name={name}, code={code}, category={category}')
# 验证必填字段
if not name:
return NodeResult(
success=False,
error='仪表盘名称不能为空',
)
# 自动生成编码(如果未提供)
if not code:
code = self._generate_code(name)
# 验证编码格式
if not self._validate_code(code):
return NodeResult(
success=False,
error=f'仪表盘编码格式不正确: {code},只能包含字母、数字、下划线和连字符',
)
# 验证分类
valid_categories = ['dashboard', 'portal', 'databoard', 'other']
if category and category not in valid_categories:
category = 'dashboard'
# 构建基础信息
basic_info = {
'name': name,
'code': code,
'category': category or 'dashboard',
'description': description or '',
'sort': sort,
}
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
return self._build_result(basic_info, require_confirmation)
except Exception as e:
logger.error(f'DashboardBasicInfoNode - 执行失败: {str(e)}')
return NodeResult(
success=False,
error=f'处理仪表盘基础信息失败: {str(e)}',
)
def _parse_user_edit(self, user_input: str) -> Optional[Dict[str, Any]]:
"""解析用户编辑的数据"""
try:
import json
if isinstance(user_input, str):
data = json.loads(user_input)
else:
data = user_input
# 验证必要字段
if isinstance(data, dict) and data.get('name'):
return data
return None
except Exception:
return None
def _build_result(self, basic_info: Dict[str, Any], require_confirmation: bool) -> NodeResult:
"""构建节点结果"""
output_data = {
'dashboard_basic_info': basic_info,
'dashboard_name': basic_info.get('name', ''),
'dashboard_code': basic_info.get('code', ''),
}
if require_confirmation:
return NodeResult(
success=True,
output=output_data,
output_variables=output_data, # 添加 output_variables
waiting_for_input=True,
waiting_config={
'type': 'design_preview',
'preview_type': 'dashboard_basic_info',
'title': '仪表盘基础信息确认',
'data': basic_info,
'editable': True,
'fields': [
{'key': 'name', 'label': '仪表盘名称', 'type': 'text', 'required': True},
{'key': 'code', 'label': '仪表盘编码', 'type': 'text', 'required': True},
{'key': 'category', 'label': '分类', 'type': 'select', 'options': [
{'label': '仪表盘', 'value': 'dashboard'},
{'label': '门户', 'value': 'portal'},
{'label': '数据看板', 'value': 'databoard'},
{'label': '其他', 'value': 'other'},
]},
{'key': 'description', 'label': '描述', 'type': 'textarea'},
{'key': 'sort', 'label': '排序', 'type': 'number'},
],
},
)
return NodeResult(
success=True,
output=output_data,
output_variables=output_data, # 添加 output_variables
)
def _generate_code(self, name: str) -> str:
"""根据名称生成编码"""
try:
from pypinyin import lazy_pinyin
# 使用拼音生成编码
pinyin_list = lazy_pinyin(name)
code = '_'.join(pinyin_list)
# 清理非法字符
code = re.sub(r'[^a-zA-Z0-9_]', '', code)
return f'page_{code}'
except ImportError:
# 如果没有pypinyin,使用简单的处理
code = re.sub(r'[^a-zA-Z0-9_]', '', name)
return f'page_{code}' if code else f'page_{id(name)}'
def _validate_code(self, code: str) -> bool:
"""验证编码格式"""
if not code:
return False
# 只允许字母、数字、下划线和连字符
pattern = r'^[a-zA-Z][a-zA-Z0-9_-]*$'
return bool(re.match(pattern, code))
@@ -0,0 +1,338 @@
"""
仪表盘创建节点
将仪表盘配置保存到数据库
"""
import json
import logging
from typing import Any, Dict
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class DashboardCreateNode(BaseNode):
"""
仪表盘创建节点
将仪表盘基础信息和页面配置保存到数据库
"""
node_type = 'dashboard_create'
node_name = '仪表盘创建'
node_category = 'dashboard'
node_icon = 'file-plus'
node_description = '将仪表盘配置保存到数据库'
inputs = [
{
'name': 'dashboard_basic_info',
'type': 'object',
'description': '仪表盘基础信息(来自基础信息节点)',
'required': True,
},
{
'name': 'page_config',
'type': 'object',
'description': '页面设计配置(来自设计节点)',
'required': True,
},
{
'name': 'application_id',
'type': 'string',
'description': '所属应用ID(可选)',
'required': False,
},
]
outputs = [
{
'name': 'dashboard_id',
'type': 'string',
'description': '创建的仪表盘ID',
},
{
'name': 'dashboard_code',
'type': 'string',
'description': '仪表盘编码',
},
{
'name': 'page_meta',
'type': 'object',
'description': '完整的页面元数据',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点(同步方法,通过运行异步方法实现)"""
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
else:
return loop.run_until_complete(self.execute_async(context))
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 获取输入参数
dashboard_basic_info_raw = self.config.get('dashboard_basic_info', '{}')
page_config_raw = self.config.get('page_config', '{}')
# 解析变量引用
dashboard_basic_info = self._resolve_variable_value(context, dashboard_basic_info_raw)
page_config = self._resolve_variable_value(context, page_config_raw)
application_id = context.resolve_template(self.config.get('application_id', ''))
# 配置选项
update_if_exists = self.config.get('update_if_exists', True)
logger.info(f'DashboardCreateNode - 开始创建仪表盘')
logger.info(f'DashboardCreateNode - 原始配置: dashboard_basic_info_raw={dashboard_basic_info_raw}, page_config_raw={page_config_raw}')
logger.info(f'DashboardCreateNode - 上下文变量: {list(context.variables.keys())}')
logger.info(f'DashboardCreateNode - dashboard_basic_info type: {type(dashboard_basic_info)}, value: {dashboard_basic_info}')
logger.info(f'DashboardCreateNode - page_config type: {type(page_config)}, value: {str(page_config)[:200]}')
# 确保 dashboard_basic_info 是字典
if isinstance(dashboard_basic_info, str):
try:
dashboard_basic_info = json.loads(dashboard_basic_info)
logger.info(f'DashboardCreateNode - 成功解析 dashboard_basic_info 字符串为字典')
except json.JSONDecodeError as e:
logger.error(f'DashboardCreateNode - 解析 dashboard_basic_info 失败: {e}')
return NodeResult(
success=False,
error=f'仪表盘基础信息格式错误: {str(e)}',
)
# 确保 page_config 是字典
if isinstance(page_config, str):
try:
page_config = json.loads(page_config)
logger.info(f'DashboardCreateNode - 成功解析 page_config 字符串为字典')
except json.JSONDecodeError as e:
logger.error(f'DashboardCreateNode - 解析 page_config 失败: {e}')
return NodeResult(
success=False,
error=f'页面配置格式错误: {str(e)}',
)
# 验证必要配置
if not dashboard_basic_info:
return NodeResult(
success=False,
error='仪表盘基础信息不能为空',
)
if not page_config:
return NodeResult(
success=False,
error='页面设计配置不能为空',
)
# 提取基础信息
name = dashboard_basic_info.get('name', '')
code_base = dashboard_basic_info.get('code', '')
category = dashboard_basic_info.get('category', 'dashboard')
description = dashboard_basic_info.get('description', '')
sort = dashboard_basic_info.get('sort', 0)
if not name or not code_base:
return NodeResult(
success=False,
error='仪表盘名称和编码不能为空',
)
# 保存到数据库
from app.database import AsyncSessionLocal
from online_dev.page_manager.model import PageMeta
from sqlalchemy import select
async with AsyncSessionLocal() as db:
# 生成唯一的 code(如果已存在则自动添加后缀)
code = await self._get_unique_code(db, code_base)
logger.info(f'DashboardCreateNode - 使用 code: {code} (原始: {code_base})')
# 检查是否已存在(理论上不应该存在,因为已经生成了唯一 code)
stmt = select(PageMeta).where(PageMeta.code == code, PageMeta.is_deleted == False)
result = await db.execute(stmt)
existing_page = result.scalar_one_or_none()
if existing_page:
if update_if_exists:
# 更新现有记录
existing_page.name = name
existing_page.category = category
existing_page.description = description
existing_page.sort = sort
existing_page.page_config = page_config
if application_id:
existing_page.application_id = application_id
existing_page.version = (existing_page.version or 1) + 1
await db.commit()
await db.refresh(existing_page)
page_meta = self._page_to_dict(existing_page)
logger.info(f'DashboardCreateNode - 更新仪表盘成功: {code}')
else:
return NodeResult(
success=False,
error=f'仪表盘编码已存在: {code}',
)
else:
# 创建新记录
new_page = PageMeta(
name=name,
code=code,
category=category,
description=description,
sort=sort,
status='draft',
version=1,
page_config=page_config,
application_id=application_id if application_id else None,
)
db.add(new_page)
await db.commit()
await db.refresh(new_page)
page_meta = self._page_to_dict(new_page)
logger.info(f'DashboardCreateNode - 创建仪表盘成功: {code}')
output_data = {
'dashboard_id': page_meta['id'],
'dashboard_code': page_meta['code'],
'page_meta': page_meta,
}
return NodeResult(
success=True,
output=output_data,
output_variables=output_data,
)
except Exception as e:
logger.error(f'DashboardCreateNode - 执行失败: {str(e)}')
return NodeResult(
success=False,
error=f'创建仪表盘失败: {str(e)}',
)
def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any:
"""解析变量引用,保持原始类型"""
import re
import ast
# 如果已经是字典,直接返回
if isinstance(value, dict):
return value
# 如果不是字符串,返回原值
if not isinstance(value, str):
return value
value = value.strip()
if not value or value == '{}':
return {}
# 检查是否是单个变量引用 {{node_id.variable_name}}
single_var_pattern = r'^\{\{([^}]+)\}\}$'
match = re.match(single_var_pattern, value)
if match:
var_ref = match.group(1).strip()
# 解析 node_id.variable_name 格式
if '.' in var_ref:
parts = var_ref.split('.', 1)
node_id = parts[0]
key = parts[1]
# 从节点输出命名空间获取
node_outputs = context.variables.get(f'_node_{node_id}')
if isinstance(node_outputs, dict) and key in node_outputs:
return node_outputs[key]
# 直接从变量中获取
if key in context.variables:
return context.variables[key]
logger.warning(f'变量引用未找到: {var_ref}')
else:
# 直接变量引用
if var_ref in context.variables:
return context.variables[var_ref]
logger.warning(f'变量引用未找到: {var_ref}')
# 不是变量引用,尝试解析为 JSON 或 Python dict
# 尝试 JSON 解析
try:
return json.loads(value)
except json.JSONDecodeError:
pass
# 尝试 Python literal_eval(处理单引号格式)
try:
result = ast.literal_eval(value)
if isinstance(result, dict):
return result
return result
except (ValueError, SyntaxError) as e:
logger.warning(f'配置解析失败: {e}, 值: {value[:200]}')
return value
async def _get_unique_code(self, db, base_code: str) -> str:
"""
生成唯一的 code,如果已存在则自动添加数字后缀
Args:
db: 数据库会话
base_code: 基础 code
Returns:
唯一的 code
"""
from online_dev.page_manager import PageService
code = base_code
counter = 1
while True:
try:
existing = await PageService.get_by_code(db, code)
if not existing:
# code 不存在,可以使用
break
# code 已存在,添加后缀
code = f"{base_code}_{counter}"
counter += 1
except Exception:
# 查询出错,认为 code 可用
break
return code
def _page_to_dict(self, page: Any) -> Dict[str, Any]:
"""将PageMeta对象转换为字典"""
return {
'id': page.id,
'name': page.name,
'code': page.code,
'category': page.category,
'description': page.description,
'status': page.status,
'version': page.version,
'sort': page.sort,
'page_config': page.page_config,
'application_id': page.application_id,
}
@@ -0,0 +1,483 @@
"""
仪表盘设计节点
提供可视化设计界面,让用户设计仪表盘布局
"""
import ast
import json
import logging
import random
from typing import Any, Dict, List, Optional
from uuid import uuid4
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class DashboardDesignNode(BaseNode):
"""
仪表盘设计节点
提供可视化设计界面,让用户设计仪表盘布局和组件
"""
node_type = 'dashboard_design'
node_name = '仪表盘设计'
node_category = 'dashboard'
node_icon = 'layout-dashboard'
node_description = '可视化设计仪表盘布局和组件'
inputs = [
{
'name': 'dashboard_code',
'type': 'string',
'description': '仪表盘编码(来自基础信息节点)',
'required': False,
},
{
'name': 'design_suggestion',
'type': 'string',
'description': 'LLM生成的设计建议(可选)',
'required': False,
},
{
'name': 'initial_config',
'type': 'object',
'description': '初始页面配置(可选)',
'required': False,
},
]
outputs = [
{
'name': 'page_config',
'type': 'object',
'description': '页面设计配置(dashboard-design的JSON配置)',
},
{
'name': 'design_title',
'type': 'string',
'description': '设计方案标题',
},
{
'name': 'confirmed',
'type': 'boolean',
'description': '是否已确认',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户提交的设计配置
user_input = context.variables.get('__user_input__')
if user_input:
design_data = self._parse_user_design(user_input)
if design_data:
logger.info(f'DashboardDesignNode - 使用用户设计的配置')
context.variables.pop('__user_input__', None)
return self._build_result(design_data, confirmed=True)
# 获取输入参数
dashboard_code = context.resolve_template(self.config.get('dashboard_code', ''))
design_suggestion = context.resolve_template(self.config.get('design_suggestion', ''))
initial_config = self.config.get('initial_config', {})
# 如果initial_config是变量引用,解析它
if isinstance(initial_config, str) and initial_config.startswith('{{'):
initial_config = self._resolve_variable_value(context, initial_config) or {}
logger.info(f'DashboardDesignNode - dashboard_code={dashboard_code}')
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
design_title = self.config.get('design_title', '仪表盘设计')
# 如果没有初始配置,根据 design_suggestion 生成默认模板
if not initial_config or not initial_config.get('widgets'):
initial_config = self._generate_dashboard_template(design_suggestion, dashboard_code)
# 构建设计数据
design_data = {
'dashboard_code': dashboard_code,
'design_title': design_title,
'design_suggestion': design_suggestion,
'page_config': initial_config,
}
if require_confirmation:
return NodeResult(
success=True,
output={
'page_config': design_data.get('page_config', {}),
'design_title': design_title,
'confirmed': False,
},
waiting_for_input=True,
waiting_config={
'type': 'design_preview',
'preview_type': 'dashboard_design',
'title': design_title,
'data': design_data,
'editable': True,
'fullscreen': True,
},
)
return self._build_result(design_data, confirmed=True)
except Exception as e:
logger.error(f'DashboardDesignNode - 执行失败: {str(e)}')
return NodeResult(
success=False,
error=f'仪表盘设计节点执行失败: {str(e)}',
)
def _parse_user_design(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户提交的设计配置"""
try:
if isinstance(user_input, str):
data = json.loads(user_input)
else:
data = user_input
if isinstance(data, dict):
return data
return None
except Exception as e:
logger.error(f'解析用户设计配置失败: {e}')
return None
def _build_result(self, design_data: Dict[str, Any], confirmed: bool) -> NodeResult:
"""构建节点结果"""
page_config = design_data.get('page_config', {})
design_title = design_data.get('design_title', '仪表盘设计')
output_data = {
'page_config': page_config,
'design_title': design_title,
'confirmed': confirmed,
}
return NodeResult(
success=True,
output=output_data,
output_variables=output_data, # 添加 output_variables
)
def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any:
"""解析变量引用"""
if isinstance(value, str):
resolved = context.resolve_template(value)
if resolved != value:
try:
return json.loads(resolved) if isinstance(resolved, str) else resolved
except (json.JSONDecodeError, TypeError):
return resolved
return value
def _generate_dashboard_template(self, design_suggestion: str, dashboard_code: str) -> Dict[str, Any]:
"""根据 LLM 建议生成仪表盘模板"""
try:
# 解析 design_suggestion(可能是字符串形式的字典)
modules = self._parse_design_suggestion(design_suggestion)
# 生成基础模板
template = {
"id": str(uuid4()),
"name": "我的仪表盘",
"columns": 12,
"rowHeight": 50,
"margin": [12, 12],
"widgets": []
}
# 添加欢迎卡片
welcome_id = str(uuid4())
template["widgets"].append({
"id": welcome_id,
"i": welcome_id,
"type": "welcome-card",
"x": 0,
"y": 0,
"w": 8,
"h": 2,
"minW": 4,
"minH": 2,
"title": "欢迎卡片",
"props": {
"title": "欢迎回来",
"subtitle": "今天是个好日子",
"showTime": True,
"showWeather": False
}
})
# 添加天气组件
weather_id = str(uuid4())
template["widgets"].append({
"id": weather_id,
"i": weather_id,
"type": "weather",
"x": 8,
"y": 0,
"w": 4,
"h": 2,
"minW": 2,
"minH": 2,
"title": "天气",
"props": {
"title": "今日天气",
"city": "北京",
"temperature": 25,
"weather": "",
"humidity": 45,
"wind": "东北风 3级",
"icon": "sunny"
}
})
# 添加日历
calendar_id = str(uuid4())
template["widgets"].append({
"id": calendar_id,
"i": calendar_id,
"type": "calendar",
"x": 8,
"y": 2,
"w": 4,
"h": 5,
"minW": 3,
"minH": 4,
"title": "日历",
"props": {
"title": "日历",
"showLunar": False
}
})
# 根据模块生成图表
y_offset = 2
if modules and len(modules) > 0:
# 生成折线图(基于第一个模块)
module1 = modules[0] if len(modules) > 0 else {"module_name": "数据", "moduld_fields": []}
line_chart = self._generate_line_chart(module1, 0, y_offset)
template["widgets"].append(line_chart)
y_offset += 5
# 生成柱状图(基于第二个模块)
module2 = modules[1] if len(modules) > 1 else module1
bar_chart = self._generate_bar_chart(module2, 0, y_offset)
template["widgets"].append(bar_chart)
# 生成饼图(基于第三个模块)
module3 = modules[2] if len(modules) > 2 else module1
pie_chart = self._generate_pie_chart(module3, 8, y_offset)
template["widgets"].append(pie_chart)
else:
# 没有模块信息,使用默认图表
template["widgets"].extend([
self._generate_line_chart({"module_name": "访问趋势", "moduld_fields": []}, 0, y_offset),
self._generate_bar_chart({"module_name": "销售统计", "moduld_fields": []}, 0, y_offset + 5),
self._generate_pie_chart({"module_name": "流量来源", "moduld_fields": []}, 8, y_offset + 5)
])
return template
except Exception as e:
logger.error(f'生成仪表盘模板失败: {e}')
# 返回空模板
return {
"id": str(uuid4()),
"name": "我的仪表盘",
"columns": 12,
"rowHeight": 50,
"margin": [12, 12],
"widgets": []
}
def _parse_design_suggestion(self, design_suggestion: str) -> List[Dict[str, Any]]:
"""解析 LLM 生成的设计建议"""
try:
if not design_suggestion:
return []
# 尝试解析为 JSON
if isinstance(design_suggestion, str):
try:
data = json.loads(design_suggestion)
except json.JSONDecodeError:
# 尝试使用 ast.literal_eval
try:
data = ast.literal_eval(design_suggestion)
except (ValueError, SyntaxError):
return []
else:
data = design_suggestion
# 提取模块信息
if isinstance(data, dict):
modules = data.get('app_modules', [])
if isinstance(modules, list):
return modules
return []
except Exception as e:
logger.error(f'解析设计建议失败: {e}')
return []
def _generate_line_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]:
"""生成折线图"""
module_name = module.get('module_name', '访问趋势')
fields = module.get('moduld_fields', [])
# 生成模拟数据
months = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"]
series_data = []
# 根据字段生成系列(最多2个)
for i, field in enumerate(fields[:2]):
data = [random.randint(500, 1500) for _ in range(12)]
series_data.append({
"name": field if isinstance(field, str) else f"系列{i+1}",
"data": data
})
# 如果没有字段,使用默认系列
if not series_data:
series_data = [
{"name": "访问量", "data": [820, 932, 901, 934, 1290, 1330, 1320, 1450, 1200, 1100, 1350, 1500]},
{"name": "下载量", "data": [620, 732, 701, 734, 1090, 1130, 1120, 1250, 1000, 900, 1150, 1300]}
]
line_id = str(uuid4())
return {
"id": line_id,
"i": line_id,
"type": "chart-line",
"x": x,
"y": y,
"w": 8,
"h": 5,
"minW": 4,
"minH": 3,
"title": "折线图",
"props": {
"title": module_name,
"smooth": True,
"showArea": False,
"showSymbol": True,
"symbolSize": 6,
"lineWidth": 2,
"showLegend": True,
"legendPosition": "top",
"colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"],
"xAxisData": months,
"seriesData": series_data
}
}
def _generate_bar_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]:
"""生成柱状图"""
module_name = module.get('module_name', '销售统计')
fields = module.get('moduld_fields', [])
# 生成模拟数据
regions = ["华东", "华南", "华北", "华中", "西南", "西北", "东北"]
series_data = []
# 根据字段生成系列(最多2个)
for i, field in enumerate(fields[:2]):
data = [random.randint(200, 500) for _ in range(7)]
series_data.append({
"name": field if isinstance(field, str) else f"{2023+i}",
"data": data
})
# 如果没有字段,使用默认系列
if not series_data:
series_data = [
{"name": "2023", "data": [320, 302, 301, 334, 390, 330, 320]},
{"name": "2024", "data": [420, 382, 391, 434, 490, 430, 420]}
]
bar_id = str(uuid4())
return {
"id": bar_id,
"i": bar_id,
"type": "chart-bar",
"x": x,
"y": y,
"w": 8,
"h": 6,
"minW": 4,
"minH": 3,
"title": "柱状图",
"props": {
"title": module_name,
"horizontal": False,
"stack": False,
"barWidth": "auto",
"barRadius": 4,
"showBackground": False,
"showLegend": True,
"legendPosition": "top",
"colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"],
"xAxisData": regions,
"seriesData": series_data
}
}
def _generate_pie_chart(self, module: Dict[str, Any], x: int, y: int) -> Dict[str, Any]:
"""生成饼图"""
module_name = module.get('module_name', '流量来源')
fields = module.get('moduld_fields', [])
# 根据字段生成数据
series_data = []
if fields and len(fields) > 0:
# 使用字段名作为分类(最多5个)
for field in fields[:5]:
series_data.append({
"name": field if isinstance(field, str) else f"分类{len(series_data)+1}",
"value": random.randint(300, 1000)
})
# 如果没有字段,使用默认数据
if not series_data:
series_data = [
{"name": "搜索引擎", "value": 1048},
{"name": "直接访问", "value": 735},
{"name": "邮件营销", "value": 580},
{"name": "联盟广告", "value": 484},
{"name": "视频广告", "value": 300}
]
pie_id = str(uuid4())
return {
"id": pie_id,
"i": pie_id,
"type": "chart-pie",
"x": x,
"y": y,
"w": 4,
"h": 6,
"minW": 3,
"minH": 3,
"title": "饼图",
"props": {
"title": module_name,
"pieType": "rose",
"radius": ["0%", "70%"],
"showLabel": True,
"labelPosition": "outside",
"showLegend": True,
"legendPosition": "bottom",
"colors": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"],
"seriesData": series_data
}
}
@@ -0,0 +1,311 @@
"""
仪表盘发布节点
将仪表盘发布到菜单系统
"""
import logging
from typing import Any, Dict
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class DashboardPublishNode(BaseNode):
"""
仪表盘发布节点
将仪表盘发布到菜单系统,创建菜单项
"""
node_type = 'dashboard_publish'
node_name = '仪表盘发布'
node_category = 'dashboard'
node_icon = 'upload'
node_description = '将仪表盘发布到菜单系统'
inputs = [
{
'name': 'dashboard_id',
'type': 'string',
'description': '仪表盘ID(来自创建节点)',
'required': True,
},
{
'name': 'menu_name',
'type': 'string',
'description': '菜单名称',
'required': True,
},
{
'name': 'menu_parent_id',
'type': 'string',
'description': '父菜单ID(可选)',
'required': False,
},
{
'name': 'menu_icon',
'type': 'string',
'description': '菜单图标',
'required': False,
},
{
'name': 'menu_order',
'type': 'number',
'description': '菜单排序',
'required': False,
},
{
'name': 'application_id',
'type': 'string',
'description': '所属应用ID',
'required': False,
},
]
outputs = [
{
'name': 'menu_id',
'type': 'string',
'description': '创建的菜单ID',
},
{
'name': 'route_path',
'type': 'string',
'description': '访问路径',
},
{
'name': 'publish_result',
'type': 'object',
'description': '发布结果',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点(同步方法,通过运行异步方法实现)"""
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
else:
return loop.run_until_complete(self.execute_async(context))
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
# 检查是否有用户输入(确认后的数据)
user_input = context.user_input
logger.info(f'DashboardPublishNode - user_input 类型: {type(user_input)}, 值: {user_input}')
logger.info(f'DashboardPublishNode - require_confirmation: {require_confirmation}')
# 解析 user_input(可能是字符串或字典)
user_input_dict = None
if user_input:
if isinstance(user_input, str):
try:
import json
user_input_dict = json.loads(user_input)
logger.info(f'DashboardPublishNode - 解析 JSON 字符串为字典: {user_input_dict}')
except json.JSONDecodeError as e:
logger.warning(f'DashboardPublishNode - JSON 解析失败: {e}')
elif isinstance(user_input, dict):
user_input_dict = user_input
# 检查 user_input 是否包含发布节点需要的字段(dashboard_id 和 menu_name
# 如果包含,说明是用户确认后的数据;否则是从上一个节点传递过来的数据
if user_input_dict and 'dashboard_id' in user_input_dict and 'menu_name' in user_input_dict:
# 用户已确认,使用确认后的数据
logger.info(f'DashboardPublishNode - 使用用户确认的发布配置')
return await self._execute_publish(context, user_input_dict)
# 获取输入参数
dashboard_id = context.resolve_template(self.config.get('dashboard_id', ''))
menu_name = context.resolve_template(self.config.get('menu_name', ''))
menu_parent_id = context.resolve_template(self.config.get('menu_parent_id', ''))
menu_icon = context.resolve_template(self.config.get('menu_icon', 'lucide:layout-dashboard'))
application_id = context.resolve_template(self.config.get('application_id', ''))
# 处理menu_order
menu_order_value = self.config.get('menu_order', 0)
menu_order_str = context.resolve_template(str(menu_order_value))
try:
menu_order = int(menu_order_str) if menu_order_str else 0
except (ValueError, TypeError):
menu_order = 0
logger.info(f'DashboardPublishNode - 开始发布仪表盘: {dashboard_id}')
# 验证必要参数
if not dashboard_id:
return NodeResult(
success=False,
error='仪表盘ID不能为空',
)
if not menu_name:
return NodeResult(
success=False,
error='菜单名称不能为空',
)
# 获取仪表盘的 code(用于显示路由路径)
dashboard_code = ''
try:
from app.database import AsyncSessionLocal
from online_dev.page_manager.model import PageMeta
from sqlalchemy import select
async with AsyncSessionLocal() as db:
stmt = select(PageMeta.code).where(PageMeta.id == dashboard_id, PageMeta.is_deleted == False)
result = await db.execute(stmt)
dashboard_code = result.scalar_one_or_none() or ''
except Exception as e:
logger.warning(f'获取仪表盘 code 失败: {e}')
# 构建发布数据
publish_data = {
'dashboard_id': dashboard_id,
'dashboard_code': dashboard_code,
'menu_name': menu_name,
'menu_parent_id': menu_parent_id,
'menu_icon': menu_icon,
'menu_order': menu_order,
'application_id': application_id,
}
# 如果需要确认,返回等待用户输入
if require_confirmation:
return NodeResult(
success=True,
output=publish_data,
output_variables=publish_data,
waiting_for_input=True,
waiting_config={
'type': 'design_preview',
'preview_type': 'dashboard_publish',
'title': '仪表盘发布确认',
'data': publish_data,
'editable': True,
},
)
# 不需要确认,直接发布
return await self._execute_publish(context, publish_data)
except Exception as e:
logger.error(f'DashboardPublishNode - 执行失败: {str(e)}')
return NodeResult(
success=False,
error=f'发布仪表盘失败: {str(e)}',
)
async def _execute_publish(self, context: NodeContext, publish_data: Dict[str, Any]) -> NodeResult:
"""执行发布操作"""
try:
dashboard_id = publish_data.get('dashboard_id', '')
menu_name = publish_data.get('menu_name', '')
menu_parent_id = publish_data.get('menu_parent_id', '')
menu_icon = publish_data.get('menu_icon', 'lucide:layout-dashboard')
menu_order = publish_data.get('menu_order', 0)
application_id = publish_data.get('application_id', '')
if not dashboard_id or not menu_name:
return NodeResult(
success=False,
error='仪表盘ID和菜单名称不能为空',
)
# 发布到数据库
from app.database import AsyncSessionLocal
from online_dev.page_manager.model import PageMeta
from core.menu.model import Menu
from sqlalchemy import select
async with AsyncSessionLocal() as db:
# 获取仪表盘信息
stmt = select(PageMeta).where(PageMeta.id == dashboard_id, PageMeta.is_deleted == False)
result = await db.execute(stmt)
page = result.scalar_one_or_none()
if not page:
return NodeResult(
success=False,
error=f'仪表盘不存在: {dashboard_id}',
)
# 生成路由路径
route_path = f'/page-render/{page.code}'
# 检查菜单是否已存在
menu_stmt = select(Menu).where(
Menu.path == route_path,
Menu.is_deleted == False
)
menu_result = await db.execute(menu_stmt)
existing_menu = menu_result.scalar_one_or_none()
if existing_menu:
# 更新现有菜单
existing_menu.title = menu_name
existing_menu.icon = menu_icon
existing_menu.order = menu_order
if menu_parent_id:
existing_menu.parent_id = menu_parent_id
menu_id = existing_menu.id
logger.info(f'DashboardPublishNode - 更新菜单: {menu_id}')
else:
# 创建新菜单
new_menu = Menu(
name=menu_name,
title=menu_name,
path=route_path,
component='_core/page-render/index',
icon=menu_icon,
order=menu_order,
type='online_page',
parent_id=menu_parent_id if menu_parent_id else None,
application_id=application_id if application_id else page.application_id,
)
db.add(new_menu)
await db.flush()
menu_id = new_menu.id
logger.info(f'DashboardPublishNode - 创建菜单: {menu_id}')
# 更新页面状态为已发布
page.status = 'published'
await db.commit()
output_data = {
'menu_id': menu_id,
'route_path': route_path,
'dashboard_id': dashboard_id,
'dashboard_code': page.code,
'dashboard_name': page.name,
'menu_name': menu_name,
'status': 'published',
}
return NodeResult(
success=True,
output=output_data,
output_variables=output_data,
)
except Exception as e:
logger.error(f'DashboardPublishNode - 执行失败: {str(e)}')
return NodeResult(
success=False,
error=f'发布仪表盘失败: {str(e)}',
)
@@ -0,0 +1,333 @@
"""
表单基础信息处理节点
接收LLM提取的基础信息,进行验证和补充
"""
import logging
import re
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormBasicInfoNode(BaseNode):
"""
表单基础信息处理节点
接收LLM提取的基础信息,进行验证和补充
"""
node_type = 'form_basic_info'
node_name = '表单基础信息'
node_category = 'form'
node_icon = 'file-text'
node_description = '处理表单基础信息,验证和补充表单名称、编码等'
inputs = [
{
'name': 'name',
'type': 'string',
'description': '表单名称',
'required': True,
},
{
'name': 'code',
'type': 'string',
'description': '表单编码',
'required': False,
},
{
'name': 'form_type',
'type': 'string',
'description': '表单类型(normal/workflow',
'required': False,
},
{
'name': 'description',
'type': 'string',
'description': '表单描述',
'required': False,
},
{
'name': 'sort',
'type': 'number',
'description': '排序',
'required': False,
},
]
outputs = [
{
'name': 'form_basic_info',
'type': 'object',
'description': '处理后的表单基础信息',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info(f'FormBasicInfoNode - 使用用户编辑的数据: {edited_data}')
# 使用编辑后的数据,清除用户输入
context.variables.pop('__user_input__', None)
return self._build_result(edited_data, require_confirmation=False)
# 获取输入参数
name = context.resolve_template(self.config.get('name', ''))
code = context.resolve_template(self.config.get('code', ''))
form_type = context.resolve_template(self.config.get('form_type', 'normal'))
description = context.resolve_template(self.config.get('description', ''))
# 处理sort(可能是字符串或数字)
sort_value = self.config.get('sort', 0)
# 先转换为字符串,再调用resolve_template
sort_str = context.resolve_template(str(sort_value))
try:
sort = int(sort_str) if sort_str else 0
except (ValueError, TypeError):
sort = 0
logger.info(f'FormBasicInfoNode - 输入参数: name={name}, code={code}, form_type={form_type}')
# 验证必填字段
if not name or not name.strip():
return NodeResult(
success=False,
error='表单名称不能为空',
)
name = name.strip()
# 自动生成编码(如果未提供或配置了自动生成)
auto_generate = self.config.get('auto_generate_code', False)
if not code or not code.strip() or auto_generate:
code = self._generate_code(name)
logger.info(f'自动生成编码: {code}')
else:
code = code.strip()
# 验证编码格式
if not re.match(r'^[a-z][a-z0-9_]*$', code):
return NodeResult(
success=False,
error=f'表单编码格式不正确:{code},必须以字母开头,只能包含小写字母、数字和下划线',
)
# 验证表单类型
if form_type not in ['normal', 'workflow']:
logger.warning(f'表单类型 {form_type} 不合法,使用默认值 normal')
form_type = 'normal'
# 构建输出
basic_info = {
'name': name,
'code': code,
'form_type': form_type,
'description': description.strip() if description else name,
'sort': sort,
}
logger.info(f'FormBasicInfoNode - 输出: {basic_info}')
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
return self._build_result(basic_info, require_confirmation)
except Exception as e:
logger.exception(f'表单基础信息节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点(直接调用同步方法)"""
return self.execute(context)
def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户编辑的数据"""
import json
if isinstance(user_input, dict):
# 检查是否包含表单基础信息字段
if 'name' in user_input:
return user_input
return None
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict) and 'name' in data:
return data
except (json.JSONDecodeError, TypeError):
pass
return None
def _build_result(self, basic_info: Dict[str, Any], require_confirmation: bool = False) -> NodeResult:
"""构建节点结果"""
name = basic_info.get('name', '')
code = basic_info.get('code', '')
form_type = basic_info.get('form_type', 'normal')
description = basic_info.get('description', name)
sort = basic_info.get('sort', 0)
# 构建预览数据
preview_data = {
'type': 'form_basic_info',
'title': '表单基础信息',
'data': basic_info,
'editable': True,
} if require_confirmation else None
return NodeResult(
success=True,
output=basic_info,
output_variables={
'form_basic_info': basic_info,
'form_name': name,
'form_code': code,
'form_type': form_type,
'form_description': description,
'form_sort': sort,
},
preview=preview_data,
waiting_for_input=require_confirmation,
waiting_config={
'type': 'design_preview',
'preview_type': 'form_basic_info',
'title': '表单基础信息',
'message': '请确认或编辑表单基础信息',
'data': basic_info,
} if require_confirmation else {},
)
def _generate_code(self, name: str) -> str:
"""
根据表单名称自动生成编码
Args:
name: 表单名称
Returns:
生成的编码
"""
try:
from pypinyin import lazy_pinyin
# 将中文转拼音
pinyin_list = lazy_pinyin(name)
code = '_'.join(pinyin_list)
except ImportError:
logger.warning('pypinyin 未安装,使用简单的编码生成策略')
# 如果没有安装 pypinyin,使用简单策略
code = name.lower()
# 只保留字母、数字和下划线
code = re.sub(r'[^a-z0-9_]', '', code.lower())
# 确保以字母开头
if code and not code[0].isalpha():
code = 'form_' + code
# 如果为空,使用默认值
if not code:
code = 'form_default'
return code
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""
获取节点配置 Schema(供前端表单渲染)
Returns:
配置 Schema
"""
return {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'title': '表单名称',
'description': '表单的显示名称,支持变量引用如 {{llm_basic_name}}',
'required': True,
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入表单名称或使用变量 {{llm_basic_name}}',
},
},
'code': {
'type': 'string',
'title': '表单编码',
'description': '表单的唯一编码,支持变量引用如 {{llm_basic_code}}',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '留空则自动生成,或使用变量 {{llm_basic_code}}',
},
},
'form_type': {
'type': 'string',
'title': '表单类型',
'description': '表单类型:normal(普通表单)或 workflow(工作流表单)',
'default': 'normal',
'enum': ['normal', 'workflow'],
'x-component': 'Select',
'x-component-props': {
'placeholder': '请选择表单类型',
'options': [
{'label': '普通表单', 'value': 'normal'},
{'label': '工作流表单', 'value': 'workflow'},
],
},
},
'description': {
'type': 'string',
'title': '表单描述',
'description': '表单的详细描述,支持变量引用',
'required': False,
'x-component': 'Textarea',
'x-component-props': {
'placeholder': '请输入表单描述',
'rows': 3,
},
},
'sort': {
'type': 'number',
'title': '排序',
'description': '表单的排序值,数字越小越靠前',
'default': 0,
'x-component': 'InputNumber',
'x-component-props': {
'placeholder': '请输入排序值',
'min': 0,
},
},
'auto_generate_code': {
'type': 'boolean',
'title': '自动生成编码',
'description': '是否自动根据表单名称生成编码(拼音转换)',
'default': False,
'x-component': 'Switch',
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '设计完成后是否暂停等待用户确认或编辑',
'default': True,
'x-component': 'Switch',
},
},
'required': ['name'],
}
@@ -0,0 +1,499 @@
"""
表单创建节点
将表单配置保存到数据库,创建完整的表单元数据
"""
import ast
import json
import logging
from typing import Any, Dict, List, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormCreateNode(BaseNode):
"""
表单创建节点
将表单基础信息、表单UI设计、列表设计等配置保存到数据库
"""
node_type = 'form_create'
node_name = '表单创建'
node_category = 'form'
node_icon = 'file-plus'
node_description = '将表单配置保存到数据库,创建完整的表单元数据'
inputs = [
{
'name': 'form_basic_info',
'type': 'object',
'description': '表单基础信息(来自表单基础信息节点)',
'required': True,
},
{
'name': 'database_design',
'type': 'object',
'description': '数据库设计配置(来自数据库设计节点)',
'required': True,
},
{
'name': 'form_ui_design',
'type': 'object',
'description': '表单UI设计配置(来自表单UI设计节点)',
'required': True,
},
{
'name': 'list_config',
'type': 'object',
'description': '列表配置(来自列表UI设计节点)',
'required': True,
},
{
'name': 'application_id',
'type': 'string',
'description': '所属应用ID(可选)',
'required': False,
},
]
outputs = [
{
'name': 'form_id',
'type': 'string',
'description': '创建的表单ID',
},
{
'name': 'form_code',
'type': 'string',
'description': '表单编码',
},
{
'name': 'form_meta',
'type': 'object',
'description': '完整的表单元数据',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点(同步方法,通过运行异步方法实现)"""
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
else:
return loop.run_until_complete(self.execute_async(context))
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 获取输入参数(原始配置值)
form_basic_info_raw = self.config.get('form_basic_info', '{}')
database_design_raw = self.config.get('database_design', '{}')
form_ui_design_raw = self.config.get('form_ui_design', '{}')
list_config_raw = self.config.get('list_config', '{}')
# 解析变量引用,保持原始类型
form_basic_info = self._resolve_variable_value(context, form_basic_info_raw)
database_design = self._resolve_variable_value(context, database_design_raw)
form_ui_design = self._resolve_variable_value(context, form_ui_design_raw)
list_config = self._resolve_variable_value(context, list_config_raw)
application_id = context.resolve_template(self.config.get('application_id', ''))
logger.info(f'FormCreateNode - 解析结果: form_basic_info={bool(form_basic_info)}, database_design={bool(database_design)}, form_ui_design={bool(form_ui_design)}, list_config={bool(list_config)}')
# 配置选项
update_if_exists = self.config.get('update_if_exists', True)
logger.info(f'FormCreateNode - 开始创建表单')
# 验证必要配置
if not form_basic_info:
return NodeResult(
success=False,
error='表单基础信息配置不能为空',
)
if not database_design:
return NodeResult(
success=False,
error='数据库设计配置不能为空',
)
if not form_ui_design:
return NodeResult(
success=False,
error='表单UI设计配置不能为空',
)
if not list_config:
return NodeResult(
success=False,
error='列表配置不能为空',
)
# 提取基础信息
form_name = form_basic_info.get('name', '')
form_code_base = form_basic_info.get('code', '')
form_type = form_basic_info.get('form_type', 'normal')
description = form_basic_info.get('description', '')
sort = form_basic_info.get('sort', 0)
if not form_name or not form_code_base:
return NodeResult(
success=False,
error='表单名称和编码不能为空',
)
# 提取数据库配置
table_info = database_design.get('table', {})
db_config = database_design.get('dbConfig', 'default')
meta = table_info.get('meta', {})
main_table = table_info.get('tableName', '')
# 优先使用原始 schema 配置(可能包含变量),如果不存在则使用解析后的值
main_table_schema = meta.get('schemaRaw', meta.get('schema', ''))
main_table_database = meta.get('database', '')
if not main_table:
return NodeResult(
success=False,
error='主表名不能为空',
)
# 构建表配置(用于 form_config.tableConfigs
table_configs = self._build_table_configs(database_design)
# 构建 form_config
form_config = {
'items': form_ui_design.get('items', []),
'labelWidth': form_ui_design.get('labelWidth', 100),
'labelPosition': form_ui_design.get('labelPosition', 'right'),
'size': form_ui_design.get('size', 'default'),
'tableConfigs': table_configs,
}
# 调用 FormService 创建表单
from app.database import AsyncSessionLocal
from online_dev.form_manager.service import FormService, FormServiceException
async with AsyncSessionLocal() as db:
try:
# 生成唯一的 code(如果已存在则自动添加后缀)
form_code = await self._get_unique_code(db, form_code_base, FormService)
logger.info(f'FormCreateNode - 使用 code: {form_code} (原始: {form_code_base})')
# 构建表单数据
form_data = {
'application_id': application_id if application_id else None,
'name': form_name,
'code': form_code,
'form_type': form_type,
'description': description,
'sort': sort,
'db_config': db_config,
'main_table': main_table,
'main_table_schema': main_table_schema,
'main_table_database': main_table_database,
'form_config': form_config,
'list_config': list_config,
'sub_tables': [], # 暂不支持子表
}
logger.info(f'FormCreateNode - 表单数据: name={form_name}, code={form_code}, main_table={main_table}')
# 由于 _get_unique_code 已确保 code 唯一,直接创建表单
# 如果仍然出现重复(极端并发情况),捕获异常并处理
try:
form = await FormService.create(db, form_data)
logger.info(f'表单创建成功: {form_code}')
except FormServiceException as e:
# 如果是编码重复错误且允许更新,尝试更新
if '已存在' in str(e) and update_if_exists:
logger.warning(f'表单编码冲突,尝试更新: {form_code}')
try:
existing_form = await FormService.get_by_code(db, form_code)
form = await FormService.update(db, existing_form.id, form_data)
logger.info(f'表单更新成功: {form_code}')
except Exception as update_error:
return NodeResult(
success=False,
error=f'表单创建和更新均失败: {str(update_error)}',
)
else:
return NodeResult(
success=False,
error=f'表单创建失败: {str(e)}',
)
# 构建输出
form_meta = {
'id': form.id,
'name': form.name,
'code': form.code,
'form_type': form.form_type,
'description': form.description,
'status': form.status,
'version': form.version,
'db_config': form.db_config,
'main_table': form.main_table,
'main_table_schema': form.main_table_schema,
'main_table_database': form.main_table_database,
}
logger.info(f'FormCreateNode - 表单创建成功: id={form.id}, code={form.code}')
return NodeResult(
success=True,
output=form_meta,
output_variables={
'form_id': form.id,
'form_code': form.code,
'form_meta': form_meta,
},
)
except FormServiceException as e:
logger.error(f'表单服务异常: {e}')
return NodeResult(
success=False,
error=f'表单创建失败: {str(e)}',
)
except Exception as e:
logger.exception(f'表单创建异常: {e}')
return NodeResult(
success=False,
error=f'表单创建异常: {str(e)}',
)
except Exception as e:
logger.exception(f'表单创建节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
def _resolve_variable_value(self, context: NodeContext, raw_value: Any) -> Optional[Dict]:
"""
解析变量引用,保持原始类型
支持格式:
- {{node_id.variable_name}} - 节点变量引用
- 直接的字典值
- JSON 字符串
"""
import re
# 如果已经是字典,直接返回
if isinstance(raw_value, dict):
return raw_value if raw_value else None
# 如果不是字符串,尝试解析
if not isinstance(raw_value, str):
return None
raw_value = raw_value.strip()
if not raw_value or raw_value == '{}':
return None
# 检查是否是单个变量引用 {{node_id.variable_name}}
single_var_pattern = r'^\{\{([^}]+)\}\}$'
match = re.match(single_var_pattern, raw_value)
if match:
var_ref = match.group(1).strip()
# 解析 node_id.variable_name 格式
if '.' in var_ref:
parts = var_ref.split('.', 1)
node_id = parts[0]
key = parts[1]
# 从节点输出命名空间获取
node_outputs = context.variables.get(f'_node_{node_id}')
if isinstance(node_outputs, dict) and key in node_outputs:
value = node_outputs[key]
if isinstance(value, dict):
return value if value else None
return self._parse_config(value)
# 变量未找到
logger.warning(f'变量引用未找到: {var_ref}')
else:
# 直接变量引用
if var_ref in context.variables:
value = context.variables[var_ref]
if isinstance(value, dict):
return value if value else None
return self._parse_config(value)
logger.warning(f'变量引用未找到: {var_ref}')
# 不是变量引用,尝试解析为 JSON 或 Python dict
return self._parse_config(raw_value)
def _parse_config(self, config_str: Any) -> Optional[Dict]:
"""解析配置"""
# 如果已经是字典,直接返回
if isinstance(config_str, dict):
return config_str if config_str else None
# 如果是列表,返回 None(不是有效的配置)
if isinstance(config_str, list):
return None
# 如果是字符串,尝试解析
if isinstance(config_str, str):
config_str = config_str.strip()
if not config_str or config_str == '{}' or config_str == 'None':
return None
# 检查是否是未解析的模板变量(仍然包含 {{ }})
if '{{' in config_str and '}}' in config_str:
logger.warning(f'配置值仍包含未解析的模板变量: {config_str[:100]}')
return None
# 尝试 JSON 解析
try:
result = json.loads(config_str)
if isinstance(result, dict):
return result if result else None
return None
except json.JSONDecodeError:
pass
# 尝试 Python literal_eval(处理单引号格式)
try:
result = ast.literal_eval(config_str)
if isinstance(result, dict):
return result if result else None
return None
except (ValueError, SyntaxError) as e:
logger.warning(f'配置解析失败: {e}, 值: {config_str[:200]}')
return None
return None
async def _get_unique_code(self, db, base_code: str, service) -> str:
"""
生成唯一的 code,如果已存在则自动添加数字后缀
Args:
db: 数据库会话
base_code: 基础 code
service: 服务类(需要有 get_by_code 方法)
Returns:
唯一的 code
"""
from sqlalchemy import select
from online_dev.form_manager.model import FormMeta
code = base_code
counter = 1
while True:
# 直接查询数据库,避免 get_by_code 的异常处理问题
stmt = select(FormMeta).where(
FormMeta.code == code,
FormMeta.is_deleted == False
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if not existing:
# code 不存在,可以使用
break
# code 已存在,添加后缀
code = f"{base_code}_{counter}"
counter += 1
# 防止无限循环
if counter > 100:
logger.warning(f'生成唯一 code 失败,已尝试 {counter}')
break
return code
def _build_table_configs(self, database_design: Dict) -> List[Dict]:
"""构建表配置列表"""
table_configs = []
table_info = database_design.get('table', {})
if not table_info:
return table_configs
meta = table_info.get('meta', {}).copy() if table_info.get('meta') else {}
# 确保 meta 中包含 dbName(从 database_design.dbConfig 获取,如果 meta 中没有的话)
if not meta.get('dbName') and database_design.get('dbConfig'):
meta['dbName'] = database_design.get('dbConfig')
# 主表配置
main_config = {
'type': 'main',
'tableName': table_info.get('tableName', ''),
'alias': table_info.get('alias', table_info.get('tableName', '')),
'fields': table_info.get('fields', []),
'meta': meta,
}
table_configs.append(main_config)
return table_configs
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取节点配置 Schema"""
return {
'type': 'object',
'properties': {
'form_basic_info': {
'type': 'string',
'title': '表单基础信息',
'description': '来自表单基础信息节点的输出',
'x-component': 'SmartInput',
},
'database_design': {
'type': 'string',
'title': '数据库设计配置',
'description': '来自数据库设计节点的输出',
'x-component': 'SmartInput',
},
'form_ui_design': {
'type': 'string',
'title': '表单UI设计配置',
'description': '来自表单UI设计节点的输出',
'x-component': 'SmartInput',
},
'list_config': {
'type': 'string',
'title': '列表配置',
'description': '来自列表UI设计节点的输出',
'x-component': 'SmartInput',
},
'application_id': {
'type': 'string',
'title': '所属应用ID',
'description': '表单所属的应用ID(子应用模式下自动获取)',
'default': '{{application_id}}',
'x-component': 'SmartInput',
},
'update_if_exists': {
'type': 'boolean',
'title': '存在时更新',
'description': '表单编码已存在时是否更新',
'default': True,
'x-component': 'Switch',
},
},
'required': ['form_basic_info', 'database_design', 'form_ui_design', 'list_config'],
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,349 @@
"""
数据库表创建节点
根据数据库设计配置实际创建Schema和表
"""
import logging
from typing import Any, Dict, List
from core.database_connection.resolver import ConnectionResolver
from core.database_manager.ddl_builder import (
build_create_schema_sql,
build_create_table_ddl,
normalize_db_type,
)
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
from ..utils.config_utils import resolve_object_config
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormDatabaseCreateNode(BaseNode):
"""
数据库表创建节点
根据数据库设计配置实际创建Schema和表
"""
node_type = 'form_database_create'
node_name = '数据库表创建'
node_category = 'form'
node_icon = 'database-zap'
node_description = '根据设计配置实际创建数据库Schema和表'
inputs = [
{
'name': 'database_design',
'type': 'object',
'description': '数据库设计配置',
'required': True,
},
{
'name': 'if_exists',
'type': 'string',
'description': '表已存在时的处理方式',
'required': False,
},
{
'name': 'create_schema_if_not_exists',
'type': 'boolean',
'description': '自动创建Schema',
'required': False,
},
]
outputs = [
{
'name': 'creation_result',
'type': 'object',
'description': '创建结果',
},
{
'name': 'all_tables_ready',
'type': 'boolean',
'description': '所有表是否就绪',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点(同步方法,通过运行异步方法实现)"""
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
return loop.run_until_complete(self.execute_async(context))
async def _resolve_db_type(self, db_name: str, meta_db_type: str, context: NodeContext) -> str:
configured = normalize_db_type(meta_db_type or 'postgresql')
try:
conn_info = await ConnectionResolver.resolve(db_name, context.db_session)
resolved = normalize_db_type(conn_info.db_type)
if configured != resolved:
logger.warning(
'FormDatabaseCreateNode - db_type 不一致: meta=%s, 连接 %s 解析为 %s,以连接为准',
configured,
db_name,
resolved,
)
return resolved
except Exception as exc:
logger.warning(
'FormDatabaseCreateNode - 无法解析连接 %s 的 db_type: %s,使用 meta.dbType=%s',
db_name,
exc,
configured,
)
return configured
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
database_design = resolve_object_config(
context, self.config.get('database_design', '')
)
if_exists = context.resolve_template(self.config.get('if_exists', 'skip'))
create_schema = self.config.get('create_schema_if_not_exists', True)
node_schema_raw = self.config.get('db_schema', '')
node_schema = context.resolve_template(node_schema_raw)
node_database_raw = self.config.get('db_database', '')
node_database = context.resolve_template(node_database_raw)
logger.info(
'FormDatabaseCreateNode - database: raw=%s resolved=%s schema: raw=%s resolved=%s',
node_database_raw,
node_database,
node_schema_raw,
node_schema,
)
if database_design is None:
return NodeResult(
success=False,
error='数据库设计配置不能为空或解析失败',
)
design_type = database_design.get('type', '')
db_config = database_design.get('dbConfig', 'default')
if design_type in ['main', 'sub']:
table_info = database_design.get('table', {})
if not table_info:
return NodeResult(
success=False,
error='数据库设计配置中缺少 table 信息',
)
all_tables = [table_info]
main_table = table_info
elif database_design.get('mainTable'):
main_table = database_design.get('mainTable', {})
sub_tables = database_design.get('subTables', [])
all_tables = [main_table] + sub_tables
else:
return NodeResult(
success=False,
error='数据库设计配置格式无效,需要包含 type+table 或 mainTable',
)
meta = main_table.get('meta', {})
db_name = meta.get('dbName', db_config)
database = node_database if node_database else meta.get('database', '')
meta_schema = meta.get('schema', '')
schema = node_schema if node_schema else meta_schema
db_type = await self._resolve_db_type(
db_name,
meta.get('dbType', 'postgresql'),
context,
)
logger.info(
'FormDatabaseCreateNode - dbName=%s dbType=%s database=%s schema=%s',
db_name,
db_type,
database,
schema,
)
created_tables: List[str] = []
skipped_tables: List[str] = []
errors: List[str] = []
sql_statements: List[str] = []
comment_statements: List[str] = []
if create_schema and schema and db_type in ('postgresql', 'sqlserver'):
schema_sql = build_create_schema_sql(schema, db_type)
if schema_sql:
sql_statements.append(schema_sql)
for table in all_tables:
table_name = table.get('tableName', '')
if not table_name:
errors.append('表配置缺少 tableName')
continue
try:
ddl_result = build_create_table_ddl(
table,
db_type=db_type,
if_exists=if_exists,
effective_schema=schema,
)
if ddl_result.skipped:
skipped_tables.append(table_name)
continue
if ddl_result.create_sql:
sql_statements.append(ddl_result.create_sql)
created_tables.append(table_name)
comment_statements.extend(ddl_result.comment_sqls)
except Exception as exc:
error_msg = f'生成表 {table_name} 的SQL失败: {str(exc)}'
errors.append(error_msg)
logger.error(error_msg)
if sql_statements:
try:
from core.database_manager.service import AsyncDatabaseManagerService
full_sql = '\n\n'.join(sql_statements)
logger.info('FormDatabaseCreateNode - 执行 DDL,共 %d', len(sql_statements))
db_service = await AsyncDatabaseManagerService.create(db_name)
result = await db_service.execute_ddl(full_sql, database, schema)
if not result.get('success', False):
error_msg = result.get('message', '执行DDL失败')
errors.append(
f'[{db_name}/{db_type}] {error_msg}; SQL开头: {full_sql[:200]}...'
)
logger.error('DDL执行失败: %s', error_msg)
elif comment_statements and if_exists != 'skip':
try:
comment_sql = '\n'.join(comment_statements)
comment_result = await db_service.execute_ddl(
comment_sql, database, schema,
)
if not comment_result.get('success', False):
logger.warning(
'字段注释执行失败: %s',
comment_result.get('message', '未知错误'),
)
except Exception as comment_error:
logger.warning('执行字段注释时发生异常: %s', comment_error)
except Exception as exc:
error_msg = f'执行DDL时发生异常: {str(exc)}'
errors.append(error_msg)
logger.exception(error_msg)
all_tables_ready = len(errors) == 0 and (len(created_tables) > 0 or len(skipped_tables) > 0)
creation_result = {
'success': all_tables_ready,
'created_tables': created_tables,
'skipped_tables': skipped_tables,
'errors': errors,
'sql_statements': sql_statements,
'total_tables': len(all_tables),
'db_name': db_name,
'db_type': db_type,
}
first_table_name = created_tables[0] if created_tables else (
skipped_tables[0] if skipped_tables else ''
)
output_vars = {
'creation_result': creation_result,
'all_tables_ready': all_tables_ready,
'created_count': len(created_tables),
'error_count': len(errors),
'schema_name': schema,
'table_name': first_table_name,
'db_name': db_name,
'db_type': db_type,
}
if errors:
return NodeResult(
success=False,
error=f'创建表时发生错误: {"; ".join(errors)}',
output=creation_result,
output_variables=output_vars,
)
return NodeResult(
success=True,
output=creation_result,
output_variables=output_vars,
)
except Exception as exc:
logger.exception('数据库表创建节点执行失败: %s', exc)
return NodeResult(
success=False,
error=f'节点执行失败: {str(exc)}',
)
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取节点配置 Schema"""
return {
'type': 'object',
'properties': {
'db_schema': {
'type': 'string',
'title': '数据库 Schema(可选)',
'description': 'PostgreSQL/SQL Server schema 名称(如 public、app),支持变量如 {{schema_name}}。如果配置则优先使用此值,否则使用设计节点的 schema',
'default': '',
'x-component': 'SmartInput',
},
'db_database': {
'type': 'string',
'title': '目标数据库(可选)',
'description': 'PostgreSQL/MySQL/SQL Server 目标库名,支持变量。如果配置则优先使用此值,否则使用设计节点的 database',
'default': '',
'x-component': 'SmartInput',
},
'database_design': {
'type': 'string',
'title': '数据库设计配置',
'description': '从上一节点获取的数据库设计配置,支持变量引用如 {{database_design}}',
'required': True,
'x-component': 'Textarea',
'x-component-props': {
'placeholder': '请使用变量 {{database_design}}',
'rows': 4,
},
},
'if_exists': {
'type': 'string',
'title': '表已存在时',
'description': '表已存在时的处理方式',
'default': 'skip',
'enum': ['skip', 'error', 'replace'],
'x-component': 'Select',
'x-component-props': {
'placeholder': '请选择处理方式',
'options': [
{'label': '跳过(推荐)', 'value': 'skip'},
{'label': '报错', 'value': 'error'},
{'label': '删除重建(危险)', 'value': 'replace'},
],
},
},
'create_schema_if_not_exists': {
'type': 'boolean',
'title': '自动创建Schema',
'description': 'Schema不存在时自动创建(PostgreSQL/SQL Server',
'default': True,
'x-component': 'Switch',
},
},
'required': ['database_design'],
}
@@ -0,0 +1,507 @@
"""
数据库表结构设计节点
接收LLM提取的表结构,进行验证、标准化和补充系统字段
"""
import asyncio
import json
import logging
import uuid
from typing import Any, Dict, List, Optional
from core.database_connection.resolver import ConnectionResolver
from core.database_manager.ddl_builder import (
SYSTEM_FIELDS,
normalize_db_type,
process_canonical_field,
)
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
from ..utils.config_utils import resolve_list_config
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormDatabaseDesignNode(BaseNode):
"""
数据库表结构设计节点
接收LLM提取的表结构,验证、标准化并补充系统字段
"""
node_type = 'form_database_design'
node_name = '数据库表结构设计'
node_category = 'form'
node_icon = 'database'
node_description = '验证和标准化数据库表结构,自动补充系统字段'
inputs = [
{
'name': 'design_mode',
'type': 'string',
'description': '设计模式:main(主表)或 sub(从表)',
'required': True,
},
{
'name': 'table_name',
'type': 'string',
'description': '主表名(主表模式)',
'required': False,
},
{
'name': 'fields',
'type': 'array',
'description': '主表字段列表(主表模式)',
'required': False,
},
{
'name': 'sub_table_name',
'type': 'string',
'description': '从表名(从表模式)',
'required': False,
},
{
'name': 'sub_fields',
'type': 'array',
'description': '从表字段列表(从表模式)',
'required': False,
},
{
'name': 'parent_table',
'type': 'string',
'description': '关联主表名(从表模式)',
'required': False,
},
{
'name': 'foreign_key',
'type': 'string',
'description': '外键字段名(从表模式)',
'required': False,
},
{
'name': 'db_config',
'type': 'string',
'description': '数据库配置名',
'required': False,
},
{
'name': 'auto_add_system_fields',
'type': 'boolean',
'description': '是否自动添加系统字段',
'required': False,
},
]
outputs = [
{
'name': 'database_design',
'type': 'object',
'description': '标准化的数据库设计配置',
},
{
'name': 'table_name',
'type': 'string',
'description': '表名',
},
{
'name': 'field_count',
'type': 'number',
'description': '字段数量',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
return loop.run_until_complete(self.execute_async(context))
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info('FormDatabaseDesignNode - 使用用户编辑的数据')
context.variables.pop('__user_input__', None)
return self._build_result_from_edit(edited_data)
design_mode = self.config.get('design_mode', 'main')
db_config = context.resolve_template(self.config.get('db_config', 'default'))
db_schema_raw = self.config.get('db_schema', '')
db_schema = context.resolve_template(db_schema_raw)
db_database_raw = self.config.get('db_database', '')
db_database = context.resolve_template(db_database_raw)
config_db_type = self.config.get('db_type', 'postgresql')
db_type, db_type_resolved = await self._resolve_db_type(
db_config, config_db_type, context,
)
auto_add_system = self.config.get('auto_add_system_fields', True)
logger.info(
'FormDatabaseDesignNode - mode=%s database=%s schema=%s db_config=%s db_type=%s resolved=%s',
design_mode,
db_database,
db_schema,
db_config,
db_type,
db_type_resolved,
)
if design_mode == 'main':
table_name_str = context.resolve_template(self.config.get('table_name', ''))
fields = resolve_list_config(context, self.config.get('fields', '[]'))
if not table_name_str:
return NodeResult(success=False, error='主表名不能为空')
if fields is None:
return NodeResult(success=False, error='字段列表解析失败')
if not fields:
return NodeResult(success=False, error='字段列表不能为空')
table_config = {'tableName': table_name_str, 'fields': fields}
processed_table = self._process_table(
table_config,
'main',
auto_add_system,
db_config,
db_database,
db_database_raw,
db_schema,
db_schema_raw,
db_type,
db_type_resolved,
)
database_design = {
'type': 'main',
'table': processed_table,
'dbConfig': db_config,
}
field_count = len(processed_table['fields'])
require_confirmation = self.resolve_require_confirmation(context, default=True)
preview_data = {
'type': 'database_design',
'title': '数据库表结构设计',
'data': database_design,
'editable': True,
} if require_confirmation else None
return NodeResult(
success=True,
output=database_design,
output_variables={
'database_design': database_design,
'table_name': table_name_str,
'field_count': field_count,
},
preview=preview_data,
waiting_for_input=require_confirmation,
waiting_config={
'type': 'design_preview',
'preview_type': 'database_design',
'title': '数据库表结构设计',
'message': '请确认或编辑数据库表结构',
'data': database_design,
} if require_confirmation else {},
)
sub_table_name_str = context.resolve_template(self.config.get('sub_table_name', ''))
sub_fields = resolve_list_config(context, self.config.get('sub_fields', '[]'))
parent_table = context.resolve_template(self.config.get('parent_table', ''))
foreign_key = context.resolve_template(self.config.get('foreign_key', ''))
if not sub_table_name_str:
return NodeResult(success=False, error='从表名不能为空')
if not parent_table:
return NodeResult(success=False, error='关联主表不能为空')
if sub_fields is None:
return NodeResult(success=False, error='从表字段列表解析失败')
if not sub_fields:
return NodeResult(success=False, error='从表字段列表不能为空')
if not foreign_key:
foreign_key = f'{parent_table}_id'
table_config = {
'tableName': sub_table_name_str,
'fields': sub_fields,
'foreignKey': foreign_key,
'parentTable': parent_table,
}
processed_table = self._process_table(
table_config,
'sub',
auto_add_system,
db_config,
db_database,
db_database_raw,
db_schema,
db_schema_raw,
db_type,
db_type_resolved,
)
database_design = {
'type': 'sub',
'table': processed_table,
'parentTable': parent_table,
'foreignKey': foreign_key,
'dbConfig': db_config,
}
field_count = len(processed_table['fields'])
return NodeResult(
success=True,
output=database_design,
output_variables={
'database_design': database_design,
'table_name': sub_table_name_str,
'field_count': field_count,
},
)
except Exception as exc:
logger.exception('数据库表结构设计节点执行失败: %s', exc)
return NodeResult(success=False, error=f'节点执行失败: {str(exc)}')
async def _resolve_db_type(
self,
db_config: str,
config_db_type: str,
context: NodeContext,
) -> tuple[str, bool]:
configured = normalize_db_type(config_db_type or 'postgresql')
try:
conn_info = await ConnectionResolver.resolve(db_config, context.db_session)
resolved = normalize_db_type(conn_info.db_type)
if configured != resolved:
logger.warning(
'FormDatabaseDesignNode - db_type 配置=%s 与连接 %s 解析=%s 不一致,以连接为准',
configured,
db_config,
resolved,
)
return resolved, True
except Exception as exc:
logger.warning(
'FormDatabaseDesignNode - 无法解析连接 %s: %s,使用配置 db_type=%s',
db_config,
exc,
configured,
)
return configured, False
def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]:
if isinstance(user_input, dict):
if 'table' in user_input or 'type' in user_input:
return user_input
return None
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict) and ('table' in data or 'type' in data):
return data
except (json.JSONDecodeError, TypeError):
pass
return None
def _build_result_from_edit(self, edited_data: Dict[str, Any]) -> NodeResult:
table_info = edited_data.get('table', {})
table_name = table_info.get('tableName', '')
field_count = len(table_info.get('fields', []))
return NodeResult(
success=True,
output=edited_data,
output_variables={
'database_design': edited_data,
'table_name': table_name,
'field_count': field_count,
},
)
def _process_table(
self,
table: dict,
table_type: str,
auto_add_system: bool,
db_config: str,
db_database: str = '',
db_database_raw: str = '',
db_schema: str = '',
db_schema_raw: str = '',
db_type: str = 'postgresql',
db_type_resolved: bool = False,
) -> dict:
table_name = table.get('tableName', '')
if not table_name:
raise ValueError(f'{table_type} 表必须包含 tableName 字段')
alias = table.get('alias', table_name)
fields = table.get('fields', [])
if not isinstance(fields, list):
raise ValueError(f'{table_name} 的 fields 必须是数组')
processed_fields = []
existing_field_names = set()
for fld in fields:
if not isinstance(fld, dict):
logger.warning('跳过无效的字段配置: %s', fld)
continue
field_name = fld.get('name', '')
if not field_name:
logger.warning('跳过没有名称的字段: %s', fld)
continue
if field_name in existing_field_names:
logger.warning('跳过重复字段: %s', field_name)
continue
existing_field_names.add(field_name)
processed_fields.append(process_canonical_field(fld))
if auto_add_system:
for sys_field in SYSTEM_FIELDS:
if sys_field['name'] not in existing_field_names:
processed_fields.append(sys_field.copy())
final_schema = db_schema or table.get('schema', '')
final_database = db_database or table.get('database', '')
result = {
'id': str(uuid.uuid4()).replace('-', ''),
'type': table_type,
'tableName': table_name,
'alias': alias,
'fields': processed_fields,
'meta': {
'dbName': db_config,
'database': final_database,
'databaseRaw': db_database_raw,
'schema': final_schema,
'schemaRaw': db_schema_raw,
'dbType': normalize_db_type(db_type),
'dbTypeResolved': db_type_resolved,
},
}
if table_type == 'sub':
result['foreignKey'] = table.get('foreignKey', '')
result['parentTable'] = table.get('parentTable', '')
result['relatedField'] = table.get('relatedField', 'id')
result['relationType'] = table.get('relationType', 'one-to-many')
return result
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
return {
'type': 'object',
'properties': {
'design_mode': {
'type': 'string',
'title': '设计模式',
'description': '选择设计主表或从表',
'default': 'main',
'enum': ['main', 'sub'],
'x-component': 'RadioGroup',
},
'table_name': {
'type': 'string',
'title': '表名',
'description': '主表名(string类型)',
'x-component': 'SmartInput',
},
'fields': {
'type': 'string',
'title': '字段列表',
'description': '主表字段配置(array类型)',
'x-component': 'Textarea',
},
'sub_table_name': {
'type': 'string',
'title': '从表名',
'description': '从表名(string类型)',
'x-component': 'SmartInput',
},
'sub_fields': {
'type': 'string',
'title': '从表字段列表',
'description': '从表字段配置(array类型)',
'x-component': 'Textarea',
},
'parent_table': {
'type': 'string',
'title': '关联主表',
'description': '从表关联的主表名(string类型)',
'x-component': 'SmartInput',
},
'foreign_key': {
'type': 'string',
'title': '外键字段',
'description': '外键字段名(string类型),不填则自动生成',
'x-component': 'SmartInput',
},
'db_config': {
'type': 'string',
'title': '数据库配置',
'description': '数据库配置名称',
'default': 'default',
'x-component': 'Input',
},
'db_database': {
'type': 'string',
'title': '目标数据库',
'description': 'PostgreSQL/MySQL/SQL Server 的目标库名,为空则使用连接默认库',
'default': '',
'x-component': 'SmartInput',
},
'db_schema': {
'type': 'string',
'title': '数据库 Schema',
'description': 'PostgreSQL schema 名称(如 public、app),为空则使用默认',
'default': '',
'x-component': 'SmartInput',
},
'db_type': {
'type': 'string',
'title': '数据库类型',
'description': '由数据库连接自动解析,一般无需手动修改',
'default': 'postgresql',
'enum': ['postgresql', 'mysql', 'oracle', 'sqlserver'],
'x-component': 'Select',
},
'auto_add_system_fields': {
'type': 'boolean',
'title': '自动添加系统字段',
'description': '是否自动添加系统字段(id, sys_create_datetime等)',
'default': True,
'x-component': 'Switch',
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '设计完成后是否暂停等待用户确认或编辑',
'default': True,
'x-component': 'Switch',
},
},
'required': ['design_mode'],
}
@@ -0,0 +1,748 @@
"""
列表UI设计节点
根据表单UI设计配置自动生成列表配置
"""
import json
import logging
import re
import uuid
from typing import Any, Dict, List, Optional, Tuple
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
from ..utils.config_utils import resolve_object_config
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormListDesignNode(BaseNode):
"""
列表UI设计节点
根据表单UI设计配置自动生成列表配置,包括:
- 查询字段配置
- 列表列配置
- 表格属性配置
- 按钮配置
"""
node_type = 'form_list_design'
node_name = '列表UI设计'
node_category = 'form'
node_icon = 'table'
node_description = '根据表单UI设计自动生成列表配置'
# ========== 查询字段推断规则 ==========
# 默认添加为查询字段的字段模式
QUERY_INCLUDE_PATTERNS = [
r'^(name|title|code|no|status|type|category)$',
r'(creator|modifier)_id$',
]
# name 相关字段模式(优先级最高,作为第一个查询字段)
NAME_FIELD_PATTERNS = [
r'^name$',
r'_name$',
r'^title$',
]
# 排除的查询字段模式
QUERY_EXCLUDE_PATTERNS = [
r'^id$',
r'^is_deleted$',
r'^sort$',
r'(remark|note|description|content|address)$',
r'(password|pwd|secret)$',
]
# ========== 列表列推断规则 ==========
# 默认显示的列模式
COLUMN_INCLUDE_PATTERNS = [
r'^(name|title|code|no|status|type|category)$',
r'(amount|price|count|quantity|total)$',
r'sys_create_datetime$',
]
# 排除的列模式
COLUMN_EXCLUDE_PATTERNS = [
r'^id$',
r'^is_deleted$',
r'^sort$',
r'(password|pwd|secret)$',
r'^sys_(creator|modifier|dept)_id$',
]
# 固定在左侧的列模式
FIXED_LEFT_PATTERNS = [
r'^(name|title|code)$',
]
inputs = [
{
'name': 'form_ui_design',
'type': 'object',
'description': '表单UI设计配置(来自表单UI设计节点)',
'required': True,
},
{
'name': 'database_design',
'type': 'object',
'description': '数据库设计配置(可选,用于补充字段信息)',
'required': False,
},
]
outputs = [
{
'name': 'list_config',
'type': 'object',
'description': '完整的列表配置',
},
{
'name': 'query_field_count',
'type': 'number',
'description': '查询字段数量',
},
{
'name': 'column_count',
'type': 'number',
'description': '列数量',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info(f'FormListDesignNode - 使用用户编辑的数据')
context.variables.pop('__user_input__', None)
return self._build_result_from_edit(edited_data)
# 获取配置
form_ui_design = resolve_object_config(
context, self.config.get('form_ui_design', '{}')
)
database_design = resolve_object_config(
context, self.config.get('database_design', '{}')
)
# 列表配置选项
auto_query_fields = self.config.get('auto_query_fields', True)
auto_columns = self.config.get('auto_columns', True)
container_type = self.config.get('container_type', 'drawer')
page_size = self.config.get('page_size', 20)
show_index = self.config.get('show_index', True)
show_selection = self.config.get('show_selection', True)
enable_export = self.config.get('enable_export', True)
enable_import = self.config.get('enable_import', False)
logger.info(f'FormListDesignNode - 开始生成列表配置')
# 解析表单UI设计
if not form_ui_design:
return NodeResult(
success=False,
error='表单UI设计配置解析失败或为空',
)
# 解析数据库设计(可选)
# 获取表单项
form_items = form_ui_design.get('items', [])
if not form_items:
return NodeResult(
success=False,
error='表单项列表为空',
)
# 提取表单字段列表(用于前端列表设计组件)
form_fields = self._extract_form_fields(form_items, database_design)
# 生成查询字段
query_fields = []
if auto_query_fields:
query_fields = self._generate_query_fields(form_items)
# 生成列表列
columns = []
if auto_columns:
columns = self._generate_columns(form_items, database_design)
# 构建列表配置
list_config = {
'queryFields': query_fields,
'columns': columns,
'containerType': container_type,
'table': {
'showPagination': True,
'pageSize': page_size,
'showIndex': show_index,
'showSelection': show_selection,
'stripe': True,
'border': True,
'size': 'default',
'height': 'auto',
'defaultSort': [],
'showSummary': False,
'summaryType': 'sum',
'summaryPrecision': 2,
},
'dialog': {
'width': '800px',
'fullscreen': False,
'draggable': True,
'closeOnClickModal': False,
'closeOnPressEscape': True,
},
'drawer': {
'size': '800px',
'direction': 'rtl',
'withHeader': True,
'closeOnClickModal': False,
'closeOnPressEscape': True,
},
'page': {
'showBackButton': True,
'openInNewTab': True,
},
'buttons': {
'showAdd': True,
'showEdit': True,
'showDelete': True,
'showView': True,
'showExport': enable_export,
'showImport': enable_import,
'showBatchDelete': True,
},
}
query_field_count = len(query_fields)
column_count = len(columns)
logger.info(f'FormListDesignNode - 列表配置生成完成: 查询字段数: {query_field_count}, 列数: {column_count}')
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
# 构建预览数据(包含 form_fields 供前端列表设计组件使用)
preview_data = {
'type': 'list_config',
'title': '列表UI设计',
'data': list_config,
'form_fields': form_fields,
'editable': True,
} if require_confirmation else None
return NodeResult(
success=True,
output=list_config,
output_variables={
'list_config': list_config,
'form_fields': form_fields,
'query_field_count': query_field_count,
'column_count': column_count,
},
preview=preview_data,
waiting_for_input=require_confirmation,
waiting_config={
'type': 'design_preview',
'preview_type': 'list_config',
'title': '列表UI设计',
'message': '请确认或编辑列表UI设计',
'data': list_config,
'form_fields': form_fields,
} if require_confirmation else {},
)
except Exception as e:
logger.exception(f'列表UI设计节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点(直接调用同步方法)"""
return self.execute(context)
def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户编辑的数据"""
if isinstance(user_input, dict):
if 'columns' in user_input or 'queryFields' in user_input:
return user_input
return None
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict) and ('columns' in data or 'queryFields' in data):
return data
except (json.JSONDecodeError, TypeError):
pass
return None
def _build_result_from_edit(self, edited_data: Dict[str, Any]) -> NodeResult:
"""从用户编辑的数据构建节点结果"""
query_fields = edited_data.get('queryFields', [])
columns = edited_data.get('columns', [])
return NodeResult(
success=True,
output=edited_data,
output_variables={
'list_config': edited_data,
'query_field_count': len(query_fields),
'column_count': len(columns),
},
)
def _parse_config(self, config_str: Any) -> Optional[Dict]:
"""解析配置"""
if isinstance(config_str, dict):
return config_str
if isinstance(config_str, str):
config_str = config_str.strip()
if not config_str or config_str == '{}':
return None
try:
return json.loads(config_str)
except json.JSONDecodeError:
import ast
try:
return ast.literal_eval(config_str)
except (ValueError, SyntaxError):
return None
return None
def _extract_form_fields(self, form_items: List[Dict], database_design: Optional[Dict] = None) -> List[Dict]:
"""
从表单项中提取字段列表,供前端列表设计组件使用
Args:
form_items: 表单项列表
database_design: 数据库设计配置(可选,用于获取字段类型)
Returns:
字段列表,每个字段包含 label, field, component, options, props, dbType 等
"""
fields = []
# 获取数据库字段类型映射
db_field_types = {}
if database_design:
table_data = database_design.get('table', database_design)
db_fields = table_data.get('fields', [])
for db_field in db_fields:
db_field_types[db_field.get('name', '')] = db_field.get('type', '')
def extract_from_items(items: List[Dict]):
"""递归提取字段"""
for item in items:
item_type = item.get('type', '')
# 跳过布局组件,递归处理其子元素
if item_type in ['grid', 'row', 'card', 'tabs', 'collapse', 'divider', 'alert', 'timeline', 'text', 'html', 'spacer', 'title', 'steps']:
if item.get('children'):
extract_from_items(item.get('children', []))
if item.get('columns'):
for col in item.get('columns', []):
if col.get('children'):
extract_from_items(col.get('children', []))
if item.get('items'):
for sub_item in item.get('items', []):
if sub_item.get('children'):
extract_from_items(sub_item.get('children', []))
continue
# 提取有字段名的组件
field_name = item.get('field', '')
if field_name:
db_type = db_field_types.get(field_name, '')
fields.append({
'label': item.get('label', field_name),
'field': field_name,
'component': item_type,
'options': item.get('options', []),
'props': item.get('props', {}),
'dbType': db_type,
'isNumeric': db_type.lower() in ['int', 'integer', 'bigint', 'smallint', 'decimal', 'numeric', 'float', 'double', 'real'],
})
extract_from_items(form_items)
# 添加系统字段
system_fields = [
{'name': 'sys_create_datetime', 'label': '创建时间', 'component': 'date-picker', 'dbType': 'timestamp'},
{'name': 'sys_update_datetime', 'label': '更新时间', 'component': 'date-picker', 'dbType': 'timestamp'},
{'name': 'sys_creator_id', 'label': '创建人', 'component': 'user-select', 'dbType': 'varchar'},
{'name': 'sys_modifier_id', 'label': '修改人', 'component': 'user-select', 'dbType': 'varchar'},
{'name': 'sys_dept_id', 'label': '所属部门', 'component': 'dept-select', 'dbType': 'varchar'},
]
existing_field_names = [f['field'] for f in fields]
for sys_field in system_fields:
if sys_field['name'] not in existing_field_names:
fields.append({
'label': sys_field['label'],
'field': sys_field['name'],
'component': sys_field['component'],
'options': [],
'props': {},
'dbType': sys_field['dbType'],
'isNumeric': False,
'isSystemField': True,
})
return fields
def _generate_query_fields(self, form_items: List[Dict]) -> List[Dict]:
"""生成查询字段配置"""
name_fields = [] # name 相关字段(优先级最高)
other_fields = [] # 其他查询字段
for item in form_items:
field_name = item.get('field', '')
if not field_name:
continue
# 检查是否应该排除
if self._matches_patterns(field_name, self.QUERY_EXCLUDE_PATTERNS):
continue
# 检查是否为 name 相关字段
is_name_field = self._matches_patterns(field_name, self.NAME_FIELD_PATTERNS)
# 检查是否应该包含
should_include = False
if is_name_field:
should_include = True
elif self._matches_patterns(field_name, self.QUERY_INCLUDE_PATTERNS):
should_include = True
else:
# 如果不在包含列表中,检查是否是常用查询字段
component = item.get('type', 'input')
# 选择类组件通常需要查询
if component in ['select', 'radio', 'checkbox', 'user-selector', 'dept-selector', 'date']:
should_include = True
if not should_include:
continue
# 生成查询字段配置
query_field = self._build_query_field(item)
if query_field:
# name 字段放在前面,其他字段放在后面
if is_name_field:
name_fields.append(query_field)
else:
other_fields.append(query_field)
# name 字段排在最前面
return name_fields + other_fields
def _build_query_field(self, item: Dict) -> Optional[Dict]:
"""构建单个查询字段配置"""
field_name = item.get('field', '')
label = item.get('label', field_name)
component = item.get('type', 'input')
props = item.get('props', {})
options = item.get('options', [])
# 推断查询组件类型
query_component = self._infer_query_component(component)
# 推断查询类型
query_type = self._infer_query_type(component, field_name)
# 判断是否为日期时间类型
is_datetime = component in ['date', 'datetime'] or props.get('type') in ['datetime', 'datetimerange']
return {
'label': label,
'field': field_name,
'type': query_type,
'component': query_component,
'originalComponent': component,
'options': options,
'props': props,
'dbType': '',
'width': 6,
'defaultValue': '',
'hidden': False,
'multiple': props.get('multiple', False),
'showTime': is_datetime,
}
def _infer_query_component(self, component: str) -> str:
"""推断查询组件类型"""
# 用户/部门/岗位选择器保持原组件
if component in ['user-selector', 'dept-selector', 'role-selector', 'post-selector']:
return component
# 带选项的组件映射为 select
if component in ['select', 'radio', 'checkbox', 'cascader', 'tree-select']:
return 'select'
# 日期类组件保持日期选择
if component in ['date', 'datetime', 'date-picker', 'time']:
return 'date'
# 其他组件默认使用输入框
return 'input'
def _infer_query_type(self, component: str, field_name: str) -> str:
"""推断查询类型"""
# 用户/部门/岗位选择器使用 in 查询
if component in ['user-selector', 'dept-selector', 'role-selector', 'post-selector']:
return 'in'
# 选项组件默认精确匹配
if component in ['select', 'radio', 'checkbox', 'cascader', 'tree-select']:
return 'eq'
# 日期类组件默认范围查询
if component in ['date', 'datetime', 'date-picker', 'time']:
return 'range'
# 文本类默认模糊匹配
return 'like'
def _generate_columns(self, form_items: List[Dict], database_design: Optional[Dict]) -> List[Dict]:
"""生成列表列配置"""
columns = []
# 获取数据库字段信息(用于补充类型信息)
db_fields = {}
if database_design:
table_info = database_design.get('table', {})
for field in table_info.get('fields', []):
db_fields[field.get('name', '')] = field
for item in form_items:
field_name = item.get('field', '')
if not field_name:
continue
# 检查是否应该排除
if self._matches_patterns(field_name, self.COLUMN_EXCLUDE_PATTERNS):
continue
# 生成列配置
column = self._build_column(item, db_fields.get(field_name, {}))
if column:
columns.append(column)
return columns
def _build_column(self, item: Dict, db_field: Dict) -> Optional[Dict]:
"""构建单个列配置"""
field_name = item.get('field', '')
label = item.get('label', field_name)
component = item.get('type', 'input')
props = item.get('props', {})
options = item.get('options', [])
# 判断是否为数值类型
db_type = db_field.get('type', '')
is_numeric = self._is_numeric_type(db_type)
# 判断是否为选项组件
has_options = component in ['select', 'radio', 'checkbox', 'cascader', 'tree-select']
# 判断是否为关联组件
is_relation = component in ['user-selector', 'dept-selector', 'role-selector', 'post-selector']
# 判断是否固定在左侧
fixed = 'left' if self._matches_patterns(field_name, self.FIXED_LEFT_PATTERNS) else False
# 推断对齐方式
align = 'right' if is_numeric else ('center' if component in ['date', 'datetime', 'switch'] else 'left')
# 判断是否为日期时间类型
is_datetime = db_type.lower() in ['datetime', 'timestamp'] or component in ['datetime']
# 推断过滤类型
filter_type = self._infer_filter_type(component)
# 深拷贝选项并添加 tagType
cloned_options = None
if options:
cloned_options = [
{**opt, 'tagType': opt.get('tagType', '')}
for opt in options
]
return {
'label': label,
'field': field_name,
# 排序配置
'sortable': not self._is_no_sort_component(component),
'sortType': 'backend',
# 过滤配置
'filterable': False,
'filterType': filter_type,
'filterQueryType': 'range',
'filterShowTime': is_datetime,
'filterMultiple': True,
# 列显示配置
'fixed': fixed,
'align': align,
'width': '',
'minWidth': '',
'resizable': True,
'showOverflowTooltip': True,
'ellipsis': True,
'formatter': 'none',
'formatPattern': '',
'prefix': '',
'suffix': '',
# 选项显示
'originalComponent': component,
'options': cloned_options,
'showAsTag': has_options,
# 关联字段
'isRelation': is_relation,
'showDisplayName': is_relation,
'displayField': self._generate_display_field(field_name) if is_relation else '',
# 统计
'dbType': db_type,
'isNumeric': is_numeric,
'summaryEnabled': False,
}
def _infer_filter_type(self, component: str) -> str:
"""推断过滤类型"""
if component in ['date', 'datetime', 'date-picker']:
return 'date-range'
elif component in ['user-selector', 'user-select']:
return 'user-select'
elif component in ['dept-selector', 'dept-select', 'department-selector']:
return 'dept-select'
elif component in ['select', 'radio', 'checkbox', 'cascader', 'tree-select']:
return 'select'
return 'input'
def _is_numeric_type(self, db_type: str) -> bool:
"""判断是否为数值类型"""
if not db_type:
return False
db_type_lower = db_type.lower()
return any(t in db_type_lower for t in ['int', 'decimal', 'numeric', 'float', 'double', 'real', 'money'])
def _is_no_sort_component(self, component: str) -> bool:
"""判断是否为不支持排序的组件类型"""
return component in ['file-selector', 'image-selector', 'rich-text', 'textarea']
def _generate_display_field(self, field_name: str) -> str:
"""生成显示字段名"""
if field_name.endswith('_id'):
return field_name.replace('_id', '_name')
if field_name.endswith('Id'):
return field_name.replace('Id', 'Name')
return f'{field_name}_name'
def _matches_patterns(self, field_name: str, patterns: List[str]) -> bool:
"""检查字段名是否匹配任一模式"""
field_name_lower = field_name.lower()
for pattern in patterns:
if re.search(pattern, field_name_lower):
return True
return False
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取节点配置 Schema"""
return {
'type': 'object',
'properties': {
'form_ui_design': {
'type': 'string',
'title': '表单UI设计配置',
'description': '来自表单UI设计节点的输出',
'x-component': 'SmartInput',
},
'database_design': {
'type': 'string',
'title': '数据库设计配置',
'description': '来自数据库设计节点的输出(可选,用于补充字段类型信息)',
'x-component': 'SmartInput',
},
'auto_query_fields': {
'type': 'boolean',
'title': '自动生成查询字段',
'description': '根据表单字段自动生成查询条件',
'default': True,
'x-component': 'Switch',
},
'auto_columns': {
'type': 'boolean',
'title': '自动生成列配置',
'description': '根据表单字段自动生成列表列',
'default': True,
'x-component': 'Switch',
},
'container_type': {
'type': 'string',
'title': '容器类型',
'description': '表单编辑的容器类型',
'default': 'drawer',
'enum': ['drawer', 'dialog', 'page'],
'enumNames': ['抽屉', '弹窗', '页面'],
'x-component': 'Select',
},
'page_size': {
'type': 'number',
'title': '每页条数',
'description': '列表每页显示的数据条数',
'default': 20,
'enum': [10, 20, 50, 100],
'x-component': 'Select',
},
'show_index': {
'type': 'boolean',
'title': '显示序号列',
'description': '是否显示行序号',
'default': True,
'x-component': 'Switch',
},
'show_selection': {
'type': 'boolean',
'title': '显示选择列',
'description': '是否显示多选框列',
'default': True,
'x-component': 'Switch',
},
'enable_export': {
'type': 'boolean',
'title': '启用导出',
'description': '是否显示导出按钮',
'default': True,
'x-component': 'Switch',
},
'enable_import': {
'type': 'boolean',
'title': '启用导入',
'description': '是否显示导入按钮',
'default': False,
'x-component': 'Switch',
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '设计完成后是否暂停等待用户确认或编辑',
'default': True,
'x-component': 'Switch',
},
},
'required': ['form_ui_design'],
}
@@ -0,0 +1,313 @@
"""
表单发布节点
将已创建的表单发布到菜单系统
"""
import logging
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormPublishNode(BaseNode):
"""
表单发布节点
将已创建的表单发布到菜单系统,生成菜单和权限
"""
node_type = 'form_publish'
node_name = '表单发布'
node_category = 'form'
node_icon = 'send'
node_description = '将已创建的表单发布到菜单系统'
inputs = [
{
'name': 'form_id',
'type': 'string',
'description': '表单ID(来自表单创建节点)',
'required': True,
},
{
'name': 'form_code',
'type': 'string',
'description': '表单编码(可选,用于生成路由)',
'required': False,
},
]
outputs = [
{
'name': 'menu_id',
'type': 'string',
'description': '创建的菜单ID',
},
{
'name': 'route_path',
'type': 'string',
'description': '表单访问路径',
},
{
'name': 'publish_result',
'type': 'object',
'description': '发布结果详情',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点(同步方法,通过运行异步方法实现)"""
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, self.execute_async(context))
return future.result()
else:
return loop.run_until_complete(self.execute_async(context))
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 获取输入参数
form_id = context.resolve_template(self.config.get('form_id', ''))
form_code = context.resolve_template(self.config.get('form_code', ''))
require_confirmation = self.resolve_require_confirmation(context, default=True)
logger.info(f'FormPublishNode - 开始发布表单: form_id={form_id}')
# 验证必要参数
if not form_id:
return NodeResult(
success=False,
error='表单ID不能为空',
)
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
publish_config = self._parse_user_input(user_input)
if publish_config:
logger.info(f'FormPublishNode - 使用用户编辑的数据: {publish_config}')
# 清除用户输入,防止重复处理
context.variables.pop('__user_input__', None)
return await self._do_publish(context, form_id, publish_config)
# 如果需要确认,返回等待用户输入
if require_confirmation:
return await self._wait_for_confirmation(form_id)
# 不需要确认,直接发布(使用默认配置)
return await self._do_publish(context, form_id, {})
except Exception as e:
logger.exception(f'表单发布节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
def _parse_user_input(self, user_input: Any) -> Optional[Dict]:
"""解析用户输入"""
import json
if isinstance(user_input, dict):
# 检查是否是 publishData 格式
if 'publishData' in user_input:
return user_input.get('publishData', {})
return user_input
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict):
if 'publishData' in data:
return data.get('publishData', {})
return data
except json.JSONDecodeError:
pass
return None
async def _wait_for_confirmation(self, form_id: str) -> NodeResult:
"""等待用户确认"""
from app.database import AsyncSessionLocal
from online_dev.form_manager.service import FormService
async with AsyncSessionLocal() as db:
form = await FormService.get(db, form_id)
if not form:
return NodeResult(
success=False,
error=f'表单不存在: {form_id}',
)
# 构建预览数据
preview_data = {
'type': 'form_publish',
'data': {
'publishData': {
'menu_name': form.name,
'menu_parent_id': None,
'menu_icon': 'lucide:file-text',
'menu_order': 1,
},
},
}
# 返回等待用户输入的结果
return NodeResult(
success=True,
waiting_for_input=True,
waiting_config={
'type': 'design_preview',
'preview_type': 'form_publish',
'title': '发布表单',
'message': '请确认或编辑发布配置',
'data': {
'menu_name': form.name,
'menu_parent_id': None,
'menu_icon': 'lucide:file-text',
'menu_order': 1,
},
},
preview=preview_data,
)
async def _do_publish(self, context: NodeContext, form_id: str, publish_config: Dict) -> NodeResult:
"""执行发布"""
from app.database import AsyncSessionLocal
from online_dev.form_manager.service import FormService, FormServiceException
async with AsyncSessionLocal() as db:
try:
# 获取表单
form = await FormService.get(db, form_id)
if not form:
return NodeResult(
success=False,
error=f'表单不存在: {form_id}',
)
# 构建发布配置
menu_name = publish_config.get('menu_name', form.name)
menu_parent_id = publish_config.get('menu_parent_id')
menu_icon = publish_config.get('menu_icon', 'lucide:file-text')
menu_order = publish_config.get('menu_order', 1)
publish_data = {
'menu_name': menu_name,
'menu_parent_id': menu_parent_id if menu_parent_id else None,
'menu_icon': menu_icon,
'menu_order': menu_order,
'allow_add': True,
'allow_edit': True,
'allow_delete': True,
'allow_export': True,
'allow_import': False,
}
# 构建路由路径
route_path = f'/form-render/{form.code}'
# 检查表单是否已发布
if form.status == 'published':
# 表单已发布,更新菜单配置
logger.info(f'FormPublishNode - 表单已发布,更新菜单配置: form_id={form_id}')
await self._update_menu_config(db, form, publish_data)
else:
# 发布表单
form = await FormService.publish(db, form_id, publish_data)
# 构建输出
publish_result = {
'form_id': form.id,
'form_code': form.code,
'form_name': form.name,
'status': form.status,
'menu_name': menu_name,
'menu_icon': menu_icon,
'route_path': route_path,
}
logger.info(f'FormPublishNode - 表单发布成功: form_id={form_id}, route_path={route_path}')
return NodeResult(
success=True,
output=publish_result,
output_variables={
'menu_id': form.menu_id if hasattr(form, 'menu_id') else '',
'route_path': route_path,
'publish_result': publish_result,
},
)
except FormServiceException as e:
logger.error(f'表单服务异常: {e}')
return NodeResult(
success=False,
error=f'表单发布失败: {str(e)}',
)
except Exception as e:
logger.exception(f'表单发布异常: {e}')
return NodeResult(
success=False,
error=f'表单发布异常: {str(e)}',
)
async def _update_menu_config(self, db, form, publish_data: Dict):
"""更新已发布表单的菜单配置"""
from sqlalchemy import select
from core.menu.model import Menu
# 查找表单对应的菜单
menu_stmt = select(Menu).where(
Menu.path == f"/form-render/{form.code}"
)
menu_result = await db.execute(menu_stmt)
existing_menu = menu_result.scalar_one_or_none()
if existing_menu:
# 更新菜单配置
existing_menu.name = publish_data.get('menu_name', form.name)
existing_menu.title = publish_data.get('menu_name', form.name)
existing_menu.parent_id = publish_data.get('menu_parent_id')
existing_menu.icon = publish_data.get('menu_icon', 'lucide:file-text')
existing_menu.order = publish_data.get('menu_order', 0)
await db.commit()
logger.info(f'更新表单菜单配置: {form.code}')
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取节点配置 Schema"""
return {
'type': 'object',
'properties': {
'form_id': {
'type': 'string',
'title': '表单ID',
'description': '来自表单创建节点的输出',
'x-component': 'SmartInput',
},
'form_code': {
'type': 'string',
'title': '表单编码',
'description': '用于生成访问路由(可选)',
'x-component': 'SmartInput',
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '发布前是否需要用户确认配置',
'default': True,
'x-component': 'Switch',
},
},
'required': ['form_id'],
}
@@ -0,0 +1,847 @@
"""
表单UI设计节点
根据数据库表结构自动生成表单UI配置
"""
import json
import logging
import re
import uuid
from typing import Any, Dict, List, Optional, Tuple
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
from ..utils.config_utils import resolve_object_config
logger = logging.getLogger(__name__)
@NodeRegistry.register
class FormUIDesignNode(BaseNode):
"""
表单UI设计节点
根据数据库表结构自动生成表单UI配置,包括:
- 字段到组件的映射
- 验证规则推断
- 组件属性配置
- 字段分组
- 布局配置
"""
node_type = 'form_ui_design'
node_name = '表单UI设计'
node_category = 'form'
node_icon = 'layout'
node_description = '根据数据库表结构自动生成表单UI配置'
# ========== 字段类型到组件的映射规则 ==========
FIELD_TYPE_MAPPING = {
# 字符串类型
'varchar': {
'default': 'input',
'long_text_threshold': 200, # 超过此长度使用 textarea
},
'char': {'default': 'input'},
'text': {'default': 'textarea'},
# 数字类型
'int': {'default': 'input-number'},
'integer': {'default': 'input-number'},
'bigint': {'default': 'input-number'},
'smallint': {'default': 'input-number'},
# 小数类型
'decimal': {'default': 'input-number'},
'numeric': {'default': 'input-number'},
'float': {'default': 'input-number'},
'double': {'default': 'input-number'},
# 日期时间类型
'datetime': {'default': 'date', 'props': {'type': 'datetime', 'format': 'YYYY-MM-DD HH:mm:ss'}},
'timestamp': {'default': 'date', 'props': {'type': 'datetime', 'format': 'YYYY-MM-DD HH:mm:ss'}},
'date': {'default': 'date', 'props': {'type': 'date', 'format': 'YYYY-MM-DD'}},
'time': {'default': 'time', 'props': {'format': 'HH:mm:ss'}},
# 布尔类型
'boolean': {'default': 'switch'},
'bool': {'default': 'switch'},
# JSON类型
'json': {'default': 'textarea', 'props': {'rows': 6}},
'jsonb': {'default': 'textarea', 'props': {'rows': 6}},
}
# ========== 字段名模式匹配规则 ==========
FIELD_NAME_PATTERNS = [
# 邮箱
(r'(email|mail)', 'input', {'type': 'email'}),
# 手机号
(r'(phone|mobile|tel)', 'input', {'type': 'tel'}),
# URL
(r'(url|link|website)', 'input', {'type': 'url'}),
# 密码
(r'(password|pwd)', 'input', {'type': 'password', 'show-password': True}),
# 颜色
(r'(color|colour)$', 'color', {}),
# 编码生成器
(r'(code|no|number)$', 'code-generator', {}),
# 长文本
(r'(remark|note|memo)', 'textarea', {'rows': 4}),
(r'(desc|description)', 'textarea', {'rows': 4}),
(r'(content)', 'textarea', {'rows': 6}),
(r'(address|addr)', 'textarea', {'rows': 3}),
# 富文本
(r'(html|rich_text|richtext)', 'rich-text', {}),
# 用户选择器
(r'(user|creator|modifier|operator)_id$', 'user-selector', {}),
# 部门选择器
(r'(dept|department)_id$', 'dept-selector', {}),
# 角色选择器
(r'(role)_id$', 'role-selector', {}),
# 岗位选择器
(r'(post|position)_id$', 'post-selector', {}),
# 通用外键 - 下拉选择
(r'_id$', 'select', {'filterable': True}),
# 评分
(r'(rate|rating|score)$', 'rate', {'max': 5}),
# 百分比
(r'(percent|percentage)$', 'slider', {'min': 0, 'max': 100}),
# 排序
(r'(sort|order|seq)$', 'input-number', {'min': 0, 'step': 1}),
# 年龄
(r'(age)$', 'input-number', {'min': 0, 'max': 150}),
# 金额
(r'(amount|price|money|fee|cost|salary|wage|budget)', 'money-input', {}),
# 图片
(r'(image|photo|picture|avatar|logo)', 'image-selector', {}),
# 文件
(r'(file|attachment|document)', 'file-selector', {}),
# Cron表达式
(r'(cron)', 'cron-selector', {}),
]
# ========== 字段分组规则 ==========
FIELD_GROUP_RULES = {
'basic_info': {
'label': '基本信息',
'patterns': [r'^(name|title|code|no|type|category|status)'],
'priority': 1,
'collapsed': False,
},
'detail_info': {
'label': '详细信息',
'patterns': [r'(desc|description|remark|note|content|detail)'],
'priority': 2,
'collapsed': False,
},
'contact_info': {
'label': '联系方式',
'patterns': [r'(phone|mobile|tel|email|address|contact|wechat|qq)'],
'priority': 3,
'collapsed': False,
},
'financial_info': {
'label': '金额信息',
'patterns': [r'(amount|price|money|fee|cost|salary|wage|budget)'],
'priority': 4,
'collapsed': False,
},
'time_info': {
'label': '时间信息',
'patterns': [r'(date|time|datetime|start|end|begin|finish|deadline)'],
'priority': 5,
'collapsed': False,
},
'attachment_info': {
'label': '附件信息',
'patterns': [r'(file|image|photo|picture|attachment|document)'],
'priority': 6,
'collapsed': False,
},
'system_info': {
'label': '系统信息',
'patterns': [r'^sys_', r'^is_', r'(creator|modifier|create_time|update_time)'],
'priority': 99,
'collapsed': True,
},
}
# 系统字段(默认隐藏)
SYSTEM_FIELDS = [
'id', 'sys_create_datetime', 'sys_update_datetime',
'sys_creator_id', 'sys_modifier_id', 'sys_dept_id',
'is_deleted', 'sort'
]
inputs = [
{
'name': 'database_design',
'type': 'object',
'description': '数据库设计配置(来自数据库设计节点)',
'required': True,
},
{
'name': 'form_name',
'type': 'string',
'description': '表单名称',
'required': False,
},
{
'name': 'form_code',
'type': 'string',
'description': '表单编码',
'required': False,
},
]
outputs = [
{
'name': 'form_ui_design',
'type': 'object',
'description': '表单UI设计配置',
},
{
'name': 'form_code',
'type': 'string',
'description': '表单编码',
},
{
'name': 'field_count',
'type': 'number',
'description': '字段数量',
},
{
'name': 'group_count',
'type': 'number',
'description': '分组数量',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户编辑的数据(从设计预览面板提交)
user_input = context.variables.get('__user_input__')
if user_input:
edited_data = self._parse_user_edit(user_input)
if edited_data:
logger.info(f'FormUIDesignNode - 使用用户编辑的数据')
context.variables.pop('__user_input__', None)
return self._build_result_from_edit(edited_data)
# 获取配置
database_design = resolve_object_config(
context, self.config.get('database_design', '{}')
)
form_name = context.resolve_template(self.config.get('form_name', ''))
form_code = context.resolve_template(self.config.get('form_code', ''))
# 布局配置
layout_mode = self.config.get('layout_mode', 'auto')
label_width = self.config.get('label_width', 120)
enable_grouping = self.config.get('enable_grouping', True)
hide_system_fields = self.config.get('hide_system_fields', True)
logger.info(f'FormUIDesignNode - 开始生成表单UI配置')
# 解析数据库设计
if not database_design:
return NodeResult(
success=False,
error='数据库设计配置解析失败或为空',
)
# 获取表信息
table_info = database_design.get('table', {})
table_name = table_info.get('tableName', '')
fields = table_info.get('fields', [])
if not table_name:
return NodeResult(
success=False,
error='表名不能为空',
)
if not fields:
return NodeResult(
success=False,
error='字段列表不能为空',
)
# 使用表名作为默认表单编码
if not form_code:
form_code = table_name
if not form_name:
form_name = table_info.get('alias', table_name)
# 生成表单项
form_items = []
for field in fields:
field_name = field.get('name', '')
# 跳过系统字段
if hide_system_fields and field_name in self.SYSTEM_FIELDS:
continue
# 生成表单项配置
form_item = self._generate_form_item(field)
if form_item:
form_items.append(form_item)
# 计算布局 - 默认使用双列布局
col_span = self._calculate_col_span(len(form_items), layout_mode)
for item in form_items:
if 'colSpan' not in item or item.get('colSpan') == 12:
# 只更新默认值,保留特殊组件的全宽设置
if item.get('type') not in ['textarea', 'rich-text', 'sub-table']:
item['colSpan'] = col_span
# 将所有字段包裹在栅格布局容器中(默认两列)
grid_layout_item = {
'id': str(uuid.uuid4()).replace('-', ''),
'type': 'grid-layout',
'label': '',
'props': {
'columns': 2, # 默认两列
'gap': 16,
},
'children': form_items,
'colSpan': 24, # 栅格容器占满整行
}
# 用栅格容器替换原有的扁平列表
wrapped_items = [grid_layout_item]
# 分组处理
if enable_grouping:
grouped_items = self._group_fields(form_items)
else:
grouped_items = [{
'title': '',
'collapsed': False,
'fields': form_items,
}]
# 构建表单UI配置
form_ui_design = {
'formCode': form_code,
'formName': form_name,
'tableName': table_name,
'layout': {
'mode': layout_mode,
'labelWidth': label_width,
'labelPosition': 'right',
'size': 'default',
'colSpan': col_span,
},
'groups': grouped_items,
'items': wrapped_items, # 使用包裹了栅格布局的表单项
'tableConfigs': [], # 表单设计器需要的表配置
'dbConfig': database_design.get('dbConfig', 'default'),
}
field_count = len(form_items)
group_count = len([g for g in grouped_items if g.get('fields')])
logger.info(f'FormUIDesignNode - 表单UI配置生成完成: {form_code}, 字段数: {field_count}, 分组数: {group_count}')
# 构建 table_configs(供前端表单设计组件使用)
table_configs = [{
'id': 'main-table',
'type': 'main',
'tableName': table_name,
'alias': table_info.get('alias', table_name),
'fields': fields,
'meta': {
'schema': table_info.get('schema', 'public'),
'database': table_info.get('database', ''),
},
}]
# 检查是否需要确认(支持布尔值、字符串模式和变量引用)
require_confirmation = self.resolve_require_confirmation(context, default=True)
# 构建预览数据(包含 table_configs 供前端表单设计组件使用)
preview_data = {
'type': 'form_ui_design',
'title': '表单UI设计',
'data': form_ui_design,
'table_configs': table_configs,
'editable': True,
} if require_confirmation else None
return NodeResult(
success=True,
output=form_ui_design,
output_variables={
'form_ui_design': form_ui_design,
'form_code': form_code,
'field_count': field_count,
'group_count': group_count,
'table_configs': table_configs,
},
preview=preview_data,
waiting_for_input=require_confirmation,
waiting_config={
'type': 'design_preview',
'preview_type': 'form_ui_design',
'title': '表单UI设计',
'message': '请确认或编辑表单UI设计',
'data': form_ui_design,
'table_configs': table_configs,
} if require_confirmation else {},
)
except Exception as e:
logger.exception(f'表单UI设计节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点(直接调用同步方法)"""
return self.execute(context)
def _parse_user_edit(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户编辑的数据"""
if isinstance(user_input, dict):
if 'items' in user_input or 'groups' in user_input:
return user_input
return None
if isinstance(user_input, str):
try:
data = json.loads(user_input)
if isinstance(data, dict) and ('items' in data or 'groups' in data):
return data
except (json.JSONDecodeError, TypeError):
pass
return None
def _build_result_from_edit(self, edited_data: Dict[str, Any]) -> NodeResult:
"""从用户编辑的数据构建节点结果"""
form_code = edited_data.get('formCode', '')
items = edited_data.get('items', [])
groups = edited_data.get('groups', [])
return NodeResult(
success=True,
output=edited_data,
output_variables={
'form_ui_design': edited_data,
'form_code': form_code,
'field_count': len(items),
'group_count': len([g for g in groups if g.get('fields')]),
},
)
def _parse_database_design(self, design_str: Any) -> Optional[Dict]:
"""解析数据库设计配置"""
if isinstance(design_str, dict):
return design_str
if isinstance(design_str, str):
design_str = design_str.strip()
if not design_str or design_str == '{}':
return None
try:
return json.loads(design_str)
except json.JSONDecodeError:
import ast
try:
return ast.literal_eval(design_str)
except (ValueError, SyntaxError):
return None
return None
def _generate_form_item(self, field: Dict) -> Optional[Dict]:
"""生成单个表单项配置"""
field_name = field.get('name', '')
field_type = field.get('type', 'varchar').lower()
comment = field.get('comment', field_name)
nullable = field.get('nullable', True)
max_length = field.get('maxLength')
if not field_name:
return None
# 确定组件类型和属性
component, props = self._infer_component(field_name, field_type, field)
# 生成验证规则
rules = self._infer_validation_rules(field, component)
# 生成布局配置
layout = self._infer_layout(field_name, component)
# 构建表单项
form_item = {
'id': str(uuid.uuid4()).replace('-', ''),
'type': component,
'field': field_name,
'label': comment,
'props': props,
'rules': rules,
'colSpan': layout.get('colSpan', 12),
'order': layout.get('order', 0),
}
# 添加选项配置(如果需要)
if component in ['select', 'radio', 'checkbox']:
form_item['options'] = []
form_item['dataSource'] = self._infer_data_source(field_name)
return form_item
def _infer_component(self, field_name: str, field_type: str, field: Dict) -> Tuple[str, Dict]:
"""推断组件类型和属性"""
props = {}
component = 'input' # 默认组件
# 1. 先根据字段名模式匹配
field_name_lower = field_name.lower()
for pattern, comp, pattern_props in self.FIELD_NAME_PATTERNS:
if re.search(pattern, field_name_lower):
component = comp
props.update(pattern_props)
break
else:
# 2. 如果没有匹配到模式,根据字段类型映射
type_config = self.FIELD_TYPE_MAPPING.get(field_type, {'default': 'input'})
component = type_config.get('default', 'input')
# 添加类型默认属性
if 'props' in type_config:
props.update(type_config['props'])
# 特殊处理:varchar 长度判断
if field_type == 'varchar':
max_length = field.get('maxLength', 255)
# 确保 max_length 是整数
try:
max_length = int(max_length) if max_length else 255
except (ValueError, TypeError):
max_length = 255
threshold = type_config.get('long_text_threshold', 200)
if max_length > threshold:
component = 'textarea'
props['rows'] = 4
# 3. 添加通用属性
props = self._add_common_props(props, field, component)
return component, props
def _add_common_props(self, props: Dict, field: Dict, component: str) -> Dict:
"""添加通用组件属性"""
field_name = field.get('name', '')
comment = field.get('comment', field_name)
max_length = field.get('maxLength')
# 确保 max_length 是整数
if max_length:
try:
max_length = int(max_length)
except (ValueError, TypeError):
max_length = None
# 占位符
if component in ['input', 'textarea']:
props.setdefault('placeholder', f'请输入{comment}')
elif component in ['select', 'cascader', 'tree-select', 'user-selector', 'dept-selector', 'role-selector', 'post-selector']:
props.setdefault('placeholder', f'请选择{comment}')
elif component == 'date':
props.setdefault('placeholder', f'请选择{comment}')
# 可清空
props.setdefault('clearable', True)
# 字符串长度限制
if max_length and component in ['input', 'textarea']:
props['maxlength'] = max_length
props['show-word-limit'] = True
# textarea 行数
if component == 'textarea':
props.setdefault('rows', 4)
# 数字输入框
if component == 'input-number':
props.setdefault('controls-position', 'right')
# 小数精度
if field.get('type') in ['decimal', 'numeric', 'float', 'double']:
scale = field.get('scale', 2)
props.setdefault('precision', scale)
props.setdefault('step', 10 ** (-scale))
else:
props.setdefault('step', 1)
# 下拉选择
if component == 'select':
props.setdefault('filterable', True)
# 开关
if component == 'switch':
props.setdefault('active-text', '')
props.setdefault('inactive-text', '')
return props
def _infer_validation_rules(self, field: Dict, component: str) -> List[Dict]:
"""推断验证规则"""
rules = []
field_name = field.get('name', '').lower()
field_type = field.get('type', '')
nullable = field.get('nullable', True)
max_length = field.get('maxLength')
# 确保 max_length 是整数
if max_length:
try:
max_length = int(max_length)
except (ValueError, TypeError):
max_length = None
comment = field.get('comment', field_name)
# 必填规则
if not nullable:
rules.append({
'required': True,
'message': f'{comment}不能为空',
'trigger': 'blur' if component in ['input', 'textarea'] else 'change',
})
# 长度规则
if max_length and field_type in ['varchar', 'char']:
rules.append({
'max': max_length,
'message': f'最多{max_length}个字符',
'trigger': 'blur',
})
# 格式规则
if 'email' in field_name or 'mail' in field_name:
rules.append({
'type': 'email',
'message': '请输入正确的邮箱格式',
'trigger': 'blur',
})
if 'phone' in field_name or 'mobile' in field_name:
rules.append({
'pattern': r'^1[3-9]\d{9}$',
'message': '请输入正确的手机号码',
'trigger': 'blur',
})
if 'url' in field_name or 'link' in field_name:
rules.append({
'type': 'url',
'message': '请输入正确的URL格式',
'trigger': 'blur',
})
return rules
def _infer_layout(self, field_name: str, component: str) -> Dict:
"""推断布局配置"""
layout = {
'colSpan': 12, # 默认占一半宽度
'order': 0,
}
field_name_lower = field_name.lower()
# 全宽组件
if component in ['textarea', 'rich-text', 'sub-table', 'divider']:
layout['colSpan'] = 24
# 小组件
elif component in ['switch', 'rate']:
layout['colSpan'] = 8
# 根据字段名调整
if any(kw in field_name_lower for kw in ['remark', 'note', 'description', 'content', 'address']):
layout['colSpan'] = 24
return layout
def _calculate_col_span(self, field_count: int, layout_mode: str) -> int:
"""
根据字段数量计算列宽
规则:
- 字段数 <= 6: 单列布局 (colSpan=24)
- 字段数 7-12: 双列布局 (colSpan=12)
- 字段数 > 12: 三列布局 (colSpan=8)
- 最多三列
"""
if layout_mode == 'single':
return 24
elif layout_mode == 'double':
return 12
elif layout_mode == 'triple':
return 8
else: # auto
if field_count <= 6:
return 24 # 单列
elif field_count <= 12:
return 12 # 双列
else:
return 8 # 三列(最多)
def _infer_data_source(self, field_name: str) -> Optional[Dict]:
"""推断数据来源配置"""
field_name_lower = field_name.lower()
# 外键字段
if field_name_lower.endswith('_id'):
table_name = field_name_lower[:-3]
# 特殊业务字段
if 'user' in table_name:
return {
'type': 'api',
'apiUrl': '/api/v1/system/users',
'labelField': 'name',
'valueField': 'id',
}
elif 'dept' in table_name or 'department' in table_name:
return {
'type': 'api',
'apiUrl': '/api/v1/system/departments',
'labelField': 'name',
'valueField': 'id',
}
elif 'role' in table_name:
return {
'type': 'api',
'apiUrl': '/api/v1/system/roles',
'labelField': 'name',
'valueField': 'id',
}
# 枚举类型字段
if 'status' in field_name_lower or 'type' in field_name_lower or 'category' in field_name_lower:
return {
'type': 'dict',
'dictCode': f'{field_name_lower}_dict',
}
return None
def _group_fields(self, form_items: List[Dict]) -> List[Dict]:
"""将表单项分组"""
groups = {}
ungrouped = []
for item in form_items:
field_name = item.get('field', '').lower()
group_key = self._match_group(field_name)
if group_key:
if group_key not in groups:
group_config = self.FIELD_GROUP_RULES.get(group_key, {})
groups[group_key] = {
'key': group_key,
'title': group_config.get('label', group_key),
'priority': group_config.get('priority', 50),
'collapsed': group_config.get('collapsed', False),
'fields': [],
}
groups[group_key]['fields'].append(item)
else:
ungrouped.append(item)
# 将未分组的字段放入"其他信息"组
if ungrouped:
groups['other_info'] = {
'key': 'other_info',
'title': '其他信息',
'priority': 50,
'collapsed': False,
'fields': ungrouped,
}
# 按优先级排序
sorted_groups = sorted(groups.values(), key=lambda g: g.get('priority', 50))
# 移除空组
return [g for g in sorted_groups if g.get('fields')]
def _match_group(self, field_name: str) -> Optional[str]:
"""匹配字段所属分组"""
for group_key, group_config in self.FIELD_GROUP_RULES.items():
patterns = group_config.get('patterns', [])
for pattern in patterns:
if re.search(pattern, field_name):
return group_key
return None
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取节点配置 Schema"""
return {
'type': 'object',
'properties': {
'database_design': {
'type': 'string',
'title': '数据库设计配置',
'description': '来自数据库设计节点的输出',
'x-component': 'SmartInput',
},
'form_name': {
'type': 'string',
'title': '表单名称',
'description': '表单显示名称,不填则使用表别名',
'x-component': 'SmartInput',
},
'form_code': {
'type': 'string',
'title': '表单编码',
'description': '表单唯一编码,不填则使用表名',
'x-component': 'SmartInput',
},
'layout_mode': {
'type': 'string',
'title': '布局模式',
'description': '表单布局模式',
'default': 'auto',
'enum': ['auto', 'single', 'double', 'triple'],
'enumNames': ['自动', '单列', '双列', '三列'],
'x-component': 'Select',
},
'label_width': {
'type': 'number',
'title': '标签宽度',
'description': '表单标签宽度(像素)',
'default': 120,
'x-component': 'InputNumber',
},
'enable_grouping': {
'type': 'boolean',
'title': '启用分组',
'description': '是否按字段类型自动分组',
'default': True,
'x-component': 'Switch',
},
'hide_system_fields': {
'type': 'boolean',
'title': '隐藏系统字段',
'description': '是否隐藏系统字段(id, sys_*等)',
'default': True,
'x-component': 'Switch',
},
'require_confirmation': {
'type': 'boolean',
'title': '需要确认',
'description': '设计完成后是否暂停等待用户确认或编辑',
'default': True,
'x-component': 'Switch',
},
},
'required': ['database_design'],
}
@@ -0,0 +1,501 @@
"""
系统总结节点
收集和展示AI创建的完整系统信息,包括应用、表单模块和仪表盘
"""
import ast
import json
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class SystemSummaryNode(BaseNode):
"""
系统总结节点
收集和展示AI创建的完整系统信息,支持以下场景:
1. 完整应用创建:应用 + 多个表单模块 + 仪表盘
2. 子应用内创建模块:仅多个表单模块
3. 单个表单创建:单个表单模块
"""
node_type = 'system_summary'
node_name = '系统总结'
node_category = 'application'
node_icon = 'clipboard-check'
node_description = '收集和展示AI创建的完整系统信息'
inputs = [
{
'name': 'app_info',
'type': 'object',
'description': '应用信息(来自AppCreate节点,可选)',
'required': False,
},
{
'name': 'form_results',
'type': 'array',
'description': '表单结果列表(来自循环节点或单个表单发布节点)',
'required': True,
},
{
'name': 'dashboard_info',
'type': 'object',
'description': '仪表盘信息(来自DashboardPublish节点,可选)',
'required': False,
},
{
'name': 'summary_title',
'type': 'string',
'description': '总结标题',
'required': False,
},
]
outputs = [
{
'name': 'summary',
'type': 'object',
'description': '完整的总结数据',
},
{
'name': 'total_forms',
'type': 'number',
'description': '表单总数',
},
{
'name': 'has_app',
'type': 'boolean',
'description': '是否包含应用',
},
{
'name': 'has_dashboard',
'type': 'boolean',
'description': '是否包含仪表盘',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行节点"""
try:
# 检查是否有用户确认(点击完成按钮)
user_input = context.variables.get('__user_input__')
if user_input:
# 用户已确认,直接返回成功
logger.info('SystemSummaryNode - 用户已确认,完成展示')
context.variables.pop('__user_input__', None)
# 从用户输入中获取之前构建的总结数据
summary_data = self._parse_user_input(user_input)
if summary_data:
return self._build_final_result(summary_data)
# 获取输入参数
app_info_raw = self.config.get('app_info', '')
form_results_raw = self.config.get('form_results', '')
dashboard_info_raw = self.config.get('dashboard_info', '')
summary_title = context.resolve_template(self.config.get('summary_title', ''))
show_statistics = self.config.get('show_statistics', True)
base_url = context.resolve_template(self.config.get('base_url', ''))
# 解析变量引用
app_info = self._resolve_variable_value(context, app_info_raw)
form_results = self._resolve_variable_value(context, form_results_raw)
dashboard_info = self._resolve_variable_value(context, dashboard_info_raw)
logger.info(f'SystemSummaryNode - 开始构建系统总结')
logger.info(f'SystemSummaryNode - app_info: {bool(app_info)}, form_results: {type(form_results)}, dashboard_info: {bool(dashboard_info)}')
# 处理表单结果
forms = self._process_form_results(form_results)
# 处理应用信息
app = self._process_app_info(app_info) if app_info else None
# 处理仪表盘信息
dashboard = self._process_dashboard_info(dashboard_info) if dashboard_info else None
# 构建总结数据
summary = self._build_summary(
title=summary_title or '系统创建完成',
app=app,
forms=forms,
dashboard=dashboard,
show_statistics=show_statistics,
base_url=base_url,
)
logger.info(f'SystemSummaryNode - 总结构建完成: {len(forms)} 个表单')
# 返回等待用户确认的结果
return NodeResult(
success=True,
output=summary,
output_variables={
'summary': summary,
'total_forms': len(forms),
'has_app': app is not None,
'has_dashboard': dashboard is not None,
},
waiting_for_input=True,
waiting_config={
'type': 'design_preview',
'preview_type': 'system_summary',
'title': summary.get('title', '系统创建完成'),
'data': summary,
'editable': False,
},
)
except Exception as e:
logger.exception(f'SystemSummaryNode - 执行失败: {e}')
return NodeResult(
success=False,
error=f'系统总结节点执行失败: {str(e)}',
)
def _parse_user_input(self, user_input: Any) -> Optional[Dict[str, Any]]:
"""解析用户输入"""
if isinstance(user_input, dict):
return user_input
if isinstance(user_input, str):
try:
return json.loads(user_input)
except (json.JSONDecodeError, TypeError):
pass
return None
def _resolve_variable_value(self, context: NodeContext, value: Any) -> Any:
"""解析变量引用,保持原始类型"""
import re
# 如果已经是字典或列表,直接返回
if isinstance(value, (dict, list)):
return value
# 如果不是字符串,返回原值
if not isinstance(value, str):
return value
value = value.strip()
if not value or value in ['{}', '[]', '']:
return None
# 检查是否是单个变量引用 {{node_id.variable_name}}
single_var_pattern = r'^\{\{([^}]+)\}\}$'
match = re.match(single_var_pattern, value)
if match:
var_ref = match.group(1).strip()
# 解析 node_id.variable_name 格式
if '.' in var_ref:
parts = var_ref.split('.', 1)
node_id = parts[0]
key = parts[1]
# 从节点输出命名空间获取
node_outputs = context.variables.get(f'_node_{node_id}')
if isinstance(node_outputs, dict) and key in node_outputs:
return node_outputs[key]
# 直接从变量中获取
if key in context.variables:
return context.variables[key]
logger.warning(f'变量引用未找到: {var_ref}')
else:
# 直接变量引用
if var_ref in context.variables:
return context.variables[var_ref]
logger.warning(f'变量引用未找到: {var_ref}')
# 不是变量引用,尝试解析为 JSON
try:
return json.loads(value)
except json.JSONDecodeError:
pass
# 尝试 Python literal_eval
try:
result = ast.literal_eval(value)
return result
except (ValueError, SyntaxError):
pass
return value
def _process_form_results(self, form_results: Any) -> List[Dict[str, Any]]:
"""处理表单结果列表"""
forms = []
if not form_results:
return forms
# 如果是单个表单结果(字典),包装成列表
if isinstance(form_results, dict):
form_results = [form_results]
if not isinstance(form_results, list):
logger.warning(f'form_results 类型不正确: {type(form_results)}')
return forms
for i, result in enumerate(form_results):
if not isinstance(result, dict):
continue
# 尝试从不同的数据结构中提取表单信息
form_info = self._extract_form_info(result, i)
if form_info:
forms.append(form_info)
return forms
def _extract_form_info(self, result: Dict[str, Any], index: int) -> Optional[Dict[str, Any]]:
"""从结果中提取表单信息"""
# 直接是表单发布节点的输出
if 'form_id' in result or 'menu_id' in result:
return {
'id': result.get('form_id', ''),
'name': result.get('form_name', result.get('menu_name', f'表单{index + 1}')),
'code': result.get('form_code', ''),
'description': result.get('description', ''),
'link': result.get('route_path', ''),
'menu_id': result.get('menu_id', ''),
'icon': result.get('menu_icon', 'lucide:file-text'),
}
# 循环节点的迭代结果(包含多个节点的输出)
# 尝试从 form_publish 或 form_create 节点输出中提取
for key in ['form_publish', 'form_create']:
if key in result and isinstance(result[key], dict):
sub_result = result[key]
return {
'id': sub_result.get('form_id', ''),
'name': sub_result.get('form_name', sub_result.get('menu_name', f'表单{index + 1}')),
'code': sub_result.get('form_code', ''),
'description': sub_result.get('description', ''),
'link': sub_result.get('route_path', ''),
'menu_id': sub_result.get('menu_id', ''),
'icon': sub_result.get('menu_icon', 'lucide:file-text'),
}
# 尝试从嵌套的 publish_result 中提取
if 'publish_result' in result and isinstance(result['publish_result'], dict):
pub = result['publish_result']
return {
'id': pub.get('form_id', result.get('form_id', '')),
'name': pub.get('form_name', pub.get('menu_name', f'表单{index + 1}')),
'code': pub.get('form_code', result.get('form_code', '')),
'description': pub.get('description', ''),
'link': pub.get('route_path', ''),
'menu_id': pub.get('menu_id', ''),
'icon': pub.get('menu_icon', 'lucide:file-text'),
}
# 尝试从 _node_xxx 格式的输出中提取
for key, value in result.items():
if key.startswith('_node_') and isinstance(value, dict):
if 'form_id' in value or 'route_path' in value:
return {
'id': value.get('form_id', ''),
'name': value.get('form_name', value.get('menu_name', f'表单{index + 1}')),
'code': value.get('form_code', ''),
'description': value.get('description', ''),
'link': value.get('route_path', ''),
'menu_id': value.get('menu_id', ''),
'icon': value.get('menu_icon', 'lucide:file-text'),
}
return None
def _process_app_info(self, app_info: Any) -> Optional[Dict[str, Any]]:
"""处理应用信息 - 只需要名称"""
if not app_info:
return None
# 如果是字符串,直接作为名称
if isinstance(app_info, str):
app_name = app_info.strip()
if not app_name:
return None
return {
'name': app_name,
'icon': 'lucide:app-window',
'link': '/', # 应用的基础URL
}
# 如果是字典,尝试提取名称
if isinstance(app_info, dict):
app_name = app_info.get('app_name') or app_info.get('name', '')
if not app_name:
return None
return {
'name': app_name,
'icon': app_info.get('icon', 'lucide:app-window'),
'link': '/',
}
return None
def _process_dashboard_info(self, dashboard_info: Any) -> Optional[Dict[str, Any]]:
"""处理仪表盘信息 - 只需要名称和路径"""
if not dashboard_info:
return None
# 如果是字符串,直接作为名称
if isinstance(dashboard_info, str):
dashboard_name = dashboard_info.strip()
if not dashboard_name:
return None
return {
'name': dashboard_name,
'icon': 'lucide:layout-dashboard',
'link': '', # 没有路径信息
}
# 如果是字典,提取名称和路径
if isinstance(dashboard_info, dict):
dashboard_name = (
dashboard_info.get('dashboard_name') or
dashboard_info.get('menu_name') or
dashboard_info.get('name', '')
)
if not dashboard_name:
return None
return {
'name': dashboard_name,
'icon': dashboard_info.get('menu_icon', 'lucide:layout-dashboard'),
'link': dashboard_info.get('route_path', ''),
}
return None
def _build_summary(
self,
title: str,
app: Optional[Dict[str, Any]],
forms: List[Dict[str, Any]],
dashboard: Optional[Dict[str, Any]],
show_statistics: bool,
base_url: str,
) -> Dict[str, Any]:
"""构建总结数据"""
# 计算统计信息
total_modules = len(forms)
if app:
total_modules += 1
if dashboard:
total_modules += 1
summary = {
'title': title,
'created_at': datetime.now().isoformat(),
'statistics': {
'total_modules': total_modules,
'forms_count': len(forms),
'has_app': app is not None,
'has_dashboard': dashboard is not None,
} if show_statistics else None,
'app': app,
'forms': forms,
'dashboard': dashboard,
'base_url': base_url,
}
return summary
def _build_final_result(self, summary_data: Dict[str, Any]) -> NodeResult:
"""构建最终结果(用户确认后)"""
forms = summary_data.get('forms', [])
app = summary_data.get('app')
dashboard = summary_data.get('dashboard')
return NodeResult(
success=True,
output=summary_data,
output_variables={
'summary': summary_data,
'total_forms': len(forms),
'has_app': app is not None,
'has_dashboard': dashboard is not None,
},
)
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取节点配置 Schema"""
return {
'type': 'object',
'properties': {
'app_info': {
'type': 'string',
'title': '应用信息',
'description': '应用信息(来自AppCreate节点),支持变量引用如 {{app_create.app_id}}',
'required': False,
'x-component': 'SmartInput',
'x-component-props': {
'placeholder': '留空则不显示应用信息',
},
},
'form_results': {
'type': 'string',
'title': '表单结果列表',
'description': '表单结果列表(来自循环节点或单个表单发布节点),支持变量引用如 {{loop.results}}',
'required': True,
'x-component': 'SmartInput',
'x-component-props': {
'placeholder': '{{loop.results}} 或 {{form_publish.publish_result}}',
},
},
'dashboard_info': {
'type': 'string',
'title': '仪表盘信息',
'description': '仪表盘信息(来自DashboardPublish节点),支持变量引用',
'required': False,
'x-component': 'SmartInput',
'x-component-props': {
'placeholder': '留空则不显示仪表盘信息',
},
},
'summary_title': {
'type': 'string',
'title': '总结标题',
'description': '总结页面的标题',
'default': '系统创建完成',
'x-component': 'Input',
'x-component-props': {
'placeholder': '系统创建完成',
},
},
'show_statistics': {
'type': 'boolean',
'title': '显示统计信息',
'description': '是否显示模块数量等统计信息',
'default': True,
'x-component': 'Switch',
},
'base_url': {
'type': 'string',
'title': '基础URL',
'description': '用于生成完整链接的基础URL(可选)',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '留空则使用相对路径',
},
},
},
'required': ['form_results'],
}
@@ -130,6 +130,23 @@ def _load_builtin_nodes():
'text_to_sql_node',
'snowflake_cortex_node',
'knowledge_retrieval_node',
'form_basic_info_node',
'form_database_design_node',
'form_database_create_node',
'form_ui_design_node',
'form_list_design_node',
'form_create_node',
'form_publish_node',
'form_data_node',
'app_create_node',
'app_design_node',
'app_settings_node',
'app_update_node',
'dashboard_basic_info_node',
'dashboard_design_node',
'dashboard_create_node',
'dashboard_publish_node',
'system_summary_node',
]
from importlib import import_module
+2
View File
@@ -7,6 +7,7 @@ from ai_platform.api.chat_api import router as chat_router
from ai_platform.api.workflow_api import router as workflow_router
from ai_platform.api.agent_api import router as agent_router
from ai_platform.api.speech_api import router as speech_router
from ai_platform.api.form_api import router as form_router
from ai_platform.knowledge.api import router as knowledge_router
# 创建总路由
@@ -20,4 +21,5 @@ router.include_router(chat_router)
router.include_router(workflow_router)
router.include_router(agent_router)
router.include_router(speech_router)
router.include_router(form_router)
router.include_router(knowledge_router)
@@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
表单数据管理模块
"""
@@ -0,0 +1,300 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""表单数据数据库执行适配层:平台库 vs 第三方连接"""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.form_data_manager.dynamic_sql_builder import DynamicSQLBuilder
from online_dev.form_data_manager.exceptions import (
FormDataConnectionError,
FormDataException,
QueryError,
)
from utils.sql_param_compile import compile_sql_with_named_params
logger = logging.getLogger(__name__)
PLATFORM_DB_TYPE = "postgresql"
PLATFORM_SCHEMA = "public"
class FormDataWriteForbidden(FormDataException):
"""系统连接不允许表单业务数据写入"""
error_code = "FORM_WRITE_FORBIDDEN"
http_status = 403
def __init__(self, db_config: str = ""):
super().__init__(
"系统数据库连接不允许写入表单业务数据,请使用第三方数据库连接",
context={"db_config": db_config},
)
class FormDataDbAdapter:
"""表单 CRUD SQL 执行抽象"""
db_type: str
db_config: str
is_system: bool
is_external: bool
async def execute_query(
self,
sql: str,
params: Optional[Dict[str, Any]] = None,
*,
database: Optional[str] = None,
) -> List[Dict[str, Any]]:
raise NotImplementedError
async def execute_command(
self,
sql: str,
params: Optional[Dict[str, Any]] = None,
*,
database: Optional[str] = None,
) -> int:
raise NotImplementedError
def ensure_write_allowed(self) -> None:
"""写操作前校验(系统连接禁止写业务表)"""
@asynccontextmanager
async def transaction(self, database: Optional[str] = None):
"""请求级事务;平台库由 API Session 提交,第三方由 handler 事务域。"""
yield
class PlatformSessionAdapter(FormDataDbAdapter):
"""平台 AsyncSessiondb_config=default"""
def __init__(
self,
platform_db: AsyncSession,
db_type: str,
db_config: str = "default",
default_database: str = "",
):
self._db = platform_db
self.db_type = db_type
self.db_config = db_config
self.default_database = (default_database or "").strip()
self.is_system = db_config == "default"
self.is_external = False
def ensure_write_allowed(self) -> None:
return
async def execute_query(
self,
sql: str,
params: Optional[Dict[str, Any]] = None,
*,
database: Optional[str] = None,
) -> List[Dict[str, Any]]:
result = await self._db.execute(text(sql), params or {})
rows = result.fetchall()
columns = result.keys()
return [dict(zip(columns, row)) for row in rows]
async def execute_command(
self,
sql: str,
params: Optional[Dict[str, Any]] = None,
*,
database: Optional[str] = None,
) -> int:
result = await self._db.execute(text(sql), params or {})
return result.rowcount
@asynccontextmanager
async def transaction(self, database: Optional[str] = None):
yield
class ExternalConnectionAdapter(FormDataDbAdapter):
"""第三方连接:经 ConnectionResolver + database_manager 执行"""
def __init__(
self,
platform_db: AsyncSession,
db_config: str,
db_type: str,
is_system: bool,
default_database: str = "",
):
self._platform_db = platform_db
self.db_config = db_config
self.db_type = db_type
self.default_database = (default_database or "").strip()
self.is_system = is_system
self.is_external = True
self._manager_service = None
def ensure_write_allowed(self) -> None:
if self.is_system:
raise FormDataWriteForbidden(self.db_config)
async def _get_manager_service(self):
if self._manager_service is None:
from core.database_manager.service import AsyncDatabaseManagerService
self._manager_service = await AsyncDatabaseManagerService.create(
self.db_config, self._platform_db
)
return self._manager_service
async def execute_query(
self,
sql: str,
params: Optional[Dict[str, Any]] = None,
*,
database: Optional[str] = None,
) -> List[Dict[str, Any]]:
compiled = compile_sql_with_named_params(sql, params or {}, self.db_type)
service = await self._get_manager_service()
result_data = await service.execute_sql(
compiled, is_query=True, database=database
)
if not result_data.get("success"):
msg = result_data.get("message", "查询失败")
logger.error(
"External form query failed [%s] db_type=%s: %s | SQL: %s",
self.db_config,
self.db_type,
msg,
compiled[:500] if len(compiled) > 500 else compiled,
)
raise QueryError(detail=msg)
return result_data.get("rows") or []
async def execute_command(
self,
sql: str,
params: Optional[Dict[str, Any]] = None,
*,
database: Optional[str] = None,
) -> int:
self.ensure_write_allowed()
compiled = compile_sql_with_named_params(sql, params or {}, self.db_type)
service = await self._get_manager_service()
result_data = await service.execute_sql(
compiled, is_query=False, database=database
)
if not result_data.get("success"):
msg = result_data.get("message", "执行失败")
logger.error(
"External form command failed [%s] db_type=%s: %s | SQL: %s",
self.db_config,
self.db_type,
msg,
compiled[:500] if len(compiled) > 500 else compiled,
)
raise QueryError(detail=msg)
affected = result_data.get("affected_rows")
return int(affected) if affected is not None else 0
@asynccontextmanager
async def transaction(self, database: Optional[str] = None):
service = await self._get_manager_service()
if not await service.begin_transaction(database=database):
raise QueryError(detail="无法开启数据库事务")
try:
yield
await service.commit_transaction()
except Exception:
await service.rollback_transaction()
raise
async def create_savepoint(self, name: str) -> None:
service = await self._get_manager_service()
await service.create_savepoint(name)
async def release_savepoint(self, name: str) -> None:
service = await self._get_manager_service()
await service.release_savepoint(name)
async def rollback_to_savepoint(self, name: str) -> None:
service = await self._get_manager_service()
await service.rollback_to_savepoint(name)
async def create_platform_adapter(platform_db: AsyncSession) -> PlatformSessionAdapter:
"""平台库适配器(固定 default,用于 core_* 元数据查询)。"""
from core.database_connection.resolver import ConnectionResolver
info = await ConnectionResolver.resolve("default", platform_db)
return PlatformSessionAdapter(
platform_db,
PLATFORM_DB_TYPE,
"default",
default_database=info.database or "",
)
async def create_form_data_adapter(
db_config: str,
platform_db: AsyncSession,
) -> FormDataDbAdapter:
"""按表单 db_config 创建业务库执行适配器"""
from core.database_connection.resolver import ConnectionResolver
code = (db_config or "default").strip() or "default"
try:
info = await ConnectionResolver.resolve(code, platform_db)
except ValueError as e:
raise FormDataConnectionError(detail=str(e)) from e
if code == "default":
return PlatformSessionAdapter(
platform_db,
info.db_type,
code,
default_database=info.database or "",
)
return ExternalConnectionAdapter(
platform_db,
code,
info.db_type,
info.is_system,
default_database=info.database or "",
)
def create_platform_sql_builder() -> DynamicSQLBuilder:
return DynamicSQLBuilder(PLATFORM_DB_TYPE)
def create_sql_builder_for_adapter(adapter: FormDataDbAdapter) -> DynamicSQLBuilder:
return DynamicSQLBuilder(
adapter.db_type,
default_database=getattr(adapter, "default_database", "") or "",
)
async def resolve_form_sql_context(
platform_db: AsyncSession,
form_meta,
*,
adapter_cache: Optional[Dict[str, FormDataDbAdapter]] = None,
builder_cache: Optional[Dict[str, DynamicSQLBuilder]] = None,
) -> Tuple[FormDataDbAdapter, DynamicSQLBuilder]:
"""按 FormMeta 解析业务 adapter 与 sql_builder(支持请求内缓存)。"""
code = (form_meta.db_config or "default").strip() or "default"
adapters = adapter_cache if adapter_cache is not None else {}
builders = builder_cache if builder_cache is not None else {}
if code not in adapters:
adapters[code] = await create_form_data_adapter(code, platform_db)
if code not in builders:
builders[code] = create_sql_builder_for_adapter(adapters[code])
return adapters[code], builders[code]
@@ -0,0 +1,551 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据库异常转换器
将 SQLAlchemy / DBAPI 原生异常转换为 FormData 业务异常。
同时提供 API 层的统一错误响应构建函数。
典型用法:
from .db_error_handler import translate_db_error, build_error_response
# Service 层 — 在 _execute_command / _execute_query 中
try:
await db.execute(...)
except Exception as e:
raise translate_db_error(e) from e
# API 层 — 在路由函数中
except FormDataException as e:
raise build_error_response(e)
"""
from __future__ import annotations
import logging
import re
from typing import Optional
from fastapi import HTTPException
from sqlalchemy.exc import (
DBAPIError,
IntegrityError,
OperationalError,
ProgrammingError,
DataError,
InternalError,
StatementError,
TimeoutError as SATimeoutError,
)
from online_dev.form_data_manager.exceptions import (
FormDataException,
FormDataValidationError,
UniqueConstraintError,
NotNullConstraintError,
CheckConstraintError,
ForeignKeyConstraintError,
DataTypeMismatchError,
DataTooLongError,
TableNotFoundError,
ColumnNotFoundError,
DatabaseConnectionError,
ConnectionPoolExhaustedError,
ConnectionTimeoutError,
DeadlockError,
LockTimeoutError,
QueryError,
InternalDatabaseError,
)
logger = logging.getLogger(__name__)
# =====================================================================
# 正则:提取数据库错误中的关键信息
# =====================================================================
_RE_COLUMN = re.compile(r'column\s+["\']?(\w+)["\']?', re.IGNORECASE)
_RE_TABLE = re.compile(r'(?:relation|table)\s+["\']?([\w."]+)["\']?', re.IGNORECASE)
_RE_CONSTRAINT = re.compile(r'constraint\s+["\']?(\w+)["\']?', re.IGNORECASE)
_RE_PG_DETAIL = re.compile(r'DETAIL:\s*(.*?)(?:\n|$)', re.IGNORECASE)
_RE_REFERENCED_FROM_TABLE = re.compile(
r'referenced from table\s+"?([\w.]+)"?',
re.IGNORECASE,
)
_RE_FK_ON_TABLE = re.compile(
r'foreign key constraint\s+"?(\w+)"?\s+on table\s+"?([\w.]+)"?',
re.IGNORECASE,
)
_RE_NOT_PRESENT_IN_TABLE = re.compile(
r'not present in table\s+"?([\w.]+)"?',
re.IGNORECASE,
)
def _extract_column(msg: str) -> str:
m = _RE_COLUMN.search(msg)
return m.group(1) if m else ""
def _extract_table(msg: str) -> str:
m = _RE_TABLE.search(msg)
return m.group(1) if m else ""
def _extract_constraint(msg: str) -> str:
m = _RE_CONSTRAINT.search(msg)
return m.group(1) if m else ""
def _normalize_table_name(table: str) -> str:
if not table:
return ""
return table.strip().strip('"').split(".")[-1]
def _extract_fk_referencing_table(msg: str) -> str:
detail_match = _RE_PG_DETAIL.search(msg)
detail = detail_match.group(1).strip() if detail_match else msg
referenced_match = _RE_REFERENCED_FROM_TABLE.search(detail)
if referenced_match:
return _normalize_table_name(referenced_match.group(1))
not_present_match = _RE_NOT_PRESENT_IN_TABLE.search(detail)
if not_present_match:
return _normalize_table_name(not_present_match.group(1))
fk_on_table_match = _RE_FK_ON_TABLE.search(msg)
if fk_on_table_match:
return _normalize_table_name(fk_on_table_match.group(2))
return ""
# =====================================================================
# 核心转换函数
# =====================================================================
def translate_db_error(exc: Exception) -> FormDataException:
"""
将数据库异常转换为业务异常。
优先使用 SQLAlchemy 异常类型进行判断,再辅以错误信息字符串匹配,
确保对 PostgreSQL 和 MySQL 两种方言都有良好的覆盖。
"""
msg = str(exc).lower()
orig_msg = str(exc)
# ------------------------------------------------------------------
# 1. IntegrityError — 约束违反
# SQLAlchemy 会将 asyncpg/MySQL 的约束错误包装为 IntegrityError
# 实际 DBAPI 异常在 exc.orig 中,类型判断必须用外层包装类。
# ------------------------------------------------------------------
if isinstance(exc, IntegrityError):
return _handle_integrity_error(exc, msg, orig_msg)
# ------------------------------------------------------------------
# 2. DataError — 数据格式/长度问题
# ------------------------------------------------------------------
if isinstance(exc, DataError):
return _handle_data_error(exc, msg, orig_msg)
# ------------------------------------------------------------------
# 3. OperationalError — 连接、死锁、超时等运行时问题
# ------------------------------------------------------------------
if isinstance(exc, OperationalError):
return _handle_operational_error(exc, msg, orig_msg)
# ------------------------------------------------------------------
# 4. ProgrammingError — SQL 语法、表/列不存在
# ------------------------------------------------------------------
if isinstance(exc, ProgrammingError):
return _handle_programming_error(exc, msg, orig_msg)
# ------------------------------------------------------------------
# 5. InternalError — 数据库内部错误(部分 MySQL 死锁也走这里)
# ------------------------------------------------------------------
if isinstance(exc, InternalError):
if "deadlock" in msg:
return DeadlockError()
return InternalDatabaseError(detail=orig_msg)
# ------------------------------------------------------------------
# 6. SQLAlchemy TimeoutError — 连接池超时
# ------------------------------------------------------------------
if isinstance(exc, SATimeoutError):
return ConnectionPoolExhaustedError()
# ------------------------------------------------------------------
# 7. MissingGreenlet(异步上下文错误)
# ------------------------------------------------------------------
if "missinggreenlet" in msg or "MissingGreenlet" in orig_msg:
return InternalDatabaseError(detail="async context error (MissingGreenlet)")
# ------------------------------------------------------------------
# 8. 其他 DBAPIError(含 PG 事务已中止)
# ------------------------------------------------------------------
if isinstance(exc, DBAPIError):
if _is_failed_transaction_error(msg):
return _handle_failed_transaction_error(orig_msg)
return InternalDatabaseError(detail=orig_msg)
# ------------------------------------------------------------------
# 9. 如果已经是业务异常,原样返回
# ------------------------------------------------------------------
if isinstance(exc, FormDataException):
return exc
# ------------------------------------------------------------------
# 10. 兜底 — 通过字符串匹配再尝试识别一轮
# ------------------------------------------------------------------
return _fallback_string_match(exc, msg, orig_msg)
# =====================================================================
# 分类处理器
# =====================================================================
def _handle_integrity_error(
exc: IntegrityError, msg: str, orig_msg: str
) -> FormDataException:
"""处理 IntegrityError 的各种子类型"""
# --- 唯一约束 ---
if any(kw in msg for kw in ("unique", "duplicate key", "duplicate entry", "uniqueviolation")):
fields = _extract_unique_fields(orig_msg)
constraint = _extract_constraint(orig_msg)
if fields:
labels = ", ".join(f"'{f}'" for f in fields)
return UniqueConstraintError(
f"以下字段的值已存在: {labels}",
fields=fields,
constraint_name=constraint,
)
return UniqueConstraintError(constraint_name=constraint)
# --- 非空约束 ---
if any(kw in msg for kw in ("not-null", "notnullviolation", "null value in column", "cannot be null", "doesn't have a default")):
col = _extract_column(orig_msg)
return NotNullConstraintError(column=col)
# --- 外键约束 ---
if any(
kw in msg
for kw in (
"foreign key",
"foreignkeyviolation",
"restrictviolation",
"a foreign key constraint fails",
"is still referenced",
"referenced from table",
"restrict setting of foreign key",
)
):
return _build_foreign_key_error(orig_msg)
# --- CHECK 约束 ---
if any(kw in msg for kw in ("check constraint", "checkviolation", "check_violation")):
constraint = _extract_constraint(orig_msg)
return CheckConstraintError(constraint_name=constraint)
# --- 无法识别的 IntegrityError ---
return FormDataValidationError(
"数据违反约束条件,请检查是否存在重复或无效数据",
)
def _handle_data_error(
exc: DataError, msg: str, orig_msg: str
) -> FormDataException:
"""处理 DataError"""
# 数据超长
if any(kw in msg for kw in ("too long", "value too long", "data too long", "string data, right truncation")):
col = _extract_column(orig_msg)
return DataTooLongError(column=col)
# 数据类型不匹配
if any(kw in msg for kw in (
"invalid input syntax", "invalid text representation", "invalid input for query argument",
"incorrect", "out of range", "numeric value", "expected str", "expected int",
)):
return DataTypeMismatchError(detail=orig_msg)
return DataTypeMismatchError(detail=orig_msg)
def _handle_operational_error(
exc: OperationalError, msg: str, orig_msg: str
) -> FormDataException:
"""处理 OperationalError"""
# 死锁
if any(kw in msg for kw in ("deadlock", "deadlock detected")):
return DeadlockError()
# 锁等待超时
if any(kw in msg for kw in ("lock wait timeout", "lock timeout", "could not obtain lock")):
return LockTimeoutError()
# 连接相关
if any(kw in msg for kw in (
"connection refused", "connection reset",
"connection timed out", "connection is closed",
"server closed the connection", "broken pipe",
"can't connect", "unable to connect",
"lost connection", "gone away",
)):
return DatabaseConnectionError(detail=orig_msg)
# 连接池超时
if "queuepool" in msg and ("timeout" in msg or "limit" in msg):
return ConnectionPoolExhaustedError()
# 查询超时 (statement_timeout / max_execution_time)
if any(kw in msg for kw in ("statement timeout", "query timeout", "max_execution_time")):
return ConnectionTimeoutError()
# 表不存在(MySQL 的 OperationalError 1146
if any(kw in msg for kw in ("doesn't exist", "does not exist", "no such table")):
table = _extract_table(orig_msg)
return TableNotFoundError(table=table)
return DatabaseConnectionError(detail=orig_msg)
def _handle_programming_error(
exc: ProgrammingError, msg: str, orig_msg: str
) -> FormDataException:
"""处理 ProgrammingError"""
# 表不存在
if any(kw in msg for kw in ("relation", "table")) and "does not exist" in msg:
table = _extract_table(orig_msg)
return TableNotFoundError(table=table)
# 列不存在
if any(kw in msg for kw in ("column", "field", "unknown column")) and any(
kw in msg for kw in ("does not exist", "not found", "unknown")
):
col = _extract_column(orig_msg)
return ColumnNotFoundError(column=col)
# SQL 语法错误
if any(kw in msg for kw in ("syntax error", "you have an error in your sql")):
return QueryError(detail=orig_msg)
return QueryError(detail=orig_msg)
def _fallback_string_match(
exc: Exception, msg: str, orig_msg: str
) -> FormDataException:
"""兜底的字符串匹配,尽可能识别常见场景"""
if "column" in msg and "does not exist" in msg:
col = _extract_column(orig_msg)
return ColumnNotFoundError(column=col)
if ("relation" in msg or "table" in msg) and "does not exist" in msg:
table = _extract_table(orig_msg)
return TableNotFoundError(table=table)
if "duplicate key" in msg or "unique" in msg:
return UniqueConstraintError()
if "null value" in msg or "cannot be null" in msg:
col = _extract_column(orig_msg)
return NotNullConstraintError(column=col)
if any(
kw in msg
for kw in (
"foreign key",
"referenced from table",
"restrict setting of foreign key",
"a foreign key constraint fails",
)
):
return _build_foreign_key_error(orig_msg)
if _is_failed_transaction_error(msg):
return _handle_failed_transaction_error(orig_msg)
if "connection" in msg:
return DatabaseConnectionError(detail=orig_msg)
return InternalDatabaseError(detail=orig_msg)
def _build_foreign_key_error(orig_msg: str) -> ForeignKeyConstraintError:
detail_match = _RE_PG_DETAIL.search(orig_msg)
detail = detail_match.group(1).strip() if detail_match else ""
referenced_table = _extract_fk_referencing_table(orig_msg)
constraint = _extract_constraint(orig_msg)
msg_lower = orig_msg.lower()
if any(kw in msg_lower for kw in ("delete", "referenced from table", "restrict setting")):
if referenced_table:
return ForeignKeyConstraintError(
message=(
f"无法删除,该数据已被「{referenced_table}」引用,"
"请先删除或解除关联数据"
),
detail=detail,
referenced_table=referenced_table,
constraint_name=constraint,
)
return ForeignKeyConstraintError(
message="无法删除,该数据已被其他数据引用,请先删除或解除关联数据",
detail=detail,
constraint_name=constraint,
)
if any(kw in msg_lower for kw in ("insert", "update")):
if referenced_table:
return ForeignKeyConstraintError(
message=f"操作失败,引用的「{referenced_table}」数据不存在或无效",
detail=detail,
referenced_table=referenced_table,
constraint_name=constraint,
)
return ForeignKeyConstraintError(
message="操作失败,引用的关联数据不存在或无效",
detail=detail,
constraint_name=constraint,
)
return ForeignKeyConstraintError(
detail=detail,
referenced_table=referenced_table,
constraint_name=constraint,
)
def _is_failed_transaction_error(msg: str) -> bool:
return any(
kw in msg
for kw in (
"infailedsqltransaction",
"current transaction is aborted",
"commands ignored until end of transaction block",
)
)
def is_aborted_transaction_error(exc: Exception) -> bool:
"""判断异常是否由 PostgreSQL 已中止事务引起。"""
messages = [str(exc).lower()]
orig = getattr(exc, "orig", None)
if orig is not None:
messages.append(str(orig).lower())
return any(_is_failed_transaction_error(msg) for msg in messages)
def _handle_failed_transaction_error(orig_msg: str) -> FormDataException:
"""事务已失败后再次执行 SQL 的兜底处理,尽量保留原始约束错误语义。"""
if any(
kw in orig_msg.lower()
for kw in (
"foreign key",
"referenced from table",
"restrict setting of foreign key",
"a foreign key constraint fails",
)
):
return _build_foreign_key_error(orig_msg)
logger.error("Database operation attempted inside failed transaction: %s", orig_msg)
return InternalDatabaseError(
detail="transaction aborted before error could be translated"
)
# =====================================================================
# 辅助函数
# =====================================================================
def _extract_unique_fields(msg: str) -> list[str]:
"""
从唯一约束错误中尝试提取冲突的字段名。
PostgreSQL DETAIL: Key (email)=(xxx) already exists.
MySQL: Duplicate entry 'xxx' for key 'uq_email'
"""
# PostgreSQL: Key (col1, col2)=(...) already exists
pg_match = re.search(r'Key\s*\(([^)]+)\)', msg)
if pg_match:
return [f.strip().strip('"') for f in pg_match.group(1).split(",")]
# MySQL: for key 'index_name'
mysql_match = re.search(r"for key\s+'(\w+)'", msg, re.IGNORECASE)
if mysql_match:
return [mysql_match.group(1)]
return []
# =====================================================================
# API 层:统一错误响应
# =====================================================================
def build_error_response(exc: FormDataException) -> HTTPException:
"""
将 FormDataException 转换为 FastAPI HTTPException。
响应 body 结构:
{
"detail": "用户友好的错误信息",
"error_code": "UNIQUE_CONSTRAINT_VIOLATION",
"context": { ... } // 可选的结构化上下文
}
"""
return HTTPException(
status_code=exc.http_status,
detail={
"message": str(exc),
"error_code": exc.error_code,
"context": exc.context,
},
)
def format_error_message(exc: Exception, *, max_length: int = 300) -> str:
"""将异常转为面向用户的简短错误信息(用于导入行级错误等场景)"""
if isinstance(exc, FormDataException):
return str(exc)[:max_length]
return str(translate_db_error(exc))[:max_length]
def _log_form_data_exception(exc: FormDataException) -> None:
"""记录表单数据业务异常的完整信息(含 context 中的原始错误详情,不截断)"""
log_fn = logger.error if exc.http_status >= 500 else logger.warning
log_fn("表单数据业务异常: [%s] %s", exc.error_code, exc)
if not exc.context:
return
if detail := exc.context.get("detail"):
log_fn("原始错误详情:\n%s", detail)
other = {k: v for k, v in exc.context.items() if k != "detail" and v not in (None, "", [], {})}
if other:
log_fn("异常上下文: %s", other)
def handle_db_error(e: Exception) -> HTTPException:
"""
API 层的统一入口:先转换为业务异常,再构建 HTTP 响应。
用法:
except Exception as e:
raise handle_db_error(e)
"""
if isinstance(e, FormDataException):
_log_form_data_exception(e)
return build_error_response(e)
logger.error("数据库操作错误: %s", e, exc_info=True)
biz_exc = translate_db_error(e)
_log_form_data_exception(biz_exc)
return build_error_response(biz_exc)
@@ -0,0 +1,979 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
动态 SQL 构建器(异步版本)
支持 PostgreSQL、MySQL、SQL Server、Oracle 的 SQL 语法差异
"""
import json
from datetime import date, datetime
from typing import Any, Dict, List, Optional, Tuple
from app.timezone import APP_TIMEZONE
from core.database_manager.sql_utils import quote_identifier as _quote_identifier
from core.database_manager.sql_utils import quote_table as _quote_table
def _normalize_db_type(db_type: str) -> str:
normalized = (db_type or "postgresql").lower()
if normalized in ("mssql",):
return "sqlserver"
if normalized in ("postgres", "psql"):
return "postgresql"
return normalized
class DynamicSQLBuilder:
"""动态 SQL 构建器 - 适配多种数据库"""
def __init__(
self,
db_type: str,
default_database: Optional[str] = None,
):
"""
初始化 SQL 构建器
Args:
db_type: postgresql | mysql | sqlserver | oracle
default_database: 连接默认库(SQL Server 与之一致时不生成三段表名)
"""
self.db_type = _normalize_db_type(db_type)
self.default_database = (default_database or "").strip()
def _uses_postgres_like(self) -> bool:
return self.db_type == "postgresql"
def is_deleted_predicate(self) -> str:
"""未删除行的 WHERE 条件片段(不含 AND)"""
if self.db_type == "postgresql":
return "is_deleted = false"
return "is_deleted = 0"
def is_deleted_and_clause(self) -> str:
"""未删除行的 AND 条件片段"""
return f" AND {self.is_deleted_predicate()}"
def _like_operator(self, case_sensitive: bool) -> str:
if self.db_type == "mysql":
return "LIKE BINARY" if case_sensitive else "LIKE"
if self._uses_postgres_like():
return "LIKE" if case_sensitive else "ILIKE"
return "LIKE"
# ============ 标识符引用 ============
def quote_identifier(self, name: str) -> str:
"""引用标识符(表名、列名等)"""
return _quote_identifier(name, self.db_type)
def _split_table_identifiers(
self,
table: str,
schema: Optional[str] = None,
database: Optional[str] = None,
) -> Tuple[str, Optional[str], Optional[str]]:
"""
解析表名中的限定符,避免 SQL Server 将 schema.table 当成单段对象名。
- MySQL: database.table(表名含点且无 database 时拆分)
- PG / SQL Server / Oracle: schema.table
"""
table = (table or "").strip()
schema = (schema or "").strip() or None
database = (database or "").strip() or None
if not table:
return table, schema, database
if self.db_type == "mysql":
if "." in table and not database:
db_part, tbl_part = table.split(".", 1)
if db_part.strip() and tbl_part.strip():
return tbl_part.strip(), schema, db_part.strip()
return table, schema, database
if "." in table:
if not schema:
sch, tbl = table.split(".", 1)
if sch.strip() and tbl.strip():
return tbl.strip(), sch.strip(), database
else:
prefix = f"{schema}."
if table.lower().startswith(prefix.lower()):
return table[len(prefix) :].strip(), schema, database
if table.count(".") >= 1:
return table.rsplit(".", 1)[-1].strip(), schema, database
return table, schema, database
def build_table_name(
self,
table: str,
schema: Optional[str] = None,
database: Optional[str] = None,
) -> str:
"""
构建完整的表名
PostgreSQL / SQL Server / Oracle: schema.table
MySQL: database.table
SQL Server 可选: database.schema.tablemain_table_database
"""
table, schema, database = self._split_table_identifiers(table, schema, database)
if self.db_type == "mysql":
if database:
return (
f"{_quote_identifier(database, self.db_type)}"
f".{_quote_identifier(table, self.db_type)}"
)
return _quote_identifier(table, self.db_type)
effective_schema = schema
if self.db_type == "sqlserver" and not effective_schema:
effective_schema = "dbo"
table_ref = (
_quote_table(effective_schema, table, self.db_type)
if effective_schema
else _quote_identifier(table, self.db_type)
)
if self.db_type == "sqlserver" and database:
conn_db = self.default_database.lower()
if conn_db and database.lower() == conn_db:
return table_ref
return f"{_quote_identifier(database, self.db_type)}.{table_ref}"
return table_ref
def _default_paging_order_by(self) -> str:
"""SQL Server / Oracle 使用 OFFSET/FETCH 时必须带 ORDER BY。"""
if self.db_type == "sqlserver":
return "(SELECT NULL)"
if self.db_type == "oracle":
return "1"
return ""
def _append_order_by_for_paging(self, sql: str, order_by: Optional[str]) -> str:
if order_by:
return f"{sql} ORDER BY {order_by}"
if self.db_type in ("sqlserver", "oracle"):
placeholder = self._default_paging_order_by()
if placeholder:
return f"{sql} ORDER BY {placeholder}"
return sql
def _append_limit_offset(
self, sql: str, limit: Optional[int], offset: Optional[int]
) -> str:
"""追加分页子句"""
if limit is None:
return sql
off = offset or 0
if self.db_type == "sqlserver":
return f"{sql} OFFSET {off} ROWS FETCH NEXT {limit} ROWS ONLY"
if self.db_type == "oracle":
if off:
return f"{sql} OFFSET {off} ROWS FETCH NEXT {limit} ROWS ONLY"
return f"{sql} FETCH FIRST {limit} ROWS ONLY"
sql += f" LIMIT {limit}"
if off:
sql += f" OFFSET {off}"
return sql
# ============ 参数占位符 ============
def get_placeholder(self, index: int = 0) -> str:
"""
获取参数占位符
PostgreSQL: $1, $2, ...
MySQL: %s
"""
if self.db_type == "postgresql":
return f"${index + 1}"
return "%s"
def get_placeholders(self, count: int, start_index: int = 0) -> List[str]:
"""获取多个占位符"""
return [self.get_placeholder(start_index + i) for i in range(count)]
# ============ 数据类型转换 ============
@staticmethod
def _convert_value(value: Any) -> Any:
"""
转换数据值为适合数据库的类型
注意:日期时间的转换应该由 service.py 的 _convert_data_types 方法
根据字段类型来处理,这里只处理基本的数据结构转换
Args:
value: 原始值
Returns:
转换后的值
"""
# 处理列表和字典类型
if isinstance(value, list):
return json.dumps(value, ensure_ascii=False)
if isinstance(value, dict):
return json.dumps(value, ensure_ascii=False)
return value
def _build_like_expr(self, quoted_field: str, case_sensitive: bool) -> str:
"""根据数据库类型和大小写敏感标志构建 LIKE 表达式
Args:
quoted_field: 已引用的字段名
case_sensitive: True=大小写敏感, False=大小写不敏感
Returns:
如: "CAST(name AS TEXT) LIKE :p""CAST(name AS TEXT) ILIKE :p"
"""
if self._uses_postgres_like():
if case_sensitive:
return f"CAST({quoted_field} AS TEXT) LIKE :{{}}"
return f"CAST({quoted_field} AS TEXT) ILIKE :{{}}"
if case_sensitive:
return f"CAST({quoted_field} AS VARCHAR(4000)) LIKE :{{}}"
return f"LOWER(CAST({quoted_field} AS VARCHAR(4000))) LIKE LOWER(:{{}})"
# ============ SELECT 构建 ============
def build_select(
self,
table: str,
columns: List[str] = None,
schema: Optional[str] = None,
database: Optional[str] = None,
where: Optional[Dict[str, Any]] = None,
order_by: Optional[str] = None,
limit: Optional[int] = None,
offset: Optional[int] = None
) -> Tuple[str, Dict[str, Any]]:
"""构建 SELECT 语句,返回命名参数"""
full_table = self.build_table_name(table, schema, database)
# 列
if columns:
cols = ", ".join(self.quote_identifier(c) for c in columns)
else:
cols = "*"
sql = f"SELECT {cols} FROM {full_table}"
params = {}
# WHERE
if where:
where_clause, where_params = self._build_where_named(where)
if where_clause:
sql += f" WHERE {where_clause}"
params.update(where_params)
sql = self._append_order_by_for_paging(sql, order_by)
sql = self._append_limit_offset(sql, limit, offset)
return sql, params
def build_count(
self,
table: str,
schema: Optional[str] = None,
database: Optional[str] = None,
where: Optional[Dict[str, Any]] = None
) -> Tuple[str, Dict[str, Any]]:
"""构建 COUNT 查询,返回命名参数"""
full_table = self.build_table_name(table, schema, database)
sql = f"SELECT COUNT(*) as total FROM {full_table}"
params = {}
if where:
where_clause, where_params = self._build_where_named(where)
if where_clause:
sql += f" WHERE {where_clause}"
params.update(where_params)
return sql, params
def build_cursor_select(
self,
table: str,
columns: List[str] = None,
schema: Optional[str] = None,
database: Optional[str] = None,
where: Optional[Dict[str, Any]] = None,
order_fields: Optional[List[Tuple[str, str]]] = None,
cursor_values: Optional[Dict[str, Any]] = None,
direction: str = "next",
limit: int = 20
) -> Tuple[str, Dict[str, Any]]:
"""构建游标分页 SELECT 语句
Args:
order_fields: 排序字段列表 [(field, "ASC"|"DESC"), ...],最后一个必须是唯一键(id)
cursor_values: 游标值字典 {field: value, ...},键与 order_fields 的 field 对应
direction: 翻页方向 "next"(下一页) 或 "prev"(上一页)
limit: 每页条数(内部会 +1 来判断 has_more
"""
full_table = self.build_table_name(table, schema, database)
if columns:
cols = ", ".join(self.quote_identifier(c) for c in columns)
else:
cols = "*"
sql = f"SELECT {cols} FROM {full_table}"
params = {}
where_clauses = []
# 先构建业务 WHERE 条件
if where:
biz_clause, biz_params = self._build_where_named(where)
if biz_clause:
where_clauses.append(biz_clause)
params.update(biz_params)
# 构建游标 WHERE 条件(行值比较)
if cursor_values and order_fields:
cursor_clause, cursor_params = self._build_cursor_where(
order_fields, cursor_values, direction
)
if cursor_clause:
where_clauses.append(cursor_clause)
params.update(cursor_params)
if where_clauses:
sql += f" WHERE {' AND '.join(where_clauses)}"
# 构建 ORDER BY(prev 方向需要反转排序)
order_by = None
if order_fields:
order_parts = []
for field, raw_dir in order_fields:
if direction == "prev":
actual_dir = "ASC" if raw_dir.upper() == "DESC" else "DESC"
else:
actual_dir = raw_dir.upper()
order_parts.append(f"{self.quote_identifier(field)} {actual_dir}")
order_by = ", ".join(order_parts)
sql = self._append_order_by_for_paging(sql, order_by)
sql = self._append_limit_offset(sql, limit + 1, 0)
return sql, params
def _cursor_comparison_op(
self,
fields_with_values: List[Tuple[str, str]],
direction: str,
) -> str:
"""游标翻页比较符(与首列排序方向、翻页方向一致)。"""
first_dir = fields_with_values[0][1].upper()
if direction == "prev":
return ">" if first_dir == "DESC" else "<"
return "<" if first_dir == "DESC" else ">"
def _build_cursor_where_lexographic(
self,
fields_with_values: List[Tuple[str, str]],
cursor_values: Dict[str, Any],
direction: str,
) -> Tuple[str, Dict[str, Any]]:
"""SQL Server / Oracle:等值前缀 + OR 链,避免行值比较语法不兼容。"""
op = self._cursor_comparison_op(fields_with_values, direction)
params: Dict[str, Any] = {}
or_parts: List[str] = []
for i, (field, _) in enumerate(fields_with_values):
conjuncts: List[str] = []
for j in range(i):
prev_field = fields_with_values[j][0]
eq_name = f"cursor_eq_{i}_{j}"
conjuncts.append(
f"{self.quote_identifier(prev_field)} = :{eq_name}"
)
params[eq_name] = cursor_values[prev_field]
cmp_name = f"cursor_cmp_{i}"
conjuncts.append(
f"{self.quote_identifier(field)} {op} :{cmp_name}"
)
params[cmp_name] = cursor_values[field]
or_parts.append(f"({' AND '.join(conjuncts)})")
return f"({' OR '.join(or_parts)})", params
def _build_cursor_where(
self,
order_fields: List[Tuple[str, str]],
cursor_values: Dict[str, Any],
direction: str
) -> Tuple[str, Dict[str, Any]]:
"""构建游标 WHERE 条件。
PostgreSQL / MySQL:行值比较 (a, b) > (:a, :b)
SQL Server / Oracle:等值前缀 + OR 链
"""
fields_with_values = [
(f, d) for f, d in order_fields if f in cursor_values
]
if not fields_with_values:
return "", {}
if self.db_type in ("sqlserver", "oracle"):
return self._build_cursor_where_lexographic(
fields_with_values, cursor_values, direction
)
params = {}
quoted_fields = []
param_placeholders = []
for i, (field, _) in enumerate(fields_with_values):
quoted_fields.append(self.quote_identifier(field))
param_name = f"cursor_{i}"
param_placeholders.append(f":{param_name}")
params[param_name] = cursor_values[field]
op = self._cursor_comparison_op(fields_with_values, direction)
lhs = f"({', '.join(quoted_fields)})"
rhs = f"({', '.join(param_placeholders)})"
clause = f"{lhs} {op} {rhs}"
return clause, params
# ============ INSERT 构建 ============
def build_insert(
self,
table: str,
data: Dict[str, Any],
schema: Optional[str] = None,
database: Optional[str] = None,
return_id: bool = True
) -> Tuple[str, Dict[str, Any]]:
"""构建 INSERT 语句,返回 SQL 和命名参数字典"""
full_table = self.build_table_name(table, schema, database)
# 清理字段名中的空格,并转换数据类型
cleaned_data = {}
for key, value in data.items():
cleaned_key = key.strip()
# 转换日期时间字符串为 Python 对象
converted_value = self._convert_value(value)
cleaned_data[cleaned_key] = converted_value
columns = list(cleaned_data.keys())
cols = ", ".join(self.quote_identifier(c) for c in columns)
# 使用命名参数 :param_name(使用清理后的字段名)
placeholders = ", ".join(f":{col}" for col in columns)
sql = f"INSERT INTO {full_table} ({cols}) VALUES ({placeholders})"
# 返回自增 ID
if return_id and self.db_type == "postgresql":
sql += " RETURNING id"
return sql, cleaned_data
def build_batch_insert(
self,
table: str,
columns: List[str],
rows: List[List[Any]],
schema: Optional[str] = None,
database: Optional[str] = None
) -> Tuple[str, List[Any]]:
"""构建批量 INSERT 语句"""
full_table = self.build_table_name(table, schema, database)
cols = ", ".join(self.quote_identifier(c) for c in columns)
# 构建多行 VALUES
row_placeholders = []
params = []
param_index = 0
for row in rows:
placeholders = ", ".join(self.get_placeholders(len(row), param_index))
row_placeholders.append(f"({placeholders})")
params.extend(row)
param_index += len(row)
sql = f"INSERT INTO {full_table} ({cols}) VALUES {', '.join(row_placeholders)}"
return sql, params
def build_batch_insert_named(
self,
table: str,
data_list: List[Dict[str, Any]],
schema: Optional[str] = None,
database: Optional[str] = None
) -> Tuple[str, Any]:
"""
构建批量 INSERT 语句
Args:
table: 表名
data_list: 数据列表,每个元素是一个字典
schema: Schema 名
database: 数据库名
Returns:
PostgreSQL: (SQL 语句, 位置参数列表)
MySQL: (SQL 语句, 命名参数字典)
"""
if not data_list:
raise ValueError("data_list 不能为空")
full_table = self.build_table_name(table, schema, database)
# 获取所有列名(使用第一条数据的键)
columns = list(data_list[0].keys())
cols = ", ".join(self.quote_identifier(c) for c in columns)
if self.db_type == "postgresql":
# PostgreSQL 使用位置参数 $1, $2, ...
row_placeholders = []
params = []
param_index = 0
for data in data_list:
placeholders = []
for col in columns:
placeholders.append(f"${param_index + 1}")
# 转换数据类型并添加到参数列表
params.append(self._convert_value(data.get(col)))
param_index += 1
row_placeholders.append(f"({', '.join(placeholders)})")
sql = f"INSERT INTO {full_table} ({cols}) VALUES {', '.join(row_placeholders)}"
return sql, params
else:
# MySQL 使用命名参数
row_placeholders = []
params = {}
for row_idx, data in enumerate(data_list):
placeholders = []
for col in columns:
param_name = f"p{row_idx}_{col}"
placeholders.append(f":{param_name}")
# 转换数据类型
params[param_name] = self._convert_value(data.get(col))
row_placeholders.append(f"({', '.join(placeholders)})")
sql = f"INSERT INTO {full_table} ({cols}) VALUES {', '.join(row_placeholders)}"
return sql, params
# ============ UPDATE 构建 ============
def build_update(
self,
table: str,
data: Dict[str, Any],
pk_field: str,
pk_value: Any,
schema: Optional[str] = None,
database: Optional[str] = None
) -> Tuple[str, Dict[str, Any]]:
"""构建 UPDATE 语句,返回 SQL 和命名参数字典"""
full_table = self.build_table_name(table, schema, database)
set_clauses = []
params = {}
param_counter = 0
# 清理字段名中的空格,并转换数据类型
for col, val in data.items():
cleaned_col = col.strip()
# 转换日期时间字符串为 Python 对象
converted_val = self._convert_value(val)
param_name = f"param_{param_counter}"
set_clauses.append(f"{self.quote_identifier(cleaned_col)} = :{param_name}")
params[param_name] = converted_val
param_counter += 1
# 清理主键字段名
cleaned_pk_field = pk_field.strip()
pk_param_name = f"param_{param_counter}"
sql = f"UPDATE {full_table} SET {', '.join(set_clauses)} WHERE {self.quote_identifier(cleaned_pk_field)} = :{pk_param_name}"
params[pk_param_name] = pk_value
return sql, params
# ============ DELETE 构建 ============
def build_delete(
self,
table: str,
pk_field: str,
pk_value: Any,
schema: Optional[str] = None,
database: Optional[str] = None
) -> Tuple[str, Dict[str, Any]]:
"""构建 DELETE 语句,返回 SQL 和命名参数字典"""
full_table = self.build_table_name(table, schema, database)
sql = f"DELETE FROM {full_table} WHERE {self.quote_identifier(pk_field)} = :pk_value"
return sql, {"pk_value": pk_value}
def build_delete_by_foreign_key(
self,
table: str,
fk_field: str,
fk_value: Any,
schema: Optional[str] = None,
database: Optional[str] = None
) -> Tuple[str, Dict[str, Any]]:
"""根据外键删除,返回 SQL 和命名参数字典"""
full_table = self.build_table_name(table, schema, database)
sql = f"DELETE FROM {full_table} WHERE {self.quote_identifier(fk_field)} = :fk_value"
return sql, {"fk_value": fk_value}
# ============ WHERE 条件构建 ============
def _build_where_named(self, conditions: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
"""构建 WHERE 子句(使用命名参数)"""
clauses = []
params = {}
param_counter = 0
for field, condition in conditions.items():
if condition is None:
continue
# 处理多字段搜索(OR 关系)
if field == '__search__' and isinstance(condition, dict):
keyword = condition.get('keyword')
search_fields = condition.get('fields', [])
if keyword and search_fields:
search_clauses = []
for search_field in search_fields:
quoted_search_field = self.quote_identifier(search_field)
param_name = f"search_param_{param_counter}"
# 使用 ILIKEPostgreSQL)或 LIKE(其他数据库)进行不区分大小写的模糊搜索
if self._uses_postgres_like():
search_clauses.append(
f"CAST({quoted_search_field} AS TEXT) ILIKE :{param_name}"
)
else:
search_clauses.append(
f"LOWER(CAST({quoted_search_field} AS VARCHAR(4000))) "
f"LIKE LOWER(:{param_name})"
)
params[param_name] = f"%{keyword}%"
param_counter += 1
if search_clauses:
clauses.append(f"({' OR '.join(search_clauses)})")
continue
quoted_field = self.quote_identifier(field)
if isinstance(condition, dict):
cond_type = condition.get("type", "eq")
value = condition.get("value")
if cond_type == "like" and value:
case_sensitive = condition.get("case_sensitive", True)
param_name = f"param_{param_counter}"
like_expr = self._build_like_expr(quoted_field, case_sensitive)
clauses.append(like_expr.format(param_name))
params[param_name] = f"%{value}%"
param_counter += 1
elif cond_type == "eq" and value is not None:
param_name = f"param_{param_counter}"
case_sensitive = condition.get("case_sensitive", True)
if case_sensitive:
clauses.append(f"{quoted_field} = :{param_name}")
else:
clauses.append(f"LOWER({quoted_field}) = LOWER(:{param_name})")
params[param_name] = value
param_counter += 1
elif cond_type == "ne" and value is not None:
param_name = f"param_{param_counter}"
clauses.append(f"{quoted_field} != :{param_name}")
params[param_name] = value
param_counter += 1
elif cond_type == "gt" and value is not None:
param_name = f"param_{param_counter}"
clauses.append(f"{quoted_field} > :{param_name}")
params[param_name] = value
param_counter += 1
elif cond_type == "gte" and value is not None:
param_name = f"param_{param_counter}"
clauses.append(f"{quoted_field} >= :{param_name}")
params[param_name] = value
param_counter += 1
elif cond_type == "lt" and value is not None:
param_name = f"param_{param_counter}"
clauses.append(f"{quoted_field} < :{param_name}")
params[param_name] = value
param_counter += 1
elif cond_type == "lte" and value is not None:
param_name = f"param_{param_counter}"
clauses.append(f"{quoted_field} <= :{param_name}")
params[param_name] = value
param_counter += 1
elif cond_type == "range" and value and isinstance(value, list) and len(value) == 2:
param_name_start = f"param_{param_counter}"
param_name_end = f"param_{param_counter + 1}"
clauses.append(f"{quoted_field} BETWEEN :{param_name_start} AND :{param_name_end}")
params[param_name_start] = value[0]
params[param_name_end] = value[1]
param_counter += 2
elif cond_type == "in" and value:
# IN 条件:value 是一个列表
if isinstance(value, list) and len(value) > 0:
placeholders = []
for v in value:
param_name = f"param_{param_counter}"
placeholders.append(f":{param_name}")
params[param_name] = v
param_counter += 1
clauses.append(f"{quoted_field} IN ({', '.join(placeholders)})")
elif cond_type == "eq_or_null" and value is not None:
# 等值 OR 字段为空(用于数据权限:字段为空时所有人可见)
param_name = f"param_{param_counter}"
clauses.append(f"({quoted_field} = :{param_name} OR {quoted_field} IS NULL)")
params[param_name] = value
param_counter += 1
elif cond_type == "in_or_null" and value:
# IN 条件 OR 字段为空(用于数据权限:字段为空时所有人可见)
if isinstance(value, list) and len(value) > 0:
placeholders = []
for v in value:
param_name = f"param_{param_counter}"
placeholders.append(f":{param_name}")
params[param_name] = v
param_counter += 1
clauses.append(f"({quoted_field} IN ({', '.join(placeholders)}) OR {quoted_field} IS NULL)")
elif cond_type == "space_like_and" and value:
# 空格模糊且:按空格拆分关键词,用 AND + LIKE 连接(范围逐渐缩小)
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
param_name = f"param_{param_counter}"
like_expr = self._build_like_expr(quoted_field, case_sensitive)
keyword_clauses.append(like_expr.format(param_name))
params[param_name] = f"%{keyword}%"
param_counter += 1
clauses.append(f"({' AND '.join(keyword_clauses)})")
elif cond_type == "space_like_or" and value:
# 空格模糊或:按空格拆分关键词,用 OR + LIKE 连接(范围逐渐扩大)
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
param_name = f"param_{param_counter}"
like_expr = self._build_like_expr(quoted_field, case_sensitive)
keyword_clauses.append(like_expr.format(param_name))
params[param_name] = f"%{keyword}%"
param_counter += 1
clauses.append(f"({' OR '.join(keyword_clauses)})")
elif cond_type == "space_eq_and" and value:
# 空格精确且:按空格拆分关键词,用 AND + = 连接(范围逐渐缩小)
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
param_name = f"param_{param_counter}"
if case_sensitive:
keyword_clauses.append(f"{quoted_field} = :{param_name}")
else:
keyword_clauses.append(f"LOWER({quoted_field}) = LOWER(:{param_name})")
params[param_name] = keyword
param_counter += 1
clauses.append(f"({' AND '.join(keyword_clauses)})")
elif cond_type == "space_eq_or" and value:
# 空格精确或:按空格拆分关键词,用 OR + = 连接(范围逐渐扩大)
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
param_name = f"param_{param_counter}"
if case_sensitive:
keyword_clauses.append(f"{quoted_field} = :{param_name}")
else:
keyword_clauses.append(f"LOWER({quoted_field}) = LOWER(:{param_name})")
params[param_name] = keyword
param_counter += 1
clauses.append(f"({' OR '.join(keyword_clauses)})")
elif cond_type == "null":
clauses.append(f"{quoted_field} IS NULL")
elif cond_type == "not_null":
clauses.append(f"{quoted_field} IS NOT NULL")
else:
# 简单等值条件
if condition is not None and condition != "":
param_name = f"param_{param_counter}"
clauses.append(f"{quoted_field} = :{param_name}")
params[param_name] = condition
param_counter += 1
return " AND ".join(clauses), params
def _build_where(self, conditions: Dict[str, Any], start_index: int = 0) -> Tuple[str, List[Any], int]:
"""
构建 WHERE 子句
支持的条件格式:
- {"field": value} -> field = value
- {"field": {"type": "like", "value": "xxx"}} -> field LIKE '%xxx%'
- {"field": {"type": "eq", "value": xxx}} -> field = xxx
- {"field": {"type": "ne", "value": xxx}} -> field != xxx
- {"field": {"type": "gt", "value": xxx}} -> field > xxx
- {"field": {"type": "gte", "value": xxx}} -> field >= xxx
- {"field": {"type": "lt", "value": xxx}} -> field < xxx
- {"field": {"type": "lte", "value": xxx}} -> field <= xxx
- {"field": {"type": "in", "value": [...]}} -> field IN (...)
- {"field": {"type": "range", "value": [start, end]}} -> field BETWEEN start AND end
- {"field": {"type": "null"}} -> field IS NULL
- {"field": {"type": "not_null"}} -> field IS NOT NULL
"""
clauses = []
params = []
param_index = start_index
for field, condition in conditions.items():
quoted_field = self.quote_identifier(field)
if condition is None:
continue
if isinstance(condition, dict):
cond_type = condition.get("type", "eq")
value = condition.get("value")
if cond_type == "like" and value:
case_sensitive = condition.get("case_sensitive", True)
if self.db_type == "postgresql":
like_op = "LIKE" if case_sensitive else "ILIKE"
clauses.append(f"CAST({quoted_field} AS TEXT) {like_op} {self.get_placeholder(param_index)}")
else:
like_op = self._like_operator(case_sensitive)
cast_type = "VARCHAR(4000)" if self.db_type in ("sqlserver", "oracle") else "CHAR"
clauses.append(
f"CAST({quoted_field} AS {cast_type}) {like_op} "
f"{self.get_placeholder(param_index)}"
)
params.append(f"%{value}%")
param_index += 1
elif cond_type == "eq" and value is not None:
case_sensitive = condition.get("case_sensitive", True)
if case_sensitive:
clauses.append(f"{quoted_field} = {self.get_placeholder(param_index)}")
else:
clauses.append(f"LOWER({quoted_field}) = LOWER({self.get_placeholder(param_index)})")
params.append(value)
param_index += 1
elif cond_type == "ne" and value is not None:
clauses.append(f"{quoted_field} != {self.get_placeholder(param_index)}")
params.append(value)
param_index += 1
elif cond_type == "gt" and value is not None:
clauses.append(f"{quoted_field} > {self.get_placeholder(param_index)}")
params.append(value)
param_index += 1
elif cond_type == "gte" and value is not None:
clauses.append(f"{quoted_field} >= {self.get_placeholder(param_index)}")
params.append(value)
param_index += 1
elif cond_type == "lt" and value is not None:
clauses.append(f"{quoted_field} < {self.get_placeholder(param_index)}")
params.append(value)
param_index += 1
elif cond_type == "lte" and value is not None:
clauses.append(f"{quoted_field} <= {self.get_placeholder(param_index)}")
params.append(value)
param_index += 1
elif cond_type == "in" and value:
placeholders = ", ".join(self.get_placeholders(len(value), param_index))
clauses.append(f"{quoted_field} IN ({placeholders})")
params.extend(value)
param_index += len(value)
elif cond_type == "range" and value and len(value) == 2:
clauses.append(f"{quoted_field} BETWEEN {self.get_placeholder(param_index)} AND {self.get_placeholder(param_index + 1)}")
params.extend(value)
param_index += 2
elif cond_type == "space_like_and" and value:
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
if self.db_type == "postgresql":
like_op = "LIKE" if case_sensitive else "ILIKE"
keyword_clauses.append(f"CAST({quoted_field} AS TEXT) {like_op} {self.get_placeholder(param_index)}")
else:
like_op = self._like_operator(case_sensitive)
cast_type = "VARCHAR(4000)" if self.db_type in ("sqlserver", "oracle") else "CHAR"
keyword_clauses.append(
f"CAST({quoted_field} AS {cast_type}) {like_op} "
f"{self.get_placeholder(param_index)}"
)
params.append(f"%{keyword}%")
param_index += 1
clauses.append(f"({' AND '.join(keyword_clauses)})")
elif cond_type == "space_like_or" and value:
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
if self.db_type == "postgresql":
like_op = "LIKE" if case_sensitive else "ILIKE"
keyword_clauses.append(f"CAST({quoted_field} AS TEXT) {like_op} {self.get_placeholder(param_index)}")
else:
like_op = self._like_operator(case_sensitive)
cast_type = "VARCHAR(4000)" if self.db_type in ("sqlserver", "oracle") else "CHAR"
keyword_clauses.append(
f"CAST({quoted_field} AS {cast_type}) {like_op} "
f"{self.get_placeholder(param_index)}"
)
params.append(f"%{keyword}%")
param_index += 1
clauses.append(f"({' OR '.join(keyword_clauses)})")
elif cond_type == "space_eq_and" and value:
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
if case_sensitive:
keyword_clauses.append(f"{quoted_field} = {self.get_placeholder(param_index)}")
else:
keyword_clauses.append(f"LOWER({quoted_field}) = LOWER({self.get_placeholder(param_index)})")
params.append(keyword)
param_index += 1
clauses.append(f"({' AND '.join(keyword_clauses)})")
elif cond_type == "space_eq_or" and value:
case_sensitive = condition.get("case_sensitive", True)
keywords = str(value).split()
if keywords:
keyword_clauses = []
for keyword in keywords:
if case_sensitive:
keyword_clauses.append(f"{quoted_field} = {self.get_placeholder(param_index)}")
else:
keyword_clauses.append(f"LOWER({quoted_field}) = LOWER({self.get_placeholder(param_index)})")
params.append(keyword)
param_index += 1
clauses.append(f"({' OR '.join(keyword_clauses)})")
elif cond_type == "null":
clauses.append(f"{quoted_field} IS NULL")
elif cond_type == "not_null":
clauses.append(f"{quoted_field} IS NOT NULL")
else:
# 简单等值条件
if condition is not None and condition != "":
clauses.append(f"{quoted_field} = {self.get_placeholder(param_index)}")
params.append(condition)
param_index += 1
return " AND ".join(clauses), params, param_index
@@ -0,0 +1,308 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
表单数据操作 — 细粒度异常体系
异常层次:
FormDataException (所有表单数据异常的基类)
├── FormNotFoundException (表单不存在)
├── RecordNotFoundException (记录不存在)
├── FormDataValidationError (数据校验失败的基类)
│ ├── UniqueConstraintError (唯一约束冲突)
│ ├── NotNullConstraintError (非空约束违反)
│ ├── CheckConstraintError (CHECK 约束违反)
│ ├── ForeignKeyConstraintError (外键约束违反)
│ ├── DataTypeMismatchError (数据类型不匹配)
│ └── DataTooLongError (数据超长)
├── FormDataSchemaError (表结构/配置错误的基类)
│ ├── TableNotFoundError (表不存在)
│ └── ColumnNotFoundError (列不存在)
├── FormDataConnectionError (连接相关错误的基类)
│ ├── DatabaseConnectionError (连接失败/断开)
│ ├── ConnectionPoolExhaustedError (连接池耗尽)
│ └── ConnectionTimeoutError (连接超时)
├── FormDataConcurrencyError (并发相关错误的基类)
│ ├── DeadlockError (死锁)
│ └── LockTimeoutError (锁等待超时)
├── QueryError (SQL 查询构建/执行错误)
└── InternalDatabaseError (其他数据库内部错误)
每个异常类携带 error_code 和结构化的上下文信息 (context),
方便 API 层映射到 HTTP 响应 + 前端根据 error_code 做差异化处理。
"""
from __future__ import annotations
from typing import Any, Dict, Optional
# =====================================================================
# 基类
# =====================================================================
class FormDataException(Exception):
"""表单数据操作异常基类"""
error_code: str = "FORM_DATA_ERROR"
http_status: int = 400
def __init__(
self,
message: str,
*,
error_code: Optional[str] = None,
http_status: Optional[int] = None,
context: Optional[Dict[str, Any]] = None,
):
super().__init__(message)
if error_code is not None:
self.error_code = error_code
if http_status is not None:
self.http_status = http_status
self.context: Dict[str, Any] = context or {}
# =====================================================================
# 资源不存在
# =====================================================================
class FormNotFoundException(FormDataException):
"""表单定义不存在"""
error_code = "FORM_NOT_FOUND"
http_status = 404
def __init__(self, form_code: str):
super().__init__(
f"表单不存在: {form_code}",
context={"form_code": form_code},
)
class RecordNotFoundException(FormDataException):
"""记录不存在"""
error_code = "RECORD_NOT_FOUND"
http_status = 404
def __init__(self, pk: Any, *, table: str = ""):
super().__init__(
f"数据不存在: {pk}",
context={"pk": str(pk), "table": table},
)
# =====================================================================
# 数据校验 / 约束违反
# =====================================================================
class FormDataValidationError(FormDataException):
"""数据校验失败的基类"""
error_code = "VALIDATION_ERROR"
http_status = 400
class UniqueConstraintError(FormDataValidationError):
"""唯一约束冲突(含重复数据信息)"""
error_code = "UNIQUE_CONSTRAINT_VIOLATION"
def __init__(
self,
message: str = "数据重复,违反唯一约束",
*,
fields: Optional[list[str]] = None,
constraint_name: str = "",
):
super().__init__(
message,
context={"fields": fields or [], "constraint_name": constraint_name},
)
class NotNullConstraintError(FormDataValidationError):
"""非空约束违反"""
error_code = "NOT_NULL_VIOLATION"
def __init__(self, column: str = ""):
msg = f"字段 '{column}' 不能为空" if column else "存在必填字段为空"
super().__init__(msg, context={"column": column})
class CheckConstraintError(FormDataValidationError):
"""CHECK 约束违反"""
error_code = "CHECK_CONSTRAINT_VIOLATION"
def __init__(self, constraint_name: str = "", detail: str = ""):
msg = "数据不满足校验规则"
if constraint_name:
msg += f" ({constraint_name})"
super().__init__(msg, context={"constraint_name": constraint_name, "detail": detail})
class ForeignKeyConstraintError(FormDataValidationError):
"""外键约束违反(删除/更新时关联数据存在)"""
error_code = "FOREIGN_KEY_VIOLATION"
def __init__(
self,
message: str = "",
*,
detail: str = "",
referenced_table: str = "",
constraint_name: str = "",
):
if not message:
message = "操作失败,该数据被其他数据引用"
if referenced_table:
message = (
f"无法删除,该数据已被「{referenced_table}」引用,"
"请先删除或解除关联数据"
)
elif detail:
message += f"{detail}"
super().__init__(
message,
context={
"detail": detail,
"referenced_table": referenced_table,
"constraint_name": constraint_name,
},
)
class DataTypeMismatchError(FormDataValidationError):
"""数据类型不匹配"""
error_code = "DATA_TYPE_MISMATCH"
def __init__(self, detail: str = ""):
msg = "数据类型不匹配,请检查输入数据格式"
if detail:
msg += f"{detail}"
super().__init__(msg, context={"detail": detail})
class DataTooLongError(FormDataValidationError):
"""数据长度超出字段限制"""
error_code = "DATA_TOO_LONG"
def __init__(self, column: str = "", max_length: int = 0):
msg = f"字段 '{column}' 的数据过长" if column else "数据长度超出限制"
if max_length:
msg += f"(最大 {max_length} 个字符)"
super().__init__(
msg,
context={"column": column, "max_length": max_length},
)
# =====================================================================
# 表结构 / 配置错误
# =====================================================================
class FormDataSchemaError(FormDataException):
"""表结构或配置相关错误基类"""
error_code = "SCHEMA_ERROR"
http_status = 400
class TableNotFoundError(FormDataSchemaError):
"""数据库表不存在"""
error_code = "TABLE_NOT_FOUND"
def __init__(self, table: str = ""):
msg = f"数据库表 {table} 不存在,请检查数据源配置" if table else "数据库表不存在"
super().__init__(msg, context={"table": table})
class ColumnNotFoundError(FormDataSchemaError):
"""数据库列不存在"""
error_code = "COLUMN_NOT_FOUND"
def __init__(self, column: str = "", table: str = ""):
msg = f"数据库字段 '{column}' 不存在" if column else "数据库字段不存在"
msg += ",请检查表单配置与数据库表结构是否一致"
super().__init__(msg, context={"column": column, "table": table})
# =====================================================================
# 连接相关
# =====================================================================
class FormDataConnectionError(FormDataException):
"""数据库连接相关错误基类"""
error_code = "CONNECTION_ERROR"
http_status = 503
class DatabaseConnectionError(FormDataConnectionError):
"""数据库连接失败或断开"""
error_code = "DB_CONNECTION_FAILED"
def __init__(self, detail: str = ""):
msg = "数据库连接异常,请稍后重试"
super().__init__(msg, context={"detail": detail})
class ConnectionPoolExhaustedError(FormDataConnectionError):
"""连接池耗尽"""
error_code = "CONNECTION_POOL_EXHAUSTED"
def __init__(self):
super().__init__(
"服务器繁忙(数据库连接池已满),请稍后重试",
)
class ConnectionTimeoutError(FormDataConnectionError):
"""连接超时"""
error_code = "CONNECTION_TIMEOUT"
def __init__(self):
super().__init__("数据库连接超时,请稍后重试")
# =====================================================================
# 并发相关
# =====================================================================
class FormDataConcurrencyError(FormDataException):
"""并发相关错误基类"""
error_code = "CONCURRENCY_ERROR"
http_status = 409
class DeadlockError(FormDataConcurrencyError):
"""死锁"""
error_code = "DEADLOCK"
def __init__(self):
super().__init__("操作冲突(死锁),请重试")
class LockTimeoutError(FormDataConcurrencyError):
"""锁等待超时"""
error_code = "LOCK_TIMEOUT"
def __init__(self):
super().__init__("操作等待超时,请稍后重试")
# =====================================================================
# 查询 / 其他
# =====================================================================
class QueryError(FormDataException):
"""SQL 查询构建或执行错误"""
error_code = "QUERY_ERROR"
http_status = 400
def __init__(self, detail: str = ""):
msg = "数据库查询异常,请检查表单配置是否正确"
super().__init__(msg, context={"detail": detail})
class InternalDatabaseError(FormDataException):
"""其他未分类的数据库内部错误"""
error_code = "INTERNAL_DB_ERROR"
http_status = 500
def __init__(self, detail: str = ""):
msg = "数据库操作失败,请联系管理员"
super().__init__(msg, context={"detail": detail})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
表单管理模块
"""
@@ -0,0 +1,59 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
表单管理数据模型
"""
from sqlalchemy import Column, String, Text, Integer, Index, JSON, Boolean
from app.base_model import BaseModel
class FormMeta(BaseModel):
"""表单元数据"""
__tablename__ = "form_meta"
# 所属应用(逻辑外键关联 core_application
application_id = Column(String(21), nullable=True, index=True, comment="所属应用ID")
name = Column(String(100), nullable=False, comment="表单名称")
code = Column(String(100), unique=True, nullable=False, comment="表单编码")
form_type = Column(String(20), default="normal", comment="表单类型: normal/workflow")
description = Column(Text, default="", comment="描述")
status = Column(String(20), default="draft", index=True, comment="状态: draft/published")
version = Column(Integer, default=1, comment="版本号")
# 数据源配置
db_config = Column(String(100), nullable=False, comment="数据库配置名")
main_table = Column(String(100), nullable=False, comment="主表名")
main_table_schema = Column(String(100), default="", comment="主表Schema")
main_table_database = Column(String(100), default="", comment="主表数据库")
# 移动端配置
show_in_mobile = Column(Boolean, default=False, comment="是否在移动端显示")
# 跨应用引用
globally_visible = Column(Boolean, default=False, comment="是否全局可见(供其他应用引用)")
# 图标配置
icon = Column(String(100), default="", comment="图标")
icon_bg_color = Column(String(200), default="", comment="图标背景色")
# JSON 配置
form_config = Column(JSON, default=dict, comment="表单设计配置")
list_config = Column(JSON, default=dict, comment="列表设计配置")
class FormSubTable(BaseModel):
"""表单子表关联"""
__tablename__ = "form_sub_table"
# 所属表单(逻辑外键)
form_id = Column(String(50), nullable=False, index=True, comment="所属表单ID")
table_name = Column(String(100), nullable=False, comment="从表名")
table_schema = Column(String(100), default="", comment="从表Schema")
table_database = Column(String(100), default="", comment="从表数据库")
alias = Column(String(100), default="", comment="别名")
foreign_key = Column(String(100), nullable=False, comment="外键字段")
related_field = Column(String(100), default="id", comment="关联主表字段")
relation_type = Column(String(20), default="one-to-many", comment="关联类型: one-to-one/one-to-many")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
from .service import PageService, PageServiceException
__all__ = ["PageService", "PageServiceException"]
@@ -0,0 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
页面管理数据模型
"""
from sqlalchemy import Column, String, Text, Integer, JSON
from app.base_model import BaseModel
class PageMeta(BaseModel):
"""页面元数据"""
__tablename__ = "page_meta"
# 所属应用(逻辑外键关联 core_application
application_id = Column(String(21), nullable=True, index=True, comment="所属应用ID")
name = Column(String(100), nullable=False, comment="页面名称")
code = Column(String(100), unique=True, nullable=False, comment="页面编码")
category = Column(String(50), default="", comment="分类")
description = Column(Text, default="", comment="描述")
status = Column(String(20), default="draft", index=True, comment="状态: draft/published")
version = Column(Integer, default=1, comment="版本号")
# 页面配置(存储 dashboard-design 的配置)
page_config = Column(JSON, default=dict, comment="页面设计配置")
@@ -0,0 +1,507 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
页面元数据管理服务(异步版本)
"""
import logging
from typing import Any, Dict, List
from sqlalchemy import select, update, delete, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.page_manager.model import PageMeta
from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions
logger = logging.getLogger(__name__)
# 资源类型(用于数据权限配置)
RESOURCE_TYPE = "page"
RESOURCE_DISPLAY_NAME = "页面管理"
class PageServiceException(Exception):
"""页面服务异常"""
pass
class PageService:
"""
页面元数据管理服务
数据权限:
- 使用 list_with_data_scope() 自动应用数据权限
- 支持本人、本部门、本部门及下级、全部等数据范围
"""
# ============ 查询 ============
@staticmethod
async def list(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: str = None,
name: str = None,
code: str = None,
category: str = None,
status: str = None
) -> Dict[str, Any]:
"""分页查询页面列表"""
conditions = [PageMeta.is_deleted == False]
# 应用过滤
if application_id:
conditions.append(PageMeta.application_id == application_id)
else:
# 如果没有指定 application_id,只返回主应用的页面(application_id 为 NULL
conditions.append(PageMeta.application_id.is_(None))
if name:
conditions.append(PageMeta.name.ilike(f"%{name}%"))
if code:
conditions.append(PageMeta.code.ilike(f"%{code}%"))
if category:
conditions.append(PageMeta.category == category)
if status:
conditions.append(PageMeta.status == status)
# 获取总数
count_stmt = select(func.count(PageMeta.id)).where(and_(*conditions))
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 获取列表
offset = (page - 1) * page_size
stmt = select(PageMeta).where(and_(*conditions)).order_by(
PageMeta.sort, PageMeta.sys_create_datetime.desc()
).offset(offset).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
@staticmethod
async def list_with_data_scope(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: str = None,
name: str = None,
code: str = None,
category: str = None,
status: str = None
) -> Dict[str, Any]:
"""
分页查询页面列表(带数据权限过滤)
自动从上下文获取当前用户信息,应用数据权限过滤
"""
conditions = [PageMeta.is_deleted == False]
# 应用过滤
if application_id:
conditions.append(PageMeta.application_id == application_id)
else:
conditions.append(PageMeta.application_id.is_(None))
if name:
conditions.append(PageMeta.name.ilike(f"%{name}%"))
if code:
conditions.append(PageMeta.code.ilike(f"%{code}%"))
if category:
conditions.append(PageMeta.category == category)
if status:
conditions.append(PageMeta.status == status)
# 获取数据权限过滤条件并应用
data_scope_filter = await get_data_scope_filter(db, RESOURCE_TYPE)
scope_conditions = apply_data_scope_to_conditions(PageMeta, data_scope_filter)
conditions.extend(scope_conditions)
# 获取总数
count_stmt = select(func.count(PageMeta.id)).where(and_(*conditions))
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 获取列表
offset = (page - 1) * page_size
stmt = select(PageMeta).where(and_(*conditions)).order_by(
PageMeta.sort, PageMeta.sys_create_datetime.desc()
).offset(offset).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
@staticmethod
async def get(db: AsyncSession, page_id: str) -> PageMeta:
"""获取页面详情"""
stmt = select(PageMeta).where(
PageMeta.id == page_id,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
page = result.scalar_one_or_none()
if not page:
raise PageServiceException(f"页面不存在: {page_id}")
return page
@staticmethod
async def get_by_code(db: AsyncSession, code: str) -> PageMeta:
"""根据编码获取页面"""
stmt = select(PageMeta).where(
PageMeta.code == code,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
page = result.scalar_one_or_none()
if not page:
raise PageServiceException(f"页面不存在: {code}")
return page
# ============ 创建 ============
@staticmethod
async def create(
db: AsyncSession,
data: Dict[str, Any],
user_id: str = None
) -> PageMeta:
"""创建页面"""
code = data.get("code")
# 检查编码唯一性
stmt = select(PageMeta).where(
PageMeta.code == code,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
if result.scalar_one_or_none():
raise PageServiceException(f"页面编码已存在: {code}")
# 从上下文获取用户信息
from utils.context import get_current_user_info_from_context
user_info = get_current_user_info_from_context()
page = PageMeta(
application_id=data.get("application_id"),
name=data.get("name"),
code=code,
category=data.get("category", ""),
description=data.get("description", ""),
page_config=data.get("page_config", {}),
sort=data.get("sort", 0),
sys_creator_id=user_id or (user_info.get('user_id') if user_info else None),
sys_modifier_id=user_id or (user_info.get('user_id') if user_info else None),
)
# 自动填充部门ID
if user_info and user_info.get('dept_id'):
page.sys_dept_id = user_info.get('dept_id')
db.add(page)
await db.commit()
await db.refresh(page)
logger.info(f"页面创建成功: {page.code}")
return page
# ============ 更新 ============
@staticmethod
async def update(
db: AsyncSession,
page_id: str,
data: Dict[str, Any],
user_id: str = None
) -> PageMeta:
"""更新页面"""
page = await PageService.get(db, page_id)
# 更新基本字段
if "name" in data and data["name"] is not None:
page.name = data["name"]
if "category" in data and data["category"] is not None:
page.category = data["category"]
if "description" in data and data["description"] is not None:
page.description = data["description"]
if "sort" in data and data["sort"] is not None:
page.sort = data["sort"]
if "page_config" in data and data["page_config"] is not None:
page.page_config = data["page_config"]
page.sys_modifier_id = user_id
await db.commit()
await db.refresh(page)
logger.info(f"页面更新成功: {page.code}")
return page
# ============ 删除 ============
@staticmethod
async def _cleanup_page_publish_resources(db: AsyncSession, page: PageMeta) -> None:
"""清理页面发布产生的菜单(取消发布、删除时共用)"""
from core.menu.model import Menu
delete_menu_stmt = delete(Menu).where(
Menu.path == f"/page-render/{page.code}"
)
result = await db.execute(delete_menu_stmt)
if result.rowcount > 0:
logger.info(
"物理删除页面菜单: %s, 删除数量: %s",
page.code,
result.rowcount,
)
@staticmethod
async def delete(db: AsyncSession, page_id: str) -> bool:
"""删除页面(软删除)"""
from core.menu.service import MenuService
page = await PageService.get(db, page_id)
await PageService._cleanup_page_publish_resources(db, page)
page.is_deleted = True
page.status = "draft"
await db.commit()
await MenuService.invalidate_cache()
logger.info("页面删除成功: %s", page.code)
return True
@staticmethod
async def batch_delete(db: AsyncSession, page_ids: List[str]) -> int:
"""批量删除页面"""
from core.menu.service import MenuService
stmt = select(PageMeta).where(
PageMeta.id.in_(page_ids),
PageMeta.is_deleted == False,
)
pages = list((await db.execute(stmt)).scalars().all())
for page in pages:
await PageService._cleanup_page_publish_resources(db, page)
update_stmt = update(PageMeta).where(
PageMeta.id.in_(page_ids),
PageMeta.is_deleted == False,
).values(is_deleted=True, status="draft")
result = await db.execute(update_stmt)
await db.commit()
count = result.rowcount
if count > 0:
await MenuService.invalidate_cache()
logger.info("批量删除页面成功: %s", count)
return count
# ============ 发布/取消发布 ============
@staticmethod
async def publish(
db: AsyncSession,
page_id: str,
publish_config: Dict[str, Any] = None
) -> PageMeta:
"""发布页面并创建菜单"""
from core.menu.model import Menu
from core.menu.service import MenuService
page = await PageService.get(db, page_id)
if page.status == "published":
raise PageServiceException("页面已发布")
# 更新页面状态
page.status = "published"
page.version += 1
# 创建或更新菜单
if publish_config:
menu_parent_id = publish_config.get("menu_parent_id")
menu_path = f"/page-render/{page.code}"
# 检查是否已存在该页面的菜单
menu_stmt = select(Menu).where(
Menu.path == menu_path
)
menu_result = await db.execute(menu_stmt)
existing_menu = menu_result.scalar_one_or_none()
if existing_menu:
# 更新现有菜单
existing_menu.name = publish_config.get("menu_name", page.name)
existing_menu.title = publish_config.get("menu_name", page.name)
existing_menu.parent_id = menu_parent_id
existing_menu.icon = publish_config.get("menu_icon", "lucide:layout-dashboard")
existing_menu.order = publish_config.get("menu_order", 0)
existing_menu.type = "online_page"
existing_menu.application_id = page.application_id
logger.info(f"更新页面菜单: {page.code}")
else:
# 创建新菜单
new_menu = Menu(
application_id=page.application_id,
name=publish_config.get("menu_name", page.name),
title=publish_config.get("menu_name", page.name),
path=menu_path,
component="online-dev/page-render/index",
type="online_page",
parent_id=menu_parent_id,
icon=publish_config.get("menu_icon", "lucide:layout-dashboard"),
order=publish_config.get("menu_order", 0),
)
db.add(new_menu)
logger.info(f"创建页面菜单: {page.code}")
await db.commit()
await db.refresh(page)
# 清空菜单缓存
await MenuService.invalidate_cache()
logger.info("已清空菜单缓存")
logger.info(f"页面发布成功: {page.code}, version={page.version}")
return page
@staticmethod
async def unpublish(db: AsyncSession, page_id: str) -> PageMeta:
"""取消发布页面并物理删除菜单"""
from core.menu.service import MenuService
page = await PageService.get(db, page_id)
if page.status == "draft":
raise PageServiceException("页面未发布")
page.status = "draft"
await PageService._cleanup_page_publish_resources(db, page)
await db.commit()
await db.refresh(page)
# 清空菜单缓存
await MenuService.invalidate_cache()
logger.info("已清空菜单缓存")
logger.info("页面取消发布: %s", page.code)
return page
# ============ 复制 ============
@staticmethod
async def copy(
db: AsyncSession,
page_id: str,
new_code: str,
new_name: str = None,
user_id: str = None
) -> PageMeta:
"""复制页面"""
source = await PageService.get(db, page_id)
# 检查新编码唯一性
stmt = select(PageMeta).where(
PageMeta.code == new_code,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
if result.scalar_one_or_none():
raise PageServiceException(f"页面编码已存在: {new_code}")
new_page = PageMeta(
application_id=source.application_id,
name=new_name or f"{source.name}_副本",
code=new_code,
category=source.category,
description=source.description,
status="draft",
version=1,
page_config=source.page_config,
sort=source.sort,
sys_creator_id=user_id,
sys_modifier_id=user_id,
)
db.add(new_page)
await db.commit()
await db.refresh(new_page)
logger.info(f"页面复制成功: {source.code} -> {new_code}")
return new_page
# ============ 导入/导出 ============
@staticmethod
async def export_config(db: AsyncSession, page_id: str) -> Dict[str, Any]:
"""导出页面配置"""
page = await PageService.get(db, page_id)
return {
"name": page.name,
"code": page.code,
"category": page.category,
"description": page.description,
"page_config": page.page_config,
}
@staticmethod
async def check_import(db: AsyncSession, code: str) -> Dict[str, Any]:
"""导入预检查:编码是否冲突"""
code_exists = False
if code:
stmt = select(PageMeta).where(
PageMeta.code == code,
PageMeta.is_deleted == False,
)
result = await db.execute(stmt)
code_exists = result.scalar_one_or_none() is not None
return {
"code_exists": code_exists,
"can_import": not code_exists,
}
@staticmethod
async def import_config(
db: AsyncSession,
data: Dict[str, Any],
user_id: str = None
) -> PageMeta:
"""导入页面配置"""
required_fields = ["name", "code"]
for field in required_fields:
if not data.get(field):
raise PageServiceException(f"缺少必要字段: {field}")
return await PageService.create(db, data, user_id)
# ============ 获取分类列表 ============
@staticmethod
async def get_categories(db: AsyncSession) -> List[str]:
"""获取所有分类"""
stmt = select(PageMeta.category).where(
PageMeta.is_deleted == False,
PageMeta.category != ""
).distinct()
result = await db.execute(stmt)
return [row[0] for row in result.fetchall()]
@@ -0,0 +1,35 @@
import { requestClient } from '#/api/request';
export interface FormMetaListItem {
id: string;
application_id?: string;
application_name?: string;
application_code?: string;
name: string;
code: string;
status: string;
}
export interface FormListParams {
page?: number;
pageSize?: number;
applicationId?: string;
name?: string;
code?: string;
status?: string;
includeGloballyVisible?: boolean;
}
interface FormPaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
export async function getFormListApi(params?: FormListParams) {
return requestClient.get<FormPaginatedResponse<FormMetaListItem>>(
'/api/ai/forms/list',
{ params },
);
}
@@ -54,6 +54,72 @@ const components: Record<string, any> = {
),
// 循环节点
loop: defineAsyncComponent(() => import('../panels/LoopPanel.vue')),
form_basic_info: defineAsyncComponent(
() => import('../panels/FormBasicInfoPanel.vue'),
),
form_database_design: defineAsyncComponent(
() => import('../panels/FormDatabaseDesignPanel.vue'),
),
form_database_create: defineAsyncComponent(
() => import('../panels/FormDatabaseCreatePanel.vue'),
),
form_ui_design: defineAsyncComponent(
() => import('../panels/FormUIDesignPanel.vue'),
),
form_list_design: defineAsyncComponent(
() => import('../panels/FormListDesignPanel.vue'),
),
form_create: defineAsyncComponent(
() => import('../panels/FormCreatePanel.vue'),
),
form_publish: defineAsyncComponent(
() => import('../panels/FormPublishPanel.vue'),
),
app_create: defineAsyncComponent(
() => import('../panels/AppCreatePanel.vue'),
),
app_design: defineAsyncComponent(
() => import('../panels/AppDesignPanel.vue'),
),
app_settings: defineAsyncComponent(
() => import('../panels/AppSettingsPanel.vue'),
),
app_update: defineAsyncComponent(
() => import('../panels/AppUpdatePanel.vue'),
),
dashboard_basic_info: defineAsyncComponent(
() => import('../panels/DashboardBasicInfoPanel.vue'),
),
dashboard_design: defineAsyncComponent(
() => import('../panels/DashboardDesignPanel.vue'),
),
dashboard_create: defineAsyncComponent(
() => import('../panels/DashboardCreatePanel.vue'),
),
dashboard_publish: defineAsyncComponent(
() => import('../panels/DashboardPublishPanel.vue'),
),
system_summary: defineAsyncComponent(
() => import('../panels/SystemSummaryPanel.vue'),
),
form_data_create: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_read: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_update: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_delete: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_list: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_schema_to_llm: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
// 子流程节点
subflow: defineAsyncComponent(() => import('../panels/SubflowPanel.vue')),
// Text-to-SQL 节点
@@ -19,6 +19,7 @@ import {
Bot,
Brain,
CircleHelp,
ClipboardCheck,
Clock,
Code,
Combine,
@@ -26,11 +27,14 @@ import {
Database,
DatabaseBackup,
DatabaseZap,
FilePlus,
FileText,
Fullscreen,
GitBranch,
GitFork,
Globe,
HelpCircle,
LayoutDashboard,
LayoutTemplate,
ListChecks,
Map,
@@ -44,10 +48,13 @@ import {
Save,
Search,
Send,
Settings,
Snowflake,
Square,
TableProperties,
Trash2,
Undo2,
Upload,
Workflow,
} from '@vben/icons';
import { $t } from '@vben/locales';
@@ -77,16 +84,32 @@ import ContextMenu from './components/ContextMenu.vue';
import Panel from './components/Panel.vue';
import VersionHistoryPanel from './components/VersionHistoryPanel.vue';
import AddButtonEdge from './edges/AddButtonEdge.vue';
import AppCreateNode from './nodes/AppCreateNode.vue';
import AppDesignNode from './nodes/AppDesignNode.vue';
import AppSettingsNode from './nodes/AppSettingsNode.vue';
import AppUpdateNode from './nodes/AppUpdateNode.vue';
import ChoiceNode from './nodes/ChoiceNode.vue';
import CodeNode from './nodes/CodeNode.vue';
import ConditionNode from './nodes/ConditionNode.vue';
import ConfirmNode from './nodes/ConfirmNode.vue';
import DashboardBasicInfoNode from './nodes/DashboardBasicInfoNode.vue';
import DashboardCreateNode from './nodes/DashboardCreateNode.vue';
import DashboardDesignNode from './nodes/DashboardDesignNode.vue';
import DashboardPublishNode from './nodes/DashboardPublishNode.vue';
import DbDeleteNode from './nodes/DbDeleteNode.vue';
import DbInsertNode from './nodes/DbInsertNode.vue';
import DbQueryNode from './nodes/DbQueryNode.vue';
import DbSqlNode from './nodes/DbSqlNode.vue';
import DbUpdateNode from './nodes/DbUpdateNode.vue';
import EndNode from './nodes/EndNode.vue';
import FormBasicInfoNode from './nodes/FormBasicInfoNode.vue';
import FormCreateNode from './nodes/FormCreateNode.vue';
import FormDatabaseCreateNode from './nodes/FormDatabaseCreateNode.vue';
import FormDatabaseDesignNode from './nodes/FormDatabaseDesignNode.vue';
import FormDataNode from './nodes/FormDataNode.vue';
import FormListDesignNode from './nodes/FormListDesignNode.vue';
import FormPublishNode from './nodes/FormPublishNode.vue';
import FormUIDesignNode from './nodes/FormUIDesignNode.vue';
import HttpNode from './nodes/HttpNode.vue';
import IntentNode from './nodes/IntentNode.vue';
import KnowledgeRetrievalNode from './nodes/KnowledgeRetrievalNode.vue';
@@ -100,6 +123,7 @@ import SnowflakeCortexAnalystNode from './nodes/SnowflakeCortexAnalystNode.vue';
import SnowflakeCortexLLMNode from './nodes/SnowflakeCortexLLMNode.vue';
import StartNode from './nodes/StartNode.vue';
import SubflowNode from './nodes/SubflowNode.vue';
import SystemSummaryNode from './nodes/SystemSummaryNode.vue';
import TemplateNode from './nodes/TemplateNode.vue';
import TextToSqlNode from './nodes/TextToSqlNode.vue';
@@ -160,6 +184,28 @@ const nodeTypes = {
snowflake_cortex_analyst: markRaw(SnowflakeCortexAnalystNode),
// 循环节点
loop: markRaw(LoopNode),
form_basic_info: markRaw(FormBasicInfoNode),
form_database_design: markRaw(FormDatabaseDesignNode),
form_database_create: markRaw(FormDatabaseCreateNode),
form_ui_design: markRaw(FormUIDesignNode),
form_list_design: markRaw(FormListDesignNode),
form_create: markRaw(FormCreateNode),
form_publish: markRaw(FormPublishNode),
app_create: markRaw(AppCreateNode),
app_design: markRaw(AppDesignNode),
app_settings: markRaw(AppSettingsNode),
app_update: markRaw(AppUpdateNode),
dashboard_basic_info: markRaw(DashboardBasicInfoNode),
dashboard_design: markRaw(DashboardDesignNode),
dashboard_create: markRaw(DashboardCreateNode),
dashboard_publish: markRaw(DashboardPublishNode),
system_summary: markRaw(SystemSummaryNode),
form_data_create: markRaw(FormDataNode),
form_data_read: markRaw(FormDataNode),
form_data_update: markRaw(FormDataNode),
form_data_delete: markRaw(FormDataNode),
form_data_list: markRaw(FormDataNode),
form_schema_to_llm: markRaw(FormDataNode),
// 子流程节点
subflow: markRaw(SubflowNode),
// Text-to-SQL 节点
@@ -339,6 +385,94 @@ const nodeCategories = computed(() => [
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.formDesign'),
nodes: [
{
type: 'form_basic_info',
label: getNodeLabel('form_basic_info'),
icon: 'FileText',
color: 'purple',
},
{
type: 'form_database_design',
label: getNodeLabel('form_database_design'),
icon: 'Database',
color: 'purple',
},
{
type: 'form_database_create',
label: getNodeLabel('form_database_create'),
icon: 'DatabaseZap',
color: 'purple',
},
{
type: 'form_ui_design',
label: getNodeLabel('form_ui_design'),
icon: 'LayoutTemplate',
color: 'purple',
},
{
type: 'form_list_design',
label: getNodeLabel('form_list_design'),
icon: 'TableProperties',
color: 'purple',
},
{
type: 'form_create',
label: getNodeLabel('form_create'),
icon: 'FilePlus',
color: 'purple',
},
{
type: 'form_publish',
label: getNodeLabel('form_publish'),
icon: 'Send',
color: 'purple',
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.formData'),
nodes: [
{
type: 'form_data_create',
label: getNodeLabel('form_data_create'),
icon: 'FilePlus',
color: 'green',
},
{
type: 'form_data_read',
label: getNodeLabel('form_data_read'),
icon: 'FileText',
color: 'blue',
},
{
type: 'form_data_update',
label: getNodeLabel('form_data_update'),
icon: 'FileText',
color: 'orange',
},
{
type: 'form_data_delete',
label: getNodeLabel('form_data_delete'),
icon: 'Trash2',
color: 'red',
},
{
type: 'form_data_list',
label: getNodeLabel('form_data_list'),
icon: 'Search',
color: 'blue',
},
{
type: 'form_schema_to_llm',
label: getNodeLabel('form_schema_to_llm'),
icon: 'Code',
color: 'purple',
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.dialog'),
nodes: [
@@ -473,6 +607,72 @@ const nodeCategories = computed(() => [
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.appManage'),
nodes: [
{
type: 'app_create',
label: getNodeLabel('app_create'),
icon: 'LayoutTemplate',
color: 'indigo',
},
{
type: 'app_design',
label: getNodeLabel('app_design'),
icon: 'LayoutDashboard',
color: 'indigo',
},
{
type: 'app_settings',
label: getNodeLabel('app_settings'),
icon: 'Settings',
color: 'indigo',
},
{
type: 'app_update',
label: getNodeLabel('app_update'),
icon: 'Save',
color: 'indigo',
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.dashboard'),
icon: 'LayoutDashboard',
color: 'cyan',
nodes: [
{
type: 'dashboard_basic_info',
label: getNodeLabel('dashboard_basic_info'),
icon: 'FileText',
color: 'cyan',
},
{
type: 'dashboard_design',
label: getNodeLabel('dashboard_design'),
icon: 'LayoutDashboard',
color: 'cyan',
},
{
type: 'dashboard_create',
label: getNodeLabel('dashboard_create'),
icon: 'FilePlus',
color: 'cyan',
},
{
type: 'dashboard_publish',
label: getNodeLabel('dashboard_publish'),
icon: 'Upload',
color: 'cyan',
},
{
type: 'system_summary',
label: getNodeLabel('system_summary'),
icon: 'ClipboardCheck',
color: 'cyan',
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.knowledge'),
nodes: [
@@ -515,12 +715,18 @@ const iconComponents: Record<string, any> = {
// 循环节点图标
RefreshCw,
CornerDownLeft,
FileText,
TableProperties,
LayoutDashboard,
Settings,
FilePlus,
Upload,
ClipboardCheck,
// 表单节点图标
Send,
// 应用节点图标
Save,
// 仪表盘节点图标
FilePlus,
// 系统总结节点图标
// 子流程节点图标
Workflow,
@@ -532,6 +738,18 @@ const iconComponents: Record<string, any> = {
const filteredNodeCategories = computed(() => {
let categories = nodeCategories.value;
if (workflow.value?.workflow_type === 'general') {
const excludeNames = new Set([
$t('ai-platform.workflow.editor.categories.appManage'),
$t('ai-platform.workflow.editor.categories.dashboard'),
$t('ai-platform.workflow.editor.categories.formDesign'),
]);
categories = nodeCategories.value.filter(
(category: { name: string; nodes: any[] }) =>
!excludeNames.has(category.name),
);
}
// 再根据搜索关键词过滤
if (!nodeSearchQuery.value.trim()) {
return categories;
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { LayoutTemplate } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.app_create.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="app_create"
>
<template #icon><LayoutTemplate class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.app_create.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div v-if="data.name" class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.app_create.appName') }}</span>
<span class="max-w-[120px] truncate">{{ data.name }}</span>
</div>
<div v-if="data.code" class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.app_create.appCode') }}</span>
<span class="font-mono">{{ data.code }}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_create.updateIfExists')
}}</span>
<span>{{
data.update_if_exists
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">app_id, app_code</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { LayoutDashboard } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.app_design.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="app_design"
>
<template #icon><LayoutDashboard class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.app_design.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div v-if="data.design_title" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_design.designTitle')
}}</span>
<span class="max-w-[120px] truncate">{{ data.design_title }}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_design.needConfirm')
}}</span>
<span>{{
data.require_confirmation !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">design_content</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { Settings } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.app_settings.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="app_settings"
>
<template #icon><Settings class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.app_settings.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div v-if="data.app_name" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_settings.appName')
}}</span>
<span class="max-w-[120px] truncate">{{ data.app_name }}</span>
</div>
<div v-if="data.app_layout" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_settings.layoutMode')
}}</span>
<span>{{ data.app_layout }}</span>
</div>
<div v-if="data.theme_builtin_type" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_settings.builtinTheme')
}}</span>
<span>{{ data.theme_builtin_type }}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_settings.needConfirm')
}}</span>
<span>{{
data.require_confirmation !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">settings</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { Save } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.app_update.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="app_update"
>
<template #icon><Save class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.app_update.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div v-if="data.settings" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.app_update.settingsSource')
}}</span>
<span class="max-w-[120px] truncate font-mono">{{
data.settings
}}</span>
</div>
<div v-if="data.application_id" class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.app_update.app') }}</span>
<span class="max-w-[120px] truncate">{{ data.application_id }}</span>
</div>
<div v-else class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.app_update.target') }}</span>
<span>{{ $t('ai-platform.workflow.nodes.app_update.mainApp') }}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">update_success, config_id</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,58 @@
<script setup lang="ts">
import { FileText } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps<{
data: any;
id: string;
selected: boolean;
}>();
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="
data.label || $t('ai-platform.workflow.nodes.dashboard_basic_info.label')
"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="dashboard_basic_info"
>
<template #icon><FileText class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.dashboard_basic_info.desc') }}
</template>
<div class="space-y-1 text-xs">
<div v-if="data.name" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_basic_info.name')
}}</span>
{{ data.name }}
</div>
<div v-if="data.code" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_basic_info.code')
}}</span>
{{ data.code }}
</div>
<div v-if="data.category" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_basic_info.category')
}}</span>
{{ data.category }}
</div>
</div>
<template #footer>
<div class="text-muted-foreground truncate text-[10px]">
{{ $t('ai-platform.workflow.nodes.common.output') }}
dashboard_basic_info
</div>
</template>
</BaseNode>
</template>
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { FilePlus } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps<{
data: any;
id: string;
selected: boolean;
}>();
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="
data.label || $t('ai-platform.workflow.nodes.dashboard_create.label')
"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="dashboard_create"
>
<template #icon><FilePlus class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.dashboard_create.desc') }}
</template>
<div class="space-y-1 text-xs">
<div
v-if="data.dashboard_basic_info"
class="text-muted-foreground truncate"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_create.basicInfo')
}}</span>
{{ $t('ai-platform.workflow.nodes.common.configured') }}
</div>
<div v-if="data.page_config" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_create.pageConfig')
}}</span>
{{ $t('ai-platform.workflow.nodes.common.configured') }}
</div>
<div class="text-muted-foreground">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_create.updateIfExists')
}}</span>
{{
data.update_if_exists !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}
</div>
</div>
<template #footer>
<div class="text-muted-foreground truncate text-[10px]">
{{ $t('ai-platform.workflow.nodes.common.output') }} dashboard_id,
dashboard_code
</div>
</template>
</BaseNode>
</template>
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { LayoutDashboard } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps<{
data: any;
id: string;
selected: boolean;
}>();
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="
data.label || $t('ai-platform.workflow.nodes.dashboard_design.label')
"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="dashboard_design"
>
<template #icon><LayoutDashboard class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.dashboard_design.desc') }}
</template>
<div class="space-y-1 text-xs">
<div v-if="data.design_title" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_design.title')
}}</span>
{{ data.design_title }}
</div>
<div class="text-muted-foreground">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_design.needConfirm')
}}</span>
{{
data.require_confirmation !== false
? $t('ai-platform.workflow.nodes.dashboard_design.need')
: $t('ai-platform.workflow.nodes.dashboard_design.notNeed')
}}
</div>
</div>
<template #footer>
<div class="text-muted-foreground truncate text-[10px]">
{{ $t('ai-platform.workflow.nodes.common.output') }} page_config
</div>
</template>
</BaseNode>
</template>
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { Upload } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps<{
data: any;
id: string;
selected: boolean;
}>();
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="
data.label || $t('ai-platform.workflow.nodes.dashboard_publish.label')
"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="dashboard_publish"
>
<template #icon><Upload class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.dashboard_publish.desc') }}
</template>
<div class="space-y-1 text-xs">
<div v-if="data.dashboard_id" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_publish.dashboard')
}}</span>
{{ data.dashboard_id }}
</div>
<div v-if="data.menu_name" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_publish.menuName')
}}</span>
{{ data.menu_name }}
</div>
<div v-if="data.menu_icon" class="text-muted-foreground truncate">
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.dashboard_publish.icon')
}}</span>
{{ data.menu_icon }}
</div>
</div>
<template #footer>
<div class="text-muted-foreground truncate text-[10px]">
{{ $t('ai-platform.workflow.nodes.common.output') }} menu_id, route_path
</div>
</template>
</BaseNode>
</template>
@@ -0,0 +1,75 @@
<script setup lang="ts">
import { FileText } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="
data.label || $t('ai-platform.workflow.nodes.form_basic_info.label')
"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_basic_info"
>
<template #icon><FileText class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_basic_info.desc') }}
</template>
<div class="space-y-2">
<div
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_basic_info.formName')
}}</span>
<span class="max-w-[120px] truncate">{{
data.name || $t('ai-platform.workflow.nodes.common.notConfigured')
}}</span>
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_basic_info.formCode')
}}</span>
<span class="font-mono">{{
data.code ||
(data.auto_generate_code
? $t('ai-platform.workflow.nodes.form_basic_info.autoGenerate')
: '-')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_basic_info.formType')
}}</span>
<span>{{
data.form_type === 'workflow'
? $t('ai-platform.workflow.nodes.form_basic_info.workflow')
: $t('ai-platform.workflow.nodes.form_basic_info.normal')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">form_basic_info</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { FilePlus } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.form_create.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_create"
>
<template #icon><FilePlus class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_create.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_create.updateIfExists')
}}</span>
<span>{{
data.update_if_exists !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">form_id, form_code</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,189 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Code, FilePlus, FileText, Search, Trash2 } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
const props = defineProps<{
data: any;
id: string;
selected?: boolean;
type: string;
}>();
//
const nodeInfo = computed(() => {
switch (props.type) {
case 'form_data_create': {
return {
label: $t('ai-platform.workflow.nodes.form_data.create'),
icon: FilePlus,
desc: $t('ai-platform.workflow.nodes.form_data.createDesc'),
};
}
case 'form_data_delete': {
return {
label: $t('ai-platform.workflow.nodes.form_data.delete'),
icon: Trash2,
desc: $t('ai-platform.workflow.nodes.form_data.deleteDesc'),
};
}
case 'form_data_list': {
return {
label: $t('ai-platform.workflow.nodes.form_data.list'),
icon: Search,
desc: $t('ai-platform.workflow.nodes.form_data.listDesc'),
};
}
case 'form_data_read': {
return {
label: $t('ai-platform.workflow.nodes.form_data.read'),
icon: FileText,
desc: $t('ai-platform.workflow.nodes.form_data.readDesc'),
};
}
case 'form_data_update': {
return {
label: $t('ai-platform.workflow.nodes.form_data.update'),
icon: FileText,
desc: $t('ai-platform.workflow.nodes.form_data.updateDesc'),
};
}
case 'form_schema_to_llm': {
return {
label: $t('ai-platform.workflow.nodes.form_data.schemaToLLM'),
icon: Code,
desc: $t('ai-platform.workflow.nodes.form_data.schemaToLLMDesc'),
};
}
default: {
return {
label: $t('ai-platform.workflow.nodes.form_data.label'),
icon: FileText,
desc: $t('ai-platform.workflow.nodes.form_data.desc'),
};
}
}
});
const label = computed(() => props.data?.label || nodeInfo.value.label);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="label"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
:node-type="type"
>
<template #icon>
<component :is="nodeInfo.icon" class="size-5" />
</template>
<template #desc>{{ nodeInfo.desc }}</template>
<div class="space-y-2">
<!-- 表单编码 -->
<div
v-if="type !== 'form_schema_to_llm'"
class="flex items-center justify-between rounded bg-blue-50 px-2 py-1 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_data.formCode')
}}</span>
<span class="font-mono text-xs">{{
data.form_code ||
$t('ai-platform.workflow.nodes.common.notConfigured')
}}</span>
</div>
<!-- 表单转LLM特殊显示 -->
<div
v-if="type === 'form_schema_to_llm'"
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_data.formCode')
}}</span>
<span class="font-mono text-xs">{{
data.form_code ||
$t('ai-platform.workflow.nodes.common.notConfigured')
}}</span>
</div>
<!-- 配置信息 -->
<div class="text-muted-foreground space-y-1 text-xs">
<div v-if="type === 'form_data_read'" class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.form_data.recordId') }}</span>
<span>{{
data.record_id
? $t('ai-platform.workflow.nodes.common.configured')
: '-'
}}</span>
</div>
<div v-if="type === 'form_data_update'" class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.form_data.recordId') }}</span>
<span>{{
data.record_id
? $t('ai-platform.workflow.nodes.common.configured')
: '-'
}}</span>
</div>
<div v-if="type === 'form_data_update'" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_data.updateData')
}}</span>
<span>{{
data.data ? $t('ai-platform.workflow.nodes.common.configured') : '-'
}}</span>
</div>
<div v-if="type === 'form_data_create'" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_data.createData')
}}</span>
<span>{{
data.data ? $t('ai-platform.workflow.nodes.common.configured') : '-'
}}</span>
</div>
<div v-if="type === 'form_data_delete'" class="flex justify-between">
<span>{{ $t('ai-platform.workflow.nodes.form_data.recordId') }}</span>
<span>{{
data.record_id
? $t('ai-platform.workflow.nodes.common.configured')
: '-'
}}</span>
</div>
<div v-if="type === 'form_data_list'" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_data.filterCondition')
}}</span>
<span>{{
data.filters
? $t('ai-platform.workflow.nodes.common.configured')
: '-'
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{
$t('ai-platform.workflow.nodes.common.outputVariable')
}}</span>
<span class="font-mono">{{
data.output_variable ||
$t('ai-platform.workflow.nodes.common.notConfigured')
}}</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { DatabaseZap } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.form_db_create.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_database_create"
>
<template #icon><DatabaseZap class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_db_create.desc') }}
</template>
<div class="space-y-2">
<div
class="flex items-center justify-between rounded bg-green-50 px-2 py-1 text-green-700 dark:bg-green-950 dark:text-green-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_db_create.operation')
}}</span>
<span>{{
$t('ai-platform.workflow.nodes.form_db_create.createTable')
}}</span>
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_db_create.tableExists')
}}</span>
<span>{{
data.if_exists === 'replace'
? $t('ai-platform.workflow.nodes.form_db_create.dropRecreate')
: data.if_exists === 'error'
? $t('ai-platform.workflow.nodes.form_db_create.error')
: $t('ai-platform.workflow.nodes.form_db_create.skip')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_db_create.autoCreateSchema')
}}</span>
<span>{{
data.create_schema_if_not_exists !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">creation_result</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { Database } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.form_db_design.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_database_design"
>
<template #icon><Database class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_db_design.desc') }}
</template>
<div class="space-y-2">
<div
class="flex items-center justify-between rounded bg-blue-50 px-2 py-1 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_db_design.database')
}}</span>
<span>{{ data.db_config || 'default' }}</span>
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_db_design.mainTable')
}}</span>
<span>{{
data.main_table
? $t('ai-platform.workflow.nodes.common.configured')
: $t('ai-platform.workflow.nodes.common.notConfigured')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_db_design.subTable')
}}</span>
<span>{{
data.sub_tables
? $t('ai-platform.workflow.nodes.common.configured')
: $t('ai-platform.workflow.nodes.form_db_design.noSubTable')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_db_design.systemFields')
}}</span>
<span>{{
data.auto_add_system_fields !== false
? $t('ai-platform.workflow.nodes.form_db_design.autoAdd')
: $t('ai-platform.workflow.nodes.form_db_design.noAdd')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">database_design</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,86 @@
<script setup lang="ts">
import { TableProperties } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="
data.label || $t('ai-platform.workflow.nodes.form_list_design.label')
"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_list_design"
>
<template #icon><TableProperties class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_list_design.desc') }}
</template>
<div class="space-y-2">
<div
class="flex items-center justify-between rounded bg-indigo-50 px-2 py-1 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_list_design.containerType')
}}</span>
<span>{{
data.container_type === 'drawer'
? $t('ai-platform.workflow.nodes.form_list_design.drawer')
: data.container_type === 'dialog'
? $t('ai-platform.workflow.nodes.form_list_design.dialog')
: data.container_type === 'page'
? $t('ai-platform.workflow.nodes.form_list_design.page')
: $t('ai-platform.workflow.nodes.form_list_design.drawer')
}}</span>
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_list_design.pageSize')
}}</span>
<span>{{ data.page_size || 20 }}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_list_design.autoQueryFields')
}}</span>
<span>{{
data.auto_query_fields !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_list_design.autoColumns')
}}</span>
<span>{{
data.auto_columns !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">list_config</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { Send } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.form_publish.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_publish"
>
<template #icon><Send class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_publish.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_publish.menuName')
}}</span>
<span class="max-w-[120px] truncate">{{
data.menu_name ||
$t('ai-platform.workflow.nodes.form_publish.notSet')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_publish.menuIcon')
}}</span>
<span>{{ data.menu_icon || 'lucide:file-text' }}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">menu_id, route_path</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { LayoutTemplate } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.form_ui_design.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="form_ui_design"
>
<template #icon><LayoutTemplate class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.form_ui_design.desc') }}
</template>
<div class="space-y-2">
<div
class="flex items-center justify-between rounded bg-purple-50 px-2 py-1 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
>
<span class="font-medium">{{
$t('ai-platform.workflow.nodes.form_ui_design.layoutMode')
}}</span>
<span>{{
data.layout_mode === 'auto'
? $t('ai-platform.workflow.nodes.form_ui_design.auto')
: data.layout_mode === 'single'
? $t('ai-platform.workflow.nodes.form_ui_design.singleCol')
: data.layout_mode === 'double'
? $t('ai-platform.workflow.nodes.form_ui_design.doubleCol')
: data.layout_mode === 'triple'
? $t('ai-platform.workflow.nodes.form_ui_design.tripleCol')
: $t('ai-platform.workflow.nodes.form_ui_design.auto')
}}</span>
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_ui_design.formName')
}}</span>
<span>{{
data.form_name ||
$t('ai-platform.workflow.nodes.form_ui_design.auto')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_ui_design.enableGrouping')
}}</span>
<span>{{
data.enable_grouping !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.form_ui_design.hideSystemFields')
}}</span>
<span>{{
data.hide_system_fields !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
</div>
<!-- 输出变量 -->
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">form_ui_design</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ClipboardCheck } from '@vben/icons';
import { $t } from '@vben/locales';
import BaseNode from './BaseNode.vue';
defineProps(['selected', 'data', 'id']);
</script>
<template>
<BaseNode
:selected="selected"
:execution-status="data.executionStatus"
:execution-result="data.executionResult"
:label="data.label || $t('ai-platform.workflow.nodes.system_summary.label')"
:inputs="[{ id: 'target' }]"
:outputs="[{ id: 'source' }]"
:node-id="id"
node-type="system_summary"
>
<template #icon><ClipboardCheck class="size-5" /></template>
<template #desc>
{{ $t('ai-platform.workflow.nodes.system_summary.desc') }}
</template>
<div class="space-y-2">
<div class="text-muted-foreground space-y-1 text-xs">
<div v-if="data.summary_title" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.system_summary.title')
}}</span>
<span class="max-w-[120px] truncate">{{ data.summary_title }}</span>
</div>
<div class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.system_summary.showStatistics')
}}</span>
<span>{{
data.show_statistics !== false
? $t('ai-platform.workflow.nodes.common.yes')
: $t('ai-platform.workflow.nodes.common.no')
}}</span>
</div>
<div v-if="data.app_info" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.system_summary.appInfo')
}}</span>
<span class="text-green-600 dark:text-green-400">{{
$t('ai-platform.workflow.nodes.common.configured')
}}</span>
</div>
<div v-if="data.form_results" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.system_summary.formResult')
}}</span>
<span class="text-green-600 dark:text-green-400">{{
$t('ai-platform.workflow.nodes.common.configured')
}}</span>
</div>
<div v-if="data.dashboard_info" class="flex justify-between">
<span>{{
$t('ai-platform.workflow.nodes.system_summary.dashboard')
}}</span>
<span class="text-green-600 dark:text-green-400">{{
$t('ai-platform.workflow.nodes.common.configured')
}}</span>
</div>
</div>
<div class="rounded bg-green-50 px-2 py-1 text-xs dark:bg-green-950">
<div
class="flex items-center justify-between text-green-700 dark:text-green-300"
>
<span>{{ $t('ai-platform.workflow.nodes.common.output') }}</span>
<span class="font-mono">summary</span>
</div>
</div>
</div>
</BaseNode>
</template>
@@ -0,0 +1,189 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElOption,
ElSelect,
ElSwitch,
} from 'element-plus';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.appCreate.label'),
name: '',
code: '',
description: '',
icon: '',
app_type: 'mixed',
update_if_exists: false,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="$t('ai-platform.workflow.panels.appCreate.placeholder')"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.appCreate.appNameOutput')
}}</span>
<span class="text-red-500">*</span>
</div>
</template>
<SmartInput
v-model="form.name"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.appCreate.appNamePlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appCreate.appNameHint')
}}<code v-pre>{{llm-1.app_name}}</code>
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.appCreate.appCodeOutput')
}}</span>
<span class="text-red-500">*</span>
</div>
</template>
<SmartInput
v-model="form.code"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.appCreate.appCodePlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appCreate.appCodeHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.appCreate.appDesc')">
<SmartInput
v-model="form.description"
:current-node-id="nodeId"
type="textarea"
:rows="3"
:placeholder="
$t('ai-platform.workflow.panels.appCreate.appDescPlaceholder')
"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.appCreate.appIcon')">
<SmartInput
v-model="form.icon"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.appCreate.appIconPlaceholder')
"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.appCreate.appType')">
<ElSelect
v-model="form.app_type"
:placeholder="
$t('ai-platform.workflow.panels.appCreate.appTypePlaceholder')
"
class="w-full"
>
<ElOption
:label="$t('ai-platform.workflow.panels.appCreate.mixedApp')"
value="mixed"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.appCreate.formApp')"
value="form"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.appCreate.workflowApp')"
value="workflow"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.appCreate.dashboardApp')"
value="dashboard"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.appCreate.screenApp')"
value="screen"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.appCreate.updateIfExists')"
>
<ElSwitch v-model="form.update_if_exists" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appCreate.updateIfExistsHint') }}
</div>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">app_id</span>
<span>{{
$t('ai-platform.workflow.panels.appCreate.createdAppId')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">app_code</span>
<span>{{
$t('ai-platform.workflow.panels.appCreate.appCodeOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">app_name</span>
<span>{{
$t('ai-platform.workflow.panels.appCreate.appNameOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">success</span>
<span>{{
$t('ai-platform.workflow.panels.appCreate.successOutput')
}}</span>
</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,114 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput } from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.appDesign.label'),
design_content: '',
design_title: $t(
'ai-platform.workflow.panels.appDesign.designTitlePlaceholder',
),
require_confirmation: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="$t('ai-platform.workflow.panels.appDesign.placeholder')"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.appDesign.designContent')
}}</span>
<span class="text-red-500">*</span>
</div>
</template>
<SmartInput
v-model="form.design_content"
:current-node-id="nodeId"
type="textarea"
:rows="6"
:placeholder="
$t('ai-platform.workflow.panels.appDesign.designContentPlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appDesign.designContentHint')
}}<code v-pre>{{llm-1.output}}</code>
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.appDesign.designTitle')"
>
<SmartInput
v-model="form.design_title"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.appDesign.designTitlePlaceholder')
"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">design_content</span>
<span>{{
$t('ai-platform.workflow.panels.appDesign.designContentOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">design_title</span>
<span>{{
$t('ai-platform.workflow.panels.appDesign.designTitleOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">confirmed</span>
<span>{{
$t('ai-platform.workflow.panels.appDesign.confirmedOutput')
}}</span>
</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,196 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput, ElOption, ElSelect } from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.appSettings.label'),
app_name: '',
home_path: '',
logo_source: '',
theme_builtin_type: 'default',
theme_color_primary: '',
app_layout: 'sidebar-nav',
require_confirmation: true,
...props.data,
});
//
const layoutOptions = [
{
label: $t('ai-platform.appSettings.layoutOptions.sidebar-nav'),
value: 'sidebar-nav',
},
{
label: $t('ai-platform.appSettings.layoutOptions.sidebar-mixed-nav'),
value: 'sidebar-mixed-nav',
},
{
label: $t('ai-platform.appSettings.layoutOptions.header-nav'),
value: 'header-nav',
},
{
label: $t('ai-platform.appSettings.layoutOptions.mixed-nav'),
value: 'mixed-nav',
},
{
label: $t('ai-platform.appSettings.layoutOptions.full-content'),
value: 'full-content',
},
];
//
const themeOptions = [
{
label: $t('ai-platform.appSettings.themeOptions.default'),
value: 'default',
},
{ label: $t('ai-platform.appSettings.themeOptions.violet'), value: 'violet' },
{ label: $t('ai-platform.appSettings.themeOptions.pink'), value: 'pink' },
{ label: $t('ai-platform.appSettings.themeOptions.rose'), value: 'rose' },
{ label: $t('ai-platform.appSettings.themeOptions.sky'), value: 'sky' },
{ label: $t('ai-platform.appSettings.themeOptions.cyan'), value: 'cyan' },
{ label: $t('ai-platform.appSettings.themeOptions.green'), value: 'green' },
{ label: $t('ai-platform.appSettings.themeOptions.orange'), value: 'orange' },
{ label: $t('ai-platform.appSettings.themeOptions.yellow'), value: 'yellow' },
{ label: $t('ai-platform.appSettings.themeOptions.zinc'), value: 'zinc' },
{
label: $t('ai-platform.appSettings.themeOptions.neutral'),
value: 'neutral',
},
{ label: $t('ai-platform.appSettings.themeOptions.slate'), value: 'slate' },
{ label: $t('ai-platform.appSettings.themeOptions.gray'), value: 'gray' },
{
label: $t('ai-platform.appSettings.themeOptions.deep-blue'),
value: 'deep-blue',
},
{
label: $t('ai-platform.appSettings.themeOptions.deep-green'),
value: 'deep-green',
},
];
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="$t('ai-platform.workflow.panels.appSettings.placeholder')"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.appSettings.appName')">
<SmartInput
v-model="form.app_name"
:current-node-id="nodeId"
:placeholder="$t('ai-platform.appSettings.appNamePlaceholder')"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appSettings.varRefHint') }}
<code v-pre>{{llm-1.output}}</code>
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.appSettings.homePath')">
<SmartInput
v-model="form.home_path"
:current-node-id="nodeId"
placeholder="e.g. /dashboard"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appSettings.homePathHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.appSettings.logoSource')">
<SmartInput
v-model="form.logo_source"
:current-node-id="nodeId"
:placeholder="$t('ai-platform.appSettings.logoSourcePlaceholder')"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appSettings.logoHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.appSettings.builtinTheme')">
<ElSelect v-model="form.theme_builtin_type" class="w-full">
<ElOption
v-for="item in themeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.appSettings.primaryColor')">
<SmartInput
v-model="form.theme_color_primary"
:current-node-id="nodeId"
placeholder="e.g. hsl(212 100% 45%)"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appSettings.primaryColorHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.appSettings.layout')">
<ElSelect v-model="form.app_layout" class="w-full">
<ElOption
v-for="item in layoutOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">settings</span>
<span>{{
$t('ai-platform.workflow.panels.appSettings.settingsOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">confirmed</span>
<span>{{
$t('ai-platform.workflow.panels.appSettings.confirmedOutput')
}}</span>
</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput } from 'element-plus';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.appUpdate.label'),
settings: '',
application_id: '',
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="$t('ai-platform.workflow.panels.appUpdate.placeholder')"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.appUpdate.appSettingsLabel')
}}</span>
<span class="text-red-500">*</span>
</div>
</template>
<SmartInput
v-model="form.settings"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.appUpdate.appSettingsPlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appUpdate.appSettingsHint')
}}<code v-pre>{{app_settings-1.settings}}</code>
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.appUpdate.appId')">
<SmartInput
v-model="form.application_id"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.appUpdate.appIdPlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.appUpdate.appIdHint')
}}<code v-pre>{{ application_id }}</code>
</div>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">update_success</span>
<span>{{
$t('ai-platform.workflow.panels.appUpdate.updateSuccess')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">config_id</span>
<span>{{
$t('ai-platform.workflow.panels.appUpdate.configId')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">update_message</span>
<span>{{
$t('ai-platform.workflow.panels.appUpdate.updateMessage')
}}</span>
</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,164 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElOption,
ElSelect,
} from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{
data: any;
nodeId: string;
}>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.label'),
name: '',
code: '',
category: 'dashboard',
description: '',
sort: 0,
require_confirmation: true,
...props.data,
});
watch(
form,
(newVal) => {
emit('update', newVal);
},
{ deep: true },
);
const categoryOptions = [
{
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catDashboard'),
value: 'dashboard',
},
{
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catPortal'),
value: 'portal',
},
{
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catDataboard'),
value: 'databoard',
},
{
label: $t('ai-platform.workflow.panels.dashboardBasicInfo.catOther'),
value: 'other',
},
];
</script>
<template>
<div class="space-y-4">
<ElForm :model="form" label-position="top" size="small">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.dashboardBasicInfo.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.dashboardBasicInfo.dashboardName')
"
required
>
<SmartInput
v-model="form.name"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardBasicInfo.dashboardNamePlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.dashboardBasicInfo.dashboardCode')
"
>
<SmartInput
v-model="form.code"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardBasicInfo.dashboardCodePlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardBasicInfo.category')"
>
<ElSelect
v-model="form.category"
class="w-full"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardBasicInfo.categoryPlaceholder',
)
"
>
<ElOption
v-for="opt in categoryOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.dashboardBasicInfo.description')
"
>
<SmartInput
v-model="form.description"
type="textarea"
:placeholder="
$t('ai-platform.workflow.panels.dashboardBasicInfo.descPlaceholder')
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardBasicInfo.sort')"
>
<ElInputNumber
v-model="form.sort"
:min="0"
:max="9999"
class="w-full"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
</ElForm>
</div>
</template>
@@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput, ElSwitch } from 'element-plus';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{
data: any;
nodeId: string;
}>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.dashboardCreate.label'),
dashboard_basic_info: '',
page_config: '',
application_id: '',
update_if_exists: true,
...props.data,
});
watch(
form,
(newVal) => {
emit('update', newVal);
},
{ deep: true },
);
</script>
<template>
<div class="space-y-4">
<ElForm :model="form" label-position="top" size="small">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.dashboardCreate.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardCreate.basicInfo')"
required
>
<SmartInput
v-model="form.dashboard_basic_info"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardCreate.basicInfoPlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardCreate.pageConfig')"
required
>
<SmartInput
v-model="form.page_config"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardCreate.pageConfigPlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardCreate.appId')"
>
<SmartInput
v-model="form.application_id"
:placeholder="
$t('ai-platform.workflow.panels.dashboardCreate.appIdPlaceholder')
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.dashboardCreate.updateIfExists')
"
>
<ElSwitch v-model="form.update_if_exists" />
<span class="text-muted-foreground ml-2 text-xs">
{{
$t('ai-platform.workflow.panels.dashboardCreate.updateIfExistsHint')
}}
</span>
</ElFormItem>
</ElForm>
<div class="bg-muted/50 rounded-lg p-3">
<div class="text-muted-foreground text-xs">
<p class="mb-2 font-medium">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}:
</p>
<ul class="list-inside list-disc space-y-1">
<li>
<code>dashboard_id</code> -
{{
$t(
'ai-platform.workflow.panels.dashboardCreate.dashboardIdOutput',
)
}}
</li>
<li>
<code>dashboard_code</code> -
{{
$t(
'ai-platform.workflow.panels.dashboardCreate.dashboardCodeOutput',
)
}}
</li>
<li>
<code>page_meta</code> -
{{
$t('ai-platform.workflow.panels.dashboardCreate.pageMetaOutput')
}}
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -0,0 +1,119 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput } from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{
data: any;
nodeId: string;
}>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.dashboardDesign.label'),
dashboard_code: '',
design_title: $t('ai-platform.workflow.panels.dashboardDesign.designTitle'),
design_suggestion: '',
require_confirmation: true,
...props.data,
});
watch(
form,
(newVal) => {
emit('update', newVal);
},
{ deep: true },
);
</script>
<template>
<div class="space-y-4">
<ElForm :model="form" label-position="top" size="small">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.dashboardDesign.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardDesign.dashboardCode')"
>
<SmartInput
v-model="form.dashboard_code"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardDesign.dashboardCodePlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.dashboardDesign.designTitleLabel')
"
>
<ElInput
v-model="form.design_title"
:placeholder="
$t('ai-platform.workflow.panels.dashboardDesign.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.dashboardDesign.designSuggestion')
"
>
<SmartInput
v-model="form.design_suggestion"
type="textarea"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardDesign.designSuggestionPlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
</ElForm>
<div class="bg-muted/50 rounded-lg p-3">
<div class="text-muted-foreground text-xs">
<p class="mb-2 font-medium">
{{ $t('ai-platform.workflow.panels.common.usage') }}:
</p>
<ul class="list-inside list-disc space-y-1">
<li>
{{ $t('ai-platform.workflow.panels.dashboardDesign.usageTip1') }}
</li>
<li>
{{ $t('ai-platform.workflow.panels.dashboardDesign.usageTip2') }}
</li>
<li>
{{ $t('ai-platform.workflow.panels.dashboardDesign.usageTip3') }}
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -0,0 +1,165 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput, ElInputNumber } from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{
data: any;
nodeId: string;
}>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.dashboardPublish.label'),
dashboard_id: '',
menu_name: '',
menu_parent_id: '',
menu_icon: 'lucide:layout-dashboard',
menu_order: 0,
application_id: '',
require_confirmation: true,
...props.data,
});
watch(
form,
(newVal) => {
emit('update', newVal);
},
{ deep: true },
);
</script>
<template>
<div class="space-y-4">
<ElForm :model="form" label-position="top" size="small">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.dashboardPublish.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardPublish.dashboardId')"
required
>
<SmartInput
v-model="form.dashboard_id"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardPublish.dashboardIdPlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardPublish.menuName')"
required
>
<SmartInput
v-model="form.menu_name"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardPublish.menuNamePlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardPublish.parentMenuId')"
>
<SmartInput
v-model="form.menu_parent_id"
:placeholder="
$t(
'ai-platform.workflow.panels.dashboardPublish.parentMenuIdPlaceholder',
)
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardPublish.menuIcon')"
>
<ElInput
v-model="form.menu_icon"
placeholder="lucide:layout-dashboard"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardPublish.menuSort')"
>
<ElInputNumber
v-model="form.menu_order"
:min="0"
:max="9999"
class="w-full"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.dashboardPublish.appId')"
>
<SmartInput
v-model="form.application_id"
:placeholder="
$t('ai-platform.workflow.panels.dashboardPublish.appIdPlaceholder')
"
:current-node-id="nodeId"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
</ElForm>
<div class="bg-muted/50 rounded-lg p-3">
<div class="text-muted-foreground text-xs">
<p class="mb-2 font-medium">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}:
</p>
<ul class="list-inside list-disc space-y-1">
<li>
<code>menu_id</code> -
{{
$t('ai-platform.workflow.panels.dashboardPublish.menuIdOutput')
}}
</li>
<li>
<code>route_path</code> -
{{
$t('ai-platform.workflow.panels.dashboardPublish.routePathOutput')
}}
</li>
<li>
<code>publish_result</code> -
{{
$t(
'ai-platform.workflow.panels.dashboardPublish.publishResultOutput',
)
}}
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -0,0 +1,206 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElOption,
ElSelect,
} from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formBasicInfo.label'),
name: '',
code: '',
form_type: 'normal',
description: '',
sort: 0,
auto_generate_code: false,
require_confirmation: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.formBasicInfo.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formBasicInfo.formName')"
required
>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.formName')
}}</span>
<span class="text-red-500">*</span>
</div>
</template>
<SmartInput
v-model="form.name"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formBasicInfo.formNamePlaceholder') +
' {{llm_basic_name}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formBasicInfo.formNameHint')
}}{{ llm_basic_name }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formBasicInfo.formCode')"
>
<SmartInput
v-model="form.code"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formBasicInfo.formCodePlaceholder') +
' {{llm_basic_code}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formBasicInfo.formCodeHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formBasicInfo.autoGenCode')"
>
<ElSwitch v-model="form.auto_generate_code" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formBasicInfo.autoGenCodeHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formBasicInfo.formType')"
>
<ElSelect
v-model="form.form_type"
:placeholder="
$t('ai-platform.workflow.panels.formBasicInfo.formTypePlaceholder')
"
>
<ElOption
:label="$t('ai-platform.workflow.panels.formBasicInfo.normalForm')"
value="normal"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formBasicInfo.workflowForm')"
value="workflow"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formBasicInfo.formDesc')"
>
<SmartInput
v-model="form.description"
:current-node-id="nodeId"
type="textarea"
:rows="3"
:placeholder="
$t('ai-platform.workflow.panels.formBasicInfo.formDescPlaceholder')
"
/>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.formBasicInfo.sort')">
<ElInputNumber
v-model="form.sort"
:min="0"
:placeholder="
$t('ai-platform.workflow.panels.formBasicInfo.sortPlaceholder')
"
class="w-full"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formBasicInfo.sortHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">form_basic_info</span>
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.fullConfigOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_name</span>
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.formNameOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_code</span>
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.formCodeOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_type</span>
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.formTypeOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_description</span>
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.formDescOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_sort</span>
<span>{{
$t('ai-platform.workflow.panels.formBasicInfo.sortOutput')
}}</span>
</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,228 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput, ElSwitch } from 'element-plus';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formCreate.label'),
form_basic_info: '',
database_design: '',
form_ui_design: '',
list_config: '',
application_id: '',
update_if_exists: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="$t('ai-platform.workflow.panels.formCreate.placeholder')"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formCreate.formBasicInfo')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(object)</span>
</div>
</template>
<SmartInput
v-model="form.form_basic_info"
:current-node-id="nodeId"
type="textarea"
:rows="2"
placeholder="{{form_basic_info.form_basic_info}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formCreate.basicInfoHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formCreate.dbDesignConfig')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(object)</span>
</div>
</template>
<SmartInput
v-model="form.database_design"
:current-node-id="nodeId"
type="textarea"
:rows="2"
placeholder="{{form_database_design.database_design}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formCreate.dbDesignHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formCreate.uiDesignConfig')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(object)</span>
</div>
</template>
<SmartInput
v-model="form.form_ui_design"
:current-node-id="nodeId"
type="textarea"
:rows="2"
placeholder="{{form_ui_design.form_ui_design}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formCreate.uiDesignHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formCreate.listDesignConfig')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(object)</span>
</div>
</template>
<SmartInput
v-model="form.list_config"
:current-node-id="nodeId"
type="textarea"
:rows="2"
placeholder="{{form_list_design.list_config}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formCreate.listDesignHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{ $t('ai-platform.workflow.panels.formCreate.appId') }}</span>
<span class="text-muted-foreground text-xs">{{
$t('ai-platform.workflow.panels.llm.optional')
}}</span>
</div>
</template>
<SmartInput
v-model="form.application_id"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formCreate.appIdPlaceholder')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formCreate.updateIfExists')"
>
<ElSwitch v-model="form.update_if_exists" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formCreate.updateIfExistsHint') }}
</div>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">form_id</span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.formIdOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_code</span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.formCodeOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_meta</span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.formMetaOutput')
}}</span>
</div>
</div>
</div>
<!-- 工作流说明 -->
<div
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
>
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
{{ $t('ai-platform.workflow.panels.formCreate.fullWorkflow') }}
</div>
<div class="text-muted-foreground text-xs">
<div class="flex items-center gap-1">
<span>{{
$t('ai-platform.workflow.panels.formCreate.formBasicInfo')
}}</span>
<span></span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.dbDesign')
}}</span>
<span></span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.dbCreate')
}}</span>
</div>
<div class="mt-1 flex items-center gap-1">
<span></span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.uiDesign')
}}</span>
<span></span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.listDesign')
}}</span>
<span></span>
<span class="font-medium text-blue-600 dark:text-blue-400">{{
$t('ai-platform.workflow.panels.formCreate.formCreateStep')
}}</span>
<span></span>
<span>{{
$t('ai-platform.workflow.panels.formCreate.formPublish')
}}</span>
</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,308 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElOption,
ElRadioButton,
ElRadioGroup,
ElSelect,
ElSwitch,
} from 'element-plus';
import { getFormListApi } from '#/api/ai-platform/form-manager-lite';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{
data: any;
nodeId: string;
nodeType: string;
}>();
const emit = defineEmits(['update']);
//
const getDefaultLabel = () => {
switch (props.nodeType) {
case 'form_data_create': {
return $t('ai-platform.workflow.panels.formData.createLabel');
}
case 'form_data_delete': {
return $t('ai-platform.workflow.panels.formData.deleteLabel');
}
case 'form_data_list': {
return $t('ai-platform.workflow.panels.formData.listLabel');
}
case 'form_data_read': {
return $t('ai-platform.workflow.panels.formData.readLabel');
}
case 'form_data_update': {
return $t('ai-platform.workflow.panels.formData.updateLabel');
}
case 'form_schema_to_llm': {
return $t('ai-platform.workflow.panels.formData.schemaToLlmLabel');
}
default: {
return $t('ai-platform.workflow.panels.formData.label');
}
}
};
const getDefaultOutputVariable = () => {
switch (props.nodeType) {
case 'form_data_create': {
return 'form_data_create_result';
}
case 'form_data_delete': {
return 'form_data_delete_result';
}
case 'form_data_list': {
return 'form_data_list';
}
case 'form_data_read': {
return 'form_data';
}
case 'form_data_update': {
return 'form_data_update_result';
}
case 'form_schema_to_llm': {
return 'llm_output_schema';
}
default: {
return 'form_data_result';
}
}
};
const form = ref({
label: getDefaultLabel(),
form_code: '',
id: '',
data: '',
page: 1,
page_size: 20,
filters: '',
sort: [],
hard_delete: false,
is_array: false,
include_fields: [],
exclude_fields: [],
output_variable: getDefaultOutputVariable(),
...props.data,
});
// variable select
const formInputMode = ref<'select' | 'variable'>('variable');
//
const formList = ref<any[]>([]);
const formLoading = ref(false);
//
const loadFormList = async () => {
try {
formLoading.value = true;
const res = await getFormListApi({
page: 1,
pageSize: 100,
status: 'published',
includeGloballyVisible: true,
});
formList.value = res.items || [];
} catch (error) {
console.error('加载表单列表失败:', error);
} finally {
formLoading.value = false;
}
};
onMounted(() => {
loadFormList();
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
//
const showField = (field: string) => {
const nodeType = props.nodeType;
switch (field) {
case 'data': {
return ['form_data_create', 'form_data_update'].includes(nodeType);
}
case 'exclude_fields':
case 'include_fields':
case 'is_array': {
return nodeType === 'form_schema_to_llm';
}
case 'filters':
case 'page':
case 'page_size':
case 'sort': {
return nodeType === 'form_data_list';
}
case 'hard_delete': {
return nodeType === 'form_data_delete';
}
case 'id': {
return ['form_data_delete', 'form_data_read', 'form_data_update'].includes(nodeType);
}
default: {
return true;
}
}
};
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput v-model="form.label" :placeholder="getDefaultLabel()" />
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.formCode')">
<div class="flex items-center gap-2 mb-2">
<ElRadioGroup v-model="formInputMode" size="small">
<ElRadioButton value="variable">{{ $t('ai-platform.workflow.panels.formData.variableInput') }}</ElRadioButton>
<ElRadioButton value="select">{{ $t('ai-platform.workflow.panels.formData.selectFromList') }}</ElRadioButton>
</ElRadioGroup>
</div>
<!-- 变量输入模式 -->
<template v-if="formInputMode === 'variable'">
<SmartInput
v-model="form.form_code"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formData.formCodeInputPlaceholder') +
' {{form_code}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.formCodeInputHint') }}
</div>
</template>
<!-- 下拉选择模式 -->
<template v-else>
<ElSelect
v-model="form.form_code"
:placeholder="$t('ai-platform.workflow.panels.formData.selectFormPlaceholder')"
filterable
:loading="formLoading"
class="w-full"
>
<ElOption
v-for="f in formList"
:key="f.code"
:label="`${f.name} (${f.code})`"
:value="f.code"
/>
</ElSelect>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.selectedFormHint', { code: form.form_code || '-' }) }}
</div>
</template>
</ElFormItem>
<!-- 记录ID读取/更新/删除 -->
<ElFormItem v-if="showField('id')" :label="$t('ai-platform.workflow.panels.formData.recordId')">
<SmartInput
v-model="form.id"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formData.recordIdPlaceholder') +
' {{record_id}}'
"
/>
</ElFormItem>
<!-- 数据创建/更新 -->
<ElFormItem v-if="showField('data')">
<template #label>
<div class="flex items-center gap-2">
<span>{{ $t('ai-platform.workflow.panels.formData.data') }}</span>
<span class="text-muted-foreground text-xs">(JSON)</span>
</div>
</template>
<SmartInput
v-model="form.data"
:current-node-id="nodeId"
type="textarea"
:rows="6"
:placeholder="$t('ai-platform.workflow.panels.formData.dataPlaceholder') + ' {{llm_response}}'"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.dataHint') }}
</div>
</ElFormItem>
<!-- 分页列表查询 -->
<template v-if="showField('page')">
<div class="flex gap-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.pageNum')" class="flex-1">
<ElInputNumber v-model="form.page" :min="1" class="w-full" />
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.pageSize')" class="flex-1">
<ElInputNumber v-model="form.page_size" :min="1" :max="1000" class="w-full" />
</ElFormItem>
</div>
</template>
<!-- 过滤条件列表查询 -->
<ElFormItem v-if="showField('filters')">
<template #label>
<div class="flex items-center gap-2">
<span>{{ $t('ai-platform.workflow.panels.formData.filterConditions') }}</span>
<span class="text-muted-foreground text-xs">(JSON)</span>
</div>
</template>
<SmartInput
v-model="form.filters"
:current-node-id="nodeId"
type="textarea"
:rows="4"
placeholder="{&quot;status&quot;: &quot;active&quot;, &quot;name&quot;: {&quot;type&quot;: &quot;like&quot;, &quot;value&quot;: &quot;Zhang&quot;}}"
/>
</ElFormItem>
<!-- 硬删除删除 -->
<ElFormItem v-if="showField('hard_delete')">
<div class="flex items-center gap-2">
<ElSwitch v-model="form.hard_delete" />
<span class="text-sm">{{ $t('ai-platform.workflow.panels.formData.hardDelete') }}</span>
</div>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.hardDeleteHint') }}
</div>
</ElFormItem>
<!-- 数组模式表单转LLM结构 -->
<ElFormItem v-if="showField('is_array')">
<div class="flex items-center gap-2">
<ElSwitch v-model="form.is_array" />
<span class="text-sm">{{ $t('ai-platform.workflow.panels.formData.generateArray') }}</span>
</div>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.generateArrayHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.outputVarName')">
<ElInput v-model="form.output_variable" :placeholder="getDefaultOutputVariable()" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.outputVarHint') }}
</div>
</ElFormItem>
</ElForm>
</template>
@@ -0,0 +1,206 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElAlert,
ElForm,
ElFormItem,
ElInput,
ElOption,
ElSelect,
ElSwitch,
} from 'element-plus';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formDbCreate.label'),
database_design: '',
db_database: '',
db_schema: '',
if_exists: 'skip',
create_schema_if_not_exists: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.formDbCreate.placeholder')
"
/>
</ElFormItem>
<ElAlert type="warning" :closable="false" show-icon class="mb-4">
<template #title>
<span class="text-sm">{{
$t('ai-platform.workflow.panels.formDbCreate.cautionWarning')
}}</span>
</template>
</ElAlert>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formDbCreate.dbDesignConfig')"
required
>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbCreate.dbDesignConfig')
}}</span>
<span class="text-red-500">*</span>
</div>
</template>
<SmartInput
v-model="form.database_design"
:current-node-id="nodeId"
type="textarea"
:rows="4"
:placeholder="
$t(
'ai-platform.workflow.panels.formDbCreate.dbDesignConfigPlaceholder',
) + ' {{database_design}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbCreate.dbDesignConfigHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formDbCreate.database')"
>
<SmartInput
v-model="form.db_database"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formDbCreate.databasePlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbCreate.databaseHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.formDbCreate.schema')">
<SmartInput
v-model="form.db_schema"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formDbCreate.schemaPlaceholder') +
' {{schema_name}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbCreate.schemaHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formDbCreate.ifExists')"
>
<ElSelect
v-model="form.if_exists"
:placeholder="
$t('ai-platform.workflow.panels.formDbCreate.ifExistsPlaceholder')
"
>
<ElOption
:label="
$t('ai-platform.workflow.panels.formDbCreate.skipRecommended')
"
value="skip"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formDbCreate.throwError')"
value="error"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formDbCreate.replaceDanger')"
value="replace"
/>
</ElSelect>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbCreate.ifExistsHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formDbCreate.autoCreateSchema')"
>
<ElSwitch v-model="form.create_schema_if_not_exists" />
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formDbCreate.autoCreateSchemaHint')
}}
</div>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">creation_result</span>
<span>{{
$t('ai-platform.workflow.panels.formDbCreate.resultOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">all_tables_ready</span>
<span>{{
$t('ai-platform.workflow.panels.formDbCreate.allReadyOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">created_count</span>
<span>{{
$t('ai-platform.workflow.panels.formDbCreate.successCountOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">error_count</span>
<span>{{
$t('ai-platform.workflow.panels.formDbCreate.errorCountOutput')
}}</span>
</div>
</div>
</div>
<!-- 警告提示 -->
<div
class="mt-4 rounded border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-950"
>
<div class="mb-2 font-medium text-orange-700 dark:text-orange-300">
{{ $t('ai-platform.workflow.panels.formDbCreate.notes') }}
</div>
<ul class="text-muted-foreground list-disc space-y-1 pl-4 text-xs">
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note1') }}</li>
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note2') }}</li>
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note3') }}</li>
<li>{{ $t('ai-platform.workflow.panels.formDbCreate.note4') }}</li>
</ul>
</div>
</ElForm>
</template>
@@ -0,0 +1,360 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElRadioButton,
ElRadioGroup,
ElSwitch,
} from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import DbConnectionSelect from '../components/DbConnectionSelect.vue';
import DbDatabaseSelect from '../components/DbDatabaseSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formDbDesign.label'),
design_mode: 'main', // 'main' or 'sub'
//
table_name: '',
fields: '',
//
sub_table_name: '',
sub_fields: '',
parent_table: '',
foreign_key: '',
//
db_config: 'default',
db_database: '',
db_schema: '',
db_type: 'postgresql',
auto_add_system_fields: true,
require_confirmation: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
const resolvedDbTypeLabel = computed(() => form.value.db_type || 'postgresql');
function handleConnectionChange(payload: {
database?: string;
dbName: string;
dbType: string;
}) {
const connectionChanged = form.value.db_config !== payload.dbName;
form.value.db_config = payload.dbName;
form.value.db_type = payload.dbType;
if (connectionChanged) {
form.value.db_database = payload.database || '';
}
}
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.formDbDesign.placeholder')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formDbDesign.designMode')"
>
<ElRadioGroup v-model="form.design_mode">
<ElRadioButton value="main">
{{ $t('ai-platform.workflow.panels.formDbDesign.mainTable') }}
</ElRadioButton>
<ElRadioButton value="sub">
{{ $t('ai-platform.workflow.panels.formDbDesign.subTable') }}
</ElRadioButton>
</ElRadioGroup>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.designModeHint') }}
</div>
</ElFormItem>
<!-- 主表配置 -->
<template v-if="form.design_mode === 'main'">
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.tableName')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.table_name"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formDbDesign.tableNamePlaceholder') +
' {{llm_table_name}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.tableNameHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.fieldList')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(array)</span>
</div>
</template>
<SmartInput
v-model="form.fields"
:current-node-id="nodeId"
type="textarea"
:rows="8"
:placeholder="
$t('ai-platform.workflow.panels.formDbDesign.fieldListPlaceholder') +
' {{llm_fields}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.fieldListHint') }}
</div>
</ElFormItem>
</template>
<!-- 从表配置 -->
<template v-else>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.subTableName')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.sub_table_name"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formDbDesign.subTableNamePlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.subTableNameHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.relatedMainTable')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.parent_table"
:current-node-id="nodeId"
:placeholder="
$t(
'ai-platform.workflow.panels.formDbDesign.relatedMainTablePlaceholder',
)
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formDbDesign.relatedMainTableHint')
}}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.foreignKey')
}}</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.foreign_key"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formDbDesign.foreignKeyPlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.foreignKeyHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.fieldList')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(array)</span>
</div>
</template>
<SmartInput
v-model="form.sub_fields"
:current-node-id="nodeId"
type="textarea"
:rows="8"
:placeholder="
$t(
'ai-platform.workflow.panels.formDbDesign.subFieldListPlaceholder',
)
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.subFieldListHint') }}
</div>
</ElFormItem>
</template>
<DbConnectionSelect
v-model="form.db_config"
write-mode
@change="handleConnectionChange"
/>
<DbDatabaseSelect
v-model="form.db_database"
:db-name="form.db_config"
:db-type="form.db_type"
:current-node-id="nodeId"
/>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.dbSchema')
}}</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.db_schema"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formDbDesign.dbSchemaPlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.dbSchemaHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.formDbDesign.dbType')">
<ElInput :model-value="resolvedDbTypeLabel" readonly />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formDbDesign.dbTypeHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="
$t('ai-platform.workflow.panels.formDbDesign.autoAddSystemFields')
"
>
<ElSwitch v-model="form.auto_add_system_fields" />
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formDbDesign.autoAddSystemFieldsHint')
}}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">database_design</span>
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.fullDesignOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">table_name</span>
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.tableNameOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">field_count</span>
<span>{{
$t('ai-platform.workflow.panels.formDbDesign.fieldCountOutput')
}}</span>
</div>
</div>
</div>
<!-- 字段配置示例 -->
<div
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
>
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
{{ $t('ai-platform.workflow.panels.formDbDesign.fieldConfigExample') }}
</div>
<pre class="text-muted-foreground overflow-x-auto text-[10px]">
[
{
"name": "name",
"type": "varchar",
"maxLength": 100,
"comment": "Name",
"nullable": false
},
{
"name": "age",
"type": "int",
"comment": "Age",
"nullable": true
}
]</pre
>
</div>
</ElForm>
</template>
@@ -0,0 +1,286 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElOption,
ElSelect,
ElSwitch,
} from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formListDesign.label'),
form_ui_design: '',
database_design: '',
auto_query_fields: true,
auto_columns: true,
container_type: 'drawer',
page_size: 20,
show_index: true,
show_selection: true,
enable_export: true,
enable_import: false,
require_confirmation: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.formListDesign.placeholder')
"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formListDesign.uiDesignConfig')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(object)</span>
</div>
</template>
<SmartInput
v-model="form.form_ui_design"
:current-node-id="nodeId"
type="textarea"
:rows="3"
:placeholder="
$t(
'ai-platform.workflow.panels.formListDesign.uiDesignConfigPlaceholder',
) + ' {{form_ui_design.form_ui_design}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formListDesign.uiDesignConfigHint')
}}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formListDesign.dbDesignConfig')
}}</span>
<span class="text-muted-foreground text-xs"
>(object,
{{ $t('ai-platform.workflow.panels.llm.optional') }})</span
>
</div>
</template>
<SmartInput
v-model="form.database_design"
:current-node-id="nodeId"
type="textarea"
:rows="2"
:placeholder="
$t(
'ai-platform.workflow.panels.formListDesign.dbDesignConfigPlaceholder',
) + ' {{form_database_design.database_design}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formListDesign.dbDesignConfigHint')
}}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.autoGenQuery')"
>
<ElSwitch v-model="form.auto_query_fields" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formListDesign.autoGenQueryHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.autoGenColumns')"
>
<ElSwitch v-model="form.auto_columns" />
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formListDesign.autoGenColumnsHint')
}}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.containerType')"
>
<ElSelect
v-model="form.container_type"
:placeholder="
$t(
'ai-platform.workflow.panels.formListDesign.containerTypePlaceholder',
)
"
>
<ElOption
:label="$t('ai-platform.workflow.panels.formListDesign.drawer')"
value="drawer"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formListDesign.dialog')"
value="dialog"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formListDesign.page')"
value="page"
/>
</ElSelect>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formListDesign.containerTypeHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.pageSize')"
>
<ElSelect
v-model="form.page_size"
:placeholder="
$t('ai-platform.workflow.panels.formListDesign.pageSizePlaceholder')
"
>
<ElOption
:label="
$t('ai-platform.workflow.panels.formListDesign.perPage', {
count: 10,
})
"
:value="10"
/>
<ElOption
:label="
$t('ai-platform.workflow.panels.formListDesign.perPage', {
count: 20,
})
"
:value="20"
/>
<ElOption
:label="
$t('ai-platform.workflow.panels.formListDesign.perPage', {
count: 50,
})
"
:value="50"
/>
<ElOption
:label="
$t('ai-platform.workflow.panels.formListDesign.perPage', {
count: 100,
})
"
:value="100"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.showIndex')"
>
<ElSwitch v-model="form.show_index" />
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.showSelection')"
>
<ElSwitch v-model="form.show_selection" />
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.enableExport')"
>
<ElSwitch v-model="form.enable_export" />
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formListDesign.enableImport')"
>
<ElSwitch v-model="form.enable_import" />
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">list_config</span>
<span>{{
$t('ai-platform.workflow.panels.formListDesign.fullConfigOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">query_field_count</span>
<span>{{
$t(
'ai-platform.workflow.panels.formListDesign.queryFieldCountOutput',
)
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">column_count</span>
<span>{{
$t('ai-platform.workflow.panels.formListDesign.columnCountOutput')
}}</span>
</div>
</div>
</div>
<!-- 智能推断说明 -->
<div
class="mt-4 rounded border border-indigo-200 bg-indigo-50 p-3 dark:border-indigo-800 dark:bg-indigo-950"
>
<div class="mb-2 font-medium text-indigo-700 dark:text-indigo-300">
{{ $t('ai-platform.workflow.panels.formListDesign.inferRules') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div> {{ $t('ai-platform.workflow.panels.formListDesign.inferRule1') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formListDesign.inferRule2') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formListDesign.inferRule3') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formListDesign.inferRule4') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formListDesign.inferRule5') }}</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { ElForm, ElFormItem, ElInput } from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formPublish.label'),
form_id: '',
form_code: '',
require_confirmation: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="$t('ai-platform.workflow.panels.formPublish.placeholder')"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formPublish.formId')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.form_id"
:current-node-id="nodeId"
placeholder="{{form_create.form_id}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formPublish.formIdHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formPublish.formCode')
}}</span>
<span class="text-muted-foreground text-xs">{{
$t('ai-platform.workflow.panels.llm.optional')
}}</span>
</div>
</template>
<SmartInput
v-model="form.form_code"
:current-node-id="nodeId"
placeholder="{{form_create.form_code}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formPublish.formCodeHint') }}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">menu_id</span>
<span>{{
$t('ai-platform.workflow.panels.formPublish.menuIdOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">route_path</span>
<span>{{
$t('ai-platform.workflow.panels.formPublish.routePathOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">publish_result</span>
<span>{{
$t('ai-platform.workflow.panels.formPublish.publishResultOutput')
}}</span>
</div>
</div>
</div>
<!-- 工作流说明 -->
<div
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
>
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
{{ $t('ai-platform.workflow.panels.common.usage') }}
</div>
<div class="text-muted-foreground text-xs">
<p>{{ $t('ai-platform.workflow.panels.formPublish.usageTip1') }}</p>
<p class="mt-1">
{{ $t('ai-platform.workflow.panels.formPublish.usageTip2') }}
</p>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,248 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElOption,
ElSelect,
ElSwitch,
} from 'element-plus';
import ConfirmModeSelect from '../components/ConfirmModeSelect.vue';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.formUIDesign.label'),
database_design: '',
form_name: '',
form_code: '',
layout_mode: 'auto',
label_width: 120,
enable_grouping: true,
hide_system_fields: true,
require_confirmation: true,
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top" size="small" class="space-y-4">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.formUIDesign.placeholder')
"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.dbDesignConfig')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(object)</span>
</div>
</template>
<SmartInput
v-model="form.database_design"
:current-node-id="nodeId"
type="textarea"
:rows="3"
:placeholder="
$t(
'ai-platform.workflow.panels.formUIDesign.dbDesignConfigPlaceholder',
) + ' {{form_database_design.database_design}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formUIDesign.dbDesignConfigHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.formName')
}}</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.form_name"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formUIDesign.formNamePlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formUIDesign.formNameHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.formCode')
}}</span>
<span class="text-muted-foreground text-xs">(string)</span>
</div>
</template>
<SmartInput
v-model="form.form_code"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formUIDesign.formCodePlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formUIDesign.formCodeHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutMode')"
>
<ElSelect
v-model="form.layout_mode"
:placeholder="
$t('ai-platform.workflow.panels.formUIDesign.layoutModePlaceholder')
"
>
<ElOption
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutAuto')"
value="auto"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutSingle')"
value="single"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutDouble')"
value="double"
/>
<ElOption
:label="$t('ai-platform.workflow.panels.formUIDesign.layoutTriple')"
value="triple"
/>
</ElSelect>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formUIDesign.layoutAutoHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formUIDesign.labelWidth')"
>
<ElInputNumber
v-model="form.label_width"
:min="60"
:max="200"
:step="10"
controls-position="right"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formUIDesign.labelWidthHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formUIDesign.enableGrouping')"
>
<ElSwitch v-model="form.enable_grouping" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formUIDesign.enableGroupingHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.formUIDesign.hideSystemFields')"
>
<ElSwitch v-model="form.hide_system_fields" />
<div class="text-muted-foreground mt-1 text-xs">
{{
$t('ai-platform.workflow.panels.formUIDesign.hideSystemFieldsHint')
}}
</div>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.common.confirmMode')">
<ConfirmModeSelect
v-model="form.require_confirmation"
:current-node-id="nodeId"
/>
</ElFormItem>
<!-- 输出变量说明 -->
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">form_ui_design</span>
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.fullConfigOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">form_code</span>
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.formCodeOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">field_count</span>
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.fieldCountOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">group_count</span>
<span>{{
$t('ai-platform.workflow.panels.formUIDesign.groupCountOutput')
}}</span>
</div>
</div>
</div>
<!-- 智能映射说明 -->
<div
class="mt-4 rounded border border-purple-200 bg-purple-50 p-3 dark:border-purple-800 dark:bg-purple-950"
>
<div class="mb-2 font-medium text-purple-700 dark:text-purple-300">
{{ $t('ai-platform.workflow.panels.formUIDesign.mappingRules') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div> {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule1') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule2') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule3') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule4') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule5') }}</div>
<div> {{ $t('ai-platform.workflow.panels.formUIDesign.mappingRule6') }}</div>
</div>
</div>
</ElForm>
</template>
@@ -0,0 +1,214 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ElForm, ElFormItem, ElInput, ElSwitch } from 'element-plus';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{ data: any; nodeId: string }>();
const emit = defineEmits(['update']);
const form = ref({
label: $t('ai-platform.workflow.panels.systemSummary.label'),
app_info: '',
form_results: '',
dashboard_info: '',
summary_title: $t(
'ai-platform.workflow.panels.systemSummary.summaryTitleDefault',
),
show_statistics: true,
base_url: '',
...props.data,
});
watch(
form,
(val) => {
emit('update', val);
},
{ deep: true },
);
</script>
<template>
<ElForm label-position="top">
<ElFormItem :label="$t('ai-platform.workflow.panels.common.nodeName')">
<ElInput
v-model="form.label"
:placeholder="
$t('ai-platform.workflow.panels.systemSummary.placeholder')
"
/>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.systemSummary.formResultList')
}}</span>
<span class="text-red-500">*</span>
<span class="text-muted-foreground text-xs">(array)</span>
</div>
</template>
<SmartInput
v-model="form.form_results"
:current-node-id="nodeId"
type="textarea"
:rows="2"
placeholder="{{loop.results}} or {{form_publish.publish_result}}"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.systemSummary.formResultListHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.systemSummary.appName')
}}</span>
<span class="text-muted-foreground text-xs">{{
$t('ai-platform.workflow.panels.llm.optional')
}}</span>
</div>
</template>
<SmartInput
v-model="form.app_info"
:current-node-id="nodeId"
placeholder="{{app_create.app_name}} or enter name directly"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.systemSummary.appNameHint') }}
</div>
</ElFormItem>
<ElFormItem>
<template #label>
<div class="flex items-center gap-2">
<span>{{
$t('ai-platform.workflow.panels.systemSummary.dashboardName')
}}</span>
<span class="text-muted-foreground text-xs">{{
$t('ai-platform.workflow.panels.llm.optional')
}}</span>
</div>
</template>
<SmartInput
v-model="form.dashboard_info"
:current-node-id="nodeId"
placeholder="{{dashboard_publish.menu_name}} or enter name directly"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.systemSummary.dashboardNameHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.systemSummary.summaryTitle')"
>
<SmartInput
v-model="form.summary_title"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.systemSummary.summaryTitleDefault')
"
/>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.systemSummary.showStats')"
>
<ElSwitch v-model="form.show_statistics" />
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.systemSummary.showStatsHint') }}
</div>
</ElFormItem>
<ElFormItem
:label="$t('ai-platform.workflow.panels.systemSummary.baseUrl')"
>
<SmartInput
v-model="form.base_url"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.systemSummary.baseUrlPlaceholder')
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.systemSummary.baseUrlHint') }}
</div>
</ElFormItem>
<div
class="mt-4 rounded border border-green-200 bg-green-50 p-3 dark:border-green-800 dark:bg-green-950"
>
<div class="mb-2 font-medium text-green-700 dark:text-green-300">
{{ $t('ai-platform.workflow.panels.common.outputVars') }}
</div>
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex justify-between">
<span class="font-mono">summary</span>
<span>{{
$t('ai-platform.workflow.panels.systemSummary.summaryDataOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">total_forms</span>
<span>{{
$t('ai-platform.workflow.panels.systemSummary.formCountOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">has_app</span>
<span>{{
$t('ai-platform.workflow.panels.systemSummary.hasAppOutput')
}}</span>
</div>
<div class="flex justify-between">
<span class="font-mono">has_dashboard</span>
<span>{{
$t('ai-platform.workflow.panels.systemSummary.hasDashboardOutput')
}}</span>
</div>
</div>
</div>
<div
class="mt-4 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950"
>
<div class="mb-2 font-medium text-blue-700 dark:text-blue-300">
{{ $t('ai-platform.workflow.panels.systemSummary.usageScenarios') }}
</div>
<div class="text-muted-foreground space-y-2 text-xs">
<div>
<strong
>{{
$t('ai-platform.workflow.panels.systemSummary.fullAppCreate')
}}:</strong
>
<div class="mt-1">
AppCreate Loop[Form] Dashboard SystemSummary
</div>
</div>
<div>
<strong
>{{
$t('ai-platform.workflow.panels.systemSummary.subAppCreate')
}}:</strong
>
<div class="mt-1">Loop[Form] SystemSummary</div>
</div>
<div>
<strong
>{{
$t('ai-platform.workflow.panels.systemSummary.singleFormCreate')
}}:</strong
>
<div class="mt-1">FormPublish SystemSummary</div>
</div>
</div>
</div>
</ElForm>
</template>