更新市场APP订阅等级名称
This commit is contained in:
parent
de0b85f471
commit
baf5183079
@ -94,10 +94,10 @@ export default {
|
||||
// 如果应用不可安装,显示对应提示
|
||||
if (!checkResult.installable) {
|
||||
const subscriptionTypeMap = {
|
||||
2: 'Pro',
|
||||
3: 'Business',
|
||||
4: 'Enterprise',
|
||||
5: 'Ultimate'
|
||||
2: '299元/月',
|
||||
3: '399元/月',
|
||||
4: '499元/月',
|
||||
5: '599元/月'
|
||||
};
|
||||
|
||||
const requiredPlan = subscriptionTypeMap[checkResult.required_plan_level] ||
|
||||
|
||||
@ -1,303 +0,0 @@
|
||||
// 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: `
|
||||
<div id="schema-builder-container" style="height: 80vh;">
|
||||
<div id="schema-builder"></div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
],
|
||||
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: `
|
||||
<SchemaBuilder
|
||||
:initial-schema="initialSchema"
|
||||
:node-type="nodeType"
|
||||
@save="handleSave"
|
||||
@change="handleChange"
|
||||
/>
|
||||
`,
|
||||
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]), __("错误"));
|
||||
});
|
||||
}
|
||||
|
||||
// 简化实现,移除复杂的对话框和状态指示器
|
||||
@ -1,49 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
# 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)))
|
||||
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2025, Jingrow and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import jingrow
|
||||
from jingrow.tests.utils import JingrowTestCase
|
||||
|
||||
|
||||
class TestAINodeSchema(JingrowTestCase):
|
||||
pass
|
||||
@ -2,7 +2,6 @@
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-01-28 20:07:40.451028",
|
||||
"pagetype": "PageType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@ -166,15 +165,16 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_pagetype": "Error Log",
|
||||
"link_fieldname": "reference_name"
|
||||
"link_fieldname": "reference_name",
|
||||
"link_pagetype": "Error Log"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-05 10:12:54.374115",
|
||||
"modified": "2025-09-06 01:23:01.905284",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Jcloud",
|
||||
"name": "App Source",
|
||||
"owner": "Administrator",
|
||||
"pagetype": "PageType",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
@ -201,6 +201,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@ -24,15 +24,15 @@ class AppSource(Document):
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jingrow.types import DF
|
||||
from jcloud.jcloud.pagetype.app_source_version.app_source_version import AppSourceVersion
|
||||
from jingrow.types import DF
|
||||
|
||||
app: DF.Link
|
||||
app_title: DF.Data
|
||||
branch: DF.Data
|
||||
enabled: DF.Check
|
||||
jingrow: DF.Check
|
||||
github_installation_id: DF.Data | None
|
||||
jingrow: DF.Check
|
||||
last_github_poll_failed: DF.Check
|
||||
last_github_response: DF.Code | None
|
||||
last_synced: DF.Datetime | None
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div class="schema-builder-container">
|
||||
<div class="schema-main" :class="[store.preview ? 'preview' : '']">
|
||||
<SchemaCanvas />
|
||||
</div>
|
||||
<div class="schema-sidebar">
|
||||
<SchemaSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SchemaCanvas from "./components/SchemaCanvas.vue";
|
||||
import SchemaSidebar from "./components/SchemaSidebar.vue";
|
||||
import { useSchemaStore } from "./store";
|
||||
|
||||
const store = useSchemaStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<span
|
||||
class="editable-input"
|
||||
:class="{ 'is-editing': isEditing }"
|
||||
@click="startEdit"
|
||||
@keydown.enter="finishEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
@blur="finishEdit"
|
||||
tabindex="0"
|
||||
>
|
||||
<span v-if="!isEditing" class="editable-text">
|
||||
{{ displayText }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="editValue"
|
||||
@blur="finishEdit"
|
||||
@keydown.enter="finishEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
emptyLabel: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editable-input {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-light-gray);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.2rem 0.4rem;
|
||||
margin: -0.2rem -0.4rem;
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div class="property-dialog-overlay" @click="close">
|
||||
<div class="property-dialog" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h5>{{ isEditing ? '编辑属性' : '添加属性' }}</h5>
|
||||
<button type="button" class="btn-close" @click="close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<PropertyEditor
|
||||
:property="localProperty"
|
||||
:property-key="propertyKey"
|
||||
@update="updateProperty"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="close">
|
||||
取消
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="save">
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import PropertyEditor from './PropertyEditor.vue'
|
||||
|
||||
const props = defineProps({
|
||||
property: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
propertyKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['save', 'close'])
|
||||
|
||||
// 响应式数据
|
||||
const localProperty = ref({ ...props.property })
|
||||
|
||||
// 计算属性
|
||||
const isEditing = computed(() => !!props.propertyKey)
|
||||
|
||||
// 监听属性变化
|
||||
watch(() => props.property, (newProperty) => {
|
||||
localProperty.value = { ...newProperty }
|
||||
}, { deep: true })
|
||||
|
||||
// 方法
|
||||
function updateProperty(updatedProperty) {
|
||||
localProperty.value = updatedProperty
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', localProperty.value)
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.property-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.property-dialog {
|
||||
background-color: var(--fg-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color-dark);
|
||||
border-color: var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
border-color: var(--secondary-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-color-dark);
|
||||
border-color: var(--secondary-color-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<div class="property-editor">
|
||||
<div class="field" v-for="(df, i) in propertyFields" :key="i">
|
||||
<component
|
||||
:is="df.fieldtype.replaceAll(' ', '') + 'Control'"
|
||||
:args="args"
|
||||
:df="df"
|
||||
:read_only="false"
|
||||
:value="property[df.fieldname]"
|
||||
v-model="property[df.fieldname]"
|
||||
:data-fieldname="df.fieldname"
|
||||
:data-fieldtype="df.fieldtype"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
property: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
propertyKey: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const args = ref({});
|
||||
|
||||
// 定义属性编辑字段
|
||||
const propertyFields = computed(() => {
|
||||
return [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "title",
|
||||
label: "标题",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
fieldname: "type",
|
||||
label: "类型",
|
||||
options: [
|
||||
["字符串", "string"],
|
||||
["数字", "number"],
|
||||
["整数", "integer"],
|
||||
["布尔值", "boolean"],
|
||||
["数组", "array"],
|
||||
["对象", "object"]
|
||||
],
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
fieldname: "description",
|
||||
label: "描述"
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
fieldname: "required",
|
||||
label: "必填"
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "default",
|
||||
label: "默认值"
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
fieldname: "format",
|
||||
label: "格式",
|
||||
depends_on: "type",
|
||||
depends_on_value: "string"
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
fieldname: "minLength",
|
||||
label: "最小长度",
|
||||
depends_on: "type",
|
||||
depends_on_value: "string"
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
fieldname: "maxLength",
|
||||
label: "最大长度",
|
||||
depends_on: "type",
|
||||
depends_on_value: "string"
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "minimum",
|
||||
label: "最小值",
|
||||
depends_on: "type",
|
||||
depends_on_value: ["number", "integer"]
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "maximum",
|
||||
label: "最大值",
|
||||
depends_on: "type",
|
||||
depends_on_value: ["number", "integer"]
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
fieldname: "enum",
|
||||
label: "枚举选项",
|
||||
description: "每行一个选项"
|
||||
}
|
||||
].filter(field => {
|
||||
// 根据属性类型显示相关字段
|
||||
if (field.depends_on) {
|
||||
const dependsOnValue = Array.isArray(field.depends_on_value)
|
||||
? field.depends_on_value
|
||||
: [field.depends_on_value];
|
||||
return dependsOnValue.includes(props.property.type);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.property-editor {
|
||||
.field {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
:deep(.form-control:disabled) {
|
||||
color: var(--disabled-text-color);
|
||||
background-color: var(--disabled-control-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.label) {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.3rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
:deep(.description) {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,146 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'property-item',
|
||||
hovered ? 'hovered' : '',
|
||||
selected ? 'selected' : '',
|
||||
]"
|
||||
:title="propertyKey"
|
||||
@click.stop="selectProperty"
|
||||
@mouseover.stop="hovered = true"
|
||||
@mouseout.stop="hovered = false"
|
||||
>
|
||||
<div class="property-header">
|
||||
<div class="property-info">
|
||||
<span class="property-name">{{ property.title || propertyKey }}</span>
|
||||
<span class="property-type">{{ property.type }}</span>
|
||||
</div>
|
||||
<div class="property-actions" v-if="!store.readOnly">
|
||||
<button class="btn btn-xs btn-outline-secondary" @click.stop="editProperty">
|
||||
编辑
|
||||
</button>
|
||||
<button class="btn btn-xs btn-outline-danger" @click.stop="deleteProperty">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-details" v-if="selected">
|
||||
<PropertyEditor
|
||||
:property="property"
|
||||
:property-key="propertyKey"
|
||||
@update="updateProperty"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PropertyEditor from "./PropertyEditor.vue";
|
||||
import { ref } from "vue";
|
||||
import { useSchemaStore } from "../store";
|
||||
|
||||
const props = defineProps({
|
||||
property: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
propertyKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'edit', 'delete']);
|
||||
|
||||
const store = useSchemaStore();
|
||||
const hovered = ref(false);
|
||||
|
||||
function selectProperty() {
|
||||
emit('select', props.propertyKey);
|
||||
}
|
||||
|
||||
function editProperty() {
|
||||
emit('edit', props.propertyKey);
|
||||
}
|
||||
|
||||
function deleteProperty() {
|
||||
emit('delete', props.propertyKey);
|
||||
}
|
||||
|
||||
function updateProperty(updatedProperty) {
|
||||
store.updateProperty(props.propertyKey, updatedProperty);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.property-item {
|
||||
background-color: var(--fg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&.hovered {
|
||||
border-color: var(--border-primary);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--border-primary);
|
||||
background-color: var(--bg-light-gray);
|
||||
}
|
||||
|
||||
.property-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.property-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.property-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.property-type {
|
||||
background-color: var(--bg-gray);
|
||||
color: var(--text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.property-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .property-actions,
|
||||
&.selected .property-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.property-details {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,207 +0,0 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="property-title">
|
||||
<h6>{{ store.selectedProperty }}</h6>
|
||||
</div>
|
||||
<button
|
||||
class="close-btn btn btn-xs"
|
||||
:title="__('关闭属性面板')"
|
||||
@click="store.selectedProperty = null"
|
||||
>
|
||||
<div v-html="jingrow.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-data">
|
||||
<div v-if="store.selectedProperty">
|
||||
<div class="field" v-for="(df, i) in propertyFields" :key="i">
|
||||
<component
|
||||
:is="df.fieldtype.replaceAll(' ', '') + 'Control'"
|
||||
:args="args"
|
||||
:df="df"
|
||||
:read_only="false"
|
||||
:value="getPropertyValue(df.fieldname)"
|
||||
v-model="propertyData[df.fieldname]"
|
||||
:data-fieldname="df.fieldname"
|
||||
:data-fieldtype="df.fieldtype"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useSchemaStore } from "../store";
|
||||
|
||||
const store = useSchemaStore();
|
||||
|
||||
const args = ref({});
|
||||
const propertyData = ref({});
|
||||
|
||||
// 定义属性配置字段
|
||||
const propertyFields = computed(() => {
|
||||
return [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "title",
|
||||
label: "标题",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
fieldname: "type",
|
||||
label: "类型",
|
||||
options: [
|
||||
["字符串", "string"],
|
||||
["数字", "number"],
|
||||
["整数", "integer"],
|
||||
["布尔值", "boolean"],
|
||||
["数组", "array"],
|
||||
["对象", "object"]
|
||||
],
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
fieldname: "description",
|
||||
label: "描述"
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
fieldname: "required",
|
||||
label: "必填"
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "default",
|
||||
label: "默认值"
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
fieldname: "format",
|
||||
label: "格式",
|
||||
depends_on: "type",
|
||||
depends_on_value: "string"
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
fieldname: "minLength",
|
||||
label: "最小长度",
|
||||
depends_on: "type",
|
||||
depends_on_value: "string"
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
fieldname: "maxLength",
|
||||
label: "最大长度",
|
||||
depends_on: "type",
|
||||
depends_on_value: "string"
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "minimum",
|
||||
label: "最小值",
|
||||
depends_on: "type",
|
||||
depends_on_value: ["number", "integer"]
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "maximum",
|
||||
label: "最大值",
|
||||
depends_on: "type",
|
||||
depends_on_value: ["number", "integer"]
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
fieldname: "enum",
|
||||
label: "枚举选项",
|
||||
description: "每行一个选项"
|
||||
}
|
||||
].filter(field => {
|
||||
// 根据属性类型显示相关字段
|
||||
if (field.depends_on) {
|
||||
const currentType = propertyData.value.type || store.getProperty(store.selectedProperty)?.type;
|
||||
const dependsOnValue = Array.isArray(field.depends_on_value)
|
||||
? field.depends_on_value
|
||||
: [field.depends_on_value];
|
||||
return dependsOnValue.includes(currentType);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function getPropertyValue(fieldname) {
|
||||
const property = store.getProperty(store.selectedProperty);
|
||||
return property ? property[fieldname] : "";
|
||||
}
|
||||
|
||||
// 监听属性数据变化,实时更新到 store
|
||||
watch(propertyData, (newData) => {
|
||||
if (store.selectedProperty) {
|
||||
store.updateProperty(store.selectedProperty, newData);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 监听选中的属性变化,更新属性数据
|
||||
watch(() => store.selectedProperty, (newPropertyKey) => {
|
||||
if (newPropertyKey) {
|
||||
const property = store.getProperty(newPropertyKey);
|
||||
if (property) {
|
||||
propertyData.value = { ...property };
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.property-title {
|
||||
h6 {
|
||||
margin: 0;
|
||||
color: var(--heading-color);
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
margin-right: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-data {
|
||||
height: calc(100vh - 202px);
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
|
||||
.field {
|
||||
margin: 5px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
:deep(.form-control:disabled) {
|
||||
color: var(--disabled-text-color);
|
||||
background-color: var(--disabled-control-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.label) {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.3rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
:deep(.description) {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<div class="schema-canvas">
|
||||
<div class="schema-header" v-if="!store.preview">
|
||||
<div class="schema-title">
|
||||
<EditableInput
|
||||
:text="store.schema.title || 'Schema 标题'"
|
||||
:placeholder="__('Schema Title')"
|
||||
v-model="store.schema.title"
|
||||
/>
|
||||
</div>
|
||||
<div class="schema-actions">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="store.togglePreview"
|
||||
>
|
||||
{{ store.preview ? '编辑' : '预览' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
@click="saveSchema"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schema-content">
|
||||
<div class="schema-properties" v-if="!store.preview">
|
||||
<div class="properties-header">
|
||||
<h5>属性配置</h5>
|
||||
</div>
|
||||
|
||||
<div class="properties-list">
|
||||
<draggable
|
||||
v-model="store.properties"
|
||||
group="properties"
|
||||
:animation="200"
|
||||
item-key="key"
|
||||
:disabled="store.readOnly"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SchemaField
|
||||
:field="element"
|
||||
:field-key="element.key"
|
||||
:selected="store.selectedField === element.key"
|
||||
@select="selectField"
|
||||
@edit="editField"
|
||||
@delete="deleteField"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schema-preview" v-if="store.preview">
|
||||
<pre>{{ JSON.stringify(store.schema, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from "vuedraggable";
|
||||
import SchemaField from "./SchemaField.vue";
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { useSchemaStore } from "../store";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
|
||||
const store = useSchemaStore();
|
||||
|
||||
// delete/backspace to delete the field
|
||||
const { Backspace } = useMagicKeys();
|
||||
whenever(Backspace, (value) => {
|
||||
if (value && store.selectedField && store.notUsingInput) {
|
||||
deleteField(store.selectedField);
|
||||
}
|
||||
});
|
||||
|
||||
function selectField(fieldKey) {
|
||||
store.selectedField = fieldKey;
|
||||
}
|
||||
|
||||
function editField(fieldKey) {
|
||||
store.selectedField = fieldKey;
|
||||
// 可以在这里打开属性编辑面板
|
||||
}
|
||||
|
||||
function deleteField(fieldKey) {
|
||||
store.removeProperty(fieldKey);
|
||||
}
|
||||
|
||||
function saveSchema() {
|
||||
// 触发保存事件
|
||||
store.saveSchema();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
|
||||
:deep(span) {
|
||||
font-weight: 600;
|
||||
color: var(--heading-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.schema-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.schema-content {
|
||||
.properties-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,165 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['schema-field', selected ? 'selected' : hovered ? 'hovered' : '']"
|
||||
:title="fieldKey"
|
||||
@click.stop="selectField"
|
||||
@mouseover.stop="hovered = true"
|
||||
@mouseout.stop="hovered = false"
|
||||
>
|
||||
<div class="field-header">
|
||||
<div class="field-info">
|
||||
<EditableInput
|
||||
:text="field.title || '新属性'"
|
||||
:placeholder="__('Property Title')"
|
||||
v-model="field.title"
|
||||
/>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
</div>
|
||||
<div class="field-actions" v-if="!store.readOnly">
|
||||
<button
|
||||
class="btn btn-xs btn-outline-secondary"
|
||||
@click.stop="editField"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-outline-danger"
|
||||
@click.stop="deleteField"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-description" v-if="field.description">
|
||||
<EditableInput
|
||||
:text="field.description"
|
||||
:placeholder="__('Description')"
|
||||
v-model="field.description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-properties" v-if="field.properties">
|
||||
<div class="properties-count">
|
||||
{{ Object.keys(field.properties).length }} 个子属性
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { useSchemaStore } from "../store";
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
fieldKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const store = useSchemaStore();
|
||||
const hovered = ref(false);
|
||||
|
||||
function selectField() {
|
||||
store.selectedField = props.fieldKey;
|
||||
}
|
||||
|
||||
function editField() {
|
||||
store.selectedField = props.fieldKey;
|
||||
// 可以在这里打开属性编辑面板
|
||||
}
|
||||
|
||||
function deleteField() {
|
||||
store.removeProperty(props.fieldKey);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&.hovered,
|
||||
&.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;
|
||||
|
||||
:deep(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;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .field-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.field-description {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
:deep(span) {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.field-properties {
|
||||
.properties-count {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
background-color: var(--gray-100);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: var(--border-radius-sm);
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,237 +0,0 @@
|
||||
<template>
|
||||
<div class="schema-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h5>属性类型</h5>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="property-types">
|
||||
<div
|
||||
v-for="type in propertyTypes"
|
||||
:key="type.value"
|
||||
class="property-type-item"
|
||||
:draggable="true"
|
||||
@dragstart="onDragStart($event, type)"
|
||||
>
|
||||
<div class="type-icon">
|
||||
<div v-html="jingrow.utils.icon(type.icon, 'sm')"></div>
|
||||
</div>
|
||||
<div class="type-info">
|
||||
<div class="type-name">{{ type.label }}</div>
|
||||
<div class="type-description">{{ type.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-actions" v-if="store.selectedField">
|
||||
<h6>属性设置</h6>
|
||||
<div class="field-settings">
|
||||
<div class="setting-item">
|
||||
<label>属性名称</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="selectedField.title"
|
||||
@input="updateField"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>属性类型</label>
|
||||
<select
|
||||
class="form-control"
|
||||
v-model="selectedField.type"
|
||||
@change="updateField"
|
||||
>
|
||||
<option value="string">字符串</option>
|
||||
<option value="number">数字</option>
|
||||
<option value="boolean">布尔值</option>
|
||||
<option value="array">数组</option>
|
||||
<option value="object">对象</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>描述</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
v-model="selectedField.description"
|
||||
@input="updateField"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedField.required"
|
||||
@change="updateField"
|
||||
/>
|
||||
必填
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSchemaStore } from "../store";
|
||||
import { computed } from "vue";
|
||||
|
||||
const store = useSchemaStore();
|
||||
|
||||
const propertyTypes = [
|
||||
{
|
||||
value: "string",
|
||||
label: "字符串",
|
||||
description: "文本输入",
|
||||
icon: "text"
|
||||
},
|
||||
{
|
||||
value: "number",
|
||||
label: "数字",
|
||||
description: "数字输入",
|
||||
icon: "number"
|
||||
},
|
||||
{
|
||||
value: "boolean",
|
||||
label: "布尔值",
|
||||
description: "是/否选择",
|
||||
icon: "check"
|
||||
},
|
||||
{
|
||||
value: "array",
|
||||
label: "数组",
|
||||
description: "列表数据",
|
||||
icon: "list"
|
||||
},
|
||||
{
|
||||
value: "object",
|
||||
label: "对象",
|
||||
description: "嵌套对象",
|
||||
icon: "object"
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.schema-sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-primary);
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
&: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;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--heading-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--text-sm);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,892 +0,0 @@
|
||||
// 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: `
|
||||
<span
|
||||
class="editable-input"
|
||||
:class="{ 'is-editing': isEditing }"
|
||||
@click="startEdit"
|
||||
@keydown.enter="finishEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
@blur="finishEdit"
|
||||
tabindex="0"
|
||||
>
|
||||
<span v-if="!isEditing" class="editable-text">
|
||||
{{ displayText }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="editValue"
|
||||
@blur="finishEdit"
|
||||
@keydown.enter="finishEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
/>
|
||||
</span>
|
||||
`
|
||||
};
|
||||
|
||||
// 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: `
|
||||
<div
|
||||
:class="['schema-field', selected ? 'selected' : hovered ? 'hovered' : '']"
|
||||
:title="fieldKey"
|
||||
@click.stop="selectField"
|
||||
@mouseover.stop="hovered = true"
|
||||
@mouseout.stop="hovered = false"
|
||||
>
|
||||
<div class="field-header">
|
||||
<div class="field-info">
|
||||
<EditableInput
|
||||
:text="field.title || '新属性'"
|
||||
:placeholder="__('Property Title')"
|
||||
v-model="field.title"
|
||||
/>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
</div>
|
||||
<div class="field-actions" v-if="!store.readOnly">
|
||||
<button
|
||||
class="btn btn-xs btn-outline-secondary"
|
||||
@click.stop="editField"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-outline-danger"
|
||||
@click.stop="deleteField"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-description" v-if="field.description">
|
||||
<EditableInput
|
||||
:text="field.description"
|
||||
:placeholder="__('Description')"
|
||||
v-model="field.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// 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: `
|
||||
<div class="schema-canvas">
|
||||
<div class="schema-header" v-if="!store.preview">
|
||||
<div class="schema-title">
|
||||
<EditableInput
|
||||
:text="store.schema.title || 'Schema 标题'"
|
||||
:placeholder="__('Schema Title')"
|
||||
v-model="store.schema.title"
|
||||
/>
|
||||
</div>
|
||||
<div class="schema-actions">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="store.togglePreview"
|
||||
>
|
||||
{{ store.preview ? '编辑' : '预览' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
@click="saveSchema"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schema-content">
|
||||
<div class="schema-properties" v-if="!store.preview">
|
||||
<div class="properties-header">
|
||||
<h5>属性配置</h5>
|
||||
</div>
|
||||
|
||||
<div class="properties-list">
|
||||
<div
|
||||
v-for="element in store.properties"
|
||||
:key="element.key"
|
||||
class="property-item-wrapper"
|
||||
>
|
||||
<SchemaField
|
||||
:field="element"
|
||||
:field-key="element.key"
|
||||
:selected="store.selectedField === element.key"
|
||||
@select="selectField"
|
||||
@edit="editField"
|
||||
@delete="deleteField"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schema-preview" v-if="store.preview">
|
||||
<pre>{{ JSON.stringify(store.schema, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// 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: `
|
||||
<div class="schema-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h5>属性类型</h5>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="property-types">
|
||||
<div
|
||||
v-for="type in propertyTypes"
|
||||
:key="type.value"
|
||||
class="property-type-item"
|
||||
:draggable="true"
|
||||
@dragstart="onDragStart($event, type)"
|
||||
>
|
||||
<div class="type-icon">
|
||||
<span class="icon-text">{{ type.iconText }}</span>
|
||||
</div>
|
||||
<div class="type-info">
|
||||
<div class="type-name">{{ type.label }}</div>
|
||||
<div class="type-description">{{ type.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-actions" v-if="store.selectedField">
|
||||
<h6>属性设置</h6>
|
||||
<div class="field-settings">
|
||||
<div class="setting-item">
|
||||
<label>属性名称</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="selectedField.title"
|
||||
@input="updateField"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>属性类型</label>
|
||||
<select
|
||||
class="form-control"
|
||||
v-model="selectedField.type"
|
||||
@change="updateField"
|
||||
>
|
||||
<option value="string">字符串</option>
|
||||
<option value="number">数字</option>
|
||||
<option value="boolean">布尔值</option>
|
||||
<option value="array">数组</option>
|
||||
<option value="object">对象</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>描述</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
v-model="selectedField.description"
|
||||
@input="updateField"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedField.required"
|
||||
@change="updateField"
|
||||
/>
|
||||
必填
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// 主 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: `
|
||||
<div class="schema-builder-container">
|
||||
<div class="schema-main" :class="[store.preview ? 'preview' : '']">
|
||||
<SchemaCanvas />
|
||||
</div>
|
||||
<div class="schema-sidebar">
|
||||
<SchemaSidebar />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// 添加样式
|
||||
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 加载完成");
|
||||
@ -1,146 +0,0 @@
|
||||
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
|
||||
};
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user