diff --git a/packages/amis-editor-core/package.json b/packages/amis-editor-core/package.json index a9b95098e..cbad48d91 100644 --- a/packages/amis-editor-core/package.json +++ b/packages/amis-editor-core/package.json @@ -1,6 +1,6 @@ { "name": "amis-editor-core", - "version": "5.2.0-beta.74", + "version": "5.2.1-alpha.3", "description": "amis 可视化编辑器", "main": "lib/index.min.js", "types": "lib/index.d.ts", diff --git a/packages/amis-editor-core/src/component/Editor.tsx b/packages/amis-editor-core/src/component/Editor.tsx index 335803d53..4cd865e37 100644 --- a/packages/amis-editor-core/src/component/Editor.tsx +++ b/packages/amis-editor-core/src/component/Editor.tsx @@ -16,6 +16,7 @@ import {PopOverForm} from './PopOverForm'; import {ContextMenuPanel} from './Panel/ContextMenuPanel'; import {LeftPanels} from './Panel/LeftPanels'; import {RightPanels} from './Panel/RightPanels'; +import type {VariableGroup, VariableOptions} from '../variable'; export interface EditorProps extends PluginEventListener { value: SchemaObject; @@ -89,6 +90,11 @@ export interface EditorProps extends PluginEventListener { }; }; + /** 上下文变量 */ + variables?: VariableGroup[]; + /** 变量配置 */ + variableOptions?: VariableOptions; + onUndo?: () => void; // 用于触发外部 undo 事件 onRedo?: () => void; // 用于触发外部 redo 事件 onSave?: () => void; // 用于触发外部 save 事件 diff --git a/packages/amis-editor-core/src/manager.ts b/packages/amis-editor-core/src/manager.ts index b231dac1c..ed4a34271 100644 --- a/packages/amis-editor-core/src/manager.ts +++ b/packages/amis-editor-core/src/manager.ts @@ -2,7 +2,7 @@ * @file 把一些功能性的东西放在了这个里面,辅助 compoennt/Editor.tsx 组件的。 * 编辑器非 UI 相关的东西应该放在这。 */ -import {getRenderers, RenderOptions} from 'amis-core'; +import {getRenderers, RenderOptions, mapTree} from 'amis-core'; import { PluginInterface, BasicPanelItem, @@ -53,12 +53,16 @@ import {reaction} from 'mobx'; import {hackIn, makeSchemaFormRender, makeWrapper} from './component/factory'; import {env} from './env'; import debounce from 'lodash/debounce'; +import sortBy from 'lodash/sortBy'; +import reverse from 'lodash/reverse'; +import cloneDeep from 'lodash/cloneDeep'; import {openContextMenus, toast, alert, DataScope, DataSchema} from 'amis'; import {parse, stringify} from 'json-ast-comments'; import {EditorNodeType} from './store/node'; import {EditorProps} from './component/Editor'; import findIndex from 'lodash/findIndex'; import {EditorDNDManager} from './dnd'; +import {VariableManager} from './variable'; import {IScopedContext} from 'amis'; import {SchemaObject, SchemaCollection} from 'amis/lib/Schema'; import type {RendererConfig} from 'amis-core/lib/factory'; @@ -139,6 +143,9 @@ export class EditorManager { dataSchema: DataSchema; readonly isInFrame: boolean = false; + /** 变量管理 */ + readonly variableManager; + constructor( readonly config: EditorManagerConfig, readonly store: EditorStoreType, @@ -179,8 +186,15 @@ export class EditorManager { this.dnd = parent?.dnd || new EditorDNDManager(this, store); this.dataSchema = parent?.dataSchema || new DataSchema(config.schemas || []); - this.dataSchema.current.tag = '系统变量'; + + /** 初始化变量管理 */ + this.variableManager = new VariableManager( + this.dataSchema, + config?.variables, + config?.variableOptions + ); + if (isInFrame) { return; } diff --git a/packages/amis-editor-core/src/variable.ts b/packages/amis-editor-core/src/variable.ts new file mode 100644 index 000000000..f29a7ed09 --- /dev/null +++ b/packages/amis-editor-core/src/variable.ts @@ -0,0 +1,227 @@ +/** + * @file 变量管理 + * @desc 主要用于编辑器外部注入变量的管理,用于变量绑定 + */ + +import sortBy from 'lodash/sortBy'; +import cloneDeep from 'lodash/cloneDeep'; +import reverse from 'lodash/reverse'; +import pick from 'lodash/pick'; +import {JSONSchema, DataSchema, mapTree, findTree} from 'amis-core'; +import type {Option} from 'amis-core'; + +export interface VariableGroup { + /** 变量命名空间 */ + name: string; + /** 标题显示名称 */ + title: string; + /* 父节点scope id */ + parentId: string; + /** 顺序 */ + order: number; + /** 结构定义,根结点必须为object */ + schema: JSONSchema; +} + +export interface VariableOptions { + /** 变量Schema被添加到Scope之前触发 */ + beforeScopeInsert?: ( + context: VariableManager, + schema: JSONSchema + ) => JSONSchema; + /** 事件:变量Schema被添加到Scope之后触发 */ + afterScopeInsert?: (context: VariableManager) => void; + /** 获取上下文数据结构时触发,可以自定义返回的数据结构 */ + onContextSchemaChange?: ( + context: VariableManager, + schema: JSONSchema[] + ) => JSONSchema[]; + /** 获取上下文数据Options时触发,可以自定义返回的数据结构 */ + onContextOptionChange?: ( + context: VariableManager, + option: Option[], + type: 'normal' | 'formula' + ) => Option[]; +} + +export class VariableManager { + /* 变量列表 */ + readonly variables: VariableGroup[]; + /* 上下文结构 */ + readonly dataSchema: DataSchema; + /* 变量管理配置 */ + readonly options: VariableOptions; + + constructor( + dataSchema: DataSchema | undefined, + variables: VariableGroup[] | undefined, + options: VariableOptions | undefined + ) { + this.variables = Array.isArray(variables) + ? sortBy(cloneDeep(variables), [item => item.order ?? 1]) + : []; + this.dataSchema = + dataSchema instanceof DataSchema ? dataSchema : new DataSchema([]); + this.options = pick(options, [ + 'beforeScopeInsert', + 'afterScopeInsert', + 'onContextSchemaChange', + 'onContextOptionChange' + ]); + + this.init(); + } + + /** + * 初始化变量,预期的结构类似: + * ──系统变量(root) + * └── 组织变量 + * └── 应用变量 + * └── 页面变量 + * └── ... + */ + init() { + const variables = this.variables; + const dataSchema = this.dataSchema; + const {beforeScopeInsert, afterScopeInsert} = this.options ?? {}; + + variables.forEach(item => { + const {parentId, name: scopeName, title: tagName} = item; + let schema = item.schema; + + if (!dataSchema.hasScope(parentId)) { + return; + } + + dataSchema.switchTo(parentId); + + if (dataSchema.hasScope(scopeName)) { + dataSchema.removeScope(scopeName); + } + + if (beforeScopeInsert && typeof beforeScopeInsert === 'function') { + schema = beforeScopeInsert(this, schema); + } + + /** 初始化变量Scope */ + dataSchema.addScope(schema, scopeName); + dataSchema.switchTo(scopeName); + /** 这里的Tag指变量的命名空间中文名称 */ + dataSchema.current.tag = tagName; + + if (afterScopeInsert && typeof afterScopeInsert === 'function') { + afterScopeInsert(this); + } + }); + + dataSchema.switchToRoot(); + } + + /** + * 获取外部变量的上下文数据结构 + */ + getVariableContextSchema() { + let variableSchemas: JSONSchema[] = []; + const {onContextSchemaChange} = this.options ?? {}; + + if (this.variables && this.variables?.length > 0) { + variableSchemas = this.variables + .map(item => { + if (this.dataSchema.hasScope(item.name)) { + const varScope = this.dataSchema.getScope(item.name); + + /** 变量的Scope只有一个根结点 */ + return varScope.schemas.length > 0 ? varScope.schemas[0] : null; + } + return null; + }) + .filter((item): item is JSONSchema => item !== null); + } + + if (onContextSchemaChange && typeof onContextSchemaChange === 'function') { + variableSchemas = onContextSchemaChange(this, variableSchemas); + } + + return variableSchemas; + } + + /** + * 获取公式编辑器中变量的Option结构 + */ + getVariableFormulaOptions(reverseOrder: boolean = false) { + const {onContextOptionChange} = this.options ?? {}; + let options: Option[] = []; + + if (this.variables && this.variables?.length > 0) { + this.variables.forEach(item => { + if (this.dataSchema.hasScope(item.name)) { + const varScope = this.dataSchema.getScope(item.name); + const children = mapTree(varScope.getDataPropsAsOptions(), item => ({ + ...item, + /** tag默认会被赋值为description,这里得替换回来 */ + tag: item.type + })); + + if (varScope.tag) { + options.push({label: varScope.tag, children}); + } else { + options.push(...children); + } + } + }); + } + + if (onContextOptionChange && typeof onContextOptionChange === 'function') { + options = onContextOptionChange(this, options, 'formula'); + } + + return reverseOrder ? options : reverse(options); + } + + /** + * 获取通用的树形结构 + */ + getVariableOptions() { + const {onContextOptionChange} = this.options ?? {}; + let options: Option[] = + this.getVariableFormulaOptions(false)?.[0]?.children ?? []; + + options = mapTree( + options, + (item: Option, key: number, level: number, paths: Option[]) => { + return { + ...item, + valueExpression: + typeof item.value === 'string' && !item.value.startsWith('${') + ? `\${${item.value}}` + : item.value + }; + } + ); + + if (onContextOptionChange && typeof onContextOptionChange === 'function') { + options = onContextOptionChange(this, options, 'normal'); + } + + return options; + } + + /** + * 根据变量路径获取变量名称 + */ + getNameByPath(path: string, valueField = 'value', labelField = 'label') { + if (!path || typeof path !== 'string') { + return ''; + } + + const options = this.getVariableOptions(); + const node = findTree( + options, + item => item[valueField ?? 'value'] === path + ); + + return node + ? node[labelField ?? 'label'] ?? node[valueField ?? 'value'] ?? '' + : ''; + } +}