diff --git a/packages/amis-editor/src/component/BaseControl.ts b/packages/amis-editor/src/component/BaseControl.ts index 6828f41b8..9cfb2d0f9 100644 --- a/packages/amis-editor/src/component/BaseControl.ts +++ b/packages/amis-editor/src/component/BaseControl.ts @@ -4,9 +4,12 @@ import flatten from 'lodash/flatten'; import {getEventControlConfig} from '../util'; -import {getSchemaTpl, isObject} from 'amis-editor-core'; -import type {BaseEventContext} from 'amis-editor-core'; -import {SchemaObject} from 'amis/lib/Schema'; +import { + getSchemaTpl, + isObject, + BaseEventContext, + tipedLabel +} from 'amis-editor-core'; // 默认动作 export const BUTTON_DEFAULT_ACTION = { @@ -370,25 +373,6 @@ export const formItemControl: ( ]; }; -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 - }; -} - /** * 信息提示组件模版 */ @@ -400,7 +384,7 @@ export function remarkTpl(config: { return { type: 'ae-switch-more', formType: 'dialog', - className:'ae-switch-more-flex', + className: 'ae-switch-more-flex', label: config.labelRemark ? tipedLabel(config.label, config.labelRemark) : config.label, diff --git a/packages/amis-editor/src/component/InputComponentName.tsx b/packages/amis-editor/src/component/InputComponentName.tsx index 340fd0126..88c09625d 100644 --- a/packages/amis-editor/src/component/InputComponentName.tsx +++ b/packages/amis-editor/src/component/InputComponentName.tsx @@ -7,7 +7,7 @@ interface InputComponentNameProps extends RendererProps { } export function InputComponentName(props: InputComponentNameProps) { - const {value, onChange, render, name, node} = props; + const {value, onChange, render, name, node, placeholder} = props; const [options, setOptions] = useState>([]); useEffect(() => { @@ -37,7 +37,8 @@ export function InputComponentName(props: InputComponentNameProps) { 'inner', { type: 'input-text', - name + name, + placeholder }, { value, diff --git a/packages/amis-editor/src/index.tsx b/packages/amis-editor/src/index.tsx index c8cf227a8..4b4f380ea 100644 --- a/packages/amis-editor/src/index.tsx +++ b/packages/amis-editor/src/index.tsx @@ -74,6 +74,7 @@ import './plugin/ButtonGroup'; import './plugin/ButtonToolbar'; import './plugin/Breadcrumb'; import './plugin/Card'; +import './plugin/Card2'; import './plugin/Cards'; import './plugin/Carousel'; import './plugin/Chart'; @@ -119,6 +120,8 @@ import './plugin/Steps'; import './plugin/Sparkline'; import './plugin/Submit'; import './plugin/Table'; +import './plugin/Table-v2'; +import './plugin/TableCell-v2'; import './plugin/Tabs'; import './plugin/Tasks'; import './plugin/Time'; @@ -131,6 +134,8 @@ import './plugin/TooltipWrapper'; import './plugin/TableView'; import './plugin/CodeView'; import './plugin/WebComponent'; +import './plugin/CRUD2'; +import './plugin/ColumnToggler'; import {GridPlugin} from './plugin/Grid'; diff --git a/packages/amis-editor/src/plugin/Alert.tsx b/packages/amis-editor/src/plugin/Alert.tsx index a84ec3547..93e5082ac 100644 --- a/packages/amis-editor/src/plugin/Alert.tsx +++ b/packages/amis-editor/src/plugin/Alert.tsx @@ -78,7 +78,7 @@ export class AlertPlugin extends BasePlugin { name: 'showCloseButton' }), { - type: 'ae-Switch-More', + type: 'ae-switch-more', mode: 'normal', name: 'showIcon', label: '图标', diff --git a/packages/amis-editor/src/plugin/Avatar.tsx b/packages/amis-editor/src/plugin/Avatar.tsx index a65ffea38..06ea4c071 100644 --- a/packages/amis-editor/src/plugin/Avatar.tsx +++ b/packages/amis-editor/src/plugin/Avatar.tsx @@ -4,7 +4,7 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {BaseEventContext, BasePlugin} from 'amis-editor-core'; import {getSchemaTpl, defaultValue} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; const DefaultSize = 40; const DefaultBorderRadius = 20; @@ -173,7 +173,7 @@ export class AvatarPlugin extends BasePlugin { visibleOn: 'data.showtype === "text"' } ] - }, + } // TODO 临时关闭,目前角标功能还没完善,待完善后再开启 // getSchemaTpl('badge') ] diff --git a/packages/amis-editor/src/plugin/Button.tsx b/packages/amis-editor/src/plugin/Button.tsx index 960f98d5c..f00f8430b 100644 --- a/packages/amis-editor/src/plugin/Button.tsx +++ b/packages/amis-editor/src/plugin/Button.tsx @@ -4,10 +4,11 @@ import { BasePlugin, BasicRendererInfo, PluginInterface, - RendererInfoResolveEventContext + RendererInfoResolveEventContext, + tipedLabel } from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {BUTTON_DEFAULT_ACTION, tipedLabel} from '../component/BaseControl'; +import {BUTTON_DEFAULT_ACTION} from '../component/BaseControl'; import {getEventControlConfig} from '../util'; import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; import {SchemaObject} from 'amis/lib/Schema'; diff --git a/packages/amis-editor/src/plugin/ButtonGroup.tsx b/packages/amis-editor/src/plugin/ButtonGroup.tsx index 2db2f642e..7a74771f9 100644 --- a/packages/amis-editor/src/plugin/ButtonGroup.tsx +++ b/packages/amis-editor/src/plugin/ButtonGroup.tsx @@ -1,12 +1,13 @@ -import {registerEditorPlugin} from 'amis-editor-core'; -import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core'; import { - BUTTON_DEFAULT_ACTION, - formItemControl, - tipedLabel -} from '../component/BaseControl'; -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {RegionWrapper as Region} from 'amis-editor-core'; + BasePlugin, + RegionConfig, + BaseEventContext, + tipedLabel, + defaultValue, + getSchemaTpl, + registerEditorPlugin +} from 'amis-editor-core'; +import {BUTTON_DEFAULT_ACTION} from '../component/BaseControl'; export class ButtonGroupPlugin extends BasePlugin { // 关联渲染器名字 diff --git a/packages/amis-editor/src/plugin/CRUD2.tsx b/packages/amis-editor/src/plugin/CRUD2.tsx new file mode 100644 index 000000000..73e4f0b96 --- /dev/null +++ b/packages/amis-editor/src/plugin/CRUD2.tsx @@ -0,0 +1,1681 @@ +import cloneDeep from 'lodash/cloneDeep'; +import React from 'react'; + +import { + AfterBuildPanelBody, + BaseEventContext, + BasePlugin, + BasicRendererInfo, + PluginEvent, + RendererInfoResolveEventContext, + ScaffoldForm, + EditorManager, + registerEditorPlugin, + defaultValue, + getSchemaTpl, + generateNodeId, + tipedLabel, + EditorNodeType, + DSBuilder, + DSBuilderManager, + DSFeature, + DSFeatureType +} from 'amis-editor-core'; +import {flattenDeep, fromPairs, isObject, remove} from 'lodash'; +import {ButtonSchema} from 'amis/lib/renderers/Action'; +import {FormSchema, SchemaObject} from 'amis/lib/Schema'; +import {findTree} from 'amis'; +import {CRUD2Schema} from 'amis/lib/renderers/CRUD2'; +import {FeatureOption} from '../renderer/FeatureControl'; +import {getArgsWrapper} from '../renderer/event-control/helper'; + +const findObj = ( + obj: any, + predicate: (obj: any) => boolean, + stop?: (obj: any) => boolean +): any | void => { + const waitProcess = [obj]; + + while (waitProcess.length) { + let item: any = waitProcess.pop(); + if (Array.isArray(item)) { + waitProcess.push(...item); + continue; + } + + if (!isObject(item) || (stop && stop(item))) { + continue; + } + + if (predicate(item)) { + return item; + } + + waitProcess.push(...Object.values(item)); + } +}; + +const deepRemove = (obj: any, predicate: (obj: any) => boolean): any => { + const waitProcess = [obj]; + + while (waitProcess.length) { + let item: any = waitProcess.pop(); + if (Array.isArray(item)) { + remove(item, predicate); + waitProcess.push(...item); + continue; + } + + if (!isObject(item)) { + continue; + } + + Object.entries(item).forEach(([key, value]) => { + if (isObject(value) && predicate(value)) { + delete item[key]; + } + waitProcess.push(value); + }); + } +}; + +type FeatOption = { + label: string; + value: DSFeatureType; + makeSetting?: (builder: DSBuilder) => any; + resolveSchema: (setting: any, builder: DSBuilder) => any; + align?: 'left' | 'right'; +}; + +const Tools: Array = [ + { + label: '新增记录', + value: 'Insert', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + const form: FormSchema = { + type: 'form', + body: [] + }; + builder.resolveCreateSchema({ + schema: form, + setting, + feat: 'Insert', + inCrud: true + }); + + return { + type: 'button', + behavior: 'Insert', + label: '新增', + level: 'primary', + className: 'm-r-xs', + onEvent: { + click: { + actions: [ + { + actionType: 'dialog', + dialog: { + title: '新增数据', + body: form + } + } + ] + } + } + }; + }, + align: 'left' + }, + { + label: '批量编辑', + value: 'BulkEdit', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + const form: FormSchema = { + type: 'form', + // @ts-ignore + behavior: 'BulkEdit', + body: [] + }; + builder.resolveCreateSchema({ + schema: form, + setting, + feat: 'BulkEdit', + inCrud: true + }); + + return { + type: 'button', + behavior: 'BulkEdit', + label: '批量编辑', + className: 'm-r-xs', + onEvent: { + click: { + actions: [ + { + actionType: 'dialog', + dialog: { + title: '批量编辑', + body: form + } + } + ] + } + } + }; + }, + align: 'left' + }, + { + label: '批量删除', + value: 'BulkDelete', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + const button: ButtonSchema = { + type: 'button', + className: 'm-r-xs', + label: '批量删除', + level: 'danger', + // @ts-ignore + behavior: 'BulkDelete', + disabledOn: 'selectedItems != null && selectedItems.length < 1' + }; + + builder.resolveDeleteSchema({ + schema: button, + setting, + feat: 'BulkDelete' + }); + + return button; + }, + align: 'left' + }, + { + label: '数据导入', + value: 'Import', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + return { + type: 'button', + className: 'm-r-xs', + label: '导入', + // @ts-ignore + behavior: 'Import' + }; + }, + align: 'left' + }, + { + label: '数据导出', + value: 'Export', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + return { + type: 'button', + className: 'm-r-xs', + label: '导出', + // @ts-ignore + behavior: 'Export' + }; + }, + align: 'left' + } +]; + +const FilterTypes: Array = [ + { + label: '模糊查询', + value: 'FuzzyQuery', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + const formId = generateNodeId(); + return { + type: 'form', + behavior: 'FuzzyQuery', + id: formId, + submitOnChange: true, + wrapWithPanel: false, + onEvent: { + validateSucc: { + actions: [ + { + actionType: 'search', + componentId: setting.id, + args: { + query: '${event.data}' + } + } + ] + } + }, + body: [ + { + name: 'keywords', + type: 'input-text', + label: false, + addOn: { + type: 'button', + label: '搜索', + onEvent: { + click: { + actions: [ + { + actionType: 'submit', + componentId: formId + } + ] + } + } + } + } + ] + }; + } + }, + { + label: '简单查询', + value: 'SimpleQuery', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + return { + title: '简单查询', + type: 'form', + mode: 'inline', + behavior: 'SimpleQuery', + body: builder.resolveSimpleFilterSchema({setting}) || [], + actions: [ + { + type: 'submit', + label: '查询' + }, + { + type: 'reset', + label: '重置' + } + ], + onEvent: { + validateSucc: { + actions: [ + { + actionType: 'search', + componentId: setting.id, + args: { + query: { + filters: '${event.data}' + } + } + } + ] + } + } + }; + } + }, + { + label: '高级查询', + value: 'AdvancedQuery', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + return { + title: '高级查询', + type: 'form', + mode: 'inline', + behavior: 'AdvancedQuery', + body: builder.resolveAdvancedFilterSchema({setting}) || [], + actions: [ + { + type: 'submit', + label: '查询' + }, + { + type: 'reset', + label: '重置' + } + ], + onEvent: { + validateSucc: { + actions: [ + { + actionType: 'search', + componentId: setting.id, + args: { + query: '${event.data}' + } + } + ] + } + } + }; + } + } +]; + +// 数据操作 +const DataOperators: Array = [ + { + label: '查看详情', + value: 'View', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + return { + type: 'button', + label: '查看', + level: 'link', + behavior: 'View', + onEvent: { + click: { + actions: [ + { + actionType: 'dialog', + dialog: { + type: 'form', + title: '查看', + body: builder.resolveViewSchema({setting}) + } + } + ] + } + } + }; + } + }, + { + label: '编辑记录', + value: 'Edit', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + const form: FormSchema = { + type: 'form', + body: [] + }; + builder.resolveCreateSchema({ + schema: form, + setting, + feat: 'Edit', + inCrud: true + }); + + return { + type: 'button', + label: '编辑', + level: 'link', + behavior: 'Edit', + onEvent: { + click: { + actions: [ + { + actionType: 'dialog', + dialog: { + title: '编辑', + body: form + } + } + ] + } + } + }; + } + }, + { + label: '删除记录', + value: 'Delete', + resolveSchema: (setting: any = {}, builder: DSBuilder) => { + const button: ButtonSchema = { + type: 'button', + className: 'm-r-xs', + label: '删除', + level: 'link', + // @ts-ignore + behavior: 'Delete' + }; + builder.resolveDeleteSchema({ + schema: button, + setting, + feat: 'Delete' + }); + + return button; + } + } +]; + +export class CRUDPlugin extends BasePlugin { + constructor(manager: EditorManager) { + super(manager); + this.dsBuilderMgr = new DSBuilderManager('crud2', 'api'); + } + + afterBuildPanelBody(event: PluginEvent) { + const {context} = event.context; + + if ( + context.info.renderer?.isFormItem && + new RegExp('/crud2/filter/d/form/').test(context.path) + ) { + this.addFilterPanelSetting(event.context); + } else if ( + context.schema.type === 'crud2' && + context.schema.mode === this.scaffold.mode + ) { + this.addListPanelSetting(event.context); + } + } + + addFilterPanelSetting(context: AfterBuildPanelBody) { + const {info, node} = context.context; + if (info.renderer?.isFormItem) { + const form = node.getClosestParentByType('form'); + const host = node.getClosestParentByType('crud2'); + + if ( + !host || + !form || + !form.schema.behavior || + host.schema.mode !== this.scaffold.mode + ) { + return; + } + + const builder = this.dsBuilderMgr.resolveBuilderBySchema( + host.schema, + 'api' + ); + const body = builder.makeFieldFilterSetting({}); + + if (!body) { + return; + } + + // 可能会出错,但是cards table-v2 list 配置面板结构统一,因此 + (context.data as any).tabs.forEach((tab: any) => { + if (tab.title === '属性') { + tab.body[0].body.forEach((collapse: any) => { + if (collapse.title === '基本') { + // 在标题后面插入过滤条件 + collapse.body.splice(2, 0, ...body); + } + }); + } + }); + } + } + + addDataOperatorSchema(schema: any, content: any) {} + + addFeatToToolbar( + schema: any, + content: any, + position: 'header' | 'footer', + align: 'left' | 'right' + ) { + const region = `${position}Toolbar`; + schema[region] = schema[region] || [ + { + type: 'grid', + columns: [ + { + body: [] + }, + { + columnClassName: 'text-right', + body: [] + } + ] + } + ]; + // 尝试放到左面第一个,否则只能放外头了 + try { + schema[region][0].columns[align === 'left' ? 0 : 1].body.unshift(content); + } catch (e) { + schema[region].unshift(content); + } + } + + filterOperators(builder: DSBuilder, context: BaseEventContext) { + const operators: FeatureOption[] = []; + Tools.forEach(tool => { + if (!builder.features.includes(tool.value)) { + return; + } + operators.push({ + ...tool, + remove: (schema: any) => { + deepRemove( + schema.headerToolbar, + item => item.behavior === tool.value + ); + deepRemove( + schema.footerToolbar, + item => item.behavior === tool.value + ); + }, + add: (data: any) => { + this.addFeatToToolbar( + data, + tool.resolveSchema(data, builder), + 'header', + tool.align! + ); + }, + isActive: (data: any) => { + return this.isFeatActive( + data, + tool.value, + 'headerToolbar', + 'footerToolbar' + ); + } + }); + }); + + // 只有表格才能找到操作列放这个内容,卡片和列表不知道位置 + if (context.schema.mode === 'table-v2') { + DataOperators.forEach(op => { + if (!builder.features.includes(op.value)) { + return; + } + operators.push({ + ...op, + remove: (schema: any) => { + deepRemove(schema.columns, item => item.behavior === op.value); + }, + add: (data: any) => { + this.addDataOperatorSchema(data, op.resolveSchema(data, builder)); + }, + isActive: (data: any) => { + return this.isFeatActive(data, op.value, 'columns'); + } + }); + }); + } + + return operators; + } + + makeFeatSetting(feat: FeatOption, builder: DSBuilder, inScaffold: boolean) { + if (feat.makeSetting) { + return feat.makeSetting(builder); + } + + return builder.makeFieldsSettingForm({ + feat: feat.value, + inCrud: true, + inScaffold + }); + } + + isFeatActive(schema: any, feat: DSFeatureType, ...scope: string[]) { + if (scope.length === 0) { + return findObj(schema, item => item.behavior === feat); + } + let region = null; + while ((region = scope.shift())) { + if (findObj(schema[region], item => item.behavior === feat)) { + return true; + } + } + return false; + } + + removeFeatSchema(schema: any, feat: DSFeatureType, ...scope: string[]) { + if (scope.length === 0) { + return deepRemove(schema, item => item.behavior === feat); + } + let region = null; + while ((region = scope.shift())) { + deepRemove(schema[region], item => item.behavior === feat); + } + return false; + } + + filterColumns(builder: DSBuilder, context: BaseEventContext) { + const existColsName: string[] = []; + const columns: FeatureOption[] = []; + + // 实时获取,否则点击预览再回到编辑的时context中的node失效 + const node = this.manager.store.getNodeById(context.id); + + if (!node) { + return columns; + } + + const existCols = + node!.children.find(child => child.type === 'table-v2')?.children[0] + ?.children || []; + const hasOperatorCol = node!.schema.columns?.some( + (col: any) => col.type === 'operation' + ); + + existCols.some((col: EditorNodeType, index: number) => { + // 严格来说是单元格的集合,因此要屏蔽掉 + if (index !== 0 && col.id === existCols[0].id) { + return true; + } + col.schema && + columns.push({ + label: col.schema.title, + value: col.id + }); + col.schema?.key && existColsName.push(col.schema.key); + return false; + }); + + if (node!.schema.__fields) { + const appendCols = builder.makeTableColumnsByFields( + node!.schema.__fields + ); + if (appendCols?.length) { + appendCols.forEach((col: any) => { + if (existColsName.includes(col.key)) { + return; + } + + columns.push({ + label: col.title, + value: null, + add: data => { + data.columns.push(col); + }, + isActive: () => false + }); + }); + } + } + + if (!hasOperatorCol) { + columns.push({ + label: '操作列', + value: null, + isActive: () => false, + add: data => { + data.columns.push({ + type: 'operation', + title: '操作', + buttons: [] + }); + } + }); + } + + // context.node. + + // // 只有表格才能找到操作列放这个内容,卡片和列表不知道位置 + // if (context.schema.mode === 'table-v2') { + // DataOperators.forEach(op => { + // if (!builder.features.includes(op.value)) { + // return; + // } + // operators.push({ + // ...op, + // remove: (schema: any) => { + // deepRemove(schema.columns, item => item.behavior === op.value); + // }, + // add: (data: any) => { + // this.addDataOperatorSchema(data, op.resolveSchema(data, builder)); + // }, + // isActive:(data: any) => { + // return this.isFeatActive(data, op.value, 'columns'); + // } + // }); + // }); + // } + + return columns; + } + + addListPanelSetting(context: AfterBuildPanelBody) { + const body = context.data as any; + const builder = this.dsBuilderMgr.resolveBuilderBySchema( + context.context.schema, + 'api' + ); + + body.tabs.forEach((tab: any) => { + if (tab.title === '属性') { + tab.body[0].body.forEach((collapse: any) => { + if (collapse.title === '基本') { + collapse.body.unshift( + ...builder.makeSourceSettingForm({ + name: 'api', + label: '数据源', + feat: 'List', + inCrud: true + }), + getSchemaTpl('interval'), + + getSchemaTpl('switch', { + name: 'syncLocation', + label: tipedLabel( + '同步地址栏', + '开启后会把查询条件数据和分页信息同步到地址栏中,页面中出现多个时,建议只保留一个同步地址栏,否则会相互影响。' + ), + pipeIn: defaultValue(true) + }), + + getSchemaTpl('switch', { + label: '可选择', + name: 'selectable' + }), + + getSchemaTpl('switch', { + name: 'keepItemSelectionOnPageChange', + label: tipedLabel( + '保留选择项', + '默认切换页面、搜索后,用户选择项会被清空,开启此功能后会保留用户选择,可以实现跨页面批量操作。' + ), + visbileOn: 'this.selectable' + }) + ); + } + + if (collapse.title === '列设置') { + collapse.body.unshift( + getSchemaTpl('switch', { + name: 'columnsTogglable', + label: tipedLabel( + '自定义显示列', + '自动即列数量大于10自动开启。' + ), + onChange: ( + value: boolean, + oldValue: any, + model: any, + form: any + ) => { + const schema = cloneDeep(form.data); + if (value === true) { + this.addFeatToToolbar( + schema, + {type: 'column-toggler'}, + 'header', + 'right' + ); + } else { + deepRemove( + schema.headerToolbar, + item => item.type === 'column-toggler' + ); + } + form.setValues(schema); + return undefined; + } + }) + ); + + collapse.body.unshift({ + type: 'ae-feature-control', + label: false, + features: () => { + return this.filterColumns(builder, context.context); + }, + goFeatureComp: (feat: any) => feat.value, + removeFeature: (feat: any) => { + this.manager.del(feat.value); + }, + manager: this.manager, + addable: true, + removeable: true + }); + } + }); + + const moreCollapse = getSchemaTpl('collapseGroup', [ + { + title: '搜索设置', + order: 2, + body: FilterTypes.map(item => { + // 当前数据源可能不开启这个功能 + if (!builder.features.includes(item.value)) { + return null; + } + + // 开关配置 + const moreConfig = builder.makeFieldsSettingForm({ + feat: item.value, + inCrud: true, + inScaffold: false + }); + const base = { + label: item.label, + name: `__${item.value}`, // 没有真实作用,只是有这个才触发onChange + pipeIn: (value: any, form: any) => { + if (item.value === 'FuzzyQuery') { + return this.isFeatActive( + form.data, + item.value, + 'headerToolbar', + 'footerToolvar' + ); + } + return this.isFeatActive(form.data, item.value, 'filter'); + }, + onChange: ( + value: boolean, + oldValue: any, + model: any, + form: any + ) => { + const schema = cloneDeep(form.data); + if (value === true) { + if (item.value === 'FuzzyQuery') { + this.addFeatToToolbar( + schema, + item.resolveSchema(schema, builder), + 'header', + 'right' + ); + } else { + schema.filter.push(item.resolveSchema(schema, builder)); + } + } else if (value === false) { + this.removeFeatSchema(schema, item.value); + } + form.setValues(schema); + return undefined; + } + }; + return moreConfig && moreConfig.length + ? { + ...base, + type: 'ae-switch-more', + formType: 'extend', + form: { + body: moreConfig + } + } + : getSchemaTpl('switch', base); + }) + }, + { + title: '操作设置', + order: 3, + body: [ + { + type: 'ae-feature-control', + label: false, + features: this.filterOperators(builder, context.context), + goFeatureComp: (feat: any) => { + let node = context.context.node; + if (node.isSecondFactor) { + node = node.parent; + } + return findTree( + node.children, + item => item.schema.behavior === feat.value + )?.id; + }, + manager: this.manager, + addable: true, + addText: '添加操作', + removeable: true + } + ] + }, + { + title: '更多与分页', + order: 4, + body: [ + { + type: 'select', + label: '分页方式', + name: '__pgType', + options: [ + {label: '无', value: 'none'}, + {label: '分页', value: 'Pagination'}, + {label: '加载更多', value: 'LoadMore'} + ].filter(i => i), + pipeIn: (value: any, form: any) => { + const pg = findObj( + [] + .concat(form.data.headerToolbar) + .concat(form.data.footerToolbar), + item => + item && ['Pagination', 'LoadMore'].includes(item.behavior) + ); + return pg?.behavior || 'none'; + }, + onChange: ( + value: string, + oldValue: any, + model: any, + form: any + ) => { + const schema = cloneDeep(form.data); + const region = [] + .concat(schema.headerToolbar) + .concat(schema.footerToolbar); + + if (value === 'none') { + deepRemove( + region, + item => + item && + ['Pagination', 'LoadMore'].includes(item.behavior) + ); + } else { + let remove: string; + let content: any; + if (value === 'Pagination') { + remove = 'LoadMore'; + content = { + type: 'pagination', + behavior: value, + layout: ['total', 'perPage', 'pager', 'go'] + }; + } else { + remove = 'Pagination'; + content = { + type: 'button', + behavior: value, + label: '加载更多', + onEvent: { + click: { + actions: [ + { + actionType: 'loadMore', + componentId: schema.id + } + ] + } + } + }; + } + deepRemove( + region, + item => item && item.behavior === remove + ); + this.addFeatToToolbar(schema, content, 'footer', 'right'); + } + form.setValues(schema); + return undefined; + } + }, + getSchemaTpl('switch', { + label: tipedLabel( + '前端分页', + '数据一次性加载到浏览器,而非每次请求后端用户所请求的当页数据,不建议开启,对性能影响较大' + ), + name: '' + }) + ] + } + ]); + tab.body[0].body.splice(1, 0, ...moreCollapse.body); + // 让折叠器默认都展开 + tab.body[0].activeKey.push(...moreCollapse.activeKey); + } + }); + } + + disabledRendererPlugin = true; + + dsBuilderMgr: DSBuilderManager; + + // 关联渲染器名字 + rendererName = 'crud2'; + multifactor = true; + $schema = '/schemas/CRUD2Schema.json'; + + order = -1000; + + docLink = '/amis/zh-CN/components/crud'; + tags = ['数据容器']; + + scaffold: CRUD2Schema; + + getScaffoldFeatureTab() { + const generator = (feat: FeatOption, featGroup?: string) => + this.dsBuilderMgr.collectFromBuilders((builder, builderName) => { + if (!builder.features.includes(feat.value)) { + return null; + } + + const content = this.makeFeatSetting(feat, builder, true); + if (!content || content.length === 0) { + return null; + } + + const isFeatOpened = featGroup + ? `data['${featGroup}'] && ~data['${featGroup}'].indexOf('${feat.value}')` + : true; + + return { + title: feat.label, + visibleOn: `(!data.dsType || data.dsType === '${builderName}') && ${isFeatOpened}`, + body: content + .filter((i: any) => i) + .map((formItem: object) => ({ + ...formItem, + mode: 'normal' + })) + }; + }); + + return flattenDeep([ + generator({ + label: '列表展示', + value: 'List', + resolveSchema() {} + }), + Tools.map(item => generator(item, 'tools')), + FilterTypes.map(item => generator(item, 'filters')), + DataOperators.map(item => generator(item, 'operators')) + ]).filter(i => i); + } + + name: string; + + /** 将数据资源和数据操作进行填充 */ + resolveListField(setting: any, schema: any, builder: DSBuilder) {} + + get scaffoldForm(): ScaffoldForm { + return { + title: `${this.name}创建向导`, + mode: { + mode: 'horizontal', + horizontal: { + leftFixed: 'sm' + } + }, + className: 'ae-Scaffold-Modal ae-formItemControl', + stepsBody: true, + body: [ + { + title: '数据配置', + body: [ + this.dsBuilderMgr.getDSSwitch({ + onChange: (value: any, oldValue: any, model: any, form: any) => { + if (value !== oldValue) { + const data = form.data; + Object.keys(data).forEach(key => { + if (key.endsWith('Fields') || key.endsWith('api')) { + form.deleteValueByName(key); + } + }); + form.deleteValueByName('__fields'); + } + return value; + } + }), + // 数据源选择 + ...this.dsBuilderMgr.collectFromBuilders((builder, builderFlag) => { + return { + type: 'container', + visibleOn: `dsType == null || dsType === '${builderFlag}'`, + body: flattenDeep([ + builder.makeSourceSettingForm({ + feat: 'List', + inScaffold: true, + inCrud: true + }) + ]) + }; + }) + ] + }, + { + title: '功能配置', + body: [ + ...this.dsBuilderMgr.collectFromBuilders((builder, builderName) => { + const check = (item: FeatOption) => + DSFeature[item.value] == null || + builder.features.includes(item.value); + return { + type: 'container', + visibleOn: `dsType == null || dsType === '${builderName}'`, + body: [ + { + type: 'checkboxes', + label: '工具栏', + name: 'tools', + joinValues: false, + extractValue: true, + multiple: true, + options: Tools.filter(check) + }, + { + type: 'checkboxes', + label: '条件查询', + name: 'filters', + multiple: true, + joinValues: false, + extractValue: true, + options: FilterTypes.filter(check) + }, + { + type: 'checkboxes', + label: '数据操作', + name: 'operators', + multiple: true, + joinValues: false, + extractValue: true, + options: DataOperators.filter(check) + }, + // 占位,最后一个form item没有间距 + { + type: 'container' + } + ] + }; + }), + { + type: 'tabs', + tabsMode: 'vertical', + className: 'ae-Scaffold-Modal-Tabs', + tabs: this.getScaffoldFeatureTab() + } + ] + } + ], + pipeIn: (value: any) => { + return ( + value?.$$m || { + dsType: this.dsBuilderMgr.getDefaultBuilderName() + } + ); + }, + pipeOut: (value: any) => { + // 决定组件基础配置和所使用的模式 + const schema: any = cloneDeep(this.scaffold); + const builder = this.dsBuilderMgr.resolveBuilderBySetting(value); + if (!builder) { + return schema; + } + + // list功能 + builder.resolveSourceSchema({ + schema, + feat: 'List', + setting: value, + name: 'api', + inCrud: true + }); + + schema.id = schema.id ?? generateNodeId(); // 先生成一个,方便其他流程生成事件动作 + schema.$$m = value; + + if (value.filters) { + schema.filter = []; + FilterTypes.forEach(feat => { + if (!value.filters.includes(feat.value)) { + return; + } + if (feat.value === 'FuzzyQuery') { + this.addFeatToToolbar( + schema, + feat.resolveSchema(value, builder), + 'header', + 'right' + ); + return; + } + schema.filter.push(feat.resolveSchema(value, builder)); + }); + } + + if (value.tools) { + Tools.concat() + .reverse() + .forEach(feat => { + if (value.tools.includes(feat.value)) { + this.addFeatToToolbar( + schema, + feat.resolveSchema(value, builder), + 'header', + feat.align! + ); + } + }); + + if ( + value.tools.find((obj: any) => + ['BulkEdit', 'BulkDelete'].includes(obj) + ) + ) { + schema.multiple = true; + schema.selectable = true; + } + } + + this.resolveListField(value, schema, builder); + + return schema; + }, + canRebuild: true + }; + } + + events = [ + { + eventName: 'get-data', + eventLabel: '数据加载', + description: '列表数据翻页' + } + ]; + + actions = [ + { + actionType: 'search', + actionLabel: '数据查询', + description: '使用指定条件完成列表数据查询', + config: ['query'], + desc: (info: any) => { + return ( +
+ {info?.__rendererLabel} + 触发数据查询 +
+ ); + }, + schema: getArgsWrapper({ + type: 'input-formula', + variables: '${variables}', + evalMode: false, + variableMode: 'tabs', + label: '查询条件', + size: 'md', + name: 'query', + mode: 'horizontal' + }) + }, + // { + // actionType: 'resetQuery', + // actionLabel: '重置查询', + // description: '重新恢复查询条件为初始值', + // desc: (info: any) => { + // return ( + //
+ // {info?.__rendererLabel} + // 重置初始查询条件 + //
+ // ); + // } + // }, + { + actionType: 'loadMore', + actionLabel: '加载更多', + description: '加载更多条数据到列表容器', + desc: (info: any) => { + return ( +
+ {info?.__rendererLabel} + 加载更多数据 +
+ ); + } + } + ]; + + previewSchema: any = { + syncLocation: false, + type: 'crud2', + className: 'text-left', + bodyClassName: 'm-b-none', + affixHeader: false, + data: { + items: [ + {a: 1, b: 2}, + {a: 3, b: 4}, + {a: 5, b: 6} + ] + }, + source: '${items}', + columns: [ + { + title: 'A', + key: 'a' + }, + { + title: 'B', + key: 'b' + }, + { + key: 'operation', + title: '操作', + buttons: [ + { + icon: 'fa fa-eye', + type: 'button' + }, + + { + icon: 'fa fa-edit', + type: 'button' + } + ] + } + ] + }; + + getRendererInfo( + context: RendererInfoResolveEventContext + ): BasicRendererInfo | void { + const {renderer, schema} = context; + if ( + this.scaffold && + renderer.name === 'crud2' && + schema.mode === this.scaffold.mode + ) { + return super.getRendererInfo(context); + } + } + + async buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) { + const child: EditorNodeType = node.children.find( + item => !!~['table-v2', 'cards', 'list'].indexOf(item.type) + ); + + let items; + // 先从数据源获取可用字段 + const builder = this.dsBuilderMgr.resolveBuilderBySchema( + node.schema, + 'api' + ); + if (builder && node.schema.api) { + const fields = await builder.getContextFileds({ + schema: node.schema, + sourceKey: 'api', + feat: 'List' + }); + + if (fields) { + items = { + type: 'object', + properties: fromPairs( + fields.map(field => { + return [ + field.value, + { + type: field.valueType || 'string', + title: field.label + } + ]; + }) + ) + }; + } + } + + // 数据源没配置或者拿不到数据,从已配置的内容组合一下做兜底处理 + if (items == null) { + const childDataSchema = await child?.info.plugin.buildDataSchemas?.( + child, + region + ); + items = childDataSchema?.properties?.items; + } + + const schema: any = { + $id: 'crud2', + type: 'object', + properties: { + ...items.properties, + items: { + ...items, + title: '全部数据' + }, + selectedItems: { + ...items, + title: '选中数据' + }, + unSelectedItems: { + ...items, + title: '未选中数据' + }, + page: { + type: 'number', + title: '当前页码' + }, + total: { + type: 'number', + title: '总数据条数' + } + } + }; + + return schema; + } + + async getAvailableContextFields( + scopeNode: EditorNodeType, + node: EditorNodeType, + region?: EditorNodeType + ) { + // 先从数据源获取可用字段 + const builder = this.dsBuilderMgr.resolveBuilderBySchema( + scopeNode.schema, + 'api' + ); + if (builder && scopeNode.schema.api) { + return builder.getAvailableContextFileds({ + schema: scopeNode.schema, + sourceKey: 'api', + feat: 'List' + }); + } + } +} + +export class TableCRUDPlugin extends CRUDPlugin { + disabledRendererPlugin = false; + // 组件名称 + name = '表格'; + isBaseComponent = true; + description = + '用来实现对数据的增删改查,支持三种模式展示:table、cards和list. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能。集成查询条件。'; + + order = -1000; + icon = 'fa fa-table'; + + scaffold: any = { + type: 'crud2', + mode: 'table-v2', + columns: [ + { + key: 'id', + title: 'ID', + type: 'container', + body: [ + { + type: 'text' + } + ] + }, + { + key: 'engine', + title: '示例', + type: 'container', + body: [ + { + type: 'text' + } + ] + } + ] + }; + + resolveListField(setting: any, schema: any, builder: DSBuilder) { + builder.resolveTableSchema({schema, setting, inCrud: true}); + + if (setting.operators?.length) { + const operators: SchemaObject[] = []; + DataOperators.forEach(feat => { + if (setting.operators?.includes(feat.value)) { + operators.push(feat.resolveSchema(setting, builder)); + } + }); + + schema.columns.push({ + type: 'operation', + title: '操作', + buttons: operators + }); + } + } + + panelTitle: '表格'; +} + +export class CardsCRUDPlugin extends CRUDPlugin { + disabledRendererPlugin = false; + // 组件名称 + name = '卡片列表'; + isBaseComponent = true; + description = + '围绕卡片列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。'; + + order = -1000; + icon = 'fa fa-table'; + + scaffold: any = { + type: 'crud2', + mode: 'cards', + card: { + type: 'card2', + body: [ + { + type: 'container', + body: [ + { + type: 'tpl', + tpl: '标题', + inline: false, + style: { + marginTop: '0', + marginBottom: '0', + paddingTop: '', + paddingBottom: '' + }, + wrapperComponent: 'h2' + }, + { + type: 'tpl', + tpl: '副标题内容', + inline: false, + style: { + color: '#9b9b9b', + marginTop: '0', + marginBottom: '0' + } + } + ] + // style: { + // borderStyle: 'solid', + // borderColor: '#ebebeb', + // borderWidth: '1px', + // 'borderRadius': '5px', + // 'paddingTop': '10px', + // 'paddingRight': '10px', + // 'paddingBottom': '0', + // 'paddingLeft': '10px' + // } + } + ] + } + }; + + resolveListField(setting: any, schema: any, builder: DSBuilder) { + let fields = setting.listFields; + if (!fields || !fields.length) { + return; + } + + schema.card.body[0].body = builder.resolveViewSchema({ + setting, + feat: 'List' + }); + + if (setting.operators?.length) { + const operators: SchemaObject[] = []; + DataOperators.forEach(feat => { + if (setting.operators?.includes(feat.value)) { + operators.push(feat.resolveSchema(setting, builder)); + } + }); + + schema.card.body.push({ + type: 'button-group', + buttons: operators + }); + } + } + + /**填充一个数据操作 */ + fillOperatorSchema(schema: any, content: any) { + let col = schema.columns.find((item: any) => item.type === 'operators'); + if (!col) { + schema.columns.push({ + type: 'operation', + title: '操作', + buttons: [content] + }); + return; + } + col.buttons.push(content); + } + + /** 判断内容区是否有填充数据操作 */ + existOperator(feat: DSFeatureType, schema: any) { + return findObj(schema.card.body, item => item.behavior === feat); + } +} + +export class ListCRUDPlugin extends CRUDPlugin { + disabledRendererPlugin = false; + // 组件名称 + name = '列表'; + isBaseComponent = true; + description = + '围绕列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。'; + + order = -1000; + icon = 'fa fa-align-justify'; + + scaffold: any = { + type: 'crud2', + mode: 'list', + listItem: { + body: [ + { + type: 'container', + body: [ + { + type: 'tpl', + tpl: '简单的展示数据' + // style: { + // marginTop: '0', + // marginBottom: '0', + // paddingTop: '', + // paddingBottom: '' + // } + } + ] + } + ] + } + }; + + resolveListField(setting: any, schema: any, builder: DSBuilder) { + let fields = setting.listFields; + if (!fields || !fields.length) { + return; + } + + schema.listItem.body = builder.resolveViewSchema({setting, feat: 'List'}); + + if (setting.operators?.length) { + const operators: SchemaObject[] = []; + DataOperators.forEach(feat => { + if (setting.operators?.includes(feat.value)) { + operators.push(feat.resolveSchema(setting, builder)); + } + }); + + schema.listItem.body.push({ + type: 'button-group', + buttons: operators + }); + } + } + + /** 判断内容区是否有填充数据操作 */ + existOperator(feat: DSFeatureType, schema: any) { + return findObj(schema.listItem.body, item => item.behavior === feat); + } +} + +registerEditorPlugin(TableCRUDPlugin); +registerEditorPlugin(CardsCRUDPlugin); +registerEditorPlugin(ListCRUDPlugin); diff --git a/packages/amis-editor/src/plugin/Card.tsx b/packages/amis-editor/src/plugin/Card.tsx index 5ef604fc6..e74faffba 100644 --- a/packages/amis-editor/src/plugin/Card.tsx +++ b/packages/amis-editor/src/plugin/Card.tsx @@ -4,10 +4,13 @@ import {registerEditorPlugin} from 'amis-editor-core'; import { BaseEventContext, BasePlugin, + BasicRendererInfo, InsertEventContext, PluginEvent, + PluginInterface, RegionConfig, RendererInfo, + RendererInfoResolveEventContext, VRendererConfig } from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; @@ -204,6 +207,8 @@ export class CardPlugin extends BasePlugin { }*/ fieldWrapperResolve = (dom: HTMLElement) => dom; + + overrides = { renderFeild: function ( this: any, diff --git a/packages/amis-editor/src/plugin/Card2.tsx b/packages/amis-editor/src/plugin/Card2.tsx new file mode 100644 index 000000000..612fe81f3 --- /dev/null +++ b/packages/amis-editor/src/plugin/Card2.tsx @@ -0,0 +1,104 @@ +import { + BaseEventContext, + BasePlugin, + RegionConfig, + defaultValue, + getSchemaTpl, + tipedLabel, + registerEditorPlugin +} from 'amis-editor-core'; + +export class Card2Plugin extends BasePlugin { + // 关联渲染器名字 + rendererName = 'card2'; + $schema = '/schemas/Card2Schema.json'; + + // 组件名称 + name = '卡片'; + isBaseComponent = true; + description = '展示单个卡片。'; + tags = ['展示']; + icon = ''; + scaffold = { + type: 'card2', + body: '内容' + }; + previewSchema = { + ...this.scaffold + }; + + regions: Array = [ + { + key: 'body', + label: '内容区', + renderMethod: 'renderBody', + preferTag: '展示' + } + ]; + + panelTitle = '卡片'; + + panelJustify = true; + + panelBodyCreator = (context: BaseEventContext) => { + return [ + getSchemaTpl('tabs', [ + { + title: '属性', + body: getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + { + type: 'button-group-select', + label: tipedLabel( + '选择区域', + '点击触发选中或取消选中的区域' + ), + name: 'checkOnItemClick', + options: [ + {label: '整个', value: true}, + {label: '选框', value: false} + ], + pipeIn: defaultValue(false) + }, + getSchemaTpl('switch', { + label: tipedLabel( + '隐藏选框', + '不再显示选择框,可以通过自定义选中态外观实现选中样式' + ), + name: 'hideCheckToggler', + visibleOn: 'this.checkOnItemClick' + }) + ] + }, + getSchemaTpl('status', {isFormItem: false}) + ]) + }, + { + title: '外观', + body: getSchemaTpl('collapseGroup', [ + getSchemaTpl('style:classNames', { + isFormItem: false, + schema: [ + getSchemaTpl('className', { + name: 'bodyClassName', + label: '内容区', + visibleOn: 'this.icon' + }), + // TODO + getSchemaTpl('className', { + name: 'selectedClassName', + label: '选中态', + visibleOn: 'this.icon' + }) + ] + }) + ]) + } + ]) + ]; + }; +} + +registerEditorPlugin(Card2Plugin); diff --git a/packages/amis-editor/src/plugin/Cards.tsx b/packages/amis-editor/src/plugin/Cards.tsx index 7d802730d..b40aeb459 100644 --- a/packages/amis-editor/src/plugin/Cards.tsx +++ b/packages/amis-editor/src/plugin/Cards.tsx @@ -4,18 +4,17 @@ import {registerEditorPlugin} from 'amis-editor-core'; import { BaseEventContext, BasePlugin, - BasicPanelItem, BasicRendererInfo, BasicToolbarItem, ContextMenuEventContext, ContextMenuItem, PluginInterface, - RegionConfig, - RendererInfo, - RendererInfoResolveEventContext + RendererInfoResolveEventContext, + tipedLabel } from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core'; +import {getEventControlConfig} from '../util'; export class CardsPlugin extends BasePlugin { // 关联渲染器名字 @@ -41,26 +40,17 @@ export class CardsPlugin extends BasePlugin { }, columnsCount: 2, card: { - type: 'card', + type: 'card2', className: 'm-b-none', - header: { - title: '标题', - subTitle: '副标题' - }, body: [ { - name: 'a', - label: 'A' - }, - { - name: 'b', - label: 'B' - } - ], - actions: [ - { - label: '详情', - type: 'button' + type: 'container', + body: [ + { + type: 'tpl', + tpl: '这是一个模板' + } + ] } ] } @@ -71,116 +61,119 @@ export class CardsPlugin extends BasePlugin { }; panelTitle = '卡片集'; + + panelJustify = true; + panelBodyCreator = (context: BaseEventContext) => { - const isCRUDBody = context.schema.type === 'crud'; - return [ - getSchemaTpl('tabs', [ - { - title: '常规', - body: [ - { - children: ( -
- -
- ) - }, - - { - type: 'divider' - }, - { - name: 'title', - type: 'input-text', - label: '标题' - }, - { - name: 'href', - type: 'input-text', - label: '打开外部链接' - }, - isCRUDBody - ? null - : { - name: 'source', - type: 'input-text', - label: '数据源', - pipeIn: defaultValue('${items}'), - description: '绑定当前环境变量', - test: !isCRUDBody - }, - { - name: 'placeholder', - value: '暂无数据', - type: 'input-text', - label: '无数据提示' - } - ] - }, - { - title: '外观', - body: [ - getSchemaTpl('switch', { - name: 'showHeader', - label: '是否显示头部', - pipeIn: defaultValue(true) - }), - - getSchemaTpl('switch', { - name: 'showFooter', - label: '是否显示底部', - pipeIn: defaultValue(true) - }), - - getSchemaTpl('className', { - label: 'CSS 类名' - }), - getSchemaTpl('className', { - name: 'headerClassName', - label: '头部 CSS 类名' - }), - getSchemaTpl('className', { - name: 'footerClassName', - label: '底部 CSS 类名' - }), - getSchemaTpl('className', { - name: 'itemsClassName', - label: '内容 CSS 类名' - }), - getSchemaTpl('className', { - pipeIn: defaultValue('Grid-col--sm6 Grid-col--md4 Grid-col--lg3'), - name: 'itemClassName', - label: '卡片 CSS 类名' - }), - { - name: 'columnsCount', - type: 'input-range', - visibleOn: '!this.leftFixed', - min: 0, - max: 12, - step: 1, - label: '每行显示个数', - description: '不设置时,由卡片 CSS 类名决定' - }, - getSchemaTpl('switch', { - name: 'masonryLayout', - label: '启用瀑布流' - }) - ] - }, - { - title: '显隐', - body: [getSchemaTpl('ref'), getSchemaTpl('visible')] - } - ]) - ]; + const isCRUDBody = ['crud', 'crud2'].includes(context.schema.type); + + return getSchemaTpl('tabs', [ + { + title: '属性', + body: getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + getSchemaTpl('switch', { + label: '可多选', + name: 'multiple', + visibleOn: `data.selectable` + }), + // getSchemaTpl('switch', { + // label: '可全选', + // name: 'checkAll', + // pipeIn: defaultValue(true), + // visibleOn: `data.selectable && data.multiple` + // }), + { + name: 'placeholder', + value: '暂无数据', + type: 'input-text', + label: '占位内容' + } + ] + }, + { + title: '数据', + hidden: isCRUDBody, + body: [ + { + name: 'source', + type: 'input-text', + label: tipedLabel('数据', '可绑定当前页面数据'), + pipeIn: defaultValue('${items}'), + // visible: !isCRUDBody + }, + { + name: 'valueField', + type: 'input-text', + label: '值字段', + // visible: isInForm && !isCRUDBody + }, + ] + }, + getSchemaTpl('status', { + isFormItem: false + }) + ]) + }, + { + title: '外观', + body: getSchemaTpl('collapseGroup', [ + { + title: '布局', + body: [ + { + type: 'button-group-select', + name: 'masonryLayout', + label: '模式', + pipeIn: defaultValue(false), + options: [ + {label: '瀑布', value: true}, + {label: '流式', value: false} + ] + }, + { + name: 'columnsCount', + type: 'input-range', + pipeIn: defaultValue(4), + min: 0, + max: 12, + step: 1, + label: tipedLabel( + '每行显示个数', + '不设置时,由卡片 CSS 类名决定' + ) + } + ] + }, + getSchemaTpl('style:classNames', { + isFormItem: false, + schema: [ + getSchemaTpl('className', { + name: 'itemsClassName', + label: '内容' + }), + getSchemaTpl('className', { + pipeIn: defaultValue('Grid-col--sm6 Grid-col--md4 Grid-col--lg3'), + name: 'itemClassName', + label: '卡片' + }), + ] + }) + ]) + }, + { + title: '事件', + className: 'p-none', + body: [ + getSchemaTpl('eventControl', { + name: 'onEvent', + ...getEventControlConfig(this.manager, context) + }) + ] + } + ]); }; editDetail(id: string) { @@ -253,14 +246,16 @@ export class CardsPlugin extends BasePlugin { ...props.defaultData, ...props.data }; - const arr = Array.isArray(props.value) + let value = Array.isArray(props.value) ? props.value : typeof props.source === 'string' - ? resolveVariable(props.source, data) - : resolveVariable('items', data); - - if (!Array.isArray(arr) || !arr.length) { - const mockedData: any = { + ? resolveVariable(props.source, data) + : resolveVariable('items', data); + + value = !Array.isArray(value) ? [] : value; + + if (value.length < 5) { + const mockedData: any = value.length ? value[0] : { id: 666, title: '假数据', description: '假数据', @@ -268,20 +263,36 @@ export class CardsPlugin extends BasePlugin { b: '假数据' }; - props.value = repeatArray(mockedData, 1).map((item, index) => ({ + value = value.concat(repeatArray(mockedData, 3).map((item, index) => ({ ...item, id: index + 1 - })); + }))); } - - const {$schema, ...rest} = props; + + value = value.slice(0, 4); return { - ...JSONPipeOut(rest), - $schema + ...props, + value }; } + overrides = { + renderCard( + this: any, + index: number, + card: any, + ...rest: any[] + ) { + return this.super( + index, + // 使第一个卡片元素可以选择并编辑schema + index > 0 ? JSONPipeOut(card) : card, + ...rest + ); + } + } + getRendererInfo( context: RendererInfoResolveEventContext ): BasicRendererInfo | void { @@ -289,7 +300,7 @@ export class CardsPlugin extends BasePlugin { const {renderer, schema} = context; if ( !schema.$$id && - schema.$$editor?.renderer.name === 'crud' && + ['crud', 'crud2'].includes(schema.$$editor?.renderer.name) && renderer.name === 'cards' ) { return { diff --git a/packages/amis-editor/src/plugin/Collapse.tsx b/packages/amis-editor/src/plugin/Collapse.tsx index cb295c0d3..05b65d884 100644 --- a/packages/amis-editor/src/plugin/Collapse.tsx +++ b/packages/amis-editor/src/plugin/Collapse.tsx @@ -1,7 +1,7 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; export class CollapsePlugin extends BasePlugin { // 关联渲染器名字 diff --git a/packages/amis-editor/src/plugin/CollapseGroup.tsx b/packages/amis-editor/src/plugin/CollapseGroup.tsx index 8fa5ec299..f8214e00f 100644 --- a/packages/amis-editor/src/plugin/CollapseGroup.tsx +++ b/packages/amis-editor/src/plugin/CollapseGroup.tsx @@ -2,7 +2,7 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import {isObject} from 'amis-editor-core'; export class CollapseGroupPlugin extends BasePlugin { diff --git a/packages/amis-editor/src/plugin/ColumnToggler.tsx b/packages/amis-editor/src/plugin/ColumnToggler.tsx new file mode 100644 index 000000000..86a80c5ec --- /dev/null +++ b/packages/amis-editor/src/plugin/ColumnToggler.tsx @@ -0,0 +1,129 @@ +import { + BaseEventContext, + BasePlugin, + BasicRendererInfo, + PluginInterface, + RendererInfoResolveEventContext, + getSchemaTpl, + registerEditorPlugin +} from 'amis-editor-core'; + +export class ColumnToggler extends BasePlugin { + // 关联渲染器名字 + rendererName = 'column-toggler'; + $schema = '/schemas/ColumnToggler.json'; + + // 组件名称 + name = '自定义显示列'; + isBaseComponent = true; + description = + '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。'; + + tags = ['自定义显示列']; + icon = 'fa fa-square'; + + panelTitle = '自定义显示列'; + + panelJustify = true; + + panelBodyCreator = (context: BaseEventContext) => { + return getSchemaTpl('tabs', [ + { + title: '属性', + body: getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + { + label: '按钮文字', + type: 'input-text', + name: 'label' + }, + { + label: '按钮提示', + type: 'input-text', + name: 'tooltip' + }, + getSchemaTpl('switch', { + name: 'defaultIsOpened', + label: '是否默认展开' + }), + getSchemaTpl('icon', { + label: '按钮图标' + }) + ] + } + ]) + }, + { + title: '外观', + body: getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + getSchemaTpl('size', { + label: '按钮尺寸' + }) + ] + }, + { + title: 'CSS 类名', + body: [ + getSchemaTpl('className', { + name: 'className', + label: '显示列样式' + }), + + getSchemaTpl('className', { + name: 'btnClassName', + label: '按钮样式' + }), + ] + } + ]) + } + ]); + }; + + /** + * 如果禁用了没办法编辑 + */ + filterProps(props: any) { + props.disabled = false; + return props; + } + + /** + * 如果配置里面有 rendererName 自动返回渲染器信息。 + * @param renderer + */ + getRendererInfo({ + renderer, + schema + }: RendererInfoResolveEventContext): BasicRendererInfo | void { + const plugin: PluginInterface = this; + + if ( + schema.$$id && + plugin.name && + plugin.rendererName && + plugin.rendererName === renderer.name + ) { + // 复制部分信息出去 + return { + name: schema.label ? schema.label : plugin.name, + regions: plugin.regions, + patchContainers: plugin.patchContainers, + // wrapper: plugin.wrapper, + vRendererConfig: plugin.vRendererConfig, + wrapperProps: plugin.wrapperProps, + wrapperResolve: plugin.wrapperResolve, + filterProps: plugin.filterProps, + $schema: plugin.$schema, + renderRenderer: plugin.renderRenderer + }; + } + } +} + +registerEditorPlugin(ColumnToggler); diff --git a/packages/amis-editor/src/plugin/Container.tsx b/packages/amis-editor/src/plugin/Container.tsx index a294b556e..e4a50bb50 100644 --- a/packages/amis-editor/src/plugin/Container.tsx +++ b/packages/amis-editor/src/plugin/Container.tsx @@ -12,11 +12,17 @@ export class ContainerPlugin extends BasePlugin { isBaseComponent = true; description = '一个简单的容器,可以将多个渲染器放置在一起。'; tags = ['容器']; + withDataSource = false; icon = 'fa fa-square-o'; pluginIcon = 'container-plugin'; scaffold = { type: 'container', - body: '内容' + body: [ + { + type: 'tpl', + tpl: '内容' + } + ] }; previewSchema = { ...this.scaffold diff --git a/packages/amis-editor/src/plugin/DropDownButton.tsx b/packages/amis-editor/src/plugin/DropDownButton.tsx index 1affff02a..38582e359 100644 --- a/packages/amis-editor/src/plugin/DropDownButton.tsx +++ b/packages/amis-editor/src/plugin/DropDownButton.tsx @@ -1,20 +1,18 @@ import {Button} from 'amis'; import React from 'react'; -import {registerEditorPlugin} from 'amis-editor-core'; import { BaseEventContext, BasePlugin, BasicToolbarItem, ContextMenuEventContext, - ContextMenuItem + ContextMenuItem, + registerEditorPlugin, + tipedLabel, + defaultValue, + getSchemaTpl, + diff } from 'amis-editor-core'; -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {diff} from 'amis-editor-core'; -import { - BUTTON_DEFAULT_ACTION, - formItemControl, - tipedLabel -} from '../component/BaseControl'; +import {BUTTON_DEFAULT_ACTION} from '../component/BaseControl'; export class DropDownButtonPlugin extends BasePlugin { // 关联渲染器名字 rendererName = 'dropdown-button'; diff --git a/packages/amis-editor/src/plugin/Form/ButtonGroupSelect.tsx b/packages/amis-editor/src/plugin/Form/ButtonGroupSelect.tsx index e72ae69d0..9bae54f60 100644 --- a/packages/amis-editor/src/plugin/Form/ButtonGroupSelect.tsx +++ b/packages/amis-editor/src/plugin/Form/ButtonGroupSelect.tsx @@ -3,10 +3,10 @@ import {BasePlugin, BaseEventContext} from 'amis-editor-core'; import { RendererPluginAction, - RendererPluginEvent + RendererPluginEvent, + tipedLabel } from 'amis-editor-core'; import {getSchemaTpl, defaultValue} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {getEventControlConfig} from '../../util'; export class ButtonGroupControlPlugin extends BasePlugin { @@ -118,17 +118,15 @@ export class ButtonGroupControlPlugin extends BasePlugin { rendererSchema: context?.schema, useSelectMode: true, // 改用 Select 设置模式 visibleOn: 'this.options && this.options.length > 0' - }), + }) ] }, { title: '按钮管理', - body: [ - getSchemaTpl('optionControlV2'), - ] + body: [getSchemaTpl('optionControlV2')] }, getSchemaTpl('status', { - isFormItem: true, + isFormItem: true }) ]) ] @@ -172,10 +170,10 @@ export class ButtonGroupControlPlugin extends BasePlugin { getSchemaTpl('className', { label: '按钮', name: 'btnClassName' - }), + }) ] - }), - ]), + }) + ]) ] }, { diff --git a/packages/amis-editor/src/plugin/Form/ChainedSelect.tsx b/packages/amis-editor/src/plugin/Form/ChainedSelect.tsx index 49c04ff1d..5aaedf633 100644 --- a/packages/amis-editor/src/plugin/Form/ChainedSelect.tsx +++ b/packages/amis-editor/src/plugin/Form/ChainedSelect.tsx @@ -4,16 +4,15 @@ import { BaseEventContext, BasicSubRenderInfo, RendererEventContext, - SubRendererInfo -} from 'amis-editor-core'; -import {getSchemaTpl, defaultValue} from 'amis-editor-core'; -import {ValidatorTag} from '../../validator'; -import {tipedLabel} from '../../component/BaseControl'; -import {getEventControlConfig} from '../../util'; -import { + SubRendererInfo, RendererPluginAction, - RendererPluginEvent + RendererPluginEvent, + tipedLabel, + getSchemaTpl, + defaultValue } from 'amis-editor-core'; +import {ValidatorTag} from '../../validator'; +import {getEventControlConfig} from '../../util'; export class ChainedSelectControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -135,7 +134,8 @@ export class ChainedSelectControlPlugin extends BasePlugin { getSchemaTpl('labelRemark'), getSchemaTpl('remark'), getSchemaTpl('placeholder'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, { diff --git a/packages/amis-editor/src/plugin/Form/Checkbox.tsx b/packages/amis-editor/src/plugin/Form/Checkbox.tsx index 0306e4e45..0bdccfd45 100644 --- a/packages/amis-editor/src/plugin/Form/Checkbox.tsx +++ b/packages/amis-editor/src/plugin/Form/Checkbox.tsx @@ -10,14 +10,12 @@ import { BasicSubRenderInfo, RendererEventContext, SubRendererInfo, - BaseEventContext + BaseEventContext, + tipedLabel } from 'amis-editor-core'; import {ValidatorTag} from '../../validator'; -import {tipedLabel} from '../../component/BaseControl'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; + +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; import {getEventControlConfig} from '../../util'; setSchemaTpl('option', { @@ -110,7 +108,7 @@ export class CheckboxControlPlugin extends BasePlugin { getSchemaTpl('label'), getSchemaTpl('option'), { - type: 'ae-Switch-More', + type: 'ae-switch-more', hiddenOnDefault: false, mode: 'normal', label: '值格式', @@ -152,7 +150,8 @@ export class CheckboxControlPlugin extends BasePlugin { }), getSchemaTpl('labelRemark'), getSchemaTpl('remark'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, getSchemaTpl('status', {isFormItem: true}), diff --git a/packages/amis-editor/src/plugin/Form/Checkboxes.tsx b/packages/amis-editor/src/plugin/Form/Checkboxes.tsx index d4e3cc41b..ef97c1704 100644 --- a/packages/amis-editor/src/plugin/Form/Checkboxes.tsx +++ b/packages/amis-editor/src/plugin/Form/Checkboxes.tsx @@ -168,7 +168,8 @@ export class CheckboxesControlPlugin extends BasePlugin { getSchemaTpl('extractValue'), getSchemaTpl('labelRemark'), getSchemaTpl('remark'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, { diff --git a/packages/amis-editor/src/plugin/Form/CodeEditor.tsx b/packages/amis-editor/src/plugin/Form/CodeEditor.tsx index caccf444b..ad1d6f141 100644 --- a/packages/amis-editor/src/plugin/Form/CodeEditor.tsx +++ b/packages/amis-editor/src/plugin/Form/CodeEditor.tsx @@ -140,7 +140,8 @@ export class CodeEditorControlPlugin extends BasePlugin { }), getSchemaTpl('labelRemark'), getSchemaTpl('remark'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, getSchemaTpl('status', {isFormItem: true}), diff --git a/packages/amis-editor/src/plugin/Form/DiffEditor.tsx b/packages/amis-editor/src/plugin/Form/DiffEditor.tsx index 058fe111b..101606398 100644 --- a/packages/amis-editor/src/plugin/Form/DiffEditor.tsx +++ b/packages/amis-editor/src/plugin/Form/DiffEditor.tsx @@ -149,7 +149,8 @@ export class DiffEditorControlPlugin extends BasePlugin { }, getSchemaTpl('labelRemark'), getSchemaTpl('remark'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, getSchemaTpl('status', {isFormItem: true}), diff --git a/packages/amis-editor/src/plugin/Form/Form.tsx b/packages/amis-editor/src/plugin/Form/Form.tsx index 34a9c2662..0c85d4066 100644 --- a/packages/amis-editor/src/plugin/Form/Form.tsx +++ b/packages/amis-editor/src/plugin/Form/Form.tsx @@ -1,104 +1,43 @@ -import {registerEditorPlugin} from 'amis-editor-core'; +import cx from 'classnames'; +import {EditorManager, registerEditorPlugin} from 'amis-editor-core'; import { BasePlugin, ChangeEventContext, BaseEventContext, PluginEvent, RegionConfig, - ScaffoldForm -} from 'amis-editor-core'; -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {jsonToJsonSchema} from 'amis-editor-core'; -import {EditorNodeType} from 'amis-editor-core'; -import { + ScaffoldForm, RendererPluginAction, - RendererPluginEvent + RendererPluginEvent, + DSFeatureType, + DSBuilderManager, + generateNodeId, + defaultValue, + getSchemaTpl, + tipedLabel, + jsonToJsonSchema, + EditorNodeType } from 'amis-editor-core'; -import {setVariable} from 'amis-core'; import {getEventControlConfig} from '../../util'; +import {FormSchema} from 'amis/lib/Schema'; +import {clone, cloneDeep, flatten} from 'lodash'; -// 用于脚手架的常用表单控件 -const formItemOptions = [ - { - name: 'type', - label: '控件类型', - type: 'select', - required: true, - options: [ - { - label: '单行文本框', - value: 'input-text' - }, - { - label: '多行文本', - value: 'textarea' - }, - { - label: '分组', - value: 'group' - }, - { - 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' - } - ] - }, - { - name: 'label', - label: '显示名称', - type: 'input-text', - hiddenOn: 'data.type === "group"' - }, - { - name: 'name', - label: '提交字段名', - required: true, - type: 'input-text', - hiddenOn: 'data.type === "group"' - } +const Features: Array<{ + label: string; + value: DSFeatureType; +}> = [ + {label: '新增', value: 'Insert'}, + {label: '编辑', value: 'Edit'} ]; export class FormPlugin extends BasePlugin { + constructor(manager: EditorManager) { + super(manager); + this.dsBuilderMgr = new DSBuilderManager('form', 'api'); + } + + dsBuilderMgr: DSBuilderManager; + // 关联渲染器名字 rendererName = 'form'; $schema = '/schemas/FormSchema.json'; @@ -114,7 +53,8 @@ export class FormPlugin extends BasePlugin { tags = ['功能']; icon = 'fa fa-list-alt'; pluginIcon = 'form-plugin'; - scaffold = { + + scaffold: FormSchema = { type: 'form', title: '表单', body: [ @@ -123,9 +63,13 @@ export class FormPlugin extends BasePlugin { type: 'input-text', name: 'text' } + ], + actions: [ + {type: 'button', label: '提交', level: 'primary'}, + {type: 'button', label: '重置'} ] }; - previewSchema = { + previewSchema: FormSchema = { type: 'form', panelClassName: 'Panel--default text-left m-b-none', mode: 'horizontal', @@ -135,64 +79,133 @@ export class FormPlugin extends BasePlugin { name: 'a', type: 'input-text' } - ] - }; - - scaffoldForm: ScaffoldForm = { - title: '快速创建表单', - body: [ - getSchemaTpl('api', { - label: '提交地址' - }), + ], + actions: [ { - name: 'mode', - label: '文字与输入框展示模式', - type: 'button-group-select', - pipeIn: defaultValue('normal', false), - options: [ - { - label: '上下', - value: 'normal' - }, - { - label: '左右摆放', - value: 'horizontal' - }, - { - label: '内联', - value: 'inline' - } - ] - }, - { - label: '表单控件', - type: 'combo', - name: 'body', - multiple: true, - draggable: true, - multiLine: false, - items: [ - ...formItemOptions, - { - visibleOn: 'data.type === "group"', - type: 'combo', - name: 'body', - label: '分组内的控件', - multiple: true, - draggable: true, - multiLine: true, - items: [...formItemOptions] - } - ] + type: 'button', + label: '提交' } ] }; - // scaffoldForm: ScaffoldForm = { - // title: '配置表单信息', - // body: [getSchemaTpl('api')], - // canRebuild: true - // }; + get scaffoldForm(): ScaffoldForm { + return { + title: '表单创建向导', + mode: 'horizontal', + className: 'ae-Scaffold-Modal ae-formItemControl', + body: [ + { + type: 'radios', + name: 'feat', + label: '使用场景', + value: 'Insert', + options: Features + }, + this.dsBuilderMgr.getDSSwitch({ + onChange: (value: any, oldValue: any, model: any, form: any) => { + const data = form.data; + Object.keys(data).forEach(key => { + if (key.endsWith('Fields') || key.endsWith('api')) { + form.deleteValueByName(key); + } + }); + form.deleteValueByName('__fields'); + return value; + } + }), + ...flatten( + Features.map(feat => + this.dsBuilderMgr.collectFromBuilders((builder, builderName) => ({ + type: 'container', + className: 'form-item-gap', + visibleOn: `data.feat === '${feat.value}' && (!data.dsType || data.dsType === '${builderName}')`, + body: flatten([ + builder.makeSourceSettingForm({ + feat: feat.value, + label: builderName, + inScaffold: true + }), + builder.makeFieldsSettingForm({ + feat: feat.value, + inScaffold: true + }) + ]) + })) + ) + ), + { + name: 'operators', + label: '操作', + type: 'checkboxes', + value: ['submit'], + joinValues: false, + extractValue: false, + options: [ + { + label: '重置', + value: 'reset', + order: 0, + schema: { + level: 'default' + } + }, + { + label: '提交', + value: 'submit', + order: 1, + schema: { + level: 'primary' + } + } + ] + } + ], + pipeOut: (value: any) => { + const id = generateNodeId(); + const builder = this.dsBuilderMgr.resolveBuilderBySetting(value); + + let operators = clone(value.operators); + operators.sort((p: any, n: any) => p.order - n.order); + + let schema: FormSchema = { + ...cloneDeep(this.scaffold), + body: [], + id, + actions: operators.map((op: any) => { + return { + type: 'button', + label: op.label, + onEvent: { + click: { + actions: [ + { + actionType: op.value, + componentId: id + } + ] + } + }, + ...op.schema + }; + }) + }; + + builder.resolveSourceSchema({ + schema, + setting: value, + feat: value.feat + }); + + builder.resolveCreateSchema({ + schema, + setting: value, + feat: value.feat + }); + + return schema; + } + }; + } // 容器配置 regions: Array = [ @@ -205,9 +218,9 @@ export class FormPlugin extends BasePlugin { }, { - label: '按钮组', + label: '操作区', key: 'actions', - preferTag: '按钮' + preferTag: '操作按钮' } ]; @@ -380,419 +393,292 @@ export class FormPlugin extends BasePlugin { } ]; + panelJustify = true; panelBodyCreator = (context: BaseEventContext) => { - const isCRUDFilter: boolean = /\/crud\/filter\/form$/.test(context.path); + const builder = this.dsBuilderMgr.resolveBuilderBySchema( + context.schema, + 'api' + ); + /** 是否为CRUD的过滤器表单 */ + const isCRUDFilter: boolean = + /\/crud\/filter\/form$/.test(context.path) || + /\/crud2\/filter\/\d\/form$/.test(context.path); + /** 表单是否位于Dialog内 */ const isInDialog: boolean = /(?:\/|^)dialog\/.+$/.test(context.path); + /** 是否使用Panel包裹 */ + const isWrapped = 'this.wrapWithPanel !== false'; + const justifyLayout = (left: number = 2) => ({ + mode: 'horizontal', + horizontal: { + left, + justify: true + } + }); return [ getSchemaTpl('tabs', [ { - title: '常规', - body: [ - { - name: 'title', - type: 'input-text', - label: '标题', - visibleOn: `this.wrapWithPanel !== false` - }, - { - name: 'submitText', - type: 'input-text', - label: '提交按钮名称', - pipeIn: defaultValue('提交'), - visibleOn: `this.wrapWithPanel !== false && !this.actions && (!Array.isArray(this.body) || !this.body.some(function(item) {return !!~['submit','button','reset','button-group'].indexOf(item.type);}))`, - description: '当没有自定义按钮时有效。' - }, - - getSchemaTpl('switch', { - name: 'autoFocus', - label: '自动聚焦', - labelRemark: { - className: 'm-l-xs', - trigger: 'click', - rootClose: true, - content: '设置后将让表单的第一个可输入的表单项获得焦点', - placement: 'left' - } - }), - - getSchemaTpl('submitOnChange'), - - getSchemaTpl('switch', { - label: '提交完后重置表单', - name: 'resetAfterSubmit', - labelRemark: { - className: 'm-l-xs', - trigger: 'click', - rootClose: true, - content: '即表单提交完后,让所有表单项的值还原成初始值', - placement: 'left' - } - }), - + title: '属性', + body: getSchemaTpl('collapseGroup', [ isCRUDFilter ? null - : getSchemaTpl('switch', { - label: '初始化后提交一次', - name: 'submitOnInit', - labelRemark: { - className: 'm-l-xs', - trigger: 'click', - rootClose: true, - content: '开启后,表单初始完成便会触发一次提交。', - placement: 'left' - } + : { + title: '数据源', + body: builder.makeSourceSettingForm({ + name: 'api', + feat: 'Edit' + }) + }, + { + title: '基本', + body: [ + { + name: 'title', + type: 'input-text', + label: '标题', + visibleOn: isWrapped + }, + getSchemaTpl('switch', { + name: 'autoFocus', + label: tipedLabel( + '自动聚焦', + '设置后将让表单的第一个可输入的表单项获得焦点' + ) }), - - isInDialog - ? getSchemaTpl('switch', { - label: '提交后是否关闭对话框', - name: 'closeDialogOnSubmit', - pipeIn: (value: any) => value !== false + { + type: 'ae-switch-more', + mode: 'normal', + name: 'persistData', + label: tipedLabel( + '本地缓存', + '开启后,表单的数据会缓存在浏览器中,切换页面或关闭弹框不会清空当前表单内的数据' + ), + hiddenOnDefault: true, + formType: 'extend', + form: { + body: [ + getSchemaTpl('switch', { + name: 'clearPersistDataAfterSubmit', + label: tipedLabel( + '提交成功后清空缓存', + '开启本地缓存并开启本配置项后,表单提交成功后,会自动清除浏览器中当前表单的缓存数据' + ), + pipeIn: defaultValue(false), + visibleOn: 'data.persistData' + }) + ] + } + }, + getSchemaTpl('switch', { + name: 'canAccessSuperData', + label: tipedLabel( + '自动填充数据域同名变量', + '默认表单是可以获取到完整数据链中的数据的,如果想使表单的数据域独立,请关闭此配置' + ), + pipeIn: defaultValue(true) }) - : null, - - isCRUDFilter - ? null - : { - label: '提交给其他组件', - name: 'target', - type: 'input-text', - description: - '可以通过设置此属性,把当前表单的值提交给目标组件,而不是自己来通过接口保存,请填写目标组件的 name 属性,多个组件请用逗号隔开。当 targetwindow 时,则把表单数据附属到地址栏。' - }, - - getSchemaTpl('reload', { - test: !isCRUDFilter - }), - - isCRUDFilter - ? null - : { - label: '跳转', - name: 'redirect', - type: 'input-text', - description: '当设置此值后,表单提交完后跳转到目标地址。' - }, - - getSchemaTpl('switch', { - name: 'canAccessSuperData', - label: '是否自动填充父级同名变量', - pipeIn: defaultValue(true) - }), - - getSchemaTpl('switch', { - name: 'persistData', - label: '是否开启本地缓存', - pipeIn: defaultValue(false), - labelRemark: { - className: 'm-l-xs', - trigger: 'click', - rootClose: true, - content: - '开启后,表单的数据会缓存在浏览器中,切换页面或关闭弹框不会清空当前表单内的数据', - placement: 'left' - } - }), - - getSchemaTpl('switch', { - name: 'clearPersistDataAfterSubmit', - label: '提交成功后清空本地缓存', - pipeIn: defaultValue(false), - visibleOn: 'data.persistData', - labelRemark: { - className: 'm-l-xs', - trigger: 'click', - rootClose: true, - content: - '开启本地缓存并开启本配置项后,表单提交成功后,会自动清除浏览器中当前表单的缓存数据', - placement: 'left' - } - }), - + ] + }, { - name: 'rules', - label: '表单组合校验', - type: 'combo', - multiple: true, - multiLine: true, - items: [ + title: '提交设置', + body: [ { - name: 'rule', - label: '校验规则', - type: 'input-text' + name: 'submitText', + type: 'input-text', + label: tipedLabel( + '提交按钮名称', + '如果底部按钮不是自定义按钮时,可以通过该配置可以快速修改按钮名称,如果设置成空,则可以把默认按钮去掉。' + ), + pipeIn: defaultValue('提交'), + visibleOn: `${isWrapped} && !this.actions && (!Array.isArray(this.body) || !this.body.some(function(item) {return !!~['submit','button','reset','button-group'].indexOf(item.type);}))`, + ...justifyLayout(4) }, + getSchemaTpl('switch', { + name: 'submitOnChange', + label: tipedLabel( + '修改即提交', + '设置后,表单中每次有修改都会触发提交' + ) + }), + getSchemaTpl('switch', { + name: 'resetAfterSubmit', + label: tipedLabel( + '提交后重置表单', + '表单提交后,让所有表单项的值还原成初始值' + ) + }), + getSchemaTpl('switch', { + name: 'preventEnterSubmit', + label: tipedLabel( + '阻止回车提交', + '默认按回车键触发表单提交,开启后将阻止这一行为' + ) + }), + // isCRUDFilter + // ? null + // : getSchemaTpl('switch', { + // name: 'submitOnInit', + // label: tipedLabel( + // '初始化后提交一次', + // '开启后,表单初始完成便会触发一次提交' + // ) + // }), + isInDialog + ? getSchemaTpl('switch', { + label: '提交后关闭对话框', + name: 'closeDialogOnSubmit', + pipeIn: (value: any) => value !== false + }) + : null + // isCRUDFilter + // ? null + // : { + // label: tipedLabel( + // '提交其他组件', + // '可以通过设置此属性,把当前表单的值提交给目标组件,而不是自己来通过接口保存,请填写目标组件的 name 属性,多个组件请用逗号隔开。当 targetwindow 时,则把表单数据附属到地址栏。' + // ), + // name: 'target', + // type: 'input-text', + // placeholder: '请输入组件name', + // ...justifyLayout(4) + // }, + // getSchemaTpl('reload', { + // test: !isCRUDFilter + // }), + // isCRUDFilter + // ? null + // : { + // type: 'ae-switch-more', + // mode: 'normal', + // label: tipedLabel( + // '提交后跳转', + // '当设置此值后,表单提交完后跳转到目标地址' + // ), + // formType: 'extend', + // form: { + // mode: 'horizontal', + // horizontal: { + // justify: true, + // left: 4 + // }, + // body: [ + // { + // label: '跳转地址', + // name: 'redirect', + // type: 'input-text', + // placeholder: '请输入目标地址' + // } + // ] + // } + // } + ] + }, + { + title: '组合校验', + body: [ { - name: 'message', - label: '报错提示', - type: 'input-text' + name: 'rules', + label: false, + type: 'combo', + multiple: true, + multiLine: true, + subFormMode: 'horizontal', + placeholder: '', + addBtn: { + label: '添加校验规则', + block: true, + icon: 'fa fa-plus', + className: cx('ae-Button--enhance') + }, + items: [ + { + type: 'ae-formulaControl', + name: 'rule', + label: '校验规则', + ...justifyLayout(4) + }, + { + name: 'message', + label: '报错提示', + type: 'input-text', + ...justifyLayout(4) + } + ] } ] + }, + /** 状态 */ + getSchemaTpl('status', {isFormItem: false, disabled: true}), + { + title: '高级', + body: [ + getSchemaTpl('switch', { + name: 'debug', + label: tipedLabel('开启调试', '在表单顶部显示当前表单的数据') + }) + ] } - ] + ]) }, - - isCRUDFilter - ? null - : { - title: '接口', - body: [ - getSchemaTpl('api', { - label: '保存接口', - description: '用来保存表单数据', - sampleBuilder: (schema: any) => `{ - "status": 0, - "msg": "", - - // 可以不返回,如果返回了数据将被 merge 进来。 - data: {} - }` - // test: !this.isCRUDFilter - }), - - getSchemaTpl('switch', { - name: 'asyncApi', - label: '采用异步方式?', - visibleOn: 'data.api', - labelRemark: { - trigger: 'click', - rootClose: true, - title: '什么是异步方式?', - content: - '异步方式主要用来解决请求超时问题,启用异步方式后,程序会在请求完后,定时轮询请求额外的接口用来咨询操作是否完成。所以接口可以快速的返回,而不需要等待流程真正完成。', - placement: 'left' - }, - pipeIn: (value: any) => value != null, - pipeOut: (value: any) => (value ? '' : undefined) - }), - - getSchemaTpl('api', { - name: 'asyncApi', - label: '异步检测接口', - visibleOn: 'data.asyncApi != null', - description: - '设置此属性后,表单提交发送保存接口后,还会继续轮训请求该接口,直到返回 finished 属性为 true 才 结束' - }), - - { - type: 'divider' - }, - - getSchemaTpl('api', { - name: 'initApi', - label: '初始化接口', - description: '用来初始化表单数据', - sampleBuilder: (schema: any) => { - const data = {}; - - if (Array.isArray(schema.body)) { - schema.body.forEach((control: any) => { - if ( - control.name && - !~['combo', 'input-array', 'form'].indexOf( - control.type - ) - ) { - setVariable(data, control.name, 'sample'); - } - }); - } - - return JSON.stringify( - { - status: 0, - msg: '', - data: data - }, - null, - 2 - ); - } - }), - - getSchemaTpl('switch', { - label: '开启定时刷新', - name: 'interval', - visibleOn: 'data.initApi', - pipeIn: (value: any) => !!value, - pipeOut: (value: any) => (value ? 3000 : undefined) - }), - - { - name: 'interval', - type: 'input-number', - visibleOn: 'data.interval', - step: 500, - className: 'm-t-n-sm', - description: '设置后将自动定时刷新,单位 ms' - }, - - getSchemaTpl('switch', { - name: 'silentPolling', - label: '静默刷新', - visibleOn: '!!data.interval', - description: '设置自动定时刷新时是否显示loading' - }), - - { - name: 'stopAutoRefreshWhen', - label: '停止定时刷新检测表达式', - type: 'input-text', - visibleOn: '!!data.interval', - description: - '定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则不刷新了。' - }, - - getSchemaTpl('switch', { - label: '采用异步方式?', - name: 'initAsyncApi', - visibleOn: 'data.initApi', - remark: { - trigger: 'click', - rootClose: true, - title: '什么是异步方式?', - content: - '异步方式主要用来解决请求超时问题,启用异步方式后,程序会在请求完后,定时轮询请求额外的接口用来咨询操作是否完成。所以接口可以快速的返回,而不需要等待流程真正完成。', - placement: 'left' - }, - pipeIn: (value: any) => value != null, - pipeOut: (value: any) => (value ? '' : undefined) - }), - - getSchemaTpl('api', { - name: 'initAsyncApi', - label: '异步检测接口', - visibleOn: 'data.initAsyncApi != null', - description: - '设置此属性后,表单请求 initApi 后,还会继续轮训请求该接口,直到返回 finished 属性为 true 才 结束' - }), - - { - type: 'divider' - }, - - isCRUDFilter - ? { - name: 'messages', - pipeIn: defaultValue({ - fetchFailed: '初始化失败' - }), - label: '默认消息信息', - type: 'combo', - multiLine: true, - description: - '可以不设置,接口返回的 msg 字段,优先级更高', - items: [ - { - label: '获取成功提示', - name: 'fetchSuccess', - type: 'input-text' - }, - { - label: '获取失败提示', - name: 'fetchFailed', - type: 'input-text' - } - ] - } - : { - name: 'messages', - pipeIn: defaultValue({ - fetchFailed: '初始化失败', - saveSuccess: '保存成功', - saveFailed: '保存失败' - }), - label: '默认消息提示', - type: 'combo', - multiLine: true, - description: - '可以不设置,接口返回的 msg 字段,优先级更高', - items: [ - { - label: '获取成功提示', - name: 'fetchSuccess', - type: 'input-text' - }, - { - label: '获取失败提示', - name: 'fetchFailed', - type: 'input-text' - }, - { - label: '保存成功提示', - name: 'saveSuccess', - type: 'input-text' - }, - { - label: '保存失败提示', - name: 'saveFailed', - type: 'input-text' - }, - { - label: '验证失败提示', - name: 'validateFailed', - type: 'input-text' - } - ] - } - ] - }, - { title: '外观', - body: [ - getSchemaTpl('switch', { - name: 'wrapWithPanel', - label: '用 Panel 包裹', - pipeIn: defaultValue(true), - labelRemark: { - className: 'm-l-xs', - trigger: 'click', - rootClose: true, - content: '关闭后,表单只会展示表单项,标题和操作栏将不会显示。', - placement: 'left' - } - }), - + body: getSchemaTpl('collapseGroup', [ { - name: 'mode', - label: '展示模式', - type: 'button-group-select', - size: 'sm', - // mode: 'inline', - // className: 'block', - pipeIn: defaultValue('normal', false), - options: [ - { - label: '默认', - value: 'normal' - }, - { - label: '左右摆放', - value: 'horizontal' - }, - { - label: '内联', - value: 'inline' - } + title: '布局', + body: [ + getSchemaTpl('formItemMode', {isForm: true}), + getSchemaTpl('horizontal') ] }, - - getSchemaTpl('horizontal', { - visibleOn: 'this.mode == "horizontal"' - }), - - getSchemaTpl('className'), - - getSchemaTpl('className', { - name: 'panelClassName', - visibleOn: 'this.wrapWithPanel !== false', - label: 'Panel 的 CSS 类名', - description: '可以设置 Panel--info 之类的' + { + title: '其他', + body: [ + getSchemaTpl('switch', { + name: 'wrapWithPanel', + label: tipedLabel( + 'Panel包裹', + '关闭后,表单只会展示表单项,标题和操作栏将不会显示。' + ), + pipeIn: defaultValue(true) + }), + getSchemaTpl('switch', { + name: 'affixFooter', + label: tipedLabel( + '吸附操作栏', + '开启后,滚动表单内容区时使底部操作区悬浮吸附' + ), + visibleOn: isWrapped + }) + ] + }, + /** */ + getSchemaTpl('style:classNames', { + isFormItem: false, + schema: [ + getSchemaTpl('className', { + name: 'panelClassName', + label: 'Panel', + visibleOn: isWrapped + }), + getSchemaTpl('className', { + name: 'headerClassName', + label: '标题区', + visibleOn: isWrapped + }), + getSchemaTpl('className', { + name: 'bodyClassName', + label: '内容区', + visibleOn: isWrapped + }), + getSchemaTpl('className', { + name: 'actionsClassName', + label: '操作区', + visibleOn: isWrapped + }) + ] }) - ] + ]) }, - { title: '事件', className: 'p-none', @@ -802,24 +688,6 @@ export class FormPlugin extends BasePlugin { ...getEventControlConfig(this.manager, context) }) ] - }, - - { - title: '其他', - body: [ - getSchemaTpl('ref'), - getSchemaTpl('name', { - test: !isCRUDFilter - }), - getSchemaTpl('switch', { - name: 'debug', - label: '开启调试', - labelRemark: '显示当前表单的数据在表单顶部' - }), - - getSchemaTpl('disabled'), - getSchemaTpl('visible') - ] } ]) ]; @@ -895,6 +763,39 @@ export class FormPlugin extends BasePlugin { scope.addSchema(jsonschema); } } + + async getAvailableContextFields( + scopeNode: EditorNodeType, + node: EditorNodeType, + region?: EditorNodeType + ) { + // 只有表单项组件可以使用表单组件的数据域 + if (node.info.renderer.isFormItem) { + if ( + scopeNode.parent?.type === 'crud2' && + scopeNode.schemaPath.startsWith('body/0/filter/') + ) { + return scopeNode.parent.info.plugin.getAvailableContextFields?.( + scopeNode.parent, + node, + region + ); + } + + // 先从数据源获取可用字段 + const builder = this.dsBuilderMgr.resolveBuilderBySchema( + scopeNode.schema, + 'api' + ); + if (builder && scopeNode.schema.api) { + return builder.getAvailableContextFileds({ + schema: scopeNode.schema, + sourceKey: 'api', + feat: 'Insert' + }); + } + } + } } registerEditorPlugin(FormPlugin); diff --git a/packages/amis-editor/src/plugin/Form/InputCity.tsx b/packages/amis-editor/src/plugin/Form/InputCity.tsx index 9b9eb36a2..74c4895b6 100644 --- a/packages/amis-editor/src/plugin/Form/InputCity.tsx +++ b/packages/amis-editor/src/plugin/Form/InputCity.tsx @@ -9,10 +9,7 @@ import { } from 'amis-editor-core'; import {formItemControl} from '../../component/BaseControl'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class CityControlPlugin extends BasePlugin { // 关联渲染器名字 diff --git a/packages/amis-editor/src/plugin/Form/InputColor.tsx b/packages/amis-editor/src/plugin/Form/InputColor.tsx index 455c40caa..4ed56e483 100644 --- a/packages/amis-editor/src/plugin/Form/InputColor.tsx +++ b/packages/amis-editor/src/plugin/Form/InputColor.tsx @@ -1,9 +1,8 @@ -import {registerEditorPlugin} from 'amis-editor-core'; +import {registerEditorPlugin, tipedLabel} from 'amis-editor-core'; import {getSchemaTpl} from 'amis-editor-core'; import {ValidatorTag} from '../../validator'; import {BasePlugin, BaseEventContext} from 'amis-editor-core'; import {getEventControlConfig} from '../../util'; -import {tipedLabel} from '../../component/BaseControl'; import tinyColor from 'tinycolor2'; function convertColor(value: string[], format: string): string[]; @@ -171,124 +170,133 @@ export class ColorControlPlugin extends BasePlugin { return getSchemaTpl('tabs', [ { title: '属性', - body: getSchemaTpl('collapseGroup', [ - { - title: '基本', - body: [ - getSchemaTpl('formItemName', { - required: true - }), - getSchemaTpl('label'), - { - type: 'select', - label: '值格式', - name: 'format', - value: 'hex', - options: formatOptions, - onChange: ( - format: any, - oldFormat: any, - model: any, - form: any - ) => { - const {value, presetColors} = form.data; - if (value) { - form.setValueByName('value', convertColor(value, format)); + body: getSchemaTpl( + 'collapseGroup', + [ + { + title: '基本', + body: [ + getSchemaTpl('formItemName', { + required: true + }), + getSchemaTpl('label'), + { + type: 'select', + label: '值格式', + name: 'format', + value: 'hex', + options: formatOptions, + onChange: ( + format: any, + oldFormat: any, + model: any, + form: any + ) => { + const {value, presetColors} = form.data; + if (value) { + form.setValueByName('value', convertColor(value, format)); + } + if (Array.isArray(presetColors)) { + form.setValueByName( + 'presetColors', + convertColor(presetColors, format) + ); + } } - if (Array.isArray(presetColors)) { - form.setValueByName( - 'presetColors', - convertColor(presetColors, format) - ); - } - } - }, - // todo: 待优化 - [ - ...formatOptions.map(({value}) => - this.getConditionalColorPanel(value) - ) - ], - // { - // label: '默认值', - // name: 'value', - // type: 'input-color', - // format: '${format}' - // }, - getSchemaTpl('clearable'), - getSchemaTpl('labelRemark'), - getSchemaTpl('remark'), - getSchemaTpl('placeholder'), - getSchemaTpl('description') - ] - }, - { - title: '拾色器', - body: [ - getSchemaTpl('switch', { - label: tipedLabel( - '隐藏调色盘', - '开启时,禁止手动输入颜色,只能从备选颜色中选择' - ), - name: 'allowCustomColor', - disabledOn: - 'Array.isArray(presetColors) && presetColors.length === 0', - pipeIn: (value: any) => - typeof value === 'undefined' ? false : !value, - pipeOut: (value: boolean) => !value - }), - getSchemaTpl('switch', { - label: tipedLabel('备选色', '拾色器底部的备选颜色'), - name: 'presetColors', - onText: '自定义', - offText: '默认', - pipeIn: (value: any) => - typeof value === 'undefined' ? false : true, - pipeOut: ( - value: any, - originValue: any, - {format = 'hex'}: any - ) => { - return !value ? undefined : presetColorsByFormat[format]; }, - onChange: ( - colors: any, - oldValue: any, - model: any, - form: any - ) => { - if (Array.isArray(colors) && colors.length === 0) { - form.setValueByName('allowCustomColor', true); + // todo: 待优化 + [ + ...formatOptions.map(({value}) => + this.getConditionalColorPanel(value) + ) + ], + // { + // label: '默认值', + // name: 'value', + // type: 'input-color', + // format: '${format}' + // }, + getSchemaTpl('clearable'), + getSchemaTpl('labelRemark'), + getSchemaTpl('remark'), + getSchemaTpl('placeholder'), + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') + ] + }, + { + title: '拾色器', + body: [ + getSchemaTpl('switch', { + label: tipedLabel( + '隐藏调色盘', + '开启时,禁止手动输入颜色,只能从备选颜色中选择' + ), + name: 'allowCustomColor', + disabledOn: + 'Array.isArray(presetColors) && presetColors.length === 0', + pipeIn: (value: any) => + typeof value === 'undefined' ? false : !value, + pipeOut: (value: boolean) => !value + }), + getSchemaTpl('switch', { + label: tipedLabel('备选色', '拾色器底部的备选颜色'), + name: 'presetColors', + onText: '自定义', + offText: '默认', + pipeIn: (value: any) => + typeof value === 'undefined' ? false : true, + pipeOut: ( + value: any, + originValue: any, + {format = 'hex'}: any + ) => { + return !value ? undefined : presetColorsByFormat[format]; + }, + onChange: ( + colors: any, + oldValue: any, + model: any, + form: any + ) => { + if (Array.isArray(colors) && colors.length === 0) { + form.setValueByName('allowCustomColor', true); + } } - } - }), - ...formatOptions.map(({value}) => - this.getConditionalColorComb(value) - ) - ] - }, - getSchemaTpl('status', { - isFormItem: true - }), - getSchemaTpl('validation', { - tag: ValidatorTag.MultiSelect - }) - ], {...context?.schema, configTitle: 'props'}) + }), + ...formatOptions.map(({value}) => + this.getConditionalColorComb(value) + ) + ] + }, + getSchemaTpl('status', { + isFormItem: true + }), + getSchemaTpl('validation', { + tag: ValidatorTag.MultiSelect + }) + ], + {...context?.schema, configTitle: 'props'} + ) }, { title: '外观', - body: getSchemaTpl('collapseGroup', [ - getSchemaTpl('style:formItem', {renderer}), - getSchemaTpl('style:classNames', { - schema: [ - getSchemaTpl('className', { - label: '描述', - name: 'descriptionClassName', - visibleOn: 'this.description' - }) - ] - }) - ], {...context?.schema, configTitle: 'style'}) + body: getSchemaTpl( + 'collapseGroup', + [ + getSchemaTpl('style:formItem', {renderer}), + getSchemaTpl('style:classNames', { + schema: [ + getSchemaTpl('className', { + label: '描述', + name: 'descriptionClassName', + visibleOn: 'this.description' + }) + ] + }) + ], + {...context?.schema, configTitle: 'style'} + ) }, { title: '事件', diff --git a/packages/amis-editor/src/plugin/Form/InputDate.tsx b/packages/amis-editor/src/plugin/Form/InputDate.tsx index 67c14b744..64a7defc8 100644 --- a/packages/amis-editor/src/plugin/Form/InputDate.tsx +++ b/packages/amis-editor/src/plugin/Form/InputDate.tsx @@ -1,14 +1,10 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {BasePlugin, BaseEventContext} from 'amis-editor-core'; +import {BasePlugin, BaseEventContext, tipedLabel} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; const formatX = [ { @@ -237,157 +233,166 @@ export class DateControlPlugin extends BasePlugin { return getSchemaTpl('tabs', [ { title: '属性', - body: getSchemaTpl('collapseGroup', [ - { - title: '基本', - body: [ - getSchemaTpl('formItemName', { - required: true - }), - getSchemaTpl('label'), - getSchemaTpl('selectDateType', { - value: this.scaffold.type, - onChange: ( - value: string, - oldValue: any, - model: any, - form: any - ) => { - let type: string = value.split('-')[1]; + body: getSchemaTpl( + 'collapseGroup', + [ + { + title: '基本', + body: [ + getSchemaTpl('formItemName', { + required: true + }), + getSchemaTpl('label'), + getSchemaTpl('selectDateType', { + value: this.scaffold.type, + onChange: ( + value: string, + oldValue: any, + model: any, + form: any + ) => { + let type: string = value.split('-')[1]; - form.setValues({ - inputFormat: DateType[type]?.format, - placeholder: DateType[type]?.placeholder, - format: type === 'time' ? 'HH:mm' : 'X', - minDate: '', - maxDate: '', - value: '' - }); - } - }), - { - type: 'input-text', - name: 'format', - label: tipedLabel( - '值格式', - '提交数据前将根据设定格式化数据,请参考 moment 中的格式用法。' - ), - pipeIn: defaultValue('X') - }, - { - type: 'input-text', - name: 'inputFormat', - label: tipedLabel( - '显示格式', - '请参考 moment 中的格式用法。' - ), - pipeIn: defaultValue('YYYY-MM-DD'), - clearable: true, - onChange: ( - value: string, - oldValue: any, - model: any, - form: any - ) => { - const type = form.data.type.split('-')[1]; - model.setOptions(DateType[type].formatOptions); - // 时间日期类组件 input-time 需要更加关注 timeFormat 和 inputFormat 属性区别 - // inputFormat 表示输入框内的显示格式; timeFormat表示选择下拉弹窗中展示"HH、mm、ss"的组合 - if (type === 'time') { - const timeFormatObj = DateType[type].formatOptions.find( - item => item.value === value - ); - const timeFormat = timeFormatObj - ? (timeFormatObj as any).timeFormat - : 'HH:mm:ss'; form.setValues({ - timeFormat: timeFormat + inputFormat: DateType[type]?.format, + placeholder: DateType[type]?.placeholder, + format: type === 'time' ? 'HH:mm' : 'X', + minDate: '', + maxDate: '', + value: '' }); } + }), + { + type: 'input-text', + name: 'format', + label: tipedLabel( + '值格式', + '提交数据前将根据设定格式化数据,请参考 moment 中的格式用法。' + ), + pipeIn: defaultValue('X') }, - options: - DateType[this.scaffold.type.split('-')[1]].formatOptions - }, - getSchemaTpl('utc'), - getSchemaTpl('clearable', { - pipeIn: defaultValue(true) - }), - getSchemaTpl('valueFormula', { - rendererSchema: context?.schema, - label: tipedLabel( - '默认值', - '支持 now、+1day、-2weeks、+1hours、+2years等这种相对值用法' - ) - }), - getSchemaTpl('valueFormula', { - name: 'minDate', - rendererSchema: { - ...context?.schema, - value: context?.schema.minDate + { + type: 'input-text', + name: 'inputFormat', + label: tipedLabel( + '显示格式', + '请参考 moment 中的格式用法。' + ), + pipeIn: defaultValue('YYYY-MM-DD'), + clearable: true, + onChange: ( + value: string, + oldValue: any, + model: any, + form: any + ) => { + const type = form.data.type.split('-')[1]; + model.setOptions(DateType[type].formatOptions); + // 时间日期类组件 input-time 需要更加关注 timeFormat 和 inputFormat 属性区别 + // inputFormat 表示输入框内的显示格式; timeFormat表示选择下拉弹窗中展示"HH、mm、ss"的组合 + if (type === 'time') { + const timeFormatObj = DateType[type].formatOptions.find( + item => item.value === value + ); + const timeFormat = timeFormatObj + ? (timeFormatObj as any).timeFormat + : 'HH:mm:ss'; + form.setValues({ + timeFormat: timeFormat + }); + } + }, + options: + DateType[this.scaffold.type.split('-')[1]].formatOptions }, - needDeleteProps: ['minDate'], // 避免自我限制 - label: tipedLabel('最小值', tipedLabelText) - }), - getSchemaTpl('valueFormula', { - name: 'maxDate', - rendererSchema: { - ...context?.schema, - value: context?.schema.maxDate - }, - needDeleteProps: ['maxDate'], // 避免自我限制 - label: tipedLabel('最大值', tipedLabelText) - }), - getSchemaTpl('placeholder', { - pipeIn: defaultValue('请选择日期') - }), - // getSchemaTpl('remark'), - // getSchemaTpl('labelRemark'), - getSchemaTpl('description') - ] - }, - getSchemaTpl('status', {isFormItem: true}), - getSchemaTpl('validation', { - tag: ValidatorTag.Date - }) - ], {...context?.schema, configTitle: 'props'}) + getSchemaTpl('utc'), + getSchemaTpl('clearable', { + pipeIn: defaultValue(true) + }), + getSchemaTpl('valueFormula', { + rendererSchema: context?.schema, + label: tipedLabel( + '默认值', + '支持 now、+1day、-2weeks、+1hours、+2years等这种相对值用法' + ) + }), + getSchemaTpl('valueFormula', { + name: 'minDate', + rendererSchema: { + ...context?.schema, + value: context?.schema.minDate + }, + needDeleteProps: ['minDate'], // 避免自我限制 + label: tipedLabel('最小值', tipedLabelText) + }), + getSchemaTpl('valueFormula', { + name: 'maxDate', + rendererSchema: { + ...context?.schema, + value: context?.schema.maxDate + }, + needDeleteProps: ['maxDate'], // 避免自我限制 + label: tipedLabel('最大值', tipedLabelText) + }), + getSchemaTpl('placeholder', { + pipeIn: defaultValue('请选择日期') + }), + // getSchemaTpl('remark'), + // getSchemaTpl('labelRemark'), + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') + ] + }, + getSchemaTpl('status', {isFormItem: true}), + getSchemaTpl('validation', { + tag: ValidatorTag.Date + }) + ], + {...context?.schema, configTitle: 'props'} + ) }, { title: '外观', - body: getSchemaTpl('collapseGroup', [ - getSchemaTpl('style:formItem', renderer), - getSchemaTpl('style:classNames', [ - getSchemaTpl('className', { - label: '描述', - name: 'descriptionClassName', - visibleOn: 'this.description' - }), - getSchemaTpl('className', { - name: 'addOn.className', - label: 'AddOn', - visibleOn: 'this.addOn && this.addOn.type === "text"' - }) - ]), - getSchemaTpl('style:others', [ - { - name: 'embed', - type: 'button-group-select', - size: 'md', - label: '模式', - mode: 'row', - value: false, - options: [ - { - label: '浮层', - value: false - }, - { - label: '内嵌', - value: true - } - ] - } - ]) - ], {...context?.schema, configTitle: 'style'}) + body: getSchemaTpl( + 'collapseGroup', + [ + getSchemaTpl('style:formItem', renderer), + getSchemaTpl('style:classNames', [ + getSchemaTpl('className', { + label: '描述', + name: 'descriptionClassName', + visibleOn: 'this.description' + }), + getSchemaTpl('className', { + name: 'addOn.className', + label: 'AddOn', + visibleOn: 'this.addOn && this.addOn.type === "text"' + }) + ]), + getSchemaTpl('style:others', [ + { + name: 'embed', + type: 'button-group-select', + size: 'md', + label: '模式', + mode: 'row', + value: false, + options: [ + { + label: '浮层', + value: false + }, + { + label: '内嵌', + value: true + } + ] + } + ]) + ], + {...context?.schema, configTitle: 'style'} + ) }, { title: '事件', diff --git a/packages/amis-editor/src/plugin/Form/InputDateRange.tsx b/packages/amis-editor/src/plugin/Form/InputDateRange.tsx index 704e86d3a..56c593f4a 100644 --- a/packages/amis-editor/src/plugin/Form/InputDateRange.tsx +++ b/packages/amis-editor/src/plugin/Form/InputDateRange.tsx @@ -1,14 +1,10 @@ -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; +import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, BaseEventContext} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; const DateType: { [key: string]: { @@ -184,72 +180,74 @@ export class DateRangeControlPlugin extends BasePlugin { return getSchemaTpl('tabs', [ { title: '属性', - body: getSchemaTpl('collapseGroup', [ - { - title: '基本', - body: [ - getSchemaTpl('formItemName', { - required: true - }), - getSchemaTpl('label'), - getSchemaTpl('selectDateRangeType', { - value: this.scaffold.type, - onChange: ( - value: string, - oldValue: any, - model: any, - form: any - ) => { - const type: string = value.split('-')[1]; - form.setValues({ - inputFormat: DateType[type]?.format, - placeholder: DateType[type]?.placeholder, - format: type === 'time' ? 'HH:mm' : 'X', - minDate: '', - maxDate: '', - value: '', - ranges: DateType[type]?.ranges - }); - } - }), - { - type: 'input-text', - name: 'format', - label: tipedLabel( - '值格式', - '提交数据前将根据设定格式化数据,请参考 moment 中的格式用法。' - ), - pipeIn: defaultValue('X') - }, - { - type: 'input-text', - name: 'inputFormat', - label: tipedLabel( - '显示格式', - '请参考 moment 中的格式用法。' - ), - pipeIn: defaultValue('YYYY-MM-DD'), - clearable: true - // onChange: ( - // value: string, - // oldValue: any, - // model: any, - // form: any - // ) => { - // model.setOptions( - // DateType[form.data.type.split('-')[1]].formatOptions - // ); - // }, - // options: - // DateType[this.scaffold.type.split('-')[1]].formatOptions - }, - getSchemaTpl('utc'), - getSchemaTpl('clearable', { - pipeIn: defaultValue(true) - }), + body: getSchemaTpl( + 'collapseGroup', + [ + { + title: '基本', + body: [ + getSchemaTpl('formItemName', { + required: true + }), + getSchemaTpl('label'), + getSchemaTpl('selectDateRangeType', { + value: this.scaffold.type, + onChange: ( + value: string, + oldValue: any, + model: any, + form: any + ) => { + const type: string = value.split('-')[1]; + form.setValues({ + inputFormat: DateType[type]?.format, + placeholder: DateType[type]?.placeholder, + format: type === 'time' ? 'HH:mm' : 'X', + minDate: '', + maxDate: '', + value: '', + ranges: DateType[type]?.ranges + }); + } + }), + { + type: 'input-text', + name: 'format', + label: tipedLabel( + '值格式', + '提交数据前将根据设定格式化数据,请参考 moment 中的格式用法。' + ), + pipeIn: defaultValue('X') + }, + { + type: 'input-text', + name: 'inputFormat', + label: tipedLabel( + '显示格式', + '请参考 moment 中的格式用法。' + ), + pipeIn: defaultValue('YYYY-MM-DD'), + clearable: true + // onChange: ( + // value: string, + // oldValue: any, + // model: any, + // form: any + // ) => { + // model.setOptions( + // DateType[form.data.type.split('-')[1]].formatOptions + // ); + // }, + // options: + // DateType[this.scaffold.type.split('-')[1]].formatOptions + }, + getSchemaTpl('utc'), + getSchemaTpl('clearable', { + pipeIn: defaultValue(true) + }), - getSchemaTpl('valueFormula', { - /* 备注: 待 amis 日期组件优化 + getSchemaTpl('valueFormula', { + /* 备注: 待 amis 日期组件优化 rendererSchema: { ...context?.schema, size: 'full', // 备注:目前样式还有问题,需要在amis端进行优化 @@ -257,120 +255,127 @@ export class DateRangeControlPlugin extends BasePlugin { }, mode: 'vertical', */ - rendererSchema: { - type: 'input-date' - }, - label: tipedLabel( - '默认值', - '支持 now、+1day、-2weeks、+1hours、+2years等这种相对值用法' - ) - }), - getSchemaTpl('valueFormula', { - name: 'minDate', - rendererSchema: { - ...context?.schema, - value: context?.schema.minDate, - type: 'input-date' - }, - needDeleteProps: ['minDate'], // 避免自我限制 - label: tipedLabel('最小值', tipedLabelText) - }), - getSchemaTpl('valueFormula', { - name: 'maxDate', - rendererSchema: { - ...context?.schema, - value: context?.schema.maxDate, - type: 'input-date' - }, - needDeleteProps: ['maxDate'], // 避免自我限制 - label: tipedLabel('最大值', tipedLabelText) - }), + rendererSchema: { + type: 'input-date' + }, + label: tipedLabel( + '默认值', + '支持 now、+1day、-2weeks、+1hours、+2years等这种相对值用法' + ) + }), + getSchemaTpl('valueFormula', { + name: 'minDate', + rendererSchema: { + ...context?.schema, + value: context?.schema.minDate, + type: 'input-date' + }, + needDeleteProps: ['minDate'], // 避免自我限制 + label: tipedLabel('最小值', tipedLabelText) + }), + getSchemaTpl('valueFormula', { + name: 'maxDate', + rendererSchema: { + ...context?.schema, + value: context?.schema.maxDate, + type: 'input-date' + }, + needDeleteProps: ['maxDate'], // 避免自我限制 + label: tipedLabel('最大值', tipedLabelText) + }), - getSchemaTpl('formulaControl', { - name: 'minDuration', - label: tipedLabel('最小跨度', '例如 2days'), - placeholder: '请输入最小跨度', - inputClassName: 'is-inline' - }), - getSchemaTpl('formulaControl', { - name: 'maxDuration', - label: tipedLabel('最大跨度', '例如 1year'), - placeholder: '请输入最大跨度', - inputClassName: 'is-inline' - }), - getSchemaTpl('dateShortCutControl', { - mode: 'normal', - dropDownOption: { - 'yesterday': '昨天', - 'thisweek': '这个周', - 'prevweek': '上周', - '7daysago': '最近7天', - 'thismonth': '这个月', - 'prevmonth': '上个月', - 'thisquarter': '这个季度', - 'prevquarter': '上个季度', - 'thisyear': '今年' - } - }), - // getSchemaTpl('remark'), - // getSchemaTpl('labelRemark'), - { - type: 'input-text', - name: 'startPlaceholder', - label: '前占位提示', - pipeIn: defaultValue('开始时间') - }, - { - type: 'input-text', - name: 'endPlaceholder', - label: '后占位提示', - pipeIn: defaultValue('结束时间') - } - ] - }, - getSchemaTpl('status', {isFormItem: true}), - getSchemaTpl('validation', { - tag: ValidatorTag.Date - }) - ], {...context?.schema, configTitle: 'props'}) + getSchemaTpl('formulaControl', { + name: 'minDuration', + label: tipedLabel('最小跨度', '例如 2days'), + placeholder: '请输入最小跨度', + inputClassName: 'is-inline' + }), + getSchemaTpl('formulaControl', { + name: 'maxDuration', + label: tipedLabel('最大跨度', '例如 1year'), + placeholder: '请输入最大跨度', + inputClassName: 'is-inline' + }), + getSchemaTpl('dateShortCutControl', { + mode: 'normal', + dropDownOption: { + 'yesterday': '昨天', + 'thisweek': '这个周', + 'prevweek': '上周', + '7daysago': '最近7天', + 'thismonth': '这个月', + 'prevmonth': '上个月', + 'thisquarter': '这个季度', + 'prevquarter': '上个季度', + 'thisyear': '今年' + } + }), + // getSchemaTpl('remark'), + // getSchemaTpl('labelRemark'), + { + type: 'input-text', + name: 'startPlaceholder', + label: '前占位提示', + pipeIn: defaultValue('开始时间') + }, + { + type: 'input-text', + name: 'endPlaceholder', + label: '后占位提示', + pipeIn: defaultValue('选择结束时间') + }, + getSchemaTpl('autoFillApi') + ] + }, + getSchemaTpl('status', {isFormItem: true}), + getSchemaTpl('validation', { + tag: ValidatorTag.Date + }) + ], + {...context?.schema, configTitle: 'props'} + ) }, { title: '外观', - body: getSchemaTpl('collapseGroup', [ - getSchemaTpl('style:formItem', renderer), - getSchemaTpl('style:classNames', [ - getSchemaTpl('className', { - label: '描述', - name: 'descriptionClassName', - visibleOn: 'this.description' - }), - getSchemaTpl('className', { - name: 'addOn.className', - label: 'AddOn', - visibleOn: 'this.addOn && this.addOn.type === "text"' - }) - ]), - getSchemaTpl('style:others', [ - { - name: 'embed', - type: 'button-group-select', - size: 'md', - label: '模式', - mode: 'row', - value: false, - options: [ - { - label: '浮层', - value: false - }, - { - label: '内嵌', - value: true - } - ] - } - ]) - ], {...context?.schema, configTitle: 'style'}) + body: getSchemaTpl( + 'collapseGroup', + [ + getSchemaTpl('style:formItem', renderer), + getSchemaTpl('style:classNames', [ + getSchemaTpl('className', { + label: '描述', + name: 'descriptionClassName', + visibleOn: 'this.description' + }), + getSchemaTpl('className', { + name: 'addOn.className', + label: 'AddOn', + visibleOn: 'this.addOn && this.addOn.type === "text"' + }) + ]), + getSchemaTpl('style:others', [ + { + name: 'embed', + type: 'button-group-select', + size: 'md', + label: '模式', + mode: 'row', + value: false, + options: [ + { + label: '浮层', + value: false + }, + { + label: '内嵌', + value: true + } + ] + } + ]) + ], + {...context?.schema, configTitle: 'style'} + ) }, { title: '事件', diff --git a/packages/amis-editor/src/plugin/Form/InputFile.tsx b/packages/amis-editor/src/plugin/Form/InputFile.tsx index 7c39606e9..7b3de9673 100644 --- a/packages/amis-editor/src/plugin/Form/InputFile.tsx +++ b/packages/amis-editor/src/plugin/Form/InputFile.tsx @@ -1,13 +1,9 @@ import {defaultValue, getSchemaTpl, valuePipeOut} from 'amis-editor-core'; -import {registerEditorPlugin} from 'amis-editor-core'; +import {registerEditorPlugin, tipedLabel} from 'amis-editor-core'; import {BasePlugin, BaseEventContext} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class FileControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -348,7 +344,8 @@ export class FileControlPlugin extends BasePlugin { }), getSchemaTpl('remark'), getSchemaTpl('labelRemark'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') // getSchemaTpl('autoFill') ] }, diff --git a/packages/amis-editor/src/plugin/Form/InputImage.tsx b/packages/amis-editor/src/plugin/Form/InputImage.tsx index 5eabc241c..ec4e014cd 100644 --- a/packages/amis-editor/src/plugin/Form/InputImage.tsx +++ b/packages/amis-editor/src/plugin/Form/InputImage.tsx @@ -2,10 +2,8 @@ import {getSchemaTpl, valuePipeOut} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, BaseEventContext} from 'amis-editor-core'; import {formItemControl} from '../../component/BaseControl'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; + export class ImageControlPlugin extends BasePlugin { // 关联渲染器名字 rendererName = 'input-image'; diff --git a/packages/amis-editor/src/plugin/Form/InputNumber.tsx b/packages/amis-editor/src/plugin/Form/InputNumber.tsx index 92ef7f6ec..f3e94b593 100644 --- a/packages/amis-editor/src/plugin/Form/InputNumber.tsx +++ b/packages/amis-editor/src/plugin/Form/InputNumber.tsx @@ -1,7 +1,4 @@ -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; import flatten from 'lodash/flatten'; import {ContainerWrapper} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; @@ -13,8 +10,7 @@ import { SubRendererInfo, BaseEventContext } from 'amis-editor-core'; -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; +import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; diff --git a/packages/amis-editor/src/plugin/Form/InputRange.tsx b/packages/amis-editor/src/plugin/Form/InputRange.tsx index 09087d400..8ef5f0d8b 100644 --- a/packages/amis-editor/src/plugin/Form/InputRange.tsx +++ b/packages/amis-editor/src/plugin/Form/InputRange.tsx @@ -1,11 +1,7 @@ -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; +import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, BaseEventContext} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; @@ -278,7 +274,8 @@ export class RangeControlPlugin extends BasePlugin { label: '可重置', value: false, visibleOn: '!!data.showInput' - }) + }), + getSchemaTpl('autoFillApi') ] }, { diff --git a/packages/amis-editor/src/plugin/Form/InputRating.tsx b/packages/amis-editor/src/plugin/Form/InputRating.tsx index 90cb24237..dbf0fa7cb 100644 --- a/packages/amis-editor/src/plugin/Form/InputRating.tsx +++ b/packages/amis-editor/src/plugin/Form/InputRating.tsx @@ -5,14 +5,10 @@ import { undefinedPipeOut } from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; -import {BasePlugin, BaseEventContext} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; +import {BasePlugin, BaseEventContext, tipedLabel} from 'amis-editor-core'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class RateControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -234,7 +230,8 @@ export class RateControlPlugin extends BasePlugin { return res; } - }) + }), + getSchemaTpl('autoFillApi') ] }, getSchemaTpl('status', {isFormItem: true, readonly: true}), diff --git a/packages/amis-editor/src/plugin/Form/InputSubForm.tsx b/packages/amis-editor/src/plugin/Form/InputSubForm.tsx index 8b4020bad..43f4c8ab2 100644 --- a/packages/amis-editor/src/plugin/Form/InputSubForm.tsx +++ b/packages/amis-editor/src/plugin/Form/InputSubForm.tsx @@ -1,4 +1,5 @@ import {Button} from 'amis'; +import { SchemaCollection } from 'amis/lib/Schema'; import React from 'react'; import {registerEditorPlugin} from 'amis-editor-core'; import { @@ -94,7 +95,7 @@ export class SubFormControlPlugin extends BasePlugin { label: '允许最多个数', type: 'input-number' } - ]; + ] as SchemaCollection; }; filterProps(props: any) { diff --git a/packages/amis-editor/src/plugin/Form/InputTag.tsx b/packages/amis-editor/src/plugin/Form/InputTag.tsx index 7a94f5424..3ef32dfe9 100644 --- a/packages/amis-editor/src/plugin/Form/InputTag.tsx +++ b/packages/amis-editor/src/plugin/Form/InputTag.tsx @@ -9,10 +9,7 @@ import { } from 'amis-editor-core'; import {formItemControl} from '../../component/BaseControl'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class TagControlPlugin extends BasePlugin { // 关联渲染器名字 diff --git a/packages/amis-editor/src/plugin/Form/InputText.tsx b/packages/amis-editor/src/plugin/Form/InputText.tsx index 54363b57e..1f545e6b1 100644 --- a/packages/amis-editor/src/plugin/Form/InputText.tsx +++ b/packages/amis-editor/src/plugin/Form/InputText.tsx @@ -6,8 +6,7 @@ import { SubRendererInfo, BaseEventContext } from 'amis-editor-core'; -import {defaultValue, getSchemaTpl, setSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; +import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; @@ -222,7 +221,7 @@ export class TextControlPlugin extends BasePlugin { { name: 'addOn', label: tipedLabel('AddOn', '输入框左侧或右侧的附加挂件'), - type: 'ae-Switch-More', + type: 'ae-switch-more', mode: 'normal', formType: 'extend', title: 'AddOn', diff --git a/packages/amis-editor/src/plugin/Form/InputTree.tsx b/packages/amis-editor/src/plugin/Form/InputTree.tsx index 4a5aabe16..e08f32379 100644 --- a/packages/amis-editor/src/plugin/Form/InputTree.tsx +++ b/packages/amis-editor/src/plugin/Form/InputTree.tsx @@ -1,13 +1,10 @@ import React from 'react'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; import {BaseEventContext, BasePlugin} from 'amis-editor-core'; import {getEventControlConfig} from '../../util'; -import { getArgsWrapper } from '../../renderer/event-control/helper'; +import {getArgsWrapper} from '../../renderer/event-control/helper'; export class TreeControlPlugin extends BasePlugin { // 关联渲染器名字 diff --git a/packages/amis-editor/src/plugin/Form/Item.tsx b/packages/amis-editor/src/plugin/Form/Item.tsx index 30d8d2d0a..76b74da2a 100644 --- a/packages/amis-editor/src/plugin/Form/Item.tsx +++ b/packages/amis-editor/src/plugin/Form/Item.tsx @@ -112,9 +112,15 @@ export class ItemPlugin extends BasePlugin { ignoreValidator ? null : getSchemaTpl('required'), getSchemaTpl('description'), getSchemaTpl('placeholder'), - getSchemaTpl('remark'), - renderer.renderLabel !== false ? getSchemaTpl('labelRemark') : null, - autoFillApi ? getSchemaTpl('autoFillApi') : null + getSchemaTpl('remark', { + mode: 'row' + }), + renderer.renderLabel !== false + ? getSchemaTpl('labelRemark', { + mode: 'row' + }) + : null, + getSchemaTpl('autoFillApi') ] }, diff --git a/packages/amis-editor/src/plugin/Form/ListSelect.tsx b/packages/amis-editor/src/plugin/Form/ListSelect.tsx index 49438c8e0..d07b9b861 100644 --- a/packages/amis-editor/src/plugin/Form/ListSelect.tsx +++ b/packages/amis-editor/src/plugin/Form/ListSelect.tsx @@ -111,6 +111,8 @@ export class ListControlPlugin extends BasePlugin { }), getSchemaTpl('label'), getSchemaTpl('multiple'), + getSchemaTpl('extractValue'), + getSchemaTpl('valueFormula', { rendererSchema: context?.schema, useSelectMode: true, // 改用 Select 设置模式 diff --git a/packages/amis-editor/src/plugin/Form/MatrixCheckboxes.tsx b/packages/amis-editor/src/plugin/Form/MatrixCheckboxes.tsx index a61b8b44d..67477cc3c 100644 --- a/packages/amis-editor/src/plugin/Form/MatrixCheckboxes.tsx +++ b/packages/amis-editor/src/plugin/Form/MatrixCheckboxes.tsx @@ -5,15 +5,12 @@ import { BasicSubRenderInfo, RendererEventContext, SubRendererInfo, - BaseEventContext + BaseEventContext, + tipedLabel } from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class MatrixControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -151,7 +148,8 @@ export class MatrixControlPlugin extends BasePlugin { } ], pipeIn: defaultValue('column') - } + }, + getSchemaTpl('autoFillApi') ] }, { diff --git a/packages/amis-editor/src/plugin/Form/NestedSelect.tsx b/packages/amis-editor/src/plugin/Form/NestedSelect.tsx index 733e33a7c..a5e306bf7 100644 --- a/packages/amis-editor/src/plugin/Form/NestedSelect.tsx +++ b/packages/amis-editor/src/plugin/Form/NestedSelect.tsx @@ -1,8 +1,5 @@ import {relativeValueRe} from 'amis'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; import {availableLanguages} from 'amis/lib/renderers/Form/Editor'; import {defaultValue, getSchemaTpl, valuePipeOut} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; @@ -11,9 +8,9 @@ import { BasicSubRenderInfo, RendererEventContext, SubRendererInfo, - BaseEventContext + BaseEventContext, + tipedLabel } from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; @@ -285,7 +282,8 @@ export class NestedSelectControlPlugin extends BasePlugin { getSchemaTpl('labelRemark'), getSchemaTpl('remark'), getSchemaTpl('placeholder'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, { diff --git a/packages/amis-editor/src/plugin/Form/Radios.tsx b/packages/amis-editor/src/plugin/Form/Radios.tsx index 3ccc9af31..2fbdced4e 100644 --- a/packages/amis-editor/src/plugin/Form/Radios.tsx +++ b/packages/amis-editor/src/plugin/Form/Radios.tsx @@ -4,10 +4,7 @@ import {BasePlugin, BaseEventContext} from 'amis-editor-core'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class RadiosControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -122,7 +119,8 @@ export class RadiosControlPlugin extends BasePlugin { }), // getSchemaTpl('autoFill') getSchemaTpl('labelRemark'), - getSchemaTpl('remark') + getSchemaTpl('remark'), + getSchemaTpl('autoFillApi') ] }, { diff --git a/packages/amis-editor/src/plugin/Form/Select.tsx b/packages/amis-editor/src/plugin/Form/Select.tsx index c654857c0..3b9c63364 100644 --- a/packages/amis-editor/src/plugin/Form/Select.tsx +++ b/packages/amis-editor/src/plugin/Form/Select.tsx @@ -1,15 +1,10 @@ import {getSchemaTpl} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, BaseEventContext} from 'amis-editor-core'; -import isArray from 'lodash/isArray'; -import {tipedLabel} from '../../component/BaseControl'; import {ValidatorTag} from '../../validator'; import {getEventControlConfig} from '../../util'; -import { - RendererPluginAction, - RendererPluginEvent -} from 'amis-editor-core'; +import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core'; export class SelectControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -225,7 +220,8 @@ export class SelectControlPlugin extends BasePlugin { getSchemaTpl('labelRemark'), getSchemaTpl('remark'), getSchemaTpl('placeholder'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') ] }, { @@ -263,7 +259,7 @@ export class SelectControlPlugin extends BasePlugin { } }), getSchemaTpl('editable', { - type: 'ae-Switch-More', + type: 'ae-switch-more', formType: 'extend', hiddenOnDefault: true, form: { @@ -271,7 +267,7 @@ export class SelectControlPlugin extends BasePlugin { } }), getSchemaTpl('removable', { - type: 'ae-Switch-More', + type: 'ae-switch-more', formType: 'extend', hiddenOnDefault: true, form: { diff --git a/packages/amis-editor/src/plugin/Form/Static.tsx b/packages/amis-editor/src/plugin/Form/Static.tsx index d23afced5..0a04216b3 100644 --- a/packages/amis-editor/src/plugin/Form/Static.tsx +++ b/packages/amis-editor/src/plugin/Form/Static.tsx @@ -4,10 +4,7 @@ import {defaultValue, getSchemaTpl} from 'amis-editor-core'; import {registerEditorPlugin} from 'amis-editor-core'; import {BaseEventContext, BasePlugin} from 'amis-editor-core'; import {EditorNodeType} from 'amis-editor-core'; -import {mockValue} from 'amis-editor-core'; -import {tipedLabel} from '../../component/BaseControl'; -import {ValidatorTag} from '../../validator'; -import {getEventControlConfig} from '../../util'; +import {mockValue, tipedLabel} from 'amis-editor-core'; export class StaticControlPlugin extends BasePlugin { // 关联渲染器名字 @@ -64,7 +61,7 @@ export class StaticControlPlugin extends BasePlugin { rendererSchema: { ...context?.schema, type: 'textarea', // 改用多行文本编辑 - value: context?.schema.tpl, // 避免默认值丢失 + value: context?.schema.tpl // 避免默认值丢失 }, mode: 'vertical', // 改成上下展示模式 name: 'tpl' @@ -294,7 +291,8 @@ export class StaticControlPlugin extends BasePlugin { getSchemaTpl('labelRemark'), getSchemaTpl('remark'), getSchemaTpl('placeholder'), - getSchemaTpl('description') + getSchemaTpl('description'), + getSchemaTpl('autoFillApi') /*{ children: ( - ) - }, - { - type: 'divider' - }, - { - name: 'title', - type: 'input-text', - label: '标题' - }, - isCRUDBody - ? null - : { - name: 'source', + title: '基本', + body: [ + { + name: 'title', type: 'input-text', - label: '数据源', - pipeIn: defaultValue('${items}'), - description: '绑定当前环境变量' + label: '标题' }, - { - name: 'placeholder', - pipeIn: defaultValue('没有数据'), - type: 'input-text', - label: '无数据提示' - } - ] + isCRUDBody + ? null + : { + name: 'source', + type: 'input-text', + label: '数据源', + pipeIn: defaultValue('${items}'), + description: '绑定当前环境变量' + }, + { + name: 'placeholder', + pipeIn: defaultValue('没有数据'), + type: 'input-text', + label: '无数据提示' + } + ] + }, + getSchemaTpl('status', { + isFormItem: false + }) + ]) }, { title: '外观', - body: [ - getSchemaTpl('switch', { - name: 'showHeader', - label: '是否显示头部', - pipeIn: defaultValue(true) - }), + body: getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + getSchemaTpl('switch', { + name: 'showHeader', + label: '显示头部', + pipeIn: defaultValue(true) + }), - getSchemaTpl('switch', { - name: 'showFooter', - label: '是否显示底部', - pipeIn: defaultValue(true) - }), - - getSchemaTpl('className', { - label: 'CSS 类名' - }), - getSchemaTpl('className', { - name: 'listClassName', - label: 'List div CSS 类名' - }), - getSchemaTpl('className', { - name: 'headerClassName', - label: '头部 CSS 类名' - }), - getSchemaTpl('className', { - name: 'footerClassName', - label: '底部 CSS 类名' + getSchemaTpl('switch', { + name: 'showFooter', + label: '显示底部', + pipeIn: defaultValue(true) + }) + ] + }, + getSchemaTpl('style:classNames', { + isFormItem: false, + schema: [ + getSchemaTpl('className', { + name: 'listClassName', + label: '列表项' + }), + getSchemaTpl('className', { + name: 'headerClassName', + label: '头部' + }), + getSchemaTpl('className', { + name: 'footerClassName', + label: '底部' + }) + ] }) - ] + ]) }, { - title: '显隐', - body: [getSchemaTpl('ref'), getSchemaTpl('visible')] + title: '事件', + className: 'p-none', + body: [ + getSchemaTpl('eventControl', { + name: 'onEvent', + ...getEventControlConfig(this.manager, context) + }) + ] } ]); }; - filterProps(props: any) { - if (props.isSlot) { - props.value = [props.data]; - return props; + overrides = { + renderListItem( + this: any, + index: number, + itemTemplace: ListItemSchema | undefined, + ...rest: any[] + ) { + return this.super( + index, + // 使第一个卡片元素可以选择并编辑schema + index > 0 ? JSONPipeOut(itemTemplace) : itemTemplace, + ...rest + ); } + }; + filterProps(props: any) { const data = { ...props.defaultData, ...props.data }; - let arr = Array.isArray(props.value) + let value = Array.isArray(props.value) ? props.value : typeof props.source === 'string' ? resolveVariable(props.source, data) : resolveVariable('items', data); - if (!Array.isArray(arr) || !arr.length) { - const mockedData: any = this.buildMockData(); - props.value = repeatArray(mockedData, 1).map((item, index) => ({ - ...item, - id: index + 1 - })); + value = !Array.isArray(value) ? [] : value; + + if (value.length < 5) { + const mockedData = value.length + ? value[0] + : { + id: 666, + title: '假数据', + description: '假数据', + a: '假数据', + b: '假数据' + }; + + value = value.concat( + repeatArray(mockedData, 3).map((item, index) => ({ + ...item, + id: index + 1 + })) + ); } - const {$schema, ...rest} = props; + value = value.slice(0, 4); return { - ...JSONPipeOut(rest), - $schema + ...props, + value }; } @@ -251,7 +283,7 @@ export class ListPlugin extends BasePlugin { const {renderer, schema} = context; if ( !schema.$$id && - schema.$$editor?.renderer.name === 'crud' && + ['crud', 'crud2'].includes(schema.$$editor?.renderer.name) && renderer.name === 'list' ) { return { diff --git a/packages/amis-editor/src/plugin/ListItem.tsx b/packages/amis-editor/src/plugin/ListItem.tsx index 7f4240486..e6494438d 100644 --- a/packages/amis-editor/src/plugin/ListItem.tsx +++ b/packages/amis-editor/src/plugin/ListItem.tsx @@ -13,6 +13,7 @@ import { } from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; import {VRenderer} from 'amis-editor-core'; +import { getEventControlConfig } from '../util'; export class ListItemPlugin extends BasePlugin { // 关联渲染器名字 @@ -37,83 +38,78 @@ export class ListItemPlugin extends BasePlugin { ]; panelTitle = '列表项'; - panelBody = getSchemaTpl('tabs', [ - { - title: '基本', - body: [ - // { - // children: ( - // - // ) - // }, - // { - // children: ( - //
- // - //
- // ) - // }, - // { - // type: 'divider' - // }, - { - name: 'title', - type: 'input-text', - label: '标题', - descrition: '支持模板语法如: ${xxx}' - }, - { - name: 'subTitle', - type: 'input-text', - label: '副标题', - descrition: '支持模板语法如: ${xxx}' - }, - { - name: 'avatar', - type: 'input-text', - label: '图片地址', - descrition: '支持模板语法如: ${xxx}' - }, - { - name: 'desc', - type: 'textarea', - label: '描述', - descrition: '支持模板语法如: ${xxx}' - } - ] - }, - { - title: '外观', - body: [ - getSchemaTpl('className', { - name: 'avatarClassName', - label: '图片 CSS 类名', - pipeIn: defaultValue('thumb-sm avatar m-r') - }), - getSchemaTpl('className', { - name: 'titleClassName', - label: '标题 CSS 类名' - }) - ] - } - ]); + panelJustify = true; + panelBodyCreator = (context: BaseEventContext) => { + const isCRUDBody = ['crud', 'crud2'].includes(context.schema.type); + + return getSchemaTpl('tabs', [ + { + title: '属性', + body: getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + { + name: 'title', + type: 'input-text', + label: '标题', + descrition: '支持模板语法如: ${xxx}' + }, + { + name: 'subTitle', + type: 'input-text', + label: '副标题', + descrition: '支持模板语法如: ${xxx}' + }, + { + name: 'avatar', + type: 'input-text', + label: '图片地址', + descrition: '支持模板语法如: ${xxx}' + }, + { + name: 'desc', + type: 'textarea', + label: '描述', + descrition: '支持模板语法如: ${xxx}' + } + ] + }, + getSchemaTpl('status', { + isFormItem: false + }) + ]) + }, + { + title: '外观', + body: getSchemaTpl('collapseGroup', [ + getSchemaTpl('style:classNames', { + isFormItem: false, + schema: [ + getSchemaTpl('className', { + name: 'avatarClassName', + label: '图片' + }), + getSchemaTpl('className', { + name: 'titleClassName', + label: '标题' + }) + ] + }) + ]) + }, + { + title: '事件', + className: 'p-none', + body: [ + getSchemaTpl('eventControl', { + name: 'onEvent', + ...getEventControlConfig(this.manager, context) + }) + ] + } + ]); + }; getRendererInfo({ renderer, diff --git a/packages/amis-editor/src/plugin/Others/Action.tsx b/packages/amis-editor/src/plugin/Others/Action.tsx index 350a333cd..6b3c82b2d 100644 --- a/packages/amis-editor/src/plugin/Others/Action.tsx +++ b/packages/amis-editor/src/plugin/Others/Action.tsx @@ -6,10 +6,11 @@ import { BasePlugin, BasicPanelItem, BasicToolbarItem, - BuildPanelEventContext, + BuildPanelEventContext } from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; import {diff} from 'amis-editor-core'; +import {SchemaCollection} from 'amis/lib/Schema'; export class ActionPlugin extends BasePlugin { panelTitle = '按钮'; @@ -350,7 +351,7 @@ export class ActionPlugin extends BasePlugin { className: 'p-3', body: schema } - ]; + ] as SchemaCollection; }; buildEditorPanel( diff --git a/packages/amis-editor/src/plugin/Others/TableCell.tsx b/packages/amis-editor/src/plugin/Others/TableCell.tsx index 46e6a91b1..433e8b9d0 100644 --- a/packages/amis-editor/src/plugin/Others/TableCell.tsx +++ b/packages/amis-editor/src/plugin/Others/TableCell.tsx @@ -40,11 +40,9 @@ export class TableCellPlugin extends BasePlugin { type: 'input-text' }, - { - name: 'name', - type: 'input-text', + getSchemaTpl('formItemName', { label: '绑定字段名' - }, + }), { name: 'remark', diff --git a/packages/amis-editor/src/plugin/Page.tsx b/packages/amis-editor/src/plugin/Page.tsx index 417314dcb..2184b7664 100644 --- a/packages/amis-editor/src/plugin/Page.tsx +++ b/packages/amis-editor/src/plugin/Page.tsx @@ -177,15 +177,15 @@ export class PagePlugin extends BasePlugin { label: '数据初始化接口', name: 'initApi', sampleBuilder: (schema: any) => `{ - "status": 0, - "msg": "", + "status": 0, + "msg": "", - data: { - // 示例数据 - "id": 1, - "a": "sample" - } -}` + data: { + // 示例数据 + "id": 1, + "a": "sample" + } + }` }), getSchemaTpl('initFetch'), diff --git a/packages/amis-editor/src/plugin/Pagination.tsx b/packages/amis-editor/src/plugin/Pagination.tsx index b2f2bd442..e1799a464 100644 --- a/packages/amis-editor/src/plugin/Pagination.tsx +++ b/packages/amis-editor/src/plugin/Pagination.tsx @@ -1,11 +1,14 @@ -import {registerEditorPlugin} from 'amis-editor-core'; -import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; -import {ValidatorTag} from '../validator'; import {getEventControlConfig} from '../util'; -import {RendererPluginEvent} from 'amis-editor-core'; - -import {defaultValue, getSchemaTpl} from 'amis-editor-core'; +import { + getSchemaTpl, + defaultValue, + tipedLabel, + BasePlugin, + RendererPluginEvent, + RegionConfig, + BaseEventContext, + registerEditorPlugin +} from 'amis-editor-core'; export class PaginationPlugin extends BasePlugin { // 关联渲染器名字 @@ -14,17 +17,16 @@ export class PaginationPlugin extends BasePlugin { // 组件名称 name = '分页组件'; - isBaseComponent = true; - disabledRendererPlugin = true; + isBaseComponent = false; description = '分页组件,可以对列表进行分页展示,提高页面性能'; tags = ['容器']; icon = 'fa fa-window-minimize'; - // pluginIcon = 'pagination-plugin'; // 暂无新icon - baseLayoutLIst = [ + lastLayoutSetting = ['pager']; + layoutOptions = [ {text: '总数', value: 'total', checked: false}, {text: '每页条数', value: 'perPage', checked: false}, {text: '分页', value: 'pager', checked: true}, - {text: '跳转', value: 'go', checked: false} + {text: '跳转页', value: 'go', checked: false} ]; scaffold = { type: 'pagination', @@ -66,27 +68,52 @@ export class PaginationPlugin extends BasePlugin { body: [ { name: 'mode', - label: '分页类型', + label: '模式', type: 'button-group-select', size: 'sm', pipeIn: defaultValue('normal'), options: [ { - label: '普通', + label: '默认', value: 'normal' }, { - label: '简易', + label: '简约', value: 'simple' } ] }, + // { + // name: 'hasNext', + // label: '是否有下一页', + // mode: 'row', + // inputClassName: 'inline-flex justify-between flex-row-reverse', + // type: 'switch', + // visibleOn: 'data.mode === "simple"' + // }, + // { + // name: 'activePage', + // label: tipedLabel('当前页', '支持使用 \\${xxx} 来获取变量'), + // type: 'input-text' + // }, + // { + // name: 'lastPage', + // label: tipedLabel('最后页码', '支持使用 \\${xxx} 来获取变量'), + // type: 'input-text', + // visibleOn: 'data.mode === "normal"' + // }, + // { + // name: 'total', + // label: tipedLabel('总条数', '支持使用 \\${xxx} 来获取变量'), + // type: 'input-text', + // visibleOn: 'data.mode === "normal"' + // }, getSchemaTpl('combo-container', { name: 'layout', type: 'combo', label: tipedLabel( - '分页布局展示', - '选中表示渲染该项,可以拖拽排序调整显示的顺序' + '启用功能', + '选中表示启用该项,可以拖拽排序调整功能的顺序' ), visibleOn: 'data.mode === "normal"', mode: 'normal', @@ -112,52 +139,37 @@ export class PaginationPlugin extends BasePlugin { } ], pipeIn: (value: any) => { - let layoutList: string[] = []; - if (Array.isArray(value)) { - layoutList = value; + if (!value) { + value = this.lastLayoutSetting; } else if (typeof value === 'string') { - layoutList = (value as string).split(','); + value = (value as string).split(','); } - const layout = this.baseLayoutLIst.map(v => ({ + return this.layoutOptions.map(v => ({ ...v, - checked: layoutList.includes(v.value) + checked: value.includes(v.value) })); - return layout; }, pipeOut: (value: any[]) => { - this.baseLayoutLIst = [...value]; - return value.filter(v => v.checked).map(v => v.value); + this.lastLayoutSetting = value + .filter(v => v.checked) + .map(v => v.value); + return this.lastLayoutSetting.concat(); } }), - { - type: 'ae-formulaControl', - label: '是否有下一页', - name: 'hasNext', - visibleOn: 'data.mode === "simple"' - }, - { - type: 'ae-formulaControl', - label: '当前页', - name: 'activePage' - }, - { - type: 'ae-formulaControl', - label: '最后页码', - name: 'lastPage', - visibleOn: 'data.mode === "normal"' - }, - { - type: 'ae-formulaControl', - label: '总条数', - name: 'total', - visibleOn: 'data.mode === "normal"' - }, + // { + // name: 'showPerPage', + // label: '显示每页条数', + // mode: 'row', + // inputClassName: 'inline-flex justify-between flex-row-reverse', + // type: 'switch', + // visibleOn: 'data.mode === "normal"' + // }, getSchemaTpl('combo-container', { name: 'perPageAvailable', type: 'combo', label: '每页条数选项', visibleOn: - 'data.mode === "normal" && data.layout?.includes("perPage")', + 'data.mode === "normal" && data.layout && data.layout.includes("perPage")', mode: 'normal', multiple: true, multiLine: false, @@ -167,6 +179,7 @@ export class PaginationPlugin extends BasePlugin { editable: true, minLength: 1, tabsStyle: 'inline', + addButtonClassName: 'm-b-sm', items: [ { type: 'input-number', @@ -175,31 +188,35 @@ export class PaginationPlugin extends BasePlugin { } ], pipeIn: (value: any[]) => { - return value.map(v => ({value: v})); + return value?.map(v => ({value: v})) || [10]; }, pipeOut: (value: any[]) => { return value.map(v => v.value); } - }), - { - name: 'perPage', - type: 'input-text', - label: '默认每页条数', - visibleOn: - 'data.mode === "normal" && data.layout?.includes("perPage")' - }, - { - name: 'maxButton', - label: tipedLabel( - '最多按钮数', - '最多显示多少个分页按钮,最小为5,最大值为20' - ), - type: 'input-number', - min: 5, - max: 20, - pipeOut: (value: any) => value || 5, - visibleOn: 'data.mode === "normal"' - } + }) + // { + // name: 'perPage', + // type: 'input-text', + // label: '默认每页条数', + // visibleOn: 'data.mode === "normal"' + // }, + // { + // name: 'maxButton', + // label: tipedLabel('分页按钮数量', '超过此数量,将会隐藏多余按钮'), + // type: 'input-number', + // min: 5, + // max: 20, + // pipeIn: defaultValue(5), + // visibleOn: 'data.mode === "normal"' + // } + // { + // name: 'showPageInput', + // label: '显示页面跳转', + // mode: 'row', + // inputClassName: 'inline-flex justify-between flex-row-reverse', + // type: 'switch', + // visibleOn: 'data.mode === "normal"' + // } ] }, { diff --git a/packages/amis-editor/src/plugin/Panel.tsx b/packages/amis-editor/src/plugin/Panel.tsx index af0a94151..d2af59c76 100644 --- a/packages/amis-editor/src/plugin/Panel.tsx +++ b/packages/amis-editor/src/plugin/Panel.tsx @@ -85,7 +85,8 @@ export class PanelPlugin extends BasePlugin { panelJustify = true; panelBodyCreator = (context: BaseEventContext) => { - const isForm = /(?:^|\/)form$/.test(context.path) || context?.schema?.type === 'form'; + const isForm = + /(?:^|\/)form$/.test(context.path) || context?.schema?.type === 'form'; return [ getSchemaTpl('tabs', [ @@ -102,7 +103,7 @@ export class PanelPlugin extends BasePlugin { name: 'title', type: 'input-text' }, - + isForm ? null : { @@ -124,7 +125,7 @@ export class PanelPlugin extends BasePlugin { ] }, getSchemaTpl('status') - ]), + ]) ] }, { @@ -140,7 +141,7 @@ export class PanelPlugin extends BasePlugin { label: '固定底部', value: false }), - + getSchemaTpl('horizontal', { visibleOn: '(data.mode || data.$$formMode) == "horizontal" && data.$$mode == "form"' @@ -152,7 +153,9 @@ export class PanelPlugin extends BasePlugin { title: '内容区域展示', body: [ getSchemaTpl('subFormItemMode', {label: '表单展示模式'}), - getSchemaTpl('subFormHorizontalMode', {label: '表单水平占比'}), + getSchemaTpl('subFormHorizontalMode', { + label: '表单水平占比' + }), getSchemaTpl('subFormHorizontal') ] }, @@ -211,28 +214,28 @@ export class PanelPlugin extends BasePlugin { name: isForm ? 'panelClassName' : 'className', pipeIn: defaultValue('Panel--default') }), - + getSchemaTpl('className', { name: 'headerClassName', label: '头部区域' }), - + getSchemaTpl('className', { name: 'bodyClassName', label: '内容区域' }), - + getSchemaTpl('className', { name: 'footerClassName', label: '底部区域' }), - + getSchemaTpl('className', { name: 'actionsClassName', label: '按钮外层' }) - ], - }, + ] + } ]) ] } @@ -251,9 +254,10 @@ export class PanelPlugin extends BasePlugin { if ( context.info.renderer.name === 'form' && schema.wrapWithPanel !== false && - !context.selections.length + !context.selections.length && + false ) { - + /** Panel相关的配置融合到From中了 */ panels.push({ key: 'panel', icon: 'fa fa-list-alt', diff --git a/packages/amis-editor/src/plugin/Progress.tsx b/packages/amis-editor/src/plugin/Progress.tsx index a77e23363..a0e36b827 100644 --- a/packages/amis-editor/src/plugin/Progress.tsx +++ b/packages/amis-editor/src/plugin/Progress.tsx @@ -1,7 +1,7 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {BaseEventContext, BasePlugin} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import {ValidatorTag} from '../validator'; import {getEventControlConfig} from '../util'; diff --git a/packages/amis-editor/src/plugin/Service.tsx b/packages/amis-editor/src/plugin/Service.tsx index e784886ce..4271ead19 100644 --- a/packages/amis-editor/src/plugin/Service.tsx +++ b/packages/amis-editor/src/plugin/Service.tsx @@ -1,14 +1,19 @@ import {Button} from 'amis'; import React from 'react'; -import {registerEditorPlugin} from 'amis-editor-core'; -import {BaseEventContext, BasePlugin, RegionConfig} from 'amis-editor-core'; -import {getSchemaTpl} from 'amis-editor-core'; import {getEventControlConfig} from '../util'; -import type { +import { + getSchemaTpl, + EditorManager, RendererPluginAction, - RendererPluginEvent + RendererPluginEvent, + registerEditorPlugin, + BaseEventContext, + BasePlugin, + RegionConfig, + DSBuilderManager } from 'amis-editor-core'; +import {flattenDeep} from 'lodash'; export class ServicePlugin extends BasePlugin { // 关联渲染器名字 @@ -21,28 +26,30 @@ export class ServicePlugin extends BasePlugin { description = '功能性容器,可以用来加载数据或者加载渲染器配置。加载到的数据在容器可以使用。'; docLink = '/amis/zh-CN/components/service'; - tags = ['功能']; + tags = ['功能', '数据容器']; icon = 'fa fa-server'; pluginIcon = 'service-plugin'; scaffold = { + type: 'service', + body: [] + }; + previewSchema = { type: 'service', body: [ { type: 'tpl', - tpl: '内容', - inline: false + tpl: '内容区域', + inline: false, + className: 'bg-light wrapper' } ] }; - previewSchema = { - type: 'tpl', - tpl: '功能性组件,用于数据拉取。' - }; regions: Array = [ { key: 'body', - label: '内容区' + label: '内容区域', + placeholder: '内容区域' } ]; @@ -79,7 +86,30 @@ export class ServicePlugin extends BasePlugin { panelTitle = '服务'; + dsBuilderMgr: DSBuilderManager; + + constructor(manager: EditorManager) { + super(manager); + this.dsBuilderMgr = new DSBuilderManager('service', 'api'); + } + panelBodyCreator = (context: BaseEventContext) => { + const dsTypeSelect = () => + this.dsBuilderMgr.getDSSwitch({ + onChange: (value: any, oldValue: any, model: any, form: any) => { + if (value !== oldValue) { + const data = form.data; + Object.keys(data).forEach(key => { + if (key.endsWith('Fields') || key.toLowerCase().endsWith('api')) { + form.deleteValueByName(key); + } + }); + form.deleteValueByName('__fields'); + } + return value; + } + }); + return getSchemaTpl('tabs', [ { title: '属性', @@ -89,87 +119,57 @@ export class ServicePlugin extends BasePlugin { { title: '基本', body: [ - getSchemaTpl('name'), - { - children: ( - - ) - } - ] - }, - { - title: '数据接口', - body: [ - getSchemaTpl('apiControl', { - name: 'api', - label: '数据接口', - messageDesc: - '设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主' - }), - { - name: 'ws', - type: 'input-text', - label: 'WebSocket 实时更新接口' - }, - /** initFetchOn可以通过api的sendOn属性控制 */ - getSchemaTpl('switch', { - name: 'initFetch', - label: '数据接口初始加载', + dsTypeSelect(), + ...this.dsBuilderMgr.collectFromBuilders( + (builder, builderFlag) => { + return { + type: 'container', + visibleOn: `this.dsType == null || this.dsType === '${builderFlag}'`, + body: flattenDeep([ + builder.makeSourceSettingForm({ + name: 'api', + label: '数据源', + feat: 'View', + inScaffold: false, + inCrud: false + }) + ]) + }; + } + ), + getSchemaTpl('initFetch', { visibleOn: 'this.api' }), - { - name: 'interval', - label: '定时刷新间隔', - visibleOn: 'this.api', - type: 'input-number', - step: 500, - description: '设置后将自动定时刷新,单位 ms' - }, - getSchemaTpl('switch', { - name: 'silentPolling', - label: '静默加载', - visibleOn: '!!data.interval', - description: '设置自动定时刷新是否显示加载动画' + getSchemaTpl('interval', { + visibleOn: 'this.api' }), - { - name: 'stopAutoRefreshWhen', - label: '停止定时刷新检测', - type: 'input-text', - visibleOn: '!!data.interval', - description: - '定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则不刷新了。' - } + getSchemaTpl('silentPolling'), + getSchemaTpl('stopAutoRefreshWhen') ] }, { - title: 'Schema接口', + title: '状态', + body: [getSchemaTpl('visible')] + }, + { + title: '高级', body: [ getSchemaTpl('apiControl', { name: 'schemaApi', - label: '内容 Schema 接口' + label: 'Schema数据源' }), - getSchemaTpl('switch', { + getSchemaTpl('initFetch', { name: 'initFetchSchema', - label: 'Schema接口初始加载', + label: '是否Schema初始加载', visibleOn: 'this.schemaApi' - }) - ] - }, - { - title: '全局配置', - body: [ + }), + { + type: 'divider' + }, getSchemaTpl('data'), + { + type: 'divider' + }, { type: 'js-editor', allowFullscreen: true, @@ -178,30 +178,14 @@ export class ServicePlugin extends BasePlugin { description: '将会传递 data 和 setData 两个参数' }, { - label: '默认消息信息', - type: 'combo', - name: 'messages', - multiLine: true, - description: - '设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主', - items: [ - { - label: '获取成功', - type: 'input-text', - name: 'fetchSuccess' - }, - { - label: '获取失败', - type: 'input-text', - name: 'fetchFailed' - } - ] + type: 'divider' + }, + { + name: 'ws', + type: 'input-text', + label: 'WebSocket 实时更新接口' } ] - }, - { - title: '状态', - body: [getSchemaTpl('ref'), getSchemaTpl('visible')] } ]) ] diff --git a/packages/amis-editor/src/plugin/Table-v2.tsx b/packages/amis-editor/src/plugin/Table-v2.tsx new file mode 100644 index 000000000..5192f8270 --- /dev/null +++ b/packages/amis-editor/src/plugin/Table-v2.tsx @@ -0,0 +1,907 @@ +import {resolveVariable} from 'amis'; + +import {setVariable} from 'amis-core'; +import { + BasePlugin, + BaseEventContext, + PluginEvent, + RegionConfig, + RendererInfoResolveEventContext, + BasicRendererInfo, + PluginInterface, + InsertEventContext, + ScaffoldForm, + registerEditorPlugin, + defaultValue, + getSchemaTpl, + tipedLabel, + repeatArray, + mockValue, + EditorNodeType, + RendererPluginAction, + RendererPluginEvent +} from 'amis-editor-core'; +import {getEventControlConfig} from '../util'; +import {SchemaObject} from 'amis/lib/Schema'; +import {getArgsWrapper} from '../renderer/event-control/helper'; + +export class TableV2Plugin extends BasePlugin { + // 关联渲染器名字 + rendererName = 'table-v2'; + $schema = '/schemas/TableSchema.json'; + + // 组件名称 + name = '表格V2'; + isBaseComponent = true; + panelJustify = true; + description = + '用来展示表格数据,可以配置列信息,然后关联数据便能完成展示。支持嵌套、超级表头、列固定、表头固顶、合并单元格等等。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。'; + docLink = '/amis/zh-CN/components/table-v2'; + icon = 'fa fa-table'; + + scaffold: SchemaObject = { + type: 'table-v2', + + columns: [ + { + title: '列信息', + key: 'a' + } + ], + + source: '$item' + }; + + regions: Array = [ + { + key: 'columns', + label: '列集合', + renderMethod: 'renderTable', + preferTag: '展示', + dndMode: 'position-h' + } + ]; + + previewSchema: any = { + type: 'table-v2', + className: 'text-left m-b-none', + items: [ + {a: 1, b: 2, c: 9}, + {a: 3, b: 4, c: 8}, + {a: 5, b: 6, c: 7} + ], + columns: [ + { + title: 'A', + key: 'a' + }, + { + title: 'B', + key: 'b' + } + ] + }; + + scaffoldForm: ScaffoldForm = { + title: '快速构建表格', + body: [ + { + name: 'columns', + type: 'combo', + multiple: true, + label: false, + addButtonText: '新增一列', + draggable: true, + items: [ + { + type: 'input-text', + name: 'title', + placeholder: '标题' + }, + { + type: 'input-text', + name: 'key', + placeholder: '绑定字段名' + }, + { + type: 'select', + name: 'type', + placeholder: '类型', + value: 'text', + options: [ + { + value: 'text', + label: '纯文本' + }, + { + value: 'tpl', + label: '模板' + }, + { + value: 'image', + label: '图片' + }, + { + value: 'date', + label: '日期' + }, + { + value: 'progress', + label: '进度' + }, + { + value: 'status', + label: '状态' + }, + { + value: 'mapping', + label: '映射' + }, + { + value: 'container', + label: '容器' + }, + { + value: 'operation', + label: '操作栏' + } + ] + } + ] + } + ], + canRebuild: true + }; + + panelTitle = '表格'; + + events: RendererPluginEvent[] = [ + { + eventName: 'selectedChange', + eventLabel: '选择表格项', + description: '手动选择表格项事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.selectedItems': { + type: 'array', + title: '已选择行' + }, + 'event.data.unSelectedItems': { + type: 'array', + title: '未选择行' + } + } + } + ] + }, + { + eventName: 'columnSort', + eventLabel: '列排序', + description: '点击列排序事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.orderBy': { + type: 'string', + title: '列排序列名' + }, + 'event.data.orderDir': { + type: 'string', + title: '列排序值' + } + } + } + ] + }, + { + eventName: 'columnFilter', + eventLabel: '列筛选', + description: '点击列筛选事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.filterName': { + type: 'string', + title: '列筛选列名' + }, + 'event.data.filterValue': { + type: 'string', + title: '列筛选值' + } + } + } + ] + }, + { + eventName: 'columnSearch', + eventLabel: '列搜索', + description: '点击列搜索事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.searchName': { + type: 'string', + title: '列搜索列名' + }, + 'event.data.searchValue': { + type: 'object', + title: '列搜索数据' + } + } + } + ] + }, + { + eventName: 'orderChange', + eventLabel: '行排序', + description: '手动拖拽行排序事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.movedItems': { + type: 'array', + title: '已排序数据' + } + } + } + ] + }, + { + eventName: 'columnToggled', + eventLabel: '列显示变化', + description: '点击自定义列事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.columns': { + type: 'array', + title: '当前显示的列配置数据' + } + } + } + ] + }, + { + eventName: 'rowClick', + eventLabel: '行单击', + description: '点击整行事件', + dataSchema: [ + { + type: 'object', + properties: { + 'event.data.rowItem': { + type: 'object', + title: '行点击数据' + } + } + } + ] + } + ]; + + actions: RendererPluginAction[] = [ + { + actionType: 'select', + actionLabel: '设置选中项', + description: '设置表格的选中项', + schema: getArgsWrapper([ + { + type: 'input-formula', + variables: '${variables}', + evalMode: false, + variableMode: 'tabs', + label: '选中项', + size: 'lg', + name: 'selected', + mode: 'horizontal' + } + ]) + }, + { + actionType: 'selectAll', + actionLabel: '设置全部选中', + description: '设置表格全部项选中' + }, + { + actionType: 'clearAll', + actionLabel: '清空选中项', + description: '清空表格所有选中项' + } + ]; + + async buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) { + const itemsSchema: any = { + $id: 'tableRow', + type: 'object', + properties: {} + }; + const columns: EditorNodeType = node.children.find( + item => item.isRegion && item.region === 'columns' + ); + + for (let current of columns.children) { + const schema = current.schema; + if (schema && schema.key) { + itemsSchema.properties[schema.key] = current.info?.plugin + ?.buildDataSchemas + ? await current.info.plugin.buildDataSchemas(current, region) + : { + type: 'string', + title: schema.label || schema.title, + description: schema.description + }; + } + } + + const result: any = { + $id: 'table-v2', + type: 'object', + properties: { + items: { + type: 'array', + title: '表格数据', + items: itemsSchema + } + } + }; + + if (region?.region === 'columns') { + result.properties = { + ...itemsSchema.properties, + ...result.properties + }; + } + + return result; + } + + async getAvailableContextFields( + scopeNode: EditorNodeType, + node: EditorNodeType, + region?: EditorNodeType + ) { + // // 只有表单项组件可以使用表单组件的数据域 + // if ( + // scopeNode.parent?.type === 'crud2' + // ) { + // return scopeNode.parent.info.plugin.getAvailableContextFields?.( + // scopeNode.parent, + // node, + // region + // ); + // } + } + + panelBodyCreator = (context: BaseEventContext) => { + const isCRUDBody = ['crud', 'crud2'].includes(context.schema.type); + + return getSchemaTpl('tabs', [ + { + title: '属性', + body: [ + getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + { + name: 'source', + type: 'input-text', + label: tipedLabel('数据源', '绑定当前环境变量。'), + hidden: isCRUDBody, + pipeIn: defaultValue('${items}') + }, + + getSchemaTpl('switch', { + name: 'title', + label: '显示标题', + pipeIn: (value: any) => !!value, + pipeOut: (value: any) => { + if (value) { + return { + type: 'container', + body: [ + { + type: 'tpl', + tpl: '表格标题', + inline: false, + style: { + fontSize: 14 + } + } + ] + }; + } + return null; + } + }), + + getSchemaTpl('switch', { + name: 'showHeader', + label: '显示表头', + value: true, + pipeIn: (value: any) => !!value, + pipeOut: (value: any) => !!value + }), + + getSchemaTpl('switch', { + visibleOn: 'this.showHeader !== false', + name: 'sticky', + label: '冻结表头', + pipeIn: defaultValue(false) + }), + + getSchemaTpl('switch', { + name: 'footer', + label: '显示表尾', + pipeIn: (value: any) => !!value, + pipeOut: (value: any) => { + if (value) { + return { + type: 'container', + body: [ + { + type: 'tpl', + tpl: '表格尾部', + inline: false, + style: { + fontSize: 14 + } + } + ] + }; + } + return null; + } + }), + + { + name: 'scroll.y', + label: '内容高度', + type: 'button-group-select', + pipeIn: (v: any) => v != null, + pipeOut: (v: any) => (v ? '' : null), + options: [ + { + label: '适配内容', + value: false + }, + + { + label: '固定', + value: true + } + ] + }, + + { + type: 'input-group', + visibleOn: 'data.scroll && data.scroll.y !== null', + label: '高度值', + body: [ + { + type: 'input-number', + name: 'scroll.y' + }, + { + type: 'tpl', + addOnclassName: 'border-0 bg-none', + tpl: 'px' + } + ] + }, + + { + name: 'scroll.x', + label: tipedLabel( + '内容宽度', + '当列内容过多,超出宽度时,可使用横向滚动方式查看数据。' + ), + type: 'button-group-select', + pipeIn: (v: any) => v != null, + pipeOut: (v: any) => (v ? '' : null), + options: [ + { + label: '适配内容', + value: false + }, + + { + label: '固定', + value: true + } + ] + }, + + { + type: 'input-group', + visibleOn: 'data.scroll && data.scroll.x !== null', + name: 'scroll.x', + label: '宽度值', + body: [ + { + type: 'input-number', + name: 'scroll.x' + }, + { + type: 'tpl', + addOnclassName: 'border-0 bg-none', + tpl: 'px' + } + ] + }, + + { + name: 'placeholder', + pipeIn: defaultValue('暂无数据'), + type: 'input-text', + label: '占位内容' + } + ] + }, + { + title: '列设置', + body: [ + getSchemaTpl('switch', { + name: 'resizable', + label: tipedLabel('可调整列宽', '用户可通过拖拽调整列宽度'), + pipeIn: (value: any) => !!value, + pipeOut: (value: any) => value + }), + isCRUDBody + ? null + : { + type: 'ae-Switch-More', + mode: 'normal', + name: 'columnsTogglable', + hiddenOnDefault: true, + formType: 'extend', + label: tipedLabel( + '自定义显示列', + '自动即列数量大于10自动开启。' + ), + pipeOut: (value: any) => { + if (value && value.columnsTogglable) { + return {columnsTogglable: {type: 'column-toggler'}}; + } + return value; + }, + form: { + body: [ + { + mode: 'normal', + type: 'ae-columnControl' + } + ] + } + } + ] + }, + + { + title: '行设置', + body: [ + { + name: 'lineHeight', + label: '行高度', + type: 'select', + placeholder: '请选择高度', + options: [ + {label: '跟随内容', value: ''}, + {label: '高', value: 'large'}, + {label: '中', value: 'middle'} + ], + clearable: false, + value: '' + }, + + isCRUDBody + ? { + type: 'ae-Switch-More', + mode: 'normal', + name: 'rowSelection', + label: '可多选', + visibleOn: 'data.selectable', + hiddenOnDefault: true, + formType: 'extend', + form: { + body: [ + { + label: '可选区域', + name: 'rowSelection.rowClick', + type: 'button-group-select', + value: false, + options: [ + { + label: 'CheckBox', + value: false + }, + { + label: '整行', + value: true + } + ] + }, + { + name: 'rowSelection.disableOn', + type: 'ae-formulaControl', + label: '行禁用条件' + }, + { + name: 'rowSelection.selections', + label: '选择菜单项', + type: 'checkboxes', + joinValues: false, + inline: false, + itemClassName: 'text-sm', + options: [ + {label: '全选', value: 'all'}, + {label: '反选', value: 'invert'}, + {label: '取消选择', value: 'none'}, + {label: '选择奇数项', value: 'odd'}, + {label: '选择偶数项', value: 'even'} + ], + pipeIn(v: any) { + if (!v) { + return; + } + return v.map((item: any) => ({ + label: item.text, + value: item.key + })); + }, + pipeOut(v: any) { + if (!v) { + return; + } + return v.map((item: any) => ({ + key: item.value, + text: item.label + })); + } + } + ] + } + } + : null, + + { + type: 'ae-Switch-More', + mode: 'normal', + name: 'expandable', + label: '可展开', + hiddenOnDefault: true, + formType: 'extend', + form: { + body: [ + { + name: 'expandable.expandableOn', + visibleOn: 'data.expandable', + type: 'ae-formulaControl', + label: '行展开条件' + } + ] + } + }, + + getSchemaTpl('switch', { + name: 'childrenColumnName', + label: '可嵌套', + pipeIn: (value: any) => !!value, + pipeOut: (value: any) => (value ? 'children' : '') + }), + + getSchemaTpl('switch', { + name: 'draggable', + label: '可拖拽', + pipeIn: (value: any) => !!value, + pipeOut: (value: any) => value + }) + ] + }, + + { + title: '状态', + body: [ + getSchemaTpl('hidden', { + label: '隐藏' + }) + ] + }, + + { + title: '高级', + body: [ + getSchemaTpl('apiControl', { + label: '快速保存', + name: 'quickSaveApi' + }), + + getSchemaTpl('apiControl', { + label: '快速保存单条', + name: 'quickSaveItemApi' + }) + ] + } + ]) + ] + }, + { + title: '外观', + body: [ + getSchemaTpl('collapseGroup', [ + { + title: '基本', + body: [ + getSchemaTpl('switch', { + name: 'bordered', + label: '边框', + pipeIn: defaultValue(false) + }), + + { + name: 'scroll.x', + type: 'input-number', + label: '横向滚动' + }, + + { + name: 'indentSize', + visibleOn: 'data.childrenColumnName', + type: 'input-number', + unitOptions: [{label: 'px', value: 'px'}], + label: '嵌套缩进' + }, + + { + name: 'rowSelection.columnWidth', + visibleOn: 'data.rowSelection', + type: 'input-number', + label: '选择列宽度', + description: '固定选择列的宽度' + }, + + { + name: 'expandable.columnWidth', + visibleOn: 'data.expandable', + type: 'input-number', + label: '展开列宽度', + description: '固定展开列的宽度' + } + ] + }, + + getSchemaTpl('style:classNames', { + isFormItem: true, + schema: [ + { + name: 'rowClassNameExpr', + type: 'ae-formulaControl', + label: '自定义行样式' + }, + + { + name: 'expandable.expandedRowClassNameExpr', + visibleOn: 'data.expandable', + type: 'ae-formulaControl', + label: '展开行样式' + } + ] + }) + ]) + ] + }, + { + title: '事件', + body: [ + getSchemaTpl('eventControl', { + name: 'onEvent', + ...getEventControlConfig(this.manager, context) + }) + ] + } + ]); + }; + + filterProps(props: any) { + const arr = Array.isArray(props.value) + ? props.value + : typeof props.source === 'string' + ? resolveVariable(props.source, props.data) + : resolveVariable('items', props.data); + + if (!Array.isArray(arr) || !arr.length) { + const mockedData: any = {}; + + if (Array.isArray(props.columns)) { + props.columns.forEach((column: any) => { + if (column.key) { + setVariable(mockedData, column.key, mockValue(column)); + } + }); + } + + props.value = repeatArray(mockedData, 10).map((item, index) => ({ + ...item, + id: index + 1 + })); + } else { + // 只取10条预览,否则太多卡顿 + props.value = arr.slice(0, 10); + } + + // 如果设置了可展开 默认把第一行展开 + if (props.expandable) { + if (typeof props.expandable === 'boolean') { + props.expandable = {}; + } + if (!props.expandable.type) { + props.expandable.type = 'container'; + props.expandable.body = [ + { + type: 'tpl', + tpl: '展开行内容', + inline: false + } + ]; + } + + props.expandable.keyField = 'id'; + props.expandable.expandedRowKeys = [1]; + } + + return props; + } + + // 为了能够自动注入数据。 + getRendererInfo( + context: RendererInfoResolveEventContext + ): BasicRendererInfo | void { + const plugin: PluginInterface = this; + const {schema, renderer} = context; + + if ( + !schema.$$id && + ['crud', 'crud2'].includes(schema.$$editor?.renderer.name) && + renderer.name === 'table-v2' + ) { + return { + ...({id: schema.$$editor.id} as any), + name: plugin.name!, + regions: plugin.regions, + patchContainers: plugin.patchContainers, + vRendererConfig: plugin.vRendererConfig, + wrapperProps: plugin.wrapperProps, + wrapperResolve: plugin.wrapperResolve, + filterProps: plugin.filterProps, + $schema: plugin.$schema, + renderRenderer: plugin.renderRenderer + }; + } + return super.getRendererInfo(context); + } + + // 自动插入 label + beforeInsert(event: PluginEvent) { + const context = event.context; + + if ( + (context.info.plugin === this || + context.node.sameIdChild?.info.plugin === this) && + context.region === 'columns' + ) { + context.data = { + ...context.data, + title: context.data.label ?? context.subRenderer?.name ?? '列名称' + }; + } + } +} + +registerEditorPlugin(TableV2Plugin); diff --git a/packages/amis-editor/src/plugin/TableCell-v2.tsx b/packages/amis-editor/src/plugin/TableCell-v2.tsx new file mode 100644 index 000000000..d06b38888 --- /dev/null +++ b/packages/amis-editor/src/plugin/TableCell-v2.tsx @@ -0,0 +1,588 @@ +import {Button, Icon} from 'amis'; +import React from 'react'; +import {FormItemProps, getVariable} from 'amis-core'; + +import { + BasePlugin, + BasicRendererInfo, + registerEditorPlugin, + RendererInfoResolveEventContext, + ReplaceEventContext, + PluginEvent, + AfterBuildPanelBody, + defaultValue, + getSchemaTpl, + tipedLabel, + DSField +} from 'amis-editor-core'; +import {fromPairs} from 'lodash'; +import {TabsSchema} from 'amis/lib/renderers/Tabs'; +import {SchemaObject} from 'amis/lib/Schema'; +import {remarkTpl} from '../component/BaseControl'; + +export class TableCellV2Plugin extends BasePlugin { + panelTitle = '列配置'; + panelIcon = 'fa fa-columns'; + + afterBuildPanelBody(event: PluginEvent) { + const {context, data} = event.context; + if ( + !context.node.parent?.parent?.type || + context.node.parent.parent.type !== 'table-v2' + ) { + return; + } + + // @ts-ignore + const base: Array<{ + sameName?: string; + [propName: string]: any; + }> = [ + context.node.info.plugin.withDataSource === false + ? false + : { + sameName: context.info.renderer.isFormItem ? 'name' : undefined, + name: 'key', + type: 'ae-DataBindingControl', + label: '列字段', + onBindingChange( + field: DSField, + onBulkChange: (value: any) => void + ) { + const schema = field?.resolveColumnSchema?.('List') || { + title: field.label + }; + onBulkChange(schema); + } + }, + + { + sameName: context.info.renderer.isFormItem ? 'label' : undefined, + name: 'title', + label: '列标题', + type: 'input-text' + }, + + remarkTpl({ + name: 'remark', + label: '标题提示', + labelRemark: '在标题旁展示提示' + }), + + { + name: 'placeholder', + type: 'input-text', + label: tipedLabel('占位提示', '当没有值时用这个来替代展示。'), + value: '-' + } + ].filter(Boolean); + const advanced = [ + getSchemaTpl('switch', { + name: 'sorter', + label: tipedLabel( + '可排序', + '开启后可以根据当前列排序,接口类型将增加排序参数。' + ) + }), + + getSchemaTpl('switch', { + name: 'searchable', + label: '可搜索', + pipeIn: (value: any) => !!value + }), + + { + visibleOn: 'data.searchable', + name: 'searchable', + asFormItem: true, + label: false, + children: ({value, onChange, data}: any) => { + if (value === true) { + value = {}; + } else if (typeof value === 'undefined') { + value = getVariable(data, 'searchable'); + } + + const originMode = value.mode; + + value = { + ...value, + type: 'form', + mode: 'normal', + wrapWithPanel: false, + body: [ + { + type: 'input-text', + name: data.key + } + ] + }; + + delete value.mode; + // todo 多个快速编辑表单模式看来只能代码模式编辑了。 + return ( + + ); + } + }, + + { + name: 'quickEdit', + label: tipedLabel('快速编辑', '输入框左侧或右侧的附加挂件'), + type: 'ae-switch-more', + mode: 'normal', + formType: 'extend', + bulk: true, + defaultData: { + mode: 'popOver' + }, + form: { + body: [ + { + name: 'quickEdit.mode', + type: 'button-group-select', + label: '模式', + value: 'popOver', + options: [ + { + label: '下拉', + value: 'popOver' + }, + { + label: '内嵌', + value: 'inline' + } + ] + }, + + getSchemaTpl('switch', { + name: 'quickEdit.saveImmediately', + label: tipedLabel( + '修改立即保存', + '开启后修改即提交,而不是批量提交。' + ), + pipeIn: (value: any) => !!value + }), + + { + name: 'quickEdit', + asFormItem: true, + label: false, + children: ({value, onBulkChange, name, data}: any) => { + if (value === true) { + value = {}; + } else if (typeof value === 'undefined') { + value = getVariable(data, 'quickEdit'); + } + + const originMode = value?.mode || 'popOver'; + + value = { + ...value, + type: 'form', + mode: 'normal', + wrapWithPanel: false, + body: [ + { + type: 'input-text', + name: data.key + } + ] + }; + + if (value.mode) { + delete value.mode; + } + // todo 多个快速编辑表单模式看来只能代码模式编辑了。 + return ( + + ); + } + } + ] + } + }, + + { + name: 'popOver', + label: '查看更多', + type: 'ae-switch-more', + mode: 'normal', + formType: 'extend', + bulk: true, + form: { + body: [ + { + name: 'popOver.mode', + label: '模式', + type: 'button-group-select', + pipeIn: defaultValue('popOver'), + options: [ + { + label: '浮窗', + value: 'popOver' + }, + + { + label: '弹框', + value: 'dialog' + }, + + { + label: '抽屉', + value: 'drawer' + } + ] + }, + + { + name: 'popOver.position', + label: '浮窗位置', + type: 'select', + visibleOn: 'data.popOver.mode === "popOver"', + pipeIn: defaultValue('center'), + options: [ + { + label: '目标中部', + value: 'center' + }, + + { + label: '目标左上角', + value: 'left-top' + }, + + { + label: '目标右上角', + value: 'right-top' + }, + + { + label: '目标左下角', + value: 'left-bottom' + }, + + { + label: '目标右下角', + value: 'right-bottom' + }, + + { + label: '页面左上角', + value: 'fixed-left-top' + }, + + { + label: '页面右上角', + value: 'fixed-right-top' + }, + + { + label: '页面左下角', + value: 'fixed-left-bottom' + }, + + { + label: '页面右下角', + value: 'fixed-right-bottom' + } + ] + }, + + { + name: 'popOver', + asFormItem: true, + label: false, + children: ({value, onBulkChange, name}: any) => { + value = { + type: 'panel', + title: '查看详情', + body: '内容详情', + ...value + }; + + return ( + + ); + } + } + ] + } + }, + + { + name: 'copyable', + label: tipedLabel('复制内容', '默认为当前字段值,可定制。'), + type: 'ae-switch-more', + mode: 'normal', + formType: 'extend', + bulk: true, + defaultData: {}, + form: { + body: [ + { + name: 'copyable.content', + type: 'textarea', + placehoder: '默认为当前字段的值', + label: '内容模板' + } + ] + } + }, + + { + name: 'rowSpanExpr', + type: 'ae-formulaControl', + label: '合并行' + }, + { + name: 'colSpanExpr', + type: 'ae-formulaControl', + label: '合并列' + } + ]; + + const baseStyle = [ + getSchemaTpl('withUnit', { + name: 'width', + label: tipedLabel('列宽', '固定列的宽度,不推荐设置。'), + control: { + name: 'width', + type: 'input-number' + }, + unit: 'px' + }), + + { + name: 'fixed', + type: 'button-group-select', + label: '固定位置', + pipeIn: defaultValue(''), + pipeOut(value: any) { + if (!value) { + return undefined; + } + return value; + }, + options: [ + { + value: '', + label: '不固定' + }, + + { + value: 'left', + label: '左侧' + }, + + { + value: 'right', + label: '右侧' + } + ] + }, + + getSchemaTpl('switch', { + name: 'toggled', + label: '自定义列时默认展示', + pipeIn: defaultValue(true) + }), + + getSchemaTpl('switch', { + name: 'className', + label: '内容超出换行', + pipeIn: (value: any) => + typeof value === 'string' && /\word\-break\b/.test(value), + pipeOut: (value: any, originValue: any) => + (value ? 'word-break ' : '') + + (originValue || '').replace(/\bword\-break\b/g, '').trim() + }) + ]; + + // 之前的面板,不是新的组件面板,需要添加新的tab,不能合并 + if (Array.isArray(data)) { + if ((data[0] as SchemaObject).type === 'tabs') { + const body = data[0] as TabsSchema; + body.tabs.forEach((tab: any) => { + if (tab.title === '常规') { + tab.body.unshift(...base.concat(advanced)); + } + + if (tab.title === '外观') { + tab.body.unshift(...baseStyle); + } + }); + } else { + console.error('错误的组件合并对象,面板过老无法处理,除非增加新面板'); + } + return; + } + + (data as TabsSchema).tabs?.forEach((tab: any) => { + if (tab.title === '属性') { + tab.body[0].body.forEach((collapse: any) => { + if (collapse.title === '基本') { + const appendItems = fromPairs( + base.map(item => [item.sameName ?? item.name, item]) + ); + + const removeIndex: number[] = []; + collapse.body.forEach((item: any, index: number) => { + const key = item.name; + + // 重复意义的配置用现在的表达文案替换一下 + if (appendItems.hasOwnProperty(key)) { + removeIndex.push(index); + appendItems[key] = { + ...item, + ...appendItems[key] + }; + return; + } + + if (item.name === 'labelRemark') { + removeIndex.push(index); + } + }); + + removeIndex.reverse(); + removeIndex.forEach(index => { + collapse.body.splice(index, 1); + }); + + collapse.body.unshift(...Object.values(appendItems)); + } + }); + + const moreCollapse = getSchemaTpl('collapseGroup', [ + { + title: '列', + body: advanced + } + ]); + tab.body[0].body.splice(1, 0, ...moreCollapse.body); + // 让折叠器默认都展开 + tab.body[0].activeKey.push(...moreCollapse.activeKey); + } + + if (tab.title === '外观') { + const moreCollapse = getSchemaTpl('collapseGroup', [ + { + title: '列', + body: baseStyle + } + ]); + tab.body[0].body.splice(1, 0, ...moreCollapse.body); + // 让折叠器默认都展开 + tab.body[0].activeKey.push(...moreCollapse.activeKey); + } + }); + } + + // filterProps(props: any) { + // props = JSONPipeOut(props, true); + // return props; + // } + + getRendererInfo( + context: RendererInfoResolveEventContext + ): BasicRendererInfo | void { + const {renderer, schema} = context; + + if (renderer.name === 'cell-field') { + return { + name: schema.title ? `<${schema.title}>列` : '匿名列', + $schema: '/schemas/TableColumn.json', + multifactor: true, + wrapperResolve: (dom: HTMLDivElement) => { + // 固定这种结构 amis里改了 这里也得改 + const parent = dom.parentElement?.parentElement; + const groupId = parent?.getAttribute('data-group-id'); + const wrapper = dom.closest('table')!.parentElement?.parentElement; + + return [].slice.call( + wrapper?.querySelectorAll( + `th[data-group-id="${groupId}"], + td[data-group-id="${groupId}"]` + ) + ); + } + // filterProps: this.filterProps + }; + } + } + + /*exchangeRenderer(id: string) { + this.manager.showReplacePanel(id, '展示'); + }*/ + + beforeReplace(event: PluginEvent) { + const context = event.context; + + // 替换字段的时候保留 label 和 name 值。 + if (context.info.plugin === this && context.data) { + context.data.title = context.data.title || context.schema.title; + context.data.key = context.data.key || context.schema.key; + } + } +} + +registerEditorPlugin(TableCellV2Plugin); diff --git a/packages/amis-editor/src/plugin/Tabs.tsx b/packages/amis-editor/src/plugin/Tabs.tsx index 612544755..0d244365e 100644 --- a/packages/amis-editor/src/plugin/Tabs.tsx +++ b/packages/amis-editor/src/plugin/Tabs.tsx @@ -16,10 +16,10 @@ import {VRenderer} from 'amis-editor-core'; import findIndex from 'lodash/findIndex'; import {RegionWrapper as Region} from 'amis-editor-core'; import {Tab} from 'amis'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import {ValidatorTag} from '../validator'; import {getEventControlConfig} from '../util'; -import { getArgsWrapper } from '../renderer/event-control/helper'; +import {getArgsWrapper} from '../renderer/event-control/helper'; export class TabsPlugin extends BasePlugin { // 关联渲染器名字 diff --git a/packages/amis-editor/src/plugin/TooltipWrapper.tsx b/packages/amis-editor/src/plugin/TooltipWrapper.tsx index e70acd22f..5e7c11715 100644 --- a/packages/amis-editor/src/plugin/TooltipWrapper.tsx +++ b/packages/amis-editor/src/plugin/TooltipWrapper.tsx @@ -5,7 +5,7 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {BasePlugin, RegionConfig, BaseEventContext} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; export class TooltipWrapperPlugin extends BasePlugin { rendererName = 'tooltip-wrapper'; diff --git a/packages/amis-editor/src/plugin/Tpl.tsx b/packages/amis-editor/src/plugin/Tpl.tsx index 07fef9831..17ca0e3fe 100644 --- a/packages/amis-editor/src/plugin/Tpl.tsx +++ b/packages/amis-editor/src/plugin/Tpl.tsx @@ -1,7 +1,7 @@ import {registerEditorPlugin} from 'amis-editor-core'; import {BaseEventContext, BasePlugin} from 'amis-editor-core'; import {defaultValue, getSchemaTpl, setSchemaTpl} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import {ValidatorTag} from '../validator'; import {getEventControlConfig} from '../util'; @@ -146,7 +146,7 @@ export class TplPlugin extends BasePlugin { ), name: 'inline', pipeIn: defaultValue(true), - hiddenOn:'data.wrapperComponent !== ""' + hiddenOn: 'data.wrapperComponent !== ""' }), getSchemaTpl('tpl:content'), getSchemaTpl('tpl:rich-text') diff --git a/packages/amis-editor/src/plugin/WebComponent.tsx b/packages/amis-editor/src/plugin/WebComponent.tsx index 79762c99b..36a7da101 100644 --- a/packages/amis-editor/src/plugin/WebComponent.tsx +++ b/packages/amis-editor/src/plugin/WebComponent.tsx @@ -5,7 +5,7 @@ import { getSchemaTpl } from 'amis-editor-core'; import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; // 需要一个示例,不然默认的没有高度都无法选中 class WebComponentDemo extends HTMLElement { @@ -67,7 +67,7 @@ export class WebComponentPlugin extends BasePlugin { mode: 'normal', name: 'props', label: '属性' - }), + }) ] } ]) diff --git a/packages/amis-editor/src/renderer/APIControl.tsx b/packages/amis-editor/src/renderer/APIControl.tsx index 77b48222a..dc9e71ba3 100644 --- a/packages/amis-editor/src/renderer/APIControl.tsx +++ b/packages/amis-editor/src/renderer/APIControl.tsx @@ -1,26 +1,43 @@ import React from 'react'; import merge from 'lodash/merge'; +import isEqual from 'lodash/isEqual'; import cloneDeep from 'lodash/cloneDeep'; import cx from 'classnames'; -import {FormItem, InputBox, Icon} from 'amis'; -import {} from 'amis-ui'; -import {PickerContainer} from 'amis'; +import {FormItem, Icon} from 'amis'; +import {Input, PickerContainer, Spinner} from 'amis-ui'; import {getEnv} from 'mobx-state-tree'; import {normalizeApi, isEffectiveApi, isApiOutdated} from 'amis-core'; -import {isObject, autobind, createObject, anyChanged} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {isObject, autobind, createObject, tipedLabel} from 'amis-editor-core'; import type {SchemaObject, SchemaCollection, SchemaApi} from 'amis/lib/Schema'; import type {Api} from 'amis/lib/types'; import type {FormControlProps} from 'amis-core'; import type {ActionSchema} from 'amis/lib/renderers/Action'; +export type ApiObject = Api & { + messages?: Record< + | 'fetchSuccess' + | 'fetchFailed' + | 'saveOrderSuccess' + | 'saveOrderFailed' + | 'quickSaveSuccess' + | 'quickSaveFailed', + string + >; +}; + export interface APIControlProps extends FormControlProps { name?: string; label?: string; value?: any; + + /** + * 开启debug模式 + */ + debug?: boolean; + /** * 接口消息设置描述信息 */ @@ -76,6 +93,36 @@ export interface APIControlProps extends FormControlProps { */ pickerHeaderClassName?: string; + /** + * 是否只返回内部TabsPanel + */ + onlyTabs?: boolean; + + /** + * 开启高亮显示 + */ + enableHighlight?: boolean; + + /** + * Picker选项的label字段 + */ + labelField?: string; + + /** + * 检索字段 + */ + searchField?: string; + + /** + * 检索字段类型 + */ + searchType?: string; + + /** + * 底部区域CSS类名 + */ + footerClassName?: string; + /** * Picker面板确认 */ @@ -103,38 +150,52 @@ export interface APIControlState { apiStr: string; selectedItem?: any[]; schema?: SchemaCollection; + loading: boolean; } export default class APIControl extends React.Component< APIControlProps, APIControlState > { - static defaultProps: Pick = { + input?: HTMLInputElement; + + static defaultProps: Pick< + APIControlProps, + 'pickerBtnSchema' | 'labelField' | 'searchType' + > = { pickerBtnSchema: { type: 'button', level: 'link', - size: 'sm', - label: '点击选择' - } + size: 'sm' + }, + labelField: 'label', + searchType: 'key' }; + constructor(props: APIControlProps) { super(props); this.state = { apiStr: this.transformApi2Str(props.value), selectedItem: [], - schema: props.pickerSchema + schema: props.pickerSchema, + loading: false }; } + componentDidMount() { + this.updatePickerOptions(); + } + componentDidUpdate(prevProps: APIControlProps) { const props = this.props; - if (prevProps.value !== props.value) { + if (!isEqual(prevProps?.value, props?.value)) { this.setState({apiStr: this.transformApi2Str(props.value)}); + this.updatePickerOptions(); } - if (anyChanged(['enablePickerMode', 'pickerSchema'], prevProps, props)) { + if (!isEqual(prevProps?.enablePickerMode, props?.enablePickerMode)) { this.setState({schema: props.pickerSchema}); } @@ -150,23 +211,52 @@ export default class APIControl extends React.Component< } } + /** + * 已选API详情,因为list接口是分页的,所以需要单独调用一次 + */ + async updatePickerOptions() { + const apiObj = normalizeApi(this.props.value); + + if (apiObj?.url?.startsWith('api://')) { + this.setState({loading: true}); + const keyword = apiObj.url.replace('api://', ''); + + try { + await this.fetchOptions(keyword); + } catch (error) {} + } + this.setState({loading: false}); + } + transformApi2Str(value: any) { const api = normalizeApi(value); - return api.url ? `${api.method ? `${api.method}:` : ''}${api.url}` : ''; + return api.url + ? `${ + api.method && + api.method.toLowerCase() !== + 'get' /** 默认为GET请求,直接隐藏掉前缀,为了呈现更多信息 */ + ? `${api.method}:` + : '' + }${api.url}` + : ''; } - async fetchOptions() { - const {value, data, env} = this.props; + async fetchOptions(keyword?: string) { + const {value, data, env, searchField, searchType} = this.props; let {pickerSource} = this.props; const apiObj = normalizeApi(value); - const apiKey = apiObj?.url.split('api://')?.[1]; - if (!pickerSource) { + if (!pickerSource || !apiObj?.url) { return; } - const ctx = createObject(data, {value, op: 'loadOptions'}); + const apiKey = apiObj?.url?.split('api://')?.[1]; + const ctx = createObject(data, { + value, + op: 'loadOptions', + ...(keyword && searchField ? {[searchField]: keyword, searchType} : {}) + }); const schemaFilter = getEnv((window as any).editorStore).schemaFilter; // 基于爱速搭的规则转换一下 @@ -177,6 +267,7 @@ export default class APIControl extends React.Component< if (isEffectiveApi(pickerSource, ctx)) { const res = await env.fetcher(pickerSource, ctx); const items: any[] = res.data?.items || res?.data?.rows; + if (items.length) { const selectedItem = items.find(item => item.key === apiKey); @@ -186,7 +277,40 @@ export default class APIControl extends React.Component< } @autobind - handleSubmit(values: SchemaApi, action: any) { + inputRef(ref: any) { + this.input = ref; + } + + focus() { + if (!this.input) { + return; + } + + this.input.focus(); + } + + @autobind + clearPickerValue() { + const {onChange} = this.props; + + this.setState( + {apiStr: this.transformApi2Str(undefined), selectedItem: []}, + () => { + onChange?.(undefined); + this.focus(); + } + ); + } + + @autobind + handleSimpleInputChange(e: React.ChangeEvent) { + const value = e.currentTarget.value; + + this.handleSubmit(value, 'input'); + } + + @autobind + handleSubmit(values: SchemaApi, action?: 'input' | 'picker-submit') { const {onChange, value} = this.props; let api: Api = values; @@ -241,7 +365,7 @@ export default class APIControl extends React.Component< } renderHeader() { - const {render, actions, enablePickerMode} = this.props; + const {render, actions} = this.props; const actionsDom = Array.isArray(actions) && actions.length > 0 @@ -253,9 +377,9 @@ export default class APIControl extends React.Component< }) : null; - return actionsDom || enablePickerMode ? ( + return actionsDom ? (
- {enablePickerMode ? this.renderPickerSchema() : actionsDom} + {actionsDom}
) : null; } @@ -280,14 +404,19 @@ export default class APIControl extends React.Component< return ( { + bodyRender={({ + onChange, + setState + }: { + onChange: (value: any) => void; + setState: (state: any) => void; + }) => { return render('api-control-picker', schema!, { - data: {[pickerName]: selectedItem}, + value: selectedItem, onSelect: (items: Array) => { setState({selectedItem: items}); onChange(this.normalizeValue(items, onPickerSelect)); @@ -295,32 +424,50 @@ export default class APIControl extends React.Component< }); }} > - {({onClick, isOpened}) => - render('picker-action', pickerBtnSchema!, { - onClick: async (e: React.MouseEvent) => { - if (!isOpened && enablePickerMode) { - try { - await this.fetchOptions(); - } catch {} - } + {({ + onClick, + isOpened + }: { + onClick: (e: React.MouseEvent) => void; + isOpened: boolean; + }) => + render( + 'picker-action', + { + icon: ( + + ), + ...pickerBtnSchema!, + className: cx( + 'ae-ApiControl-PickerBtn', + pickerBtnSchema?.className + ) + }, + { + onClick: async (e: React.MouseEvent) => { + if (!isOpened && enablePickerMode) { + try { + await this.fetchOptions(); + } catch {} + } - onClick(e); + onClick(e); + } } - }) + ) } ); } renderApiDialog() { - const {messageDesc} = this.props; - return { label: '', type: 'action', acitonType: 'dialog', size: 'sm', - icon: , + icon: , + className: 'ae-ApiControl-setting-button', actionType: 'dialog', dialog: { title: '高级设置', @@ -331,12 +478,14 @@ export default class APIControl extends React.Component< closeOnEsc: true, closeOnOutside: false, showCloseButton: true, - body: [this.renderApiConfigTabs(messageDesc)] + body: [this.renderApiConfigTabs()] } }; } - renderApiConfigTabs(messageDesc?: string, submitOnChange: boolean = false) { + renderApiConfigTabs(submitOnChange: boolean = false) { + const {messageDesc, debug = false} = this.props; + return { type: 'form', className: 'ae-ApiControl-form', @@ -344,6 +493,7 @@ export default class APIControl extends React.Component< submitOnChange, wrapWithPanel: false, onSubmit: this.handleSubmit, + debug, body: [ { type: 'tabs', @@ -462,75 +612,6 @@ export default class APIControl extends React.Component< mode: 'horizontal', description: '默认数据为追加方式,开启后完全替换当前数据' }, - { - label: tipedLabel( - '初始加载', - '当配置初始化接口后,组件初始就会拉取接口数据,可以通过以下配置修改' - ), - type: 'group', - visibleOn: 'this.initApi', - mode: 'horizontal', - direction: 'vertical', - // labelRemark: { - // trigger: 'hover', - // rootClose: true, - // content: - // '当配置初始化接口后,组件初始就会拉取接口数据,可以通过以下配置修改', - // placement: 'top' - // }, - - body: [ - { - name: 'initFetch', - type: 'radios', - inline: true, - mode: 'normal', - renderLabel: false, - onChange: () => { - document.getElementsByClassName( - 'ae-Settings-content' - )[0].scrollTop = 0; - }, - // pipeIn: (value:any) => typeof value === 'boolean' ? value : '1' - options: [ - { - label: '是', - value: true - }, - - { - label: '否', - value: false - }, - - { - label: '表达式', - value: '' - } - ] - }, - { - name: 'initFetchOn', - autoComplete: false, - visibleOn: 'typeof this.initFetch !== "boolean"', - type: 'input-text', - mode: 'normal', - size: 'lg', - renderLabel: false, - placeholder: '如:this.id 表示有 id 值时初始加载', - className: 'm-t-n-sm' - } - ] - }, - { - label: '定时刷新', - name: 'interval', - type: 'switch', - mode: 'horizontal', - visibleOn: 'data.initApi', - pipeIn: (value: any) => !!value, - pipeOut: (value: any) => (value ? 3000 : undefined) - }, { label: '', name: 'interval', @@ -563,13 +644,6 @@ export default class APIControl extends React.Component< size: 'lg', visibleOn: '!!data.interval', placeholder: '停止定时刷新检测表达式' - // labelRemark: { - // trigger: 'hover', - // rootClose: true, - // content: - // '定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则停止刷新', - // placement: 'top' - // } } ] }, @@ -589,13 +663,6 @@ export default class APIControl extends React.Component< ), name: 'data', mode: 'row', - // labelRemark: { - // trigger: 'hover', - // rootClose: true, - // content: - // '当没开启数据映射时,发送 API 的时候会发送尽可能多的数据,如果你想自己控制发送的数据,或者需要额外的数据处理,请开启此选项', - // placement: 'top' - // }, pipeIn: (value: any) => !!value, pipeOut: (value: any) => (value ? {'&': '$$'} : null) }, @@ -656,7 +723,7 @@ export default class APIControl extends React.Component< { placeholder: 'Value', - type: 'input-text', + type: 'ae-DataPickerControl', name: 'value' } ] @@ -669,13 +736,6 @@ export default class APIControl extends React.Component< ), name: 'responseData', mode: 'row', - // labelRemark: { - // trigger: 'hover', - // rootClose: true, - // content: - // '如果需要对返回结果做额外的数据处理,请开启此选项', - // placement: 'top' - // }, pipeIn: (value: any) => !!value, pipeOut: (value: any) => (value ? {'&': '$$'} : null) }, @@ -772,18 +832,11 @@ export default class APIControl extends React.Component< type: 'switch', label: tipedLabel( '请求头', - '可以配置headers对象,添加自定义请求头' + '可以配置headers对象,添加自定义请求头' ), name: 'headers', mode: 'row', className: 'm-b-xs', - // labelRemark: { - // trigger: 'hover', - // rootClose: true, - // content: - // '可以配置headers对象,添加自定义请求头', - // placement: 'top' - // }, pipeIn: (value: any) => !!value, pipeOut: (value: any) => (value ? {'': ''} : null) }, @@ -915,42 +968,103 @@ export default class APIControl extends React.Component< } ] }; - // return } render() { const { render, className, + footerClassName, + classPrefix, + label, + labelRemark, value, footer, border = false, - messageDesc + onlyTabs = false, + messageDesc, + enablePickerMode, + disabled, + mode, + enableHighlight, + labelField = 'label', + useMobileUI, + popOverContainer, + env } = this.props; + let {apiStr, selectedItem, loading} = this.state; + selectedItem = + Array.isArray(selectedItem) && selectedItem.length !== 0 + ? selectedItem + : []; + const highlightLabel = selectedItem?.[0]?.[labelField] ?? ''; return ( -
- {this.renderHeader()} + <> +
+ {onlyTabs ? ( + render('api-control-tabs', this.renderApiConfigTabs(true), { + data: normalizeApi(value) + }) + ) : ( + <> + {this.renderHeader()} -
- this.handleSubmit(value, 'input')} - /> - {render('api-control-dialog', this.renderApiDialog(), { - data: normalizeApi(value) - })} +
+
+ {enableHighlight && highlightLabel ? ( +
+ {loading ? ( + + ) : ( + + {highlightLabel} + + + + + )} +
+ ) : ( + + )} + {enablePickerMode ? this.renderPickerSchema() : null} +
+ + {render('api-control-dialog', this.renderApiDialog(), { + data: normalizeApi(value) + })} +
+ + )}
- {Array.isArray(footer) && footer.length !== 0 ? ( -
+
{render('api-control-footer', footer)}
) : null} -
+ ); } } diff --git a/packages/amis-editor/src/renderer/ActionApiControl.tsx b/packages/amis-editor/src/renderer/ActionApiControl.tsx index bd4c262cd..3bbf1c846 100644 --- a/packages/amis-editor/src/renderer/ActionApiControl.tsx +++ b/packages/amis-editor/src/renderer/ActionApiControl.tsx @@ -9,7 +9,7 @@ import {getEnv} from 'mobx-state-tree'; import {normalizeApi, isEffectiveApi, isApiOutdated} from 'amis-core'; import {autobind, isObject, anyChanged, createObject} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import type {SchemaObject, SchemaCollection, SchemaApi} from 'amis/lib/Schema'; import type {Api} from 'amis/lib/types'; diff --git a/packages/amis-editor/src/renderer/ColumnControl.tsx b/packages/amis-editor/src/renderer/ColumnControl.tsx new file mode 100644 index 000000000..50450e794 --- /dev/null +++ b/packages/amis-editor/src/renderer/ColumnControl.tsx @@ -0,0 +1,73 @@ +/** + * @file 表格自定义列可视化编辑控件 + */ + +import React from 'react'; +import cx from 'classnames'; +import findIndex from 'lodash/findIndex'; +import { + FormControlProps, + FormItem, + TreeSelection +} from 'amis'; + + +export interface ColumnControlProps extends FormControlProps { + className?: string; +} + +export interface ColumnsControlState { + columns: Array; +} + +export default class ColumnControl extends React.Component< + ColumnControlProps, + ColumnsControlState +> { + + constructor(props: any) { + super(props); + + this.state = { + columns: this.transformColumns(props) + }; + } + + transformColumns(props: any) { + const {data} = props; + return data.columns; + } + + onChange(value: Array) { + const {onBulkChange} = this.props; + const columns = this.state.columns.map(c => ({ + ...c, + toggled: findIndex(value, (v: any) => v.value === c.key) > -1 + })); + + this.setState({columns}); + onBulkChange && onBulkChange({columns}); + } + + render() { + const {columns} = this.state; + const options = columns ? columns.map(c => ({value: c.key, label: c.title})) : []; + const value = columns ? columns.filter(c => c.toggled !== false).map(c => ({value: c.key, label: c.title})) : [] + + return ( +
+ ) => this.onChange(v)}> + +
+ ); + } +} + +@FormItem({ + type: 'ae-columnControl', + renderLabel: false +}) +export class ColumnControlRenderer extends ColumnControl {} diff --git a/packages/amis-editor/src/renderer/DataBindingControl.tsx b/packages/amis-editor/src/renderer/DataBindingControl.tsx index d530dedeb..b6d5ecd09 100644 --- a/packages/amis-editor/src/renderer/DataBindingControl.tsx +++ b/packages/amis-editor/src/renderer/DataBindingControl.tsx @@ -1,37 +1,244 @@ -import {Icon, InputBox, SchemaVariableListPicker, FormItem} from 'amis'; -import type {FormControlProps} from 'amis-core'; +import { + Icon, + InputBox, + SchemaVariableListPicker, + FormItem, + SearchBox, + CollapseGroup, + PickerContainer, + Collapse, + Checkbox, + Spinner +} from 'amis'; +import {FormControlProps, generateIcon} from 'amis-core'; +import {debounce, remove} from 'lodash'; import React from 'react'; -import {autobind} from 'amis-editor-core'; +import { + EditorManager, + EditorNodeType, + autobind, + DSField, + DSFieldGroup +} from 'amis-editor-core'; +import {matchSorter} from 'match-sorter'; + +export interface DataBindingProps extends FormControlProps { + node: EditorNodeType; + manager: EditorManager; + samePredicate?: (a: any, b: any) => boolean; + onBindingChange?: ( + value: DSField, + onBulkChange: (value: any) => void + ) => void; +} + +export interface DataBindingState { + filteredFields: DSFieldGroup[]; + sourceFields: DSFieldGroup[]; + loading: boolean; + hint: string | void; +} + +export class DataBindingControl extends React.Component< + DataBindingProps, + DataBindingState +> { + constructor(props: DataBindingProps) { + super(props); + this.handleSearchDebounced = debounce(this.handleSearch, 250, { + trailing: true, + leading: false + }); + this.state = { + filteredFields: [], + sourceFields: [], + loading: false, + hint: undefined + }; + } + + handleSearchDebounced; -export class DataBindingControl extends React.Component { @autobind - handleConfirm(result: {value: string; schema: any}) { - const {manager, data} = this.props; + handleConfirm(result: {label: string; value: string}) { + const {manager, data, onChange, onBulkChange, onBindingChange} = this.props; if (result?.value) { - this.props.onChange(`${result.value}`); + onChange(result.value); + onBulkChange && onBindingChange?.(result, onBulkChange); manager.config?.dataBindingChange?.(result.value, data, manager); } } @autobind - async handlePickerOpen() { + handlePickerOpen() { const {manager, node} = this.props; - const withSuper = manager.config?.withSuperDataSchema ?? false; - const schemas = await manager.getContextSchemas(node.info.id, !withSuper); - return {schemas}; + + // 如果node没变化,就不再重复加载 + if (this.state.sourceFields.length) { + return; + } + + this.setState({ + sourceFields: [], + filteredFields: [], + loading: true + }); + + manager + .getAvailableContextFields(node) + .then(groupedFields => { + this.setState({ + sourceFields: groupedFields || [], + filteredFields: groupedFields || [], + loading: false, + hint: groupedFields ? undefined : '暂无可绑定字段' + }); + }) + .catch(() => { + this.setState({ + loading: false, + hint: '加载可用字段失败,请联系管理员!' + }); + }); } - render() { - const {classnames: cx, value, onChange, disabled} = this.props; + @autobind + async handleSearch(keywords: string) { + this.setState({ + filteredFields: matchSorter(this.state.sourceFields, keywords, { + keys: ['label', 'value', 'children'] + }) + }); + } + @autobind + handleSelect() {} + + render() { + const { + className, + classnames: cx, + value, + onChange, + samePredicate = (a, b) => a.value === b.value, + multiple, + disabled + } = this.props; + + const {filteredFields, loading, hint} = this.state; return ( - { + if (!isOpened) { + return null; + } + + if (loading) { + return ( + + ); + } + + if (hint) { + return

{hint}

; + } + + return ( +
+
+ +
+ +
+ item.value!)} + expandIcon={ + generateIcon( + cx, + 'fa fa-chevron-right ae-DataBindingList-expandIcon', + 'Icon' + )! + } + expandIconPosition="right" + // accordion={true} + > + {filteredFields.map(item => ( + + {Array.isArray(item.children) && + item.children.length > 0 ? ( + item.children.map((childItem: DSField) => { + if (multiple) { + const checked = !!value.find((i: any) => + samePredicate(i, childItem) + ); + return ( +
+ onChange( + checked + ? value.concat(childItem) + : remove(value, childItem) + ) + } + > + + {childItem.label} + +
+ ); + } + + return ( +
onChange(childItem)} + key={childItem.value} + > + {childItem.label} +
+ ); + }) + ) : ( +

+ 暂无可用字段 +

+ )} +
+ ))} +
+
+
+ ); + }} + value={value} onConfirm={this.handleConfirm} - title="绑定变量" > - {({onClick, isOpened, setState}) => { + {({onClick}: {onClick: (e: React.MouseEvent) => void}) => { return ( { ); }} -
+ ); } } diff --git a/packages/amis-editor/src/renderer/DataPickerControl.tsx b/packages/amis-editor/src/renderer/DataPickerControl.tsx index a515ed3fa..1fb1585f7 100644 --- a/packages/amis-editor/src/renderer/DataPickerControl.tsx +++ b/packages/amis-editor/src/renderer/DataPickerControl.tsx @@ -47,7 +47,9 @@ class DataPickerControl extends React.Component { onChange={() => {}} header={''} > - {({onClick, isOpened, setState}) => { + {({onClick}: { + onClick: (e: React.MouseEvent) => void; + }) => { return ( void; + /** 提供该字段表示删除后还可以再新增回来 */ + add?: (data: any) => void; + isActive?: (data: any) => boolean; +}; + +interface FeatureControlProps extends FormControlProps { + className?: string; + removable?: boolean; + addable?: boolean; + addText?: string; + sortable?: boolean; + features: Array | ((schema: any) => Array); + goFeatureComp?: (item: FeatureOption) => string; // 去子组件 + onSort?: (value: FeatureOption[]) => void; +} + +interface FeatureControlState { + /** + * 当前启用的功能 + */ + inUseFeat: FeatureOption[]; + + /** + * 未启用的功能 + */ + unUseFeat: FeatureOption[]; +} + +export default class FeatureControl extends React.Component< + FeatureControlProps, + FeatureControlState +> { + constructor(props: FeatureControlProps) { + super(props); + this.state = FeatureControl.initState(props.data, props.features); + } + + static getDerivedStateFromProps( + nextProps: FeatureControlProps, + preState: FeatureControlState + ) { + return FeatureControl.initState( + nextProps.data, + nextProps.features, + preState.inUseFeat, + preState.unUseFeat + ); + } + + static initState( + data: any, + features: FeatureOption[] | ((schema: any) => Array), + lastInUseFeat?: FeatureOption[], + lastUnUseFeat?: FeatureOption[] + ) { + const inUseFeat: FeatureOption[] = []; + const unUseFeat: FeatureOption[] = []; + + if (!Array.isArray(features)) { + features = features(data); + } + + features.forEach(item => { + if (item.isActive == null || item.isActive?.(data)) { + inUseFeat.push(item); + } else if (item.add) { + unUseFeat.push(item); + } + }); + + return { + inUseFeat, + unUseFeat + }; + } + + @autobind + handleRemove(item: FeatureOption, index: number) { + const {removeFeature, data, onBulkChange} = this.props; + const {inUseFeat, unUseFeat} = this.state; + + item.remove?.(data); + removeFeature?.(item, data); + onBulkChange?.(data); + + remove(inUseFeat, item); + item.add && unUseFeat.push(item); + + this.setState({inUseFeat, unUseFeat}); + } + + @autobind + handleAdd(item: any) { + const {addFeature, data, onBulkChange} = this.props; + const {inUseFeat, unUseFeat} = this.state; + + inUseFeat.push(item); + remove(unUseFeat, item); + + const schema = clone(data); + item.add?.(schema); + addFeature?.(item, schema); + onBulkChange?.(schema); + } + + sortable?: Sortable; + drag?: HTMLElement | null; + @autobind + dragRef(ref: any) { + const {sortable} = this.props; + if (!sortable) { + return; + } + + if (!this.drag && ref) { + this.initDragging(); + } else if (this.drag && !ref) { + this.destroyDragging(); + } + + this.drag = ref; + } + + /** + * 初始化拖动 + */ + initDragging() { + const dom = findDOMNode(this) as HTMLElement; + this.sortable = new Sortable( + dom.querySelector(`.${klass}-features`) as HTMLElement, + { + group: 'FeatureControlGroup', + animation: 150, + handle: `.${klass}Item-dragBar`, + ghostClass: `${klass}Item-dragging`, + onEnd: (e: any) => { + // 没有移动 + if (e.newIndex === e.oldIndex) { + return; + } + // 换回来 + const parent = e.to as HTMLElement; + if ( + e.newIndex < e.oldIndex && + e.oldIndex < parent.childNodes.length - 1 + ) { + parent.insertBefore(e.item, parent.childNodes[e.oldIndex + 1]); + } else if (e.oldIndex < parent.childNodes.length - 1) { + parent.insertBefore(e.item, parent.childNodes[e.oldIndex]); + } else { + parent.appendChild(e.item); + } + + const value = this.state.inUseFeat.concat(); + value[e.oldIndex] = value.splice(e.newIndex, 1, value[e.oldIndex])[0]; + this.setState({inUseFeat: value}, () => { + this.props.onSort?.(value); + }); + } + } + ); + } + + /** + * 拖动的销毁 + */ + destroyDragging() { + this.sortable && this.sortable.destroy(); + } + + renderItem(item: FeatureOption, index: number) { + const {sortable, goFeatureComp, node, manager} = this.props; + + let content = null; + + if (goFeatureComp) { + content = ( + // @ts-ignore + goFeatureComp(item)} + /> + ); + } else { + content =
{item.label}
; + } + + return ( +
  • + {sortable && ( + + + + )} + {content} + +
  • + ); + } + + renderAction() { + const {addable, addText, render} = this.props; + if (!addable) { + return null; + } + + return render('action', { + type: 'dropdown-button', + closeOnClick: true, + label: '添加' || addText, + className: `${klass}-action`, + btnClassName: `${klass}-action--btn`, + menuClassName: `${klass}-action--menus`, + buttons: this.state.unUseFeat.map(item => { + return { + label: item.label, + onClick: () => this.handleAdd(item) + }; + }) + }); + } + + render() { + const {className} = this.props; + + return ( +
    +
      + {this.state.inUseFeat.map((item, index) => + this.renderItem(item, index) + )} +
    + + {this.renderAction()} +
    + ); + } +} + +@FormItem({ + type: 'ae-feature-control' +}) +export class FeatureControlRenderer extends FeatureControl {} diff --git a/packages/amis-editor/src/renderer/FormulaControl.tsx b/packages/amis-editor/src/renderer/FormulaControl.tsx index c56695dfd..552e2ecc3 100644 --- a/packages/amis-editor/src/renderer/FormulaControl.tsx +++ b/packages/amis-editor/src/renderer/FormulaControl.tsx @@ -248,7 +248,14 @@ export default class FormulaControl extends React.Component< const {node, manager} = this.props.formProps || this.props; await manager?.getContextSchemas(node); const dataPropsAsOptions = manager?.dataSchema?.getDataPropsAsOptions(); - return dataPropsAsOptions || []; + + if (dataPropsAsOptions) { + return dataPropsAsOptions.map((item: any) => ({ + selectMode: 'tree', + ...item + })); + } + return []; } @autobind @@ -331,9 +338,7 @@ export default class FormulaControl extends React.Component< @autobind renderFormulaValue(item: any) { const html = {__html: item.html}; - { - /* bca-disable-next-line */ - } + // bca-disable-line return ; } @@ -452,7 +457,13 @@ export default class FormulaControl extends React.Component< )} { + bodyRender={({ + value, + onChange + }: { + onChange: (value: any) => void; + value: any; + }) => { return ( - {({onClick, isOpened}) => ( + {({onClick}: {onClick: (e: React.MouseEvent) => void}) => ( - ) : null} - - {removable ? ( - - ) : null} - - - - {show ? this.renderPopover() : null} -
    - ); - } -} - -@FormItem({ - type: 'popover-edit' -}) -export class PopoverEditRenderer extends PopoverEdit {} diff --git a/packages/amis-editor/src/renderer/SwitchMoreControl.tsx b/packages/amis-editor/src/renderer/SwitchMoreControl.tsx index 4448c1f8a..4bc3555b6 100644 --- a/packages/amis-editor/src/renderer/SwitchMoreControl.tsx +++ b/packages/amis-editor/src/renderer/SwitchMoreControl.tsx @@ -169,7 +169,7 @@ export default class SwitchMore extends React.Component< @autobind handleSwitchChange(checked: boolean) { - const {onBulkChange, onChange, bulk, defaultData, name} = this.props; + const {onBulkChange, onChange, bulk, defaultData, name, pipeOut} = this.props; this.setState({checked}); @@ -179,19 +179,30 @@ export default class SwitchMore extends React.Component< if (checked) { let data = defaultData ? {...defaultData} : {}; name && (data[name] = true); + if (pipeOut) { + data = pipeOut(data); + } onBulkChange && onBulkChange(data); } // 取消选中后,讲所有字段重置 else { - const values = fromPairs( + let values = fromPairs( this.getFormItemNames().map(i => [i, undefined]) ); name && (values[name] = false); + if (pipeOut) { + values = pipeOut(values); + } onBulkChange && onBulkChange(values); } return; } - onChange(checked ? defaultData || true : undefined); + + let data = checked ? defaultData || true : undefined; + if (pipeOut) { + data = pipeOut(data); + } + onChange(data); } @autobind diff --git a/packages/amis-editor/src/renderer/ValidationItem.tsx b/packages/amis-editor/src/renderer/ValidationItem.tsx index eaf7c0892..c57a36285 100644 --- a/packages/amis-editor/src/renderer/ValidationItem.tsx +++ b/packages/amis-editor/src/renderer/ValidationItem.tsx @@ -8,7 +8,7 @@ import {render, Button, Switch} from 'amis'; import {autobind} from 'amis-editor-core'; import {Validator} from '../validator'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import {SchemaCollection} from 'amis/lib/Schema'; export type ValidatorData = { diff --git a/packages/amis-editor/src/tpl/api.tsx b/packages/amis-editor/src/tpl/api.tsx index d3a7e45a7..046104ecb 100644 --- a/packages/amis-editor/src/tpl/api.tsx +++ b/packages/amis-editor/src/tpl/api.tsx @@ -1,9 +1,11 @@ -import {setSchemaTpl, getSchemaTpl} from 'amis-editor-core'; +import {setSchemaTpl, getSchemaTpl, tipedLabel} from 'amis-editor-core'; import React from 'react'; import {buildApi, Html} from 'amis'; +import {get} from 'lodash'; setSchemaTpl('api', (patch: any = {}) => { - const {name, label, value, description, sampleBuilder, ...rest} = patch; + const {name, label, value, description, sampleBuilder, apiDesc, ...rest} = + patch; return { type: 'container', @@ -13,21 +15,25 @@ setSchemaTpl('api', (patch: any = {}) => { label: label || 'API', labelRemark: sampleBuilder ? { - icon: '', - label: '示例', + label: false, title: '接口返回示例', + icon: 'fas fa-code', + className: 'm-l-xs ae-ApiSample-icon', tooltipClassName: 'ae-ApiSample-tooltip', - render: (data: any) => ( - ${sampleBuilder(data)} - `} - /> - ), + children: () => { + return ( + ${sampleBuilder()}${ + apiDesc + ? `${apiDesc}` + : '' + }`} + /> + ); + }, trigger: 'click', - className: 'm-l-xs', rootClose: true, placement: 'left' } @@ -166,6 +172,7 @@ setSchemaTpl('api', (patch: any = {}) => { mode: 'normal', renderLabel: false, visibleOn: 'this.data', + valueType: 'ae-DataPickerControl', descriptionClassName: 'help-block text-xs m-b-none', description: '

    当没开启数据映射时,发送数据自动切成白名单模式,配置啥发送啥,请绑定数据。如:{"a": "\\${a}", "b": 2}

    如果希望在默认的基础上定制,请先添加一个 Key 为 `&` Value 为 `\\$$` 作为第一行。

    当值为 __undefined时,表示删除对应的字段,可以结合{"&": "\\$$"}来达到黑名单效果。
    ' @@ -346,55 +353,66 @@ setSchemaTpl('apiString', { placeholder: 'http://' }); -setSchemaTpl('initFetch', { - type: 'group', - label: '是否初始加载', - visibleOn: 'this.initApi', - direction: 'vertical', - className: 'm-b-none', - labelRemark: { - trigger: 'click', - rootClose: true, - className: 'm-l-xs', - content: - '当配置初始化接口后,组件初始就会拉取接口数据,可以通过以下配置修改。', - placement: 'left' - }, - body: [ - { - name: 'initFetch', - type: 'radios', - inline: true, - onChange: () => {}, - // pipeIn: (value:any) => typeof value === 'boolean' ? value : '1' - options: [ +setSchemaTpl( + 'initFetch', + (overrides: {visibleOn?: string; name?: string} = {}) => { + const visibleOn = get(overrides, 'visibleOn', 'this.initApi'); + const fieldName = get(overrides, 'name', 'initFetch'); + const label = get(overrides, 'label', '是否初始加载'); + + return { + type: 'group', + label: tipedLabel( + label, + '当配置初始化接口后,组件初始就会拉取接口数据,可以通过以下配置修改。' + ), + visibleOn, + direction: 'vertical', + body: [ { - label: '是', - value: true + name: fieldName, + type: 'radios', + inline: true, + onChange: () => {}, + // pipeIn: (value:any) => typeof value === 'boolean' ? value : '1' + options: [ + { + label: '是', + value: true + }, + + { + label: '否', + value: false + }, + + { + label: '表达式', + value: '' + } + ] }, - { - label: '否', - value: false - }, - - { - label: '表达式', - value: '' - } + getSchemaTpl('valueFormula', { + label: '', + name: `${fieldName}On`, + autoComplete: false, + visibleOn: `typeof this.${fieldName} !== "boolean"`, + placeholder: '如:this.id 表示有 id 值时初始加载', + className: 'm-t-n-sm' + }) + // { + // name: `${fieldName}On`, + // autoComplete: false, + // visibleOn: `typeof this.${fieldName} !== "boolean"`, + // type: 'input-text', + // placeholder: '如:this.id 表示有 id 值时初始加载', + // className: 'm-t-n-sm' + // } ] - }, - - { - name: 'initFetchOn', - autoComplete: false, - visibleOn: 'typeof this.initFetch !== "boolean"', - type: 'input-text', - placeholder: '如:this.id 表示有 id 值时初始加载', - className: 'm-t-n-sm' - } - ] -}); + }; + } +); setSchemaTpl('proxy', { type: 'switch', @@ -409,31 +427,35 @@ setSchemaTpl('proxy', { }); setSchemaTpl('apiControl', (patch: any = {}) => { - const {name, label, value, description, sampleBuilder, ...rest} = patch; + const {name, label, value, description, sampleBuilder, apiDesc, ...rest} = + patch; return { type: 'ae-apiControl', label, name, description, - mode: 'normal', labelRemark: sampleBuilder ? { - icon: '', - label: '示例', + label: false, title: '接口返回示例', + icon: 'fas fa-code', + className: 'm-l-xs ae-ApiSample-icon', tooltipClassName: 'ae-ApiSample-tooltip', - render: (data: any) => ( - ${sampleBuilder(data)} - `} - /> - ), + children: () => { + return ( + ${sampleBuilder()}${ + apiDesc + ? `${apiDesc}` + : '' + }`} + /> + ); + }, trigger: 'click', - className: 'm-l-xs', rootClose: true, placement: 'left' } @@ -442,6 +464,50 @@ setSchemaTpl('apiControl', (patch: any = {}) => { }; }); +setSchemaTpl('interval', (more: any = {}) => ({ + type: 'ae-switch-more', + label: '定时刷新', + name: 'interval', + formType: 'extend', + bulk: true, + mode: 'normal', + form: { + body: [ + getSchemaTpl('withUnit', { + label: '刷新间隔', + name: 'interval', + control: { + type: 'input-number', + name: 'interval', + value: 1000 + }, + unit: '毫秒' + }) + ] + }, + ...more +})); + +setSchemaTpl('silentPolling', () => + getSchemaTpl('switch', { + label: tipedLabel('静默刷新', '设置自动定时刷新时是否显示loading'), + name: 'silentPolling', + visibleOn: '!!this.interval' + }) +); + +setSchemaTpl('stopAutoRefreshWhen', (extra: any = {}) => + getSchemaTpl('valueFormula', { + name: 'stopAutoRefreshWhen', + label: tipedLabel( + '定时刷新停止', + '定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则停止刷新' + ), + visibleOn: '!!this.interval', + ...extra + }) +); + /** * 接口控件 */ diff --git a/packages/amis-editor/src/tpl/common.tsx b/packages/amis-editor/src/tpl/common.tsx index 0ef04ff60..8eda44524 100644 --- a/packages/amis-editor/src/tpl/common.tsx +++ b/packages/amis-editor/src/tpl/common.tsx @@ -2,9 +2,11 @@ import { setSchemaTpl, getSchemaTpl, defaultValue, - isObject + isObject, + tipedLabel, + DSField } from 'amis-editor-core'; -import {remarkTpl, tipedLabel} from '../component/BaseControl'; +import {remarkTpl} from '../component/BaseControl'; import {SchemaObject} from 'amis/lib/Schema'; import flatten from 'lodash/flatten'; import {InputComponentName} from '../component/InputComponentName'; @@ -22,13 +24,46 @@ setSchemaTpl('switch', { inputClassName: 'is-inline ' }); +/** + * 分割线 + */ +setSchemaTpl('divider', { + type: 'divider', + className: 'mx-0' +}); + +/** + * 带单位的控件 + */ +setSchemaTpl( + 'withUnit', + (config: {name: string; label: string; control: any; unit: string}) => { + return { + type: 'input-group', + name: config.name, + label: config.label, + body: [ + config.control, + { + type: 'tpl', + addOnclassName: 'border-0 bg-none', + tpl: config.unit + } + ] + }; + } +); + /** * 表单项字段name */ setSchemaTpl('formItemName', { label: '字段名', name: 'name', - type: 'ae-DataBindingControl' + type: 'ae-DataBindingControl', + onBindingChange(field: DSField, onBulkChange: (value: any) => void) { + onBulkChange(field.resolveEditSchema?.() || {label: field.label}); + } // validations: { // matchRegexp: /^[a-z\$][a-z0-0\-_]*$/i // }, @@ -38,37 +73,45 @@ setSchemaTpl('formItemName', { // validateOnChange: false }); -setSchemaTpl('formItemMode', { - label: '布局', - name: 'mode', - type: 'button-group-select', - option: '继承', - horizontal: { - left: 2, - justify: true - }, - // className: 'w-full', - pipeIn: defaultValue(''), - options: [ - { - label: '内联', - value: 'inline' +setSchemaTpl( + 'formItemMode', + (config: { + // 是不是独立表单,没有可以集成的内容 + isForm: boolean; + }) => ({ + label: '布局', + name: 'mode', + type: 'button-group-select', + option: '继承', + horizontal: { + left: 2, + justify: true }, - { - label: '水平', - value: 'horizontal' - }, - { - label: '垂直', - value: 'normal' - }, - { - label: '继承', - value: '' - } - ], - pipeOut: (v: string) => (v ? v : undefined) -}); + // className: 'w-full', + pipeIn: defaultValue(''), + options: [ + { + label: '内联', + value: 'inline' + }, + { + label: '水平', + value: 'horizontal' + }, + { + label: '垂直', + value: 'normal' + }, + config?.isForm + ? null + : { + label: '继承', + value: '' + } + ].filter(i => i), + pipeOut: (v: string) => (v ? v : undefined) + }) +); setSchemaTpl('formItemInline', { type: 'switch', @@ -243,37 +286,31 @@ setSchemaTpl( key: string; visibleOn: string; body: Array; - }>, - rendererSchema?: any + }> ) => { - let currentKey = rendererSchema - ? `${rendererSchema.$$id}_${rendererSchema.type}_${rendererSchema.configTitle}_collapse` - : `config_collapse`; - currentKey = currentKey.replace(/-/g, '__'); - const collapseGroupBody = config .filter( item => item && Array.isArray(item?.body) && item?.body.length > 0 ) - .map((item, index) => ({ + .map(item => ({ type: 'collapse', + collapsed: false, headingClassName: 'ae-formItemControl-header', bodyClassName: 'ae-formItemControl-body', ...item, - key: `${currentKey}_${item.key || index.toString()}`, + key: item.title, body: flatten(item.body) })); return { type: 'collapse-group', - key: currentKey, + activeKey: collapseGroupBody.map(panel => panel.title), expandIconPosition: 'right', expandIcon: { type: 'icon', icon: 'chevron-right' }, className: 'ae-formItemControl ae-styleControl', - activeKey: collapseGroupBody.map((group, index) => group.key), body: collapseGroupBody }; } @@ -346,7 +383,7 @@ setSchemaTpl( body: [ { type: 'ae-formulaControl', - label: config?.label || '默认值', + label: config?.label ?? '默认值', name: config?.name || 'value', rendererSchema: curRendererSchema, rendererWrapper: config?.rendererWrapper, @@ -509,28 +546,29 @@ setSchemaTpl('size', { }); setSchemaTpl('name', { - label: '名字', + label: tipedLabel( + '名字', + '需要联动时才需要,其他组件可以通过这个名字跟当前组件联动' + ), name: 'name', type: 'input-text', - description: '需要联动时才需要,其他组件可以通过这个名字跟当前组件联动', placeholder: '请输入字母或者数字' }); setSchemaTpl('reload', { - label: '刷新目标组件', name: 'reload', asFormItem: true, // type: 'input-text', component: InputComponentName, - description: - '可以指定操作完成后刷新目标组件,请填写目标组件的 name 属性,多个组件请用,隔开,如果目标组件为表单项,请先填写表单的名字,再用.连接表单项的名字如:xxForm.xxControl。另外如果刷新目标对象设置为 window,则会刷新整个页面。', - labelRemark: { - trigger: 'click', - className: 'm-l-xs', - rootClose: true, - content: - '设置名字后,当前组件操作完成会触发目标组件(根据设置的名字)的刷新。', - placement: 'left' + label: tipedLabel( + '刷新目标组件', + '可以指定操作完成后刷新目标组件,请填写目标组件的 name 属性,多个组件请用,隔开,如果目标组件为表单项,请先填写表单的名字,再用.连接表单项的名字如:xxForm.xxControl。另外如果刷新目标对象设置为 window,则会刷新整个页面。' + ), + placeholder: '请输入组件name', + mode: 'horizontal', + horizontal: { + left: 4, + justify: true } }); @@ -577,7 +615,7 @@ setSchemaTpl( ? getSchemaTpl('disabled') : null, config?.isFormItem ? getSchemaTpl('clearValueOnHidden') : null - ] + ].filter(Boolean) }; } ); diff --git a/packages/amis-editor/src/tpl/horizontal.tsx b/packages/amis-editor/src/tpl/horizontal.tsx index dd9dca218..1583be9cc 100644 --- a/packages/amis-editor/src/tpl/horizontal.tsx +++ b/packages/amis-editor/src/tpl/horizontal.tsx @@ -1,5 +1,6 @@ import {setSchemaTpl, getSchemaTpl, defaultValue} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {isObject} from 'lodash'; +import {tipedLabel} from 'amis-editor-core'; setSchemaTpl('horizontal-align', { type: 'button-group-select', @@ -57,47 +58,51 @@ setSchemaTpl('leftRate', { } }); -setSchemaTpl('horizontal', () => { - return [ - { - type: 'button-group-select', - label: '标题宽度', - name: 'horizontal', - options: [ - {label: '继承', value: 'formHorizontal'}, - {label: '固宽', value: 'leftFixed'}, - {label: '比例', value: 'leftRate'} - ], - pipeIn(v: any) { - if (!v) { - return 'formHorizontal'; - } - if (v.leftFixed) { - return 'leftFixed'; - } - return 'leftRate'; - }, - pipeOut(v: any) { - const defaultData = { - formHorizontal: undefined, - leftFixed: {leftFixed: 'normal'}, - leftRate: {left: 3, right: 9} - }; +setSchemaTpl( + 'horizontal', + (config: {visibleOn: string; [propName: string]: any}) => { + return [ + { + type: 'button-group-select', + label: '标题宽度', + name: 'horizontal', + options: [ + {label: '继承', value: 'formHorizontal'}, + {label: '固宽', value: 'leftFixed'}, + {label: '比例', value: 'leftRate'} + ], + pipeIn(v: any) { + if (!v) { + return 'formHorizontal'; + } + if (v.leftFixed) { + return 'leftFixed'; + } + return 'leftRate'; + }, + pipeOut(v: any) { + const defaultData = { + formHorizontal: undefined, + leftFixed: {leftFixed: 'normal'}, + leftRate: {left: 3, right: 9} + }; - // @ts-ignore - return defaultData[v]; + // @ts-ignore + return defaultData[v]; + }, + visibleOn: 'this.mode == "horizontal" && this.label !== false', + ...(isObject(config) ? config : {}) }, - visibleOn: 'this.mode == "horizontal" && this.label !== false' - }, - { - type: 'container', - className: 'ae-ExtendMore mb-3', - visibleOn: - 'this.mode == "horizontal" && this.horizontal && this.label !== false', - body: [getSchemaTpl('leftFixed'), getSchemaTpl('leftRate')] - } - ]; -}); + { + type: 'container', + className: 'ae-ExtendMore mb-3', + visibleOn: + 'this.mode == "horizontal" && this.horizontal && this.label !== false', + body: [getSchemaTpl('leftFixed'), getSchemaTpl('leftRate')] + } + ]; + } +); setSchemaTpl('subFormItemMode', { label: '子表单展示模式', diff --git a/packages/amis-editor/src/tpl/options.tsx b/packages/amis-editor/src/tpl/options.tsx index d824df552..5a56c75a7 100644 --- a/packages/amis-editor/src/tpl/options.tsx +++ b/packages/amis-editor/src/tpl/options.tsx @@ -1,5 +1,5 @@ import {setSchemaTpl, getSchemaTpl, defaultValue} from 'amis-editor-core'; -import {tipedLabel} from '../component/BaseControl'; +import {tipedLabel} from 'amis-editor-core'; import {SchemaObject} from 'amis/lib/Schema'; setSchemaTpl('options', { @@ -114,7 +114,7 @@ setSchemaTpl('tree', { setSchemaTpl('multiple', (schema: any = {}) => { return { - type: 'ae-Switch-More', + type: 'ae-switch-more', mode: 'normal', name: 'multiple', label: '可多选', diff --git a/packages/amis-editor/tsconfig.json b/packages/amis-editor/tsconfig.json index 276f20046..c4fc36ae9 100644 --- a/packages/amis-editor/tsconfig.json +++ b/packages/amis-editor/tsconfig.json @@ -9,6 +9,6 @@ "../../node_modules/@types" ] }, - "include": ["src/**/*"], + "include": ["src/**/*", "../amis-editor-core/src/builder"], "references": [] }