From de0b85f47130b46ae2829fa7804b59d6f4ffabd8 Mon Sep 17 00:00:00 2001 From: jingrow Date: Wed, 27 Aug 2025 05:14:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0AI=20Node=20Schema=E5=8F=8A?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=9A=84=E5=89=8D=E7=AB=AFschema=20builder?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=EF=BC=8C=E4=B8=8D=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pagetype/ai_node_schema/__init__.py | 0 .../pagetype/ai_node_schema/ai_node_schema.js | 303 ++++++ .../ai_node_schema/ai_node_schema.json | 49 + .../pagetype/ai_node_schema/ai_node_schema.py | 102 ++ .../ai_node_schema/test_ai_node_schema.py | 9 + .../js/schema_builder/SchemaBuilder.vue | 41 + .../components/EditableInput.vue | 104 ++ .../components/PropertyDialog.vue | 171 ++++ .../components/PropertyEditor.vue | 152 +++ .../components/PropertyItem.vue | 146 +++ .../components/PropertyProperties.vue | 207 ++++ .../components/SchemaCanvas.vue | 157 +++ .../schema_builder/components/SchemaField.vue | 165 ++++ .../components/SchemaSidebar.vue | 237 +++++ .../schema_builder/schema_builder.bundle.js | 892 ++++++++++++++++++ jcloud/public/js/schema_builder/store.js | 146 +++ 16 files changed, 2881 insertions(+) create mode 100644 jcloud/jcloud/pagetype/ai_node_schema/__init__.py create mode 100644 jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.js create mode 100644 jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.json create mode 100644 jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.py create mode 100644 jcloud/jcloud/pagetype/ai_node_schema/test_ai_node_schema.py create mode 100644 jcloud/public/js/schema_builder/SchemaBuilder.vue create mode 100644 jcloud/public/js/schema_builder/components/EditableInput.vue create mode 100644 jcloud/public/js/schema_builder/components/PropertyDialog.vue create mode 100644 jcloud/public/js/schema_builder/components/PropertyEditor.vue create mode 100644 jcloud/public/js/schema_builder/components/PropertyItem.vue create mode 100644 jcloud/public/js/schema_builder/components/PropertyProperties.vue create mode 100644 jcloud/public/js/schema_builder/components/SchemaCanvas.vue create mode 100644 jcloud/public/js/schema_builder/components/SchemaField.vue create mode 100644 jcloud/public/js/schema_builder/components/SchemaSidebar.vue create mode 100644 jcloud/public/js/schema_builder/schema_builder.bundle.js create mode 100644 jcloud/public/js/schema_builder/store.js diff --git a/jcloud/jcloud/pagetype/ai_node_schema/__init__.py b/jcloud/jcloud/pagetype/ai_node_schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.js b/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.js new file mode 100644 index 0000000..486bb97 --- /dev/null +++ b/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.js @@ -0,0 +1,303 @@ +// Copyright (c) 2025, Jingrow and contributors +// For license information, please see license.txt + +jingrow.ui.form.on("AI Node Schema", { + refresh(frm) { + console.log("AI Node Schema refresh called", frm); + + // 添加编辑节点Schema按钮到右上角 + if (frm.pg.node_type) { + frm.add_custom_button(__('编辑节点Schema'), function() { + open_schema_editor(frm); + }); + + frm.add_custom_button(__('保存到文件'), function() { + save_schema_to_file(frm); + }); + } + + console.log("AI Node Schema 按钮添加成功"); + }, + + node_type(frm) { + // 当节点类型改变时,自动加载对应的 schema + if (frm.pg.node_type && !frm.pg.node_schema) { + load_schema_for_node_type(frm, frm.pg.node_type); + } + } +}); + +function open_schema_editor(frm) { + // 检查是否有 node_type + if (!frm.pg.node_type) { + jingrow.msgprint(__("请先选择节点类型"), __("提示")); + return; + } + + // 创建 schema 编辑器对话框 + let d = new jingrow.ui.Dialog({ + title: __("编辑 {0} Schema", [frm.pg.node_type]), + size: "extra-large", + fields: [ + { + fieldtype: "HTML", + fieldname: "schema_editor", + options: ` +
+
+
+ ` + } + ], + primary_action_label: __("保存"), + primary_action: function() { + save_schema_from_editor(frm, d); + }, + secondary_action_label: __("关闭"), + secondary_action: function() { + d.hide(); + } + }); + + d.show(); + + // 初始化 Vue 编辑器 + setTimeout(() => { + init_vue_schema_editor(d, frm); + }, 100); +} + +function init_vue_schema_editor(dialog, frm) { + const container = dialog.fields_dict.schema_editor.$wrapper.find('#schema-builder')[0]; + + // 动态加载 Vue.js 和组件 + load_vue_js().then(() => { + // Vue.js 加载完成后,加载 schema builder 组件 + load_schema_builder_components().then(() => { + create_vue_app(container, frm, dialog); + }).catch(error => { + jingrow.msgprint(__("加载 Schema 编辑器失败: {0}", [error.message]), __("错误")); + }); + }).catch(error => { + jingrow.msgprint(__("加载 Vue.js 失败: {0}", [error.message]), __("错误")); + }); +} + +function load_vue_js() { + return new Promise((resolve, reject) => { + // 检查是否已经加载 + if (typeof Vue !== 'undefined' && typeof Pinia !== 'undefined') { + resolve(); + return; + } + + // 先加载 Vue.js + const vueScript = document.createElement('script'); + vueScript.src = 'https://unpkg.com/vue@3/dist/vue.global.js'; + vueScript.onload = () => { + // 然后加载 VueDemi + const vueDemiScript = document.createElement('script'); + vueDemiScript.src = 'https://unpkg.com/vue-demi@0.14.6/lib/index.iife.js'; + vueDemiScript.onload = () => { + // 最后加载 Pinia + const piniaScript = document.createElement('script'); + piniaScript.src = 'https://unpkg.com/pinia@2/dist/pinia.iife.js'; + piniaScript.onload = () => { + // 等待加载完成 + setTimeout(() => { + if (typeof Vue !== 'undefined' && typeof Pinia !== 'undefined') { + resolve(); + } else { + reject(new Error('Vue.js 或 Pinia 加载失败')); + } + }, 100); + }; + piniaScript.onerror = () => { + reject(new Error('无法加载 Pinia')); + }; + document.head.appendChild(piniaScript); + }; + vueDemiScript.onerror = () => { + reject(new Error('无法加载 VueDemi')); + }; + document.head.appendChild(vueDemiScript); + }; + vueScript.onerror = () => { + reject(new Error('无法加载 Vue.js')); + }; + document.head.appendChild(vueScript); + }); +} + +function create_vue_app(container, frm, dialog) { + // 创建 Vue 应用 + const { createApp } = Vue; + const { createPinia } = Pinia; + + const app = createApp({ + template: ` + + `, + components: { + SchemaBuilder: window.SchemaBuilder + }, + setup() { + const nodeType = frm.pg.node_type || ""; + + // 确保 initialSchema 是对象而不是字符串 + let initialSchema = {}; + if (frm.pg.node_schema) { + if (typeof frm.pg.node_schema === 'string') { + try { + initialSchema = JSON.parse(frm.pg.node_schema); + } catch (e) { + console.error('解析 node_schema 失败:', e); + initialSchema = {}; + } + } else if (typeof frm.pg.node_schema === 'object') { + initialSchema = frm.pg.node_schema; + } + } + + function handleSave(schemaData) { + // 保存时也需要转换为字符串 + frm.set_value('node_schema', JSON.stringify(schemaData, null, 2)); + frm.save().then(() => { + jingrow.msgprint(__("Schema 保存成功")); + dialog.hide(); + }); + } + + function handleChange(schemaData) { + // 实时更新表单数据 - 需要转换为字符串 + frm.set_value('node_schema', JSON.stringify(schemaData, null, 2)); + } + + return { + nodeType, + initialSchema, + handleSave, + handleChange + }; + } + }); + + // 使用 Pinia + app.use(createPinia()); + + // 挂载应用 + app.mount(container); + + // 保存应用引用到对话框 + dialog.vue_app = app; +} + +function load_schema_builder_components() { + return new Promise((resolve, reject) => { + // 检查是否已经加载 + if (window.SchemaBuilder && window.SchemaStore) { + resolve(); + return; + } + + // 动态加载组件(不使用 module 类型) + const script = document.createElement('script'); + script.src = '/assets/jcloud/js/schema_builder/schema_builder.bundle.js'; + script.onload = () => { + // 等待组件加载完成 + setTimeout(() => { + if (window.SchemaBuilder && window.SchemaStore) { + resolve(); + } else { + reject(new Error('组件加载失败')); + } + }, 100); + }; + script.onerror = () => { + reject(new Error('无法加载组件文件')); + }; + document.head.appendChild(script); + }); +} + +function save_schema_from_editor(frm, dialog) { + try { + // 如果使用 Vue 编辑器 + if (dialog.vue_app) { + // Vue 编辑器会自动保存,这里只需要关闭对话框 + dialog.hide(); + return; + } + + // 如果使用 JSON 编辑器 + if (dialog.json_editor) { + const schema_data = dialog.json_editor.getValue(); + frm.set_value('node_schema', schema_data); + frm.save().then(() => { + jingrow.msgprint(__("Schema 保存成功")); + dialog.hide(); + }); + } + } catch (e) { + jingrow.msgprint(__("保存失败: {0}", [e.message]), __("错误")); + } +} + +function load_schema_for_node_type(frm, node_type) { + jingrow.call('jingrow.ai.utils.node_schema.get_node_schema', { + node_type: node_type + }).then(schema_data => { + // 确保 schema_data 是对象 + let parsedSchema = {}; + if (typeof schema_data === 'string') { + try { + parsedSchema = JSON.parse(schema_data); + } catch (e) { + console.error('解析 schema 失败:', e); + parsedSchema = {}; + } + } else if (typeof schema_data === 'object') { + parsedSchema = schema_data; + } + + frm.set_value('node_schema', parsedSchema); + jingrow.msgprint(__("节点 Schema 加载成功")); + }).catch(e => { + jingrow.msgprint(__("加载失败: {0}", [e.message]), __("错误")); + }); +} + +function save_schema_to_file(frm) { + if (!frm.pg.node_type || !frm.pg.node_schema) { + jingrow.msgprint(__("请先选择节点类型并设置 Schema 数据"), __("提示")); + return; + } + + // 确保 schema_data 是对象 + let schemaData = frm.pg.node_schema; + if (typeof schemaData === 'string') { + try { + schemaData = JSON.parse(schemaData); + } catch (e) { + jingrow.msgprint(__("Schema 数据格式错误"), __("错误")); + return; + } + } + + // 使用现有的保存方法 + jingrow.call('jcloud.jcloud.pagetype.ai_node_schema.ai_node_schema.save_node_schema', { + node_type: frm.pg.node_type, + schema_data: schemaData + }).then(() => { + jingrow.msgprint(__("Schema 已保存到文件")); + }).catch(e => { + jingrow.msgprint(__("保存失败: {0}", [e.message]), __("错误")); + }); +} + +// 简化实现,移除复杂的对话框和状态指示器 diff --git a/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.json b/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.json new file mode 100644 index 0000000..2151d87 --- /dev/null +++ b/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-08-27 01:45:28.762568", + "engine": "InnoDB", + "field_order": [ + "node_type", + "node_schema" + ], + "fields": [ + { + "fieldname": "node_type", + "fieldtype": "Data", + "label": "Node Type" + }, + { + "fieldname": "node_schema", + "fieldtype": "JSON", + "label": "Node Schema" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-08-27 01:46:44.409389", + "modified_by": "Administrator", + "module": "Jcloud", + "name": "AI Node Schema", + "owner": "Administrator", + "pagetype": "PageType", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.py b/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.py new file mode 100644 index 0000000..62d336b --- /dev/null +++ b/jcloud/jcloud/pagetype/ai_node_schema/ai_node_schema.py @@ -0,0 +1,102 @@ +# Copyright (c) 2025, Jingrow and contributors +# For license information, please see license.txt + +import jingrow +from jingrow.model.document import Document + + +class AINodeSchema(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from jingrow.types import DF + + node_schema: DF.JSON | None + node_type: DF.Data | None + # end: auto-generated types + + def validate(self): + """验证 schema JSON 格式""" + if self.node_schema: + try: + import json + json.loads(json.dumps(self.node_schema)) + except Exception as e: + jingrow.throw(__("Schema JSON 格式错误: {0}", str(e))) + + def before_save(self): + """保存前验证""" + self.validate() + + @jingrow.whitelist(allow_guest=True) + def get_available_node_types(self): + """获取可用的节点类型""" + import os + import json + + node_types = [] + nodes_path = jingrow.get_app_path("jingrow", "ai", "pagetype", "ai_agent", "nodes") + + if os.path.exists(nodes_path): + for item in os.listdir(nodes_path): + item_path = os.path.join(nodes_path, item) + if os.path.isdir(item_path): + schema_file = os.path.join(item_path, f"{item}.json") + if os.path.exists(schema_file): + try: + with open(schema_file, 'r', encoding='utf-8') as f: + schema_data = json.load(f) + node_types.append({ + "value": item, + "label": schema_data.get("title", item), + "description": schema_data.get("description", "") + }) + except Exception as e: + jingrow.log_error("加载节点 schema 失败", f"节点: {item}, 错误: {str(e)}") + + return node_types + + @jingrow.whitelist(allow_guest=True) + def load_node_schema(self, node_type): + """加载指定节点类型的 schema""" + import os + import json + + schema_file = jingrow.get_app_path("jingrow", "ai", "pagetype", "ai_agent", "nodes", node_type, f"{node_type}.json") + + if os.path.exists(schema_file): + try: + with open(schema_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + jingrow.throw(__("加载节点 schema 失败: {0}", str(e))) + else: + jingrow.throw(__("节点类型 {0} 的 schema 文件不存在", node_type)) + + @jingrow.whitelist(allow_guest=True) + def save_node_schema(self, node_type, schema_data): + """保存节点 schema 到文件""" + import os + import json + + # 验证 schema 格式 + try: + json.loads(json.dumps(schema_data)) + except Exception as e: + jingrow.throw(__("Schema JSON 格式错误: {0}", str(e))) + + schema_file = jingrow.get_app_path("jingrow", "ai", "pagetype", "ai_agent", "nodes", node_type, f"{node_type}.json") + + try: + # 确保目录存在 + os.makedirs(os.path.dirname(schema_file), exist_ok=True) + + with open(schema_file, 'w', encoding='utf-8') as f: + json.dump(schema_data, f, ensure_ascii=False, indent=2) + + jingrow.msgprint(__("节点 schema 保存成功")) + except Exception as e: + jingrow.throw(__("保存节点 schema 失败: {0}", str(e))) diff --git a/jcloud/jcloud/pagetype/ai_node_schema/test_ai_node_schema.py b/jcloud/jcloud/pagetype/ai_node_schema/test_ai_node_schema.py new file mode 100644 index 0000000..81b7368 --- /dev/null +++ b/jcloud/jcloud/pagetype/ai_node_schema/test_ai_node_schema.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Jingrow and Contributors +# See license.txt + +# import jingrow +from jingrow.tests.utils import JingrowTestCase + + +class TestAINodeSchema(JingrowTestCase): + pass diff --git a/jcloud/public/js/schema_builder/SchemaBuilder.vue b/jcloud/public/js/schema_builder/SchemaBuilder.vue new file mode 100644 index 0000000..986e995 --- /dev/null +++ b/jcloud/public/js/schema_builder/SchemaBuilder.vue @@ -0,0 +1,41 @@ + + + + + \ No newline at end of file diff --git a/jcloud/public/js/schema_builder/components/EditableInput.vue b/jcloud/public/js/schema_builder/components/EditableInput.vue new file mode 100644 index 0000000..785149d --- /dev/null +++ b/jcloud/public/js/schema_builder/components/EditableInput.vue @@ -0,0 +1,104 @@ + + + + + \ No newline at end of file diff --git a/jcloud/public/js/schema_builder/components/PropertyDialog.vue b/jcloud/public/js/schema_builder/components/PropertyDialog.vue new file mode 100644 index 0000000..c16a825 --- /dev/null +++ b/jcloud/public/js/schema_builder/components/PropertyDialog.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/jcloud/public/js/schema_builder/components/PropertyEditor.vue b/jcloud/public/js/schema_builder/components/PropertyEditor.vue new file mode 100644 index 0000000..df40ba5 --- /dev/null +++ b/jcloud/public/js/schema_builder/components/PropertyEditor.vue @@ -0,0 +1,152 @@ + + + + + \ No newline at end of file diff --git a/jcloud/public/js/schema_builder/components/PropertyItem.vue b/jcloud/public/js/schema_builder/components/PropertyItem.vue new file mode 100644 index 0000000..01a37ae --- /dev/null +++ b/jcloud/public/js/schema_builder/components/PropertyItem.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/jcloud/public/js/schema_builder/components/PropertyProperties.vue b/jcloud/public/js/schema_builder/components/PropertyProperties.vue new file mode 100644 index 0000000..f60feba --- /dev/null +++ b/jcloud/public/js/schema_builder/components/PropertyProperties.vue @@ -0,0 +1,207 @@ + + + + + + + diff --git a/jcloud/public/js/schema_builder/components/SchemaCanvas.vue b/jcloud/public/js/schema_builder/components/SchemaCanvas.vue new file mode 100644 index 0000000..c38f604 --- /dev/null +++ b/jcloud/public/js/schema_builder/components/SchemaCanvas.vue @@ -0,0 +1,157 @@ + + + + + \ No newline at end of file diff --git a/jcloud/public/js/schema_builder/components/SchemaField.vue b/jcloud/public/js/schema_builder/components/SchemaField.vue new file mode 100644 index 0000000..7c81482 --- /dev/null +++ b/jcloud/public/js/schema_builder/components/SchemaField.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/jcloud/public/js/schema_builder/components/SchemaSidebar.vue b/jcloud/public/js/schema_builder/components/SchemaSidebar.vue new file mode 100644 index 0000000..6b12188 --- /dev/null +++ b/jcloud/public/js/schema_builder/components/SchemaSidebar.vue @@ -0,0 +1,237 @@ + + + + + \ No newline at end of file diff --git a/jcloud/public/js/schema_builder/schema_builder.bundle.js b/jcloud/public/js/schema_builder/schema_builder.bundle.js new file mode 100644 index 0000000..3d10e7f --- /dev/null +++ b/jcloud/public/js/schema_builder/schema_builder.bundle.js @@ -0,0 +1,892 @@ +// Schema Builder Bundle - 完全按照 Jingrow 表单构建器模式重新实现 + +// 检查依赖 +if (typeof Vue === 'undefined') { + console.error('Vue.js 未加载'); +} +if (typeof Pinia === 'undefined') { + console.error('Pinia 未加载'); +} + +// 使用全局 Vue 和 Pinia +const { createApp, ref, computed, nextTick, watch } = Vue; +const { createPinia, defineStore } = Pinia; + +// Schema Store +const useSchemaStore = defineStore("schema-builder-store", () => { + // State + const schema = ref({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + title: "AI 节点配置", + properties: {}, + required: [] + }); + + const properties = ref([]); + const selectedField = ref(null); + const preview = ref(false); + const readOnly = ref(false); + const dirty = ref(false); + + // Getters + const notUsingInput = computed(() => { + const activeElement = document.activeElement; + return !activeElement || + activeElement.readOnly || + activeElement.disabled || + (activeElement.tagName !== "INPUT" && activeElement.tagName !== "TEXTAREA"); + }); + + // Actions + function addProperty(type = "string") { + const key = `property_${Date.now()}`; + const newProperty = { + key, + type, + title: "新属性", + description: "", + required: false + }; + + properties.value.push(newProperty); + updateSchema(); + selectedField.value = key; + dirty.value = true; + } + + function removeProperty(key) { + const index = properties.value.findIndex(p => p.key === key); + if (index > -1) { + properties.value.splice(index, 1); + updateSchema(); + if (selectedField.value === key) { + selectedField.value = null; + } + dirty.value = true; + } + } + + function updateProperty(key, updatedProperty) { + const index = properties.value.findIndex(p => p.key === key); + if (index > -1) { + properties.value[index] = { ...properties.value[index], ...updatedProperty }; + updateSchema(); + dirty.value = true; + } + } + + function updateSchema() { + schema.value.properties = {}; + schema.value.required = []; + + properties.value.forEach(prop => { + schema.value.properties[prop.key] = { + type: prop.type, + title: prop.title, + description: prop.description + }; + + if (prop.required) { + schema.value.required.push(prop.key); + } + }); + } + + function togglePreview() { + preview.value = !preview.value; + } + + function saveSchema() { + dirty.value = false; + } + + function loadSchema(schemaData) { + if (typeof schemaData === "string") { + try { + schemaData = JSON.parse(schemaData); + } catch (e) { + console.error("Invalid JSON schema:", e); + return; + } + } + + schema.value = schemaData; + properties.value = []; + + if (schemaData.properties) { + Object.entries(schemaData.properties).forEach(([key, prop]) => { + properties.value.push({ + key, + type: prop.type || "string", + title: prop.title || key, + description: prop.description || "", + required: schemaData.required?.includes(key) || false + }); + }); + } + + dirty.value = false; + } + + return { + schema, + properties, + selectedField, + preview, + readOnly, + dirty, + notUsingInput, + addProperty, + removeProperty, + updateProperty, + updateSchema, + togglePreview, + saveSchema, + loadSchema + }; +}); + +// EditableInput 组件 +const EditableInput = { + props: { + text: { + type: String, + default: "" + }, + placeholder: { + type: String, + default: "" + }, + emptyLabel: { + type: String, + default: "" + } + }, + emits: ['update:modelValue'], + setup(props, { emit }) { + const isEditing = ref(false); + const editValue = ref(""); + const inputRef = ref(null); + + const displayText = computed(() => { + if (props.text) return props.text; + if (props.emptyLabel) return props.emptyLabel; + return props.placeholder || "点击编辑"; + }); + + async function startEdit() { + isEditing.value = true; + editValue.value = props.text; + await nextTick(); + inputRef.value?.focus(); + inputRef.value?.select(); + } + + function finishEdit() { + if (isEditing.value) { + emit("update:modelValue", editValue.value); + isEditing.value = false; + } + } + + function cancelEdit() { + editValue.value = props.text; + isEditing.value = false; + } + + return { + isEditing, + editValue, + inputRef, + displayText, + startEdit, + finishEdit, + cancelEdit + }; + }, + template: ` + + + {{ displayText }} + + + + ` +}; + +// SchemaField 组件 +const SchemaField = { + props: { + field: { + type: Object, + required: true + }, + fieldKey: { + type: String, + required: true + }, + selected: { + type: Boolean, + default: false + } + }, + emits: ['select', 'edit', 'delete'], + setup(props, { emit }) { + const store = useSchemaStore(); + const hovered = ref(false); + + function selectField() { + emit('select', props.fieldKey); + } + + function editField() { + emit('edit', props.fieldKey); + } + + function deleteField() { + emit('delete', props.fieldKey); + } + + return { + store, + hovered, + selectField, + editField, + deleteField + }; + }, + components: { + EditableInput + }, + template: ` +
+
+
+ + {{ field.type }} +
+
+ + +
+
+ +
+ +
+
+ ` +}; + +// SchemaCanvas 组件 +const SchemaCanvas = { + setup() { + const store = useSchemaStore(); + + function selectField(fieldKey) { + store.selectedField = fieldKey; + } + + function editField(fieldKey) { + store.selectedField = fieldKey; + } + + function deleteField(fieldKey) { + store.removeProperty(fieldKey); + } + + function saveSchema() { + store.saveSchema(); + } + + return { + store, + selectField, + editField, + deleteField, + saveSchema + }; + }, + components: { + SchemaField, + EditableInput + }, + template: ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
属性配置
+
+ +
+
+ +
+
+
+ +
+
{{ JSON.stringify(store.schema, null, 2) }}
+
+
+
+ ` +}; + +// SchemaSidebar 组件 +const SchemaSidebar = { + setup() { + const store = useSchemaStore(); + + const propertyTypes = [ + { + value: "string", + label: "字符串", + description: "文本输入", + icon: "text", + iconText: "T" + }, + { + value: "number", + label: "数字", + description: "数字输入", + icon: "number", + iconText: "123" + }, + { + value: "boolean", + label: "布尔值", + description: "是/否选择", + icon: "check", + iconText: "✓" + }, + { + value: "array", + label: "数组", + description: "列表数据", + icon: "list", + iconText: "[]" + }, + { + value: "object", + label: "对象", + description: "嵌套对象", + icon: "object", + iconText: "{}" + } + ]; + + const selectedField = computed(() => { + if (!store.selectedField) return null; + return store.properties.find(p => p.key === store.selectedField); + }); + + function onDragStart(event, type) { + event.dataTransfer.setData("application/json", JSON.stringify(type)); + } + + function updateField() { + if (selectedField.value) { + store.updateProperty(selectedField.value.key, selectedField.value); + } + } + + return { + store, + propertyTypes, + selectedField, + onDragStart, + updateField + }; + }, + template: ` +
+ + + +
+ ` +}; + +// 主 SchemaBuilder 组件 +const SchemaBuilder = { + props: { + initialSchema: { + type: Object, + default: () => ({}) + }, + nodeType: { + type: String, + default: "" + } + }, + emits: ['save', 'change'], + setup(props, { emit }) { + const store = useSchemaStore(); + + // 加载初始 schema + if (props.initialSchema && Object.keys(props.initialSchema).length > 0) { + store.loadSchema(props.initialSchema); + } + + // 监听 schema 变化 + watch(() => store.schema, (newSchema) => { + emit('change', newSchema); + }, { deep: true }); + + function handleSave() { + emit('save', store.schema); + } + + return { + store, + handleSave + }; + }, + components: { + SchemaCanvas, + SchemaSidebar + }, + template: ` +
+
+ +
+
+ +
+
+ ` +}; + +// 添加样式 +const style = document.createElement('style'); +style.textContent = ` + .icon-text { + display: inline-block; + width: 20px; + height: 20px; + line-height: 20px; + text-align: center; + background-color: var(--gray-200); + border-radius: 50%; + font-size: 12px; + font-weight: bold; + color: var(--text-muted); + } + + .schema-builder-container { + display: flex; + height: 100%; + } + + .schema-main { + flex: 1; + background-color: var(--disabled-control-bg); + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + background-color: var(--card-bg); + margin: 5px; + } + + .schema-sidebar { + width: 300px; + border-left: 1px solid var(--border-color); + border-bottom-right-radius: var(--border-radius); + background-color: var(--fg-color); + } + + .schema-canvas { + padding: 1rem; + } + + .schema-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); + } + + .schema-title { + flex: 1; + } + + .schema-title span { + font-weight: 600; + color: var(--heading-color); + font-size: 1.2rem; + } + + .schema-actions { + display: flex; + gap: 0.5rem; + } + + .properties-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + } + + .properties-header h5 { + margin: 0; + color: var(--heading-color); + } + + .properties-list { + min-height: 200px; + border: 1px dashed var(--border-color); + border-radius: var(--border-radius); + padding: 1rem; + } + + .schema-preview pre { + background-color: var(--bg-light-gray); + padding: 1rem; + border-radius: var(--border-radius); + overflow-x: auto; + font-size: var(--text-sm); + } + + .schema-field { + background-color: var(--bg-light-gray); + border-radius: var(--border-radius-sm); + border: 1px solid transparent; + padding: 0.75rem; + margin-bottom: 0.5rem; + cursor: pointer; + } + + .schema-field:not(:last-child) { + margin-bottom: 0.5rem; + } + + .schema-field.hovered, + .schema-field.selected { + border-color: var(--border-primary); + background-color: var(--fg-color); + } + + .field-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + } + + .field-info { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; + } + + .field-info span { + font-weight: 500; + color: var(--heading-color); + } + + .field-type { + background-color: var(--gray-200); + color: var(--text-muted); + padding: 0.2rem 0.5rem; + border-radius: var(--border-radius-sm); + font-size: var(--text-xs); + font-weight: normal; + } + + .field-actions { + display: flex; + gap: 0.25rem; + opacity: 0; + transition: opacity 0.2s; + } + + .schema-field:hover .field-actions { + opacity: 1; + } + + .field-description { + margin-bottom: 0.5rem; + } + + .field-description span { + color: var(--text-muted); + font-size: var(--text-sm); + } + + .sidebar-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + } + + .sidebar-header h5 { + margin: 0; + color: var(--heading-color); + } + + .sidebar-content { + flex: 1; + overflow-y: auto; + padding: 1rem; + } + + .property-types { + margin-bottom: 2rem; + } + + .property-type-item { + display: flex; + align-items: center; + padding: 0.75rem; + margin-bottom: 0.5rem; + background-color: var(--bg-light-gray); + border-radius: var(--border-radius); + cursor: grab; + border: 1px solid transparent; + transition: all 0.2s; + } + + .property-type-item:hover { + border-color: var(--border-primary); + background-color: var(--fg-color); + } + + .property-type-item:active { + cursor: grabbing; + } + + .type-icon { + margin-right: 0.75rem; + color: var(--text-muted); + } + + .type-info { + flex: 1; + } + + .type-name { + font-weight: 500; + color: var(--heading-color); + margin-bottom: 0.25rem; + } + + .type-description { + font-size: var(--text-sm); + color: var(--text-muted); + } + + .sidebar-actions h6 { + margin-bottom: 1rem; + color: var(--heading-color); + } + + .field-settings .setting-item { + margin-bottom: 1rem; + } + + .field-settings label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--heading-color); + } + + .field-settings .form-control { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: var(--text-sm); + } + + .field-settings .form-control:focus { + outline: none; + border-color: var(--border-primary); + } + + .field-settings textarea.form-control { + resize: vertical; + min-height: 80px; + } + + .editable-input { + display: inline-block; + cursor: pointer; + } + + .editable-input:hover { + background-color: var(--bg-light-gray); + border-radius: var(--border-radius-sm); + padding: 0.2rem 0.4rem; + margin: -0.2rem -0.4rem; + } + + .editable-input.is-editing .form-control { + border: 1px solid var(--border-primary); + outline: none; + background-color: var(--fg-color); + } + + .editable-text { + display: inline-block; + min-width: 1rem; + min-height: 1.2rem; + } +`; +document.head.appendChild(style); + +// 导出到全局 +window.SchemaBuilder = SchemaBuilder; +window.SchemaStore = useSchemaStore; + +console.log("Schema Builder Bundle 加载完成"); \ No newline at end of file diff --git a/jcloud/public/js/schema_builder/store.js b/jcloud/public/js/schema_builder/store.js new file mode 100644 index 0000000..9e95eec --- /dev/null +++ b/jcloud/public/js/schema_builder/store.js @@ -0,0 +1,146 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; + +export const useSchemaStore = defineStore("schema-builder-store", () => { + // State + const schema = ref({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + title: "AI 节点配置", + properties: {}, + required: [] + }); + + const properties = ref([]); + const selectedField = ref(null); + const preview = ref(false); + const readOnly = ref(false); + const dirty = ref(false); + + // Getters + const notUsingInput = computed(() => { + // 检查是否正在使用输入框 + const activeElement = document.activeElement; + return !activeElement || + activeElement.readOnly || + activeElement.disabled || + (activeElement.tagName !== "INPUT" && activeElement.tagName !== "TEXTAREA"); + }); + + // Actions + function addProperty(type = "string") { + const key = `property_${Date.now()}`; + const newProperty = { + key, + type, + title: "新属性", + description: "", + required: false + }; + + properties.value.push(newProperty); + updateSchema(); + selectedField.value = key; + dirty.value = true; + } + + function removeProperty(key) { + const index = properties.value.findIndex(p => p.key === key); + if (index > -1) { + properties.value.splice(index, 1); + updateSchema(); + if (selectedField.value === key) { + selectedField.value = null; + } + dirty.value = true; + } + } + + function updateProperty(key, updatedProperty) { + const index = properties.value.findIndex(p => p.key === key); + if (index > -1) { + properties.value[index] = { ...properties.value[index], ...updatedProperty }; + updateSchema(); + dirty.value = true; + } + } + + function updateSchema() { + // 更新 schema 对象 + schema.value.properties = {}; + schema.value.required = []; + + properties.value.forEach(prop => { + schema.value.properties[prop.key] = { + type: prop.type, + title: prop.title, + description: prop.description + }; + + if (prop.required) { + schema.value.required.push(prop.key); + } + }); + } + + function togglePreview() { + preview.value = !preview.value; + } + + function saveSchema() { + // 触发保存事件 + dirty.value = false; + // 这里可以触发父组件的保存事件 + } + + function loadSchema(schemaData) { + if (typeof schemaData === "string") { + try { + schemaData = JSON.parse(schemaData); + } catch (e) { + console.error("Invalid JSON schema:", e); + return; + } + } + + schema.value = schemaData; + properties.value = []; + + // 解析 properties + if (schemaData.properties) { + Object.entries(schemaData.properties).forEach(([key, prop]) => { + properties.value.push({ + key, + type: prop.type || "string", + title: prop.title || key, + description: prop.description || "", + required: schemaData.required?.includes(key) || false + }); + }); + } + + dirty.value = false; + } + + return { + // State + schema, + properties, + selectedField, + preview, + readOnly, + dirty, + + // Getters + notUsingInput, + + // Actions + addProperty, + removeProperty, + updateProperty, + updateSchema, + togglePreview, + saveSchema, + loadSchema + }; +}); \ No newline at end of file