feat(json-schema-editor): JSON编辑器新增 isContainer 容器字段联动隐藏配置
将旧的分级 upLevel/remove 联动规则改为基于完整路径的平铺结构。
新增特性:
- 标量字段(string/number/boolean)配置面板中增加"容器字段"开关
- 开关打开后展示"联动隐藏"下拉菜单,列出当前字段同级及以上所有可选字段
- 下拉展示格式:"字段key — 完整路径"(如 type — messages.attrs[0].content.attrs[0].type)
- 选中后以 { key: path } 格式输出,渲染器按路径直接定位隐藏字段
核心改动:
- index.vue: 新增 getAvailableFields / getNodeJsonPath 替代旧的分级遍历逻辑
- getAvailableFields: 从当前字段向上遍历,跳过数组层级,根级只含容器自身
- getNodeJsonPath: inAttrs 状态机生成完整路径(messages.attrs[0].content.attrs[0].type)
- linkRules 类型从 Array<{ upLevel, remove[] }> 改为 Array<Record<string, string>>
- formToConfig / hasAnyConfig 同步更新
- FieldConfigDialog.vue: 重构联动隐藏UI,单 el-select multiple 替代分级 checkbox
- JsonNode.vue: 类型同步 + 摘要显示容器触发器及规则数
数据格式示例:
"isContainer": true,
"linkRules": [
{ "type": "messages.attrs[0].content.attrs[0].type" }
]
This commit is contained in:
@@ -76,6 +76,8 @@ interface FieldConfig {
|
||||
isForm?: boolean;
|
||||
required?: boolean;
|
||||
loop?: boolean;
|
||||
isContainer?: boolean;
|
||||
linkRules?: Array<Record<string, string>>;
|
||||
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<Record<string, string>>;
|
||||
_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<string>();
|
||||
// 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: {},
|
||||
|
||||
Reference in New Issue
Block a user