mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: CRUD & Form 脚手架构建优化, 列表可视化设计优化; chore: DSBuilder迁移至amis-editor (#8003)
* feat: CRUD & Form 脚手架构建优化; chore: DSBuilder迁移至amis-editor (#7531) * tpl最大显示行数支持 * 编辑器组件通用假数据支持 * each循环次数支持 * 列表可视化设计优化 * fix: 数据源构造器 & Form & CRUD2 & Service相关问题修复 (#8002) * chore: 修复正则报错 * chore: 修复正则报错-02 * chore: 更新 CRUD 组件 Card 模式单测快照 * chore: 优化假数据merge --------- Co-authored-by: zhangtao07 <zhang.tao.1006@163.com>
This commit is contained in:
parent
415ad0135d
commit
d27948f0f5
@ -4,7 +4,7 @@ title: 可视化编辑器
|
||||
|
||||
目前 amis 可视化编辑器也作为单独的 npm 包发布了出来,可以通过 npm 安装使用。
|
||||
|
||||
在线体验:https://aisuda.github.io/amis-editor-demo
|
||||
在线体验:https://aisuda.github.io/amis-editor-demo
|
||||
示例代码:https://github.com/aisuda/amis-editor-demo
|
||||
|
||||
## 使用
|
||||
@ -46,7 +46,7 @@ render() {
|
||||
- `className?: string` 额外加个 css 类名,辅助样式定义。
|
||||
- `schemas?: JSONSchemaObject` 用来定义有哪些全局变量,辅助编辑器格式化绑定全局数据。
|
||||
- `theme?: string` amis 主题
|
||||
- `schemaFilter?: (schema: any) => any` 配置过滤器。可以用来实现 api proxy,比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
|
||||
- `schemaFilter?: (schema: any, isPreview?: boolean) => any` 配置过滤器。可以用来实现 api proxy,比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
|
||||
- `amisEnv?: any` 这是是给 amis 的 Env 对象,具体请前往 [env 说明](../start/getting-started#env)
|
||||
- `disableBultinPlugin?: boolean` 是否禁用内置插件
|
||||
- `disablePluginList?: Array<string> | (id: string, plugin: PluginClass) => boolean` 禁用插件列表
|
||||
|
@ -807,13 +807,13 @@ export const components = [
|
||||
import('../../docs/zh-CN/components/table.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
// {
|
||||
// label: 'Table2 表格',
|
||||
// path: '/zh-CN/components/table2',
|
||||
// component: React.lazy(() =>
|
||||
// import('../../docs/zh-CN/components/table2.md').then(wrapDoc)
|
||||
// )
|
||||
// },
|
||||
{
|
||||
label: 'Table2 表格',
|
||||
path: '/zh-CN/components/table2',
|
||||
component: React.lazy(() =>
|
||||
import('../../docs/zh-CN/components/table2.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Table View 表格视图',
|
||||
path: '/zh-CN/components/table-view',
|
||||
|
@ -5,5 +5,5 @@
|
||||
"packages/amis-ui",
|
||||
"packages/amis"
|
||||
],
|
||||
"version": "3.4.0"
|
||||
"version": "3.4.1-alpha.0"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-core",
|
||||
"version": "3.4.0",
|
||||
"version": "3.4.1-alpha.0",
|
||||
"description": "amis-core",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -46,7 +46,7 @@
|
||||
"esm"
|
||||
],
|
||||
"dependencies": {
|
||||
"amis-formula": "^3.4.0",
|
||||
"amis-formula": "^3.4.1-alpha.0",
|
||||
"classnames": "2.3.2",
|
||||
"file-saver": "^2.0.2",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
|
@ -661,6 +661,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
|
||||
return createObject(superData, {
|
||||
total: self.total,
|
||||
page: self.page,
|
||||
perPage: self.perPage,
|
||||
items: self.items.concat(),
|
||||
selectedItems: self.selectedItems.concat(),
|
||||
unSelectedItems: self.unSelectedItems.concat()
|
||||
|
@ -3,7 +3,7 @@
|
||||
*
|
||||
* @param string 要转换的字符串
|
||||
*/
|
||||
export const keyToPath = (string: string) => {
|
||||
export const keyToPath = (string: string = '') => {
|
||||
const result = [];
|
||||
|
||||
if (string.charCodeAt(0) === '.'.charCodeAt(0)) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-editor-core",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2-alpha.0",
|
||||
"description": "amis 可视化编辑器",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
@ -4,3 +4,28 @@
|
||||
border: 1px solid rgba(#23b7e5, 1);
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.ae-Editor-list {
|
||||
.ae-Editor-listItem,
|
||||
.ae-Editor-eachItem {
|
||||
position: relative !important;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
background: rgba(22, 40, 60, 0.2) url(../static/indication.png) repeat;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ae-Editor-eachItem:first-child::after,
|
||||
.cards-items > div:first-child > div::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
&-content {
|
||||
@include flexBox();
|
||||
align-items: stretch;
|
||||
|
||||
.ae-ApiControl-input {
|
||||
background: var(--Form-input-bg);
|
||||
@ -44,6 +45,10 @@
|
||||
width: 100%;
|
||||
height: calc(var(--Form-input-lineHeight) * var(--Form-input-fontSize));
|
||||
}
|
||||
|
||||
.ae-ApiControl-setting-button {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
163
packages/amis-editor-core/scss/control/_crud2-control.scss
Normal file
163
packages/amis-editor-core/scss/control/_crud2-control.scss
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @file crud2-control.scss
|
||||
* @desc CRUD2相关控件及样式
|
||||
*/
|
||||
|
||||
.ae-CRUDConfigControl {
|
||||
margin-bottom: var(--Form-item-gap);
|
||||
|
||||
&-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e9eb;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
padding: 0 var(--gap-sm);
|
||||
|
||||
&.is-draggable:hover {
|
||||
background-color: #f9f9f9;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
&-dragger {
|
||||
cursor: move;
|
||||
margin: 0 var(--gap-sm) 0 0;
|
||||
color: rgba(232, 233, 235, 1);
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
max-width: 140px;
|
||||
|
||||
& > span {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
color: #151b26;
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& > button {
|
||||
color: #151b26;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
width: px2rem(16px);
|
||||
height: px2rem(16px);
|
||||
|
||||
&.icon-share-link {
|
||||
width: px2rem(14px);
|
||||
height: px2rem(14px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tag {
|
||||
cursor: auto;
|
||||
background-color: transparent;
|
||||
border: 1px solid #2468f2;
|
||||
color: #2468f2;
|
||||
border-radius: 2px;
|
||||
line-height: #{px2rem(18px)};
|
||||
height: #{px2rem(20px)};
|
||||
margin-right: #{px2rem(8px)};
|
||||
scale: 0.9;
|
||||
max-width: 80px;
|
||||
|
||||
&--cascading {
|
||||
color: #531dab;
|
||||
border-color: #531dab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
color: #b4b6ba;
|
||||
padding-top: px2rem(10px);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& > span:nth-child(1) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-switch {
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
&-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 4px;
|
||||
background-color: #dfdfdf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown {
|
||||
/* margin-bottom: var(--Form-mode-default-labelGap); */
|
||||
|
||||
& > button {
|
||||
color: #4c5664;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
font-weight: bold;
|
||||
|
||||
& > svg {
|
||||
width: px2rem(14px);
|
||||
height: px2rem(14px);
|
||||
margin-right: var(--gap-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
flex-flow: row-reverse nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ae-CRUDConfigControl-modal {
|
||||
&-btn-loading {
|
||||
--Spinner-color: #fff;
|
||||
}
|
||||
}
|
@ -8,13 +8,23 @@
|
||||
display: flex;
|
||||
height: 30px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
:not(:last-child) {
|
||||
margin-right: px2rem(8px);
|
||||
}
|
||||
|
||||
&-go {
|
||||
&-content {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
& > .ae-FeatureControlItem-go {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
@ -42,6 +52,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-dragBar {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 3px;
|
||||
cursor: move;
|
||||
|
||||
& > svg {
|
||||
fill: #e7e7e7;
|
||||
color: #e7e7e7;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-dragBar {
|
||||
& > svg {
|
||||
color: transparent;
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-action {
|
||||
@ -55,24 +86,9 @@
|
||||
}
|
||||
|
||||
&--menus {
|
||||
width: calc(100% - 12px);
|
||||
margin-left: 6px;
|
||||
/* width: calc(100% - 12px);
|
||||
margin-left: 6px; */
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-dragBar {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover &-dragBar {
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba($color: #000000, $alpha: 0.4);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background-color: rgba($color: #000000, $alpha: 0.55);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @file table-column-width-control.scss
|
||||
* @desc 表格列宽控件
|
||||
*/
|
||||
|
||||
.ae-columnWidthControl {
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
@ -73,6 +73,11 @@
|
||||
> li {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-loading {
|
||||
display: flex;
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
&--fullscreen {
|
||||
> a {
|
||||
|
@ -41,6 +41,8 @@
|
||||
@import './control/_status';
|
||||
@import './control/_icon-button-group-control';
|
||||
@import './control/_flex-setting-control';
|
||||
@import './control/table-column-width-control.scss';
|
||||
@import './control/crud2-control';
|
||||
|
||||
/* 样式控件 */
|
||||
@import './style-control/box-model';
|
||||
@ -1233,7 +1235,7 @@
|
||||
|
||||
[data-region] {
|
||||
position: relative;
|
||||
min-height: 34px;
|
||||
min-height: 10px;
|
||||
|
||||
&:empty {
|
||||
min-width: 20px;
|
||||
@ -1768,15 +1770,27 @@ div.ae-DragImage {
|
||||
width: px2rem(700px);
|
||||
@include panel-sm-content();
|
||||
|
||||
--radio-default-default-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Table-thead-fontSize: #{$Editor-right-panel-font-size};
|
||||
--button-size-default-fontSize: #{$Editor-right-panel-font-size};
|
||||
--checkbox-checkbox-default-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Tabs--vertical-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Tabs--vertical-active-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Tabs--vertical-hover-fontSize: #{$Editor-right-panel-font-size};
|
||||
|
||||
.ae-Steps {
|
||||
margin: auto;
|
||||
max-width: px2rem(350px);
|
||||
--Steps-title-fontsize: #{px2rem(14px)};
|
||||
|
||||
&-Icon {
|
||||
display: flex !important;
|
||||
width: px2rem(22px) !important;
|
||||
height: px2rem(22px) !important;
|
||||
margin-top: px2rem(5px);
|
||||
font-size: px2rem(12px) !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,602 +0,0 @@
|
||||
/**
|
||||
* API数据源处理器
|
||||
*/
|
||||
|
||||
import {toast} from 'amis';
|
||||
import {
|
||||
DSBuilder,
|
||||
DSFeature,
|
||||
DSFeatureType,
|
||||
DSGrain,
|
||||
registerDSBuilder
|
||||
} from './DSBuilder';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {getEnv} from 'mobx-state-tree';
|
||||
import type {ButtonSchema} from 'amis';
|
||||
import type {FormSchema, SchemaCollection, SchemaObject} from 'amis';
|
||||
|
||||
import type {DSSourceSettingFormConfig} from './DSBuilder';
|
||||
import {getSchemaTpl, tipedLabel} from '../tpl';
|
||||
import {EditorNodeType} from '../store/node';
|
||||
|
||||
class APIBuilder extends DSBuilder {
|
||||
static type = 'api';
|
||||
|
||||
static accessable = (controlType: string, propKey: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
name = '接口';
|
||||
|
||||
order = 0;
|
||||
|
||||
public match = (value: any, schema?: SchemaObject) => {
|
||||
// https://aisuda.bce.baidu.com/amis/zh-CN/docs/types/api
|
||||
if (
|
||||
(typeof value === 'string' &&
|
||||
/^(get|post|put|delete|option):/.test(value)) ||
|
||||
(typeof value === 'object' && value.url)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public features: Array<DSFeatureType> = [
|
||||
'List',
|
||||
'Insert',
|
||||
'View',
|
||||
'Edit',
|
||||
'Delete',
|
||||
'BulkEdit',
|
||||
'BulkDelete',
|
||||
'Import',
|
||||
'Export',
|
||||
'SimpleQuery',
|
||||
'FuzzyQuery'
|
||||
];
|
||||
|
||||
public makeSourceSettingForm(
|
||||
config: DSSourceSettingFormConfig
|
||||
): SchemaObject[] {
|
||||
let {name, label, feat, inCrud, inScaffold} = config;
|
||||
|
||||
if (['Import', 'Export', 'SimpleQuery', 'FuzzyQuery'].includes(feat)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
label =
|
||||
label ??
|
||||
(inCrud && feat !== 'List' ? DSFeature[feat].label + '接口' : '接口');
|
||||
name = name ?? (inScaffold ? DSFeature[feat].value + 'Api' : 'api');
|
||||
|
||||
let sampleBuilder = null;
|
||||
let apiDesc = null;
|
||||
switch (feat) {
|
||||
case 'Insert':
|
||||
(label as any) = tipedLabel(
|
||||
label,
|
||||
`用来保存数据, 表单提交后将数据传入此接口。 <br/>
|
||||
接口响应体要求(如果data中有数据,该数据将被合并到表单上下文中):<br/>
|
||||
${JSON.stringify({status: 0, msg: '', data: {}}, null, '<br/>')}`
|
||||
);
|
||||
break;
|
||||
|
||||
case 'List':
|
||||
(label as any) = tipedLabel(
|
||||
label,
|
||||
`接口响应体要求:<br/>
|
||||
${JSON.stringify(
|
||||
{status: 0, msg: '', items: {}, page: 0, total: 0},
|
||||
null,
|
||||
'<br/>'
|
||||
)}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
getSchemaTpl('apiControl', {
|
||||
label,
|
||||
name,
|
||||
sampleBuilder,
|
||||
apiDesc
|
||||
})
|
||||
]
|
||||
.concat(
|
||||
feat === 'Edit' && !inCrud
|
||||
? getSchemaTpl('apiControl', {
|
||||
label: tipedLabel(
|
||||
'初始化接口',
|
||||
`接口响应体要求:<br/>
|
||||
${JSON.stringify({status: 0, msg: '', data: {}}, null, '<br/>')}`
|
||||
),
|
||||
name: 'initApi'
|
||||
})
|
||||
: null
|
||||
)
|
||||
.concat(
|
||||
feat === 'List' && inCrud && inScaffold
|
||||
? this.makeFieldsSettingForm({
|
||||
feat,
|
||||
setting: true
|
||||
})
|
||||
: null
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
public async getContextFileds(config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
}) {
|
||||
return config.schema.__fields;
|
||||
}
|
||||
|
||||
public async getAvailableContextFileds(
|
||||
config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
},
|
||||
target: EditorNodeType
|
||||
) {
|
||||
// API类目前没有,增加API中心的出参入参后,可以在这里提供绑定字段
|
||||
// return {
|
||||
// type: 'ae-SimpleDataBindingPanel',
|
||||
// fields: [
|
||||
// {
|
||||
// label: '可用字段',
|
||||
// children: [
|
||||
// {label: '名称', value: 'name'},
|
||||
// {label: '年级', value: 'grade'}
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// } as any;
|
||||
}
|
||||
|
||||
onFieldsInit(value: any, form: any) {
|
||||
this.features.forEach(feat => {
|
||||
const key = `${DSFeature[feat].value}Fields`;
|
||||
const currentData = form.getValueByName(key);
|
||||
|
||||
const result = cloneDeep(value || []).map((field: any) => {
|
||||
const exist = currentData?.find((f: any) => f.name === field.name);
|
||||
|
||||
return {
|
||||
...field,
|
||||
checked: exist ? exist.checked : true
|
||||
};
|
||||
});
|
||||
form.setValueByName(key, result);
|
||||
});
|
||||
}
|
||||
|
||||
public makeFieldsSettingForm(config: {
|
||||
sourceKey?: string;
|
||||
feat: DSFeatureType;
|
||||
inCrud?: boolean;
|
||||
setting?: boolean;
|
||||
inScaffold?: boolean;
|
||||
}) {
|
||||
let {sourceKey, feat, inCrud, setting, inScaffold} = config;
|
||||
if (
|
||||
inScaffold === false ||
|
||||
['Import', 'Export', 'FuzzyQuery'].includes(feat)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
sourceKey = sourceKey ?? `${DSFeature[feat].value}Api`;
|
||||
const key = setting ? '__fields' : `${DSFeature[feat].value}Fields`;
|
||||
const hasInputType =
|
||||
['Edit', 'Insert'].includes(feat) || (inCrud && feat === 'List');
|
||||
const hasType = ['View', 'List'].includes(feat);
|
||||
|
||||
return ([] as any)
|
||||
.concat(
|
||||
inCrud && feat !== 'List'
|
||||
? this.makeSourceSettingForm({
|
||||
feat,
|
||||
inScaffold,
|
||||
inCrud
|
||||
})
|
||||
: null
|
||||
)
|
||||
.concat([
|
||||
{
|
||||
type: 'combo',
|
||||
className: 'mb-0 ae-Fields-Setting',
|
||||
joinValues: false,
|
||||
name: key,
|
||||
label: inCrud ? `${DSFeature[feat].label}字段` : '字段',
|
||||
multiple: true,
|
||||
draggable: true,
|
||||
addable: false,
|
||||
removable: false,
|
||||
itemClassName: 'ae-Fields-Setting-Item',
|
||||
// CRUD的脚手架面板,基于现有字段进行选择
|
||||
hidden: setting || !inCrud || ['Delete', 'BulkDelete'].includes(feat),
|
||||
items: {
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
name: 'checked',
|
||||
label: false,
|
||||
mode: 'inline',
|
||||
className: 'm-0 ml-1',
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
className: 'ae-Fields-Setting-Item-label',
|
||||
tpl: '${label}'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input-table',
|
||||
label: '字段',
|
||||
className: 'mb-0',
|
||||
name: key,
|
||||
// 非crud,都是定义字段的模式,只有crud,有统一定义字段,因此是选择字段
|
||||
visible: setting ?? !inCrud,
|
||||
removable: true,
|
||||
columnsTogglable: false,
|
||||
needConfirm: false,
|
||||
onChange: (value: any, oldValue: any, model: any, form: any) =>
|
||||
this.onFieldsInit(value, form),
|
||||
columns: [
|
||||
{
|
||||
type: 'switch',
|
||||
name: 'checked',
|
||||
value: true,
|
||||
label: '隐藏,默认选中',
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'label',
|
||||
label: '标题'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'name',
|
||||
label: '绑定字段'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'type',
|
||||
label: '类型',
|
||||
visible: hasType,
|
||||
value: 'tpl',
|
||||
options: [
|
||||
{
|
||||
value: 'tpl',
|
||||
label: '文本',
|
||||
typeKey: 'tpl'
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
label: '图片',
|
||||
typeKey: 'src'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: '日期',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'progress',
|
||||
label: '进度',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'mapping',
|
||||
label: '映射',
|
||||
typeKey: 'value'
|
||||
}
|
||||
],
|
||||
autoFill: {
|
||||
typeKey: '${typeKey}'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'inputType',
|
||||
label: '输入类型',
|
||||
visible: hasInputType,
|
||||
value: 'input-text',
|
||||
options: [
|
||||
{
|
||||
label: '输入框',
|
||||
value: 'input-text'
|
||||
},
|
||||
{
|
||||
label: '多行文本',
|
||||
value: 'textarea'
|
||||
},
|
||||
{
|
||||
label: '数字输入',
|
||||
value: 'input-number'
|
||||
},
|
||||
{
|
||||
label: '单选框',
|
||||
value: 'radios'
|
||||
},
|
||||
{
|
||||
label: '勾选框',
|
||||
value: 'checkbox'
|
||||
},
|
||||
{
|
||||
label: '复选框',
|
||||
value: 'checkboxes'
|
||||
},
|
||||
{
|
||||
label: '下拉框',
|
||||
value: 'select'
|
||||
},
|
||||
{
|
||||
label: '开关',
|
||||
value: 'switch'
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
value: 'input-date'
|
||||
},
|
||||
{
|
||||
label: '表格',
|
||||
value: 'input-table'
|
||||
},
|
||||
{
|
||||
label: '文件上传',
|
||||
value: 'input-file'
|
||||
},
|
||||
{
|
||||
label: '图片上传',
|
||||
value: 'input-image'
|
||||
},
|
||||
{
|
||||
label: '富文本编辑器',
|
||||
value: 'input-rich-text'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
visible: setting ?? !inCrud,
|
||||
label: '',
|
||||
body: [
|
||||
{
|
||||
type: 'grid',
|
||||
columns: [
|
||||
{
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '添加字段',
|
||||
target: key,
|
||||
className: 'ae-Button--link',
|
||||
level: 'link',
|
||||
icon: 'plus',
|
||||
actionType: 'add'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
columnClassName: 'text-right',
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '基于接口自动生成字段',
|
||||
visible: feat === 'Edit' || feat === 'List',
|
||||
className: 'ae-Button--link',
|
||||
level: 'link',
|
||||
// className: 'm-t-xs m-b-xs',
|
||||
// 列表 或者 不在CRUD中的查看接口等
|
||||
onClick: async (e: Event, props: any) => {
|
||||
const data = props.data;
|
||||
const schemaFilter = getEnv(
|
||||
(window as any).editorStore
|
||||
).schemaFilter;
|
||||
const apiKey =
|
||||
feat === 'Edit' && !inCrud ? 'initApi' : sourceKey;
|
||||
let api: any = data[apiKey!];
|
||||
// 主要是给爱速搭中替换 url
|
||||
if (schemaFilter) {
|
||||
api = schemaFilter({
|
||||
api
|
||||
}).api;
|
||||
}
|
||||
if (!api) {
|
||||
toast.warning('请先填写接口');
|
||||
}
|
||||
|
||||
const result = await props.env.fetcher(api, data);
|
||||
|
||||
let autoFillKeyValues: Array<any> = [];
|
||||
let itemExample;
|
||||
if (feat === 'List') {
|
||||
const items = result.data?.rows || result.data?.items;
|
||||
itemExample = items?.[0];
|
||||
} else {
|
||||
itemExample = result.data;
|
||||
}
|
||||
|
||||
if (itemExample) {
|
||||
Object.entries(itemExample).forEach(
|
||||
([key, value]) => {
|
||||
autoFillKeyValues.push({
|
||||
label: key,
|
||||
type: 'tpl',
|
||||
inputType:
|
||||
typeof value === 'number'
|
||||
? 'input-number'
|
||||
: 'input-text',
|
||||
name: key
|
||||
});
|
||||
}
|
||||
);
|
||||
props.formStore.setValues({
|
||||
[key]: autoFillKeyValues
|
||||
});
|
||||
this.onFieldsInit(autoFillKeyValues, props.formStore);
|
||||
} else {
|
||||
toast.warning(
|
||||
'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]) as SchemaObject[];
|
||||
}
|
||||
|
||||
public async makeFieldFilterSetting(config: {
|
||||
/** 数据源字段名 */
|
||||
sourceKey: string;
|
||||
schema: any;
|
||||
fieldName: string;
|
||||
}) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public resolveSourceSchema(config: {
|
||||
schema: SchemaObject;
|
||||
setting: any;
|
||||
name?: string;
|
||||
feat?: DSFeatureType;
|
||||
inCrud?: boolean;
|
||||
}): void {
|
||||
let {name, setting, schema, feat} = config;
|
||||
name = name ?? 'api';
|
||||
// @ts-ignore
|
||||
schema[name] = setting[feat ? `${DSFeature[feat].value}Api` : 'api'];
|
||||
|
||||
// form中需要初始化接口和编辑接口
|
||||
if (feat === 'Edit') {
|
||||
(schema as FormSchema).initApi = setting.initApi;
|
||||
}
|
||||
}
|
||||
|
||||
public resolveViewSchema(config: {
|
||||
setting: any;
|
||||
feat?: DSFeatureType;
|
||||
}): SchemaObject[] {
|
||||
let {setting, feat = 'Edit'} = config;
|
||||
const fields = setting[`${DSFeature[feat].value}Fields`] || [];
|
||||
return fields
|
||||
.filter((i: any) => i.checked)
|
||||
.map((field: any) => ({
|
||||
type: field.type,
|
||||
[field.typeKey || 'value']: '${' + field.name + '}'
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveTableSchema(config: {schema: any; setting: any}): void {
|
||||
let {schema, setting} = config;
|
||||
const fields = setting.listFields.filter((i: any) => i.checked) || [];
|
||||
schema.columns = this.makeTableColumnsByFields(fields);
|
||||
}
|
||||
|
||||
public makeTableColumnsByFields(fields: any[]) {
|
||||
return fields.map((field: any) => ({
|
||||
type: field.type,
|
||||
title: field.label,
|
||||
name: field.name,
|
||||
[field.typeKey || 'value']: '${' + field.name + '}'
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveCreateSchema(config: {
|
||||
schema: FormSchema;
|
||||
setting: any;
|
||||
feat: 'Insert' | 'Edit' | 'BulkEdit';
|
||||
name?: string;
|
||||
inCrud?: boolean;
|
||||
inScaffold?: boolean;
|
||||
}): void {
|
||||
let {schema, setting, feat, name} = config;
|
||||
const fields = setting[`${DSFeature[feat].value}Fields`] || [];
|
||||
// @ts-ignore
|
||||
schema[name ?? 'api'] = setting[DSFeature[feat].value + 'Api'];
|
||||
schema.initApi = setting['initApi'];
|
||||
schema.body = fields
|
||||
.filter((i: any) => i.checked)
|
||||
.map((field: any) => ({
|
||||
type: field.inputType,
|
||||
name: field.name,
|
||||
label: field.label
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveDeleteSchema(config: {
|
||||
schema: ButtonSchema;
|
||||
setting: any;
|
||||
feat: 'BulkDelete' | 'Delete';
|
||||
name?: string | undefined;
|
||||
}) {
|
||||
const {schema, setting, feat} = config;
|
||||
schema.onEvent = Object.assign(schema.onEvent ?? {}, {
|
||||
click: {
|
||||
actions: []
|
||||
}
|
||||
});
|
||||
|
||||
const api = {
|
||||
...(setting[`${DSFeature[feat].value}Api`] || {})
|
||||
};
|
||||
if (feat === 'Delete') {
|
||||
api.data = {
|
||||
id: '${item.id}'
|
||||
};
|
||||
} else {
|
||||
api.data = {
|
||||
ids: '${ARRAYMAP(selectedItems, item=> item.id)}'
|
||||
};
|
||||
}
|
||||
|
||||
schema.onEvent.click.actions.push({
|
||||
actionType: 'ajax',
|
||||
args: {api}
|
||||
});
|
||||
}
|
||||
|
||||
public resolveSimpleFilterSchema(config: {
|
||||
setting: any;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
|
||||
}) {
|
||||
const {setting, size} = config;
|
||||
const fields = setting.simpleQueryFields || [];
|
||||
return fields
|
||||
.filter((i: any) => i.checked)
|
||||
.map((field: any) => ({
|
||||
type: field.inputType,
|
||||
name: field.name,
|
||||
label: field.label,
|
||||
size
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveAdvancedFilterSchema(config: {setting: any}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
registerDSBuilder(APIBuilder);
|
@ -1,381 +0,0 @@
|
||||
/**
|
||||
* 数据源构造器,可用于对接当前amis中的扩展数据源
|
||||
*/
|
||||
|
||||
import type {ButtonSchema} from 'amis';
|
||||
import type {CRUD2Schema} from 'amis';
|
||||
import type {FormSchema, SchemaCollection, SchemaObject} from 'amis';
|
||||
import {EditorNodeType} from '../store/node';
|
||||
|
||||
/**
|
||||
* 数据源所需操作,目前是因为schema从后端来
|
||||
*/
|
||||
export enum DSBehavior {
|
||||
create = 'create',
|
||||
view = 'view',
|
||||
update = 'update',
|
||||
table = 'table',
|
||||
filter = 'filter'
|
||||
}
|
||||
|
||||
export interface DSField {
|
||||
value: string;
|
||||
label: string;
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
export interface DSFieldGroup {
|
||||
value: string;
|
||||
label: string;
|
||||
children: DSField[];
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持数据源配置的一些属性名
|
||||
*/
|
||||
export enum DSGrain {
|
||||
entity = 'entity',
|
||||
list = 'list',
|
||||
piece = 'piece'
|
||||
}
|
||||
|
||||
export const DSFeature = {
|
||||
List: {
|
||||
value: 'list',
|
||||
label: '列表'
|
||||
},
|
||||
Insert: {
|
||||
value: 'insert',
|
||||
label: '新增'
|
||||
},
|
||||
View: {
|
||||
value: 'view',
|
||||
label: '详情'
|
||||
},
|
||||
Edit: {
|
||||
value: 'edit',
|
||||
label: '编辑'
|
||||
},
|
||||
Delete: {
|
||||
value: 'delete',
|
||||
label: '删除'
|
||||
},
|
||||
BulkEdit: {
|
||||
value: 'bulkEdit',
|
||||
label: '批量编辑'
|
||||
},
|
||||
BulkDelete: {
|
||||
value: 'bulkDelete',
|
||||
label: '批量删除'
|
||||
},
|
||||
Import: {
|
||||
value: 'import',
|
||||
label: '导入'
|
||||
},
|
||||
Export: {
|
||||
value: 'export',
|
||||
label: '导出'
|
||||
},
|
||||
SimpleQuery: {
|
||||
value: 'simpleQuery',
|
||||
label: '简单查询'
|
||||
},
|
||||
FuzzyQuery: {
|
||||
value: 'fuzzyQuery',
|
||||
label: '模糊查询'
|
||||
},
|
||||
AdvancedQuery: {
|
||||
value: 'advancedQuery',
|
||||
label: '高级查询'
|
||||
}
|
||||
};
|
||||
|
||||
export type DSFeatureType = keyof typeof DSFeature;
|
||||
|
||||
export interface DSSourceSettingFormConfig {
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
/** 数据源字段标题 */
|
||||
label?: string;
|
||||
/** 所需要配置的数据粒度 */
|
||||
grain?: DSGrain;
|
||||
/** 数据源所被使用的功能场景 */
|
||||
feat: DSFeatureType;
|
||||
/** 渲染器类型 */
|
||||
renderer?: string;
|
||||
/**
|
||||
* @deprecated 待废弃,使用renderer字段代替
|
||||
* 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置
|
||||
* */
|
||||
inCrud?: boolean;
|
||||
/** 是否在脚手架中 */
|
||||
inScaffold?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据源选择构造器
|
||||
*/
|
||||
export abstract class DSBuilder {
|
||||
/**
|
||||
* 数据源名字,中文,可以覆盖同名
|
||||
*/
|
||||
public static type: string;
|
||||
|
||||
public name: string;
|
||||
|
||||
// 数字越小排序越靠前
|
||||
public order: number;
|
||||
|
||||
/**
|
||||
* 数据源schema运行前转换
|
||||
*/
|
||||
public static schemaFilter?: (schema: any) => any;
|
||||
|
||||
/**
|
||||
* 根据组件、属性名判断是否可以使用这个数据源
|
||||
*/
|
||||
public static accessable: (controlType: string, propKey: string) => boolean;
|
||||
|
||||
public features: Array<keyof typeof DSFeature>;
|
||||
|
||||
/**
|
||||
* 根据值内容和schema配置状态,看是否是当前数据源
|
||||
*/
|
||||
public abstract match(value: any, schema?: SchemaObject): boolean;
|
||||
|
||||
/**
|
||||
* 生成数据源的配置表单
|
||||
*/
|
||||
public abstract makeSourceSettingForm(
|
||||
config: DSSourceSettingFormConfig
|
||||
): SchemaObject[];
|
||||
|
||||
public abstract makeFieldsSettingForm(config: {
|
||||
/** 数据源字段名 */
|
||||
sourceKey?: string;
|
||||
feat: DSFeatureType;
|
||||
inCrud?: boolean;
|
||||
inScaffold?: boolean;
|
||||
/** 初次设置字段还是选择字段 */
|
||||
setting?: boolean;
|
||||
}): SchemaObject[];
|
||||
|
||||
/**
|
||||
* 生成字段的筛选配置表单
|
||||
*/
|
||||
public abstract makeFieldFilterSetting(config: {
|
||||
/** 数据源字段名 */
|
||||
sourceKey: string;
|
||||
schema: any;
|
||||
fieldName: string;
|
||||
}): Promise<SchemaObject[]>;
|
||||
|
||||
/**
|
||||
* 数据源schema生成
|
||||
*/
|
||||
abstract resolveSourceSchema(config: {
|
||||
/** schema */
|
||||
schema: SchemaObject;
|
||||
/** 数据源配置结果 */
|
||||
setting: any;
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
feat?: DSFeatureType;
|
||||
/** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */
|
||||
inCrud?: boolean;
|
||||
inScaffold?: boolean;
|
||||
}): void;
|
||||
|
||||
/**
|
||||
* 数据删除schema生成
|
||||
*/
|
||||
abstract resolveDeleteSchema(config: {
|
||||
schema: ButtonSchema;
|
||||
setting: any;
|
||||
feat: 'BulkDelete' | 'Delete';
|
||||
name?: string;
|
||||
}): any;
|
||||
|
||||
/**
|
||||
* 生成数据创建表单schema
|
||||
*/
|
||||
abstract resolveCreateSchema(config: {
|
||||
/** schema */
|
||||
schema: FormSchema;
|
||||
/** 脚手架配置数据 */
|
||||
setting: any;
|
||||
feat: 'Insert' | 'Edit' | 'BulkEdit';
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
/** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */
|
||||
inCrud?: boolean;
|
||||
}): void;
|
||||
|
||||
/**
|
||||
* 生成数据表格列
|
||||
*/
|
||||
abstract resolveTableSchema(config: {
|
||||
/** schema */
|
||||
schema: CRUD2Schema;
|
||||
/** 脚手架配置数据 */
|
||||
setting: any;
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
/** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */
|
||||
inCrud?: boolean;
|
||||
}): void;
|
||||
|
||||
/**
|
||||
* 生成数据表格列
|
||||
*/
|
||||
abstract resolveViewSchema(config: {
|
||||
/** 脚手架配置数据 */
|
||||
setting: any;
|
||||
feat?: DSFeatureType;
|
||||
}): SchemaObject[];
|
||||
|
||||
abstract resolveSimpleFilterSchema(config: {
|
||||
setting: any;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
|
||||
}): SchemaObject[];
|
||||
|
||||
abstract resolveAdvancedFilterSchema(config: {
|
||||
setting: any;
|
||||
}): SchemaObject | void;
|
||||
|
||||
abstract makeTableColumnsByFields(fields: any[]): SchemaObject[];
|
||||
|
||||
/**
|
||||
* 当前上下文中使用的字段
|
||||
*/
|
||||
abstract getContextFileds(config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
}): Promise<DSField[] | void>;
|
||||
|
||||
/**
|
||||
* 上下文可以使用的字段
|
||||
*/
|
||||
abstract getAvailableContextFileds(
|
||||
config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
scopeNode?: EditorNodeType;
|
||||
},
|
||||
target: EditorNodeType
|
||||
): Promise<SchemaCollection | void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有的数据源构造器
|
||||
*/
|
||||
const __builders: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
export const registerDSBuilder = (builderKClass: any) => {
|
||||
__builders[builderKClass.type] = builderKClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造器管理工具,便于更好的缓存
|
||||
*/
|
||||
export class DSBuilderManager {
|
||||
/** 所有可用的数据源构造器实例 */
|
||||
builders: {
|
||||
[key: string]: DSBuilder;
|
||||
} = {};
|
||||
|
||||
get builderNum() {
|
||||
return Object.keys(this.builders).length;
|
||||
}
|
||||
|
||||
constructor(type: string, propKey: string) {
|
||||
Object.values(__builders)
|
||||
.filter(builder => builder.accessable?.(type, propKey) ?? true)
|
||||
.forEach(Builder => {
|
||||
this.builders[Builder.type] = new Builder();
|
||||
});
|
||||
}
|
||||
|
||||
resolveBuilderBySetting(setting: any) {
|
||||
return this.builders[setting.dsType] || Object.values(this.builders)[0];
|
||||
}
|
||||
|
||||
resolveBuilderBySchema(schema: any, propKey: string) {
|
||||
const builders = Object.values(this.builders);
|
||||
return (
|
||||
builders.find(builder => builder.match(schema[propKey])) || builders[0]
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultBuilderName() {
|
||||
// 先返回第一个,之后可以加一些order之类的
|
||||
const builderOptions = Object.entries(this.builders)
|
||||
.map(([key, builder]) => {
|
||||
return {
|
||||
value: key,
|
||||
order: builder.order
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
return builderOptions[0].value;
|
||||
}
|
||||
|
||||
getDSSwitch(setting: any = {}) {
|
||||
const multiSource = this.builderNum > 1;
|
||||
const builderOptions = Object.entries(this.builders).map(
|
||||
([key, builder]) => ({
|
||||
label: builder.name,
|
||||
value: key,
|
||||
order: builder.order
|
||||
})
|
||||
);
|
||||
builderOptions.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
|
||||
return {
|
||||
type: 'radios',
|
||||
label: '数据来源',
|
||||
name: 'dsType',
|
||||
visible: multiSource,
|
||||
selectFirst: true,
|
||||
options: builderOptions,
|
||||
...setting
|
||||
};
|
||||
}
|
||||
|
||||
// getDSSwitchFormForPanel(
|
||||
// propKey: string,
|
||||
// label: string
|
||||
// ) {
|
||||
// return Object.keys(this.builders).length > 1 ? {
|
||||
// type: Object.keys(this.builders).length > 3 ? 'select' : 'button-group-select',
|
||||
// options: Object.keys(this.builders).map(name => ({
|
||||
// label: name,
|
||||
// value: name
|
||||
// })),
|
||||
// name: propKey,
|
||||
// label: label,
|
||||
// pipeIn: (value: string) => {
|
||||
// const builders = Object.entries(this.builders);
|
||||
// return (builders.find(([, builder]) => {
|
||||
// return builder.match(value);
|
||||
// }) || builders[0])[0];
|
||||
// },
|
||||
// pipeOut: (value: string) => {
|
||||
// return this.builders[value].defaultSchema || {};
|
||||
// }
|
||||
// } : null;
|
||||
// }
|
||||
|
||||
collectFromBuilders(
|
||||
callee: (builder: DSBuilder, builderName: string) => any
|
||||
) {
|
||||
return Object.entries(this.builders).map(([name, builder]) => {
|
||||
return callee(builder, name);
|
||||
});
|
||||
}
|
||||
}
|
@ -39,6 +39,8 @@ export interface EditorProps extends PluginEventListener {
|
||||
amisDocHost?: string;
|
||||
superEditorData?: any;
|
||||
withSuperDataSchema?: boolean;
|
||||
/** 当前 Editor 为 SubEditor 时触发的宿主节点 */
|
||||
hostNode?: EditorNodeType;
|
||||
dataBindingChange?: (
|
||||
value: string,
|
||||
data: any,
|
||||
@ -49,7 +51,7 @@ export interface EditorProps extends PluginEventListener {
|
||||
* Preview 预览前可以修改配置。
|
||||
* 比如把api地址替换成 proxy 地址。
|
||||
*/
|
||||
schemaFilter?: (schema: any, preview?: boolean) => any;
|
||||
schemaFilter?: (schema: any, isPreview?: boolean) => any;
|
||||
amisEnv?: RenderOptions;
|
||||
|
||||
/**
|
||||
@ -126,6 +128,8 @@ export interface EditorProps extends PluginEventListener {
|
||||
) => Promise<void | boolean>;
|
||||
|
||||
getHostNodeDataSchema?: () => Promise<any>;
|
||||
|
||||
getAvaiableContextFields?: (node: EditorNodeType) => Promise<any>;
|
||||
}
|
||||
|
||||
export default class Editor extends Component<EditorProps> {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {RendererProps} from 'amis-core';
|
||||
import {RendererProps, isObject} from 'amis-core';
|
||||
import {observer} from 'mobx-react';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import merge from 'lodash/merge';
|
||||
import {RendererInfo} from '../plugin';
|
||||
import {EditorNodeType} from '../store/node';
|
||||
import {autobind} from '../util';
|
||||
import {autobind, isEmpty} from '../util';
|
||||
|
||||
export interface NodeWrapperProps extends RendererProps {
|
||||
$$editor: RendererInfo; // 当前节点信息(info)
|
||||
@ -71,6 +72,14 @@ export class NodeWrapper extends React.Component<NodeWrapperProps> {
|
||||
rest = $$editor.filterProps.call($$editor.plugin, rest, $$node);
|
||||
}
|
||||
|
||||
// 自动合并假数据
|
||||
if (
|
||||
isObject(rest.editorSetting?.mock) &&
|
||||
!isEmpty(rest.editorSetting.mock)
|
||||
) {
|
||||
rest = merge({}, rest, rest.editorSetting.mock);
|
||||
}
|
||||
|
||||
if ($$editor.renderRenderer) {
|
||||
return $$editor.renderRenderer.call(
|
||||
$$editor.plugin,
|
||||
|
@ -10,7 +10,7 @@ import {EditorNodeContext, EditorNodeType} from '../store/node';
|
||||
export interface RegionWrapperProps {
|
||||
name: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
placeholder?: string | JSX.Element;
|
||||
preferTag?: string;
|
||||
wrapperResolve?: (dom: HTMLElement) => HTMLElement;
|
||||
editorStore: EditorStoreType;
|
||||
|
@ -45,7 +45,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
body = [
|
||||
{
|
||||
type: 'steps',
|
||||
name: '__steps',
|
||||
name: '__step',
|
||||
className: 'ae-Steps',
|
||||
steps: body.map((step, index) => ({
|
||||
title: step.title,
|
||||
@ -133,7 +133,6 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
|
||||
@autobind
|
||||
goToNextStep() {
|
||||
// 不能更新props的data,控制amis不重新渲染,否则数据会重新初始化
|
||||
const store = this.props.store;
|
||||
const form = this.amisScope?.getComponents()[0].props.store;
|
||||
const step = store.scaffoldFormStep + 1;
|
||||
@ -178,13 +177,14 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
true
|
||||
);
|
||||
|
||||
this.handleConfirm([values]);
|
||||
await this.handleConfirm([values]);
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
store.setScaffoldError(e.message);
|
||||
} finally {
|
||||
store.setScaffoldBuzy(false);
|
||||
}
|
||||
|
||||
store.setScaffoldBuzy(false);
|
||||
store.setScaffoldStep(0);
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -197,8 +197,8 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
const {store, theme, manager} = this.props;
|
||||
const scaffoldFormContext = store.scaffoldForm;
|
||||
const cx = getTheme(theme || 'cxd').classnames;
|
||||
|
||||
const isStepBody = !!scaffoldFormContext?.stepsBody;
|
||||
const canSkip = !!scaffoldFormContext?.canSkip;
|
||||
const isLastStep =
|
||||
isStepBody &&
|
||||
store.scaffoldFormStep === scaffoldFormContext!.body.length - 1;
|
||||
@ -210,7 +210,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
size={scaffoldFormContext?.size || 'md'}
|
||||
contentClassName={scaffoldFormContext?.className}
|
||||
show={!!scaffoldFormContext}
|
||||
onHide={store.closeScaffoldForm}
|
||||
onHide={this.handleCancelClick}
|
||||
className="ae-scaffoldForm-Modal"
|
||||
closeOnEsc={!store.scaffoldFormBuzy}
|
||||
>
|
||||
@ -218,7 +218,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
{!store.scaffoldFormBuzy ? (
|
||||
<a
|
||||
data-position="left"
|
||||
onClick={store.closeScaffoldForm}
|
||||
onClick={this.handleCancelClick}
|
||||
className={cx('Modal-close')}
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
@ -233,7 +233,8 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
{
|
||||
data: store.scaffoldData,
|
||||
onValidate: scaffoldFormContext.validate,
|
||||
scopeRef: this.scopeRef
|
||||
scopeRef: this.scopeRef,
|
||||
manager
|
||||
},
|
||||
{
|
||||
...manager.env,
|
||||
@ -257,13 +258,29 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{isStepBody && canSkip && isFirstStep && (
|
||||
<Button
|
||||
onClick={this.handleConfirmClick}
|
||||
disabled={store.scaffoldFormBuzy}
|
||||
>
|
||||
跳过向导
|
||||
</Button>
|
||||
)}
|
||||
{isStepBody && !isFirstStep && (
|
||||
<Button level="primary" onClick={this.goToPrevStep}>
|
||||
<Button
|
||||
level="primary"
|
||||
onClick={this.goToPrevStep}
|
||||
disabled={store.scaffoldFormBuzy}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{isStepBody && !isLastStep && (
|
||||
<Button level="primary" onClick={this.goToNextStep}>
|
||||
<Button
|
||||
level="primary"
|
||||
onClick={this.goToNextStep}
|
||||
disabled={store.scaffoldFormBuzy}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
|
@ -146,6 +146,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
|
||||
ref={store.subEditorRef}
|
||||
onChange={onChange}
|
||||
data={store.subEditorContext?.data}
|
||||
hostNode={store.subEditorContext?.hostNode}
|
||||
superEditorData={superEditorData}
|
||||
schemaFilter={manager.config.schemaFilter}
|
||||
theme={manager.env.theme}
|
||||
@ -184,6 +185,9 @@ export class SubEditor extends React.Component<SubEditorProps> {
|
||||
getHostNodeDataSchema={() =>
|
||||
manager.getContextSchemas(manager.store.activeId)
|
||||
}
|
||||
getAvaiableContextFields={node =>
|
||||
manager.getAvailableContextFields(node)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export function makeWrapper(
|
||||
|
||||
// 查找父数据域,将当前组件数据域追加上去,使其形成父子关系
|
||||
if (
|
||||
rendererConfig.storeType &&
|
||||
(rendererConfig.storeType || info.isListComponent) &&
|
||||
!manager.dataSchema.hasScope(`${info.id}-${info.type}`)
|
||||
) {
|
||||
let from = parent;
|
||||
|
@ -22,8 +22,6 @@ export * from './manager';
|
||||
export * from './plugin';
|
||||
export * from './icons/index';
|
||||
export * from './mocker';
|
||||
export * from './builder/DSBuilder';
|
||||
import './builder/ApiBuilder';
|
||||
import {BasicEditor, RendererEditor} from './compat';
|
||||
import MiniEditor from './component/MiniEditor';
|
||||
import CodeEditor from './component/Panel/AMisCodeEditor';
|
||||
|
@ -3,12 +3,13 @@
|
||||
* 编辑器非 UI 相关的东西应该放在这。
|
||||
*/
|
||||
import {reaction} from 'mobx';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import {parse, stringify} from 'json-ast-comments';
|
||||
import debounce from 'lodash/debounce';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import omit from 'lodash/omit';
|
||||
import {openContextMenus, toast, alert, DataScope, DataSchema} from 'amis';
|
||||
import {getRenderers, RenderOptions, mapTree} from 'amis-core';
|
||||
import {getRenderers, RenderOptions, mapTree, isEmpty} from 'amis-core';
|
||||
import {
|
||||
PluginInterface,
|
||||
BasicPanelItem,
|
||||
@ -229,6 +230,37 @@ export class EditorManager {
|
||||
// 自动加载预先注册的自定义组件
|
||||
autoPreRegisterEditorCustomPlugins();
|
||||
|
||||
/** 在顶层对外部注册的Plugin和builtInPlugins合并去重 */
|
||||
const externalPlugins = (config?.plugins || []).forEach(external => {
|
||||
if (
|
||||
Array.isArray(external) ||
|
||||
!external.priority ||
|
||||
!Number.isInteger(external.priority)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = builtInPlugins.findIndex(
|
||||
builtIn =>
|
||||
!Array.isArray(builtIn) &&
|
||||
!Array.isArray(external) &&
|
||||
builtIn.id === external.id &&
|
||||
builtIn?.prototype instanceof BasePlugin
|
||||
);
|
||||
|
||||
if (~idx) {
|
||||
const current = builtInPlugins[idx] as PluginClass;
|
||||
const currentPriority =
|
||||
current.priority && Number.isInteger(current.priority)
|
||||
? current.priority
|
||||
: 0;
|
||||
/** 同ID Plugin根据优先级决定是否替换掉Builtin中的Plugin */
|
||||
if (external.priority > currentPriority) {
|
||||
builtInPlugins.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.plugins = (config.disableBultinPlugin ? [] : builtInPlugins) // 页面设计器注册的插件列表
|
||||
.concat(this.normalizeScene(config?.plugins))
|
||||
.filter(p => {
|
||||
@ -986,6 +1018,29 @@ export class EditorManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前节点是否可以添加同级节点
|
||||
*/
|
||||
canAppendSiblings() {
|
||||
const store = this.store;
|
||||
const id = store.activeId;
|
||||
const node = store.getNodeById(id)!; // 当前选中节点
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const regionNode = node.parent as EditorNodeType; // 父级节点
|
||||
if (
|
||||
regionNode &&
|
||||
!regionNode.region &&
|
||||
!regionNode.schema.body &&
|
||||
regionNode.schema?.type !== 'flex'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前选中元素追加新的schema
|
||||
* 备注:目前主要用在复制&粘贴快捷功能键中
|
||||
@ -1701,7 +1756,7 @@ export class EditorManager {
|
||||
patchList(node.uniqueChildren);
|
||||
}
|
||||
|
||||
if (!node.isRegion) {
|
||||
if (isAlive(node) && !node.isRegion) {
|
||||
node.patch(this.store, force);
|
||||
}
|
||||
});
|
||||
@ -1845,7 +1900,6 @@ export class EditorManager {
|
||||
return;
|
||||
}
|
||||
const plugin = node.info.plugin!;
|
||||
|
||||
const store = this.store;
|
||||
const context: PopOverFormContext = {
|
||||
node,
|
||||
@ -1918,42 +1972,74 @@ export class EditorManager {
|
||||
}
|
||||
|
||||
let nearestScope;
|
||||
let listScope = [];
|
||||
|
||||
// 更新组件树中的所有上下文数据声明为最新数据
|
||||
while (scope) {
|
||||
const [id, type] = scope.id.split('-');
|
||||
const node = this.store.getNodeById(id, type);
|
||||
const [nodeId, type] = scope.id.split('-');
|
||||
const scopeNode = this.store.getNodeById(nodeId, type);
|
||||
|
||||
// 拿非重复组件id的父组件作为主数据域展示,如CRUD,不展示表格,只展示增删改查信息,避免变量面板出现两份数据
|
||||
if (!nearestScope && node && !node.isSecondFactor) {
|
||||
if (!nearestScope && scopeNode && !scopeNode.isSecondFactor) {
|
||||
nearestScope = scope;
|
||||
}
|
||||
const jsonschema = await node?.info?.plugin?.buildDataSchemas?.(
|
||||
node,
|
||||
region,
|
||||
trigger,
|
||||
node
|
||||
);
|
||||
|
||||
const jsonschema = await scopeNode?.info?.plugin?.buildDataSchemas?.(
|
||||
scopeNode,
|
||||
region,
|
||||
trigger
|
||||
);
|
||||
if (jsonschema) {
|
||||
scope.removeSchema(jsonschema.$id);
|
||||
scope.addSchema(jsonschema);
|
||||
}
|
||||
|
||||
// 记录each列表等组件顺序
|
||||
if (scopeNode?.info?.isListComponent) {
|
||||
listScope.unshift(scope);
|
||||
|
||||
// 如果当前节点是list类型节点,当前scope从父节点上取
|
||||
if (nodeId === id) {
|
||||
nearestScope = scope.parent;
|
||||
}
|
||||
}
|
||||
|
||||
scope = withoutSuper ? undefined : scope.parent;
|
||||
}
|
||||
|
||||
// each列表类型嵌套时需要从上到下获取数据,重新执行一遍
|
||||
if (listScope.length > 1) {
|
||||
for (let scope of listScope) {
|
||||
const [id, type] = scope.id.split('-');
|
||||
const node = this.store.getNodeById(id, type);
|
||||
const jsonschema = await node?.info?.plugin?.buildDataSchemas?.(
|
||||
node,
|
||||
region,
|
||||
trigger
|
||||
);
|
||||
if (jsonschema) {
|
||||
scope.removeSchema(jsonschema.$id);
|
||||
scope.addSchema(jsonschema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 存在当前行时,找到最底层(todo:暂不考虑table套service+table的场景)
|
||||
const nearestScopeId = Object.keys(this.dataSchema.idMap).find(
|
||||
key =>
|
||||
/\-currentRow$/.test(key) &&
|
||||
!this.dataSchema.idMap[key].children?.length
|
||||
);
|
||||
const nearestScopeId =
|
||||
Object.keys(this.dataSchema.idMap).find(
|
||||
key =>
|
||||
/\-currentRow$/.test(key) &&
|
||||
!this.dataSchema.idMap[key].children?.length
|
||||
) || nearestScope?.id;
|
||||
|
||||
if (nearestScopeId) {
|
||||
this.dataSchema.switchTo(nearestScopeId);
|
||||
} else if (nearestScope?.id) {
|
||||
this.dataSchema.switchTo(nearestScope.id);
|
||||
}
|
||||
|
||||
// 如果当前容器是list非数据组件,scope从父scope开始
|
||||
if (node.info.isListComponent) {
|
||||
let lastScope = listScope[listScope.length - 1];
|
||||
this.dataSchema.switchTo(lastScope.parent!);
|
||||
}
|
||||
|
||||
return withoutSuper
|
||||
@ -1964,7 +2050,7 @@ export class EditorManager {
|
||||
/**
|
||||
* 获取可用上下文待绑定字段
|
||||
*/
|
||||
async getAvailableContextFields(node: EditorNodeType) {
|
||||
async getAvailableContextFields(node: EditorNodeType): Promise<any> {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
@ -1993,6 +2079,10 @@ export class EditorManager {
|
||||
}
|
||||
|
||||
if (!scope) {
|
||||
/** 如果在子编辑器中,继续去上层编辑器查找,不过这里可能受限于当前层的数据映射 */
|
||||
if (!from && this.store.isSubEditor) {
|
||||
return this.config?.getAvaiableContextFields?.(node);
|
||||
}
|
||||
return from?.info.plugin.getAvailableContextFields?.(from, node);
|
||||
}
|
||||
|
||||
@ -2000,7 +2090,7 @@ export class EditorManager {
|
||||
const [id, type] = scope.id.split('-');
|
||||
const scopeNode = this.store.getNodeById(id, type);
|
||||
|
||||
if (scopeNode) {
|
||||
if (scopeNode && !scopeNode.info?.isListComponent) {
|
||||
return scopeNode?.info.plugin.getAvailableContextFields?.(
|
||||
scopeNode,
|
||||
node
|
||||
|
@ -26,6 +26,8 @@ export function mockValue(schema: any) {
|
||||
return placeholderImage;
|
||||
} else if (schema.type === 'images' || schema.type === 'static-images') {
|
||||
return [placeholderImage];
|
||||
} else if (schema.type === 'number' || schema.type === 'input-number') {
|
||||
return (Math.random() * 1000).toFixed(schema.precision ?? 0);
|
||||
}
|
||||
|
||||
return '假数据';
|
||||
|
@ -36,7 +36,7 @@ export interface RegionConfig {
|
||||
/**
|
||||
* 区域占位字符,用于提示
|
||||
*/
|
||||
placeholder?: string;
|
||||
placeholder?: string | JSX.Element;
|
||||
|
||||
/**
|
||||
* 对于复杂的控件需要用到这个配置。
|
||||
@ -200,6 +200,12 @@ export interface RendererInfo extends RendererScaffoldInfo {
|
||||
|
||||
isBaseComponent?: boolean;
|
||||
|
||||
/**
|
||||
* 是否列表类型组件,自身没数据但是绑定了数据源里面的数组字段
|
||||
* 子组件需要能获取到单项字段,如list、each、cards
|
||||
*/
|
||||
isListComponent?: boolean;
|
||||
|
||||
disabledRendererPlugin?: boolean;
|
||||
|
||||
/**
|
||||
@ -342,7 +348,8 @@ export interface ScaffoldForm extends PopOverForm {
|
||||
* value 是具体错误信息。
|
||||
*/
|
||||
validate?: (
|
||||
values: any
|
||||
values: any,
|
||||
formStore: any
|
||||
) =>
|
||||
| void
|
||||
| {[propName: string]: string}
|
||||
@ -820,6 +827,9 @@ export interface PluginInterface
|
||||
region?: EditorNodeType
|
||||
) => Promise<SchemaCollection | void>;
|
||||
|
||||
/** 配置面板表单的 pipeOut function */
|
||||
panelFormPipeOut?: (value: any) => any;
|
||||
|
||||
/**
|
||||
* @deprecated 用 panelBodyCreator
|
||||
*/
|
||||
@ -1035,6 +1045,7 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
scaffoldForm: plugin.scaffoldForm,
|
||||
disabledRendererPlugin: plugin.disabledRendererPlugin,
|
||||
isBaseComponent: plugin.isBaseComponent,
|
||||
isListComponent: plugin.isListComponent,
|
||||
rendererName: plugin.rendererName
|
||||
};
|
||||
}
|
||||
@ -1082,6 +1093,18 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
plugin
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
definitions: plugin.panelDefinitions,
|
||||
submitOnChange: plugin.panelSubmitOnChange,
|
||||
api: plugin.panelApi,
|
||||
controls: plugin.panelControlsCreator
|
||||
? plugin.panelControlsCreator(context)
|
||||
: plugin.panelControls!,
|
||||
justify: plugin.panelJustify,
|
||||
panelById: store.activeId,
|
||||
pipeOut: plugin.panelFormPipeOut?.bind?.(plugin)
|
||||
};
|
||||
|
||||
panels.push({
|
||||
key: 'config',
|
||||
icon: plugin.panelIcon || plugin.icon || 'fa fa-cog',
|
||||
@ -1092,27 +1115,13 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
const panelBody = await (body as Promise<SchemaCollection>);
|
||||
|
||||
return this.manager.makeSchemaFormRender({
|
||||
definitions: plugin.panelDefinitions,
|
||||
submitOnChange: plugin.panelSubmitOnChange,
|
||||
api: plugin.panelApi,
|
||||
body: panelBody,
|
||||
controls: plugin.panelControlsCreator
|
||||
? plugin.panelControlsCreator(context)
|
||||
: plugin.panelControls!,
|
||||
justify: plugin.panelJustify,
|
||||
panelById: store.activeId
|
||||
...baseProps,
|
||||
body: panelBody
|
||||
});
|
||||
}, omit(plugin.async, 'enable'))
|
||||
: this.manager.makeSchemaFormRender({
|
||||
definitions: plugin.panelDefinitions,
|
||||
submitOnChange: plugin.panelSubmitOnChange,
|
||||
api: plugin.panelApi,
|
||||
body: body as SchemaCollection,
|
||||
controls: plugin.panelControlsCreator
|
||||
? plugin.panelControlsCreator(context)
|
||||
: plugin.panelControls!,
|
||||
justify: plugin.panelJustify,
|
||||
panelById: store.activeId
|
||||
...baseProps,
|
||||
body: body as SchemaCollection
|
||||
})
|
||||
});
|
||||
} else if (
|
||||
|
@ -233,7 +233,7 @@ export const MainStore = types
|
||||
// 给预览状态时的
|
||||
get filteredSchemaForPreview() {
|
||||
const schema = JSONPipeOut(self.schema);
|
||||
return getEnv(self).schemaFilter?.(schema) ?? schema;
|
||||
return getEnv(self).schemaFilter?.(schema, true) ?? schema;
|
||||
},
|
||||
|
||||
// 判断当前元素是否是根节点
|
||||
|
@ -544,6 +544,7 @@ export const EditorNode = types
|
||||
if (node.id === 'root') {
|
||||
return;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,6 +565,10 @@ export const EditorNode = types
|
||||
break;
|
||||
}
|
||||
|
||||
if (cursor.id === 'root') {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
cursor = cursor.parent;
|
||||
}
|
||||
|
||||
|
BIN
packages/amis-editor-core/static/indication.png
Normal file
BIN
packages/amis-editor-core/static/indication.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 B |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-editor",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2-alpha.0",
|
||||
"description": "amis 可视化编辑器",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
1440
packages/amis-editor/src/builder/ApiDSBuilder.ts
Normal file
1440
packages/amis-editor/src/builder/ApiDSBuilder.ts
Normal file
File diff suppressed because it is too large
Load Diff
286
packages/amis-editor/src/builder/DSBuilder.ts
Normal file
286
packages/amis-editor/src/builder/DSBuilder.ts
Normal file
@ -0,0 +1,286 @@
|
||||
/**
|
||||
* @file DSBuilder.ts
|
||||
* @desc 数据源配置构建器
|
||||
*/
|
||||
|
||||
import {EditorManager} from 'amis-editor-core';
|
||||
import {getFeatValueByKey, getFeatLabelByKey} from './utils';
|
||||
|
||||
import type {EditorNodeType} from 'amis-editor-core';
|
||||
import type {
|
||||
DSFeatureType,
|
||||
GenericSchema,
|
||||
CRUDScaffoldConfig,
|
||||
FormScaffoldConfig
|
||||
} from './type';
|
||||
|
||||
export interface DSBuilderBaseOptions {
|
||||
/** 渲染器类型 */
|
||||
renderer: string;
|
||||
/** Form应用场景 */
|
||||
feat?: DSFeatureType;
|
||||
/** CRUD应用场景 */
|
||||
feats?: DSFeatureType[];
|
||||
/** 当前组件的 Schema */
|
||||
schema?: GenericSchema;
|
||||
/** 数据源字段名 */
|
||||
sourceKey?: string;
|
||||
/** 是否在脚手架环境中 */
|
||||
inScaffold?: boolean;
|
||||
/** 如果为列表类容器,则会返回对应的节点 */
|
||||
scopeNode?: EditorNodeType;
|
||||
/** 数据源控件配置项 */
|
||||
sourceSettings?: Record<string, any>;
|
||||
/** 字段控件配置项 */
|
||||
fieldSettings?: Record<string, any>;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface DSBuilderInterface<
|
||||
T extends DSBuilderBaseOptions = DSBuilderBaseOptions
|
||||
> {
|
||||
/** 数据源中文名称,主要用于前端展示 */
|
||||
readonly name: string;
|
||||
|
||||
/** 构造器排序权重,数字越小排序越靠前,支持负数 */
|
||||
readonly order: number;
|
||||
|
||||
/** 数据源支持的功能场景 */
|
||||
readonly features: DSFeatureType[];
|
||||
|
||||
/** 是否为默认 */
|
||||
isDefault?: boolean;
|
||||
|
||||
/** 实例获取数据源的key */
|
||||
key: string;
|
||||
|
||||
/** 是否禁用 */
|
||||
disabledOn?: () => boolean;
|
||||
|
||||
/** 获取功能场景的value */
|
||||
getFeatValueByKey(feat: DSFeatureType): string;
|
||||
|
||||
/** 获取功能场景的label */
|
||||
getFeatLabelByKey(feat: DSFeatureType): string;
|
||||
|
||||
/** 按照功能场景过滤 */
|
||||
filterByFeat(feat: any): boolean;
|
||||
|
||||
/** 根据schema,判断是否匹配当前数据源 */
|
||||
match(schema?: any): boolean;
|
||||
|
||||
/** 当前上下文中使用的字段 */
|
||||
getContextFields(options: T): Promise<any>;
|
||||
|
||||
/** 当前上下文可以使用的字段 */
|
||||
getAvailableContextFields(
|
||||
options: Omit<T, 'renderer'>,
|
||||
target: EditorNodeType
|
||||
): Promise<any>;
|
||||
|
||||
/** 获取CRUD列表字段 */
|
||||
getCRUDListFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 获取CRUD简单查询字段 */
|
||||
getCRUDSimpleQueryFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 构建简单查询表单项 */
|
||||
buildSimpleQueryCollectionSchema?: (
|
||||
options: T
|
||||
) => Promise<GenericSchema[] | undefined>;
|
||||
|
||||
/** 获取CRUD高级查询字段 */
|
||||
getCRUDAdvancedQueryFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 构建高级查询 */
|
||||
buildAdvancedQuerySchema?: (options: T) => Promise<GenericSchema | undefined>;
|
||||
|
||||
/** 获取CRUD模糊查询字段 */
|
||||
getCRUDFuzzyQueryFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 构建模糊查询 */
|
||||
buildFuzzyQuerySchema?: (options: T) => Promise<GenericSchema | undefined>;
|
||||
|
||||
/** 构造数据源的可视化配置表单 */
|
||||
makeSourceSettingForm(options: T): any[];
|
||||
|
||||
/** 构造数据源字段的可视化配置表单 */
|
||||
makeFieldsSettingForm(options: T): any[];
|
||||
|
||||
/** 新建数据 */
|
||||
buildInsertSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 编辑数据 */
|
||||
buildEditSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 批量编辑数据 */
|
||||
buildBulkEditSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 查看详情数据 */
|
||||
buildViewSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 删除数据 */
|
||||
buildCRUDDeleteSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 批量删除数据 */
|
||||
buildCRUDBulkDeleteSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 构建 CRUD 的顶部工具栏 */
|
||||
buildCRUDHeaderToolbar?: (
|
||||
options: T,
|
||||
componentId?: string
|
||||
) => Promise<GenericSchema>;
|
||||
|
||||
/** 表格的表头查询 */
|
||||
buildCRUDFilterSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 表格单列 */
|
||||
buildCRUDColumn?: (
|
||||
field: Record<string, any>,
|
||||
options: T,
|
||||
componentId?: string
|
||||
) => Promise<any>;
|
||||
|
||||
/** 表格操作列 */
|
||||
buildCRUDOpColumn?: (options: T, componentId?: string) => Promise<any>;
|
||||
|
||||
/** 表格列 */
|
||||
buildCRUDColumnsSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 表格构建 */
|
||||
buildCRUDSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表单构建 */
|
||||
buildFormSchema(options: T): Promise<any>;
|
||||
|
||||
/** 基于 schema 还原CRUD脚手架配置 */
|
||||
guessCRUDScaffoldConfig<T extends CRUDScaffoldConfig<any, any>>(options: {
|
||||
schema: GenericSchema;
|
||||
[propName: string]: any;
|
||||
}): Promise<T> | T;
|
||||
|
||||
/** 基于 schema 还原Form脚手架配置 */
|
||||
guessFormScaffoldConfig<T extends FormScaffoldConfig<any, any>>(options: {
|
||||
schema: GenericSchema;
|
||||
[propName: string]: any;
|
||||
}): Promise<T> | T;
|
||||
|
||||
/** 重新构建 API 配置 */
|
||||
buildApiSchema(options: T): Promise<any>;
|
||||
}
|
||||
|
||||
export abstract class DSBuilder<T extends DSBuilderBaseOptions>
|
||||
implements DSBuilderInterface<T>
|
||||
{
|
||||
static key: string;
|
||||
readonly name: string;
|
||||
readonly order: number;
|
||||
/** 是否为默认 */
|
||||
readonly isDefault?: boolean;
|
||||
|
||||
features: DSFeatureType[];
|
||||
|
||||
constructor(readonly manager: EditorManager) {}
|
||||
|
||||
/** 实例获取数据源的key */
|
||||
get key() {
|
||||
return (this.constructor as typeof DSBuilder<T>).key;
|
||||
}
|
||||
|
||||
/** 获取功能场景的value */
|
||||
getFeatValueByKey(feat: DSFeatureType) {
|
||||
return getFeatValueByKey(feat);
|
||||
}
|
||||
|
||||
/** 获取功能场景的label */
|
||||
getFeatLabelByKey(feat: DSFeatureType) {
|
||||
return getFeatLabelByKey(feat);
|
||||
}
|
||||
|
||||
filterByFeat(feat: any) {
|
||||
return feat && this.features.includes(feat);
|
||||
}
|
||||
|
||||
abstract match(schema?: any): boolean;
|
||||
|
||||
abstract getContextFields(options: T): Promise<any>;
|
||||
|
||||
abstract getAvailableContextFields(
|
||||
options: Omit<T, 'renderer'>,
|
||||
target: EditorNodeType
|
||||
): Promise<any>;
|
||||
|
||||
abstract makeSourceSettingForm(options: T): any[];
|
||||
|
||||
abstract makeFieldsSettingForm(options: T): any[];
|
||||
|
||||
/** 新建数据 */
|
||||
abstract buildInsertSchema(options: T): Promise<any>;
|
||||
|
||||
/** 查看详情数据 */
|
||||
abstract buildViewSchema(options: T): Promise<any>;
|
||||
|
||||
/** 编辑数据 */
|
||||
abstract buildEditSchema(options: T): Promise<any>;
|
||||
|
||||
/** 批量编辑数据 */
|
||||
abstract buildBulkEditSchema(options: T): Promise<any>;
|
||||
|
||||
/** 删除数据 */
|
||||
abstract buildCRUDDeleteSchema(options: T): Promise<any>;
|
||||
|
||||
/** 批量删除数据 */
|
||||
abstract buildCRUDBulkDeleteSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表格的表头查询 */
|
||||
abstract buildCRUDFilterSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表格列 */
|
||||
abstract buildCRUDColumnsSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表格 */
|
||||
abstract buildCRUDSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表单 */
|
||||
abstract buildFormSchema(options: T): Promise<any>;
|
||||
|
||||
/** 基于 schema 还原CRUD脚手架配置 */
|
||||
abstract guessCRUDScaffoldConfig<
|
||||
T extends CRUDScaffoldConfig<any, any>
|
||||
>(options: {schema: GenericSchema; [propName: string]: any}): Promise<T> | T;
|
||||
|
||||
/** 基于 schema 还原Form脚手架配置 */
|
||||
abstract guessFormScaffoldConfig<
|
||||
T extends FormScaffoldConfig<any, any>
|
||||
>(options: {schema: GenericSchema; [propName: string]: any}): Promise<T> | T;
|
||||
|
||||
abstract buildApiSchema(options: T): Promise<any>;
|
||||
}
|
||||
|
||||
export interface DSBuilderClass {
|
||||
new (manager: EditorManager): DSBuilderInterface;
|
||||
/** 数据源类型,使用英文,可以覆盖同名 */
|
||||
key: string;
|
||||
}
|
||||
|
||||
export const builderFactory = new Map<string, DSBuilderClass>();
|
||||
|
||||
/** 注册数据源构造器 */
|
||||
export const registerDSBuilder = (klass: DSBuilderClass) => {
|
||||
if (builderFactory.has(klass.key)) {
|
||||
console.warn(
|
||||
`[amis-editor][DSBuilder] duplicate DSBuilder「${klass.key}」`
|
||||
);
|
||||
}
|
||||
|
||||
/** 重名覆盖 */
|
||||
builderFactory.set(klass.key, klass);
|
||||
};
|
115
packages/amis-editor/src/builder/DSBuilderManager.ts
Normal file
115
packages/amis-editor/src/builder/DSBuilderManager.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @file DSBuilderManager
|
||||
* @desc 数据源构造管理器
|
||||
*/
|
||||
|
||||
import {builderFactory, DSBuilderInterface} from './DSBuilder';
|
||||
import {EditorManager} from 'amis-editor-core';
|
||||
|
||||
export class DSBuilderManager {
|
||||
private builders: Map<string, DSBuilderInterface>;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
this.builders = new Map();
|
||||
|
||||
builderFactory.forEach((Builder, key) => {
|
||||
this.builders.set(key, new Builder(manager));
|
||||
});
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.builders.size;
|
||||
}
|
||||
|
||||
getBuilderByKey(key: string) {
|
||||
return this.builders.get(key);
|
||||
}
|
||||
|
||||
getBuilderByScaffoldSetting(scaffoldConfig: any) {
|
||||
return this.builders.get(scaffoldConfig.dsType);
|
||||
}
|
||||
|
||||
getBuilderBySchema(schema: any) {
|
||||
let builder: DSBuilderInterface | undefined;
|
||||
|
||||
for (let [key, value] of Array.from(this.builders.entries())) {
|
||||
if (value.match(schema)) {
|
||||
builder = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder ? builder : this.getDefaultBuilder();
|
||||
}
|
||||
|
||||
getDefaultBuilderKey() {
|
||||
const collections = Array.from(this.builders.entries()).filter(
|
||||
([_, builder]) => builder?.disabledOn?.() !== true
|
||||
);
|
||||
const [defaultKey, _] =
|
||||
collections.find(([_, builder]) => builder.isDefault === true) ??
|
||||
collections.sort((lhs, rhs) => {
|
||||
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
|
||||
})?.[0] ??
|
||||
[];
|
||||
|
||||
return defaultKey;
|
||||
}
|
||||
|
||||
getDefaultBuilder() {
|
||||
const collections = Array.from(this.builders.entries()).filter(
|
||||
([_, builder]) => builder?.disabledOn?.() !== true
|
||||
);
|
||||
const [_, defaultBuilder] =
|
||||
collections.find(([_, builder]) => builder.isDefault === true) ??
|
||||
collections.sort((lhs, rhs) => {
|
||||
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
|
||||
})?.[0] ??
|
||||
[];
|
||||
|
||||
return defaultBuilder;
|
||||
}
|
||||
|
||||
getAvailableBuilders() {
|
||||
return Array.from(this.builders.entries())
|
||||
.filter(([_, builder]) => builder?.disabledOn?.() !== true)
|
||||
.sort((lhs, rhs) => {
|
||||
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
getDSSelectorSchema(patch: Record<string, any>) {
|
||||
const builders = this.getAvailableBuilders();
|
||||
const options = builders.map(([key, builder]) => ({
|
||||
label: builder.name,
|
||||
value: key
|
||||
}));
|
||||
|
||||
return {
|
||||
type: 'radios',
|
||||
label: '数据来源',
|
||||
name: 'dsType',
|
||||
visible: options.length > 0,
|
||||
selectFirst: true,
|
||||
options: options,
|
||||
...patch
|
||||
};
|
||||
}
|
||||
|
||||
buildCollectionFromBuilders(
|
||||
callback: (
|
||||
builder: DSBuilderInterface,
|
||||
builderKey: string,
|
||||
index: number
|
||||
) => any
|
||||
) {
|
||||
const builders = this.getAvailableBuilders();
|
||||
const collection = builders
|
||||
.map(([key, builder], index) => {
|
||||
return callback(builder, key, index);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
130
packages/amis-editor/src/builder/constants.ts
Normal file
130
packages/amis-editor/src/builder/constants.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @file constants.ts
|
||||
* @desc builder 相关常量
|
||||
*/
|
||||
|
||||
import {FormOperatorValue, FormOperator} from './type';
|
||||
|
||||
/**
|
||||
* 数据源所需操作,目前是因为schema从后端来
|
||||
*/
|
||||
export enum DSBehavior {
|
||||
/** 创建操作 */
|
||||
create = 'create',
|
||||
/** 查询操作 */
|
||||
view = 'view',
|
||||
/** 更新操作 */
|
||||
update = 'update',
|
||||
table = 'table',
|
||||
filter = 'filter'
|
||||
}
|
||||
|
||||
/** 数据粒度 */
|
||||
export enum DSGrain {
|
||||
/** 实体 */
|
||||
entity = 'entity',
|
||||
/** 多条数据 */
|
||||
list = 'list',
|
||||
/** 单条数据 */
|
||||
piece = 'piece'
|
||||
}
|
||||
|
||||
/** 数据源所使用的功能场景 */
|
||||
export const DSFeature = {
|
||||
List: {
|
||||
value: 'list',
|
||||
label: '列表'
|
||||
},
|
||||
Insert: {
|
||||
value: 'insert',
|
||||
label: '新增'
|
||||
},
|
||||
View: {
|
||||
value: 'view',
|
||||
label: '详情'
|
||||
},
|
||||
Edit: {
|
||||
value: 'edit',
|
||||
label: '编辑'
|
||||
},
|
||||
Delete: {
|
||||
value: 'delete',
|
||||
label: '删除'
|
||||
},
|
||||
BulkEdit: {
|
||||
value: 'bulkEdit',
|
||||
label: '批量编辑'
|
||||
},
|
||||
BulkDelete: {
|
||||
value: 'bulkDelete',
|
||||
label: '批量删除'
|
||||
},
|
||||
Import: {
|
||||
value: 'import',
|
||||
label: '导入'
|
||||
},
|
||||
Export: {
|
||||
value: 'export',
|
||||
label: '导出'
|
||||
},
|
||||
SimpleQuery: {
|
||||
value: 'simpleQuery',
|
||||
label: '简单查询'
|
||||
},
|
||||
FuzzyQuery: {
|
||||
value: 'fuzzyQuery',
|
||||
label: '模糊查询'
|
||||
},
|
||||
AdvancedQuery: {
|
||||
value: 'advancedQuery',
|
||||
label: '高级查询'
|
||||
}
|
||||
};
|
||||
|
||||
export enum DSFeatureEnum {
|
||||
List = 'List',
|
||||
Insert = 'Insert',
|
||||
View = 'View',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
BulkEdit = 'BulkEdit',
|
||||
BulkDelete = 'BulkDelete',
|
||||
Import = 'Import',
|
||||
Export = 'Export',
|
||||
SimpleQuery = 'SimpleQuery',
|
||||
FuzzyQuery = 'FuzzyQuery',
|
||||
AdvancedQuery = 'AdvancedQuery'
|
||||
}
|
||||
|
||||
export const DSFeatureList = Object.keys(
|
||||
DSFeature
|
||||
) as (keyof typeof DSFeature)[];
|
||||
|
||||
export const FormOperatorMap: Record<FormOperatorValue, FormOperator> = {
|
||||
cancel: {
|
||||
label: '取消',
|
||||
value: 'cancel',
|
||||
order: 0,
|
||||
schema: {
|
||||
level: 'default'
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
label: '重置',
|
||||
value: 'reset',
|
||||
order: 1,
|
||||
schema: {
|
||||
level: 'default'
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
label: '提交',
|
||||
value: 'submit',
|
||||
order: 2,
|
||||
schema: {
|
||||
level: 'primary'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ModelDSBuilderKey = 'model-entity';
|
7
packages/amis-editor/src/builder/index.ts
Normal file
7
packages/amis-editor/src/builder/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './type';
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
export * from './DSBuilder';
|
||||
export * from './DSBuilderManager';
|
||||
|
||||
import './ApiDSBuilder';
|
122
packages/amis-editor/src/builder/type.ts
Normal file
122
packages/amis-editor/src/builder/type.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @file type.ts
|
||||
* @desc builder 相关声明
|
||||
*/
|
||||
|
||||
import {DSFeature} from './constants';
|
||||
import type {BaseApiObject} from 'amis-core';
|
||||
|
||||
export interface DSField {
|
||||
value: string;
|
||||
label: string;
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
/** 数据源字段集合 */
|
||||
export interface DSFieldGroup {
|
||||
value: string;
|
||||
label: string;
|
||||
children: DSField[];
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
export type DSFeatureType = keyof typeof DSFeature;
|
||||
|
||||
export type GenericSchema = Record<string, any>;
|
||||
|
||||
export type DSRendererType = 'form' | 'crud' | 'service';
|
||||
|
||||
export interface ScaffoldField {
|
||||
/** 标题 */
|
||||
label: string;
|
||||
/** 字段名 */
|
||||
name: string;
|
||||
/** 展示控件类型 */
|
||||
displayType: string;
|
||||
/** 输入控件类型 */
|
||||
inputType: string;
|
||||
typeKey?: string;
|
||||
/** 是否启用 */
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
/** 表单操作 */
|
||||
export type ApiConfig = string | BaseApiObject;
|
||||
|
||||
/** 表单操作 */
|
||||
export type FormOperatorValue = 'cancel' | 'reset' | 'submit';
|
||||
|
||||
/** 表单操作按钮 */
|
||||
export interface FormOperator {
|
||||
label: string;
|
||||
value: FormOperatorValue;
|
||||
order: number;
|
||||
schema: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ScaffoldConfigBase {
|
||||
/** 数据源类型 */
|
||||
dsType: string;
|
||||
/** 重新构建时用户的原始 Schema */
|
||||
__pristineSchema?: Record<string, any>;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface FormScaffoldConfig<
|
||||
Fields extends Record<string, any> = ScaffoldField,
|
||||
API extends any = ApiConfig
|
||||
> extends ScaffoldConfigBase {
|
||||
/** Form功能场景 */
|
||||
feat?: DSFeatureType;
|
||||
/** 表单初始化接口 */
|
||||
initApi?: API;
|
||||
insertApi?: API;
|
||||
editApi?: API;
|
||||
bulkEditApi?: API;
|
||||
insertFields?: Fields[];
|
||||
editFields?: Fields[];
|
||||
bulkEditFields?: Fields[];
|
||||
operators?: FormOperator[];
|
||||
}
|
||||
|
||||
export interface CRUDScaffoldConfig<
|
||||
Fields extends Record<string, any> = ScaffoldField,
|
||||
API extends any = ApiConfig
|
||||
> extends ScaffoldConfigBase {
|
||||
/** 工具栏 */
|
||||
tools?: Extract<DSFeatureType, 'Insert' | 'BulkDelete' | 'BulkEdit'>[];
|
||||
/** 数据操作 */
|
||||
operators?: Extract<DSFeatureType, 'View' | 'Edit' | 'Delete'>[];
|
||||
/** 条件查询 */
|
||||
filters?: Extract<
|
||||
DSFeatureType,
|
||||
'FuzzyQuery' | 'SimpleQuery' | 'AdvancedQuery'
|
||||
>[];
|
||||
/** 表格 list 接口 */
|
||||
listApi?: API;
|
||||
viewApi?: API;
|
||||
editApi?: API;
|
||||
/** 编辑表单的初始化接口 */
|
||||
initApi?: API;
|
||||
bulkEditApi?: API;
|
||||
deleteApi?: API;
|
||||
bulkDeleteApi?: API;
|
||||
insertApi?: API;
|
||||
listFields?: Fields[];
|
||||
insertFields?: Fields[];
|
||||
viewFields?: Fields[];
|
||||
editFields?: Fields[];
|
||||
bulkEditFields?: Fields[];
|
||||
fuzzyQueryFields?: Fields[];
|
||||
simpleQueryFields?: Fields[];
|
||||
advancedQueryFields?: Fields[];
|
||||
importFields?: Fields[];
|
||||
exportFields?: Fields[];
|
||||
/** 表格脚手架时的主键 */
|
||||
primaryField?: string;
|
||||
}
|
||||
|
||||
export type ScaffoldConfig<
|
||||
Fields extends Record<string, any> = ScaffoldField,
|
||||
API extends any = ApiConfig
|
||||
> = FormScaffoldConfig<Fields, API> | CRUDScaffoldConfig<Fields, API>;
|
95
packages/amis-editor/src/builder/utils.ts
Normal file
95
packages/amis-editor/src/builder/utils.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @file utils
|
||||
* @desc builder用到的 utils
|
||||
*/
|
||||
|
||||
import isObjectLike from 'lodash/isObjectLike';
|
||||
import {DSFeature} from './constants';
|
||||
import type {DSFeatureType} from './type';
|
||||
|
||||
export const getFeatValueByKey = (feat: DSFeatureType) => {
|
||||
return `${DSFeature?.[feat]?.value}`;
|
||||
};
|
||||
|
||||
export const getFeatLabelByKey = (feat: DSFeatureType) => {
|
||||
return `${DSFeature?.[feat]?.label}`;
|
||||
};
|
||||
|
||||
const _traverseSchemaDeep = (
|
||||
schema: Record<string, any>,
|
||||
mapper: (originKey: string, originValue: any, origin: any) => any[],
|
||||
cache = new WeakMap()
|
||||
) => {
|
||||
const target: Record<string, any> = {};
|
||||
|
||||
if (cache.has(schema)) {
|
||||
return cache.get(schema);
|
||||
}
|
||||
|
||||
cache.set(schema, target);
|
||||
|
||||
const mapArray = (arr: any[]): any =>
|
||||
arr.map((item: any) => {
|
||||
return isObjectLike(item)
|
||||
? _traverseSchemaDeep(item, mapper, cache)
|
||||
: item;
|
||||
});
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return mapArray(schema);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
const result = mapper(key, value, schema);
|
||||
|
||||
let [updatedKey, updatedValue] = result;
|
||||
|
||||
if (updatedKey === '__proto__') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isObjectLike(updatedValue)) {
|
||||
updatedValue = Array.isArray(updatedValue)
|
||||
? mapArray(updatedValue)
|
||||
: _traverseSchemaDeep(updatedValue, mapper, cache);
|
||||
}
|
||||
|
||||
target[updatedKey] = updatedValue;
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
export const traverseSchemaDeep = (
|
||||
schema: Record<string, any>,
|
||||
mapper: (originKey: string, originValue: any, origin: any) => any[]
|
||||
) => {
|
||||
if (!isObjectLike(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
return _traverseSchemaDeep(schema, mapper);
|
||||
};
|
||||
|
||||
/** CRUD列类型转 Form 表单类型 */
|
||||
export const displayType2inputType = (inputType: string): string => {
|
||||
if (!inputType || typeof inputType !== 'string') {
|
||||
return inputType;
|
||||
}
|
||||
|
||||
const map: Record<string, string> = {
|
||||
tpl: 'input-text',
|
||||
image: 'input-image',
|
||||
date: 'input-date',
|
||||
progress: 'input-number',
|
||||
status: 'tag',
|
||||
mapping: 'tag',
|
||||
list: 'input-table'
|
||||
};
|
||||
|
||||
return map.hasOwnProperty(inputType) ? map[inputType] : inputType;
|
||||
};
|
1
packages/amis-editor/src/icons/crud/column-add.svg
Normal file
1
packages/amis-editor/src/icons/crud/column-add.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class=""><path d="M2.66699 8L13.3337 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><path d="M8 2.66699L8 13.3337" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>
|
After Width: | Height: | Size: 315 B |
1
packages/amis-editor/src/icons/crud/column-delete.svg
Normal file
1
packages/amis-editor/src/icons/crud/column-delete.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6.19561 6.20513L14.5925 14.4728M5.83984 14.3673L14.2367 6.09961" stroke="currentColor" stroke-width="1.6"></path></svg>
|
After Width: | Height: | Size: 190 B |
1
packages/amis-editor/src/icons/crud/column-setting.svg
Normal file
1
packages/amis-editor/src/icons/crud/column-setting.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class=""><path d="M3 6.5C2.17157 6.5 1.5 7.17157 1.5 8C1.5 8.82843 2.17157 9.5 3 9.5C3.82843 9.5 4.5 8.82843 4.5 8C4.5 7.17157 3.82843 6.5 3 6.5Z" fill="currentColor"></path><path d="M6.5 8C6.5 7.17157 7.17157 6.5 8 6.5C8.82843 6.5 9.5 7.17157 9.5 8C9.5 8.82843 8.82843 9.5 8 9.5C7.17157 9.5 6.5 8.82843 6.5 8Z" fill="currentColor"></path><path d="M13 6.5C12.1716 6.5 11.5 7.17157 11.5 8C11.5 8.82843 12.1716 9.5 13 9.5C13.8284 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.8284 6.5 13 6.5Z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 615 B |
2
packages/amis-editor/src/icons/crud/share-link.svg
Normal file
2
packages/amis-editor/src/icons/crud/share-link.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2532" width="64" height="64"><path d="M938.666667 469.333333c-23.466667 0-42.666667 19.2-42.666667 42.666667l0 85.333333 0 320c0 12.8-8.533333 21.333333-21.333333 21.333333L106.666667 938.666667c-12.8 0-21.333333-8.533333-21.333333-21.333333l0-106.666667L85.333333 234.666667c0-12.8 8.533333-21.333333 21.333333-21.333333l362.666667 0c23.466667 0 42.666667-19.2 42.666667-42.666667 0-23.466667-19.2-42.666667-42.666667-42.666667L85.333333 128c-46.933333 0-85.333333 38.4-85.333333 85.333333l0 341.333333 0 256 0 128c0 46.933333 38.4 85.333333 85.333333 85.333333l810.666667 0c46.933333 0 85.333333-38.4 85.333333-85.333333l0-128L981.333333 597.333333l0-85.333333C981.333333 488.533333 962.133333 469.333333 938.666667 469.333333zM1011.2 162.133333l-149.333333-149.333333C855.466667 4.266667 844.8 0 832 0c-23.466667 0-42.666667 19.2-42.666667 42.666667 0 12.8 4.266667 23.466667 12.8 29.866667l83.2 83.2C554.666667 202.666667 298.666667 488.533333 298.666667 832c0 23.466667 19.2 42.666667 42.666667 42.666667s42.666667-19.2 42.666667-42.666667c0-290.133333 206.933333-533.333333 484.266667-586.666667l-66.133333 66.133333C793.6 317.866667 789.333333 328.533333 789.333333 341.333333c0 23.466667 19.2 42.666667 42.666667 42.666667 12.8 0 23.466667-4.266667 29.866667-12.8l149.333333-149.333333C1019.733333 215.466667 1024 204.8 1024 192 1024 179.2 1019.733333 168.533333 1011.2 162.133333z" p-id="2533"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -183,6 +183,12 @@ import jSpaceAround from './display/jSpaceAround.svg';
|
||||
// 主题
|
||||
import themeCss from './theme/css.svg';
|
||||
|
||||
// CRUD相关
|
||||
import ColumnSetting from './crud/column-setting.svg';
|
||||
import ColumnDelete from './crud/column-delete.svg';
|
||||
import ColumnAdd from './crud/column-add.svg';
|
||||
import ShareLink from './crud/share-link.svg';
|
||||
|
||||
// 功能类组件 icon x 11
|
||||
registerIcon('audio-plugin', audio);
|
||||
registerIcon('custom-plugin', custom);
|
||||
@ -351,4 +357,10 @@ registerIcon('jSpaceAround', jSpaceAround);
|
||||
// 主题
|
||||
registerIcon('theme-css', themeCss);
|
||||
|
||||
// CRUD相关
|
||||
registerIcon('column-setting', ColumnSetting);
|
||||
registerIcon('column-delete', ColumnDelete);
|
||||
registerIcon('column-add', ColumnAdd);
|
||||
registerIcon('share-link', ShareLink);
|
||||
|
||||
export {Icon};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'amis';
|
||||
import './locale/index';
|
||||
export * from 'amis-editor-core';
|
||||
export * from './builder';
|
||||
import './tpl/index';
|
||||
export * from './plugin';
|
||||
|
||||
@ -40,6 +41,11 @@ import './renderer/TransferTableControl';
|
||||
import './renderer/style-control/ThemeCssCode';
|
||||
import './renderer/ButtonGroupControl';
|
||||
import './renderer/FlexSettingControl';
|
||||
import './renderer/FieldSetting';
|
||||
import './renderer/TableColumnWidthControl';
|
||||
import './renderer/crud2-control/CRUDColumnControl';
|
||||
import './renderer/crud2-control/CRUDToolbarControl';
|
||||
import './renderer/crud2-control/CRUDFiltersControl';
|
||||
import 'amis-theme-editor/lib/locale/zh-CN';
|
||||
import 'amis-theme-editor/lib/locale/en-US';
|
||||
import 'amis-theme-editor/lib/renderers/Border';
|
||||
|
File diff suppressed because it is too large
Load Diff
1295
packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx
Normal file
1295
packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx
Normal file
File diff suppressed because it is too large
Load Diff
149
packages/amis-editor/src/plugin/CRUD2/CRUDCards.tsx
Normal file
149
packages/amis-editor/src/plugin/CRUD2/CRUDCards.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @file CRUDCards.tsx
|
||||
* @desc 卡片模式的 CRUD2
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {autobind} from 'amis';
|
||||
import {
|
||||
EditorManager,
|
||||
JSONPipeIn,
|
||||
BuildPanelEventContext,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {DSBuilderManager, DSFeatureEnum} from '../../builder';
|
||||
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
|
||||
import {BaseCRUDPlugin} from './BaseCRUD';
|
||||
|
||||
export class CRUDCardsPlugin extends BaseCRUDPlugin {
|
||||
static id = 'CardsCRUDPlugin';
|
||||
|
||||
disabledRendererPlugin = true;
|
||||
|
||||
name = '卡片列表';
|
||||
|
||||
panelTitle: '卡片列表';
|
||||
|
||||
icon = 'fa fa-window-maximize';
|
||||
|
||||
panelIcon = 'fa fa-table';
|
||||
|
||||
subPanelIcon = 'fa fa-table';
|
||||
|
||||
pluginIcon = 'cards-plugin';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
multifactor = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
description =
|
||||
'围绕卡片列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。';
|
||||
|
||||
order = -1000;
|
||||
|
||||
$schema = '/schemas/CRUD2CardsSchema.json';
|
||||
|
||||
docLink = '/amis/zh-CN/components/crud2';
|
||||
|
||||
previewSchema: Record<string, any> = this.generatePreviewSchema('cards');
|
||||
|
||||
scaffold: any = this.generateScaffold('cards');
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager, Table2RenderereEvent, Table2RendererAction);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
/** 非实体数据源走默认构建 */
|
||||
panelBodyCreator = (context: BuildPanelEventContext) => {
|
||||
/** 先写入动态控件 */
|
||||
this.dynamicControls = {
|
||||
/** 列配置 */
|
||||
columns: context => this.renderColumnsControl(context),
|
||||
/** 工具栏配置 */
|
||||
toolbar: context => this.renderToolbarCollapse(context),
|
||||
/** 搜索栏 */
|
||||
filters: context => this.renderFiltersCollapse(context)
|
||||
};
|
||||
|
||||
return this.baseCRUDPanelBody(context);
|
||||
};
|
||||
|
||||
@autobind
|
||||
renderColumnsControl(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
title: '列设置',
|
||||
order: 5,
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-column-control',
|
||||
name: 'columns',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderToolbarCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
order: 20,
|
||||
title: '工具栏',
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-toolbar-control',
|
||||
name: 'headerToolbar',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFiltersCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
const collection: any[] = [];
|
||||
|
||||
builder.features.forEach(feat => {
|
||||
if (/Query$/.test(feat)) {
|
||||
collection.push({
|
||||
type: 'ae-crud-filters-control',
|
||||
name:
|
||||
feat === DSFeatureEnum.SimpleQuery ||
|
||||
feat === DSFeatureEnum.AdvancedQuery
|
||||
? 'filter'
|
||||
: feat === DSFeatureEnum.FuzzyQuery
|
||||
? 'headerToolbar'
|
||||
: undefined,
|
||||
label:
|
||||
feat === DSFeatureEnum.SimpleQuery
|
||||
? '简单查询'
|
||||
: feat === DSFeatureEnum.AdvancedQuery
|
||||
? '高级查询'
|
||||
: '模糊查询',
|
||||
nodeId: context.id,
|
||||
feat: feat,
|
||||
builder
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return collection.length > 0
|
||||
? {
|
||||
order: 10,
|
||||
title: '搜索设置',
|
||||
body: collection
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// registerEditorPlugin(CRUDCardsPlugin);
|
149
packages/amis-editor/src/plugin/CRUD2/CRUDList.tsx
Normal file
149
packages/amis-editor/src/plugin/CRUD2/CRUDList.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @file CRUDList.tsx
|
||||
* @desc 列表模式的 CRUD2
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {autobind} from 'amis';
|
||||
import {
|
||||
EditorManager,
|
||||
JSONPipeIn,
|
||||
BuildPanelEventContext,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {DSBuilderManager, DSFeatureEnum} from '../../builder';
|
||||
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
|
||||
import {BaseCRUDPlugin} from './BaseCRUD';
|
||||
|
||||
export class CRUDListPlugin extends BaseCRUDPlugin {
|
||||
static id = 'ListCRUDPlugin';
|
||||
|
||||
disabledRendererPlugin = true;
|
||||
|
||||
name = '列表';
|
||||
|
||||
panelTitle: '列表';
|
||||
|
||||
icon = 'fa fa-list';
|
||||
|
||||
panelIcon = 'fa fa-list';
|
||||
|
||||
subPanelIcon = 'fa fa-list';
|
||||
|
||||
pluginIcon = 'list-plugin';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
multifactor = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
description =
|
||||
'围绕列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。';
|
||||
|
||||
order = -1000;
|
||||
|
||||
$schema = '/schemas/CRUD2ListSchema.json';
|
||||
|
||||
docLink = '/amis/zh-CN/components/crud2';
|
||||
|
||||
previewSchema: Record<string, any> = this.generatePreviewSchema('list');
|
||||
|
||||
scaffold: any = this.generateScaffold('list');
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager, Table2RenderereEvent, Table2RendererAction);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
/** 非实体数据源走默认构建 */
|
||||
panelBodyCreator = (context: BuildPanelEventContext) => {
|
||||
/** 先写入动态控件 */
|
||||
this.dynamicControls = {
|
||||
/** 列配置 */
|
||||
columns: context => this.renderColumnsControl(context),
|
||||
/** 工具栏配置 */
|
||||
toolbar: context => this.renderToolbarCollapse(context),
|
||||
/** 搜索栏 */
|
||||
filters: context => this.renderFiltersCollapse(context)
|
||||
};
|
||||
|
||||
return this.baseCRUDPanelBody(context);
|
||||
};
|
||||
|
||||
@autobind
|
||||
renderColumnsControl(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
title: '列设置',
|
||||
order: 5,
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-column-control',
|
||||
name: 'columns',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderToolbarCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
order: 20,
|
||||
title: '工具栏',
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-toolbar-control',
|
||||
name: 'headerToolbar',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFiltersCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
const collection: any[] = [];
|
||||
|
||||
builder.features.forEach(feat => {
|
||||
if (/Query$/.test(feat)) {
|
||||
collection.push({
|
||||
type: 'ae-crud-filters-control',
|
||||
name:
|
||||
feat === DSFeatureEnum.SimpleQuery ||
|
||||
feat === DSFeatureEnum.AdvancedQuery
|
||||
? 'filter'
|
||||
: feat === DSFeatureEnum.FuzzyQuery
|
||||
? 'headerToolbar'
|
||||
: undefined,
|
||||
label:
|
||||
feat === DSFeatureEnum.SimpleQuery
|
||||
? '简单查询'
|
||||
: feat === DSFeatureEnum.AdvancedQuery
|
||||
? '高级查询'
|
||||
: '模糊查询',
|
||||
nodeId: context.id,
|
||||
feat: feat,
|
||||
builder
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return collection.length > 0
|
||||
? {
|
||||
order: 10,
|
||||
title: '搜索设置',
|
||||
body: collection
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// registerEditorPlugin(CRUDListPlugin);
|
58
packages/amis-editor/src/plugin/CRUD2/CRUDTable.tsx
Normal file
58
packages/amis-editor/src/plugin/CRUD2/CRUDTable.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @file CRUDTable.tsx
|
||||
* @desc 表格模式的 CRUD2
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {autobind} from 'amis';
|
||||
import {
|
||||
EditorManager,
|
||||
JSONPipeIn,
|
||||
BuildPanelEventContext,
|
||||
EditorNodeType,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {
|
||||
DSBuilder,
|
||||
DSBuilderManager,
|
||||
DSFeatureEnum,
|
||||
DSFeatureType
|
||||
} from '../../builder';
|
||||
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
|
||||
import {BaseCRUDPlugin} from './BaseCRUD';
|
||||
|
||||
export class CRUDTablePlugin extends BaseCRUDPlugin {
|
||||
static id = 'TableCRUDPlugin';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
multifactor = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
description =
|
||||
'用来实现对数据的增删改查,用来展示表格数据,可以配置列信息,然后关联数据便能完成展示。支持嵌套、超级表头、列固定、表头固顶、合并单元格等等。';
|
||||
|
||||
order = -950;
|
||||
|
||||
$schema = '/schemas/CRUD2TableSchema.json';
|
||||
|
||||
docLink = '/amis/zh-CN/components/crud2';
|
||||
|
||||
previewSchema: Record<string, any> = this.generatePreviewSchema('table2');
|
||||
|
||||
scaffold: any = this.generateScaffold('table2');
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager, Table2RenderereEvent, Table2RendererAction);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
/** 非实体数据源走默认构建 */
|
||||
panelBodyCreator = (context: BuildPanelEventContext) => {
|
||||
return this.baseCRUDPanelBody(context);
|
||||
};
|
||||
}
|
||||
|
||||
registerEditorPlugin(CRUDTablePlugin);
|
51
packages/amis-editor/src/plugin/CRUD2/constants.ts
Normal file
51
packages/amis-editor/src/plugin/CRUD2/constants.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @file constants.ts
|
||||
* @desc CRUD 配置相关常量
|
||||
*/
|
||||
|
||||
import {DSFeatureEnum} from '../../builder/constants';
|
||||
|
||||
export const ToolsConfig = {
|
||||
groupName: 'tools',
|
||||
options: [
|
||||
{
|
||||
label: '新增记录',
|
||||
value: 'Insert',
|
||||
align: 'left',
|
||||
icon: 'fa fa-layer-group',
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
label: '批量编辑',
|
||||
value: 'BulkEdit',
|
||||
align: 'left',
|
||||
icon: 'fa fa-layer-group',
|
||||
order: 20
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
value: 'BulkDelete',
|
||||
align: 'left',
|
||||
icon: 'fa fa-layer-group',
|
||||
order: 30
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const FiltersConfig = {
|
||||
groupName: 'filters',
|
||||
options: [
|
||||
{label: '模糊查询', value: 'FuzzyQuery', icon: 'fa fa-search', order: 10},
|
||||
{label: '简单查询', value: 'SimpleQuery', icon: 'fa fa-search', order: 20},
|
||||
{label: '高级查询', value: 'AdvancedQuery', icon: 'fa fa-search', order: 30}
|
||||
]
|
||||
};
|
||||
|
||||
export const OperatorsConfig = {
|
||||
groupName: 'operators',
|
||||
options: [
|
||||
{label: '查看详情', value: 'View', icon: 'fa fa-database', order: 10},
|
||||
{label: '编辑记录', value: 'Edit', icon: 'fa fa-database', order: 20},
|
||||
{label: '删除记录', value: 'Delete', icon: 'fa fa-database', order: 30}
|
||||
]
|
||||
};
|
194
packages/amis-editor/src/plugin/CRUD2/utils.ts
Normal file
194
packages/amis-editor/src/plugin/CRUD2/utils.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isObject from 'lodash/isObject';
|
||||
import remove from 'lodash/remove';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
export function findAndUpdate<T = any>(
|
||||
arr: T[],
|
||||
compareFn: (item: T) => boolean,
|
||||
target?: T
|
||||
) {
|
||||
if (!target) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const result = cloneDeep(arr);
|
||||
const idx = result.findIndex(item => compareFn(item));
|
||||
|
||||
if (~idx) {
|
||||
result.splice(idx, 1, target);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 深度删除 */
|
||||
export const deepRemove = (
|
||||
obj: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
checkAll: boolean = false
|
||||
): any => {
|
||||
const waitProcess = [obj];
|
||||
let find = false;
|
||||
|
||||
while (waitProcess.length) {
|
||||
if (find) {
|
||||
break;
|
||||
}
|
||||
|
||||
let item: any = waitProcess.pop();
|
||||
if (Array.isArray(item)) {
|
||||
remove(item, (val: any) => {
|
||||
const res = predicate(val);
|
||||
|
||||
if (res && !checkAll) {
|
||||
find = true;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
waitProcess.push(...item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObject(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
if (isObject(value) && predicate(value)) {
|
||||
delete item[key];
|
||||
checkAll || (find = true);
|
||||
}
|
||||
waitProcess.push(value);
|
||||
});
|
||||
}
|
||||
|
||||
return find;
|
||||
};
|
||||
|
||||
export const findObj = (
|
||||
obj: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
stop?: (obj: any) => boolean
|
||||
): any | void => {
|
||||
const waitProcess = [obj];
|
||||
|
||||
while (waitProcess.length) {
|
||||
let item: any = waitProcess.shift();
|
||||
if (Array.isArray(item)) {
|
||||
waitProcess.push(...item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObject(item) || (stop && stop(item))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (predicate(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
waitProcess.push(
|
||||
...Object.values(
|
||||
pickBy(item, (val: any, key: string) => !String(key).startsWith('__'))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** schema 中查找 */
|
||||
export const findSchema = (
|
||||
schema: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
...scope: string[]
|
||||
) => {
|
||||
if (scope.length === 0) {
|
||||
return findObj(schema, predicate);
|
||||
}
|
||||
let region = null;
|
||||
while ((region = scope.shift())) {
|
||||
const res = findObj(schema[region], predicate);
|
||||
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** headerToolbar 和 footerToolbar 布局换成 flex 包裹 container */
|
||||
export const addSchema2Toolbar = (
|
||||
schema: any,
|
||||
content: any,
|
||||
position: 'header' | 'footer',
|
||||
align: 'left' | 'right'
|
||||
) => {
|
||||
const region = `${position}Toolbar`;
|
||||
const buildFlex = (items: any[] = []) => ({
|
||||
type: 'flex',
|
||||
items,
|
||||
style: {
|
||||
position: 'static'
|
||||
},
|
||||
direction: 'row',
|
||||
justify: 'flex-start',
|
||||
alignItems: 'stretch'
|
||||
});
|
||||
const buildContainer = (align?: 'left' | 'right', body: any[] = []) => ({
|
||||
type: 'container',
|
||||
body,
|
||||
wrapperBody: false,
|
||||
style: {
|
||||
flexGrow: 1,
|
||||
flex: '1 1 auto',
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexBasis: 'auto',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'stretch',
|
||||
...(align
|
||||
? {
|
||||
justifyContent: align === 'left' ? 'flex-start' : 'flex-end'
|
||||
}
|
||||
: {})
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
!schema[region] ||
|
||||
isEmpty(schema[region]) ||
|
||||
!Array.isArray(schema[region])
|
||||
) {
|
||||
const isArr = Array.isArray(schema[region]);
|
||||
const newSchema = buildFlex([
|
||||
buildContainer('left', isArr || !schema[region] ? [] : [schema[region]]),
|
||||
buildContainer('right')
|
||||
]);
|
||||
|
||||
(isArr && schema[region].push(newSchema)) || (schema[region] = [newSchema]);
|
||||
}
|
||||
|
||||
// 尝试放到左面第一个,否则只能放外头了
|
||||
try {
|
||||
// 优先判断没有右边列的情况,避免都走到catch里造成嵌套层数过多的问题
|
||||
if (align === 'right' && schema[region][0].items.length < 2) {
|
||||
schema[region][0].items.push(buildContainer('right'));
|
||||
}
|
||||
|
||||
schema[region][0].items[
|
||||
align === 'left' ? 0 : schema[region][0].items.length - 1
|
||||
].body.push(content);
|
||||
} catch (e) {
|
||||
const olds = [...schema[region]];
|
||||
schema[region].length = 0;
|
||||
schema[region].push(
|
||||
buildFlex([
|
||||
buildContainer('left', olds),
|
||||
buildContainer('right', content)
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import {Button} from 'amis';
|
||||
import {Button, JSONValueMap, isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -16,7 +16,8 @@ import {
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
|
||||
import {resolveArrayDatasource} from '../util';
|
||||
import set from 'lodash/set';
|
||||
import {escapeFormula, resolveArrayDatasource} from '../util';
|
||||
|
||||
export class CardsPlugin extends BasePlugin {
|
||||
static id = 'CardsPlugin';
|
||||
@ -28,6 +29,7 @@ export class CardsPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '卡片列表';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
description =
|
||||
'功能类似于表格,但是用一个个小卡片来展示数据。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
|
||||
docLink = '/amis/zh-CN/components/cards';
|
||||
@ -36,36 +38,330 @@ export class CardsPlugin extends BasePlugin {
|
||||
pluginIcon = 'cards-plugin';
|
||||
scaffold = {
|
||||
type: 'cards',
|
||||
data: {
|
||||
items: [
|
||||
{a: 1, b: 2},
|
||||
{a: 3, b: 4}
|
||||
]
|
||||
},
|
||||
columnsCount: 2,
|
||||
columnsCount: 4,
|
||||
card: {
|
||||
type: 'card',
|
||||
className: 'm-b-none',
|
||||
header: {
|
||||
title: '标题',
|
||||
subTitle: '副标题'
|
||||
},
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
name: 'a',
|
||||
label: 'A'
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
label: 'B'
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: 'fa fa-check',
|
||||
vendor: '',
|
||||
themeCss: {
|
||||
className: {
|
||||
'font': {
|
||||
color: 'var(--colors-brand-6)',
|
||||
fontSize: '20px'
|
||||
},
|
||||
'padding-and-margin:default': {
|
||||
marginRight: '10px'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '流水线任务实例 ',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
editorSetting: {
|
||||
mock: {}
|
||||
},
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
marginBottom: '15px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
},
|
||||
{
|
||||
type: 'flex',
|
||||
className: 'p-1',
|
||||
items: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '12/',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '19',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-6)',
|
||||
fontSize: 'var(--fonts-size-6)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block',
|
||||
flex: '0 0 auto',
|
||||
marginTop: 'var(--sizes-size-0)',
|
||||
marginRight: 'var(--sizes-size-0)',
|
||||
marginBottom: 'var(--sizes-size-0)',
|
||||
marginLeft: 'var(--sizes-size-0)'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '单元测试',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flex: '0 0 auto'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '100%',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '通过率',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '99.9%',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '任务实例',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'relative'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '报告',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: '14px',
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '2023-01-01 12:00',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
color: 'var(--colors-neutral-text-6)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '20px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
'position': 'static',
|
||||
'display': 'block',
|
||||
'overflowY': 'auto',
|
||||
'overflowX': 'auto',
|
||||
'paddingTop': '10px',
|
||||
'paddingRight': '10px',
|
||||
'paddingBottom': '10px',
|
||||
'paddingLeft': '10px',
|
||||
'radius': {
|
||||
'top-left-border-radius': '6px',
|
||||
'top-right-border-radius': '6px',
|
||||
'bottom-left-border-radius': '6px',
|
||||
'bottom-right-border-radius': '6px'
|
||||
},
|
||||
'top-border-width': 'var(--borders-width-4)',
|
||||
'left-border-width': 'var(--borders-width-2)',
|
||||
'right-border-width': 'var(--borders-width-2)',
|
||||
'bottom-border-width': 'var(--borders-width-2)',
|
||||
'top-border-style': 'var(--borders-style-2)',
|
||||
'left-border-style': 'var(--borders-style-2)',
|
||||
'right-border-style': 'var(--borders-style-2)',
|
||||
'bottom-border-style': 'var(--borders-style-2)',
|
||||
'top-border-color': 'var(--colors-brand-6)',
|
||||
'left-border-color': 'var(--colors-brand-10)',
|
||||
'right-border-color': 'var(--colors-brand-10)',
|
||||
'bottom-border-color': 'var(--colors-brand-10)',
|
||||
'flex': '0 0 150px',
|
||||
'marginRight': '15px',
|
||||
'flexBasis': '100%'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
onEvent: {
|
||||
click: {
|
||||
weight: 0,
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'button'
|
||||
}
|
||||
]
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
width: '1000%',
|
||||
overflowX: 'auto',
|
||||
margin: '0',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
wrapperBody: false
|
||||
},
|
||||
placeholder: '',
|
||||
name: '',
|
||||
style: {
|
||||
gutterX: 15,
|
||||
gutterY: 15
|
||||
}
|
||||
};
|
||||
previewSchema = {
|
||||
@ -74,6 +370,7 @@ export class CardsPlugin extends BasePlugin {
|
||||
};
|
||||
|
||||
panelTitle = '卡片集';
|
||||
panelJustify = true;
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const curPosition = context?.schema?.style?.position;
|
||||
@ -82,200 +379,119 @@ export class CardsPlugin extends BasePlugin {
|
||||
return [
|
||||
getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '常规',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {
|
||||
visibleOn: isAbsolute ? isAbsolute : undefined,
|
||||
value: 'left-top'
|
||||
}),
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
children: (
|
||||
<div className="m-b">
|
||||
<Button
|
||||
level="primary"
|
||||
size="sm"
|
||||
block
|
||||
onClick={this.editDetail.bind(this, context.id)}
|
||||
>
|
||||
配置单项信息
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
getSchemaTpl('title'),
|
||||
|
||||
isCRUDBody
|
||||
? null
|
||||
: {
|
||||
name: 'source',
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
label: '数据源',
|
||||
pipeIn: defaultValue('${items}'),
|
||||
description: '绑定当前环境变量',
|
||||
test: !isCRUDBody
|
||||
label: '组件名称',
|
||||
name: 'editorSetting.displayName'
|
||||
},
|
||||
getSchemaTpl('cardsPlaceholder')
|
||||
]
|
||||
isCRUDBody
|
||||
? null
|
||||
: getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名'
|
||||
}),
|
||||
getSchemaTpl('cardsPlaceholder')
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
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 类名'
|
||||
}),
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
name: 'columnsCount',
|
||||
type: 'input-range',
|
||||
visibleOn: '!this.leftFixed',
|
||||
min: 0,
|
||||
max: 12,
|
||||
step: 1,
|
||||
label: '每行显示个数',
|
||||
description: '不设置时,由卡片 CSS 类名决定'
|
||||
title: '组件',
|
||||
body: [
|
||||
{
|
||||
name: 'columnsCount',
|
||||
type: 'input-range',
|
||||
visibleOn: '!this.leftFixed',
|
||||
min: 0,
|
||||
max: 12,
|
||||
step: 1,
|
||||
label: '每行个数',
|
||||
description: '不设置时,由卡片 CSS 类名决定'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '左右间距',
|
||||
name: 'style.gutterX'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '上下间距',
|
||||
name: 'style.gutterY'
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'masonryLayout',
|
||||
label: '启用瀑布流'
|
||||
}),
|
||||
getSchemaTpl('layout:originPosition', {
|
||||
visibleOn: isAbsolute ? isAbsolute : undefined,
|
||||
value: 'left-top'
|
||||
})
|
||||
]
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'masonryLayout',
|
||||
label: '启用瀑布流'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '显隐',
|
||||
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
])
|
||||
];
|
||||
};
|
||||
|
||||
editDetail(id: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(id);
|
||||
const value = store.getValueOf(id);
|
||||
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'cards',
|
||||
type: 'object',
|
||||
title: '当前列表项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
node &&
|
||||
value &&
|
||||
this.manager.openSubEditor({
|
||||
title: '配置成员渲染器',
|
||||
value: {
|
||||
type: 'card',
|
||||
...value.card
|
||||
},
|
||||
slot: {
|
||||
type: 'container',
|
||||
body: '$$'
|
||||
},
|
||||
typeMutable: false,
|
||||
onChange: newValue => {
|
||||
newValue = {...value, card: newValue};
|
||||
manager.panelChangeValue(newValue, diff(value, newValue));
|
||||
},
|
||||
data: {
|
||||
item: 'mocked data',
|
||||
index: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
buildEditorToolbar(
|
||||
{id, info, schema}: BaseEventContext,
|
||||
toolbars: Array<BasicToolbarItem>
|
||||
) {
|
||||
if (
|
||||
info.renderer.name === 'cards' ||
|
||||
(info.renderer.name === 'crud' && schema.mode === 'cards')
|
||||
) {
|
||||
toolbars.push({
|
||||
icon: 'fa fa-expand',
|
||||
order: 100,
|
||||
tooltip: '配置成员渲染器',
|
||||
onClick: this.editDetail.bind(this, id)
|
||||
// 列表添加序号方便处理
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '索引'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buildEditorContextMenu(
|
||||
{id, schema, region, info, selections}: ContextMenuEventContext,
|
||||
menus: Array<ContextMenuItem>
|
||||
) {
|
||||
if (selections.length || info?.plugin !== this) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
info.renderer.name === 'cards' ||
|
||||
(info.renderer.name === 'crud' && schema.mode === 'cards')
|
||||
) {
|
||||
menus.push('|', {
|
||||
label: '配置成员渲染器',
|
||||
onSelect: this.editDetail.bind(this, id)
|
||||
});
|
||||
}
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
filterProps(props: any) {
|
||||
const data = {
|
||||
...props.defaultData,
|
||||
...props.data
|
||||
};
|
||||
const arr = resolveArrayDatasource({
|
||||
value: props.value,
|
||||
data,
|
||||
source: props.source
|
||||
});
|
||||
|
||||
if (!Array.isArray(arr) || !arr.length) {
|
||||
const mockedData: any = {
|
||||
id: 666,
|
||||
title: '假数据',
|
||||
description: '假数据',
|
||||
a: '假数据',
|
||||
b: '假数据'
|
||||
};
|
||||
|
||||
props.value = repeatArray(mockedData, 1).map((item, index) => ({
|
||||
// 编辑时显示两行假数据
|
||||
const count = (props.columnsCount || 3) * 2;
|
||||
props.value = repeatArray({}, count).map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
id: index + 1
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
props.className = `${props.className || ''} ae-Editor-list`;
|
||||
props.itemsClassName = `${props.itemsClassName || ''} cards-items`;
|
||||
if (props.card && !props.card.className?.includes('listItem')) {
|
||||
props.card.className = `${props.card.className || ''} ae-Editor-listItem`;
|
||||
}
|
||||
|
||||
const {$schema, ...rest} = props;
|
||||
// 列表类型内的文本元素显示原始公式
|
||||
props = escapeFormula(props);
|
||||
|
||||
return {
|
||||
...JSONPipeOut(rest),
|
||||
$schema
|
||||
};
|
||||
return props;
|
||||
}
|
||||
|
||||
getRendererInfo(
|
||||
|
@ -1,3 +1,4 @@
|
||||
import update from 'lodash/update';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -10,24 +11,47 @@ import {
|
||||
|
||||
export class ColumnToggler extends BasePlugin {
|
||||
static id = 'ColumnToggler';
|
||||
// 关联渲染器名字
|
||||
|
||||
rendererName = 'column-toggler';
|
||||
$schema = '/schemas/ColumnToggler.json';
|
||||
|
||||
// 组件名称
|
||||
name = '自定义显示列';
|
||||
isBaseComponent = true;
|
||||
disabledRendererPlugin = true;
|
||||
description = '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。';
|
||||
|
||||
tags = ['自定义显示列'];
|
||||
icon = 'fa fa-square';
|
||||
|
||||
panelTitle = '自定义显示列';
|
||||
|
||||
icon = 'fa fa-square';
|
||||
|
||||
tags = ['自定义显示列'];
|
||||
|
||||
$schema = '/schemas/ColumnTogglerSchema.json';
|
||||
|
||||
description = '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
disabledRendererPlugin = true;
|
||||
|
||||
crudInfo: {id: any; columns: any[]; schema: any};
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const crud = context?.node?.getClosestParentByType('crud2');
|
||||
|
||||
if (crud) {
|
||||
this.crudInfo = {
|
||||
id: crud.id,
|
||||
columns: crud.schema.columns || [],
|
||||
schema: crud.schema
|
||||
};
|
||||
}
|
||||
|
||||
const columns = (this.crudInfo?.schema?.columns ?? []).map(
|
||||
(item: any, index: number) => ({
|
||||
label: item.title,
|
||||
value: index
|
||||
})
|
||||
);
|
||||
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
@ -53,6 +77,64 @@ export class ColumnToggler extends BasePlugin {
|
||||
label: '按钮图标'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '列默认显示',
|
||||
body: [
|
||||
{
|
||||
name: `__toggled`,
|
||||
value: '',
|
||||
type: 'checkboxes',
|
||||
// className: 'b-a p-sm',
|
||||
label: false,
|
||||
inline: false,
|
||||
joinValues: false,
|
||||
extractValue: true,
|
||||
options: columns,
|
||||
// style: {
|
||||
// maxHeight: '200px',
|
||||
// overflow: 'auto'
|
||||
// },
|
||||
pipeIn: (value: any, form: any) => {
|
||||
const showColumnIndex: number[] = [];
|
||||
this.crudInfo?.schema?.columns?.forEach(
|
||||
(item: any, index: number) => {
|
||||
if (item.toggled !== false) {
|
||||
showColumnIndex.push(index);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return showColumnIndex;
|
||||
},
|
||||
onChange: (value: number[]) => {
|
||||
if (!this.crudInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newColumns = this.crudInfo.schema.columns;
|
||||
|
||||
newColumns = newColumns.map((item: any, index: number) => ({
|
||||
...item,
|
||||
toggled: value.includes(index) ? undefined : false
|
||||
}));
|
||||
|
||||
const updatedSchema = update(
|
||||
this.crudInfo.schema,
|
||||
'columns',
|
||||
(origin: any) => {
|
||||
return newColumns;
|
||||
}
|
||||
);
|
||||
|
||||
this.manager.store.changeValueById(
|
||||
this.crudInfo.id,
|
||||
updatedSchema
|
||||
);
|
||||
this.crudInfo.schema = updatedSchema;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -1,16 +1,10 @@
|
||||
import {Button} from 'amis';
|
||||
import {isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
BasicToolbarItem,
|
||||
ContextMenuEventContext,
|
||||
ContextMenuItem
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {diff, JSONPipeOut} from 'amis-editor-core';
|
||||
import {schemaToArray} from '../util';
|
||||
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {BaseEventContext, BasePlugin} from 'amis-editor-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
import {escapeFormula} from '../util';
|
||||
import {set} from 'lodash';
|
||||
|
||||
export class EachPlugin extends BasePlugin {
|
||||
static id = 'EachPlugin';
|
||||
@ -22,132 +16,355 @@ export class EachPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '循环 Each';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
description = '功能渲染器,可以基于现有变量循环输出渲染器。';
|
||||
tags = ['功能'];
|
||||
icon = 'fa fa-repeat';
|
||||
pluginIcon = 'each-plugin';
|
||||
scaffold = {
|
||||
type: 'each',
|
||||
name: 'arr',
|
||||
name: '',
|
||||
items: {
|
||||
type: 'tpl',
|
||||
tpl: '<%= data.index + 1 %>. 内容:<%= data.item %>',
|
||||
wrapperComponent: '',
|
||||
inline: false
|
||||
}
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: 'fa fa-plane',
|
||||
vendor: '',
|
||||
themeCss: {
|
||||
className: {
|
||||
'padding-and-margin:default': {
|
||||
marginRight: '4px'
|
||||
},
|
||||
'font': {
|
||||
color: '#2856ad',
|
||||
fontSize: '20px'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
style: {
|
||||
fontWeight: 'var(--fonts-weight-3)',
|
||||
fontSize: '16px',
|
||||
color: 'var(--colors-brand-6)'
|
||||
},
|
||||
tpl: '回访数量TOP1',
|
||||
inline: true,
|
||||
wrapperComponent: ''
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
marginBottom: '6px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '北京分公司',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
'fontSize': 'var(--fonts-size-4)',
|
||||
'color': 'var(--colors-neutral-text-2)',
|
||||
'fontWeight': 'var(--fonts-weight-3)',
|
||||
'font-family': '-apple-system'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block'
|
||||
},
|
||||
wrapperBody: false
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block',
|
||||
flex: '0 0 150px',
|
||||
marginRight: '20px',
|
||||
paddingTop: '20px',
|
||||
paddingRight: '15px',
|
||||
paddingBottom: '20px',
|
||||
paddingLeft: '15px',
|
||||
flexBasis: '250px',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
boxShadow: ' 0px 0px 8px 0px rgba(3, 3, 3, 0.1)',
|
||||
radius: {
|
||||
'top-left-border-radius': 'var(--borders-radius-3)',
|
||||
'top-right-border-radius': 'var(--borders-radius-3)',
|
||||
'bottom-left-border-radius': 'var(--borders-radius-3)',
|
||||
'bottom-right-border-radius': 'var(--borders-radius-3)'
|
||||
}
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false
|
||||
},
|
||||
placeholder: '',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
marginTop: '10px',
|
||||
marginBottom: '10px'
|
||||
},
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
};
|
||||
|
||||
previewSchema = {
|
||||
...this.scaffold,
|
||||
value: ['a', 'b', 'c']
|
||||
};
|
||||
|
||||
panelTitle = '循环';
|
||||
panelJustify = true;
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
return [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'name',
|
||||
label: '关联字段',
|
||||
placeholder: 'varname',
|
||||
description:
|
||||
'如果所在容器有下发 value 则不需要配置,如果没有请配置变量名,支持多层级如:a.b,表示关联a对象下的b属性。目标变量可以是数组,也可以是对象。'
|
||||
},
|
||||
const curRendererSchema = context?.schema;
|
||||
const isFreeContainer = curRendererSchema?.isFreeContainer || false;
|
||||
const isFlexItem = this.manager?.isFlexItem(context?.id);
|
||||
const isFlexColumnItem = this.manager?.isFlexColumnItem(context?.id);
|
||||
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
size="sm"
|
||||
level="primary"
|
||||
className="m-b"
|
||||
block
|
||||
onClick={this.editDetail.bind(this, context.id)}
|
||||
>
|
||||
配置成员渲染器
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
const displayTpl = [
|
||||
getSchemaTpl('layout:display'),
|
||||
|
||||
getSchemaTpl('placeholder', {
|
||||
label: '占位符',
|
||||
pipeIn: defaultValue('暂无内容'),
|
||||
description:
|
||||
'当没有关联变量,或者目标变量不是数组或者对象时显示此占位信息'
|
||||
getSchemaTpl('layout:flex-setting', {
|
||||
visibleOn:
|
||||
'data.style && (data.style.display === "flex" || data.style.display === "inline-flex")',
|
||||
direction: curRendererSchema.direction,
|
||||
justify: curRendererSchema.justify,
|
||||
alignItems: curRendererSchema.alignItems
|
||||
}),
|
||||
|
||||
getSchemaTpl('className')
|
||||
getSchemaTpl('layout:flex-wrap', {
|
||||
visibleOn:
|
||||
'data.style && (data.style.display === "flex" || data.style.display === "inline-flex")'
|
||||
})
|
||||
];
|
||||
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
label: '组件名称',
|
||||
name: 'editorSetting.displayName'
|
||||
},
|
||||
getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名',
|
||||
paramType: 'output'
|
||||
}),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: {
|
||||
type: 'input-number',
|
||||
min: 1
|
||||
},
|
||||
name: 'maxLength',
|
||||
label: '最大显示个数',
|
||||
valueType: 'number'
|
||||
}),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: {
|
||||
type: 'input-text'
|
||||
},
|
||||
name: 'placeholder',
|
||||
label: '空数据提示'
|
||||
})
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '布局',
|
||||
body: [
|
||||
getSchemaTpl('layout:padding'),
|
||||
getSchemaTpl('layout:position', {
|
||||
visibleOn: '!data.stickyStatus'
|
||||
}),
|
||||
getSchemaTpl('layout:originPosition'),
|
||||
getSchemaTpl('layout:inset', {
|
||||
mode: 'vertical'
|
||||
}),
|
||||
|
||||
// 自由容器不需要 display 相关配置项
|
||||
...(!isFreeContainer ? displayTpl : []),
|
||||
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex', {
|
||||
isFlexColumnItem,
|
||||
label: isFlexColumnItem ? '高度设置' : '宽度设置',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative")'
|
||||
})
|
||||
: null,
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex-grow', {
|
||||
visibleOn:
|
||||
'data.style && data.style.flex === "1 1 auto" && (data.style.position === "static" || data.style.position === "relative")'
|
||||
})
|
||||
: null,
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex-basis', {
|
||||
label: isFlexColumnItem ? '弹性高度' : '弹性宽度',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "1 1 auto"'
|
||||
})
|
||||
: null,
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex-basis', {
|
||||
label: isFlexColumnItem ? '固定高度' : '固定宽度',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "0 0 150px"'
|
||||
})
|
||||
: null,
|
||||
|
||||
getSchemaTpl('layout:overflow-x', {
|
||||
visibleOn: `${
|
||||
isFlexItem && !isFlexColumnItem
|
||||
} && data.style.flex === '0 0 150px'`
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:isFixedHeight', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`,
|
||||
onChange: (value: boolean) => {
|
||||
context?.node.setHeightMutable(value);
|
||||
}
|
||||
}),
|
||||
getSchemaTpl('layout:height', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:max-height', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:min-height', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:overflow-y', {
|
||||
visibleOn: `${
|
||||
!isFlexItem || !isFlexColumnItem
|
||||
} && (data.isFixedHeight || data.style && data.style.maxHeight) || (${
|
||||
isFlexItem && isFlexColumnItem
|
||||
} && data.style.flex === '0 0 150px')`
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:isFixedWidth', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`,
|
||||
onChange: (value: boolean) => {
|
||||
context?.node.setWidthMutable(value);
|
||||
}
|
||||
}),
|
||||
getSchemaTpl('layout:width', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:max-width', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:min-width', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:overflow-x', {
|
||||
visibleOn: `${
|
||||
!isFlexItem || isFlexColumnItem
|
||||
} && (data.isFixedWidth || data.style && data.style.maxWidth)`
|
||||
}),
|
||||
|
||||
!isFlexItem ? getSchemaTpl('layout:margin-center') : null,
|
||||
!isFlexItem && !isFreeContainer
|
||||
? getSchemaTpl('layout:textAlign', {
|
||||
name: 'style.textAlign',
|
||||
label: '内部对齐方式',
|
||||
visibleOn:
|
||||
'data.style && data.style.display !== "flex" && data.style.display !== "inline-flex"'
|
||||
})
|
||||
: null,
|
||||
getSchemaTpl('layout:z-index'),
|
||||
getSchemaTpl('layout:sticky', {
|
||||
visibleOn:
|
||||
'data.style && (data.style.position !== "fixed" && data.style.position !== "absolute")'
|
||||
}),
|
||||
getSchemaTpl('layout:stickyPosition')
|
||||
]
|
||||
},
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
filterProps(props: any) {
|
||||
props = JSONPipeOut(props);
|
||||
// 列表类型内的文本元素显示{{公式}}或者自定义展位,不显示实际值
|
||||
props = escapeFormula(props);
|
||||
// 循环编辑态显示2个元素
|
||||
props.value = [{}, {}];
|
||||
|
||||
// 至少显示一个成员,否则啥都不显示。
|
||||
if (!props.value) {
|
||||
props.value = [
|
||||
{
|
||||
item: 'mocked data'
|
||||
}
|
||||
];
|
||||
props.className = `${props.className || ''} ae-Editor-list`;
|
||||
if (props.items && !props.items.className?.includes('listItem')) {
|
||||
props.items.className = `${
|
||||
props.items.className || ''
|
||||
} ae-Editor-eachItem`;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
buildEditorToolbar(
|
||||
{id, info}: BaseEventContext,
|
||||
toolbars: Array<BasicToolbarItem>
|
||||
) {
|
||||
if (info.renderer.name === 'each') {
|
||||
toolbars.push({
|
||||
icon: 'fa fa-expand',
|
||||
order: 100,
|
||||
tooltip: '配置成员渲染器',
|
||||
onClick: this.editDetail.bind(this, id)
|
||||
buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'each',
|
||||
type: 'object',
|
||||
title: '当前循环项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
// 循环添加索引方便渲染序号
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '索引'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buildEditorContextMenu(
|
||||
{id, schema, region, info, selections}: ContextMenuEventContext,
|
||||
menus: Array<ContextMenuItem>
|
||||
) {
|
||||
if (selections.length || info?.plugin !== this) {
|
||||
return;
|
||||
}
|
||||
if (info.renderer.name === 'each') {
|
||||
menus.push('|', {
|
||||
label: '配置成员渲染器',
|
||||
onSelect: this.editDetail.bind(this, id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editDetail(id: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(id);
|
||||
const value = store.getValueOf(id);
|
||||
|
||||
node &&
|
||||
value &&
|
||||
this.manager.openSubEditor({
|
||||
title: '配置成员渲染器',
|
||||
value: schemaToArray(value.items),
|
||||
slot: {
|
||||
type: 'container',
|
||||
body: '$$'
|
||||
},
|
||||
typeMutable: true,
|
||||
onChange: newValue => {
|
||||
newValue = {...value, items: newValue};
|
||||
manager.panelChangeValue(newValue, diff(value, newValue));
|
||||
},
|
||||
data: {
|
||||
item: 'mocked data',
|
||||
index: 0
|
||||
}
|
||||
});
|
||||
return dataSchema;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {setVariable, someTree} from 'amis-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -11,11 +12,9 @@ import {
|
||||
RegionConfig,
|
||||
getI18nEnabled,
|
||||
EditorNodeType,
|
||||
EditorManager,
|
||||
DSBuilderManager
|
||||
EditorManager
|
||||
} from 'amis-editor-core';
|
||||
import {setVariable, someTree} from 'amis-core';
|
||||
|
||||
import {DSBuilderManager} from '../../builder/DSBuilderManager';
|
||||
import {ValidatorTag} from '../../validator';
|
||||
import {
|
||||
getArgsWrapper,
|
||||
@ -240,11 +239,11 @@ export class ComboControlPlugin extends BasePlugin {
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
dsBuilderManager: DSBuilderManager;
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsBuilderManager = new DSBuilderManager('combo', 'api');
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
@ -750,14 +749,11 @@ export class ComboControlPlugin extends BasePlugin {
|
||||
(target.parent.isRegion && target.parent.region === 'items')
|
||||
) {
|
||||
scope = scopeNode.parent.parent;
|
||||
builder = this.dsBuilderManager.resolveBuilderBySchema(
|
||||
scope.schema,
|
||||
'api'
|
||||
);
|
||||
builder = this.dsManager.getBuilderBySchema(scope.schema);
|
||||
}
|
||||
|
||||
if (builder && scope.schema.api) {
|
||||
return builder.getAvailableContextFileds(
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scope.schema,
|
||||
sourceKey: 'api',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,10 +15,10 @@ import {
|
||||
repeatArray,
|
||||
mockValue,
|
||||
EditorNodeType,
|
||||
EditorManager,
|
||||
DSBuilderManager
|
||||
EditorManager
|
||||
} from 'amis-editor-core';
|
||||
import {getTreeAncestors, setVariable, someTree} from 'amis-core';
|
||||
import {setVariable, someTree} from 'amis-core';
|
||||
import {DSBuilderManager} from '../../builder/DSBuilderManager';
|
||||
import {ValidatorTag} from '../../validator';
|
||||
import {
|
||||
getEventControlConfig,
|
||||
@ -814,11 +814,11 @@ export class TableControlPlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
dsBuilderManager: DSBuilderManager;
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsBuilderManager = new DSBuilderManager('input-table', 'api');
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
@ -1064,12 +1064,14 @@ export class TableControlPlugin extends BasePlugin {
|
||||
filterProps(props: any) {
|
||||
const arr = resolveArrayDatasource(props);
|
||||
|
||||
/** 可 */
|
||||
if (!Array.isArray(arr) || !arr.length) {
|
||||
const mockedData: any = {};
|
||||
|
||||
if (Array.isArray(props.columns)) {
|
||||
props.columns.forEach((column: any) => {
|
||||
if (column.name) {
|
||||
/** 可编辑状态下不写入 Mock 数据,避免误导用户 */
|
||||
if (column.name && !props.editable) {
|
||||
setVariable(mockedData, column.name, mockValue(column));
|
||||
}
|
||||
});
|
||||
@ -1197,14 +1199,11 @@ export class TableControlPlugin extends BasePlugin {
|
||||
(target.parent.isRegion && target.parent.region === 'columns')
|
||||
) {
|
||||
scope = scopeNode.parent.parent;
|
||||
builder = this.dsBuilderManager.resolveBuilderBySchema(
|
||||
scope.schema,
|
||||
'api'
|
||||
);
|
||||
builder = this.dsManager.getBuilderBySchema(scope.schema);
|
||||
}
|
||||
|
||||
if (builder && scope.schema.api) {
|
||||
return builder.getAvailableContextFileds(
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scope.schema,
|
||||
sourceKey: 'api',
|
||||
|
@ -90,6 +90,14 @@ export class ImagePlugin extends BasePlugin {
|
||||
label: '缩略图地址',
|
||||
description: '如果已绑定字段名,可以不用设置,支持用变量。'
|
||||
}),
|
||||
|
||||
getSchemaTpl('backgroundImageUrl', {
|
||||
name: 'editorSetting.mock.src',
|
||||
label: tipedLabel(
|
||||
'假数据图片',
|
||||
'只在编辑区显示的模拟图片,运行时将显示图片实际内容'
|
||||
)
|
||||
}),
|
||||
{
|
||||
type: 'ae-switch-more',
|
||||
mode: 'normal',
|
||||
|
@ -232,6 +232,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
const isFlexItem = this.manager?.isFlexItem(id);
|
||||
const isFlexColumnItem = this.manager?.isFlexColumnItem(id);
|
||||
const newColumnSchema = defaultFlexColumnSchema('新的一列');
|
||||
const canAppendSiblings = this.manager?.canAppendSiblings();
|
||||
|
||||
const toolbarsTooltips: any = {};
|
||||
toolbars.forEach(toolbar => {
|
||||
@ -245,7 +246,8 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
(info.renderer?.name === 'flex' || info.renderer?.name === 'container') &&
|
||||
!isFlexItem && // 备注:如果是列级元素就不需要显示了
|
||||
!draggableContainer &&
|
||||
!schema?.isFreeContainer
|
||||
!schema?.isFreeContainer &&
|
||||
canAppendSiblings
|
||||
) {
|
||||
// 非特殊布局元素(fixed、absolute)支持前后插入追加布局元素功能icon
|
||||
if (!toolbarsTooltips['上方插入布局容器']) {
|
||||
@ -294,7 +296,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
if (isFlexItem && !draggableContainer) {
|
||||
if (isFlexItem && !draggableContainer && canAppendSiblings) {
|
||||
if (
|
||||
!toolbarsTooltips[`${isFlexColumnItem ? '上方' : '左侧'}插入列级容器`]
|
||||
) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {BasePlugin, RegionConfig, RendererInfo} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {tipedLabel} from 'amis-editor-core';
|
||||
import {
|
||||
registerEditorPlugin,
|
||||
BasePlugin,
|
||||
getSchemaTpl,
|
||||
tipedLabel
|
||||
} from 'amis-editor-core';
|
||||
|
||||
export class LinkPlugin extends BasePlugin {
|
||||
static id = 'LinkPlugin';
|
||||
@ -37,15 +39,23 @@ export class LinkPlugin extends BasePlugin {
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
{
|
||||
getSchemaTpl('valueFormula', {
|
||||
name: 'href',
|
||||
type: 'input-text',
|
||||
label: tipedLabel(
|
||||
'目标地址',
|
||||
'支持取变量,如果已绑定字段名,可以不用设置'
|
||||
)
|
||||
),
|
||||
rendererSchema: {
|
||||
type: 'input-text'
|
||||
}
|
||||
}),
|
||||
{
|
||||
label: tipedLabel('内容', '不填写时,自动使用目标地址值'),
|
||||
type: 'ae-textareaFormulaControl',
|
||||
mode: 'normal',
|
||||
pipeIn: (value: any, data: any) => value || (data && data.html),
|
||||
name: 'body'
|
||||
},
|
||||
getSchemaTpl('inputBody'),
|
||||
getSchemaTpl('switch', {
|
||||
name: 'blank',
|
||||
label: '在新窗口打开'
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Button} from 'amis';
|
||||
import {Button, isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {getI18nEnabled, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
EditorNodeType,
|
||||
getI18nEnabled,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -13,6 +17,7 @@ import {
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
|
||||
import set from 'lodash/set';
|
||||
import {
|
||||
schemaArrayFormat,
|
||||
resolveArrayDatasource,
|
||||
@ -28,6 +33,8 @@ export class ListPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '列表';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
disabledRendererPlugin = true;
|
||||
description =
|
||||
'展示一个列表,可以自定标题、副标题,内容及按钮组部分。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
|
||||
docLink = '/amis/zh-CN/components/list';
|
||||
@ -64,7 +71,7 @@ export class ListPlugin extends BasePlugin {
|
||||
panelTitle = '列表';
|
||||
panelJustify = true;
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const isCRUDBody = ['crud', 'crud2'].includes(context.schema.type);
|
||||
const i18nEnabled = getI18nEnabled();
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
@ -73,21 +80,21 @@ export class ListPlugin extends BasePlugin {
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
level="primary"
|
||||
size="sm"
|
||||
block
|
||||
onClick={this.editDetail.bind(this, context.id)}
|
||||
>
|
||||
配置成员渲染器
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
// {
|
||||
// children: (
|
||||
// <Button
|
||||
// level="primary"
|
||||
// size="sm"
|
||||
// block
|
||||
// onClick={this.editDetail.bind(this, context.id)}
|
||||
// >
|
||||
// 配置成员详情
|
||||
// </Button>
|
||||
// )
|
||||
// },
|
||||
// {
|
||||
// type: 'divider'
|
||||
// },
|
||||
{
|
||||
name: 'title',
|
||||
type: i18nEnabled ? 'input-text-i18n' : 'input-text',
|
||||
@ -95,8 +102,8 @@ export class ListPlugin extends BasePlugin {
|
||||
},
|
||||
isCRUDBody
|
||||
? null
|
||||
: getSchemaTpl('sourceBindControl', {
|
||||
label: '数据源'
|
||||
: getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名'
|
||||
}),
|
||||
{
|
||||
name: 'placeholder',
|
||||
@ -210,7 +217,8 @@ export class ListPlugin extends BasePlugin {
|
||||
const {$schema, ...rest} = props;
|
||||
|
||||
return {
|
||||
...JSONPipeOut(rest),
|
||||
// ...JSONPipeOut(rest),
|
||||
...rest,
|
||||
$schema
|
||||
};
|
||||
}
|
||||
@ -326,6 +334,36 @@ export class ListPlugin extends BasePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'each',
|
||||
type: 'object',
|
||||
title: '当前循环项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
// 循环添加序号方便处理
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '序号'
|
||||
});
|
||||
}
|
||||
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
buildEditorContextMenu(
|
||||
{id, schema, region, info, selections}: ContextMenuEventContext,
|
||||
menus: Array<ContextMenuItem>
|
||||
@ -352,7 +390,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 {
|
||||
|
459
packages/amis-editor/src/plugin/List2.tsx
Normal file
459
packages/amis-editor/src/plugin/List2.tsx
Normal file
@ -0,0 +1,459 @@
|
||||
import {Button, JSONValueMap, isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
BasicRendererInfo,
|
||||
PluginInterface,
|
||||
RendererInfoResolveEventContext
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {repeatArray} from 'amis-editor-core';
|
||||
import set from 'lodash/set';
|
||||
import {escapeFormula, resolveArrayDatasource} from '../util';
|
||||
|
||||
export class List2Plugin extends BasePlugin {
|
||||
static id = 'List2Plugin';
|
||||
static scene = ['layout'];
|
||||
// 关联渲染器名字
|
||||
rendererName = 'cards';
|
||||
$schema = '/schemas/CardsSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '列表';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
description =
|
||||
'功能类似于表格,但是用一个个小卡片来展示数据。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
|
||||
docLink = '/amis/zh-CN/components/cards';
|
||||
tags = ['展示'];
|
||||
icon = 'fa fa-window-maximize';
|
||||
pluginIcon = 'cards-plugin';
|
||||
scaffold = {
|
||||
type: 'cards',
|
||||
columnsCount: 1,
|
||||
card: {
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'flex',
|
||||
items: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '01',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontSize: 'var(--fonts-size-3)',
|
||||
fontWeight: 'var(--fonts-weight-5)',
|
||||
marginRight: '10px'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '/',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
marginRight: '10px',
|
||||
fontSize: 'var(--fonts-size-3)',
|
||||
color: '#cccccc'
|
||||
},
|
||||
id: 'u:95d2a3ac3e70'
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '3月',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '2023',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
'position': 'static',
|
||||
'display': 'flex',
|
||||
'flex': '1 1 auto',
|
||||
'flexGrow': 0,
|
||||
'flexBasis': 'auto',
|
||||
'flexWrap': 'nowrap',
|
||||
'justifyContent': 'flex-start',
|
||||
'alignItems': 'center',
|
||||
'paddingLeft': '20px',
|
||||
'paddingRight': '40px',
|
||||
'right-border-width': 'var(--borders-width-2)',
|
||||
'right-border-style': 'var(--borders-style-2)',
|
||||
'right-border-color': '#ececec',
|
||||
'marginRight': '40px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '列表标题',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-5)',
|
||||
color: 'var(--colors-neutral-text-4)',
|
||||
fontWeight: 'var(--fonts-weight-4)',
|
||||
marginBottom: '10px'
|
||||
},
|
||||
maxLine: 1,
|
||||
id: 'u:105ca9cda3ef'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '这是内容简介,可以设置显示行数',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
maxLine: 1,
|
||||
style: {
|
||||
fontSize: '13px',
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '查看详情',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: []
|
||||
}
|
||||
},
|
||||
level: 'default',
|
||||
size: 'default',
|
||||
editorState: 'default',
|
||||
themeCss: {
|
||||
className: {
|
||||
'border:default': {
|
||||
'top-border-width': 'var(--borders-width-2)',
|
||||
'left-border-width': 'var(--borders-width-2)',
|
||||
'right-border-width': 'var(--borders-width-2)',
|
||||
'bottom-border-width': 'var(--borders-width-2)',
|
||||
'top-border-style': 'var(--borders-style-2)',
|
||||
'left-border-style': 'var(--borders-style-2)',
|
||||
'right-border-style': 'var(--borders-style-2)',
|
||||
'bottom-border-style': 'var(--borders-style-2)',
|
||||
'top-border-color': 'var(--colors-brand-6)',
|
||||
'left-border-color': 'var(--colors-brand-6)',
|
||||
'right-border-color': 'var(--colors-brand-6)',
|
||||
'bottom-border-color': 'var(--colors-brand-6)'
|
||||
},
|
||||
'padding-and-margin:default': {
|
||||
paddingLeft: '20px',
|
||||
paddingRight: '20px'
|
||||
},
|
||||
'radius:default': {
|
||||
'top-left-border-radius': '20px',
|
||||
'top-right-border-radius': '20px',
|
||||
'bottom-left-border-radius': '20px',
|
||||
'bottom-right-border-radius': '20px'
|
||||
},
|
||||
'font:default': {
|
||||
color: 'var(--colors-brand-6)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 0,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
id: 'u:77cb3edb2288'
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
'position': 'static',
|
||||
'display': 'block',
|
||||
'overflowY': 'auto',
|
||||
'overflowX': 'auto',
|
||||
'paddingTop': '10px',
|
||||
'paddingRight': '10px',
|
||||
'paddingBottom': '10px',
|
||||
'paddingLeft': '10px',
|
||||
'radius': {
|
||||
'top-left-border-radius': '6px',
|
||||
'top-right-border-radius': '6px',
|
||||
'bottom-left-border-radius': '6px',
|
||||
'bottom-right-border-radius': '6px'
|
||||
},
|
||||
'top-border-width': 'var(--borders-width-1)',
|
||||
'left-border-width': 'var(--borders-width-1)',
|
||||
'right-border-width': 'var(--borders-width-1)',
|
||||
'bottom-border-width': 'var(--borders-width-1)',
|
||||
'top-border-style': 'var(--borders-style-1)',
|
||||
'left-border-style': 'var(--borders-style-1)',
|
||||
'right-border-style': 'var(--borders-style-1)',
|
||||
'bottom-border-style': 'var(--borders-style-1)',
|
||||
'top-border-color': '#3be157',
|
||||
'left-border-color': '#3be157',
|
||||
'right-border-color': '#3be157',
|
||||
'bottom-border-color': '#3be157',
|
||||
'flex': '0 0 150px',
|
||||
'marginRight': '15px',
|
||||
'flexBasis': '100%',
|
||||
'boxShadow': ' 0px 0px 10px 0px var(--colors-neutral-line-8)'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
onEvent: {
|
||||
click: {
|
||||
weight: 0,
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
overflowX: 'visible',
|
||||
margin: '0',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
wrapperBody: false
|
||||
},
|
||||
placeholder: '',
|
||||
name: 'items',
|
||||
style: {
|
||||
gutterY: 10
|
||||
}
|
||||
};
|
||||
|
||||
previewSchema = {
|
||||
...this.scaffold,
|
||||
className: 'text-left '
|
||||
};
|
||||
|
||||
panelTitle = '列表';
|
||||
panelJustify = true;
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const curPosition = context?.schema?.style?.position;
|
||||
const isAbsolute = curPosition === 'fixed' || curPosition === 'absolute';
|
||||
|
||||
return [
|
||||
getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
label: '组件名称',
|
||||
name: 'editorSetting.displayName'
|
||||
},
|
||||
isCRUDBody
|
||||
? null
|
||||
: getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名'
|
||||
}),
|
||||
getSchemaTpl('cardsPlaceholder')
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '组件',
|
||||
body: [
|
||||
{
|
||||
name: 'columnsCount',
|
||||
type: 'input-range',
|
||||
visibleOn: '!this.leftFixed',
|
||||
min: 1,
|
||||
max: 12,
|
||||
step: 1,
|
||||
label: '每行个数'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '左右间距',
|
||||
name: 'style.gutterX',
|
||||
visibleOn: 'this.columnsCount > 1'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '上下间距',
|
||||
name: 'style.gutterY'
|
||||
},
|
||||
getSchemaTpl('layout:originPosition', {
|
||||
visibleOn: isAbsolute ? isAbsolute : undefined,
|
||||
value: 'left-top'
|
||||
})
|
||||
]
|
||||
},
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
])
|
||||
];
|
||||
};
|
||||
|
||||
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'cards',
|
||||
type: 'object',
|
||||
title: '当前列表项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
// 列表添加序号方便处理
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '索引'
|
||||
});
|
||||
}
|
||||
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
filterProps(props: any) {
|
||||
// 编辑时显示两行假数据
|
||||
const count = (props.columnsCount || 3) * 2;
|
||||
props.value = repeatArray({}, count).map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
id: index + 1
|
||||
};
|
||||
});
|
||||
|
||||
props.className = `${props.className || ''} ae-Editor-list`;
|
||||
props.itemsClassName = `${props.itemsClassName || ''} cards-items`;
|
||||
if (props.card && !props.card.className?.includes('listItem')) {
|
||||
props.card.className = `${props.card.className || ''} ae-Editor-listItem`;
|
||||
}
|
||||
|
||||
// 列表类型内的文本元素显示原始公式
|
||||
props = escapeFormula(props);
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
getRendererInfo(
|
||||
context: RendererInfoResolveEventContext
|
||||
): BasicRendererInfo | void {
|
||||
const plugin: PluginInterface = this;
|
||||
const {renderer, schema} = context;
|
||||
if (
|
||||
!schema.$$id &&
|
||||
schema.$$editor?.renderer.name === 'crud' &&
|
||||
renderer.name === 'cards'
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorPlugin(List2Plugin);
|
@ -58,7 +58,6 @@ export class OperationPlugin extends BasePlugin {
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
size="sm"
|
||||
block
|
||||
className="m-b-sm ae-Button--enhance"
|
||||
onClick={() => {
|
||||
|
@ -1,15 +1,14 @@
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BasePlugin,
|
||||
RegionConfig,
|
||||
BaseEventContext,
|
||||
tipedLabel
|
||||
tipedLabel,
|
||||
defaultValue,
|
||||
getSchemaTpl,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {ValidatorTag} from '../validator';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {getEventControlConfig} from '../renderer/event-control/helper';
|
||||
import {RendererPluginEvent} from 'amis-editor-core';
|
||||
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
|
||||
export class PaginationPlugin extends BasePlugin {
|
||||
static id = 'PaginationPlugin';
|
||||
@ -20,7 +19,6 @@ export class PaginationPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '分页组件';
|
||||
isBaseComponent = true;
|
||||
disabledRendererPlugin = true;
|
||||
description = '分页组件,可以对列表进行分页展示,提高页面性能';
|
||||
tags = ['容器'];
|
||||
icon = 'fa fa-window-minimize';
|
||||
@ -109,7 +107,7 @@ export class PaginationPlugin extends BasePlugin {
|
||||
'启用功能',
|
||||
'选中表示启用该项,可以拖拽排序调整功能的顺序'
|
||||
),
|
||||
visibleOn: 'data.mode === "normal"',
|
||||
visibleOn: '!data.mode || data.mode === "normal"',
|
||||
mode: 'normal',
|
||||
multiple: true,
|
||||
multiLine: false,
|
||||
@ -124,7 +122,8 @@ export class PaginationPlugin extends BasePlugin {
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'checked',
|
||||
className: 'm-t-n-xxs'
|
||||
className: 'm-t-n-xxs',
|
||||
inputClassName: 'p-t-none'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
@ -133,15 +132,31 @@ export class PaginationPlugin extends BasePlugin {
|
||||
}
|
||||
],
|
||||
pipeIn: (value: any) => {
|
||||
if (!value) {
|
||||
value = this.lastLayoutSetting;
|
||||
} else if (typeof value === 'string') {
|
||||
if (typeof value === 'string') {
|
||||
value = (value as string).split(',');
|
||||
} else if (!value || !Array.isArray(value)) {
|
||||
value = this.lastLayoutSetting;
|
||||
}
|
||||
return this.layoutOptions.map(v => ({
|
||||
...v,
|
||||
checked: value.includes(v.value)
|
||||
}));
|
||||
|
||||
return sortBy(
|
||||
this.layoutOptions.map(op => ({
|
||||
...op,
|
||||
checked: value.includes(op.value)
|
||||
})),
|
||||
[
|
||||
item => {
|
||||
const idx = value.findIndex(
|
||||
(v: string) => v === item.value
|
||||
);
|
||||
return ~idx ? idx : Infinity;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// return this.layoutOptions.map(v => ({
|
||||
// ...v,
|
||||
// checked: value.includes(v.value)
|
||||
// }));
|
||||
},
|
||||
pipeOut: (value: any[]) => {
|
||||
this.lastLayoutSetting = value
|
||||
@ -163,7 +178,7 @@ export class PaginationPlugin extends BasePlugin {
|
||||
type: 'combo',
|
||||
label: '每页条数选项',
|
||||
visibleOn:
|
||||
'data.mode === "normal" && data.layout && data.layout.includes("perPage")',
|
||||
'(!data.mode || data.mode === "normal") && data.layout && data.layout.includes("perPage")',
|
||||
mode: 'normal',
|
||||
multiple: true,
|
||||
multiLine: false,
|
||||
@ -185,15 +200,18 @@ export class PaginationPlugin extends BasePlugin {
|
||||
return value?.map(v => ({value: v})) || [10];
|
||||
},
|
||||
pipeOut: (value: any[]) => {
|
||||
return value.map(v => v.value);
|
||||
const pages = value.map(v => v.value);
|
||||
return pages.map(
|
||||
page => page || Math.max(...pages.filter(Boolean)) + 5
|
||||
);
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'perPage',
|
||||
type: 'input-text',
|
||||
type: 'input-number',
|
||||
label: '默认每页条数',
|
||||
visibleOn:
|
||||
'data.mode === "normal" && data.layout?.includes("perPage")'
|
||||
'(!data.mode || data.mode === "normal") && data.layout?.includes("perPage")'
|
||||
},
|
||||
{
|
||||
name: 'maxButtons',
|
||||
@ -205,13 +223,17 @@ export class PaginationPlugin extends BasePlugin {
|
||||
min: 5,
|
||||
max: 20,
|
||||
pipeOut: (value: any) => value || 5,
|
||||
visibleOn: 'data.mode === "normal"'
|
||||
visibleOn: '!data.mode || data.mode === "normal"'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
body: [getSchemaTpl('disabled')]
|
||||
body: [
|
||||
getSchemaTpl('disabled'),
|
||||
getSchemaTpl('hidden'),
|
||||
getSchemaTpl('visible')
|
||||
]
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
registerEditorPlugin,
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
RendererPluginEvent
|
||||
RendererPluginEvent,
|
||||
RendererPluginAction
|
||||
} from 'amis-editor-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
import {getEventControlConfig} from '../renderer/event-control/helper';
|
||||
@ -18,6 +19,7 @@ export class SearchBoxPlugin extends BasePlugin {
|
||||
|
||||
// 组件名称
|
||||
name = '搜索框';
|
||||
searchKeywords = '搜索框、searchbox';
|
||||
isBaseComponent = true;
|
||||
description =
|
||||
'用于展示一个简单搜索框,通常需要搭配其他组件使用。比如 page 配置 initApi 后,可以用来实现简单数据过滤查找,name keywords 会作为参数传递给 page 的 initApi。';
|
||||
@ -28,6 +30,7 @@ export class SearchBoxPlugin extends BasePlugin {
|
||||
|
||||
scaffold: Schema = {
|
||||
type: 'search-box',
|
||||
name: 'keyword',
|
||||
body: {
|
||||
type: 'tpl',
|
||||
tpl: '搜索框',
|
||||
@ -137,6 +140,19 @@ export class SearchBoxPlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
actions: RendererPluginAction[] = [
|
||||
{
|
||||
actionType: 'clear',
|
||||
actionLabel: '清空',
|
||||
description: '清空输入框'
|
||||
},
|
||||
{
|
||||
actionType: 'setValue',
|
||||
actionLabel: '更新数据',
|
||||
description: '更新数据'
|
||||
}
|
||||
];
|
||||
|
||||
notRenderFormZone = true;
|
||||
panelTitle = '搜索框';
|
||||
panelJustify = true;
|
||||
|
@ -1,53 +1,81 @@
|
||||
import {Button} from 'amis';
|
||||
import React from 'react';
|
||||
import {render as amisRender} from 'amis';
|
||||
import flattenDeep from 'lodash/flattenDeep';
|
||||
import {
|
||||
EditorNodeType,
|
||||
JSONPipeOut,
|
||||
jsonToJsonSchema,
|
||||
registerEditorPlugin
|
||||
registerEditorPlugin,
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
RegionConfig,
|
||||
getSchemaTpl,
|
||||
tipedLabel
|
||||
} from 'amis-editor-core';
|
||||
import {BaseEventContext, BasePlugin, RegionConfig} from 'amis-editor-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
import {DSBuilderManager} from '../builder/DSBuilderManager';
|
||||
import {DSFeatureEnum, ModelDSBuilderKey} from '../builder';
|
||||
import {getEventControlConfig} from '../renderer/event-control/helper';
|
||||
|
||||
import type {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core';
|
||||
import type {
|
||||
EditorManager,
|
||||
RendererPluginAction,
|
||||
RendererPluginEvent
|
||||
} from 'amis-editor-core';
|
||||
|
||||
export class ServicePlugin extends BasePlugin {
|
||||
static id = 'ServicePlugin';
|
||||
// 关联渲染器名字
|
||||
rendererName = 'service';
|
||||
|
||||
name = '服务Service';
|
||||
|
||||
panelTitle = '服务Service';
|
||||
|
||||
icon = 'fa fa-server';
|
||||
|
||||
pluginIcon = 'service-plugin';
|
||||
|
||||
panelIcon = 'service-plugin';
|
||||
|
||||
$schema = '/schemas/ServiceSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '服务 Service';
|
||||
isBaseComponent = true;
|
||||
|
||||
order = -850;
|
||||
|
||||
description =
|
||||
'功能性容器,可以用来加载数据或者加载渲染器配置。加载到的数据在容器可以使用。';
|
||||
|
||||
docLink = '/amis/zh-CN/components/service';
|
||||
|
||||
tags = ['数据容器'];
|
||||
icon = 'fa fa-server';
|
||||
pluginIcon = 'service-plugin';
|
||||
|
||||
scaffold = {
|
||||
type: 'service',
|
||||
/** region 区域的 placeholder 会撑开内容区 */
|
||||
body: []
|
||||
};
|
||||
previewSchema = {
|
||||
type: 'service',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '内容',
|
||||
wrapperComponent: '',
|
||||
inline: false
|
||||
tpl: '内容区域',
|
||||
inline: false,
|
||||
className: 'bg-light wrapper'
|
||||
}
|
||||
]
|
||||
};
|
||||
previewSchema = {
|
||||
type: 'tpl',
|
||||
wrapperComponent: '',
|
||||
tpl: '功能性组件,用于数据拉取。'
|
||||
};
|
||||
|
||||
regions: Array<RegionConfig> = [
|
||||
{
|
||||
key: 'body',
|
||||
label: '内容区'
|
||||
label: '内容区',
|
||||
placeholder: amisRender({
|
||||
type: 'wrapper',
|
||||
size: 'lg',
|
||||
body: {type: 'tpl', tpl: '内容区域'}
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
@ -149,9 +177,76 @@ export class ServicePlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
panelTitle = '服务';
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const dsManager = this.dsManager;
|
||||
/** 数据来源选择器 */
|
||||
const dsTypeSelect = () =>
|
||||
dsManager.getDSSelectorSchema({
|
||||
type: 'select',
|
||||
mode: 'horizontal',
|
||||
horizontal: {
|
||||
justify: true,
|
||||
left: 'col-sm-4'
|
||||
},
|
||||
onChange: (value: any, oldValue: any, model: any, form: any) => {
|
||||
if (value !== oldValue) {
|
||||
const data = form.data;
|
||||
Object.keys(data).forEach(key => {
|
||||
if (
|
||||
key?.toLowerCase()?.endsWith('fields') ||
|
||||
key?.toLowerCase().endsWith('api')
|
||||
) {
|
||||
form.deleteValueByName(key);
|
||||
}
|
||||
});
|
||||
form.deleteValueByName('__fields');
|
||||
form.deleteValueByName('__relations');
|
||||
form.setValueByName('api', undefined);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
/** 数据源配置 */
|
||||
const dsSetting = dsManager.buildCollectionFromBuilders(
|
||||
(builder, builderKey) => {
|
||||
return {
|
||||
type: 'container',
|
||||
visibleOn: `this.dsType == null || this.dsType === '${builderKey}'`,
|
||||
body: flattenDeep([
|
||||
builder.makeSourceSettingForm({
|
||||
feat: 'View',
|
||||
renderer: 'service',
|
||||
inScaffold: false,
|
||||
sourceSettings: {
|
||||
name: 'api',
|
||||
label: '接口配置',
|
||||
mode: 'horizontal',
|
||||
...(builderKey === 'api' || builderKey === 'apicenter'
|
||||
? {
|
||||
horizontalConfig: {
|
||||
labelAlign: 'left',
|
||||
horizontal: {
|
||||
justify: true,
|
||||
left: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
useFieldManager: builderKey === ModelDSBuilderKey
|
||||
}
|
||||
})
|
||||
])
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
@ -162,112 +257,56 @@ export class ServicePlugin extends BasePlugin {
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
getSchemaTpl('name'),
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
level="info"
|
||||
size="sm"
|
||||
className="m-b-sm"
|
||||
block
|
||||
onClick={() => {
|
||||
// this.manager.showInsertPanel('body', context.id);
|
||||
this.manager.showRendererPanel('');
|
||||
}}
|
||||
>
|
||||
添加内容
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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: '数据接口初始加载',
|
||||
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: '设置自动定时刷新是否显示加载动画'
|
||||
}),
|
||||
{
|
||||
name: 'stopAutoRefreshWhen',
|
||||
label: '停止定时刷新检测',
|
||||
type: 'input-text',
|
||||
visibleOn: '!!data.interval',
|
||||
description:
|
||||
'定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则不刷新了。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Schema接口',
|
||||
body: [
|
||||
getSchemaTpl('apiControl', {
|
||||
name: 'schemaApi',
|
||||
label: '内容 Schema 接口'
|
||||
}),
|
||||
getSchemaTpl('switch', {
|
||||
name: 'initFetchSchema',
|
||||
label: 'Schema接口初始加载',
|
||||
visibleOn: 'this.schemaApi'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '全局配置',
|
||||
body: [
|
||||
getSchemaTpl('loadingConfig', {}, {context}),
|
||||
getSchemaTpl('data'),
|
||||
{
|
||||
type: 'js-editor',
|
||||
allowFullscreen: true,
|
||||
name: 'dataProvider',
|
||||
label: '自定义函数获取数据',
|
||||
description: '将会传递 data 和 setData 两个参数'
|
||||
},
|
||||
{
|
||||
label: '默认消息信息',
|
||||
type: 'combo',
|
||||
name: 'messages',
|
||||
multiLine: true,
|
||||
description:
|
||||
'设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主',
|
||||
items: [
|
||||
getSchemaTpl('fetchSuccess'),
|
||||
getSchemaTpl('fetchFailed')
|
||||
]
|
||||
}
|
||||
dsTypeSelect(),
|
||||
...dsSetting
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
|
||||
body: [getSchemaTpl('hidden')]
|
||||
},
|
||||
{
|
||||
title: '高级',
|
||||
body: [
|
||||
getSchemaTpl('combo-container', {
|
||||
type: 'input-kv',
|
||||
mode: 'normal',
|
||||
name: 'data',
|
||||
label: '初始化静态数据'
|
||||
}),
|
||||
getSchemaTpl('apiControl', {
|
||||
name: 'schemaApi',
|
||||
label: tipedLabel(
|
||||
'Schema数据源',
|
||||
'配置schemaApi后,可以实现动态渲染页面内容'
|
||||
)
|
||||
}),
|
||||
getSchemaTpl('initFetch', {
|
||||
name: 'initFetchSchema',
|
||||
label: '是否Schema初始加载',
|
||||
visibleOn:
|
||||
'typeof this.schemaApi === "string" ? this.schemaApi : this.schemaApi && this.schemaApi.url'
|
||||
}),
|
||||
{
|
||||
name: 'ws',
|
||||
type: 'input-text',
|
||||
label: tipedLabel(
|
||||
'WebSocket接口',
|
||||
'Service 支持通过WebSocket(ws)获取数据,用于获取实时更新的数据。'
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'js-editor',
|
||||
allowFullscreen: true,
|
||||
name: 'dataProvider',
|
||||
label: tipedLabel(
|
||||
'自定义函数获取数据',
|
||||
'对于复杂的数据获取情况,可以使用外部函数获取数据'
|
||||
),
|
||||
placeholder:
|
||||
'/**\n * @param data 上下文数据\n * @param setData 更新数据的函数\n * @param env 环境变量\n */\ninterface DataProvider {\n (data: any, setData: (data: any) => void, env: any): void;\n}\n'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
]
|
||||
@ -289,6 +328,29 @@ export class ServicePlugin extends BasePlugin {
|
||||
]);
|
||||
};
|
||||
|
||||
panelFormPipeOut = async (schema: any) => {
|
||||
const entity = schema?.api?.entity;
|
||||
|
||||
if (!entity || schema?.dsType !== ModelDSBuilderKey) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const builder = this.dsManager.getBuilderBySchema(schema);
|
||||
|
||||
try {
|
||||
const updatedSchema = await builder.buildApiSchema({
|
||||
schema,
|
||||
renderer: 'service',
|
||||
sourceKey: 'api'
|
||||
});
|
||||
return updatedSchema;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
async buildDataSchemas(
|
||||
node: EditorNodeType,
|
||||
region?: EditorNodeType,
|
||||
@ -331,6 +393,25 @@ export class ServicePlugin extends BasePlugin {
|
||||
scope?.addSchema(jsonschema);
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailableContextFields(
|
||||
scopeNode: EditorNodeType,
|
||||
node: EditorNodeType,
|
||||
region?: EditorNodeType
|
||||
) {
|
||||
const builder = this.dsManager.getBuilderBySchema(scopeNode.schema);
|
||||
|
||||
if (builder && scopeNode.schema.api) {
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: DSFeatureEnum.List
|
||||
},
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorPlugin(ServicePlugin);
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
RendererPluginEvent
|
||||
} from 'amis-editor-core';
|
||||
import {findTree, setVariable, someTree} from 'amis-core';
|
||||
|
||||
import {registerEditorPlugin, repeatArray, diff} from 'amis-editor-core';
|
||||
import {
|
||||
BasePlugin,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
InsertEventContext,
|
||||
ScaffoldForm
|
||||
} from 'amis-editor-core';
|
||||
import {DSBuilderManager} from '../builder/DSBuilderManager';
|
||||
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
|
||||
import {mockValue} from 'amis-editor-core';
|
||||
import {EditorNodeType} from 'amis-editor-core';
|
||||
@ -34,6 +34,8 @@ import {
|
||||
} from '../util';
|
||||
import {reaction} from 'mobx';
|
||||
|
||||
import type {EditorManager} from 'amis-editor-core';
|
||||
|
||||
export class TablePlugin extends BasePlugin {
|
||||
static id = 'TablePlugin';
|
||||
// 关联渲染器名字
|
||||
@ -41,7 +43,7 @@ export class TablePlugin extends BasePlugin {
|
||||
$schema = '/schemas/TableSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '表格';
|
||||
name = '原子表格';
|
||||
tags = ['展示'];
|
||||
isBaseComponent = true;
|
||||
description =
|
||||
@ -433,7 +435,16 @@ export class TablePlugin extends BasePlugin {
|
||||
description: '开启表格拖拽排序功能'
|
||||
}
|
||||
];
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const i18nEnabled = getI18nEnabled();
|
||||
@ -888,6 +899,38 @@ export class TablePlugin extends BasePlugin {
|
||||
};
|
||||
}
|
||||
|
||||
async getAvailableContextFields(
|
||||
scopeNode: EditorNodeType,
|
||||
node: EditorNodeType,
|
||||
region?: EditorNodeType
|
||||
) {
|
||||
if (node?.info?.renderer?.name === 'table-cell') {
|
||||
if (
|
||||
scopeNode.parent?.type === 'service' &&
|
||||
scopeNode.parent?.parent?.path?.endsWith('service')
|
||||
) {
|
||||
return scopeNode.parent.parent.info.plugin.getAvailableContextFields?.(
|
||||
scopeNode.parent.parent,
|
||||
node,
|
||||
region
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const builder = this.dsManager.getBuilderBySchema(scopeNode.schema);
|
||||
|
||||
if (builder && scopeNode.schema.api) {
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: 'List'
|
||||
},
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editHeaderDetail(id: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -189,7 +189,13 @@ export class TagPlugin extends BasePlugin {
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('label'),
|
||||
getSchemaTpl('valueFormula', {
|
||||
name: 'label',
|
||||
label: '标签内容',
|
||||
rendererSchema: {
|
||||
type: 'input-text'
|
||||
}
|
||||
}),
|
||||
{
|
||||
type: 'button-group-select',
|
||||
label: '模式',
|
||||
|
@ -230,7 +230,23 @@ export class TplPlugin extends BasePlugin {
|
||||
pipeIn: defaultValue(true),
|
||||
hiddenOn: 'data.wrapperComponent !== ""'
|
||||
}),
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '最大显示行数',
|
||||
name: 'maxLine',
|
||||
min: 0
|
||||
},
|
||||
getSchemaTpl('tpl:content'),
|
||||
{
|
||||
type: 'textarea',
|
||||
name: 'editorSetting.mock.tpl',
|
||||
mode: 'vertical',
|
||||
label: tipedLabel(
|
||||
'填充假数据',
|
||||
'只在编辑区显示的假数据文本,运行时将显示文本实际内容'
|
||||
),
|
||||
pipeOut: (value: any) => (value === '' ? undefined : value)
|
||||
},
|
||||
getSchemaTpl('tpl:rich-text')
|
||||
]
|
||||
},
|
||||
|
@ -12,12 +12,7 @@ export * from './Tabs'; // 选项卡
|
||||
|
||||
// 数据容器
|
||||
export * from './CRUD'; // 增删改查
|
||||
export {
|
||||
TableCRUDPlugin,
|
||||
ListCRUDPlugin,
|
||||
CardsCRUDPlugin,
|
||||
CRUDPlugin as CRUD2Plugin
|
||||
} from './CRUD2';
|
||||
export * from './CRUD2/CRUDTable'; // 增删改查v2.0
|
||||
export * from './Form/Form'; // 表单
|
||||
export * from './Service'; // 服务service
|
||||
|
||||
@ -91,6 +86,7 @@ export * from './Tpl'; // 文字
|
||||
export * from './Icon'; // 图标
|
||||
export * from './Link'; // 链接
|
||||
export * from './List'; // 列表
|
||||
export * from './List2'; // 列表
|
||||
export * from './Mapping'; // 映射
|
||||
export * from './Avatar'; // 头像
|
||||
export * from './Card'; // 卡片
|
||||
|
@ -15,11 +15,12 @@ import debounce from 'lodash/debounce';
|
||||
import remove from 'lodash/remove';
|
||||
import React from 'react';
|
||||
import {EditorManager, EditorNodeType, autobind} from 'amis-editor-core';
|
||||
import type {DSField, DSFieldGroup} from 'amis-editor-core';
|
||||
import {matchSorter} from 'match-sorter';
|
||||
import type {SchemaCollection} from 'amis';
|
||||
import {default as cx} from 'classnames';
|
||||
|
||||
import type {SchemaCollection} from 'amis';
|
||||
import type {DSField, DSFieldGroup} from '../builder';
|
||||
|
||||
export interface DataBindingProps extends FormControlProps {
|
||||
node: EditorNodeType;
|
||||
manager: EditorManager;
|
||||
|
@ -4,15 +4,18 @@
|
||||
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import Sortable from 'sortablejs';
|
||||
import cx from 'classnames';
|
||||
import {FormItem, Button, Icon, FormControlProps, autobind} from 'amis';
|
||||
|
||||
import clone from 'lodash/clone';
|
||||
import remove from 'lodash/remove';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import {FormItem, Button, Icon, FormControlProps, autobind} from 'amis';
|
||||
import {Checkbox} from 'amis-ui';
|
||||
import {evalExpression} from 'amis-core';
|
||||
import {GoConfigControl} from './GoConfigControl';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
const klass = 'ae-FeatureControl';
|
||||
|
||||
export type FeatureOption = {
|
||||
label: string;
|
||||
value: any;
|
||||
@ -28,9 +31,16 @@ interface FeatureControlProps extends FormControlProps {
|
||||
addable?: boolean;
|
||||
addText?: string;
|
||||
sortable?: boolean;
|
||||
checkable?: boolean;
|
||||
checkableOn?: string;
|
||||
features: Array<FeatureOption> | ((schema: any) => Array<FeatureOption>);
|
||||
goFeatureComp?: (item: FeatureOption) => string; // 去子组件
|
||||
onSort?: (value: FeatureOption[]) => void;
|
||||
goFeatureComp?: (item: FeatureOption, index: number) => string; // 去子组件
|
||||
onSort?: (data: any, value: {oldIndex: number; newIndex: number}) => void;
|
||||
// 自定义添加内容,按钮变成普通按钮
|
||||
customAction?: (props: {schema: any; onBulkChange: any}) => any;
|
||||
onItemCheck?: (checked: boolean, index: number, schema: any) => void;
|
||||
// 所有都添加完成后,隐藏添加按钮
|
||||
hideAddWhenAll?: boolean;
|
||||
}
|
||||
|
||||
interface FeatureControlState {
|
||||
@ -97,7 +107,6 @@ export default class FeatureControl extends React.Component<
|
||||
handleRemove(item: FeatureOption, index: number) {
|
||||
const {removeFeature, data, onBulkChange} = this.props;
|
||||
const {inUseFeat, unUseFeat} = this.state;
|
||||
|
||||
item.remove?.(data);
|
||||
removeFeature?.(item, data);
|
||||
onBulkChange?.(data);
|
||||
@ -108,6 +117,12 @@ export default class FeatureControl extends React.Component<
|
||||
this.setState({inUseFeat, unUseFeat});
|
||||
}
|
||||
|
||||
handleSort(e: any) {
|
||||
const {data, onBulkChange, onSort} = this.props;
|
||||
onSort?.(data, e);
|
||||
onBulkChange?.(data);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleAdd(item: any) {
|
||||
const {addFeature, data, onBulkChange} = this.props;
|
||||
@ -173,7 +188,10 @@ export default class FeatureControl extends React.Component<
|
||||
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);
|
||||
this.handleSort({
|
||||
oldIndex: e.oldIndex,
|
||||
newIndex: e.newIndex
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -187,8 +205,24 @@ export default class FeatureControl extends React.Component<
|
||||
this.sortable && this.sortable.destroy();
|
||||
}
|
||||
|
||||
renderItem(item: FeatureOption, index: number) {
|
||||
const {sortable, goFeatureComp, node, manager} = this.props;
|
||||
@autobind
|
||||
handleCheck(res: boolean, index: number) {
|
||||
const {data, onBulkChange, onItemCheck} = this.props;
|
||||
const schema = clone(data);
|
||||
onItemCheck?.(res, index, schema);
|
||||
onBulkChange?.(schema);
|
||||
}
|
||||
|
||||
renderItem(item: FeatureOption, index: number, checkable: boolean) {
|
||||
const {
|
||||
sortable,
|
||||
goFeatureComp,
|
||||
node,
|
||||
manager,
|
||||
onItemCheck,
|
||||
isItemChecked,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
let content = null;
|
||||
|
||||
@ -199,7 +233,7 @@ export default class FeatureControl extends React.Component<
|
||||
className={cx(`${klass}Item-go`)}
|
||||
label={item.label}
|
||||
manager={manager}
|
||||
compId={() => goFeatureComp(item)}
|
||||
compId={() => goFeatureComp(item, index)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -208,12 +242,21 @@ export default class FeatureControl extends React.Component<
|
||||
|
||||
return (
|
||||
<li className={klass + 'Item'} key={index}>
|
||||
{sortable && (
|
||||
<a className={klass + 'Item-dragBar'}>
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
{checkable && onItemCheck && (
|
||||
<Checkbox
|
||||
checked={isItemChecked(item, index, data)}
|
||||
onChange={(val: any) => this.handleCheck(val, index)}
|
||||
/>
|
||||
)}
|
||||
{content}
|
||||
|
||||
<div className={klass + 'Item-content'}>
|
||||
{sortable && (
|
||||
<a className={klass + 'Item-dragBar'}>
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
<Button
|
||||
className={klass + 'Item-action'}
|
||||
onClick={() => this.handleRemove(item, index)}
|
||||
@ -225,11 +268,31 @@ export default class FeatureControl extends React.Component<
|
||||
}
|
||||
|
||||
renderAction() {
|
||||
const {addable, addText, render} = this.props;
|
||||
const {
|
||||
addable,
|
||||
addText,
|
||||
render,
|
||||
customAction,
|
||||
data,
|
||||
onBulkChange,
|
||||
hideAddWhenAll
|
||||
} = this.props;
|
||||
if (!addable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (customAction && typeof customAction === 'function') {
|
||||
const schema = customAction({onBulkChange, schema: clone(data)});
|
||||
|
||||
if (isPlainObject(schema) && typeof schema.type === 'string') {
|
||||
return render('custom-action', schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (hideAddWhenAll && !this.state.unUseFeat.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return render('action', {
|
||||
type: 'dropdown-button',
|
||||
closeOnClick: true,
|
||||
@ -252,13 +315,21 @@ export default class FeatureControl extends React.Component<
|
||||
}
|
||||
|
||||
render() {
|
||||
const {className} = this.props;
|
||||
const {className, checkable, checkableOn, data} = this.props;
|
||||
|
||||
let isCheckable = false;
|
||||
|
||||
if (checkable !== undefined) {
|
||||
isCheckable = checkable;
|
||||
} else if (checkableOn) {
|
||||
isCheckable = evalExpression(checkableOn, data) === true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('ae-FeatureControl', className)}>
|
||||
<ul className={cx('ae-FeatureControl-features')} ref={this.dragRef}>
|
||||
{this.state.inUseFeat.map((item, index) =>
|
||||
this.renderItem(item, index)
|
||||
this.renderItem(item, index, isCheckable)
|
||||
)}
|
||||
</ul>
|
||||
|
||||
|
585
packages/amis-editor/src/renderer/FieldSetting.tsx
Normal file
585
packages/amis-editor/src/renderer/FieldSetting.tsx
Normal file
@ -0,0 +1,585 @@
|
||||
/**
|
||||
* @file FieldSetting.tsx
|
||||
* @desc 脚手架中字段管理
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {reaction} from 'mobx';
|
||||
import pick from 'lodash/pick';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {
|
||||
FormItem,
|
||||
FormControlProps,
|
||||
autobind,
|
||||
isValidApi,
|
||||
normalizeApi
|
||||
} from 'amis-core';
|
||||
import {
|
||||
Form,
|
||||
InputTable,
|
||||
Controller,
|
||||
InputBox,
|
||||
Select,
|
||||
Button,
|
||||
toast
|
||||
} from 'amis-ui';
|
||||
|
||||
import type {IReactionDisposer} from 'mobx';
|
||||
import type {InputTableColumnProps} from 'amis-ui';
|
||||
import type {DSFeatureType, ScaffoldField} from '../builder/type';
|
||||
|
||||
interface FieldSettingProps extends FormControlProps {
|
||||
/** 脚手架渲染类型 */
|
||||
renderer?: string;
|
||||
feat: DSFeatureType;
|
||||
config: {
|
||||
showInputType?: boolean;
|
||||
showDisplayType?: boolean;
|
||||
};
|
||||
onAutoGenerateFields: (params: {
|
||||
api: any;
|
||||
props: FieldSettingProps;
|
||||
setState: (state: any) => void;
|
||||
}) => Promise<any[]>;
|
||||
}
|
||||
|
||||
interface RowData extends ScaffoldField {}
|
||||
|
||||
export class FieldSetting extends React.Component<
|
||||
FieldSettingProps,
|
||||
{loading: boolean}
|
||||
> {
|
||||
static defaultProps = {
|
||||
config: {
|
||||
showInputType: true,
|
||||
showDisplayType: true
|
||||
}
|
||||
};
|
||||
|
||||
static validator = (items: RowData[], isInternal?: boolean) => {
|
||||
const cache: Record<string, boolean> = {};
|
||||
const fields = items ?? [];
|
||||
let error: string | boolean = false;
|
||||
|
||||
for (let [index, item] of fields.entries()) {
|
||||
/** 提交时再校验 */
|
||||
if (!item.name && isInternal !== true) {
|
||||
error = `序号「${index + 1}」的字段名称不能为空`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!cache.hasOwnProperty(item.name)) {
|
||||
cache[item.name] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
error = `序号「${index + 1}」的字段名称「${item.name}」不唯一`;
|
||||
break;
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
reaction: IReactionDisposer;
|
||||
|
||||
dom: HTMLElement;
|
||||
|
||||
formRef = React.createRef<{submit: () => Promise<Record<string, any>>}>();
|
||||
|
||||
tableRef = React.createRef<any>();
|
||||
|
||||
scaffold: RowData = {
|
||||
label: '',
|
||||
name: '',
|
||||
displayType: 'tpl',
|
||||
inputType: 'input-text'
|
||||
};
|
||||
|
||||
constructor(props: FieldSettingProps) {
|
||||
super(props);
|
||||
this.state = {loading: false};
|
||||
this.reaction = reaction(
|
||||
() => {
|
||||
const ctx = props?.store?.data;
|
||||
const initApi = ctx?.initApi;
|
||||
const listApi = ctx?.listApi;
|
||||
return `${initApi}${listApi}`;
|
||||
},
|
||||
() => this.forceUpdate()
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dom = findDOMNode(this) as HTMLElement;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.reaction?.();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleColumnBlur() {
|
||||
this?.formRef?.current?.submit();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSubmit(data: {items: RowData[]}) {
|
||||
const {value} = this.props;
|
||||
const items = (data?.items ?? []).map((field: RowData) => {
|
||||
const item = value?.find((f: RowData) => f.name === field.name);
|
||||
return {
|
||||
...pick(
|
||||
{
|
||||
...item,
|
||||
...field
|
||||
},
|
||||
['label', 'name', 'displayType', 'inputType']
|
||||
),
|
||||
checked: true
|
||||
};
|
||||
});
|
||||
|
||||
this.handleFieldsChange(items);
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleGenerateFields(e: React.MouseEvent<any>) {
|
||||
const {
|
||||
store,
|
||||
renderer,
|
||||
feat,
|
||||
env,
|
||||
manager,
|
||||
data: ctx,
|
||||
onAutoGenerateFields
|
||||
} = this.props;
|
||||
const scaffoldData = store?.data;
|
||||
let api =
|
||||
renderer === 'form'
|
||||
? scaffoldData?.initApi
|
||||
: renderer === 'crud'
|
||||
? scaffoldData?.listApi
|
||||
: '';
|
||||
|
||||
if (!api || (renderer === 'form' && feat !== 'Edit')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
let fields: RowData[] = [];
|
||||
|
||||
if (onAutoGenerateFields && typeof onAutoGenerateFields === 'function') {
|
||||
try {
|
||||
fields = await onAutoGenerateFields({
|
||||
api: api,
|
||||
props: this.props,
|
||||
setState: this.setState
|
||||
});
|
||||
} catch (error) {
|
||||
toast.warning(
|
||||
error.message ?? 'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const schemaFilter = manager?.store?.schemaFilter;
|
||||
|
||||
if (schemaFilter) {
|
||||
api = schemaFilter({
|
||||
api
|
||||
}).api;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await env?.fetcher(api, ctx);
|
||||
|
||||
if (!result.ok) {
|
||||
toast.warning(
|
||||
result.defaultMsg ??
|
||||
result.msg ??
|
||||
'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
this.setState({loading: false});
|
||||
return;
|
||||
}
|
||||
|
||||
let sampleRow: Record<string, any>;
|
||||
if (feat === 'List') {
|
||||
const items = result.data?.rows || result.data?.items || result.data;
|
||||
sampleRow = items?.[0];
|
||||
} else {
|
||||
sampleRow = result.data;
|
||||
}
|
||||
|
||||
if (sampleRow) {
|
||||
Object.entries(sampleRow).forEach(([key, value]) => {
|
||||
fields.push({
|
||||
label: key,
|
||||
name: key,
|
||||
displayType: 'tpl',
|
||||
inputType:
|
||||
typeof value === 'number' ? 'input-number' : 'input-text',
|
||||
checked: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.warning(
|
||||
error.message ?? 'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (fields && fields.length > 0) {
|
||||
this.handleFieldsChange(fields);
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleFieldsChange(fields: RowData[]) {
|
||||
const {
|
||||
onChange,
|
||||
onBulkChange,
|
||||
submitOnChange,
|
||||
renderer,
|
||||
data: ctx
|
||||
} = this.props;
|
||||
const isFirstStep = ctx?.__step === 0;
|
||||
|
||||
if (renderer === 'form') {
|
||||
onChange?.(fields, submitOnChange, true);
|
||||
} else {
|
||||
if (isFirstStep) {
|
||||
onBulkChange?.(
|
||||
{
|
||||
listFields: fields,
|
||||
editFields: fields,
|
||||
bulkEditFields: fields,
|
||||
insertFields: fields,
|
||||
viewFields: fields,
|
||||
simpleQueryFields: fields
|
||||
},
|
||||
submitOnChange
|
||||
);
|
||||
} else {
|
||||
onChange?.(fields, submitOnChange, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFooter() {
|
||||
const {renderer, store, data: ctx, feat} = this.props;
|
||||
const scaffoldData = store?.data;
|
||||
const {initApi, listApi} = scaffoldData || {};
|
||||
const {loading} = this.state;
|
||||
const fieldApi =
|
||||
renderer === 'form' ? initApi : renderer === 'crud' ? listApi : '';
|
||||
const isApiValid = isValidApi(normalizeApi(fieldApi)?.url);
|
||||
const showAutoGenBtn =
|
||||
(renderer === 'form' && feat === 'Edit') ||
|
||||
(renderer === 'crud' && feat === 'List' && ctx?.__step === 0);
|
||||
|
||||
return showAutoGenBtn ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
level="link"
|
||||
loading={loading}
|
||||
disabled={!isApiValid}
|
||||
disabledTip={{
|
||||
content:
|
||||
renderer === 'form' ? '请先填写初始化接口' : '请先填写接口',
|
||||
tooltipTheme: 'dark'
|
||||
}}
|
||||
onClick={e => this.handleGenerateFields(e)}
|
||||
>
|
||||
<span>基于接口自动生成字段</span>
|
||||
</Button>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
value: formValue,
|
||||
defaultValue: formDefaultValue,
|
||||
env,
|
||||
renderer,
|
||||
config,
|
||||
data: ctx,
|
||||
feat
|
||||
} = this.props;
|
||||
const {showDisplayType, showInputType} = config || {};
|
||||
const isForm = renderer === 'form';
|
||||
const defaultValue = Array.isArray(formDefaultValue)
|
||||
? {items: formDefaultValue}
|
||||
: {items: []};
|
||||
const value = Array.isArray(formValue) ? {items: formValue} : undefined;
|
||||
const popOverContainer = env?.getModalContainer?.() ?? this.dom;
|
||||
const isFirstStep = ctx?.__step === 0;
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={cx('ae-FieldSetting')}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
autoSubmit={false}
|
||||
// onChange={this.handleTableChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
ref={this.formRef}
|
||||
>
|
||||
{({control}: any) => (
|
||||
<>
|
||||
<InputTable
|
||||
ref={this.tableRef}
|
||||
name="items"
|
||||
label={false}
|
||||
labelAlign="left"
|
||||
mode="horizontal"
|
||||
horizontal={{left: 4}}
|
||||
control={control}
|
||||
scaffold={this.scaffold}
|
||||
addable={true}
|
||||
removable={true}
|
||||
isRequired={false}
|
||||
rules={{
|
||||
validate: (values: any[]) =>
|
||||
FieldSetting.validator(values, true)
|
||||
}}
|
||||
addButtonText="添加字段"
|
||||
addButtonProps={{level: 'link'}}
|
||||
scroll={{y: '315.5px'}}
|
||||
footer={this.renderFooter}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
title: '序号',
|
||||
tdRender: (
|
||||
{control}: any,
|
||||
index: number,
|
||||
rowIndex: number
|
||||
) => {
|
||||
return (
|
||||
<Controller
|
||||
name="index"
|
||||
control={control}
|
||||
render={({field, fieldState}) => (
|
||||
<span>{rowIndex + 1}</span>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '字段名称',
|
||||
tdRender: ({control}: any) => {
|
||||
return (
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={renderProps => {
|
||||
const {field, fieldState} = renderProps;
|
||||
return (
|
||||
<InputBox
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
this.handleColumnBlur();
|
||||
}}
|
||||
hasError={!!fieldState.error}
|
||||
className={cx('ae-FieldSetting-input')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
tdRender: ({control}: any) => {
|
||||
return (
|
||||
<Controller
|
||||
name="label"
|
||||
control={control}
|
||||
render={renderProps => {
|
||||
const {field, fieldState} = renderProps;
|
||||
return (
|
||||
<InputBox
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
this.handleColumnBlur();
|
||||
}}
|
||||
hasError={!!fieldState.error}
|
||||
className={cx('ae-FieldSetting-input')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
showInputType &&
|
||||
!(renderer === 'crud' && feat === 'List' && !isFirstStep)
|
||||
? {
|
||||
title: '输入类型',
|
||||
tdRender: ({control}: any, index: number) => {
|
||||
return (
|
||||
<Controller
|
||||
name="inputType"
|
||||
control={control}
|
||||
isRequired
|
||||
render={({field, fieldState}) => (
|
||||
<Select
|
||||
{...field}
|
||||
className={'w-full'}
|
||||
hasError={!!fieldState.error}
|
||||
searchable
|
||||
disabled={false}
|
||||
clearable={false}
|
||||
popOverContainer={popOverContainer}
|
||||
options={[
|
||||
{
|
||||
label: '单行文本框',
|
||||
value: 'input-text'
|
||||
},
|
||||
{
|
||||
label: '多行文本',
|
||||
value: 'textarea'
|
||||
},
|
||||
{
|
||||
label: '数字输入',
|
||||
value: 'input-number'
|
||||
},
|
||||
{
|
||||
label: '单选框',
|
||||
value: 'radios'
|
||||
},
|
||||
{
|
||||
label: '勾选框',
|
||||
value: 'checkbox'
|
||||
},
|
||||
{
|
||||
label: '复选框',
|
||||
value: 'checkboxes'
|
||||
},
|
||||
{
|
||||
label: '下拉框',
|
||||
value: 'select'
|
||||
},
|
||||
{
|
||||
label: '开关',
|
||||
value: 'switch'
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
value: 'input-date'
|
||||
},
|
||||
{
|
||||
label: '表格编辑',
|
||||
value: 'input-table'
|
||||
},
|
||||
{
|
||||
label: '组合输入',
|
||||
value: 'combo'
|
||||
},
|
||||
{
|
||||
label: '文件上传',
|
||||
value: 'input-file'
|
||||
},
|
||||
{
|
||||
label: '图片上传',
|
||||
value: 'input-image'
|
||||
},
|
||||
{
|
||||
label: '富文本编辑器',
|
||||
value: 'input-rich-text'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
showDisplayType
|
||||
? {
|
||||
title: '展示类型',
|
||||
tdRender: ({control}: any) => {
|
||||
return (
|
||||
<Controller
|
||||
name="displayType"
|
||||
control={control}
|
||||
isRequired
|
||||
render={({field, fieldState}) => (
|
||||
<Select
|
||||
{...field}
|
||||
className={'w-full'}
|
||||
hasError={!!fieldState.error}
|
||||
searchable
|
||||
disabled={false}
|
||||
clearable={false}
|
||||
popOverContainer={popOverContainer}
|
||||
options={[
|
||||
{
|
||||
value: 'tpl',
|
||||
label: '文本',
|
||||
typeKey: 'tpl'
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
label: '图片',
|
||||
typeKey: 'src'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: '日期',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'progress',
|
||||
label: '进度',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'mapping',
|
||||
label: '映射',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
label: '列表',
|
||||
typeKey: 'value'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
].filter(
|
||||
(f): f is Exclude<typeof f, null | undefined> => f != null
|
||||
) as InputTableColumnProps[]
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({type: 'ae-field-setting'})
|
||||
export default class FieldSettingRenderer extends FieldSetting {}
|
@ -46,6 +46,12 @@ export interface SwitchMoreProps extends FormControlProps {
|
||||
onClose: (e: React.UIEvent<any> | void) => void;
|
||||
clearChildValuesOnOff?: boolean; // 关闭开关时,删除子表单字段,默认 true
|
||||
defaultData?: any; // 默认数据
|
||||
isChecked?: (options: {
|
||||
data: any;
|
||||
value: any;
|
||||
name?: string;
|
||||
bulk?: boolean;
|
||||
}) => boolean;
|
||||
}
|
||||
|
||||
interface SwitchMoreState {
|
||||
@ -104,13 +110,25 @@ export default class SwitchMore extends React.Component<
|
||||
}
|
||||
|
||||
initState() {
|
||||
const {data, value, trueValue, falseValue, name, bulk, hiddenOnDefault} =
|
||||
this.props;
|
||||
const {
|
||||
data,
|
||||
value,
|
||||
trueValue,
|
||||
falseValue,
|
||||
name,
|
||||
bulk,
|
||||
hiddenOnDefault,
|
||||
isChecked
|
||||
} = this.props;
|
||||
let checked = false;
|
||||
let show = false;
|
||||
|
||||
if (isChecked && typeof isChecked === 'function') {
|
||||
checked = isChecked({data, value, name, bulk});
|
||||
show = checked;
|
||||
}
|
||||
// 这个开关 无具体属性对应
|
||||
if (!name) {
|
||||
else if (!name) {
|
||||
// 子表单项是组件根属性,遍历看是否有值
|
||||
if (bulk) {
|
||||
const formNames = this.getFormItemNames();
|
||||
|
211
packages/amis-editor/src/renderer/TableColumnWidthControl.tsx
Normal file
211
packages/amis-editor/src/renderer/TableColumnWidthControl.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @file 表格自定义列可视化编辑控件
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import {FormControlProps, FormItem, TreeSelection} from 'amis';
|
||||
import {toNumber} from 'amis-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
|
||||
interface optionType {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TableColumnWidthProps extends FormControlProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TableColumnWidthState {
|
||||
columns?: Array<any>;
|
||||
activeOption: optionType;
|
||||
}
|
||||
|
||||
export default class TableColumnWidthControl extends React.Component<
|
||||
TableColumnWidthProps,
|
||||
TableColumnWidthState
|
||||
> {
|
||||
options: Array<optionType> = [
|
||||
{
|
||||
label: '自适应',
|
||||
value: 'adaptive'
|
||||
},
|
||||
{
|
||||
label: '百分比',
|
||||
value: 'percentage'
|
||||
},
|
||||
{
|
||||
label: '固定宽度',
|
||||
value: 'fixed'
|
||||
}
|
||||
];
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeOption: this.options[0]
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const {value} = this.props;
|
||||
|
||||
if (value === undefined) return;
|
||||
|
||||
if (typeof value === 'number') {
|
||||
this.state.activeOption !== this.options[2] &&
|
||||
this.setState({
|
||||
activeOption: this.options[2]
|
||||
});
|
||||
} else if (typeof value === 'string' && value.endsWith('%')) {
|
||||
this.state.activeOption !== this.options[1] &&
|
||||
this.setState({
|
||||
activeOption: this.options[1]
|
||||
});
|
||||
} else {
|
||||
this.state.activeOption !== this.options[0] &&
|
||||
this.setState({
|
||||
activeOption: this.options[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOptionChange(item: optionType) {
|
||||
if (item === this.state.activeOption) return;
|
||||
|
||||
this.setState({
|
||||
activeOption: item
|
||||
});
|
||||
this.props?.onChange?.(undefined);
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const {
|
||||
render,
|
||||
formLabel,
|
||||
labelRemark,
|
||||
useMobileUI,
|
||||
env,
|
||||
popOverContainer,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
const classPrefix = env?.theme?.classPrefix;
|
||||
|
||||
const {activeOption} = this.state;
|
||||
|
||||
return (
|
||||
<div className="ae-columnWidthControl-header">
|
||||
<label className={cx(`${classPrefix}Form-label`)}>
|
||||
{formLabel || ''}
|
||||
{labelRemark
|
||||
? render('label-remark', {
|
||||
type: 'remark',
|
||||
icon: labelRemark.icon || 'warning-mark',
|
||||
tooltip: labelRemark,
|
||||
className: cx(`Form-lableRemark`, labelRemark?.className),
|
||||
useMobileUI,
|
||||
container: popOverContainer
|
||||
? popOverContainer
|
||||
: env && env.getModalContainer
|
||||
? env.getModalContainer
|
||||
: undefined
|
||||
})
|
||||
: null}
|
||||
</label>
|
||||
{render(
|
||||
'columnWidthControl-options',
|
||||
{
|
||||
type: 'dropdown-button',
|
||||
level: 'link',
|
||||
size: 'sm',
|
||||
label: activeOption.label,
|
||||
align: 'right',
|
||||
closeOnClick: true,
|
||||
closeOnOutside: true,
|
||||
buttons: this.options.map(item => ({
|
||||
...item,
|
||||
onClick: () => this.handleOptionChange(item)
|
||||
}))
|
||||
},
|
||||
{
|
||||
popOverContainer: null
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleChange(type: 'fixed' | 'percentage', val: number) {
|
||||
const onChange = this.props.onChange;
|
||||
|
||||
if (typeof val !== 'number' || isNaN(val)) return;
|
||||
|
||||
if (val <= 0) {
|
||||
onChange?.(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange?.(type === 'percentage' ? val + '%' : val);
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
const {onBulkChange, render, onChange, value} = this.props;
|
||||
const {activeOption} = this.state;
|
||||
|
||||
if (activeOption.value === 'adaptive') {
|
||||
return null;
|
||||
}
|
||||
if (activeOption.value === 'fixed') {
|
||||
return render(
|
||||
'columnWidthControl-fixed',
|
||||
getSchemaTpl('withUnit', {
|
||||
label: '固定列宽',
|
||||
name: 'interval',
|
||||
control: {
|
||||
type: 'input-number',
|
||||
min: 0,
|
||||
value
|
||||
// onChange: (val: number) => this.handleChange('fixed', val)
|
||||
},
|
||||
unit: 'px',
|
||||
className: 'mt-3'
|
||||
}),
|
||||
{
|
||||
onChange: (val: number) => this.handleChange('fixed', val)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return render('columnWidthControl-fixed', {
|
||||
type: 'input-range',
|
||||
name: 'range',
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
label: '百分比列宽',
|
||||
value: toNumber(value),
|
||||
onChange: (val: number) =>
|
||||
activeOption.value === 'percentage' &&
|
||||
this.handleChange('percentage', val)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={cx('ae-columnWidthControl')}>
|
||||
{this.renderHeader()}
|
||||
{this.renderBody()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({
|
||||
type: 'ae-columnWidthControl',
|
||||
renderLabel: false
|
||||
})
|
||||
export class TableColumnWidthControlRender extends TableColumnWidthControl {}
|
@ -0,0 +1,298 @@
|
||||
/**
|
||||
* @file AddColumnModal
|
||||
* @desc 添加列
|
||||
*/
|
||||
|
||||
import {useEffect, useRef} from 'react';
|
||||
import omit from 'lodash/omit';
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import {Button, Modal, themeable, ThemeProps, utils} from 'amis';
|
||||
import {getSchemaTpl, JSONPipeIn, EditorManager} from 'amis-editor-core';
|
||||
import {DSFeatureType, DSFeatureEnum, ModelDSBuilderKey} from '../../builder';
|
||||
|
||||
import type {RendererProps, BaseApiObject} from 'amis';
|
||||
import type {CRUDColumnControlState} from './CRUDColumnControl';
|
||||
import type {ColumnSchema} from 'amis/lib/renderers/Table2';
|
||||
import type {DSBuilderInterface} from '../../builder';
|
||||
|
||||
type InitData = Exclude<CRUDColumnControlState['addModalData'], undefined>;
|
||||
|
||||
interface AddColumnModalProps extends ThemeProps {
|
||||
visible: boolean;
|
||||
initData: InitData;
|
||||
ctx: Record<string, any>;
|
||||
manager: EditorManager;
|
||||
builder: DSBuilderInterface;
|
||||
render: RendererProps['render'];
|
||||
onConfirm: (scaffold: Record<string, any>) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/** 表单数据 */
|
||||
interface FormData extends InitData {
|
||||
name: string;
|
||||
title: string;
|
||||
feats: Extract<DSFeatureType, 'View' | 'Edit' | 'Delete'>[];
|
||||
viewApi?: string | BaseApiObject;
|
||||
editApi?: string | BaseApiObject;
|
||||
deleteApi?: string | BaseApiObject;
|
||||
__fieldItem: Record<string, any>[];
|
||||
}
|
||||
|
||||
const AddColumnModal: React.FC<AddColumnModalProps> = props => {
|
||||
const {
|
||||
classnames: cx,
|
||||
render,
|
||||
visible,
|
||||
initData,
|
||||
ctx,
|
||||
manager,
|
||||
builder,
|
||||
onConfirm,
|
||||
onClose
|
||||
} = props;
|
||||
const componentId = ctx?.id;
|
||||
const modalRef = useRef<any>(null);
|
||||
const formRef = useRef<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleModalConfirm = useCallback(async () => {
|
||||
const form = formRef?.current?.getWrappedInstance?.();
|
||||
let schema;
|
||||
let errorStack: any;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
if (form) {
|
||||
try {
|
||||
schema = await form.submit?.(async (values: FormData) => {
|
||||
let scaffold;
|
||||
|
||||
if (values.colType === 'field') {
|
||||
const column = await builder.buildCRUDColumn?.(
|
||||
values.__fieldItem
|
||||
? {
|
||||
...values.__fieldItem,
|
||||
checked: true
|
||||
}
|
||||
: {
|
||||
...values,
|
||||
label: values.title,
|
||||
name: values.name,
|
||||
displayType: 'tpl'
|
||||
},
|
||||
{
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: ctx
|
||||
},
|
||||
componentId
|
||||
);
|
||||
scaffold =
|
||||
column !== false
|
||||
? column
|
||||
: {
|
||||
label: values.title,
|
||||
name: values.name
|
||||
};
|
||||
} else if (values.colType === 'operation') {
|
||||
const fields = (ctx?.columns ?? []).map((item: ColumnSchema) => ({
|
||||
displayType: item.type ?? 'input-text',
|
||||
inputType: item.type ?? 'input-text',
|
||||
name: item.name,
|
||||
label: item.title
|
||||
}));
|
||||
scaffold = await builder.buildCRUDOpColumn?.(
|
||||
{
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
feats: values.feats,
|
||||
schema: ctx,
|
||||
scaffoldConfig: {
|
||||
viewFields: fields,
|
||||
editFields: fields,
|
||||
viewApi: values?.viewApi,
|
||||
editApi: values?.editApi,
|
||||
deleteApi: values?.deleteApi
|
||||
},
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
},
|
||||
componentId
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(JSONPipeIn(omit(scaffold, ['key'])));
|
||||
});
|
||||
} catch (error) {
|
||||
errorStack = error.stack;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!errorStack) {
|
||||
onConfirm(schema);
|
||||
onClose?.();
|
||||
} else {
|
||||
/** 表单校验没通过就不自动关闭Dialog */
|
||||
console.error(errorStack);
|
||||
}
|
||||
}, [onConfirm]);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Modal
|
||||
ref={modalRef}
|
||||
size="sm"
|
||||
show={visible}
|
||||
onHide={onClose}
|
||||
closeOnEsc={false}
|
||||
contentClassName="ae-Scaffold-Modal"
|
||||
>
|
||||
<Modal.Header showCloseButton onClose={onClose}>
|
||||
<Modal.Title>添加列</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{render(
|
||||
'column-control-modal',
|
||||
{
|
||||
type: 'form',
|
||||
title: '',
|
||||
mode: 'horizontal',
|
||||
horizontal: {
|
||||
justify: true,
|
||||
leftFixed: 'sm'
|
||||
},
|
||||
submitOnChange: true,
|
||||
wrapWithPanel: false,
|
||||
clearValueOnHidden: true,
|
||||
preventEnterSubmit: true,
|
||||
actions: [],
|
||||
body: [
|
||||
{
|
||||
type: 'input-tag',
|
||||
name: 'colType',
|
||||
label: '列类型',
|
||||
static: true,
|
||||
className: 'mb-2',
|
||||
options: [
|
||||
{label: '字段列', value: 'field'},
|
||||
{label: '操作列', value: 'operation'}
|
||||
]
|
||||
},
|
||||
...(initData?.colType === 'field'
|
||||
? [
|
||||
getSchemaTpl('formItemName', {
|
||||
name: 'name',
|
||||
label: '列字段',
|
||||
required: true,
|
||||
onBindingChange: async (
|
||||
field: Record<string, any>,
|
||||
onBulkChange: (value: any, submit?: boolean) => void
|
||||
) => {
|
||||
onBulkChange?.(
|
||||
{
|
||||
name: field.value,
|
||||
title: field.label,
|
||||
__fieldItem: field
|
||||
},
|
||||
true
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'title',
|
||||
label: '列标题',
|
||||
type: 'input-text',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(initData?.colType === 'operation'
|
||||
? [
|
||||
{
|
||||
type: 'checkboxes',
|
||||
label: '数据操作',
|
||||
name: 'feats',
|
||||
joinValues: false,
|
||||
extractValue: true,
|
||||
multiple: true,
|
||||
inline: false,
|
||||
options: [
|
||||
{label: '查看详情', value: 'View'},
|
||||
{label: '编辑记录', value: 'Edit'},
|
||||
{label: '删除记录', value: 'Delete'}
|
||||
],
|
||||
value: [
|
||||
DSFeatureEnum.View,
|
||||
DSFeatureEnum.Edit,
|
||||
DSFeatureEnum.Delete
|
||||
]
|
||||
},
|
||||
...(builder.key !== ModelDSBuilderKey
|
||||
? [
|
||||
...builder.makeSourceSettingForm({
|
||||
feat: 'View',
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
sourceSettings: {
|
||||
name: 'viewApi',
|
||||
visibleOn:
|
||||
"data.feats && data.feats.indexOf('View') > -1"
|
||||
}
|
||||
}),
|
||||
...builder.makeSourceSettingForm({
|
||||
feat: 'Edit',
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
sourceSettings: {
|
||||
name: 'editApi',
|
||||
visibleOn:
|
||||
"data.feats && data.feats.indexOf('Edit') > -1"
|
||||
}
|
||||
}),
|
||||
...builder.makeSourceSettingForm({
|
||||
feat: 'Delete',
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
sourceSettings: {
|
||||
name: 'deleteApi',
|
||||
visibleOn:
|
||||
"data.feats && data.feats.indexOf('Delete') > -1"
|
||||
}
|
||||
})
|
||||
].filter(Boolean)
|
||||
: [])
|
||||
].filter(i => !!i)
|
||||
: [])
|
||||
]
|
||||
},
|
||||
{
|
||||
ref: formRef,
|
||||
popOverContainer: modalRef.current,
|
||||
disabled: loading,
|
||||
data: utils.createObject(ctx, {...initData})
|
||||
}
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
loading={loading}
|
||||
loadingClassName={cx('ae-CRUDConfigControl-modal-btn-loading')}
|
||||
level="primary"
|
||||
onClick={handleModalConfirm}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default themeable(AddColumnModal);
|
@ -0,0 +1,489 @@
|
||||
/**
|
||||
* @file CRUDColumnControl
|
||||
* @desc 列配置控件
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import Sortable from 'sortablejs';
|
||||
import {FormItem, Button, Icon, toast, Tag, Spinner, autobind} from 'amis';
|
||||
import {TooltipWrapper} from 'amis-ui';
|
||||
import {JSONPipeIn} from 'amis-editor-core';
|
||||
import AddColumnModal from './AddColumnModal';
|
||||
|
||||
import type {FormControlProps} from 'amis';
|
||||
import type {SortableEvent} from 'sortablejs';
|
||||
import type {ColumnSchema} from 'amis/lib/renderers/Table2';
|
||||
import type {DSBuilderInterface} from '../../builder';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
nodeId: string;
|
||||
hidden: boolean;
|
||||
/** 原始结构 */
|
||||
pristine: DesignColumnSchema;
|
||||
/** 字段信息 */
|
||||
context?: any;
|
||||
}
|
||||
|
||||
interface DesignColumnSchema extends ColumnSchema {
|
||||
/** 设计态节点 ID */
|
||||
$$id: string;
|
||||
/** schema ID */
|
||||
id: string;
|
||||
/** 绑定的实体字段的 ID */
|
||||
fieldId?: string;
|
||||
relationBuildSetting?: any;
|
||||
}
|
||||
|
||||
export interface CRUDColumnControlProps extends FormControlProps {
|
||||
/** CRUD 节点的 ID */
|
||||
nodeId: string;
|
||||
builder: DSBuilderInterface;
|
||||
}
|
||||
|
||||
export interface CRUDColumnControlState {
|
||||
options: Option[];
|
||||
loading: boolean;
|
||||
showAddModal: boolean;
|
||||
addModalData?: {
|
||||
colTypeLabel: string;
|
||||
colType: 'field' | 'operation';
|
||||
};
|
||||
}
|
||||
|
||||
export class CRUDColumnControl extends React.Component<
|
||||
CRUDColumnControlProps,
|
||||
CRUDColumnControlState
|
||||
> {
|
||||
sortable?: Sortable;
|
||||
|
||||
drag?: HTMLElement | null;
|
||||
|
||||
dom?: HTMLElement;
|
||||
|
||||
constructor(props: CRUDColumnControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: [],
|
||||
loading: false,
|
||||
showAddModal: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.dom = findDOMNode(this) as HTMLElement;
|
||||
this.initOptions();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<CRUDColumnControlProps>): void {
|
||||
if (prevProps.value !== this.props.value) {
|
||||
this.initOptions();
|
||||
}
|
||||
}
|
||||
|
||||
transformOption(option: DesignColumnSchema): Option | false {
|
||||
if (option.name || option.type === 'operation') {
|
||||
return {
|
||||
label:
|
||||
typeof option.title === 'string'
|
||||
? option.title
|
||||
: option.type === 'tpl' && typeof (option as any).tpl === 'string'
|
||||
? (option as any).tpl /** 处理 SchemaObject 的场景 */
|
||||
: option.name,
|
||||
value: option.name ?? (option as any).key,
|
||||
/** 使用$$id用于定位 */
|
||||
nodeId: option.$$id,
|
||||
hidden: option.type === 'operation',
|
||||
pristine: option
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async initOptions() {
|
||||
const {manager, nodeId, builder, data: ctx} = this.props;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(nodeId);
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
if (builder && builder.getCRUDListFields) {
|
||||
try {
|
||||
const options = await builder.getCRUDListFields<Option>({
|
||||
renderer: 'crud',
|
||||
schema: node.schema,
|
||||
inScaffold: false,
|
||||
controlSettings: {
|
||||
fieldMapper: this.transformOption.bind(this)
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({options});
|
||||
} catch (error) {}
|
||||
} else {
|
||||
/** 从 Node 取是为了获取设计态节点 ID */
|
||||
const columns = node?.schema?.columns as DesignColumnSchema[];
|
||||
const result: Option[] = [];
|
||||
columns.forEach(col => {
|
||||
const option = this.transformOption(col);
|
||||
|
||||
if (option !== false) {
|
||||
result.push(option);
|
||||
}
|
||||
});
|
||||
this.setState({options: result});
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
@autobind
|
||||
dragRef(ref: any) {
|
||||
if (!this.drag && ref) {
|
||||
this.initDragging();
|
||||
} else if (this.drag && !ref) {
|
||||
this.destroyDragging();
|
||||
}
|
||||
|
||||
this.drag = ref;
|
||||
}
|
||||
|
||||
initDragging() {
|
||||
const {classnames: cx} = this.props;
|
||||
const dom = findDOMNode(this) as HTMLElement;
|
||||
|
||||
this.sortable = new Sortable(
|
||||
dom.querySelector(
|
||||
`.${cx('ae-CRUDConfigControl-list')}`
|
||||
) as HTMLUListElement,
|
||||
{
|
||||
group: 'CRUDColumnControlGroup',
|
||||
animation: 150,
|
||||
handle: `.${cx('ae-CRUDConfigControl-list-item')}`,
|
||||
ghostClass: `.${cx('ae-CRUDConfigControl-list-item--dragging')}`,
|
||||
onEnd: (e: SortableEvent) => {
|
||||
if (
|
||||
e.newIndex === e.oldIndex ||
|
||||
e.newIndex == null ||
|
||||
e.oldIndex == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = e.to as HTMLElement;
|
||||
|
||||
if (e.oldIndex < parent.childNodes.length - 1) {
|
||||
parent.insertBefore(e.item, parent.childNodes[e.oldIndex]);
|
||||
} else {
|
||||
parent.appendChild(e.item);
|
||||
}
|
||||
|
||||
const options = this.state.options.concat();
|
||||
|
||||
options[e.oldIndex] = options.splice(
|
||||
e.newIndex,
|
||||
1,
|
||||
options[e.oldIndex]
|
||||
)[0];
|
||||
|
||||
this.setState({options}, () => this.handleSort());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
destroyDragging() {
|
||||
this.sortable && this.sortable.destroy();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSort() {
|
||||
const {onBulkChange} = this.props;
|
||||
const options = this.state.options.concat();
|
||||
|
||||
onBulkChange?.({columns: options.map(item => item.pristine)});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleEdit(item: Option) {
|
||||
const {manager, node} = this.props;
|
||||
const columns = node?.schema?.columns ?? [];
|
||||
const idx = columns.findIndex((c: any) => c.id === item.pristine.id);
|
||||
|
||||
if (!~idx) {
|
||||
toast.warning(`未找到对应列「${item.label}」`);
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: 理论上用item.nodeId就可以,不知道为何会重新构建一次导致store中node.id更新
|
||||
manager.setActiveId(columns[idx]?.$$id);
|
||||
}
|
||||
|
||||
/** 添加列 */
|
||||
@autobind
|
||||
handleAddColumn(type: 'field' | 'empty' | 'container' | 'operation') {
|
||||
const {onBulkChange} = this.props;
|
||||
const options = this.state.options.concat();
|
||||
let scaffold: any;
|
||||
|
||||
switch (type) {
|
||||
case 'field':
|
||||
this.setState({
|
||||
showAddModal: true,
|
||||
addModalData: {colTypeLabel: '字段列', colType: type}
|
||||
});
|
||||
break;
|
||||
case 'empty':
|
||||
scaffold = {
|
||||
title: '空列',
|
||||
name: 'empty'
|
||||
};
|
||||
break;
|
||||
case 'container':
|
||||
scaffold = {
|
||||
title: '容器',
|
||||
name: 'container',
|
||||
type: 'container',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block'
|
||||
},
|
||||
wrapperBody: false,
|
||||
body: []
|
||||
};
|
||||
break;
|
||||
case 'operation':
|
||||
this.setState({
|
||||
showAddModal: true,
|
||||
addModalData: {colTypeLabel: '操作列', colType: type}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (!scaffold) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnSchema = JSONPipeIn({...scaffold});
|
||||
|
||||
options.push({
|
||||
label: columnSchema.title,
|
||||
value: columnSchema.name,
|
||||
nodeId: columnSchema.$$id,
|
||||
hidden: type === 'operation',
|
||||
pristine: columnSchema
|
||||
});
|
||||
|
||||
this.setState({options}, () => {
|
||||
onBulkChange?.({columns: options.map(item => item.pristine)});
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleAddModalConfirm(scaffold: DesignColumnSchema) {
|
||||
const {onBulkChange} = this.props;
|
||||
const options = this.state.options.concat();
|
||||
|
||||
options.push({
|
||||
label:
|
||||
typeof scaffold.title === 'string' ? scaffold.title : scaffold.name,
|
||||
value: scaffold.name,
|
||||
nodeId: scaffold.$$id,
|
||||
hidden: scaffold.type === 'operation',
|
||||
pristine: scaffold
|
||||
});
|
||||
|
||||
this.setState({options}, () => {
|
||||
onBulkChange?.({columns: options.map(item => item.pristine)});
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleAddModalClose() {
|
||||
this.setState({
|
||||
showAddModal: false,
|
||||
addModalData: undefined
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleDelete(item: Option, index: number) {
|
||||
const {onBulkChange, env} = this.props;
|
||||
const options = this.state.options;
|
||||
const confirmed = await env.confirm(`确定要删除列「${item.label}」吗?`);
|
||||
|
||||
if (~index && confirmed) {
|
||||
options.splice(index, 1);
|
||||
|
||||
this.setState({options}, () => {
|
||||
onBulkChange?.({columns: options.map(item => item.pristine)});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderOption(item: Option, index: number) {
|
||||
const {
|
||||
classnames: cx,
|
||||
data: ctx,
|
||||
render,
|
||||
popOverContainer,
|
||||
env
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className={cx('ae-CRUDConfigControl-list-item', 'is-draggable')}
|
||||
>
|
||||
<TooltipWrapper
|
||||
tooltip={{
|
||||
content: item.label,
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
container={popOverContainer || env?.getModalContainer?.()}
|
||||
trigger={['hover']}
|
||||
delay={150}
|
||||
>
|
||||
<div className={cx('ae-CRUDConfigControl-list-item-info')}>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
|
||||
<div className={cx('ae-CRUDConfigControl-list-item-actions')}>
|
||||
{item.hidden || !item?.context?.isCascadingField ? null : (
|
||||
<Tag
|
||||
label={item?.context?.modelLabel}
|
||||
displayMode="normal"
|
||||
className={cx(
|
||||
'ae-CRUDConfigControl-list-item-tag',
|
||||
'ae-CRUDConfigControl-list-item-tag--cascading'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
level="link"
|
||||
size="sm"
|
||||
tooltip={{
|
||||
content: '去编辑',
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
onClick={() => this.handleEdit(item)}
|
||||
>
|
||||
<Icon icon="column-setting" className="icon" />
|
||||
</Button>
|
||||
<Button
|
||||
level="link"
|
||||
size="sm"
|
||||
onClick={() => this.handleDelete(item, index)}
|
||||
>
|
||||
<Icon icon="column-delete" className="icon" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const {classnames: cx, data: ctx, render, env} = this.props;
|
||||
|
||||
return (
|
||||
<header className={cx('ae-CRUDConfigControl-header')}>
|
||||
<span className={cx('Form-label')}>列配置</span>
|
||||
{render('column-control-dropdown', {
|
||||
type: 'dropdown-button',
|
||||
closeOnClick: true,
|
||||
hideCaret: true,
|
||||
level: 'link',
|
||||
align: 'right',
|
||||
trigger: ['click'],
|
||||
popOverContainer: env.getModalContainer ?? this.dom ?? document.body,
|
||||
icon: 'column-add',
|
||||
label: '添加列',
|
||||
className: cx('ae-CRUDConfigControl-dropdown'),
|
||||
buttons: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '字段列',
|
||||
onClick: () => this.handleAddColumn('field')
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
label: '空列',
|
||||
onClick: () => this.handleAddColumn('empty')
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
label: '容器列',
|
||||
onClick: () => this.handleAddColumn('container')
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
label: '操作列',
|
||||
onClick: () => this.handleAddColumn('operation')
|
||||
}
|
||||
]
|
||||
})}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classnames: cx, data: ctx, manager, builder} = this.props;
|
||||
const {options, loading, showAddModal, addModalData} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('ae-CRUDConfigControl')}>
|
||||
{loading ? (
|
||||
<Spinner
|
||||
show
|
||||
tip="字段加载中"
|
||||
tipPlacement="bottom"
|
||||
size="sm"
|
||||
className={cx('flex')}
|
||||
/>
|
||||
) : Array.isArray(options) && options.length > 0 ? (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<ul className={cx('ae-CRUDConfigControl-list')} ref={this.dragRef}>
|
||||
{options.map((item, index) => {
|
||||
return this.renderOption(item, index);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<ul className={cx('ae-CRUDConfigControl-list')} ref={this.dragRef}>
|
||||
<p className={cx(`ae-CRUDConfigControl-placeholder`)}>暂无数据</p>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{showAddModal ? (
|
||||
<AddColumnModal
|
||||
render={this.props.render}
|
||||
visible={showAddModal}
|
||||
initData={addModalData as any}
|
||||
ctx={ctx}
|
||||
manager={manager}
|
||||
builder={builder}
|
||||
onConfirm={this.handleAddModalConfirm}
|
||||
onClose={this.handleAddModalClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({
|
||||
type: 'ae-crud-column-control',
|
||||
renderLabel: false,
|
||||
wrap: false
|
||||
})
|
||||
export class CRUDColumnControlRenderer extends CRUDColumnControl {}
|
@ -0,0 +1,807 @@
|
||||
/**
|
||||
* @file CRUDFiltersControl
|
||||
* @desc 搜索控件
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import uniq from 'lodash/uniq';
|
||||
import {
|
||||
FormItem,
|
||||
Button,
|
||||
Icon,
|
||||
toast,
|
||||
Switch,
|
||||
Spinner,
|
||||
Tag,
|
||||
autobind
|
||||
} from 'amis';
|
||||
import {TooltipWrapper} from 'amis-ui';
|
||||
import {DSFeatureEnum} from '../../builder/constants';
|
||||
import {traverseSchemaDeep} from '../../builder/utils';
|
||||
import {deepRemove} from '../../plugin/CRUD2/utils';
|
||||
|
||||
import type {
|
||||
DSFeatureType,
|
||||
DSBuilderInterface,
|
||||
CRUDScaffoldConfig
|
||||
} from '../../builder';
|
||||
import type {EditorNodeType} from 'amis-editor-core';
|
||||
import type {FormControlProps} from 'amis';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
nodeId: string;
|
||||
node?: EditorNodeType;
|
||||
/** 原始结构 */
|
||||
pristine: Record<string, any>;
|
||||
/** 字段信息 */
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface CRUDFiltersControlProps extends FormControlProps {
|
||||
/** CRUD配置面板的数据 */
|
||||
data: Record<string, any>;
|
||||
/** CRUD 节点的 ID */
|
||||
nodeId: string;
|
||||
// TODO:暂时支持简单查询先不扩展了
|
||||
feat: Extract<DSFeatureType, 'SimpleQuery' | 'AdvancedQuery' | 'FuzzyQuery'>;
|
||||
/** 数据源构造器 */
|
||||
builder: DSBuilderInterface;
|
||||
}
|
||||
|
||||
interface CRUDFiltersControlState {
|
||||
options: Option[];
|
||||
loading: boolean;
|
||||
checked: boolean;
|
||||
/** 目标组件的 Node.id */
|
||||
targetNodeId?: string;
|
||||
}
|
||||
|
||||
export class CRUDFiltersControl extends React.Component<
|
||||
CRUDFiltersControlProps,
|
||||
CRUDFiltersControlState
|
||||
> {
|
||||
dom?: HTMLElement;
|
||||
|
||||
constructor(props: CRUDFiltersControlProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
options: [],
|
||||
loading: false,
|
||||
checked: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.dom = findDOMNode(this) as HTMLElement;
|
||||
this.initOptions();
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<CRUDFiltersControlProps>,
|
||||
prevState: Readonly<CRUDFiltersControlState>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
if (
|
||||
prevProps.data.headerToolbar !== this.props.data.headerToolbar ||
|
||||
prevProps.data.filter !== this.props.data.filter
|
||||
) {
|
||||
this.initOptions();
|
||||
}
|
||||
}
|
||||
|
||||
transformOption(option: any): Option | false {
|
||||
if (option.name) {
|
||||
return {
|
||||
label:
|
||||
typeof option.label === 'string'
|
||||
? option.label
|
||||
: option.label?.type === 'tpl' &&
|
||||
typeof (option.label as any).tpl === 'string'
|
||||
? (option.label as any).tpl /** 处理 SchemaObject 的场景 */
|
||||
: option.name,
|
||||
value: option.name ?? (option as any).key,
|
||||
/** 使用id用于定位 */
|
||||
nodeId: option.$$id,
|
||||
pristine: option
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@autobind
|
||||
async initOptions() {
|
||||
const {manager, nodeId, feat, builder} = this.props;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(nodeId);
|
||||
const CRUDSchema = node.schema;
|
||||
const filterSchema = CRUDSchema?.filter
|
||||
? Array.isArray(CRUDSchema.filter)
|
||||
? CRUDSchema.filter.find(
|
||||
(item: any) => item.behavior && Array.isArray(item.behavior)
|
||||
)
|
||||
: CRUDSchema.filter?.type === 'form'
|
||||
? CRUDSchema.filter
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
let targetNodeId: string = filterSchema ? filterSchema.$$id : '';
|
||||
|
||||
if (!builder) {
|
||||
const options: Option[] = [];
|
||||
(filterSchema?.body ?? []).forEach((formItem: any) => {
|
||||
if (
|
||||
formItem.type === 'condition-builder' ||
|
||||
formItem.behavior === 'AdvancedQuery'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = this.transformOption(formItem);
|
||||
|
||||
if (option !== false) {
|
||||
options.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
options,
|
||||
checked: options.length > 0,
|
||||
targetNodeId: targetNodeId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
const baseOpitons = {
|
||||
feat,
|
||||
renderer: 'crud',
|
||||
schema: node.schema,
|
||||
inScaffold: false,
|
||||
controlSettings: {
|
||||
fieldMapper: this.transformOption.bind(this)
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (feat === DSFeatureEnum.SimpleQuery && builder.filterByFeat(feat)) {
|
||||
const fields =
|
||||
(await builder.getCRUDSimpleQueryFields?.<Option>({
|
||||
...baseOpitons
|
||||
})) ?? [];
|
||||
|
||||
this.setState({
|
||||
options: fields,
|
||||
checked: fields.length > 0,
|
||||
targetNodeId
|
||||
});
|
||||
} else if (
|
||||
feat === DSFeatureEnum.AdvancedQuery &&
|
||||
builder.filterByFeat(feat)
|
||||
) {
|
||||
const fields =
|
||||
(await builder?.getCRUDAdvancedQueryFields?.<Option>({
|
||||
...baseOpitons
|
||||
})) ?? [];
|
||||
const CBSchema = (filterSchema?.body ?? []).find(
|
||||
(item: any) =>
|
||||
item.type === 'condition-builder' &&
|
||||
(item.behavior === DSFeatureEnum.AdvancedQuery ||
|
||||
item.name === '__filter')
|
||||
);
|
||||
|
||||
targetNodeId = CBSchema ? CBSchema.$$id : '';
|
||||
this.setState({
|
||||
options: fields,
|
||||
checked: fields.length > 0,
|
||||
targetNodeId
|
||||
});
|
||||
} else if (
|
||||
feat === DSFeatureEnum.FuzzyQuery &&
|
||||
builder.filterByFeat(feat)
|
||||
) {
|
||||
const fields =
|
||||
(await builder?.getCRUDFuzzyQueryFields?.<Option>({
|
||||
...baseOpitons
|
||||
})) ?? [];
|
||||
|
||||
let fuzzyQuerySchema: any;
|
||||
|
||||
traverseSchemaDeep(CRUDSchema, (key: string, value: any, host: any) => {
|
||||
if (
|
||||
key === 'behavior' &&
|
||||
value === DSFeatureEnum.FuzzyQuery &&
|
||||
host.type === 'search-box'
|
||||
) {
|
||||
fuzzyQuerySchema = host;
|
||||
}
|
||||
return [key, value];
|
||||
});
|
||||
|
||||
targetNodeId = fuzzyQuerySchema ? fuzzyQuerySchema.$$id : '';
|
||||
this.setState({
|
||||
options: fields,
|
||||
checked: fields.length > 0,
|
||||
targetNodeId
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
async updateSimpleQuery(enable: boolean) {
|
||||
const {manager, nodeId, builder} = this.props;
|
||||
const store = manager.store;
|
||||
const CRUDNode = store.getNodeById(nodeId);
|
||||
const CRUDSchema = CRUDNode?.schema;
|
||||
const CRUDSchemaID = CRUDSchema?.schema?.id;
|
||||
const config = await builder.guessCRUDScaffoldConfig({schema: CRUDSchema});
|
||||
const filterSchema = cloneDeep(
|
||||
CRUDSchema?.filter
|
||||
? Array.isArray(CRUDSchema.filter)
|
||||
? CRUDSchema.filter.find(
|
||||
(item: any) =>
|
||||
item.behavior &&
|
||||
Array.isArray(item.behavior) &&
|
||||
item.type === 'form'
|
||||
)
|
||||
: CRUDSchema.filter?.type === 'form'
|
||||
? CRUDSchema.filter
|
||||
: undefined
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (filterSchema) {
|
||||
if (enable) {
|
||||
const simpleQuerySchema =
|
||||
(await builder.buildSimpleQueryCollectionSchema?.({
|
||||
renderer: 'crud',
|
||||
schema: CRUDSchema,
|
||||
inScaffold: false,
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
})) ?? [];
|
||||
|
||||
const newFilterSchema = traverseSchemaDeep(
|
||||
filterSchema,
|
||||
(key: string, value: any, host: any) => {
|
||||
/** 更新标识符 */
|
||||
if (key === 'behavior' && Array.isArray(value)) {
|
||||
return [
|
||||
key,
|
||||
uniq([...value, DSFeatureEnum.SimpleQuery].filter(Boolean))
|
||||
];
|
||||
}
|
||||
|
||||
/** 更新内容区 */
|
||||
if (
|
||||
key === 'body' &&
|
||||
Array.isArray(value) &&
|
||||
host?.type === 'form'
|
||||
) {
|
||||
return [key, [...value, ...simpleQuerySchema].filter(Boolean)];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}
|
||||
);
|
||||
|
||||
const targetNode = manager.store.getNodeById(filterSchema.$$id);
|
||||
|
||||
if (targetNode) {
|
||||
targetNode.updateSchema(newFilterSchema);
|
||||
}
|
||||
} else {
|
||||
const newFilterSchema = traverseSchemaDeep(
|
||||
filterSchema,
|
||||
(key: string, value: any, host: any) => {
|
||||
/** 更新标识符 */
|
||||
if (key === 'behavior' && Array.isArray(value)) {
|
||||
return [
|
||||
key,
|
||||
value.filter(
|
||||
(i: DSFeatureType) => i !== DSFeatureEnum.SimpleQuery
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/** 更新内容区 */
|
||||
if (
|
||||
key === 'body' &&
|
||||
Array.isArray(value) &&
|
||||
host?.type === 'form'
|
||||
) {
|
||||
return [
|
||||
key,
|
||||
value.filter(
|
||||
(item: any) => item?.behavior !== DSFeatureEnum.SimpleQuery
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}
|
||||
);
|
||||
|
||||
const targetNode = manager.store.getNodeById(filterSchema.$$id);
|
||||
|
||||
if (targetNode) {
|
||||
targetNode.updateSchema(newFilterSchema);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (enable) {
|
||||
/** 没有查询表头新建一个 */
|
||||
const simpleQuerySchema =
|
||||
(await builder.buildSimpleQueryCollectionSchema?.({
|
||||
renderer: 'crud',
|
||||
schema: CRUDSchema,
|
||||
inScaffold: false,
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
})) ?? [];
|
||||
const filter = await builder.buildCRUDFilterSchema({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
feats: [DSFeatureEnum.SimpleQuery],
|
||||
scaffoldConfig: {
|
||||
dsType: CRUDSchema.dsType,
|
||||
simpleQueryFields: simpleQuerySchema
|
||||
},
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
});
|
||||
|
||||
const newFilterSchema = cloneDeep(CRUDNode?.schema.filter);
|
||||
const isArrayFilter = Array.isArray(newFilterSchema);
|
||||
|
||||
if (isArrayFilter) {
|
||||
newFilterSchema.push(filter);
|
||||
}
|
||||
|
||||
CRUDNode.updateSchema({
|
||||
...CRUDSchema,
|
||||
filter: isArrayFilter ? newFilterSchema : filter
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async updateAdvancedQuery(enable: boolean) {
|
||||
const {manager, nodeId, builder} = this.props;
|
||||
const store = manager.store;
|
||||
const CRUDNode = store.getNodeById(nodeId);
|
||||
const CRUDSchema = CRUDNode?.schema;
|
||||
const filterSchema = cloneDeep(
|
||||
Array.isArray(CRUDSchema.filter)
|
||||
? CRUDNode?.schema.filter.find(
|
||||
(item: any) => item.behavior && Array.isArray(item.behavior)
|
||||
)
|
||||
: CRUDNode?.schema.filter
|
||||
);
|
||||
|
||||
if (filterSchema) {
|
||||
if (enable) {
|
||||
const advancedQuerySchema = await builder.buildAdvancedQuerySchema?.({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
});
|
||||
|
||||
const newFilterSchema = traverseSchemaDeep(
|
||||
filterSchema,
|
||||
(key: string, value: any, host: any) => {
|
||||
/** 更新标识符 */
|
||||
if (key === 'behavior' && Array.isArray(value)) {
|
||||
return [
|
||||
key,
|
||||
uniq([...value, DSFeatureEnum.AdvancedQuery].filter(Boolean))
|
||||
];
|
||||
}
|
||||
|
||||
/** 更新内容区 */
|
||||
if (
|
||||
key === 'body' &&
|
||||
Array.isArray(value) &&
|
||||
host?.type === 'form'
|
||||
) {
|
||||
return [key, [advancedQuerySchema, ...value]];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}
|
||||
);
|
||||
|
||||
const targetNode = manager.store.getNodeById(filterSchema.$$id);
|
||||
|
||||
if (targetNode) {
|
||||
targetNode.updateSchema(newFilterSchema);
|
||||
}
|
||||
} else {
|
||||
const newFilterSchema = traverseSchemaDeep(
|
||||
filterSchema,
|
||||
(key: string, value: any, host: any) => {
|
||||
/** 更新标识符 */
|
||||
if (key === 'behavior' && Array.isArray(value)) {
|
||||
return [
|
||||
key,
|
||||
value.filter(
|
||||
(i: DSFeatureType) => i !== DSFeatureEnum.AdvancedQuery
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/** 更新内容区 */
|
||||
if (
|
||||
key === 'body' &&
|
||||
Array.isArray(value) &&
|
||||
host?.type === 'form'
|
||||
) {
|
||||
return [
|
||||
key,
|
||||
value.filter(
|
||||
(item: any) =>
|
||||
item?.behavior !== DSFeatureEnum.AdvancedQuery &&
|
||||
item.type !== 'condition-builder'
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}
|
||||
);
|
||||
|
||||
const targetNode = manager.store.getNodeById(filterSchema.$$id);
|
||||
|
||||
if (targetNode) {
|
||||
targetNode.updateSchema(newFilterSchema);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (enable) {
|
||||
/** 没有查询表头新建一个 */
|
||||
const filter = await builder.buildCRUDFilterSchema({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
feats: [DSFeatureEnum.AdvancedQuery],
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
});
|
||||
|
||||
const newFilterSchema = cloneDeep(CRUDNode?.schema.filter);
|
||||
const isArrayFilter = Array.isArray(newFilterSchema);
|
||||
|
||||
if (isArrayFilter) {
|
||||
newFilterSchema.push(filter);
|
||||
}
|
||||
|
||||
CRUDNode.updateSchema({
|
||||
...CRUDSchema,
|
||||
filter: isArrayFilter ? newFilterSchema : filter
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async updateFuzzyQuery(enable: boolean) {
|
||||
const {manager, nodeId, builder} = this.props;
|
||||
const store = manager.store;
|
||||
const CRUDNode = store.getNodeById(nodeId);
|
||||
const CRUDSchema = CRUDNode?.schema;
|
||||
const CRUDSchemaID = CRUDSchema?.schema?.id;
|
||||
const headerToolbar = cloneDeep(CRUDSchema.headerToolbar);
|
||||
|
||||
/** 关闭功能且存在定位容器 */
|
||||
if (!enable) {
|
||||
if (headerToolbar) {
|
||||
deepRemove(
|
||||
headerToolbar,
|
||||
(schema: any) => {
|
||||
return (
|
||||
schema.behavior === DSFeatureEnum.FuzzyQuery &&
|
||||
schema.type === 'search-box'
|
||||
);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
CRUDNode.updateSchema({
|
||||
...CRUDSchema,
|
||||
headerToolbar
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** 没有工具栏的话直接重新创建 */
|
||||
if (!headerToolbar) {
|
||||
CRUDNode.updateSchema({
|
||||
...CRUDSchema,
|
||||
headerToolbar: await builder.buildCRUDHeaderToolbar?.(
|
||||
{
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
},
|
||||
CRUDSchemaID
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/** 定位容器 */
|
||||
let fuzzyQueryParent: any;
|
||||
traverseSchemaDeep(CRUDSchema, (key: string, value: any, host: any) => {
|
||||
if (
|
||||
key === 'behavior' &&
|
||||
Array.isArray(value) &&
|
||||
host?.behavior.includes('FuzzyQuery') &&
|
||||
host?.type === 'container' &&
|
||||
Array.isArray(host?.body)
|
||||
) {
|
||||
fuzzyQueryParent = host;
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
});
|
||||
|
||||
if (fuzzyQueryParent) {
|
||||
const fuzzyQuerySchema = await builder.buildFuzzyQuerySchema?.({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
});
|
||||
const newFuzzyParent = cloneDeep(fuzzyQueryParent);
|
||||
newFuzzyParent.body = [...newFuzzyParent.body, fuzzyQuerySchema];
|
||||
const targetNode = manager.store.getNodeById(fuzzyQueryParent.$$id);
|
||||
|
||||
if (targetNode) {
|
||||
targetNode.updateSchema(newFuzzyParent);
|
||||
}
|
||||
} else {
|
||||
const fuzzyQuerySchema = await builder.buildFuzzyQuerySchema?.({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
buildSettings: {
|
||||
useDefaultFields: true
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
headerToolbar?.[0]?.type === 'flex' &&
|
||||
Array.isArray(headerToolbar[0]?.items)
|
||||
) {
|
||||
const newFlexContainer = cloneDeep(headerToolbar[0]);
|
||||
|
||||
/** toolbar 里有 flex 容器,直接追加 */
|
||||
const container = await builder.buildFuzzyQuerySchema?.({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
buildSettings: {
|
||||
useDefaultFields: true,
|
||||
wrapContainer: 'container'
|
||||
}
|
||||
});
|
||||
newFlexContainer.items.push(container);
|
||||
|
||||
const targetNode = manager.store.getNodeById(headerToolbar[0].$$id);
|
||||
|
||||
if (targetNode) {
|
||||
targetNode.updateSchema(newFlexContainer);
|
||||
}
|
||||
} else {
|
||||
/** toolbar 里没有 flex 容器,重新创建一个 */
|
||||
const newHeaderToolbar = cloneDeep(headerToolbar);
|
||||
|
||||
const flexContainer = await builder.buildFuzzyQuerySchema?.({
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: CRUDSchema,
|
||||
buildSettings: {
|
||||
useDefaultFields: true,
|
||||
wrapContainer: 'flex'
|
||||
}
|
||||
});
|
||||
newHeaderToolbar.push(flexContainer);
|
||||
|
||||
CRUDNode.updateSchema({
|
||||
...CRUDSchema,
|
||||
headerToolbar: newHeaderToolbar
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleToggle(checked: boolean) {
|
||||
const {feat, builder} = this.props;
|
||||
this.setState({loading: true, checked});
|
||||
|
||||
try {
|
||||
if (feat === DSFeatureEnum.SimpleQuery && builder.filterByFeat(feat)) {
|
||||
await this.updateSimpleQuery(checked);
|
||||
} else if (
|
||||
feat === DSFeatureEnum.AdvancedQuery &&
|
||||
builder.filterByFeat(feat)
|
||||
) {
|
||||
await this.updateAdvancedQuery(checked);
|
||||
}
|
||||
if (feat === DSFeatureEnum.FuzzyQuery && builder.filterByFeat(feat)) {
|
||||
await this.updateFuzzyQuery(checked);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
this.setState({loading: false, checked});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleEdit(item?: Option) {
|
||||
const {manager} = this.props;
|
||||
const targetNodeId = item ? item?.nodeId : this.state.targetNodeId;
|
||||
|
||||
if (!targetNodeId) {
|
||||
toast.warning(`未找到目标组件`);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.setActiveId(targetNodeId);
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderOption(item: Option, index: number) {
|
||||
const {classnames: cx, feat, popOverContainer, env} = this.props;
|
||||
|
||||
return (
|
||||
<li key={index} className={cx('ae-CRUDConfigControl-list-item')}>
|
||||
<TooltipWrapper
|
||||
tooltip={{
|
||||
content: item.label,
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
container={popOverContainer || env?.getModalContainer?.()}
|
||||
trigger={['hover']}
|
||||
delay={150}
|
||||
>
|
||||
<div className={cx('ae-CRUDConfigControl-list-item-info')}>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
|
||||
<div className={cx('ae-CRUDConfigControl-list-item-actions')}>
|
||||
{item?.context?.isCascadingField ? (
|
||||
<Tag
|
||||
label={item?.context?.modelLabel}
|
||||
displayMode="normal"
|
||||
className={cx(
|
||||
'ae-CRUDConfigControl-list-item-tag',
|
||||
'ae-CRUDConfigControl-list-item-tag--cascading'
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{feat === 'SimpleQuery' ? (
|
||||
<Button
|
||||
level="link"
|
||||
size="sm"
|
||||
tooltip={{
|
||||
content: '去编辑',
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
onClick={() => this.handleEdit(item)}
|
||||
>
|
||||
<Icon icon="column-setting" className={cx('icon')} />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const {
|
||||
classPrefix: ns,
|
||||
classnames: cx,
|
||||
render,
|
||||
env,
|
||||
label,
|
||||
feat
|
||||
} = this.props;
|
||||
const {options, checked} = this.state;
|
||||
|
||||
return (
|
||||
<header className={cx('ae-CRUDConfigControl-header', 'mb-2')}>
|
||||
<span className={cx('Form-label')}>{label}</span>
|
||||
|
||||
<div className={cx('ae-CRUDConfigControl-header-actions')}>
|
||||
<Switch
|
||||
className={cx('ae-CRUDConfigControl-header-actions-switch')}
|
||||
key="switch"
|
||||
size="sm"
|
||||
classPrefix={ns}
|
||||
value={checked}
|
||||
onChange={this.handleToggle}
|
||||
/>
|
||||
<div className={cx('ae-CRUDConfigControl-header-actions-divider')} />
|
||||
<Button
|
||||
level="link"
|
||||
size="sm"
|
||||
tooltip={{
|
||||
content: '去编辑目标组件',
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
onClick={() => this.handleEdit()}
|
||||
>
|
||||
<Icon
|
||||
icon="share-link"
|
||||
className={cx('icon')}
|
||||
style={{width: '16px', height: '16px'}}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const {classnames: cx} = this.props;
|
||||
const {options, loading} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('ae-CRUDConfigControl')}>
|
||||
{this.renderHeader()}
|
||||
<ul className={cx('ae-CRUDConfigControl-list')}>
|
||||
{loading ? (
|
||||
<Spinner
|
||||
show
|
||||
tip="字段加载中"
|
||||
tipPlacement="bottom"
|
||||
size="sm"
|
||||
className={cx('flex')}
|
||||
/>
|
||||
) : Array.isArray(options) && options.length > 0 ? (
|
||||
options.map((item, index) => {
|
||||
return this.renderOption(item, index);
|
||||
})
|
||||
) : (
|
||||
<p className={cx(`ae-CRUDConfigControl-placeholder`)}>暂无字段</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({
|
||||
type: 'ae-crud-filters-control',
|
||||
renderLabel: false,
|
||||
wrap: false
|
||||
})
|
||||
export class CRUDFiltersControlRenderer extends CRUDFiltersControl {}
|
@ -0,0 +1,408 @@
|
||||
/**
|
||||
* @file CRUDToolbarControl
|
||||
* @desc 顶部工具栏控件
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {FormItem, Button, Icon, toast, Spinner, autobind} from 'amis';
|
||||
import {TooltipWrapper} from 'amis-ui';
|
||||
import {findTreeAll} from 'amis-core';
|
||||
import {JSONPipeIn} from 'amis-editor-core';
|
||||
import {DSFeature, DSFeatureType, DSFeatureEnum} from '../../builder';
|
||||
import {deepRemove} from '../../plugin/CRUD2/utils';
|
||||
|
||||
import type {FormControlProps} from 'amis';
|
||||
import type {EditorNodeType} from 'amis-editor-core';
|
||||
import type {ColumnSchema} from 'amis/lib/renderers/Table2';
|
||||
import type {DSBuilderInterface} from '../../builder';
|
||||
|
||||
type ActionValue =
|
||||
| Extract<DSFeatureType, 'Insert' | 'BulkEdit' | 'BulkDelete'>
|
||||
| 'custom';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: ActionValue;
|
||||
nodeId: string;
|
||||
/** 原始结构 */
|
||||
pristine: Record<string, any>;
|
||||
node?: EditorNodeType;
|
||||
}
|
||||
|
||||
export interface CRUDToolbarControlProps extends FormControlProps {
|
||||
/** CRUD 节点的 ID */
|
||||
nodeId: string;
|
||||
builder: DSBuilderInterface;
|
||||
}
|
||||
|
||||
export interface CRUDToolbarControlState {
|
||||
options: Option[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class CRUDToolbarControl extends React.Component<
|
||||
CRUDToolbarControlProps,
|
||||
CRUDToolbarControlState
|
||||
> {
|
||||
drag?: HTMLElement | null;
|
||||
|
||||
dom?: HTMLElement;
|
||||
|
||||
/** 可供使用的功能集合 */
|
||||
collection: ActionValue[] = [
|
||||
DSFeatureEnum.Insert,
|
||||
DSFeatureEnum.BulkEdit,
|
||||
DSFeatureEnum.BulkDelete
|
||||
];
|
||||
|
||||
constructor(props: CRUDToolbarControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: [],
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.dom = findDOMNode(this) as HTMLElement;
|
||||
const actions = this.getActions(this.props);
|
||||
this.initOptions(actions);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<CRUDToolbarControlProps>): void {
|
||||
if (prevProps.data.headerToolbar !== this.props.data.headerToolbar) {
|
||||
const actions = this.getActions(this.props);
|
||||
this.initOptions(actions);
|
||||
}
|
||||
}
|
||||
|
||||
getActions(props: CRUDToolbarControlProps) {
|
||||
const {manager, nodeId} = props;
|
||||
const store = manager.store;
|
||||
const node: EditorNodeType = store.getNodeById(nodeId);
|
||||
const actions = findTreeAll(node.children, item =>
|
||||
[
|
||||
DSFeatureEnum.Insert,
|
||||
DSFeatureEnum.BulkEdit,
|
||||
DSFeatureEnum.BulkDelete,
|
||||
'custom'
|
||||
].includes(item.schema.behavior)
|
||||
) as unknown as EditorNodeType[];
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
initOptions(actions: EditorNodeType[]) {
|
||||
if (!actions || !actions.length) {
|
||||
this.setState({options: []});
|
||||
return;
|
||||
}
|
||||
|
||||
const options = actions.map(node => {
|
||||
const schema = node.schema;
|
||||
const behavior = schema.behavior as ActionValue;
|
||||
|
||||
return {
|
||||
label: this.getOptionLabel(schema, behavior),
|
||||
value: behavior,
|
||||
nodeId: schema.$$id,
|
||||
node: node,
|
||||
pristine: node.schema
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({options});
|
||||
}
|
||||
|
||||
getOptionLabel(schema: any, behavior: ActionValue) {
|
||||
return behavior === 'custom' ? schema.label : DSFeature[behavior].label;
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleEdit(item: Option) {
|
||||
const {manager} = this.props;
|
||||
|
||||
if (!item.nodeId) {
|
||||
toast.warning(`未找到工具栏中对应操作「${item.label}」`);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.setActiveId(item.nodeId);
|
||||
}
|
||||
|
||||
/** 添加列 */
|
||||
@autobind
|
||||
async handleAddAction(type: ActionValue) {
|
||||
this.setState({loading: true});
|
||||
const {onBulkChange, data: ctx, nodeId, manager, builder} = this.props;
|
||||
const options = this.state.options.concat();
|
||||
const node = manager.store.getNodeById(nodeId);
|
||||
const CRUDSchemaID = node?.schema?.id;
|
||||
let scaffold: any;
|
||||
|
||||
switch (type) {
|
||||
case 'Insert':
|
||||
scaffold = await builder.buildInsertSchema(
|
||||
{
|
||||
feat: DSFeatureEnum.Insert,
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: ctx,
|
||||
scaffoldConfig: {
|
||||
insertFields: (ctx?.columns ?? [])
|
||||
.filter((item: ColumnSchema) => item.type !== 'operation')
|
||||
.map((item: ColumnSchema) => ({
|
||||
inputType: item.type ?? 'input-text',
|
||||
name: item.name,
|
||||
label: item.title
|
||||
})),
|
||||
insertApi: ''
|
||||
}
|
||||
},
|
||||
CRUDSchemaID
|
||||
);
|
||||
break;
|
||||
case 'BulkEdit':
|
||||
scaffold = await builder.buildBulkEditSchema(
|
||||
{
|
||||
feat: DSFeatureEnum.BulkEdit,
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: ctx,
|
||||
scaffoldConfig: {
|
||||
bulkEditFields: (ctx?.columns ?? [])
|
||||
.filter((item: ColumnSchema) => item.type !== 'operation')
|
||||
.map((item: ColumnSchema) => ({
|
||||
inputType: item.type ?? 'input-text',
|
||||
name: item.name,
|
||||
label: item.title
|
||||
})),
|
||||
bulkEdit: ''
|
||||
}
|
||||
},
|
||||
CRUDSchemaID
|
||||
);
|
||||
break;
|
||||
case 'BulkDelete':
|
||||
scaffold = await builder.buildCRUDBulkDeleteSchema(
|
||||
{
|
||||
feat: DSFeatureEnum.BulkDelete,
|
||||
renderer: 'crud',
|
||||
inScaffold: false,
|
||||
schema: ctx,
|
||||
scaffoldConfig: {
|
||||
bulkDeleteApi: ''
|
||||
}
|
||||
},
|
||||
CRUDSchemaID
|
||||
);
|
||||
break;
|
||||
default:
|
||||
scaffold = {
|
||||
type: 'button',
|
||||
label: '按钮',
|
||||
behavior: 'custom',
|
||||
className: 'm-r-xs',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!scaffold) {
|
||||
this.setState({loading: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const headerToolbarSchema = cloneDeep(ctx.headerToolbar);
|
||||
const actionSchema = JSONPipeIn({...scaffold});
|
||||
|
||||
options.push({
|
||||
label: this.getOptionLabel(actionSchema, type),
|
||||
value: type,
|
||||
nodeId: actionSchema.$$id,
|
||||
pristine: actionSchema
|
||||
});
|
||||
|
||||
this.setState({options, loading: false}, () => {
|
||||
const target = headerToolbarSchema?.[0]?.items?.[0]?.body;
|
||||
|
||||
if (target && Array.isArray(target)) {
|
||||
target.push(actionSchema);
|
||||
} else {
|
||||
headerToolbarSchema.unshift(actionSchema);
|
||||
}
|
||||
onBulkChange?.({headerToolbar: headerToolbarSchema});
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleDelete(option: Option, index: number) {
|
||||
const {env, data: ctx, onBulkChange} = this.props;
|
||||
const options = this.state.options.concat();
|
||||
const confirmed = await env.confirm(
|
||||
`确定要删除工具栏中「${option.label}」吗?`
|
||||
);
|
||||
|
||||
const headerToolbarSchema = cloneDeep(ctx.headerToolbar);
|
||||
|
||||
if (confirmed) {
|
||||
const marked = deepRemove(
|
||||
headerToolbarSchema,
|
||||
item => item.behavior === option.value
|
||||
);
|
||||
|
||||
if (marked) {
|
||||
options.splice(index, 1);
|
||||
|
||||
this.setState({options}, () => {
|
||||
onBulkChange?.({headerToolbar: headerToolbarSchema});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderOption(item: Option, index: number) {
|
||||
const {classnames: cx, popOverContainer, env} = this.props;
|
||||
|
||||
return (
|
||||
<li key={index} className={cx('ae-CRUDConfigControl-list-item')}>
|
||||
<TooltipWrapper
|
||||
tooltip={{
|
||||
content: item.label,
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
container={popOverContainer || env?.getModalContainer?.()}
|
||||
trigger={['hover']}
|
||||
delay={150}
|
||||
>
|
||||
<div className={cx('ae-CRUDConfigControl-list-item-info')}>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
|
||||
<div className={cx('ae-CRUDConfigControl-list-item-actions')}>
|
||||
<Button
|
||||
level="link"
|
||||
size="sm"
|
||||
tooltip={{
|
||||
content: '去编辑',
|
||||
tooltipTheme: 'dark',
|
||||
style: {fontSize: '12px'}
|
||||
}}
|
||||
onClick={() => this.handleEdit(item)}
|
||||
>
|
||||
<Icon icon="column-setting" className="icon" />
|
||||
</Button>
|
||||
<Button
|
||||
level="link"
|
||||
size="sm"
|
||||
onClick={() => this.handleDelete(item, index)}
|
||||
>
|
||||
<Icon icon="column-delete" className="icon" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const {classnames: cx, render, env} = this.props;
|
||||
const options = this.state.options;
|
||||
const actions = this.collection.concat();
|
||||
|
||||
// options.forEach(item => {
|
||||
// if (actions.includes(item.value)) {
|
||||
// const idx = actions.indexOf(item.value);
|
||||
// if (~idx) {
|
||||
// actions.splice(idx, 1);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
const optionValues = options.map(item => item.value);
|
||||
|
||||
return (
|
||||
<header className={cx('ae-CRUDConfigControl-header')}>
|
||||
<span className={cx('Form-label')}>工具栏</span>
|
||||
{render('crud-toolbar-control-dropdown', {
|
||||
type: 'dropdown-button',
|
||||
closeOnClick: true,
|
||||
hideCaret: true,
|
||||
level: 'link',
|
||||
align: 'right',
|
||||
trigger: ['click'],
|
||||
popOverContainer: env.getModalContainer ?? this.dom ?? document.body,
|
||||
icon: 'column-add',
|
||||
label: '添加操作',
|
||||
className: cx('ae-CRUDConfigControl-dropdown'),
|
||||
disabledTip: {
|
||||
content: '暂无可添加操作',
|
||||
tooltipTheme: 'dark'
|
||||
},
|
||||
buttons: actions
|
||||
.map((item: Exclude<ActionValue, 'custom'>) => ({
|
||||
type: 'button',
|
||||
label: DSFeature[item].label,
|
||||
disabled: !!~optionValues.findIndex(op => op === item),
|
||||
onClick: () => this.handleAddAction(item)
|
||||
}))
|
||||
.concat({
|
||||
type: 'button',
|
||||
label: '自定义按钮',
|
||||
disabled: false,
|
||||
onClick: () => this.handleAddAction('custom')
|
||||
})
|
||||
})}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classnames: cx, data: ctx} = this.props;
|
||||
const {options, loading} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('ae-CRUDConfigControl')}>
|
||||
{loading ? (
|
||||
<Spinner
|
||||
show
|
||||
tip="操作生成中"
|
||||
tipPlacement="bottom"
|
||||
size="sm"
|
||||
className={cx('flex')}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<ul className={cx('ae-CRUDConfigControl-list')}>
|
||||
{Array.isArray(options) && options.length > 0 ? (
|
||||
options.map((item, index) => {
|
||||
return this.renderOption(item, index);
|
||||
})
|
||||
) : (
|
||||
<p className={cx(`ae-CRUDConfigControl-placeholder`)}>
|
||||
暂无数据
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({
|
||||
type: 'ae-crud-toolbar-control',
|
||||
renderLabel: false,
|
||||
wrap: false
|
||||
})
|
||||
export class CRUDToolbarControlRenderer extends CRUDToolbarControl {}
|
@ -2702,7 +2702,7 @@ export const getEventControlConfig = (
|
||||
: ACTION_TYPE_TREE(manager);
|
||||
const allComponents = manager?.store?.getComponentTreeSource();
|
||||
const checkComponent = (node: any, action: RendererPluginAction) => {
|
||||
const actionType = action.actionType!;
|
||||
const actionType = action?.actionType;
|
||||
const actions = manager?.pluginActions[node.type];
|
||||
const haveChild = !!node.children?.length;
|
||||
let isSupport = false;
|
||||
@ -2711,7 +2711,7 @@ export const getEventControlConfig = (
|
||||
action.supportComponents === '*' ||
|
||||
action.supportComponents === node.type;
|
||||
// 内置逻辑
|
||||
if (action.supportComponents === 'byComponent') {
|
||||
if (action.supportComponents === 'byComponent' && actionType) {
|
||||
isSupport = hasActionType(actionType, actions);
|
||||
node.scoped = isSupport;
|
||||
}
|
||||
@ -2756,6 +2756,10 @@ export const getEventControlConfig = (
|
||||
return manager.dataSchema;
|
||||
},
|
||||
getComponents: (action: RendererPluginAction) => {
|
||||
if (!action) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let components = manager?.store?.getComponentTreeSource();
|
||||
let finalCmpts: any[] = [];
|
||||
if (isSubEditor) {
|
||||
|
@ -418,7 +418,7 @@ export class EventControl extends React.Component<
|
||||
// 找到激活的事件面板
|
||||
Object.keys(onEvent)
|
||||
.filter((key: string) => {
|
||||
return onEvent[key].actions.length && eventPanelActive[key];
|
||||
return onEvent[key]?.actions?.length && eventPanelActive[key];
|
||||
})
|
||||
.forEach((key: string, index: number) => {
|
||||
if (!this.eventPanelSortMap[key]) {
|
||||
@ -748,7 +748,7 @@ export class EventControl extends React.Component<
|
||||
getContextSchemas,
|
||||
...actionConfig,
|
||||
groupType: actionConfig?.__actionType || action.actionType,
|
||||
__actionDesc: actionNode!.description!, // 树节点描述
|
||||
__actionDesc: actionNode?.description ?? '', // 树节点描述
|
||||
__actionSchema: actionNode!.schema, // 树节点schema
|
||||
__subActions: hasSubActionNode?.actions, // 树节点子动作
|
||||
__cmptTreeSource: supportComponents ?? [],
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import React, {MouseEvent} from 'react';
|
||||
import cx from 'classnames';
|
||||
import {Icon, FormItem, TooltipWrapper} from 'amis';
|
||||
import {Icon, FormItem, TooltipWrapper, Spinner} from 'amis';
|
||||
import {autobind, FormControlProps, render as renderAmis} from 'amis-core';
|
||||
import {CodeMirrorEditor, FormulaEditor} from 'amis-ui';
|
||||
import type {VariableItem, CodeMirror} from 'amis-ui';
|
||||
@ -120,6 +120,8 @@ interface TextareaFormulaControlState {
|
||||
isFullscreen: boolean; //是否全屏
|
||||
|
||||
tooltipStyle: {[key: string]: string}; // 提示框样式
|
||||
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class TextareaFormulaControl extends React.Component<
|
||||
@ -150,7 +152,8 @@ export class TextareaFormulaControl extends React.Component<
|
||||
formulaPickerOpen: false,
|
||||
formulaPickerValue: '',
|
||||
isFullscreen: false,
|
||||
tooltipStyle: {}
|
||||
tooltipStyle: {},
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
|
||||
@ -282,12 +285,46 @@ export class TextareaFormulaControl extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleFormulaEditorOpen() {
|
||||
const {node, manager, data} = this.props;
|
||||
const onFormulaEditorOpen = manager?.config?.onFormulaEditorOpen;
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
try {
|
||||
if (
|
||||
manager &&
|
||||
onFormulaEditorOpen &&
|
||||
typeof onFormulaEditorOpen === 'function'
|
||||
) {
|
||||
const res = await onFormulaEditorOpen(node, manager, data);
|
||||
|
||||
if (res !== false) {
|
||||
const variables = await getVariables(this);
|
||||
this.setState({variables});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[amis-editor][TextareaFormulaControl] onFormulaEditorOpen failed: ',
|
||||
error?.stack
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleFormulaClick(e: React.MouseEvent, type?: string) {
|
||||
if (this.props.onOverallClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handleFormulaEditorOpen();
|
||||
} catch (error) {}
|
||||
|
||||
const variablesArr = await getVariables(this);
|
||||
|
||||
this.setState({
|
||||
@ -337,7 +374,8 @@ export class TextareaFormulaControl extends React.Component<
|
||||
formulaPickerValue,
|
||||
isFullscreen,
|
||||
variables,
|
||||
tooltipStyle
|
||||
tooltipStyle,
|
||||
loading
|
||||
} = this.state;
|
||||
|
||||
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
|
||||
@ -394,14 +432,22 @@ export class TextareaFormulaControl extends React.Component<
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li className="ae-TextareaResultBox-footer-fxIcon">
|
||||
<a
|
||||
data-tooltip="表达式"
|
||||
data-position="top"
|
||||
onClick={this.handleFormulaClick}
|
||||
>
|
||||
<Icon icon="input-add-fx" className="icon" />
|
||||
</a>
|
||||
<li
|
||||
className={cx('ae-TextareaResultBox-footer-fxIcon', {
|
||||
'is-loading': loading
|
||||
})}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner show icon="reload" size="sm" />
|
||||
) : (
|
||||
<a
|
||||
data-tooltip="表达式"
|
||||
data-position="top"
|
||||
onClick={this.handleFormulaClick}
|
||||
>
|
||||
<Icon icon="function" className="icon" />
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
{/* 附加底部按钮菜单项 */}
|
||||
{Array.isArray(additionalMenus) &&
|
||||
|
@ -373,23 +373,12 @@ setSchemaTpl(
|
||||
name: fieldName,
|
||||
type: 'radios',
|
||||
inline: true,
|
||||
onChange: () => {},
|
||||
value: false,
|
||||
// pipeIn: (value:any) => typeof value === 'boolean' ? value : '1'
|
||||
options: [
|
||||
{
|
||||
label: '是',
|
||||
value: true
|
||||
},
|
||||
|
||||
{
|
||||
label: '否',
|
||||
value: false
|
||||
},
|
||||
|
||||
{
|
||||
label: '表达式',
|
||||
value: ''
|
||||
}
|
||||
{label: '是', value: true},
|
||||
{label: '否', value: false},
|
||||
{label: '表达式', value: ''}
|
||||
]
|
||||
},
|
||||
|
||||
@ -460,29 +449,38 @@ 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',
|
||||
setSchemaTpl(
|
||||
'interval',
|
||||
(config?: {
|
||||
switchMoreConfig?: any;
|
||||
formItems?: any[];
|
||||
intervalConfig?: any;
|
||||
}) => ({
|
||||
type: 'ae-switch-more',
|
||||
label: '定时刷新',
|
||||
name: 'interval',
|
||||
formType: 'extend',
|
||||
bulk: true,
|
||||
mode: 'normal',
|
||||
form: {
|
||||
body: [
|
||||
getSchemaTpl('withUnit', {
|
||||
label: '刷新间隔',
|
||||
name: 'interval',
|
||||
value: 1000
|
||||
},
|
||||
unit: '毫秒'
|
||||
})
|
||||
]
|
||||
},
|
||||
...more
|
||||
}));
|
||||
control: {
|
||||
type: 'input-number',
|
||||
name: 'interval',
|
||||
value: 1000
|
||||
},
|
||||
unit: '毫秒',
|
||||
...((config && config.intervalConfig) || {})
|
||||
}),
|
||||
...((config && config.formItems) || [])
|
||||
]
|
||||
},
|
||||
...((config && config.switchMoreConfig) || {})
|
||||
})
|
||||
);
|
||||
|
||||
setSchemaTpl('silentPolling', () =>
|
||||
getSchemaTpl('switch', {
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
tipedLabel,
|
||||
EditorManager
|
||||
} from 'amis-editor-core';
|
||||
import type {DSField} from 'amis-editor-core';
|
||||
import type {SchemaObject} from 'amis';
|
||||
import flatten from 'lodash/flatten';
|
||||
import {InputComponentName} from '../component/InputComponentName';
|
||||
@ -17,6 +16,8 @@ import map from 'lodash/map';
|
||||
import omit from 'lodash/omit';
|
||||
import keys from 'lodash/keys';
|
||||
|
||||
import type {DSField} from '../builder';
|
||||
|
||||
/**
|
||||
* @deprecated 兼容当前组件的switch
|
||||
*/
|
||||
@ -428,6 +429,7 @@ setSchemaTpl(
|
||||
variables?: Array<VariableItem> | Function; // 自定义变量集合
|
||||
requiredDataPropsVariables?: boolean; // 是否再从amis数据域中取变量结合, 默认 false
|
||||
variableMode?: 'tabs' | 'tree'; // 变量展现模式
|
||||
className?: string; // 外层类名
|
||||
[key: string]: any; // 其他属性,例如包括表单项pipeIn\Out 等等
|
||||
}) => {
|
||||
const {
|
||||
@ -463,6 +465,7 @@ setSchemaTpl(
|
||||
// 上下展示,可避免 自定义渲染器 出现挤压
|
||||
mode: mode === 'vertical' ? 'vertical' : 'horizontal',
|
||||
visibleOn,
|
||||
className: config?.className,
|
||||
body: [
|
||||
getSchemaTpl('formulaControl', {
|
||||
label: label ?? '默认值',
|
||||
@ -1691,3 +1694,13 @@ setSchemaTpl('anchorNavTitle', {
|
||||
type: 'input-text',
|
||||
required: true
|
||||
});
|
||||
|
||||
setSchemaTpl('primaryField', {
|
||||
type: 'input-text',
|
||||
name: 'primaryField',
|
||||
label: tipedLabel(
|
||||
'主键',
|
||||
'每行记录的唯一标识符,通常用于行选择、批量操作等场景。'
|
||||
),
|
||||
pipeIn: defaultValue('id')
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {findTree, resolveVariableAndFilter} from 'amis';
|
||||
import {JSONValueMap, findTree, resolveVariableAndFilter} from 'amis';
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
/**
|
||||
@ -64,3 +64,19 @@ export const resolveOptionType = (options: any) => {
|
||||
|
||||
return value !== undefined ? typeof value : 'string';
|
||||
};
|
||||
|
||||
/**
|
||||
* 将组件配置里面的公式进行转义,一般是文本组件编辑器里直接显示公式所用
|
||||
*
|
||||
* @param conf 组件schema 配置
|
||||
* @param keys 转义的字段key列表
|
||||
* @returns 转义后的配置
|
||||
*/
|
||||
export function escapeFormula(conf: any, keys: string[] = ['tpl']) {
|
||||
return JSONValueMap(conf, (value: any, key: string | number) => {
|
||||
if (keys.includes(String(key)) && /(^|[^\\])\$\{.+\}/.test(value)) {
|
||||
return value.replace(/\${/g, ' \\${');
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-formula",
|
||||
"version": "3.4.0",
|
||||
"version": "3.4.1-alpha.0",
|
||||
"description": "负责 amis 里面的表达式实现,内置公式,编辑器等",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "3.4.0",
|
||||
"version": "3.4.1-alpha.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "npm run clean-dist && NODE_ENV=production rollup -c ",
|
||||
@ -36,8 +36,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@rc-component/mini-decimal": "^1.0.1",
|
||||
"amis-core": "^3.4.0",
|
||||
"amis-formula": "^3.4.0",
|
||||
"amis-core": "^3.4.1-alpha.0",
|
||||
"amis-formula": "^3.4.1-alpha.0",
|
||||
"classnames": "2.3.2",
|
||||
"codemirror": "^5.63.0",
|
||||
"downshift": "6.1.12",
|
||||
|
@ -3,3 +3,11 @@
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.max-line {
|
||||
overflow : hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
17
packages/amis-ui/scss/components/form/_input-table.scss
Normal file
17
packages/amis-ui/scss/components/form/_input-table.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.#{$ns}InputTable-UI {
|
||||
.#{$ns}Table-contentWrap.is-fixed {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
|
||||
& thead > tr > th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: $zindex-sticky; /* 遮挡一下 tbody */
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
@import '../layout/aside';
|
||||
@import '../layout/hbox';
|
||||
@import '../layout/vbox';
|
||||
@import '../components/button'; // 这个要放在最前面
|
||||
@import '../components/button';
|
||||
@import '../components/avatar';
|
||||
@import '../components/breadcrumb';
|
||||
@import '../components/badge';
|
||||
@ -126,6 +126,8 @@
|
||||
@import '../components/form/icon-select';
|
||||
@import '../components/form/form';
|
||||
@import '../components/form/user-select';
|
||||
@import '../components/form/input-table';
|
||||
|
||||
@import '../components/anchor-nav';
|
||||
@import '../components/markdown';
|
||||
@import '../components/link';
|
||||
|
@ -8,7 +8,8 @@ import TooltipWrapper, {TooltipObject, Trigger} from './TooltipWrapper';
|
||||
import {pickEventsProps} from 'amis-core';
|
||||
import {ClassNamesFn, themeable} from 'amis-core';
|
||||
import Spinner, {SpinnerExtraProps} from './Spinner';
|
||||
interface ButtonProps
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.DOMAttributes<HTMLButtonElement>,
|
||||
SpinnerExtraProps {
|
||||
id?: string;
|
||||
|
@ -117,7 +117,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
||||
|
||||
{clearable && !disabled && value ? (
|
||||
<a onClick={this.clearValue} className={cx('InputBox-clear')}>
|
||||
<Icon icon="close" className="icon" />
|
||||
<Icon icon="input-clear" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -20,11 +20,26 @@ import Button from './Button';
|
||||
import FormField, {FormFieldProps} from './FormField';
|
||||
import {Icon} from './icons';
|
||||
|
||||
import type {ButtonProps} from './Button';
|
||||
|
||||
export interface tdRenderFunc {
|
||||
(
|
||||
methods: UseFormReturn,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
): JSX.Element | null;
|
||||
}
|
||||
|
||||
export interface InputTableColumnProps {
|
||||
title?: string;
|
||||
className?: string;
|
||||
thRender?: () => JSX.Element;
|
||||
tdRender: (methods: UseFormReturn, index: number) => JSX.Element | null;
|
||||
tdRender: tdRenderFunc;
|
||||
}
|
||||
|
||||
interface InputTableScrollProps {
|
||||
/** 垂直滚动区域的最大高度 */
|
||||
y: number | string;
|
||||
}
|
||||
|
||||
export interface InputTabbleProps<T = any>
|
||||
@ -45,10 +60,24 @@ export interface InputTabbleProps<T = any>
|
||||
addable?: boolean;
|
||||
addButtonClassName?: string;
|
||||
addButtonText?: string;
|
||||
addButtonProps?: Partial<Omit<ButtonProps, 'onClick'>>;
|
||||
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
removable?: boolean;
|
||||
/** 表格CSS类名 */
|
||||
tableClassName?: string;
|
||||
/** 表格头CSS类名 */
|
||||
tableHeadClassName?: string;
|
||||
/** 表格内容区CSS类名 */
|
||||
tableBodyClassName?: string;
|
||||
/** 空状态文字 */
|
||||
placeholder?: React.ReactNode;
|
||||
/** 滚动设置 */
|
||||
scroll?: InputTableScrollProps;
|
||||
/** 底部工具栏 */
|
||||
footer?: () => React.ReactNode;
|
||||
onItemAdd?: (values: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export function InputTable({
|
||||
@ -69,12 +98,23 @@ export function InputTable({
|
||||
addable,
|
||||
addButtonText,
|
||||
addButtonClassName,
|
||||
addButtonProps,
|
||||
scaffold,
|
||||
minLength,
|
||||
maxLength,
|
||||
isRequired,
|
||||
rules
|
||||
rules,
|
||||
tableClassName,
|
||||
tableHeadClassName,
|
||||
tableBodyClassName,
|
||||
placeholder,
|
||||
scroll,
|
||||
footer,
|
||||
onItemAdd
|
||||
}: InputTabbleProps) {
|
||||
const enableScroll = scroll?.y != null;
|
||||
const tBodyRef = React.useRef<HTMLTableElement>(null);
|
||||
const tableRef = React.useRef<HTMLTableElement>(null);
|
||||
const subForms = React.useRef<Record<any, UseFormReturn>>({});
|
||||
const subFormRef = React.useCallback(
|
||||
(subform: UseFormReturn | null, id: string) => {
|
||||
@ -160,11 +200,32 @@ export function InputTable({
|
||||
);
|
||||
|
||||
function renderBody() {
|
||||
const handleItemAdd = () => {
|
||||
const values = {...scaffold};
|
||||
append(values);
|
||||
|
||||
/** 开启滚动后新增元素定位到底部 */
|
||||
if (enableScroll && tableRef) {
|
||||
requestAnimationFrame(() => {
|
||||
tableRef?.current?.scrollIntoView?.({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onItemAdd?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(`Table`)}>
|
||||
<div className={cx(`Table-contentWrap`)}>
|
||||
<table className={cx(`Table-table`)}>
|
||||
<thead>
|
||||
<div className={cx(`Table`, `InputTable-UI`, className)}>
|
||||
<div
|
||||
className={cx(`Table-contentWrap`, {'is-fixed': enableScroll})}
|
||||
style={{maxHeight: enableScroll ? scroll.y : 'unset'}}
|
||||
>
|
||||
<table className={cx(`Table-table`, tableClassName)} ref={tableRef}>
|
||||
<thead className={cx(tableHeadClassName)}>
|
||||
<tr>
|
||||
{columns.map((item, index) => (
|
||||
<th key={index} className={item.className}>
|
||||
@ -174,7 +235,7 @@ export function InputTable({
|
||||
<th key="operation">{__('Table.operation')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className={cx(tableBodyClassName)}>
|
||||
{fields.length ? (
|
||||
fields.map((field, index) => (
|
||||
<tr key={field.id}>
|
||||
@ -212,7 +273,7 @@ export function InputTable({
|
||||
icon="desk-empty"
|
||||
className={cx('Table-placeholder-empty-icon', 'icon')}
|
||||
/>
|
||||
{__('placeholder.noData')}
|
||||
{placeholder ?? __('placeholder.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -223,12 +284,14 @@ export function InputTable({
|
||||
<div className={cx(`InputTable-toolbar`)}>
|
||||
<Button
|
||||
className={cx(addButtonClassName)}
|
||||
onClick={() => append({...scaffold})}
|
||||
size="sm"
|
||||
{...addButtonProps}
|
||||
onClick={() => handleItemAdd()}
|
||||
>
|
||||
<Icon icon="plus" className="icon" />
|
||||
<span>{__(addButtonText || 'add')}</span>
|
||||
</Button>
|
||||
{footer?.()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@ -257,7 +320,7 @@ export interface InputTableRowProps {
|
||||
value: any;
|
||||
control: Control<any>;
|
||||
columns: Array<{
|
||||
tdRender: (methods: UseFormReturn, index: number) => JSX.Element | null;
|
||||
tdRender: tdRenderFunc;
|
||||
className?: string;
|
||||
}>;
|
||||
update: (index: number, data: Record<string, any>) => void;
|
||||
@ -293,9 +356,9 @@ export function InputTableRow({
|
||||
|
||||
return (
|
||||
<>
|
||||
{columns.map((item, index) => (
|
||||
<td key={index} className={item.className}>
|
||||
{item.tdRender(methods, index)}
|
||||
{columns.map((item, colIndex) => (
|
||||
<td key={colIndex} className={item.className}>
|
||||
{item.tdRender(methods, colIndex, index)}
|
||||
</td>
|
||||
))}
|
||||
</>
|
||||
|
@ -149,7 +149,10 @@ export class Tag extends React.Component<TagProps> {
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<span className={cx('Tag-text')}>
|
||||
<span
|
||||
className={cx('Tag-text')}
|
||||
title={typeof label === 'string' ? label : undefined}
|
||||
>
|
||||
{prevIcon}
|
||||
{label || children}
|
||||
</span>
|
||||
@ -187,6 +190,7 @@ class CheckableTagComp extends React.Component<CheckableTagProps> {
|
||||
})}
|
||||
onClick={disabled ? noop : this.handleClick}
|
||||
style={style}
|
||||
title={typeof label === 'string' ? label : undefined}
|
||||
>
|
||||
{label || children}
|
||||
</span>
|
||||
|
@ -468,7 +468,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
|
||||
}
|
||||
current && this.updateTableDom(current);
|
||||
|
||||
if (this.props.draggable) {
|
||||
if (this.props.draggable && this.tbodyDom?.current) {
|
||||
this.initDragging();
|
||||
}
|
||||
|
||||
@ -1140,8 +1140,8 @@ export class Table extends React.PureComponent<TableProps, TableState> {
|
||||
|
||||
if (record) {
|
||||
let target = event.target;
|
||||
if (target.tagName !== 'TR') {
|
||||
target = target.closest('tr');
|
||||
if (target?.tagName !== 'TR') {
|
||||
target = target?.closest('tr');
|
||||
}
|
||||
|
||||
this.setState({hoverRow: {target, rowIndex, record}});
|
||||
|
@ -67,11 +67,7 @@ test('Renderer:input table', async () => {
|
||||
});
|
||||
|
||||
test('Renderer: input-table with default value column', async () => {
|
||||
const onSubmitCallbackFn = jest
|
||||
.fn()
|
||||
.mockImplementation((values: any, actions: any) => {
|
||||
return true;
|
||||
});
|
||||
const onSubmitCallbackFn = jest.fn();
|
||||
const {container, getByText} = render(
|
||||
amisRender(
|
||||
{
|
||||
|
@ -1279,6 +1279,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1347,6 +1348,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1415,6 +1417,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1483,6 +1486,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1551,6 +1555,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1619,6 +1624,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1687,6 +1693,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1755,6 +1762,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1823,6 +1831,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
@ -1891,6 +1900,7 @@ exports[`5. Renderer:crud cards 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Grid-col--sm6"
|
||||
style="flex: 0 0 50%; max-width: 50%;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card cxd-Card--link"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis",
|
||||
"version": "3.4.0",
|
||||
"version": "3.4.1-alpha.0",
|
||||
"description": "一种MIS页面生成工具",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -37,8 +37,8 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"amis-core": "^3.4.0",
|
||||
"amis-ui": "^3.4.0",
|
||||
"amis-core": "^3.4.1-alpha.0",
|
||||
"amis-ui": "^3.4.1-alpha.0",
|
||||
"attr-accept": "2.2.2",
|
||||
"blueimp-canvastoblob": "2.1.0",
|
||||
"classnames": "2.3.2",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user