diff --git a/packages/amis-editor-core/eidtor-core-i18n-en-US-1660304908871.xlsx b/packages/amis-editor-core/eidtor-core-i18n-en-US-1660304908871.xlsx deleted file mode 100644 index 669c61239..000000000 Binary files a/packages/amis-editor-core/eidtor-core-i18n-en-US-1660304908871.xlsx and /dev/null differ diff --git a/packages/amis-editor-core/eidtor-core-i18n-en-US-1662711211029.xlsx b/packages/amis-editor-core/eidtor-core-i18n-en-US-1662711211029.xlsx new file mode 100644 index 000000000..497972e98 Binary files /dev/null and b/packages/amis-editor-core/eidtor-core-i18n-en-US-1662711211029.xlsx differ diff --git a/packages/amis-editor-core/eidtor-core-i18n-en-US-1663318244212.xlsx b/packages/amis-editor-core/eidtor-core-i18n-en-US-1663318244212.xlsx new file mode 100644 index 000000000..5608c2c8e Binary files /dev/null and b/packages/amis-editor-core/eidtor-core-i18n-en-US-1663318244212.xlsx differ diff --git a/packages/amis-editor-core/package.json b/packages/amis-editor-core/package.json index 2379ddde6..a9b95098e 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.61", + "version": "5.2.0-beta.74", "description": "amis 可视化编辑器", "main": "lib/index.min.js", "types": "lib/index.d.ts", diff --git a/packages/amis-editor-core/scss/_classname-picker.scss b/packages/amis-editor-core/scss/_classname-picker.scss index f36ef598b..5959c5542 100644 --- a/packages/amis-editor-core/scss/_classname-picker.scss +++ b/packages/amis-editor-core/scss/_classname-picker.scss @@ -1,7 +1,7 @@ .ae-ClassNamePicker-popover { padding: 10px; width: 610px; - max-height: 400px; + max-height: calc(100% - 400px); overflow: auto; &.ae-PopOver--leftBottomLeftTop { diff --git a/packages/amis-editor-core/scss/_mixin.scss b/packages/amis-editor-core/scss/_mixin.scss index beaff169c..f24447e40 100644 --- a/packages/amis-editor-core/scss/_mixin.scss +++ b/packages/amis-editor-core/scss/_mixin.scss @@ -89,7 +89,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 ee3f82420..30d60324c 100644 --- a/packages/amis-editor-core/scss/_rightPanel.scss +++ b/packages/amis-editor-core/scss/_rightPanel.scss @@ -280,7 +280,7 @@ $category-2-height: px2rem(32px); // tab导航 ul[role='tablist'], &-links { - margin: 0 0 -1px 0; + margin: 0; flex: 0; border-bottom: 1px solid #d4d6d9; display: flex; @@ -354,8 +354,9 @@ $category-2-height: px2rem(32px); position: absolute; width: 100%; padding: 0; - overflow-y: overlay; + overflow-y: overlay !important; @include minScrollBar(); + margin-top: -1px; } div.ae-switch-more-flex { diff --git a/packages/amis-editor-core/scss/control/_api-control.scss b/packages/amis-editor-core/scss/control/_api-control.scss index 505cd7cd8..14732c664 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 { margin: #{px2rem(16px)} 0 #{px2rem(24px)}; @@ -58,3 +111,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..9bed3e59a --- /dev/null +++ b/packages/amis-editor-core/scss/control/_databinding-control.scss @@ -0,0 +1,98 @@ +.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); + overflow: scroll; + + &-hint { + width: 100%; + line-height: 3; + text-align: center; + color: var(--text--muted-color); + } + + &-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: block !important; + padding: #{px2rem(5px)} #{px2rem(12px)}; + background: transparent !important; + font-size: var(--fontSizeSm); + font-weight: bold; + position: relative; + + .expandIcon { + font-size: var(--fontSizeSm); + line-height: var(--fontSizeXl); + transform-origin: #{px2rem(7px)} #{px2rem(9px)}; + transition: transform 0.2s; + position: absolute; + right: #{px2rem(6px)}; + margin-top: 3px; + } + } + + &-body { + background: #fff; + color: #303540; + + > div { + padding: 5px 0; + } + } + } + + &-item { + display: flex; + flex-direction: row; + align-items: baseline; + cursor: pointer; + padding: 0 var(--gap-xl); // 和标题对齐不好看,加个缩进 + height: px2rem(32px); + line-height: px2rem(32px); + color: #303540; + font-weight: 400; + + span { + flex-grow: 1; + } + + &: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/_event-action.scss b/packages/amis-editor-core/scss/control/_event-action.scss index 081d4879b..4ee942cc3 100644 --- a/packages/amis-editor-core/scss/control/_event-action.scss +++ b/packages/amis-editor-core/scss/control/_event-action.scss @@ -9,11 +9,12 @@ } &-header { position: fixed; - top: #{px2rem(45px)}; + top: #{px2rem(48px)}; width: 100%; padding: #{px2rem(12px)}; background: #fff; z-index: 1; + margin-top: 1px; .add-event-dropdown { button { top: 44px; @@ -75,6 +76,14 @@ } } } + &-desc { + margin: #{px2rem(12px)}; + color: #84868c; + button > svg { + width: #{px2rem(12px)}; + height: #{px2rem(12px)}; + } + } &:last-child { .event-item-header { border-bottom: #{px2rem(1px)} solid #d4d6d9; @@ -87,7 +96,9 @@ @include flexBox(column, flex-start); @include minScrollBar(); margin: 0; - padding: #{px2rem(13px)} #{px2rem(8px)} 0; + padding-left: #{px2rem(12px)}; + padding-right: #{px2rem(12px)}; + padding-top: #{px2rem(12px)}; background: #ffffff; list-style-type: none; .ae-option-control-item { @@ -333,13 +344,13 @@ } li .is-checked > div { background: var(--Tree-item-onChekced-bg); - &:hover{ + &:hover { background-color: var(--Tree-item-onChekced-bg) !important; } } } } - + li .is-checked::before { content: ''; background: url('../static/check.svg') no-repeat center center; 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 1b1e058ea..6f85577a8 100644 --- a/packages/amis-editor-core/scss/control/_formItem-control.scss +++ b/packages/amis-editor-core/scss/control/_formItem-control.scss @@ -17,6 +17,12 @@ } div[data-role='form-item'] { + &.ae-ExtendMore { + > label { + flex: 0; + padding: 0; + } + } > label { line-height: 32px; margin: 0; @@ -134,3 +140,7 @@ height: px2rem(32px); } } + +.form-item-gap { + margin-bottom: var(--Form-item-gap); +} diff --git a/packages/amis-editor-core/scss/control/_formula-control.scss b/packages/amis-editor-core/scss/control/_formula-control.scss index a52a558b2..e560ec2d8 100644 --- a/packages/amis-editor-core/scss/control/_formula-control.scss +++ b/packages/amis-editor-core/scss/control/_formula-control.scss @@ -69,7 +69,7 @@ &.is-clearable { > div:first-child { - max-width: calc(100% - 20px); // 避免表达式内容太长撑开面板 + max-width: calc(100% - 24px); // 避免表达式内容太长撑开面板 > div, span { 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/control/_inpupt-file.scss b/packages/amis-editor-core/scss/control/_inpupt-file.scss new file mode 100644 index 000000000..f47fa75e2 --- /dev/null +++ b/packages/amis-editor-core/scss/control/_inpupt-file.scss @@ -0,0 +1,13 @@ +.inputFile-apiControl { + margin-top: 30px; + + .ApiControl { + margin-bottom: 0; + + &-header { + position: absolute; + right: 0; + top: -25px; + } + } +} diff --git a/packages/amis-editor-core/scss/control/_option-control.scss b/packages/amis-editor-core/scss/control/_option-control.scss index 8a499024c..e8505c639 100644 --- a/packages/amis-editor-core/scss/control/_option-control.scss +++ b/packages/amis-editor-core/scss/control/_option-control.scss @@ -58,6 +58,12 @@ flex: 1; margin: 0; margin-right: #{px2rem(12px)}; + + input { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } } &-dropdown i { diff --git a/packages/amis-editor-core/scss/control/_timeline_item_control.scss b/packages/amis-editor-core/scss/control/_timeline_item_control.scss new file mode 100644 index 000000000..b54212fa8 --- /dev/null +++ b/packages/amis-editor-core/scss/control/_timeline_item_control.scss @@ -0,0 +1,83 @@ +.ae-TimelineItemControl { + &-header { + @include flexBox(); + width: 100%; + height: #{px2rem(24px)}; + margin-bottom: #{px2rem(12px)}; + } + + &-content { + @include flexBox(column, flex-start); + margin: 0; + padding: 0; + + .ae-TimelineItemControlItem { + display: block; + width: 100%; + + &-input-title { + flex: 1; + margin-left: #{px2rem(20px)}; + margin-right: #{px2rem(44px)}; + input { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + &-input { + flex: 1; + margin: 0; + margin-right: #{px2rem(12px)}; + } + + &-Main { + @include flexBox(); + width: 100%; + background: #fff; + padding-bottom: px2rem(12px); + } + + &--dragging { + height: 0 !important; + padding: 0; + border-top: 2px solid var(--primary); + overflow: hidden; + background: #e9effd; + } + + &-dragBar { + display: inline-flex; + margin-left: 0; + margin-right: var(--gap-sm); + cursor: move; + color: #8c8c8c; + } + + &-dropdown i { + margin-right: 0px; + } + } + + .ae-TimelineItemControlItem-inputDate { + margin-bottom: 0; + } + } + + &-border { + background-color: #e5e5e5; + width: 100%; + height: 1px; + margin-top: 12px; + margin-bottom: 12px; + } + + &-footer > * { + width: calc(50% - #{px2rem(6px)}); + + &:first-child { + margin-right: px2rem(12px); + } + } +} diff --git a/packages/amis-editor-core/scss/editor.scss b/packages/amis-editor-core/scss/editor.scss index 8a95e6857..ac57ee91c 100644 --- a/packages/amis-editor-core/scss/editor.scss +++ b/packages/amis-editor-core/scss/editor.scss @@ -26,8 +26,13 @@ @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'; +@import './control/timeline_item_control'; @import './control/tree_option_control'; +@import './control/_inpupt-file'; /* 样式控件 */ @import './style-control/box-model'; @@ -1053,55 +1058,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; } @@ -1505,10 +1481,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; @@ -1518,12 +1511,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; } } @@ -1565,3 +1559,51 @@ div.ae-DragImage { 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); + } + } +} 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..b55a9666a --- /dev/null +++ b/packages/amis-editor-core/src/builder/ApiBuilder.ts @@ -0,0 +1,598 @@ +/** + * 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, SchemaCollection, SchemaObject} from 'amis/lib/Schema'; + +import type {DSSourceSettingFormConfig} from './DSBuilder'; +import {getSchemaTpl, tipedLabel} from '../tpl'; +import {EditorNodeType} from '../store/node'; + +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; + }, + target: EditorNodeType + ) { + // API类目前没有,增加API中心的出参入参后,可以在这里提供绑定字段 + // return { + // type: 'ae-SimpleDataBindingPanel', + // fields: [ + // { + // label: '可用字段', + // children: [ + // {label: '名称', value: 'name'}, + // {label: '年级', value: 'grade'} + // ] + // } + // ] + // } as any; + } + + onFieldsInit(value: 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); + }); + } + + 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', + // CRUD的脚手架面板,基于现有字段进行选择 + 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.onFieldsInit(value, form), + 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 + }); + this.onFieldsInit(autoFillKeyValues, props.formStore); + } else { + toast.warning( + 'API返回格式不正确,请查看接口响应格式要求' + ); + } + } + } + ] + } + ] + } + ] + } + ]) as SchemaObject[]; + } + + public async makeFieldFilterSetting(config: { + /** 数据源字段名 */ + sourceKey: string; + schema: any; + fieldName: string; + }) { + 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..964c846b4 --- /dev/null +++ b/packages/amis-editor-core/src/builder/DSBuilder.ts @@ -0,0 +1,372 @@ +/** + * 数据源构造器,可用于对接当前amis中的扩展数据源 + */ + +import {ButtonSchema} from 'amis/lib/renderers/Action'; +import {CRUD2Schema} from 'amis/lib/renderers/CRUD2'; +import {FormSchema, SchemaCollection, SchemaObject} from 'amis/lib/Schema'; +import {EditorNodeType} from '../store/node'; + +/** + * 数据源所需操作,目前是因为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; + schema: any; + fieldName: string; + }): Promise; + + /** + * 数据源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; + }, + target: EditorNodeType + ): 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/base/SearchPanel.tsx b/packages/amis-editor-core/src/component/base/SearchPanel.tsx index a669e7516..71fdd56e6 100644 --- a/packages/amis-editor-core/src/component/base/SearchPanel.tsx +++ b/packages/amis-editor-core/src/component/base/SearchPanel.tsx @@ -87,6 +87,7 @@ export default class SearchPanel extends React.Component< this.curInputBox = this.ref.current.childNodes[0].childNodes[0]; this.curInputBox.addEventListener('keyup', this.bindEnterEvent); } + this.updateCurKeyword(''); } componentWillUnmount() { @@ -101,7 +102,7 @@ export default class SearchPanel extends React.Component< if (externalKeyword !== this.state.curKeyword) { this.setState( { - curKeyword: externalKeyword, + curKeyword: externalKeyword }, () => { this.groupedResultByKeyword(externalKeyword); @@ -122,7 +123,7 @@ export default class SearchPanel extends React.Component< } this.setState({ resultTags: curResultTags, - resultByTag: curResultByTag, + resultByTag: curResultByTag }); } } @@ -199,10 +200,9 @@ export default class SearchPanel extends React.Component< if (isString(item) && regular && regular.test(item)) { // 兼容字符串类型 curSearchResult.push(item); - } - else if ( - !keywords - || ['name', 'description', 'scaffold.type'].some( + } else if ( + !keywords || + ['name', 'description', 'scaffold.type'].some( key => item[key] && regular && regular.test(item[key]) ) ) { @@ -210,14 +210,13 @@ export default class SearchPanel extends React.Component< const tags = Array.isArray(item[curTagKey]) ? item[curTagKey].concat() : item[curTagKey] - ? [item[curTagKey]] - : ['其他']; + ? [item[curTagKey]] + : ['其他']; tags.forEach((tag: string) => { curSearchResultByTag[tag] = grouped[tag] || []; curSearchResultByTag[tag].push(item); }); - } - else { + } else { curSearchResult.push(item); } } @@ -226,7 +225,7 @@ export default class SearchPanel extends React.Component< // 更新当前搜索结果数据(备注: 附带重置功能) this.setState({ searchResult: curSearchResult, - searchResultByTag: curSearchResultByTag, + searchResultByTag: curSearchResultByTag }); } @@ -452,13 +451,8 @@ export default class SearchPanel extends React.Component< render() { const {allResult, closeAutoComplete, immediateChange} = this.props; - const { - resultTags, - curKeyword, - searchResult, - searchResultByTag, - visible - } = this.state; + const {resultTags, curKeyword, searchResult, searchResultByTag, visible} = + this.state; const searchResultTags = searchResultByTag ? Object.keys(searchResultByTag) : []; diff --git a/packages/amis-editor-core/src/component/factory.tsx b/packages/amis-editor-core/src/component/factory.tsx index 24267a9a2..6a11be4f1 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}`) @@ -301,7 +303,7 @@ function SchemaFrom({ export function makeSchemaFormRender( manager: EditorManager, schema: { - body?: Array; + body?: SchemaCollection; controls?: Array; definitions?: any; api?: any; @@ -309,7 +311,7 @@ export function makeSchemaFormRender( justify?: boolean; panelById?: string; formKey?: string; - }, + } ) { const env = {...manager.env, session: 'schema-form'}; @@ -330,9 +332,10 @@ 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; /* bca-enable */ 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 0cbca643e..bd1403541 100644 --- a/packages/amis-editor-core/src/index.ts +++ b/packages/amis-editor-core/src/index.ts @@ -22,6 +22,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/locale/en-US.ts b/packages/amis-editor-core/src/locale/en-US.ts index e6b78f369..69cf572f6 100644 --- a/packages/amis-editor-core/src/locale/en-US.ts +++ b/packages/amis-editor-core/src/locale/en-US.ts @@ -189,5 +189,51 @@ extendLocale('en-US', { 'eadc8c8d4a8776893672330598babca0': 'Location error, target location not found', 'f3c057f37fb9a4e7dd44b04919c12578': - 'Please click add new element from the component panel on the left.' + 'Please click add new element from the component panel on the left.', + '54ea89b497ec3bb319c68844dfa3687f': '', + '51e213e66b37d716a35baebc9193035c': '', + '388e0ff896ea4f23e71d36c06443f157': '', + 'b4bc91701b86fe8543d649e97daea602': '', + 'f8747759c73697367bc8a570977c4a62': '', + 'd2b46e7989e18239d7036affd4d521db': '', + '9caecd931b956381e0763d05aa42835c': '', + '47cd88592f6ef2b258f02c0690d267ed': '', + '32c65d8d7431e76029678ec7bb73a5ab': '', + '020586d0c69f8211840ddf9ee9bbf6ab': '', + '226b0912184333c81babf2f1894ec0c1': '', + '97d07614380da93d257f9fbf81aa56fb': '', + '20def7942674282277c3714ed7ea6ce0': '', + '4ff1e74e43a3586339251494117185ad': '', + 'c7bff79d059a0b7ff9b02441959d8be2': '', + '3fea7ca76cdece641436d7ab0d02ab1b': '', + '9da188491dd34c4382a5b9f006194e41': '', + 'b3e55578af5dd473bab62641bb2f5f8e': '', + '9b6425cd2d496c9cb5a6c6b8ff125d1b': '', + '15d169d28cd48c97fe751e4cc92ca926': '', + '9597dcaf432ceba92a160d61cb1ef65f': '', + '9913107b19cb6012250134ff91377430': '', + '454e60f5759903d7d3dba58e3f9bd590': '', + 'db98f889ce6bc235e66bd4b2a788d137': '', + '006ded9fa277cf030592021f595a07d5': '', + 'a6beb974cc0b50eebd18120b8110a88b': '', + 'b339aa87104709397ba68e7ebbc6e5ba': '', + '481e034e6026969aae4ce7ce7c8a7b6f': '', + '6bfb9bb2218ff32b6139e98bc93707c0': '', + '24b6d4c0892a8f3ee2a982e3ab0afe38': '', + '4484fa04e7b71db4c8293e5bcb53eca4': '', + '4cc6a76c146c0360a41ceaf5e212c891': '', + 'a9fea442707e26dee478b34a2f2ce263': '', + '91aa2166ee4811414381c8d94e6567e6': '', + '3712972d84adf48acbd6ad24b4d75ad0': '', + '66ab5e9f24c8f46012a25c89919fb191': '', + 'e73cefac9d030927da1618c7b15c98c9': '', + '7fb62b30119c3797a843a48368463314': '', + '8d9a071ee2ef45e045968e117a205c07': '', + '55405ea6ff6fd823ffab7e6b10ddfa95': '', + 'c26996a6506adf397f0668d376d0b40b': '', + '6ff4bf3d567e977aa4c90c27dff1e6db': '', + '9c4666fd08c2738eb9611a3721cb5f0f': '', + 'a094e5b7699ea4b61094cc4120170423': '', + 'eeb6908870e058bc23d52c1e405a054e': '', + '38ce27d84639f3a6e07c00b3b4995c0e': '' }); diff --git a/packages/amis-editor-core/src/locale/zh-CN.ts b/packages/amis-editor-core/src/locale/zh-CN.ts index 8b53aca22..5c342a12d 100644 --- a/packages/amis-editor-core/src/locale/zh-CN.ts +++ b/packages/amis-editor-core/src/locale/zh-CN.ts @@ -1,6 +1,57 @@ import {extendLocale} from 'i18n-runtime'; extendLocale('zh-CN', { + '54ea89b497ec3bb319c68844dfa3687f': '接口', + '51e213e66b37d716a35baebc9193035c': + "用来保存数据, 表单提交后将数据传入此接口。
\n 接口响应体要求(如果data中有数据,该数据将被合并到表单上下文中):
\n {{@1}}\n}, null, '
')}", + '388e0ff896ea4f23e71d36c06443f157': + "接口响应体要求:
\n {{@1}},\n page: 0,\n total: 0\n}, null, '
')}", + 'b4bc91701b86fe8543d649e97daea602': '初始化接口', + 'f8747759c73697367bc8a570977c4a62': + "接口响应体要求:
\n {{@1}}\n}, null, '
')}", + 'd2b46e7989e18239d7036affd4d521db': '{{@1}}字段', + '9caecd931b956381e0763d05aa42835c': '字段', + '47cd88592f6ef2b258f02c0690d267ed': '隐藏,默认选中', + '32c65d8d7431e76029678ec7bb73a5ab': '标题', + '020586d0c69f8211840ddf9ee9bbf6ab': '绑定字段', + '226b0912184333c81babf2f1894ec0c1': '类型', + '97d07614380da93d257f9fbf81aa56fb': '文本', + '20def7942674282277c3714ed7ea6ce0': '图片', + '4ff1e74e43a3586339251494117185ad': '日期', + 'c7bff79d059a0b7ff9b02441959d8be2': '进度', + '3fea7ca76cdece641436d7ab0d02ab1b': '状态', + '9da188491dd34c4382a5b9f006194e41': '映射', + 'b3e55578af5dd473bab62641bb2f5f8e': '输入类型', + '9b6425cd2d496c9cb5a6c6b8ff125d1b': '输入框', + '15d169d28cd48c97fe751e4cc92ca926': '多行文本', + '9597dcaf432ceba92a160d61cb1ef65f': '数字输入', + '9913107b19cb6012250134ff91377430': '单选框', + '454e60f5759903d7d3dba58e3f9bd590': '勾选框', + 'db98f889ce6bc235e66bd4b2a788d137': '复选框', + '006ded9fa277cf030592021f595a07d5': '下拉框', + 'a6beb974cc0b50eebd18120b8110a88b': '开关', + 'b339aa87104709397ba68e7ebbc6e5ba': '表格', + '481e034e6026969aae4ce7ce7c8a7b6f': '文件上传', + '6bfb9bb2218ff32b6139e98bc93707c0': '图片上传', + '24b6d4c0892a8f3ee2a982e3ab0afe38': '富文本编辑器', + '4484fa04e7b71db4c8293e5bcb53eca4': '添加字段', + '4cc6a76c146c0360a41ceaf5e212c891': '基于接口自动生成字段', + 'a9fea442707e26dee478b34a2f2ce263': '请先填写接口', + '91aa2166ee4811414381c8d94e6567e6': + 'API返回格式不正确,请查看接口响应格式要求', + '3712972d84adf48acbd6ad24b4d75ad0': '列表', + '66ab5e9f24c8f46012a25c89919fb191': '新增', + 'f26225bde6a250894a04db4c53ea03d0': '详情', + '95b351c86267f3aedf89520959bce689': '编辑', + '2f4aaddde33c9b93c36fd2503f3d122b': '删除', + 'e73cefac9d030927da1618c7b15c98c9': '批量编辑', + '7fb62b30119c3797a843a48368463314': '批量删除', + '8d9a071ee2ef45e045968e117a205c07': '导入', + '55405ea6ff6fd823ffab7e6b10ddfa95': '导出', + 'c26996a6506adf397f0668d376d0b40b': '简单查询', + '6ff4bf3d567e977aa4c90c27dff1e6db': '模糊查询', + '9c4666fd08c2738eb9611a3721cb5f0f': '高级查询', + 'a094e5b7699ea4b61094cc4120170423': '数据来源', '4e7f76261f8c4c6d78998f85fc1f4c6e': '外边距', '16a20243f9b741c08216dc9548de2968': '整体', '23ecf42cada8bf2715792d718544d107': '极小', @@ -74,7 +125,6 @@ extendLocale('zh-CN', { '41150516bf0d90646edc5239593366e9': '选中组件插入到', 'd87481b371771b4f150da76e311bbbef': '输入关键字可过滤组件', 'becdc848350872592201e31bab03892a': '无法预览', - 'f26225bde6a250894a04db4c53ea03d0': '详情', '751dfe6f476903c21381c9acf88332e2': '没有可用组件,也许你该切换容器试试。', 'e22c9a05b424b761efce11f17726fdd7': '替换', '9bdb07e72d3a9a6084201a7398523f5a': '插入', @@ -96,6 +146,8 @@ extendLocale('zh-CN', { '0bd36c8db19e3a93506f39ebc8ff0ab9': '当表单、列表等组件有名字时会出现在这里方便选择', 'bb28ec819520ced0ffb4c3da01f112e2': '点击清空当前区域', + 'eeb6908870e058bc23d52c1e405a054e': '上一步', + '38ce27d84639f3a6e07c00b3b4995c0e': '下一步', 'e83a256e4f5bb4ff8b3d804b5473217a': '确认', '22c799040acdb2601b437ed5449de076': '容器', 'bd9fcf46b4e5993f97fe04ee9ebcd7ed': '撤销', @@ -119,7 +171,6 @@ extendLocale('zh-CN', { '1f81fd4598e9151538f29c41b8aa0020': '保存当前所有操作', '645dbc5504e722a30896486085a06b32': '预览', '5bc425ac8b75c571093a63eb6073c354': '开启预览模式', - '2f4aaddde33c9b93c36fd2503f3d122b': '删除', '426cd14ebd62a4922186527d07ba37f3': '删除当前节点', '499e58e764420aeed2d1476a56d8fa34': '向上移动', 'd040485f0e3887f0b297f8f772db03e4': '向上移动当前节点', @@ -150,7 +201,6 @@ extendLocale('zh-CN', { '7f2f0461a58c43667d7245ce92bb2e77': '按住拖动调整位置', '78c1c38b91c672da1113fa2564c14ea6': '向前插入组件', '87f48bbadfbef5ef4554e06b7e141d37': '向后插入组件', - '95b351c86267f3aedf89520959bce689': '编辑', '0ec9eaf9c3525eb110db58aae5912210': '更多', '417db09508befe7dbe9f84a517a6edec': '重复一份', '99b81127ef28368151621cdfccce69f8': '取消多选', diff --git a/packages/amis-editor-core/src/manager.ts b/packages/amis-editor-core/src/manager.ts index 4f69ec630..b231dac1c 100644 --- a/packages/amis-editor-core/src/manager.ts +++ b/packages/amis-editor-core/src/manager.ts @@ -45,7 +45,9 @@ import { reGenerateID, isString, isObject, - JSONPipeOut + JSONPipeOut, + generateNodeId, + JSONTraverse } from './util'; import {reaction} from 'mobx'; import {hackIn, makeSchemaFormRender, makeWrapper} from './component/factory'; @@ -58,8 +60,9 @@ 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'; +import {isPlainObject} from 'lodash'; export interface EditorManagerConfig extends Omit {} @@ -439,6 +442,7 @@ export class EditorManager { const node = this.store.getNodeById(id); panels = node ? this.collectPanels(node, true) : panels; } + this.store.setPanels( panels.map(item => ({ ...item, @@ -1277,9 +1281,12 @@ export class EditorManager { let index: number = -1; const commonContext = this.buildEventContext(id); - if (!('id' in json)) { - json = {...json, id: 'u:' + guid()}; - } + // 填充id,有些脚手架生成了复杂的布局等,这里都填充一下id + JSONTraverse(json, (value: any) => { + if (isPlainObject(value) && value.type && !value.id) { + value.id = generateNodeId(); + } + }); if (beforeId) { const arr = commonContext.schema[region]; @@ -1559,7 +1566,7 @@ export class EditorManager { * @param schema */ makeSchemaFormRender(schema: { - body?: Array; + body?: SchemaCollection; controls?: Array; definitions?: any; api?: any; @@ -1672,6 +1679,8 @@ export class EditorManager { let scope: DataScope | void; let from = node; let region = node; + + // 查找最近一层的数据域 while (!scope && from) { const nodeId = from.info?.id; const type = from.info?.type; @@ -1684,11 +1693,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 @@ -1710,6 +1726,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; + } + } + beforeDispatchEvent( originHook: any, e: any, @@ -1725,7 +1779,11 @@ export class EditorManager { component.props.$$id, component.props.type ); - node?.info?.plugin?.rendererBeforeDispatchEvent?.(node, e, JSONPipeOut(data)); + node?.info?.plugin?.rendererBeforeDispatchEvent?.( + node, + e, + JSONPipeOut(data) + ); } } diff --git a/packages/amis-editor-core/src/plugin.ts b/packages/amis-editor-core/src/plugin.ts index ef53254cf..2c985f87b 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,31 @@ export interface PluginInterface */ panelJustify?: boolean; + /** + * 有数据域的容器,可以为子组件提供读取的字段绑定页面 + */ + getAvailableContextFields?: ( + // 提供数据域的容器节点 + scopeNode: EditorNodeType, + // 数据域的应用节点 + target: EditorNodeType, + // 节点所属的容器region + 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; @@ -879,7 +918,11 @@ export interface RendererPluginAction { } // 分支动作 -export interface SubRendererPluginAction extends Pick{} +export interface SubRendererPluginAction + extends Pick< + RendererPluginAction, + 'actionType' | 'innerArgs' | 'descDetail' + > {} export interface PluginEvents { [propName: string]: RendererPluginEvent[]; @@ -957,6 +1000,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', @@ -966,9 +1019,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 d7d578a81..e836cdecd 100644 --- a/packages/amis-editor-core/src/store/editor.ts +++ b/packages/amis-editor-core/src/store/editor.ts @@ -538,7 +538,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 b149e24bf..6868a0e12 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, @@ -513,10 +514,15 @@ export function JSONMoveDownById(json: any, id: string) { }); } -export function JSONDuplicate(json: any, id: string) { +export function JSONDuplicate( + json: any, + id: string, + // 有时候复制时因为局部会有事件动作等内容,需要改为复制部分的新id,这里把老id与新id的关系存下来 + reIds: {[propKey: string]: string} = {} +) { return JSONChangeInArray(json, id, (arr: any[], node: any, index: number) => { const copy = JSONPipeIn(JSONPipeOut(node)); - arr.splice(index + 1, 0, reGenerateID(copy)); + arr.splice(index + 1, 0, reGenerateID(copy, reIds)); }); } @@ -524,16 +530,24 @@ export function JSONDuplicate(json: any, id: string) { * 用于复制或粘贴的时候重新生成 * @param json */ -export function reGenerateID(json: any) { +export function reGenerateID( + json: any, + // 有时候复制时因为局部会有事件动作等内容,需要改为复制部分的新id,这里把老id与新id的关系存下来 + reIds: {[propKey: string]: string} = {} +) { JSONTraverse(json, (value: any, key: string, host: any) => { - if ( - key === 'id' && - typeof value === 'string' && - value.indexOf('u:') === 0 && - host - ) { - host.id = 'u:' + guid(); + const isNodeIdFormat = + typeof value === 'string' && value.indexOf('u:') === 0; + if (key === 'id' && isNodeIdFormat && host) { + const newID = generateNodeId(); + reIds[host.id] = newID; + host.id = newID; } + // 组件ID,给新的id内容 + else if (key === 'componentId' && isNodeIdFormat) { + host.componentId = reIds[value] ?? value; + } + return value; }); return json; @@ -873,6 +887,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); diff --git a/packages/amis-editor-core/webpack.config.js b/packages/amis-editor-core/webpack.config.js index 6a54adfca..1bb451cce 100644 --- a/packages/amis-editor-core/webpack.config.js +++ b/packages/amis-editor-core/webpack.config.js @@ -35,10 +35,12 @@ module.exports = { } } }, + /* { loader: 'webpack-react-i18n', options: i18nConfig } + */ ] },