diff --git a/packages/amis-editor-core/scss/_mixin.scss b/packages/amis-editor-core/scss/_mixin.scss index 7b9313c3d..26699b50b 100644 --- a/packages/amis-editor-core/scss/_mixin.scss +++ b/packages/amis-editor-core/scss/_mixin.scss @@ -84,7 +84,8 @@ } @mixin panel-sm-content { - --ColorPicker-fontSize: var(--fontSizeBase); + --Form-fontSize: #{$Editor-right-panel-font-size}; + --ColorPicker-fontSize: var($Editor-right-panel-font-size); --fontSizeBase: #{$Editor-right-panel-font-size}; --Form-item-fontSize: #{$Editor-right-panel-font-size}; --Button--md-fontSize: #{$Editor-right-panel-font-size}; diff --git a/packages/amis-editor-core/scss/_rightPanel.scss b/packages/amis-editor-core/scss/_rightPanel.scss index b8d0282fa..bb2bf7523 100644 --- a/packages/amis-editor-core/scss/_rightPanel.scss +++ b/packages/amis-editor-core/scss/_rightPanel.scss @@ -365,7 +365,7 @@ $category-2-height: px2rem(32px); &-cont { flex: 1 1 auto; padding: 0; - overflow-y: overlay; + overflow-y: overlay !important; @include minScrollBar(); } diff --git a/packages/amis-editor-core/scss/control/_api-control.scss b/packages/amis-editor-core/scss/control/_api-control.scss index 87623792f..486dc19d7 100644 --- a/packages/amis-editor-core/scss/control/_api-control.scss +++ b/packages/amis-editor-core/scss/control/_api-control.scss @@ -19,11 +19,64 @@ @include flexBox(); .ae-ApiControl-input { + background: var(--Form-input-bg); + border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor); + border-radius: var(--Form-input-borderRadius); + line-height: var(--Form-input-lineHeight); + padding: var(--Form-input-paddingY) var(--Form-input-paddingX); + font-size: var(--Form-input-fontSize); + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: center; flex: 1; margin-right: #{px2rem(10px)}; + max-width: calc(100% - 52px); + height: var(--Button--sm-height); + + & > input { + flex-basis: 5rem; + flex-grow: 1; + outline: 0; + background: transparent; + border: 0; + color: var(--Form-input-color); + width: 100%; + height: calc(var(--Form-input-lineHeight) * var(--Form-input-fontSize)); + } } } + &-highlight { + width: 100%; + max-width: calc(100% - var(--fontSizeLg)); + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: center; + + &-tag { + display: inline-block; + background: #007bff; + padding: 3px 5px; + margin: 0 1px; + color: #fff; + font-size: 12px; + line-height: 14px; + height: 20px; + border-radius: #{px2rem(4px)}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 90%; + } + } + + &-icon { + width: var(--fontSizeLg) !important; + height: var(--fontSizeLg) !important; + } + &-dialog { &-body { padding: 0 !important; @@ -59,3 +112,12 @@ } } } + +.ae-ApiControl-PickerBtn { + padding: 0; + + &:hover > svg path { + stroke: var(--primary); + color: var(--primary); + } +} diff --git a/packages/amis-editor-core/scss/control/_databinding-control.scss b/packages/amis-editor-core/scss/control/_databinding-control.scss new file mode 100644 index 000000000..c35c276f8 --- /dev/null +++ b/packages/amis-editor-core/scss/control/_databinding-control.scss @@ -0,0 +1,85 @@ +.ae-DataBindingList { + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + align-items: stretch; + height: px2rem(350px); + border: 1px solid rgba(232, 233, 235, 1); + border-radius: px2rem(4px); + + &-searchBox { + width: auto; + padding: #{px2rem(12px)}; + + & > div { + width: 100% !important; + } + } + + &-body { + @include minScrollBar(); + flex: 1; + overflow-x: hidden; + overflow-y: auto; + } + + &-collapse { + border: none; + background: #f7f7f9; + + &-title { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: unset; + padding: #{px2rem(5px)} #{px2rem(12px)}; + background: transparent; + font-size: var(--fontSizeSm); + font-weight: bold; + position: relative; + + .#{$ns}DataSourceList-expandIcon { + font-size: var(--fontSizeSm); + line-height: var(--fontSizeXl); + transform-origin: #{px2rem(7px)} #{px2rem(9px)}; + transition: transform 0.2s; + position: absolute; + right: #{px2rem(6px)}; + } + } + + &-body { + background: #fff; + color: #303540; + + > div { + padding: 5px 0; + } + } + } + + &-item { + cursor: pointer; + padding: 0 var(--gap-xl); // 和标题对齐不好看,加个缩进 + height: px2rem(32px); + line-height: px2rem(32px); + color: #303540; + font-weight: 400; + + &:hover { + background: var(--Tree-item-onHover-bg); + } + + &.is-active { + color: var(--primary); + background: var(--Tree-item-onHover-bg); + } + } + + &-empty { + color: #b4b6ba; + padding-top: px2rem(10px); + text-align: center; + vertical-align: middle; + } +} diff --git a/packages/amis-editor-core/scss/control/_feature-control.scss b/packages/amis-editor-core/scss/control/_feature-control.scss new file mode 100644 index 000000000..13a38fd5a --- /dev/null +++ b/packages/amis-editor-core/scss/control/_feature-control.scss @@ -0,0 +1,64 @@ +.ae-FeatureControl { + &-features { + margin: 0; + padding: 0; + } + + &Item { + display: flex; + height: 30px; + margin-bottom: 12px; + + :not(:last-child) { + margin-right: px2rem(8px); + } + + &-go { + flex-grow: 1; + } + + &-label { + flex-grow: 1; + height: px2rem(32px); + display: block; + line-height: px2rem(32px); + padding: 0 px2rem(8px); + border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor); + border-radius: var(--Form-input-borderRadius); + text-align: center; + } + + &-action { + padding: 0 6px; + svg { + width: px2rem(16px); + height: px2rem(16px); + fill: #000; + } + + &:hover { + svg { + fill: $Editor-theme; + } + } + } + } + + &-action { + display: block; + width: 100%; + + &--btn { + width: 100%; + border-color: $Editor-theme; + color: $Editor-theme; + } + + &--menus { + width: calc(100% - 12px); + margin-left: 6px; + text-align: center; + } + } + +} diff --git a/packages/amis-editor-core/scss/control/_formItem-control.scss b/packages/amis-editor-core/scss/control/_formItem-control.scss index b53ca5277..f72f9ed11 100644 --- a/packages/amis-editor-core/scss/control/_formItem-control.scss +++ b/packages/amis-editor-core/scss/control/_formItem-control.scss @@ -120,4 +120,9 @@ width: 100%; height: px2rem(32px); } + +} + +.form-item-gap { + margin-bottom: var(--Form-item-gap); } diff --git a/packages/amis-editor-core/scss/control/_go-config.scss b/packages/amis-editor-core/scss/control/_go-config.scss new file mode 100644 index 000000000..c5ac4b6bb --- /dev/null +++ b/packages/amis-editor-core/scss/control/_go-config.scss @@ -0,0 +1,29 @@ +.ae-GoConfig { + height: 32px; + line-height: 32px; + position: relative; + background-color: #fff; + text-align: center; + font-size: $Editor-right-panel-font-size; + border: 1px solid #e6e6e8; + border-radius: $Editor-borderRadius; + + &-trigger { + display: none; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background-color: rgba($color: #000000, $alpha: .4); + color: #fff; + cursor: pointer; + } + + &:hover { + .ae-GoConfig-trigger { + display: block; + } + } + +} diff --git a/packages/amis-editor-core/scss/editor.scss b/packages/amis-editor-core/scss/editor.scss index 33b62127b..d47708b96 100644 --- a/packages/amis-editor-core/scss/editor.scss +++ b/packages/amis-editor-core/scss/editor.scss @@ -26,6 +26,9 @@ @import './control/formula-control'; @import './control/dateshortcut-control'; @import './control/badge-control'; +@import './control/go-config'; +@import './control/feature-control'; +@import './control/databinding-control'; @import './control/event-action'; /* 样式控件 */ @@ -1051,55 +1054,26 @@ .ae-Region-placeholder { display: none; + text-align: center; + color: var(--text--muted-color); + user-select: none; + text-align: center; + text-transform: uppercase; + border: 1px dashed rgb(206, 208, 211); + background: rgba(10, 19, 37, 0.05); - // &:first-child { - // position: relative; - // display: flex; - // flex: 1; - // flex-direction: column; - // justify-content: center; - // min-width: 60px; - // padding: 0 5px; - // -webkit-user-select: none; - // user-select: none; - // text-align: center; - // text-transform: uppercase; - // color: var(--text--muted-color); - // // border: 1px dashed rgb(206, 208, 211); - // background: rgba(10, 19, 37, 0.05); - // } + &:first-child { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } } [data-region] { position: relative; min-height: 34px; - &:empty { - min-width: 20px; - - &:before { - height: 100%; - content: attr(data-region-placeholder); - position: relative; - display: flex; - flex: 1; - flex-direction: column; - justify-content: center; - - padding: 0 5px; - -webkit-user-select: none; - user-select: none; - text-align: center; - text-transform: uppercase; - color: rgb(108, 113, 124); - border: 1px dashed rgb(206, 208, 211); - background: rgba(10, 19, 37, 0.05); - } - } - // &.is-region-active { - // min-height: 34px; - // } - &.is-dragenter { background-color: #fff; } @@ -1503,10 +1477,27 @@ div.ae-DragImage { .ae-ApiSample { min-width: 200px; + max-height: 300px; + + &-desc { + font-size: var(--fontSizeSm); + display: inline-block; + margin-top: #{px2rem(5px)}; + color: #84868c; + } + + &-icon { + --Remark-onHover-bg: #{$Editor-theme-color}; + + & > i { + border: none; + padding: #{px2rem(10px)}; + border-radius: #{px2rem(3px)}; + } + } > pre { overflow: auto; - border: 1px solid #999; page-break-inside: avoid; display: block; padding: 9.5px; @@ -1516,12 +1507,13 @@ div.ae-DragImage { color: #333; word-break: break-all; word-wrap: break-word; - background-color: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; + background-color: #f7f7f9; + border-radius: #{$Editor-borderRadius}; + border: none; > code { white-space: pre; + color: #151a26; } } @@ -1558,8 +1550,66 @@ div.ae-DragImage { } } +.ae-InputVariable { + width: 100%; + flex-wrap: nowrap; + + > span { + margin-left: auto; + cursor: pointer; + } +} + .ae-collapse-checkbox{ label{ margin-right: 0; } } + +.ae-Scaffold-Modal { + width: px2rem(700px); + @include panel-sm-content(); + + .ae-Steps { + margin: auto; + max-width: px2rem(350px); + --Steps-title-fontsize: #{px2rem(14px)}; + + &-Icon { + width: px2rem(22px) !important; + height: px2rem(22px) !important; + font-size: px2rem(12px) !important; + } + } + + &-Tabs { + --Tabs-linkFontSize: #{px2rem(12px)}; + } +} + +.ae-Button--link { + display: inline-flex; + align-items: center; + padding: 0 !important; + + svg { + width: 12px; + margin-right: 4px !important; + } +} + +.ae-Fields-Setting { + &-Item { + display: flex; + height: px2rem(32px); + margin-bottom: 12px; + padding: 0 px2rem(8px); + border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor); + border-radius: var(--Form-input-borderRadius); + + &-label { + flex-grow: 1; + line-height: px2rem(30px); + } + } +} \ No newline at end of file diff --git a/packages/amis-editor-core/src/builder/ApiBuilder.ts b/packages/amis-editor-core/src/builder/ApiBuilder.ts new file mode 100644 index 000000000..4277140f9 --- /dev/null +++ b/packages/amis-editor-core/src/builder/ApiBuilder.ts @@ -0,0 +1,584 @@ +/** + * API数据源处理器 + */ + +import {Schema, toast} from 'amis'; +import { + DSBuilder, + DSFeature, + DSFeatureType, + DSGrain, + registerDSBuilder +} from './DSBuilder'; +import cloneDeep from 'lodash/cloneDeep'; +import {getEnv} from 'mobx-state-tree'; +import {ButtonSchema} from 'amis/lib/renderers/Action'; +import {FormSchema, SchemaObject} from 'amis/lib/Schema'; + +import type {DSSourceSettingFormConfig} from './DSBuilder'; +import {getSchemaTpl, tipedLabel} from '../tpl'; + +class APIBuilder extends DSBuilder { + public static type = 'api'; + + name = '接口'; + + order = 0; + + public match = (value: any, schema?: SchemaObject) => { + // https://aisuda.bce.baidu.com/amis/zh-CN/docs/types/api + if ( + (typeof value === 'string' && + /^(get|post|put|delete|option):/.test(value)) || + (typeof value === 'object' && value.url) + ) { + return true; + } + + return false; + }; + + public static accessable = (controlType: string, propKey: string) => { + return true; + }; + + public features: Array = [ + 'List', + 'Insert', + 'View', + 'Edit', + 'Delete', + 'BulkEdit', + 'BulkDelete', + 'Import', + 'Export', + 'SimpleQuery', + 'FuzzyQuery' + ]; + + public makeSourceSettingForm( + config: DSSourceSettingFormConfig + ): SchemaObject[] { + let {name, label, feat, inCrud, inScaffold} = config; + + if (['Import', 'Export', 'SimpleQuery', 'FuzzyQuery'].includes(feat)) { + return []; + } + + label = + label ?? + (inCrud && feat !== 'List' ? DSFeature[feat].label + '接口' : '接口'); + name = name ?? (inScaffold ? DSFeature[feat].value + 'Api' : 'api'); + + let sampleBuilder = null; + let apiDesc = null; + switch (feat) { + case 'Insert': + (label as any) = tipedLabel( + label, + `用来保存数据, 表单提交后将数据传入此接口。
+ 接口响应体要求(如果data中有数据,该数据将被合并到表单上下文中):
+ ${JSON.stringify({status: 0, msg: '', data: {}}, null, '
')}` + ); + break; + + case 'List': + (label as any) = tipedLabel( + label, + `接口响应体要求:
+ ${JSON.stringify( + {status: 0, msg: '', items: {}, page: 0, total: 0}, + null, + '
' + )}` + ); + break; + } + + return [ + getSchemaTpl('apiControl', { + label, + name, + sampleBuilder, + apiDesc + }) + ] + .concat( + feat === 'Edit' && !inCrud + ? getSchemaTpl('apiControl', { + label: tipedLabel( + '初始化接口', + `接口响应体要求:
+ ${JSON.stringify({status: 0, msg: '', data: {}}, null, '
')}` + ), + name: 'initApi' + }) + : null + ) + .concat( + feat === 'List' && inCrud && inScaffold + ? this.makeFieldsSettingForm({ + feat, + setting: true + }) + : null + ) + .filter(Boolean); + } + + public async getContextFileds(config: { + schema: any; + sourceKey: string; + feat: DSFeatureType; + }) { + return config.schema.__fields; + } + + public async getAvailableContextFileds(config: { + schema: any; + sourceKey: string; + feat: DSFeatureType; + }) { + if (!config.schema.__fields) { + return; + } + + return [ + { + label: '字段', + value: 'fields', + children: config.schema.__fields + } + ]; + } + + public makeFieldsSettingForm(config: { + sourceKey?: string; + feat: DSFeatureType; + inCrud?: boolean; + setting?: boolean; + inScaffold?: boolean; + }) { + let {sourceKey, feat, inCrud, setting, inScaffold} = config; + if ( + inScaffold === false || + ['Import', 'Export', 'FuzzyQuery'].includes(feat) + ) { + return []; + } + + sourceKey = sourceKey ?? `${DSFeature[feat].value}Api`; + const key = setting ? '__fields' : `${DSFeature[feat].value}Fields`; + const hasInputType = + ['Edit', 'Insert'].includes(feat) || (inCrud && feat === 'List'); + const hasType = ['View', 'List'].includes(feat); + + return ([] as any) + .concat( + inCrud && feat !== 'List' + ? this.makeSourceSettingForm({ + feat, + inScaffold, + inCrud + }) + : null + ) + .concat([ + { + type: 'combo', + className: 'mb-0 ae-Fields-Setting', + joinValues: false, + name: key, + label: inCrud ? `${DSFeature[feat].label}字段` : '字段', + multiple: true, + draggable: true, + addable: false, + removable: false, + itemClassName: 'ae-Fields-Setting-Item', + hidden: setting || !inCrud || ['Delete', 'BulkDelete'].includes(feat), + items: { + type: 'container', + body: [ + { + name: 'checked', + label: false, + mode: 'inline', + className: 'm-0 ml-1', + type: 'checkbox' + }, + { + type: 'tpl', + className: 'ae-Fields-Setting-Item-label', + tpl: '${label}' + } + ] + } + }, + { + type: 'input-table', + label: '字段', + className: 'mb-0', + name: key, + // 非crud,都是定义字段的模式,只有crud,有统一定义字段,因此是选择字段 + visible: setting ?? !inCrud, + removable: true, + columnsTogglable: false, + needConfirm: false, + onChange: (value: any, oldValue: any, model: any, form: any) => { + this.features.forEach(feat => { + const key = `${DSFeature[feat].value}Fields`; + const currentData = form.getValueByName(key); + + const result = cloneDeep(value || []).map((field: any) => { + const exist = currentData?.find( + (f: any) => f.name === field.name + ); + + return { + ...field, + checked: exist ? exist.checked : true + }; + }); + form.setValueByName(key, result); + }); + }, + columns: [ + { + type: 'switch', + name: 'checked', + value: true, + label: '隐藏,默认选中', + visible: false + }, + { + type: 'input-text', + name: 'label', + label: '标题' + }, + { + type: 'input-text', + name: 'name', + label: '绑定字段' + }, + { + type: 'select', + name: 'type', + label: '类型', + visible: hasType, + value: 'tpl', + options: [ + { + value: 'tpl', + label: '文本', + typeKey: 'tpl' + }, + { + value: 'image', + label: '图片', + typeKey: 'src' + }, + { + value: 'date', + label: '日期', + typeKey: 'value' + }, + { + value: 'progress', + label: '进度', + typeKey: 'value' + }, + { + value: 'status', + label: '状态', + typeKey: 'value' + }, + { + value: 'mapping', + label: '映射', + typeKey: 'value' + } + ], + autoFill: { + typeKey: '${typeKey}' + } + }, + { + type: 'select', + name: 'inputType', + label: '输入类型', + visible: hasInputType, + value: 'input-text', + options: [ + { + label: '输入框', + value: 'input-text' + }, + { + label: '多行文本', + value: 'textarea' + }, + { + label: '数字输入', + value: 'input-number' + }, + { + label: '单选框', + value: 'radios' + }, + { + label: '勾选框', + value: 'checkbox' + }, + { + label: '复选框', + value: 'checkboxes' + }, + { + label: '下拉框', + value: 'select' + }, + { + label: '开关', + value: 'switch' + }, + { + label: '日期', + value: 'input-date' + }, + { + label: '表格', + value: 'input-table' + }, + { + label: '文件上传', + value: 'input-file' + }, + { + label: '图片上传', + value: 'input-image' + }, + { + label: '富文本编辑器', + value: 'input-rich-text' + } + ] + } + ] + }, + { + type: 'group', + visible: setting ?? !inCrud, + label: '', + body: [ + { + type: 'grid', + columns: [ + { + body: [ + { + type: 'button', + label: '添加字段', + target: key, + className: 'ae-Button--link', + level: 'link', + icon: 'plus', + actionType: 'add' + } + ] + }, + { + columnClassName: 'text-right', + body: [ + { + type: 'button', + label: '基于接口自动生成字段', + visible: feat === 'Edit' || feat === 'List', + className: 'ae-Button--link', + level: 'link', + // className: 'm-t-xs m-b-xs', + // 列表 或者 不在CRUD中的查看接口等 + onClick: async (e: Event, props: any) => { + const data = props.data; + const schemaFilter = getEnv( + (window as any).editorStore + ).schemaFilter; + const apiKey = + feat === 'Edit' && !inCrud ? 'initApi' : sourceKey; + let api: any = data[apiKey!]; + // 主要是给爱速搭中替换 url + if (schemaFilter) { + api = schemaFilter({ + api + }).api; + } + if (!api) { + toast.warning('请先填写接口'); + } + + const result = await props.env.fetcher(api, data); + + let autoFillKeyValues: Array = []; + let itemExample; + if (feat === 'List') { + const items = result.data?.rows || result.data?.items; + itemExample = items?.[0]; + } else { + itemExample = result.data; + } + + if (itemExample) { + Object.entries(itemExample).forEach( + ([key, value]) => { + autoFillKeyValues.push({ + label: key, + type: 'tpl', + inputType: + typeof value === 'number' + ? 'input-number' + : 'input-text', + name: key + }); + } + ); + props.formStore.setValues({ + [key]: autoFillKeyValues + }); + } else { + toast.warning( + 'API返回格式不正确,请查看接口响应格式要求' + ); + } + } + } + ] + } + ] + } + ] + } + ]) as SchemaObject[]; + } + + public makeFieldFilterSetting(): SchemaObject[] { + return []; + } + + public resolveSourceSchema(config: { + schema: SchemaObject; + setting: any; + name?: string; + feat?: DSFeatureType; + inCrud?: boolean; + }): void { + let {name, setting, schema, feat} = config; + name = name ?? 'api'; + // @ts-ignore + schema[name] = setting[feat ? `${DSFeature[feat].value}Api` : 'api']; + + // form中需要初始化接口和编辑接口 + if (feat === 'Edit') { + (schema as FormSchema).initApi = setting.initApi; + } + } + + public resolveViewSchema(config: { + setting: any; + feat?: DSFeatureType; + }): SchemaObject[] { + let {setting, feat = 'Edit'} = config; + const fields = setting[`${DSFeature[feat].value}Fields`] || []; + return fields + .filter((i: any) => i.checked) + .map((field: any) => ({ + type: field.type, + [field.typeKey || 'value']: '${' + field.name + '}' + })); + } + + public resolveTableSchema(config: {schema: any; setting: any}): void { + let {schema, setting} = config; + const fields = setting.listFields.filter((i: any) => i.checked) || []; + schema.columns = this.makeTableColumnsByFields(fields); + } + + public makeTableColumnsByFields(fields: any[]) { + return fields.map((field: any) => ({ + type: field.type, + title: field.label, + key: field.name, + [field.typeKey || 'value']: '${' + field.name + '}' + })); + } + + public resolveCreateSchema(config: { + schema: FormSchema; + setting: any; + feat: 'Insert' | 'Edit' | 'BulkEdit'; + name?: string; + inCrud?: boolean; + inScaffold?: boolean; + }): void { + let {schema, setting, feat, name} = config; + const fields = setting[`${DSFeature[feat].value}Fields`] || []; + // @ts-ignore + schema[name ?? 'api'] = setting[DSFeature[feat].value + 'Api']; + schema.initApi = setting['initApi']; + schema.body = fields + .filter((i: any) => i.checked) + .map((field: any) => ({ + type: field.inputType, + name: field.name, + label: field.label + })); + } + + public resolveDeleteSchema(config: { + schema: ButtonSchema; + setting: any; + feat: 'BulkDelete' | 'Delete'; + name?: string | undefined; + }) { + const {schema, setting, feat} = config; + schema.onEvent = Object.assign(schema.onEvent ?? {}, { + click: { + actions: [] + } + }); + + const api = { + ...(setting[`${DSFeature[feat].value}Api`] || {}) + }; + if (feat === 'Delete') { + api.data = { + id: '${item.id}' + }; + } else { + api.data = { + ids: '${ARRAYMAP(selectedItems, item=> item.id)}' + }; + } + + schema.onEvent.click.actions.push({ + actionType: 'ajax', + args: {api} + }); + } + + public resolveSimpleFilterSchema(config: {setting: any}) { + const {setting} = config; + const fields = setting.simpleQueryFields || []; + return fields + .filter((i: any) => i.checked) + .map((field: any) => ({ + type: field.inputType, + name: field.name, + label: field.label + })); + } + + public resolveAdvancedFilterSchema(config: {setting: any}) { + return; + } +} + +registerDSBuilder(APIBuilder); diff --git a/packages/amis-editor-core/src/builder/DSBuilder.ts b/packages/amis-editor-core/src/builder/DSBuilder.ts new file mode 100644 index 000000000..945d278b0 --- /dev/null +++ b/packages/amis-editor-core/src/builder/DSBuilder.ts @@ -0,0 +1,368 @@ +/** + * 数据源构造器,可用于对接当前amis中的扩展数据源 + */ + +import {ButtonSchema} from 'amis/lib/renderers/Action'; +import {CRUD2Schema} from 'amis/lib/renderers/CRUD2'; +import {FormSchema, SchemaObject} from 'amis/lib/Schema'; + +/** + * 数据源所需操作,目前是因为schema从后端来 + */ +export enum DSBehavior { + create = 'create', + view = 'view', + update = 'update', + table = 'table', + filter = 'filter' +} + +export interface DSField { + value: string; + label: string; + [propKey: string]: any; +} + +export interface DSFieldGroup { + value: string; + label: string; + children: DSField[]; + [propKey: string]: any; +} + +/** + * 支持数据源配置的一些属性名 + */ +export enum DSGrain { + entity = 'entity', + list = 'list', + piece = 'piece' +} + +export const DSFeature = { + List: { + value: 'list', + label: '列表' + }, + Insert: { + value: 'insert', + label: '新增' + }, + View: { + value: 'view', + label: '详情' + }, + Edit: { + value: 'edit', + label: '编辑' + }, + Delete: { + value: 'delete', + label: '删除' + }, + BulkEdit: { + value: 'bulkEdit', + label: '批量编辑' + }, + BulkDelete: { + value: 'bulkDelete', + label: '批量删除' + }, + Import: { + value: 'import', + label: '导入' + }, + Export: { + value: 'export', + label: '导出' + }, + SimpleQuery: { + value: 'simpleQuery', + label: '简单查询' + }, + FuzzyQuery: { + value: 'fuzzyQuery', + label: '模糊查询' + }, + AdvancedQuery: { + value: 'advancedQuery', + label: '高级查询' + } +}; + +export type DSFeatureType = keyof typeof DSFeature; + +export interface DSSourceSettingFormConfig { + /** 数据源字段名 */ + name?: string; + /** 数据源字段标题 */ + label?: string; + /** 所需要配置的数据粒度 */ + grain?: DSGrain; + /** 数据源所被使用的功能场景 */ + feat: DSFeatureType; + /** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */ + inCrud?: boolean; + /** 是否在脚手架中 */ + inScaffold?: boolean; +} + +/** + * 数据源选择构造器 + */ +export abstract class DSBuilder { + /** + * 数据源名字,中文,可以覆盖同名 + */ + public static type: string; + + public name: string; + + // 数字越小排序越靠前 + public order: number; + + /** + * 数据源schema运行前转换 + */ + public static schemaFilter?: (schema: any) => any; + + /** + * 根据组件、属性名判断是否可以使用这个数据源 + */ + public static accessable: (controlType: string, propKey: string) => boolean; + + public features: Array; + + /** + * 根据值内容和schema配置状态,看是否是当前数据源 + */ + public abstract match(value: any, schema?: SchemaObject): boolean; + + /** + * 生成数据源的配置表单 + */ + public abstract makeSourceSettingForm( + config: DSSourceSettingFormConfig + ): SchemaObject[]; + + public abstract makeFieldsSettingForm(config: { + /** 数据源字段名 */ + sourceKey?: string; + feat: DSFeatureType; + inCrud?: boolean; + inScaffold?: boolean; + /** 初次设置字段还是选择字段 */ + setting?: boolean; + }): SchemaObject[]; + + /** + * 生成字段的筛选配置表单 + */ + public abstract makeFieldFilterSetting(config: { + /** 数据源字段名 */ + sourceKey?: string; + inCrud?: boolean; + inScaffold?: boolean; + schema?: any; + }): SchemaObject[]; + + /** + * 数据源schema生成 + */ + abstract resolveSourceSchema(config: { + /** schema */ + schema: SchemaObject; + /** 数据源配置结果 */ + setting: any; + /** 数据源字段名 */ + name?: string; + feat?: DSFeatureType; + /** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */ + inCrud?: boolean; + inScaffold?: boolean; + }): void; + + /** + * 数据删除schema生成 + */ + abstract resolveDeleteSchema(config: { + schema: ButtonSchema; + setting: any; + feat: 'BulkDelete' | 'Delete'; + name?: string; + }): any; + + /** + * 生成数据创建表单schema + */ + abstract resolveCreateSchema(config: { + /** schema */ + schema: FormSchema; + /** 脚手架配置数据 */ + setting: any; + feat: 'Insert' | 'Edit' | 'BulkEdit'; + /** 数据源字段名 */ + name?: string; + /** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */ + inCrud?: boolean; + }): void; + + /** + * 生成数据表格列 + */ + abstract resolveTableSchema(config: { + /** schema */ + schema: CRUD2Schema; + /** 脚手架配置数据 */ + setting: any; + /** 数据源字段名 */ + name?: string; + /** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */ + inCrud?: boolean; + }): void; + + /** + * 生成数据表格列 + */ + abstract resolveViewSchema(config: { + /** 脚手架配置数据 */ + setting: any; + feat?: DSFeatureType; + }): SchemaObject[]; + + abstract resolveSimpleFilterSchema(config: { + setting: any + }): SchemaObject[]; + + abstract resolveAdvancedFilterSchema(config: { + setting: any; + }): SchemaObject | void; + + abstract makeTableColumnsByFields(fields: any[]): SchemaObject[]; + + /** + * 当前上下文中使用的字段 + */ + abstract getContextFileds(config: { + schema: any, + sourceKey: string, + feat: DSFeatureType + }): Promise; + + /** + * 上下文可以使用的字段 + */ + abstract getAvailableContextFileds(config: { + schema: any, + sourceKey: string, + feat: DSFeatureType + }): Promise; +} + +/** + * 所有的数据源构造器 + */ +const __builders: { + [key: string]: any; +} = {}; + +export const registerDSBuilder = (builderKClass: any) => { + __builders[builderKClass.type] = builderKClass; +}; + +/** + * 构造器管理工具,便于更好的缓存 + */ +export class DSBuilderManager { + /** 所有可用的数据源构造器实例 */ + builders: { + [key: string]: DSBuilder; + } = {}; + + get builderNum() { + return Object.keys(this.builders).length; + } + + constructor(type: string, propKey: string) { + Object.values(__builders) + .filter(builder => builder.accessable?.(type, propKey) ?? true) + .forEach(Builder => { + this.builders[Builder.type] = new Builder(); + }); + } + + resolveBuilderBySetting(setting: any) { + return this.builders[setting.dsType] || Object.values(this.builders)[0]; + } + + resolveBuilderBySchema(schema: any, propKey: string) { + const builders = Object.values(this.builders); + return builders.find(builder => builder.match(schema[propKey])) || builders[0]; + } + + getDefaultBuilderName() { + // 先返回第一个,之后可以加一些order之类的 + const builderOptions = Object.entries(this.builders) + .map(([key, builder]) => { + return { + value: key, + order: builder.order + }; + }).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + return builderOptions[0].value; + } + + getDSSwitch(setting: any = {}) { + const multiSource = this.builderNum > 1; + const builderOptions = Object.entries(this.builders).map( + ([key, builder]) => ({ + label: builder.name, + value: key, + order: builder.order + }) + ); + builderOptions.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + + return { + type: 'radios', + label: '数据来源', + name: 'dsType', + visible: multiSource, + selectFirst: true, + options: builderOptions, + ...setting + }; + } + + // getDSSwitchFormForPanel( + // propKey: string, + // label: string + // ) { + // return Object.keys(this.builders).length > 1 ? { + // type: Object.keys(this.builders).length > 3 ? 'select' : 'button-group-select', + // options: Object.keys(this.builders).map(name => ({ + // label: name, + // value: name + // })), + // name: propKey, + // label: label, + // pipeIn: (value: string) => { + // const builders = Object.entries(this.builders); + // return (builders.find(([, builder]) => { + // return builder.match(value); + // }) || builders[0])[0]; + // }, + // pipeOut: (value: string) => { + // return this.builders[value].defaultSchema || {}; + // } + // } : null; + // } + + collectFromBuilders( + callee: (builder: DSBuilder, builderName: string) => any + ) { + return Object.entries(this.builders).map(([name, builder]) => { + return callee(builder, name); + }); + } +} diff --git a/packages/amis-editor-core/src/component/RegionWrapper.tsx b/packages/amis-editor-core/src/component/RegionWrapper.tsx index bf967fe79..40f923214 100644 --- a/packages/amis-editor-core/src/component/RegionWrapper.tsx +++ b/packages/amis-editor-core/src/component/RegionWrapper.tsx @@ -97,10 +97,6 @@ export class RegionWrapper extends React.Component { wrapper.setAttribute('data-region', region); wrapper.setAttribute('data-region-host', id); - wrapper.setAttribute( - 'data-region-placeholder', - this.props.placeholder || this.props.label - ); rendererName && wrapper.setAttribute('data-renderer', rendererName); } @@ -108,7 +104,9 @@ export class RegionWrapper extends React.Component { return ( {this.props.children} - + + {this.props.placeholder || this.props.label} + ); } diff --git a/packages/amis-editor-core/src/component/ScaffoldModal.tsx b/packages/amis-editor-core/src/component/ScaffoldModal.tsx index aa733039a..853087b1f 100644 --- a/packages/amis-editor-core/src/component/ScaffoldModal.tsx +++ b/packages/amis-editor-core/src/component/ScaffoldModal.tsx @@ -3,7 +3,7 @@ import {EditorManager} from '../manager'; import {EditorStoreType} from '../store/editor'; import {render, Modal, getTheme, Icon, Spinner, Button} from 'amis'; import {observer} from 'mobx-react'; -import {autobind} from '../util'; +import {autobind, isObject} from '../util'; import {createObject} from 'amis-core'; export interface SubEditorProps { @@ -12,8 +12,20 @@ export interface SubEditorProps { theme?: string; } +interface ScaffoldState { + step: number +} + @observer -export class ScaffoldModal extends React.Component { +export class ScaffoldModal extends React.Component { + constructor(props: SubEditorProps) { + super(props); + + this.state = { + step: 0 + }; + } + @autobind handleConfirm([values]: any) { const store = this.props.store; @@ -32,21 +44,54 @@ export class ScaffoldModal extends React.Component { store.scaffoldForm?.callback(values); store.closeScaffoldForm(); + this.setState({step: 0}); } buildSchema() { const {store} = this.props; const scaffoldFormContext = store.scaffoldForm!; + let body = scaffoldFormContext.controls ?? scaffoldFormContext.body; + if (scaffoldFormContext.stepsBody) { + body = [ + { + type: 'steps', + name: '__steps', + className: 'ae-Steps', + steps: body.map((step, index) => ({ + title: step.title, + value: index, + iconClassName: 'ae-Steps-Icon' + })) + }, + ...body.map((step, index) => ({ + type: 'container', + visibleOn: `__step === ${index}`, + body: step.body + })) + ] + } + + let layout: object; + if (isObject(scaffoldFormContext.mode)) { + layout = scaffoldFormContext.mode as object; + } else { + layout = { + mode: scaffoldFormContext.mode || 'normal' + } + } + return { type: 'form', wrapWithPanel: false, initApi: scaffoldFormContext.initApi, api: scaffoldFormContext.api, - mode: scaffoldFormContext.mode || 'normal', + ...layout, wrapperComponent: 'div', - [scaffoldFormContext.controls ? 'controls' : 'body']: - scaffoldFormContext.controls ?? scaffoldFormContext.body + data: { + __step: 0 + }, + [scaffoldFormContext.controls ? 'controls' : 'body']: body, }; // const {store} = this.props; // const scaffoldFormContext = store.scaffoldForm; @@ -100,6 +145,32 @@ export class ScaffoldModal extends React.Component { this.amisScope = scoped; } + @autobind + goToNextStep() { + // 不能更新props的data,控制amis不重新渲染,否则数据会重新初始化 + const form = this.amisScope?.getComponents()[0].props.store; + const step = this.state.step + 1; + form.setValueByName('__step', step); + + // 控制按钮 + this.setState({ + step + }); + } + + @autobind + goToPrevStep() { + // 不能更新props的data,控制amis不重新渲染,否则数据会重新初始化 + const form = this.amisScope?.getComponents()[0].props.store; + const step = this.state.step - 1; + form.setValueByName('__step', step); + + // 控制按钮 + this.setState({ + step + }); + } + @autobind async handleConfirmClick() { const form = this.amisScope?.getComponents()[0]; @@ -129,14 +200,25 @@ export class ScaffoldModal extends React.Component { } } + @autobind + handleCancelClick() { + this.props.store.closeScaffoldForm(); + this.setState({step: 0}); + } + render() { const {store, theme, manager} = this.props; const scaffoldFormContext = store.scaffoldForm; const cx = getTheme(theme || 'cxd').classnames; + const isStepBody = !! scaffoldFormContext?.stepsBody; + const isLastStep = isStepBody && this.state.step === scaffoldFormContext!.body.length - 1; + const isFirstStep = isStepBody && this.state.step === 0; + return ( { render( this.buildSchema(), { - data: createObject(store.ctx, scaffoldFormContext?.value), + data: createObject(store.ctx, { + ...(scaffoldFormContext?.value || {}), + __step: 0 + }), onValidate: scaffoldFormContext.validate, scopeRef: this.scopeRef }, @@ -184,14 +269,38 @@ export class ScaffoldModal extends React.Component { ) : null} ) : null} - - + { + isStepBody && !isFirstStep && ( + + ) + } + { + isStepBody && !isLastStep && ( + + ) + } + { + (!isStepBody || isLastStep) && ( + + ) + } + ); diff --git a/packages/amis-editor-core/src/component/factory.tsx b/packages/amis-editor-core/src/component/factory.tsx index 31d7bb8c2..20319a41e 100644 --- a/packages/amis-editor-core/src/component/factory.tsx +++ b/packages/amis-editor-core/src/component/factory.tsx @@ -19,6 +19,7 @@ import {CommonConfigWrapper} from './CommonConfigWrapper'; import {Schema} from 'amis/lib/types'; import type {DataScope} from 'amis-core'; import type {RendererConfig} from 'amis-core/lib/factory'; +import {SchemaCollection} from 'amis/lib/Schema'; // 创建 Node Store 并构建成树 export function makeWrapper( @@ -69,6 +70,7 @@ export function makeWrapper( }); this.editorNode!.setRendererConfig(rendererConfig); + // 查找父数据域,将当前组件数据域追加上去,使其形成父子关系 if ( rendererConfig.storeType && !manager.dataSchema.hasScope(`${info.id}-${info.type}`) @@ -303,7 +305,7 @@ function SchemaFrom({ export function makeSchemaFormRender( manager: EditorManager, schema: { - body?: Array; + body?: SchemaCollection; controls?: Array; definitions?: any; api?: any; @@ -311,7 +313,7 @@ export function makeSchemaFormRender( justify?: boolean; panelById?: string; formKey?: string; - }, + } ) { const env = {...manager.env, session: 'schema-form'}; @@ -331,10 +333,11 @@ export function makeSchemaFormRender( } }); } - // 每一层的面板数据不要共用 - const curFormKey = `${id}-${node?.type}${schema.formKey ? '-': ''}${schema.formKey ? schema.formKey: ''}`; + const curFormKey = `${id}-${node?.type}${schema.formKey ? '-' : ''}${ + schema.formKey ? schema.formKey : '' + }`; return ( 拖入占位'; + // bca-disable-line ghost.innerHTML = html; unmountComponentAtNode(thumbHost); + // bca-disable-line thumbHost.innerHTML = ''; } diff --git a/packages/amis-editor-core/src/icons/delete-btn.svg b/packages/amis-editor-core/src/icons/delete-btn.svg index 8b1f6e47d..e23242fed 100644 --- a/packages/amis-editor-core/src/icons/delete-btn.svg +++ b/packages/amis-editor-core/src/icons/delete-btn.svg @@ -1,10 +1,4 @@ - - icon/删除 - - - - - - + + \ No newline at end of file diff --git a/packages/amis-editor-core/src/icons/index.tsx b/packages/amis-editor-core/src/icons/index.tsx index 125d981f8..8712f65a2 100644 --- a/packages/amis-editor-core/src/icons/index.tsx +++ b/packages/amis-editor-core/src/icons/index.tsx @@ -10,7 +10,8 @@ import DisplayInlineBlock from './display-inline-block.svg'; import DisplayFlex from './display-flex.svg'; import Harmmer from './hammer.svg'; import Dialog from './dialog.svg'; -import API from './api.svg'; +import Setting from './setting.svg'; +import PickerIcon from './picker-icon.svg'; registerIcon('arrow-to-right', ArrowToRight); registerIcon('left-arrow-to-left', LeftArrowToleft); @@ -19,7 +20,8 @@ registerIcon('arrow-to-bottom', ArrowToBottom); registerIcon('collapse-open', CollapseOpen); registerIcon('harmmer', Harmmer); registerIcon('dialog', Dialog); -registerIcon('api', API); +registerIcon('setting', Setting); +registerIcon('picker-icon', PickerIcon); // 「页面设计器改版」设计侧提供的icon(组件头部工具栏icon) import CopyBtn from './copy-btn.svg'; diff --git a/packages/amis-editor-core/src/icons/picker-icon.svg b/packages/amis-editor-core/src/icons/picker-icon.svg new file mode 100644 index 000000000..f0157070d --- /dev/null +++ b/packages/amis-editor-core/src/icons/picker-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/amis-editor-core/src/icons/setting.svg b/packages/amis-editor-core/src/icons/setting.svg new file mode 100644 index 000000000..eb6832a96 --- /dev/null +++ b/packages/amis-editor-core/src/icons/setting.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/amis-editor-core/src/index.ts b/packages/amis-editor-core/src/index.ts index db88b0721..1984d901a 100644 --- a/packages/amis-editor-core/src/index.ts +++ b/packages/amis-editor-core/src/index.ts @@ -20,6 +20,8 @@ export * from './manager'; export * from './plugin'; export * from './icons/index'; export * from './mocker'; +export * from './builder/DSBuilder'; +import './builder/ApiBuilder'; import {BasicEditor, RendererEditor} from './compat'; import MiniEditor from './component/MiniEditor'; import CodeEditor from './component/Panel/AMisCodeEditor'; diff --git a/packages/amis-editor-core/src/manager.ts b/packages/amis-editor-core/src/manager.ts index 18e2267ff..153ef7883 100644 --- a/packages/amis-editor-core/src/manager.ts +++ b/packages/amis-editor-core/src/manager.ts @@ -44,7 +44,8 @@ import { reactionWithOldValue, reGenerateID, isString, - isObject + isObject, + generateNodeId } from './util'; import {reaction} from 'mobx'; import {hackIn, makeSchemaFormRender, makeWrapper} from './component/factory'; @@ -63,7 +64,7 @@ import {EditorProps} from './component/Editor'; import findIndex from 'lodash/findIndex'; import {EditorDNDManager} from './dnd'; import {IScopedContext} from 'amis'; -import {SchemaObject} from 'amis/lib/Schema'; +import {SchemaObject, SchemaCollection} from 'amis/lib/Schema'; import type {RendererConfig} from 'amis-core/lib/factory'; export interface EditorManagerConfig @@ -440,6 +441,7 @@ export class EditorManager { const node = this.store.getNodeById(id); panels = node ? this.collectPanels(node, true) : panels; } + this.store.setPanels( panels.map(item => ({ ...item, @@ -1271,7 +1273,7 @@ export class EditorManager { const commonContext = this.buildEventContext(id); if (!('id' in json)) { - json = {...json, id: 'u:' + guid()}; + json = {...json, id: generateNodeId()}; } if (beforeId) { @@ -1552,7 +1554,7 @@ export class EditorManager { * @param schema */ makeSchemaFormRender(schema: { - body?: Array; + body?: SchemaCollection; controls?: Array; definitions?: any; api?: any; @@ -1665,6 +1667,8 @@ export class EditorManager { let scope: DataScope | void; let from = node; let region = node; + + // 查找最近一层的数据域 while (!scope && from) { scope = this.dataSchema.hasScope(`${from.id}-${from.type}`) ? this.dataSchema.getScope(`${from.id}-${from.type}`) @@ -1675,11 +1679,18 @@ export class EditorManager { } } - const nearestScope = scope; + let nearestScope; + // 更新组件树中的所有上下文数据声明为最新数据 while (scope) { const [id, type] = scope.id.split('-'); const node = this.store.getNodeById(id, type); + + // 拿非重复组件id的父组件作为主数据域展示,如CRUD,不展示表格,只展示增删改查信息,避免变量面板出现两份数据 + if (!nearestScope && node && !node.isSecondFactor) { + nearestScope = scope; + } + const jsonschema = await node?.info?.plugin?.buildDataSchemas?.( node, region @@ -1701,6 +1712,44 @@ export class EditorManager { : this.dataSchema.getSchemas(); } + /** + * 获取可用上下文待绑定字段 + */ + async getAvailableContextFields(node: EditorNodeType) { + if (!node) { + return []; + } + + let scope: DataScope | void; + let from = node; + let region = node; + + // 查找最近一层的数据域 + while (!scope && from) { + scope = this.dataSchema.hasScope(`${from.id}-${from.type}`) + ? this.dataSchema.getScope(`${from.id}-${from.type}`) + : undefined; + from = from.parent; + if (from?.isRegion) { + region = from; + } + } + + while (scope) { + const [id, type] = scope.id.split('-'); + const scopeNode = this.store.getNodeById(id, type); + + if (scopeNode) { + return scopeNode?.info.plugin.getAvailableContextFields?.(scopeNode, node) || []; + } + + scope = scope.parent; + } + + return []; + + } + beforeDispatchEvent( originHook: any, e: any, diff --git a/packages/amis-editor-core/src/plugin.ts b/packages/amis-editor-core/src/plugin.ts index b640640e4..0e4e7da37 100644 --- a/packages/amis-editor-core/src/plugin.ts +++ b/packages/amis-editor-core/src/plugin.ts @@ -1,7 +1,6 @@ /** * @file 定义插件的 interface,以及提供一个 BasePlugin 基类,把一些通用的方法放在这。 */ - import {RegionWrapperProps} from './component/RegionWrapper'; import {EditorManager} from './manager'; import {EditorStoreType} from './store/editor'; @@ -13,7 +12,8 @@ import {DiffChange} from './util'; import find from 'lodash/find'; import type {RendererConfig} from 'amis-core/lib/factory'; import type {MenuDivider, MenuItem} from 'amis-ui/lib/components/ContextMenu'; -import type {BaseSchema} from 'amis/lib/Schema'; +import type {BaseSchema, SchemaCollection} from 'amis/lib/Schema'; +import {DSFieldGroup} from './builder/DSBuilder'; /** * 区域的定义,容器渲染器都需要定义区域信息。 @@ -243,7 +243,7 @@ export interface RendererInfo extends RendererScaffoldInfo { wrapperProps?: any; /** - * 修改一些属性,一般用来干掉 $$id + * 修改一些属性,一般用来干掉 $$id,或者渲染假数据 * 这样它的孩子节点就不能直接点选编辑了,比如 Combo。 */ filterProps?: (props: any, node: EditorNodeType) => any; @@ -311,12 +311,22 @@ export interface PopOverForm { } export interface ScaffoldForm extends PopOverForm { - mode?: 'normal' | 'horizontal' | 'inline'; + // 内容是否是分步骤的,如果是,body必须是?: Array<{title: string,body: any[]}> + stepsBody?: boolean; + mode?: + | 'normal' + | 'horizontal' + | 'inline' + | { + mode: string; + horizontal: any; + }; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'; + className?: string; initApi?: any; api?: any; - + actions?: any[]; /** * 整体验证脚手架配置,如果有错误返回错误对象。 * key 是配置的字段名。 @@ -555,6 +565,12 @@ export interface ResizeMoveEventContext extends EventContext { node: EditorNodeType; } +export interface AfterBuildPanelBody extends EventContext { + data: SchemaCollection; + plugin: BasePlugin; + context: BaseEventContext; +} + /** * 将事件上下文转成事件对象。 */ @@ -720,6 +736,9 @@ export interface PluginInterface order?: number; + // 是否可绑定数据,一般容器类型就没有 + withDataSource?: boolean; + /** * 渲染器的名字,关联后不用自己实现 getRendererInfo 了。 */ @@ -768,11 +787,28 @@ export interface PluginInterface */ panelJustify?: boolean; + /** + * 有数据域的容器,可以为子组件提供读取的字段列表 + */ + getAvailableContextFields?: ( + scopeNode: EditorNodeType, + node: EditorNodeType, + region?: EditorNodeType + ) => Promise; + /** * @deprecated 用 panelBodyCreator */ panelControlsCreator?: (context: BaseEventContext) => Array; - panelBodyCreator?: (context: BaseEventContext) => Array; + panelBodyCreator?: (context: BaseEventContext) => SchemaCollection; + + /** + * panel还需要合并目标插件提供的配置,冲突时以当前plugin为准 + */ + panelBodyMergeable?: ( + context: BaseEventContext, + plugin: PluginInterface + ) => boolean; popOverBody?: Array; popOverBodyCreator?: (context: BaseEventContext) => Array; @@ -878,7 +914,11 @@ export interface RendererPluginAction { } // 分支动作 -export interface SubRendererPluginAction extends Pick{} +export interface SubRendererPluginAction + extends Pick< + RendererPluginAction, + 'actionType' | 'innerArgs' | 'descDetail' + > {} export interface PluginEvents { [propName: string]: RendererPluginEvent[]; @@ -956,6 +996,16 @@ export abstract class BasePlugin implements PluginInterface { plugin.panelBodyCreator) && context.info.plugin === this ) { + const body = plugin.panelBodyCreator + ? plugin.panelBodyCreator(context) + : plugin.panelBody!; + + this.manager.trigger('after-build-panel-body', { + context, + data: body, + plugin + }); + panels.push({ key: 'config', icon: plugin.panelIcon || plugin.icon || 'fa fa-cog', @@ -965,9 +1015,7 @@ export abstract class BasePlugin implements PluginInterface { definitions: plugin.panelDefinitions, submitOnChange: plugin.panelSubmitOnChange, api: plugin.panelApi, - body: plugin.panelBodyCreator - ? plugin.panelBodyCreator(context) - : plugin.panelBody!, + body: body, controls: plugin.panelControlsCreator ? plugin.panelControlsCreator(context) : plugin.panelControls!, diff --git a/packages/amis-editor-core/src/plugin/BasicToolbar.tsx b/packages/amis-editor-core/src/plugin/BasicToolbar.tsx index 76f4bf51d..9cc470699 100644 --- a/packages/amis-editor-core/src/plugin/BasicToolbar.tsx +++ b/packages/amis-editor-core/src/plugin/BasicToolbar.tsx @@ -548,9 +548,9 @@ export class BasicToolbarPlugin extends BasePlugin { menus: menus, render: this.manager.makeSchemaFormRender({ body: [ + // @ts-ignore amis中有问题,可选参数搞成了必选,改完了可以去掉这行 { type: 'button-group', - block: true, buttons: menus .filter(item => item !== '|') .map(menu => ({ diff --git a/packages/amis-editor-core/src/store/editor.ts b/packages/amis-editor-core/src/store/editor.ts index 670a7527a..64176d3b3 100644 --- a/packages/amis-editor-core/src/store/editor.ts +++ b/packages/amis-editor-core/src/store/editor.ts @@ -535,7 +535,7 @@ export const EditorStore = types ); } - return bcn; + return bcn.filter(item => !item.isSecondFactor); }, get activePath(): Array { diff --git a/packages/amis-editor-core/src/store/node.ts b/packages/amis-editor-core/src/store/node.ts index 9d9e09c55..699a171bb 100644 --- a/packages/amis-editor-core/src/store/node.ts +++ b/packages/amis-editor-core/src/store/node.ts @@ -523,6 +523,18 @@ export const EditorNode = types self.h = height; } + function getClosestParentByType(type: string): EditorNodeType | void { + let node = self; + while(node = node.parent) { + if (node.schema.type === type) { + return node as EditorNodeType; + } + if (node.id === 'root') { + return; + } + } + } + // 放到props会变成 frozen 的。 let component: any; @@ -531,6 +543,7 @@ export const EditorNode = types } return { + getClosestParentByType, updateIsCommonConfig, addChild(props: { id: string; diff --git a/packages/amis-editor-core/src/tpl.tsx b/packages/amis-editor-core/src/tpl.tsx index ffd936eb2..f0d39acb4 100644 --- a/packages/amis-editor-core/src/tpl.tsx +++ b/packages/amis-editor-core/src/tpl.tsx @@ -1,3 +1,5 @@ +import { SchemaObject } from "amis/lib/Schema"; + /** * @file amis schema 配置模板,主要很多地方都要全部配置的化, * 会有很多份,而且改起来很麻烦,复用率高的放在这管理。 @@ -66,3 +68,25 @@ export function defaultValue(defaultValue: any, strictMode: boolean = true) { ? (value: any) => (typeof value === 'undefined' ? defaultValue : value) : (value: any) => value || defaultValue; } + +/** + * 配置面板带提示信息的label + */ +export function tipedLabel( + body: string | Array, + tip: string, + style?: React.CSSProperties +) { + return { + type: 'tooltip-wrapper', + tooltip: tip, + tooltipTheme: 'dark', + placement: 'top', + tooltipStyle: { + fontSize: '12px', + ...(style || {}) + }, + className: 'ae-formItemControl-label-tip', + body + }; +} diff --git a/packages/amis-editor-core/src/util.ts b/packages/amis-editor-core/src/util.ts index 677d7c9eb..99468056f 100644 --- a/packages/amis-editor-core/src/util.ts +++ b/packages/amis-editor-core/src/util.ts @@ -8,6 +8,7 @@ import DeepDiff, {Diff} from 'deep-diff'; import isPlainObject from 'lodash/isPlainObject'; import isNumber from 'lodash/isNumber'; import type {Schema} from 'amis/lib/types'; +import {SchemaObject} from 'amis/lib/Schema'; const { guid, @@ -541,6 +542,7 @@ export function reGenerateID(json: any) { export function createElementFromHTML(htmlString: string): HTMLElement { var div = document.createElement('div'); + // bca-disable-line div.innerHTML = htmlString.trim(); // Change this to div.childNodes to support multiple top-level nodes @@ -591,9 +593,10 @@ export function filterSchemaForConfig(schema: any, valueWithConfig?: any): any { } else if (key === '$$commonSchema' && valueWithConfig) { let config: any = deepFind(valueWithConfig, value); config[value] && - (schema = mapped = { - ...config[value] - }); + (schema = mapped = + { + ...config[value] + }); } }); return modified ? mapped : schema; @@ -871,6 +874,13 @@ export function jsonToJsonSchema(json: any = {}) { return jsonschema; } +/** + * 生成节点id + */ +export function generateNodeId() { + return 'u:' + guid(); +} + // 是否使用 plugin 自带的 svg 版 icon export function isHasPluginIcon(plugin: any) { return plugin.pluginIcon && hasIcon(plugin.pluginIcon);