@@ -211,10 +229,18 @@ import { Plus } from '@element-plus/icons-vue';
interface UploadRule { format: string; maxSize?: number; maxCount?: number; }
+interface AvailableField {
+ key: string;
+ path: string;
+}
+
interface FieldFormData {
nodeKey: string; nodeId: string; jsonType: string; fieldType: string;
label: string; description: string; role: string;
isForm: boolean; required: boolean; loop: boolean; defaultValue: string; options: Array<{ label: string; value: string }>;
+ isContainer: boolean;
+ linkRules: Array>;
+ _levelFields: AvailableField[];
constraint: {
minLength?: number; maxLength?: number; pattern?: string;
min?: number; max?: number; numberType?: string;
@@ -242,6 +268,9 @@ const form = reactive({
nodeKey: '', nodeId: '', jsonType: 'string', fieldType: 'string',
label: '', description: '', role: '',
isForm: true, required: false, loop: false, defaultValue: '', options: [], constraint: {},
+ isContainer: false,
+ linkRules: [],
+ _levelFields: [],
});
const uploadRules = ref([]);
@@ -257,11 +286,26 @@ const dialogTitle = computed(() => {
: '字段配置 —— ' + form.nodeKey;
});
+// 当前选中的字段 key 列表(用于 v-model)
+const selectedKeys = computed(() =>
+ form.linkRules.map((r) => Object.keys(r)[0]).filter(Boolean) as string[]
+);
+
+function onSelectChange(keys: string[]) {
+ form.linkRules = keys.map((key) => {
+ const match = form._levelFields.find((f) => f.key === key);
+ return { [key]: match ? match.path : key };
+ });
+}
+
function resetForm() {
form.nodeKey = ''; form.nodeId = ''; form.jsonType = 'string'; form.fieldType = 'string';
form.label = ''; form.description = ''; form.role = '';
form.isForm = true; form.required = false; form.defaultValue = '';
form.loop = false;
+ form.isContainer = false;
+ form.linkRules = [];
+ form._levelFields = [];
form.options = []; form.constraint = {};
// @ts-ignore
form._createParentId = undefined; form._isArrayParent = false;
@@ -276,10 +320,9 @@ function loadForm(data: FieldFormData | null) {
form.jsonType = data.jsonType || 'string'; form.fieldType = data.fieldType;
form.label = data.label || ''; form.description = data.description || ''; form.role = data.role || '';
form.isForm = data.isForm ?? true; form.required = data.required || false;
- form.loop = data.loop ?? false;
+ form.loop = data.loop ?? false;
form.defaultValue = data.defaultValue || '';
form.options = data.options ? [...data.options] : [];
- // 兼容旧格式:如果 options 是字符串数组,转为 { label, value }
if (form.options.length > 0 && typeof (form.options as any)[0] === 'string') {
form.options = (data.options as string[]).map(s => ({ label: s, value: s }));
}
@@ -288,6 +331,9 @@ function loadForm(data: FieldFormData | null) {
form._createParentId = data._createParentId; form._isArrayParent = data._isArrayParent ?? false;
// @ts-ignore
form._childrenCount = data._childrenCount ?? 0;
+ form.isContainer = data.isContainer ?? false;
+ form.linkRules = data.linkRules ? data.linkRules.map((r) => ({ ...r })) : [];
+ form._levelFields = data._levelFields ? data._levelFields.map((l) => ({ ...l })) : [];
if (data.constraint?.uploadRules?.length) {
uploadRules.value = data.constraint.uploadRules.map((r) => ({ ...r }));
} else if (data.constraint?.accept) {
diff --git a/src/components/json-schema-editor/JsonNode.vue b/src/components/json-schema-editor/JsonNode.vue
index 69a8750..2fa5ffa 100644
--- a/src/components/json-schema-editor/JsonNode.vue
+++ b/src/components/json-schema-editor/JsonNode.vue
@@ -92,6 +92,8 @@ interface JsonNodeData {
role?: string;
isForm?: boolean;
required?: boolean;
+ isContainer?: boolean;
+ linkRules?: Array>;
defaultValue?: string | number;
options?: Array<{ label: string; value: string }>;
constraint?: Record;
@@ -105,6 +107,8 @@ function hasConfig(node: JsonNodeData): boolean {
if (c.options?.length) return true;
if (c.required) return true;
if (c.isForm === false) return true;
+ if (c.isContainer) return true;
+ if (c.linkRules?.length) return true;
if (c.constraint && Object.keys(c.constraint).length > 0) return true;
const defaultTypes: Record = { string: 'string', number: 'number', boolean: 'switch' };
if (c.fieldType && c.fieldType !== defaultTypes[node.type]) return true;
@@ -128,6 +132,10 @@ function configSummary(node: JsonNodeData): string {
if (c.defaultValue !== undefined && c.defaultValue !== '' && c.defaultValue !== null) {
parts.push(`默认=${c.defaultValue}`);
}
+ if (c.isContainer) {
+ parts.push("容器触发器");
+ if (c.linkRules?.length) parts.push(`联动 ${c.linkRules.length} 条规则`);
+ }
const con = c.constraint;
if (con) {
if (con.minLength || con.maxLength) {
diff --git a/src/components/json-schema-editor/index.vue b/src/components/json-schema-editor/index.vue
index 175bfcd..5c63c10 100644
--- a/src/components/json-schema-editor/index.vue
+++ b/src/components/json-schema-editor/index.vue
@@ -76,6 +76,8 @@ interface FieldConfig {
isForm?: boolean;
required?: boolean;
loop?: boolean;
+ isContainer?: boolean;
+ linkRules?: Array>;
defaultValue?: string | number;
options?: Array<{ label: string; value: string }>;
constraint?: {
@@ -88,6 +90,11 @@ interface FieldConfig {
};
}
+interface AvailableField {
+ key: string;
+ path: string;
+}
+
interface JsonNodeData {
_id: string; key: string; keyEditable: boolean;
type: 'string' | 'number' | 'boolean' | 'null' | 'object' | 'array';
@@ -102,6 +109,9 @@ interface FieldFormData {
jsonType: string;
fieldType: string; label: string; description: string; role: string;
isForm: boolean; required: boolean; loop: boolean; defaultValue: string; options: Array<{ label: string; value: string }>;
+ isContainer: boolean;
+ linkRules: Array>;
+ _levelFields: AvailableField[];
constraint: {
minLength?: number; maxLength?: number; pattern?: string;
min?: number; max?: number; numberType?: string;
@@ -205,6 +215,87 @@ function defaultFormType(jsonType: string): string {
return m[jsonType] || 'string';
}
+/** 收集当前字段同级及以上的所有可选字段(平铺,含完整路径) */
+function getAvailableFields(nodeId: string): AvailableField[] {
+ const result: AvailableField[] = [];
+ const currentKey = findNodeById(nodeId)?.key;
+ if (!currentKey) return [];
+
+ // 构建祖先链 [root, ..., currentNode]
+ const path: { node: JsonNodeData; parent: JsonNodeData | null }[] = [];
+ (function walk(n: JsonNodeData, p: JsonNodeData | null): boolean {
+ if (n._id === nodeId) { path.push({ node: n, parent: p }); return true; }
+ for (const c of n.children) { if (walk(c, n)) { path.push({ node: n, parent: p }); return true; } }
+ return false;
+ })(rootNode.value, null);
+ if (path.length < 2) return [];
+
+ // 从当前字段开始逐层向上,收集同辈字段(含容器自身)
+ const seen = new Set();
+ // path = [current, ..., root],所以从倒数第 2 个(root 的直子)开始向前遍历
+ for (let i = path.length - 2; i >= 0; i--) {
+ const container = path[i];
+ const parent = container.parent;
+
+ // 跳过数组层级(数组元素的兄弟是索引号,无意义)
+ if (parent && parent.type === 'array') continue;
+
+ // 根级(path.length-2 = root 的直子)只包含容器自身
+ if (i === path.length - 2) {
+ if (container.node.key !== currentKey && !seen.has(container.node.key)) {
+ seen.add(container.node.key);
+ result.push({ key: container.node.key, path: getNodeJsonPath(container.node._id) });
+ }
+ continue;
+ }
+
+ // 其他层级:收集所有同辈字段(含容器自身)
+ const siblings = parent ? parent.children : container.node.children;
+ for (const sib of siblings) {
+ if (sib.key === currentKey) continue;
+ if (seen.has(sib.key)) continue;
+ seen.add(sib.key);
+
+ result.push({ key: sib.key, path: getNodeJsonPath(sib._id) });
+ }
+ }
+ return result;
+}
+
+/** 计算某个节点在 JSON 中的完整路径(如 messages.attrs[1].content.attrs[1].type) */
+function getNodeJsonPath(nodeId: string): string {
+ // 构建祖先链 [root, ..., target]
+ const chain: JsonNodeData[] = [];
+ (function walk(n: JsonNodeData): boolean {
+ if (n._id === nodeId) { chain.unshift(n); return true; }
+ for (const c of n.children) { if (walk(c)) { chain.unshift(n); return true; } }
+ return false;
+ })(rootNode.value);
+ if (chain.length < 1) return '';
+
+ let path = '';
+ let inAttrs = true;
+
+ for (let i = 1; i < chain.length; i++) {
+ const node = chain[i];
+ const parent = chain[i - 1];
+
+ if (i === 1) {
+ path = node.key;
+ inAttrs = true;
+ } else if (parent.type === 'array') {
+ path += `.attrs[${node.key}]`;
+ inAttrs = true;
+ } else if (inAttrs) {
+ path += `.${node.key}`;
+ inAttrs = false;
+ } else {
+ path += `.attrs.${node.key}`;
+ inAttrs = true;
+ }
+ }
+ return path;
+}
// --- paste JSON ---
const showPasteDialog = ref(false);
@@ -286,6 +377,13 @@ function formToConfig(form: FieldFormData): FieldConfig {
cfg.isForm = form.isForm ?? true;
cfg.required = form.required ?? false;
cfg.loop = form.loop ?? false;
+ // isContainer + linkRules
+ if (form.isContainer && form.linkRules?.length > 0) {
+ cfg.isContainer = true;
+ cfg.linkRules = form.linkRules
+ .filter((r) => r && typeof r === 'object' && Object.keys(r).length > 0)
+ .map((r) => ({ ...r }));
+ }
// 非骨架字段:有值才发
if (form.description) cfg.description = form.description;
if (form.role) cfg.role = form.role;
@@ -313,6 +411,8 @@ function hasAnyConfig(node: JsonNodeData): boolean {
if (c.required) return true;
if (c.loop) return true;
if (c.isForm === false) return true;
+ if (c.isContainer) return true;
+ if (c.linkRules?.length) return true;
if (c.constraint && Object.keys(c.constraint).length > 0) return true;
if (c.fieldType && c.fieldType !== defaultFormType(node.type)) return true;
return false;
@@ -323,6 +423,8 @@ function openFieldDialog(nodeId: string) {
const node = findNodeById(nodeId);
if (!node) return;
const cfg = node.config || {};
+ const isScalar = node.type !== 'object' && node.type !== 'array';
+ const levelFields = isScalar ? getAvailableFields(nodeId) : [];
fieldDialog.value = {
visible: true,
typeOptions: typeOptionList,
@@ -338,6 +440,9 @@ function openFieldDialog(nodeId: string) {
isForm: cfg.isForm ?? true,
required: cfg.required ?? false,
loop: cfg.loop ?? false,
+ isContainer: cfg.isContainer ?? false,
+ linkRules: cfg.linkRules ? cfg.linkRules.map((r) => ({ ...r })) : [],
+ _levelFields: levelFields,
defaultValue: String(cfg.defaultValue ?? ''),
options: cfg.options || [],
constraint: cfg.constraint ? { ...cfg.constraint } : {},
@@ -370,6 +475,9 @@ function openAddChildDialog(parentNodeId: string) {
isForm: true,
required: false,
loop: false,
+ isContainer: false,
+ linkRules: [],
+ _levelFields: [],
defaultValue: '',
options: [],
constraint: {},