Merge branch 'master' into bugfix-editor-paste

This commit is contained in:
fujianchao 2024-03-13 16:52:28 +08:00
commit 7eab13dcb3
248 changed files with 8109 additions and 2745 deletions

View File

@ -0,0 +1,121 @@
---
title: inputSignature 签名面板
description:
type: 0
group: null
menuName: inputSignature
icon:
order: 62
---
## 基本用法
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"height": 200
}
]
}
```
## 自定义按钮名称
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"height": 160,
"confirmBtnLabel": "确定",
"undoBtnLabel": "上一步",
"clearBtnLabel": "重置"
}
]
}
```
## 自定义颜色
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"height": 200,
"color": "#ff0000",
"bgColor": "#fff"
}
]
}
```
## 配合图片组件实现实时预览
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"height": 200
},
{
"type": "image",
"name": "signature"
}
]
}
```
## 内嵌模式
在内嵌模式下,组件会以按钮的形式展示,点击按钮后弹出一个容器,用户可以在容器中完成签名。更适合在移动端使用。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"embed": true
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | --------- | ---------- | -------------------- |
| width | `number` | | 组件宽度,最小 300 |
| height | `number` | | 组件高度,最小 160 |
| color | `string` | `#000` | 手写字体颜色 |
| bgColor | `string` | `#EFEFEF` | 面板背景颜色 |
| clearBtnLabel | `string` | `清空` | 清空按钮名称 |
| undoBtnLabel | `string` | `撤销` | 撤销按钮名称 |
| confirmBtnLabel | `string` | `确认` | 确认按钮名称 |
| embed | `boolean` | | 是否内嵌 |
| embedConfirmLabel | `string` | `确认` | 内嵌容器确认按钮名称 |
| ebmedCancelLabel | `string` | `取消` | 内嵌容器取消按钮名称 |
| embedBtnIcon | `string` | | 内嵌按钮图标 |
| embedBtnLabel | `string` | `点击签名` | 内嵌按钮文案 |

View File

@ -1144,17 +1144,17 @@ true false false [{label: 'A/B/C', value: 'a/b/c'},{label: 'A
> `[name]`表示当前组件绑定的名称,即`name`属性,如果没有配置`name`属性,则通过`value`取值。
| 事件名称 | 事件参数 | 说明 |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| change | `items: object[]`选项集合(< 3.6.0 及以下版本 不支持该参数<br/>`[name]: string` 组件的值 | 选中值变化时触发 |
| addConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 新增的节点信息<br/>`items: object[]`选项集合 | 新增节点提交时触发 |
| editConfirm (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`item: object` 编辑的节点信息<br/>`items: object[]`选项集合 | 编辑节点提交时触发 |
| deleteConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 删除的节点信息<br/>`items: object[]`选项集合 | 删除节点提交时触发 |
| deferLoadFinished (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`result: object` deferApi 懒加载远程请求成功后返回的数据 <br/>`items: object[]`选项集合 | 懒加载接口远程请求成功时触发 |
| add不推荐 | `[name]: object` 新增的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 新增节点提交时触发 |
| edit不推荐 | `[name]: object` 编辑的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 编辑节点提交时触发 |
| delete不推荐 | `[name]: object` 删除的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 删除节点提交时触发 |
| loadFinished不推荐 | `[name]: object` deferApi 懒加载远程请求成功后返回的数据 | 懒加载接口远程请求成功时触发 |
| 事件名称 | 事件参数 | 说明 |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| change | `items: object[]`选项集合(3.6.0 及以上版本)<br/>`item: object`选中的节点6.2.0 及以上版本)<br/>`[name]: string` 组件的值 | 选中值变化时触发 |
| addConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 新增的节点信息<br/>`items: object[]`选项集合 | 新增节点提交时触发 |
| editConfirm (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`item: object` 编辑的节点信息<br/>`items: object[]`选项集合 | 编辑节点提交时触发 |
| deleteConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 删除的节点信息<br/>`items: object[]`选项集合 | 删除节点提交时触发 |
| deferLoadFinished (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`result: object` deferApi 懒加载远程请求成功后返回的数据 <br/>`items: object[]`选项集合 | 懒加载接口远程请求成功时触发 |
| add不推荐 | `[name]: object` 新增的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 新增节点提交时触发 |
| edit不推荐 | `[name]: object` 编辑的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 编辑节点提交时触发 |
| delete不推荐 | `[name]: object` 删除的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 删除节点提交时触发 |
| loadFinished不推荐 | `[name]: object` deferApi 懒加载远程请求成功后返回的数据 | 懒加载接口远程请求成功时触发 |
### change

View File

@ -123,6 +123,45 @@ order: 61
}
```
## Mini 版本
通过设置 `mini` 属性可以开启 mini 版本,适用于宽度窄的情况。同时通过 `advancedSettings` 可以定制弹窗中的配置面板。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"body": [
{
"type": "json-schema-editor",
"name": "schema",
"label": "字段类型",
"mini": true,
"style": {
"width": 300
},
"advancedSettings": {
"string": [
{
"type": "input-text",
"name": "maxLength",
"label": "Max Length"
}
],
"number": [
{
"type": "input-number",
"name": "max",
"label": "Max"
}
]
}
}
]
}
```
## 占位提示
> 2.8.0 及以上版本
@ -160,6 +199,7 @@ order: 61
| showRootInfo | `boolean` | false | 是否显示顶级类型信息 |
| disabledTypes | `Array<string>` | | 用来禁用默认数据类型默认类型有string、number、interger、object、number、array、boolean、null |
| definitions | `object` | | 用来配置预设类型 |
| mini | `boolean` | | 用来开启迷你模式,适应于边栏面板,宽度较低的情况 |
| placeholder | `SchemaEditorItemPlaceholder` | `{key: "字段名", title: "名称", description: "描述", default: "默认值", empty: "<空>",}` | 属性输入控件的占位提示文本 | `2.8.0` |
### SchemaEditorItemPlaceholder

View File

@ -409,19 +409,19 @@ order: 60
> `[name]`表示当前组件绑定的名称,即`name`属性,如果没有配置`name`属性,则通过`value`取值。
| 事件名称 | 事件参数 | 说明 |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| change | `[name]: string` 组件的值 <br/>`items: object[]`选项集合(< 3.6.0 及以下版本 不支持该参数 | 选中值变化时触发 |
| blur | `[name]: string` 组件的值 <br/>`items: object[]`选项集合(< 3.6.4 及以下版本 不支持该参数 | 输入框失去焦点时触发 |
| focus | `[name]: string` 组件的值 <br/>`items: object[]`选项集合(< 3.6.4 及以下版本 不支持该参数 | 输入框获取焦点时触发 |
| addConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 新增的节点信息<br/>`items: object[]`选项集合 | 新增节点提交时触发 |
| editConfirm (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`item: object` 编辑的节点信息<br/>`items: object[]`选项集合 | 编辑节点提交时触发 |
| deleteConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 删除的节点信息<br/>`items: object[]`选项集合 | 删除节点提交时触发 |
| deferLoadFinished (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`result: object` deferApi 懒加载远程请求成功后返回的数据 <br/>`items: object[]`选项集合 | 懒加载接口远程请求成功时触发 |
| add不推荐 | `[name]: object` 新增的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 新增节点提交时触发 |
| edit不推荐 | `[name]: object` 编辑的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 编辑节点提交时触发 |
| delete不推荐 | `[name]: object` 删除的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 删除节点提交时触发 |
| loadFinished不推荐 | `[name]: object` deferApi 懒加载远程请求成功后返回的数据 | 懒加载接口远程请求成功时触发 |
| 事件名称 | 事件参数 | 说明 |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| change | `[name]: string` 组件的值 <br/>`item: object`选中的节点6.2.0 及以上版本)<br/>`items: object[]`选项集合3.6.0 及以上版本) | 选中值变化时触发 |
| blur | `[name]: string` 组件的值 <br/>``item: object`选中的节点6.2.0 及以上版本)<br/>items: object[]`选项集合3.6.4 及以上版本) | 输入框失去焦点时触发 |
| focus | `[name]: string` 组件的值 <br/>`item: object`选中的节点6.2.0 及以上版本)<br/>`items: object[]`选项集合3.6.4 及以上版本) | 输入框获取焦点时触发 |
| addConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 新增的节点信息<br/>`items: object[]`选项集合 | 新增节点提交时触发 |
| editConfirm (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`item: object` 编辑的节点信息<br/>`items: object[]`选项集合 | 编辑节点提交时触发 |
| deleteConfirm (3.6.4 及以上版本) | `[name]: string` 组件的值<br/>`item: object` 删除的节点信息<br/>`items: object[]`选项集合 | 删除节点提交时触发 |
| deferLoadFinished (3.6.4 及以上版本) | `[name]: object` 组件的值<br/>`result: object` deferApi 懒加载远程请求成功后返回的数据 <br/>`items: object[]`选项集合 | 懒加载接口远程请求成功时触发 |
| add不推荐 | `[name]: object` 新增的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 新增节点提交时触发 |
| edit不推荐 | `[name]: object` 编辑的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 编辑节点提交时触发 |
| delete不推荐 | `[name]: object` 删除的节点信息<br/>`items: object[]`选项集合(< 2.3.2 及以下版本 `options` | 删除节点提交时触发 |
| loadFinished不推荐 | `[name]: object` deferApi 懒加载远程请求成功后返回的数据 | 懒加载接口远程请求成功时触发 |
### change

View File

@ -0,0 +1,56 @@
---
title: PDF Viewer
description:
type: 0
group: ⚙ 组件
menuName: PDFViewer 渲染
icon:
order: 24
---
## 基本用法
```schema: scope="body"
{
"type": "pdf-viewer",
"id": "pdf-viewer",
"src": "/examples/static/simple.pdf",
"width": 500
}
```
## 配合文件上传实现预览功能
配置和 `input-file` 相同的 `name` 即可
```schema: scope="body"
{
"type": "form",
"title": "",
"wrapWithPanel": false,
"body": [
{
"type": "input-file",
"name": "file",
"label": "File",
"asBlob": true,
"accept": ".pdf"
},
{
"type": "pdf-viewer",
"id": "pdf-viewer",
"name": "file",
"width": 500
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ---------- | ------ | ------ | ---------- |
| src | Api | | 文档地址 |
| width | number | | 宽度 |
| height | number | - | 高度 |
| background | string | #fff | PDF 背景色 |

View File

@ -1444,6 +1444,75 @@ run action ajax
| copyFormat | `string` | `text/html` | 复制格式 |
| content | [模板](../../docs/concepts/template) | - | 指定复制的内容。可用 `${xxx}` 取值 |
### 打印
> 6.2.0 及以后版本
打印页面中的某个组件,对应的组件需要配置 `testid`,如果要打印多个,可以使用 `"testids": ["x", "y"]` 来打印多个组件
```schema
{
type: 'page',
body: [
{
type: 'button',
label: '打印',
level: 'primary',
className: 'mr-2',
onEvent: {
click: {
actions: [
{
actionType: 'print',
args: {
testid: 'mycrud'
}
}
]
}
}
},
{
"type": "crud",
"api": "/api/mock2/sample",
"testid": "mycrud",
"syncLocation": false,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
]
}
```
| 属性名 | 类型 | 默认值 | 说明 |
| ------- | ---------- | ------ | ----------------- |
| testid | `string` | | 组件的 testid |
| testids | `string[]` | - | 多个组件的 testid |
### 发送邮件
通过配置`actionType: 'email'`和邮件属性实现发送邮件操作。

View File

@ -303,7 +303,16 @@ export default {
body: {
type: 'form',
name: 'sample-edit-form',
api: '/api/sample/$id',
data:{
env: 'test'
},
api: {
method:'post',
url:'/api/sample/$id',
messages:{
success: '成功了-${env}'
}
},
body: [
{
type: 'input-text',

View File

@ -785,6 +785,16 @@ export const components = [
wrapDoc
)
)
},
{
label: 'InputSignature 签名面板',
path: '/zh-CN/components/form/input-signature',
component: React.lazy(() =>
import('../../docs/zh-CN/components/form/input-signature.md').then(
wrapDoc
)
)
}
]
},
@ -987,6 +997,13 @@ export const components = [
import('../../docs/zh-CN/components/office-viewer.md').then(wrapDoc)
)
},
{
label: 'PDFViewer 渲染',
path: '/zh-CN/components/pdf-viewer',
component: React.lazy(() =>
import('../../docs/zh-CN/components/pdf-viewer.md').then(wrapDoc)
)
},
{
label: 'Progress 进度条',
path: '/zh-CN/components/progress',

View File

@ -130,6 +130,7 @@ import Tab3Schema from './Tabs/Tab3';
import Loading from './Loading';
import CodeSchema from './Code';
import OfficeViewer from './OfficeViewer';
import PdfViewer from './PdfViewer';
import InputTableEvent from './EventAction/cmpt-event-action/InputTableEvent';
import WizardPage from './WizardPage';
@ -912,6 +913,13 @@ export const examples = [
component: makeSchemaRenderer(OfficeViewer)
},
{
label: 'Pdf 预览',
icon: 'fa fa-file-pdf',
path: '/examples/pdf-viewer',
component: makeSchemaRenderer(PdfViewer)
},
{
label: '多 loading',
icon: 'fa fa-spinner',

View File

@ -0,0 +1,24 @@
export default {
type: 'page',
body: {
type: 'form',
id: 'form',
debug: true,
wrapWithPanel: false,
body: [
{
type: 'input-file',
name: 'file',
label: '选择 PDF 文件预览效果(不会上传到服务器)',
asBlob: true,
accept: '.pdf'
},
{
type: 'pdf-viewer',
id: 'pdf-viewer',
name: 'file',
width: 500
}
]
}
};

View File

@ -246,6 +246,8 @@ export default function (schema, schemaProps, showCode, envOverrides) {
};
});
},
// testid
// enableTestid: true,
...envOverrides
};

BIN
examples/static/simple.pdf Normal file

Binary file not shown.

View File

@ -241,7 +241,7 @@ fis.match('/examples/mod.js', {
isMod: false
});
fis.match('{markdown-it,moment-timezone}/**', {
fis.match('{markdown-it,moment-timezone,pdfjs-dist}/**', {
preprocessor: fis.plugin('js-require-file')
});
@ -503,6 +503,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!amis-ui/lib/components/RichText.js',
'!amis-ui/lib/components/Tinymce.js',
'!amis-ui/lib/components/ColorPicker.js',
'!amis-ui/lib/components/PdfViewer.js',
'!react-color/**',
'!material-colors/**',
'!reactcss/**',
@ -562,6 +563,11 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'tinycolor2/**'
],
'pdf-viewer.js': [
'amis-ui/lib/components/PdfViewer.js',
'pdfjs-dist/build/pdf.worker.min.js'
],
'cropperjs.js': ['cropperjs/**', 'react-cropper/**'],
'barcode.js': ['src/components/BarCode.tsx', 'jsbarcode/**'],
@ -584,6 +590,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!mpegts.js/**',
'!hls.js/**',
'!froala-editor/**',
'!pdfjs-dist/**',
'!amis-ui/lib/components/RichText.js',
'!zrender/**',

View File

@ -8,5 +8,5 @@
"packages/amis-editor"
],
"useWorkspaces": false,
"version": "6.1.0"
"version": "6.2.1"
}

View File

@ -43,7 +43,8 @@
"dependencies": {
"path-to-regexp": "^6.2.0",
"postcss": "^8.4.14",
"qs": "6.9.7"
"qs": "6.9.7",
"smooth-signature": "^1.0.13"
},
"devDependencies": {
"@babel/generator": "^7.22.9",
@ -97,6 +98,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-overlays": "5.1.1",
"rollup": "^2.79.1",
"rollup-pluginutils": "^2.8.2",
"setprototypeof": "^1.2.0",
"ts-jest": "^29.0.2",
@ -149,4 +151,4 @@
"printBasicPrototype": false
}
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "amis-core",
"version": "6.1.0",
"version": "6.2.1",
"description": "amis-core",
"main": "lib/index.js",
"module": "esm/index.js",
@ -19,6 +19,7 @@
"@types/jest": "^28.1.0",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"@types/react-is": "^18.2.4",
"immutable": "^4.1.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
@ -47,7 +48,7 @@
"esm"
],
"dependencies": {
"amis-formula": "^6.1.0",
"amis-formula": "*",
"classnames": "2.3.2",
"file-saver": "^2.0.2",
"hoist-non-react-statics": "^3.3.2",
@ -68,7 +69,8 @@
"peerDependencies": {
"amis-formula": "*",
"react": ">=16.8.6",
"react-dom": ">=16.8.6"
"react-dom": ">=16.8.6",
"react-is": ">=16.8.6"
},
"jest": {
"testEnvironment": "jsdom",
@ -104,4 +106,4 @@
]
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}
}

View File

@ -175,7 +175,7 @@ export class RootRenderer extends React.Component<RootRendererProps> {
window.open(mailto);
} else if (action.actionType === 'dialog') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store.openDialog(
ctx,
undefined,
@ -183,7 +183,7 @@ export class RootRenderer extends React.Component<RootRendererProps> {
delegate || (this.context as any)
);
} else if (action.actionType === 'drawer') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store.openDrawer(ctx, undefined, undefined, delegate);
} else if (action.actionType === 'toast') {
action.toast?.items?.forEach((item: any) => {
@ -211,7 +211,7 @@ export class RootRenderer extends React.Component<RootRendererProps> {
);
});
} else if (action.actionType === 'ajax') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store
.saveRemote(action.api as string, ctx, {
successMessage:
@ -341,11 +341,14 @@ export class RootRenderer extends React.Component<RootRendererProps> {
openFeedback(dialog: any, ctx: any) {
return new Promise(resolve => {
const store = this.store;
store.setCurrentAction({
type: 'button',
actionType: 'dialog',
dialog: dialog
});
store.setCurrentAction(
{
type: 'button',
actionType: 'dialog',
dialog: dialog
},
this.props.resolveDefinitions
);
store.openDialog(
ctx,
undefined,

View File

@ -1,6 +1,7 @@
import difference from 'lodash/difference';
import omit from 'lodash/omit';
import React from 'react';
import {isValidElementType} from 'react-is';
import LazyComponent from './components/LazyComponent';
import {
filterSchema,
@ -15,7 +16,7 @@ import {IScopedContext, ScopedContext} from './Scoped';
import {Schema, SchemaNode} from './types';
import {DebugWrapper} from './utils/debug';
import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {anyChanged, chainEvents, autobind, TestIdBuilder} from './utils/helper';
import {SimpleMap} from './utils/SimpleMap';
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';
import {isAlive} from 'mobx-state-tree';
@ -349,7 +350,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
statusStore,
dispatchEvent: this.dispatchEvent
});
} else if (typeof schema.component === 'function') {
} else if (schema.component && isValidElementType(schema.component)) {
const isSFC = !(schema.component.prototype instanceof React.Component);
const {
data: defaultData,
@ -475,8 +476,14 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
(props as any).static = isStatic;
}
if (rest.env.enableTestid && props.id && !props.testid) {
props.testid = props.id;
// 优先使用组件自己的testid或者id这个解决不了table行内的一些子元素
// 每一行都会出现这个testid的元素只在测试工具中直接使用nth拿序号
if (rest.env.enableTestid) {
if (props.testid || props.id || props.testIdBuilder == null) {
if (!(props.testIdBuilder instanceof TestIdBuilder)) {
props.testIdBuilder = new TestIdBuilder(props.testid || props.id);
}
}
}
// 自动解析变量模式,主要是方便直接引入第三方组件库,无需为了支持变量封装一层

View File

@ -344,6 +344,7 @@ export const runAction = async (
{
...action,
args,
rawData: actionConfig.data,
data: action.actionType === 'reload' ? actionData : data, // 如果是刷新动作则只传action.data
...key
},

View File

@ -76,7 +76,8 @@ export class DialogAction implements RendererAction {
{
actionType: 'dialog',
dialog: action.dialog,
reload: 'none'
reload: 'none',
data: action.rawData
},
action.data
);
@ -142,11 +143,20 @@ export class ConfirmAction implements RendererAction {
renderer: ListenerContext,
event: RendererEvent<any>
) {
const type = action.dialog?.type ?? (action.args as any)?.type;
let modal: any = action.dialog ?? action.args;
if (modal.$ref && renderer.props.resolveDefinitions) {
modal = {
...renderer.props.resolveDefinitions(modal.$ref),
...modal
};
}
const type = modal?.type;
if (!type) {
const confirmed = await event.context.env.confirm?.(
filter(action.dialog?.msg, event.data) || action.args?.msg,
filter(modal?.msg, event.data) || action.args?.msg,
filter(action.dialog?.title, event.data) || action.args?.title,
{
closeOnEsc:
@ -177,7 +187,8 @@ export class ConfirmAction implements RendererAction {
event,
{
actionType: 'dialog',
dialog: action.dialog ?? action.args,
dialog: modal,
data: action.rawData,
reload: 'none',
callback: (result: boolean) => resolve(result)
},

View File

@ -38,7 +38,8 @@ export class DrawerAction implements RendererAction {
{
actionType: 'drawer',
drawer: action.drawer,
reload: 'none'
reload: 'none',
data: action.rawData
},
action.data
);

View File

@ -0,0 +1,51 @@
import {printElements} from '../utils/printElement';
import {RendererEvent} from '../utils/renderer-event';
import {
RendererAction,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
export interface IPrintAction extends ListenerAction {
actionType: 'copy';
args: {
testid?: string;
testids?: string[];
};
}
/**
*
*
* @export
* @class PrintAction
* @implements {Action}
*/
export class PrintAction implements RendererAction {
async run(
action: IPrintAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (action.args?.testid) {
const element = document.querySelector(
`[data-testid='${action.args.testid}']`
);
if (element) {
printElements([element]);
}
} else if (action.args?.testids) {
const elements: Element[] = [];
action.args.testids.forEach(testid => {
const element = document.querySelector(`[data-testid='${testid}']`);
if (element) {
elements.push(element);
}
});
printElements(elements);
}
}
}
registerAction('print', new PrintAction());

View File

@ -19,5 +19,6 @@ import './EmailAction';
import './LinkAction';
import './ToastAction';
import './PageAction';
import './PrintAction';
export * from './Action';

View File

@ -9,7 +9,8 @@ import {
qsparse,
string2regExp,
parseQuery,
isMobile
isMobile,
TestIdBuilder
} from './utils/helper';
import {
fetcherResult,
@ -72,6 +73,7 @@ export interface RendererProps
env: RendererEnv;
$path: string; // 当前组件所在的层级信息
$schema: any; // 原始 schema 配置
testIdBuilder?: TestIdBuilder;
store?: IIRendererStore;
syncSuperStore?: boolean;
data: {

View File

@ -206,7 +206,8 @@ export {
splitTarget,
CustomStyle,
enableDebug,
disableDebug
disableDebug,
envOverwrite
};
export function render(

View File

@ -51,6 +51,7 @@ import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item';
import {injectObjectChain} from '../utils';
import {reaction} from 'mobx';
import groupBy from 'lodash/groupBy';
export interface FormHorizontal {
left?: number;
@ -461,7 +462,7 @@ export default class Form extends React.Component<FormProps, object> {
];
hooks: {
[propName: string]: Array<() => Promise<any>>;
[propName: string]: Array<(...args: any) => Promise<any>>;
} = {};
asyncCancel: () => void;
toDispose: Array<() => void> = [];
@ -762,8 +763,25 @@ export default class Form extends React.Component<FormProps, object> {
const initedAt = store.initedAt;
store.setInited(true);
const hooks: Array<(data: any) => Promise<any>> = this.hooks['init'] || [];
await Promise.all(hooks.map(hook => hook(data)));
const hooks = this.hooks['init'] || [];
const groupedHooks = groupBy(hooks, item =>
(item as any).__enforce === 'prev'
? 'prev'
: (item as any).__enforce === 'post'
? 'post'
: 'normal'
);
await Promise.all((groupedHooks.prev || []).map(hook => hook(data)));
// 有可能在前面的步骤中删除了钩子,所以需要重新验证一下
await Promise.all(
(groupedHooks.normal || []).map(
hook => hooks.includes(hook) && hook(data)
)
);
await Promise.all(
(groupedHooks.post || []).map(hook => hooks.includes(hook) && hook(data))
);
if (!isAlive(store)) {
return;
@ -976,9 +994,15 @@ export default class Form extends React.Component<FormProps, object> {
store.reset(onReset);
}
addHook(fn: () => any, type: 'validate' | 'init' | 'flush' = 'validate') {
addHook(
fn: () => any,
type: 'validate' | 'init' | 'flush' = 'validate',
enforce?: 'prev' | 'post'
) {
this.hooks[type] = this.hooks[type] || [];
this.hooks[type].push(type === 'flush' ? fn : promisify(fn));
const hook = type === 'flush' ? fn : promisify(fn);
(hook as any).__enforce = enforce;
this.hooks[type].push(hook);
return () => {
this.removeHook(fn, type);
fn = noop;
@ -1205,7 +1229,7 @@ export default class Form extends React.Component<FormProps, object> {
return;
}
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
if (action.actionType === 'reset-and-submit') {
store.reset(this.handleReset(action));
@ -1372,16 +1396,16 @@ export default class Form extends React.Component<FormProps, object> {
}
});
} else if (action.type === 'reset' || action.actionType === 'reset') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store.reset(onReset);
} else if (action.actionType === 'clear') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store.clear(onReset);
} else if (action.actionType === 'validate') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
return this.validate(true, throwErrors, true, true);
} else if (action.actionType === 'dialog') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store.openDialog(
data,
undefined,
@ -1389,10 +1413,10 @@ export default class Form extends React.Component<FormProps, object> {
delegate || (this.context as any)
);
} else if (action.actionType === 'drawer') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
store.openDrawer(data);
} else if (action.actionType === 'ajax') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
if (!isEffectiveApi(action.api)) {
return env.alert(__(`当 actionType 为 ajax 时,请设置 api 属性`));
}
@ -1447,7 +1471,7 @@ export default class Form extends React.Component<FormProps, object> {
}
});
} else if (action.actionType === 'reload') {
store.setCurrentAction(action);
store.setCurrentAction(action, this.props.resolveDefinitions);
if (action.target) {
this.reloadTarget(filterTarget(action.target, data), data);
} else {
@ -1558,11 +1582,14 @@ export default class Form extends React.Component<FormProps, object> {
openFeedback(dialog: any, ctx: any) {
return new Promise(resolve => {
const {store} = this.props;
store.setCurrentAction({
type: 'button',
actionType: 'dialog',
dialog: dialog
});
store.setCurrentAction(
{
type: 'button',
actionType: 'dialog',
dialog: dialog
},
this.props.resolveDefinitions
);
store.openDialog(
ctx,
undefined,
@ -1808,7 +1835,8 @@ export default class Form extends React.Component<FormProps, object> {
render,
staticClassName,
static: isStatic = false,
loadingConfig
loadingConfig,
testid
} = this.props;
const {restError} = store;

View File

@ -33,11 +33,19 @@ import {wrapControl} from './wrapControl';
import debounce from 'lodash/debounce';
import {isApiOutdated, isEffectiveApi} from '../utils/api';
import {findDOMNode} from 'react-dom';
import {dataMapping, setThemeClassName} from '../utils';
import {
dataMapping,
getTreeAncestors,
isEmpty,
keyToPath,
setThemeClassName,
setVariable
} from '../utils';
import Overlay from '../components/Overlay';
import PopOver from '../components/PopOver';
import CustomStyle from '../components/CustomStyle';
import classNames from 'classnames';
import isPlainObject from 'lodash/isPlainObject';
export type LabelAlign = 'right' | 'left';
@ -383,6 +391,75 @@ export interface FormBaseControl extends BaseSchemaWithoutType {
*
*/
validateApi?: string | BaseApiObject;
/**
*
*
*/
autoFill?:
| {
[propName: string]: string;
}
| {
/**
*
*/
showSuggestion?: boolean;
/**
* api
*/
api?: BaseApiObject | string;
/**
*
* @default true
*/
silent?: boolean;
/**
*
*/
fillMappinng?: {
[propName: string]: any;
};
/**
* change
*/
trigger?: 'change' | 'foucs';
/**
*
*/
mode?: 'popOver' | 'dialog' | 'drawer';
/**
*
*/
position?: string;
/**
*
*/
size?: string;
/**
*
*/
columns?: Array<any>;
/**
*
*/
filter?: any;
};
/**
* @default fillIfNotSet
*
*/
initAutoFill?: boolean | 'fillIfNotSet';
}
export interface FormItemBasicConfig extends Partial<RendererConfig> {
@ -439,7 +516,11 @@ export interface FormItemProps extends RendererProps {
values: {[propName: string]: any},
submitOnChange?: boolean
) => void;
addHook: (fn: Function, mode?: 'validate' | 'init' | 'flush') => () => void;
addHook: (
fn: Function,
mode?: 'validate' | 'init' | 'flush',
enforce?: 'prev' | 'post'
) => () => void;
removeHook: (fn: Function, mode?: 'validate' | 'init' | 'flush') => void;
renderFormItems: (
schema: Partial<FormSchemaBase>,
@ -471,7 +552,6 @@ export interface FormItemProps extends RendererProps {
// error string
error?: string;
showErrorMsg?: boolean;
testid?: string;
}
// 下发下去的属性
@ -525,9 +605,12 @@ const getItemInputClassName = (props: FormItemProps) => {
};
export class FormItemWrap extends React.Component<FormItemProps> {
reaction: Array<() => void> = [];
lastSearchTerm: any;
target: HTMLElement;
mounted = false;
initedOptionFilled = false;
initedApiFilled = false;
toDispose: Array<() => void> = [];
constructor(props: FormItemProps) {
super(props);
@ -536,28 +619,66 @@ export class FormItemWrap extends React.Component<FormItemProps> {
isOpened: false
};
const {formItem: model} = props;
const {formItem: model, formInited, addHook, initAutoFill} = props;
if (!model) {
return;
}
if (model) {
this.reaction.push(
reaction(
() => `${model.errors.join('')}${model.isFocused}${model.dialogOpen}`,
() => this.forceUpdate()
)
);
this.reaction.push(
reaction(
() => model?.filteredOptions,
() => this.forceUpdate()
)
);
this.reaction.push(
this.toDispose.push(
reaction(
() =>
`${model.errors.join('')}${model.isFocused}${
model.dialogOpen
}${JSON.stringify(model.filteredOptions)}`,
() => this.forceUpdate()
)
);
let onInit = () => {
this.initedOptionFilled = true;
initAutoFill !== false &&
this.syncOptionAutoFill(
model.getSelectedOptions(model.tmpValue),
initAutoFill === 'fillIfNotSet'
);
this.initedApiFilled = true;
initAutoFill !== false &&
this.syncApiAutoFill(
model.tmpValue ?? '',
false,
initAutoFill === 'fillIfNotSet'
);
this.toDispose.push(
reaction(
() => JSON.stringify(model.tmpValue),
() => this.syncAutoFill(model.tmpValue)
() =>
this.mounted &&
this.initedApiFilled &&
this.syncApiAutoFill(model.tmpValue)
)
);
}
this.toDispose.push(
reaction(
() => JSON.stringify(model.getSelectedOptions(model.tmpValue)),
() =>
this.mounted &&
this.initedOptionFilled &&
this.syncOptionAutoFill(model.getSelectedOptions(model.tmpValue))
)
);
};
this.toDispose.push(
formInited || !addHook
? model.addInitHook(onInit, 999)
: addHook(onInit, 'init', 'post')
);
}
componentDidMount() {
this.mounted = true;
this.target = findDOMNode(this) as HTMLElement;
}
componentDidUpdate(prevProps: FormItemProps) {
@ -573,18 +694,15 @@ export class FormItemWrap extends React.Component<FormItemProps> {
props.data
)
) {
this.syncAutoFill(model?.tmpValue, true);
this.syncApiAutoFill(model?.tmpValue, true);
}
}
componentDidMount() {
this.target = findDOMNode(this) as HTMLElement;
}
componentWillUnmount() {
this.reaction.forEach(fn => fn());
this.reaction = [];
this.syncAutoFill.cancel();
this.syncApiAutoFill.cancel();
this.mounted = false;
this.toDispose.forEach(fn => fn());
this.toDispose = [];
}
@autobind
@ -622,7 +740,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
trigger === type &&
(mode === 'dialog' || mode === 'drawer')
) {
formItem?.openDialog(this.buildSchema(), data, result => {
formItem?.openDialog(this.buildAutoFillSchema(), data, result => {
if (!result?.selectedItems) {
return;
}
@ -651,44 +769,76 @@ export class FormItemWrap extends React.Component<FormItemProps> {
onBulkChange?.(responseData);
}
syncAutoFill = debounce(
(term: any, reload?: boolean) => {
(async (term: string, reload?: boolean) => {
syncApiAutoFill = debounce(
async (term: any, forceLoad?: boolean, skipIfExits = false) => {
try {
const {autoFill, onBulkChange, formItem, data} = this.props;
// 参照录入
if (!autoFill || (autoFill && !autoFill?.hasOwnProperty('api'))) {
if (
!onBulkChange ||
!formItem ||
!autoFill ||
(autoFill && !autoFill?.hasOwnProperty('api'))
) {
return;
} else if (
skipIfExits &&
(!autoFill.fillMapping ||
Object.keys(autoFill.fillMapping).some(
key => typeof getVariable(data, key) !== 'undefined'
))
) {
// 只要目标填充值有一个有值,就初始不自动填充
return;
}
if (autoFill?.showSuggestion) {
this.handleAutoFill('change');
} else {
// 自动填充
const itemName = formItem?.name;
const itemName = formItem.name;
const ctx = createObject(data, {
[itemName || '']: term
[itemName || '']: term,
__term: term
});
if (
(onBulkChange &&
isEffectiveApi(autoFill.api, ctx) &&
this.lastSearchTerm !== term) ||
reload
forceLoad ||
(isEffectiveApi(autoFill.api, ctx) && this.lastSearchTerm !== term)
) {
let result = await formItem?.loadAutoUpdateData(
let result = await formItem.loadAutoUpdateData(
autoFill.api,
ctx,
!!(autoFill.api as BaseApiObject)?.silent
);
this.lastSearchTerm =
(result && getVariable(result, itemName)) ?? term;
// 如果没有返回不应该处理
if (!result) {
return;
}
if (autoFill?.fillMapping) {
result = dataMapping(autoFill.fillMapping, result);
}
result && onBulkChange?.(result);
if (result) {
// 不能把自己给清了吧
setVariable(
result,
itemName,
getVariable(result, itemName) || formItem.tmpValue
);
onBulkChange?.(result);
}
}
}
})(term, reload).catch(e => console.error(e));
} catch (e) {
console.error(e);
}
},
250,
{
@ -697,7 +847,81 @@ export class FormItemWrap extends React.Component<FormItemProps> {
}
);
buildSchema() {
syncOptionAutoFill(selectedOptions: Array<any>, skipIfExits = false) {
const {autoFill, multiple, onBulkChange, data} = this.props;
const formItem = this.props.formItem as IFormItemStore;
// 参照录入|自动填充
if (autoFill?.hasOwnProperty('api')) {
return;
}
if (
onBulkChange &&
autoFill &&
!isEmpty(autoFill) &&
formItem.filteredOptions.length
) {
const toSync = dataMapping(
autoFill,
multiple
? {
items: selectedOptions.map(item =>
createObject(
{
...data,
ancestors: getTreeAncestors(
formItem.filteredOptions,
item,
true
)
},
item
)
)
}
: createObject(
{
...data,
ancestors: getTreeAncestors(
formItem.filteredOptions,
selectedOptions[0],
true
)
},
selectedOptions[0]
)
);
const tmpData = {...data};
const result = {...toSync};
Object.keys(autoFill).forEach(key => {
const keys = keyToPath(key);
let value = getVariable(toSync, key);
if (skipIfExits) {
const originValue = getVariable(data, key);
if (typeof originValue !== 'undefined') {
value = originValue;
}
}
setVariable(result, key, value);
// 如果左边的 key 是一个路径
// 这里不希望直接把原始对象都给覆盖没了
// 而是保留原始的对象,只修改指定的属性
if (keys.length > 1 && isPlainObject(tmpData[keys[0]])) {
// 存在情况依次更新同一子路径的多个keyeg: a.b.c1 和 a.b.c2所以需要同步更新data
setVariable(tmpData, key, value);
result[keys[0]] = tmpData[keys[0]];
}
});
onBulkChange(result);
}
}
buildAutoFillSchema() {
const {
render,
autoFill,
@ -775,26 +999,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
}
]
};
const schema = {
type: mode,
className: 'auto-fill-dialog',
title: __('FormItem.autoFillSuggest'),
size,
body: form,
actions: [
{
type: 'button',
actionType: 'cancel',
label: __('cancel')
},
{
type: 'submit',
actionType: 'submit',
level: 'primary',
label: __('confirm')
}
]
};
if (mode === 'popOver') {
return (
<Overlay
@ -805,7 +1010,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
>
<PopOver
classPrefix={ns}
className={cx(`${ns}auto-fill-popOver`, popOverClassName)}
className={cx(`${ns}Autofill-popOver`, popOverClassName)}
style={{
minWidth: this.target ? this.target.offsetWidth : undefined
}}
@ -821,7 +1026,29 @@ export class FormItemWrap extends React.Component<FormItemProps> {
</Overlay>
);
} else {
return schema;
return {
type: mode,
className: 'auto-fill-dialog',
title: __('FormItem.autoFillSuggest'),
size,
body: {
...form,
wrapWithPanel: false
},
actions: [
{
type: 'button',
actionType: 'cancel',
label: __('cancel')
},
{
type: 'submit',
actionType: 'submit',
level: 'primary',
label: __('confirm')
}
]
};
}
}
@ -1738,7 +1965,8 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
return wrapControl(
hoistNonReactStatic(
class extends FormItemWrap {
static defaultProps = {
static defaultProps: any = {
initAutoFill: 'fillIfNotSet',
className: '',
renderLabel: config.renderLabel,
renderDescription: config.renderDescription,
@ -1757,14 +1985,14 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
...((Control as any).propsList || [])
];
static displayName = `FormItem${
static displayName: string = `FormItem${
config.type ? `(${config.type})` : ''
}`;
static ComposedComponent = Control;
ref: any;
constructor(props: FormItemProps) {
constructor(props: FormControlProps) {
super(props);
this.refFn = this.refFn.bind(this);
@ -1859,7 +2087,7 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
getItemInputClassName(this.props)
)}
></Control>
{isOpened ? this.buildSchema() : null}
{isOpened ? this.buildAutoFillSchema() : null}
</>
);
}

View File

@ -196,19 +196,6 @@ export interface FormOptionsControl extends FormBaseControl {
*
*/
deleteConfirmText?: string;
/**
*
*/
autoFill?: {
[propName: string]: string;
};
/**
* @default fillIfNotSet
*
*/
initAutoFill?: boolean | 'fillIfNotSet';
}
export interface OptionsBasicConfig extends FormItemBasicConfig {
@ -309,7 +296,6 @@ export function registerOptionsControl(config: OptionsConfig) {
placeholder: 'Select.placeholder',
resetValue: '',
deleteConfirmText: 'deleteConfirm',
initAutoFill: 'fillIfNotSet',
...Control.defaultProps
};
static propsList: any = (Control as any).propsList
@ -321,7 +307,6 @@ export function registerOptionsControl(config: OptionsConfig) {
input: any;
mounted = false;
initedFilled = false;
constructor(props: OptionsProps) {
super(props);
@ -344,63 +329,36 @@ export function registerOptionsControl(config: OptionsConfig) {
defaultCheckAll
} = props;
if (formItem) {
formItem.setOptions(
normalizeOptions(options, undefined, valueField),
this.changeOptionValue,
data
);
if (!formItem) {
return;
}
this.toDispose.push(
reaction(
() => JSON.stringify([formItem.loading, formItem.filteredOptions]),
() => this.mounted && this.forceUpdate()
)
);
formItem.setOptions(
normalizeOptions(options, undefined, valueField),
this.changeOptionValue,
data
);
this.toDispose.push(
reaction(
() =>
JSON.stringify(formItem.getSelectedOptions(formItem.tmpValue)),
() =>
this.mounted &&
this.initedFilled &&
this.syncAutoFill(formItem.getSelectedOptions(formItem.tmpValue))
)
);
this.toDispose.push(
reaction(
() => JSON.stringify([formItem.loading, formItem.filteredOptions]),
() => this.mounted && this.forceUpdate()
)
);
if (formInited || !addHook) {
this.initedFilled = true;
this.props.initAutoFill !== false &&
this.syncAutoFill(
formItem.getSelectedOptions(formItem.tmpValue),
this.props.initAutoFill === 'fillIfNotSet'
);
} else if (addHook) {
addHook(() => {
this.initedFilled = true;
this.props.initAutoFill !== false &&
this.syncAutoFill(
formItem.getSelectedOptions(formItem.tmpValue),
this.props.initAutoFill === 'fillIfNotSet'
);
}, 'init');
}
// 默认全选。这里会和默认值\回填值逻辑冲突所以如果有配置source则不执行默认全选
if (
multiple &&
defaultCheckAll &&
formItem.filteredOptions?.length &&
!source
) {
this.defaultCheckAll();
}
// 默认全选。这里会和默认值\回填值逻辑冲突所以如果有配置source则不执行默认全选
if (
multiple &&
defaultCheckAll &&
formItem.filteredOptions?.length &&
!source
) {
this.defaultCheckAll();
}
let loadOptions: boolean = initFetch !== false;
if (formItem && joinValues === false && defaultValue) {
if (joinValues === false && defaultValue) {
const selectedOptions = extractValue
? formItem
.getSelectedOptions(value)
@ -416,9 +374,11 @@ export function registerOptionsControl(config: OptionsConfig) {
loadOptions &&
config.autoLoadOptionsFromSource !== false &&
(formInited || !addHook
? this.reload()
: addHook && addHook(this.initOptions, 'init'));
this.toDispose.push(
formInited || !addHook
? formItem.addInitHook(this.reload)
: addHook(this.initOptions, 'init')
);
}
componentDidMount() {
@ -507,6 +467,7 @@ export function registerOptionsControl(config: OptionsConfig) {
componentWillUnmount() {
this.props.removeHook?.(this.reload, 'init');
this.mounted = false;
this.toDispose.forEach(fn => fn());
this.toDispose = [];
}
@ -549,80 +510,6 @@ export function registerOptionsControl(config: OptionsConfig) {
}
}
syncAutoFill(selectedOptions: Array<any>, skipIfExits = false) {
const {autoFill, multiple, onBulkChange, data} = this.props;
const formItem = this.props.formItem as IFormItemStore;
// 参照录入|自动填充
if (autoFill?.hasOwnProperty('api')) {
return;
}
if (
onBulkChange &&
autoFill &&
!isEmpty(autoFill) &&
formItem.filteredOptions.length
) {
const toSync = dataMapping(
autoFill,
multiple
? {
items: selectedOptions.map(item =>
createObject(
{
...data,
ancestors: getTreeAncestors(
formItem.filteredOptions,
item,
true
)
},
item
)
)
}
: createObject(
{
...data,
ancestors: getTreeAncestors(
formItem.filteredOptions,
selectedOptions[0],
true
)
},
selectedOptions[0]
)
);
const tmpData = {...data};
const result = {...toSync};
Object.keys(autoFill).forEach(key => {
const keys = keyToPath(key);
let value = getVariable(toSync, key);
if (skipIfExits) {
const originValue = getVariable(data, key);
if (typeof originValue !== 'undefined') {
value = originValue;
}
}
setVariable(result, key, value);
// 如果左边的 key 是一个路径
// 这里不希望直接把原始对象都给覆盖没了
// 而是保留原始的对象,只修改指定的属性
if (keys.length > 1 && isPlainObject(tmpData[keys[0]])) {
// 存在情况依次更新同一子路径的多个keyeg: a.b.c1 和 a.b.c2所以需要同步更新data
setVariable(tmpData, key, value);
result[keys[0]] = tmpData[keys[0]];
}
});
onBulkChange(result);
}
}
// 当前值,跟设置预期的值格式不一致时自动转换。
normalizeValue() {
const {

View File

@ -63,7 +63,7 @@ export interface ControlOutterProps extends RendererProps {
submitOnChange?: boolean;
validate?: (value: any, values: any, name: string) => any;
formItem?: IFormItemStore;
addHook?: (fn: () => any, type?: 'validate' | 'init' | 'flush') => void;
addHook?: (fn: () => any, type?: 'validate' | 'init' | 'flush') => () => void;
removeHook?: (fn: () => any, type?: 'validate' | 'init' | 'flush') => void;
$schema: {
pipeIn?: (value: any, data: any) => any;
@ -306,6 +306,8 @@ export function wrapControl<
};
addHook?.(this.hook2);
}
formItem?.init();
}
componentDidUpdate(prevProps: OuterProps) {
@ -468,7 +470,7 @@ export function wrapControl<
setInitialValue(value: any) {
const model = this.model!;
const {formStore: form, canAccessSuperData, data} = this.props;
const {formStore: form, data, canAccessSuperData} = this.props;
const isExp = isExpression(value);
if (isExp) {

View File

@ -548,11 +548,17 @@ export const FormStore = ServiceStore.named('FormStore')
self.submiting = true;
try {
yield validate(hooks, true, true, failedMessage, validateErrCb);
const valid = yield validate(
hooks,
true,
true,
failedMessage,
validateErrCb
);
if (fn) {
const diff = difference(self.data, self.pristine);
const result = yield fn(
const result: any = yield fn(
createObject(
createObject(self.data.__super, {
diff: diff,
@ -578,7 +584,7 @@ export const FormStore = ServiceStore.named('FormStore')
failedMessage?: string,
validateErrCb?: () => void
) => Promise<boolean> = flow(function* validate(
hooks?: Array<() => Promise<any>>,
hooks?: Array<(data: any) => Promise<any>>,
forceValidate?: boolean,
throwErrors?: boolean,
failedMessage?: string,
@ -624,10 +630,30 @@ export const FormStore = ServiceStore.named('FormStore')
}
}
if (hooks && hooks.length) {
for (let i = 0, len = hooks.length; i < len; i++) {
yield hooks[i]();
try {
if (hooks && hooks.length) {
for (let i = 0, len = hooks.length; i < len; i++) {
const msg = yield hooks[i](self.data);
if (typeof msg == 'string' && msg) {
throw new Error(msg);
} else if (msg === false) {
// 不提示直接不通过校验
throw new ValidateError(
failedMessage || self.__('Form.validateFailed'),
self.errors
);
}
}
}
} catch (e) {
if (throwErrors) {
throw e;
} else {
toastValidateError(e.message);
}
return false;
}
if (!self.valid) {

View File

@ -28,7 +28,8 @@ import {
eachTree,
mapTree,
setVariable,
cloneObject
cloneObject,
promisify
} from '../utils/helper';
import {flattenTree} from '../utils/helper';
import find from 'lodash/find';
@ -91,6 +92,7 @@ export const FormItemStore = StoreNode.named('FormItemStore')
itemId: '', // 因为 name 可能会重名,所以加个 id 进来,如果有需要用来定位具体某一个
unsetValueOnInvisible: false,
itemsRef: types.optional(types.array(types.string), []),
inited: false,
validated: false,
validating: false,
multiple: false,
@ -318,6 +320,8 @@ export const FormItemStore = StoreNode.named('FormItemStore')
const dialogCallbacks = new SimpleMap<(result?: any) => void>();
let loadAutoUpdateCancel: Function | null = null;
const initHooks: Array<(store: any) => any> = [];
function config({
name,
extraName,
@ -1495,6 +1499,19 @@ export const FormItemStore = StoreNode.named('FormItemStore')
self.isControlled = !!value;
}
const init: () => Promise<void> = flow(function* init() {
const hooks = initHooks.sort(
(a: any, b: any) => (a.__weight || 0) - (b.__weight || 0)
);
try {
for (let hook of hooks) {
yield hook(self);
}
} finally {
self.inited = true;
}
});
return {
focus,
blur,
@ -1523,7 +1540,24 @@ export const FormItemStore = StoreNode.named('FormItemStore')
addSubFormItem,
removeSubFormItem,
loadAutoUpdateData,
setIsControlled
setIsControlled,
init,
addInitHook(fn: (store: any) => any, weight = 0) {
fn = promisify(fn);
initHooks.push(fn);
(fn as any).__weight = weight;
return () => {
const idx = initHooks.indexOf(fn);
~idx && initHooks.splice(idx, 1);
};
},
beforeDestroy: () => {
// 销毁
initHooks.splice(0, initHooks.length);
}
};
});

View File

@ -155,7 +155,21 @@ export const iRendererStore = StoreNode.named('iRendererStore')
self.data = data;
},
setCurrentAction(action: object) {
setCurrentAction(action: any, resolveDefinitions?: (schema: any) => any) {
// 处理 $ref
resolveDefinitions &&
['dialog', 'drawer'].forEach(key => {
if (action[key]?.$ref) {
action = {
...action,
[key]: {
...resolveDefinitions(action[key].$ref),
...action[key]
}
};
}
});
self.action = action;
self.dialogData = false;
self.drawerOpen = false;
@ -174,11 +188,11 @@ export const iRendererStore = StoreNode.named('iRendererStore')
}
const data = createObjectFromChain(chain);
if (self.action.dialog && self.action.dialog.data) {
const mappingData = self.action.data ?? self.action.dialog?.data;
if (mappingData) {
self.dialogData = createObjectFromChain([
top?.context,
dataMapping(self.action.dialog.data, data)
dataMapping(mappingData, data)
]);
const clonedAction = {
@ -223,10 +237,11 @@ export const iRendererStore = StoreNode.named('iRendererStore')
const data = createObjectFromChain(chain);
if (self.action.drawer.data) {
const mappingData = self.action.data ?? self.action.drawer.data;
if (mappingData) {
self.drawerData = createObjectFromChain([
top?.context,
dataMapping(self.action.drawer.data, data)
dataMapping(mappingData, data)
]);
const clonedAction = {

View File

@ -6,6 +6,8 @@ import {ServerError} from '../utils/errors';
import {normalizeApiResponseData} from '../utils/api';
import {replaceText} from '../utils/replaceText';
import {concatData} from '../utils/concatData';
import {envOverwrite} from '../envOverwrite';
import {filter} from '../utils';
export const ServiceStore = iRendererStore
.named('ServiceStore')
@ -54,7 +56,7 @@ export const ServiceStore = iRendererStore
}
function updateMessage(msg?: string, error: boolean = false) {
self.msg = (msg && String(msg)) || '';
self.msg = (msg && filter(msg, self.data)) || '';
self.error = error;
}
@ -445,6 +447,7 @@ export const ServiceStore = iRendererStore
} else {
if (json.data) {
const env = getEnv(self);
json.data = envOverwrite(json.data, env.locale);
json.data = replaceText(
json.data,
env.replaceText,

View File

@ -1288,7 +1288,7 @@ export const TableStore = iRendererStore
typeof column.pristine.width === 'number'
? `width: ${column.pristine.width}px;`
: column.pristine.width
? `width: ${column.pristine.width};`
? `width: ${column.pristine.width};min-width: ${column.pristine.width};`
: '' // todo 可能需要让修改过列宽的保持相应宽度,目前这样相当于重置了
}`;
});

View File

@ -2,6 +2,7 @@
import type {JSONSchema7} from 'json-schema';
import {ListenerAction} from './actions/Action';
import {debounceConfig, trackConfig} from './utils/renderer-event';
import type {TestIdBuilder} from './utils/helper';
export interface Option {
/**
@ -571,6 +572,10 @@ export type SchemaClassName =
[propName: string]: boolean | undefined | null | SchemaExpression;
};
export interface BaseSchemaWithoutType {
/**
* id json
*/
$$id?: string;
/**
* css
*/
@ -689,6 +694,8 @@ export interface BaseSchemaWithoutType {
*
*/
useMobileUI?: boolean;
testIdBuilder?: TestIdBuilder;
}
export type OperatorType =

View File

@ -2,3 +2,10 @@ export const chromeVersion = (function getChromeVersion() {
const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
})();
export const isSafari =
navigator.vendor &&
navigator.vendor.indexOf('Apple') > -1 &&
navigator.userAgent &&
navigator.userAgent.indexOf('CriOS') == -1 &&
navigator.userAgent.indexOf('FxiOS') == -1;

View File

@ -2282,18 +2282,49 @@ export function replaceUrlParams(path: string, params: Record<string, any>) {
export const TEST_ID_KEY: 'data-testid' = 'data-testid';
export function buildTestId(testid?: string, data?: PlainObject) {
if (!testid) {
return {};
}
return {
[TEST_ID_KEY]: filter(testid, data)
};
}
export class TestIdBuilder {
testId?: string;
export function getTestId(testid?: string, data?: PlainObject) {
if (!testid) {
return undefined;
static fast(testId: string) {
return {
[TEST_ID_KEY]: testId
};
}
// 为空就表示没有启用testId后续一直返回都将是空
constructor(testId?: string) {
this.testId = testId;
}
// 生成子区域的testid生成器
getChild(childPath: string | number, data?: object) {
if (this.testId == null) {
return new TestIdBuilder();
}
return new TestIdBuilder(
data
? filter(`${this.testId}-${childPath}`, data)
: `${this.testId}-${childPath}`
);
}
// 获取当前组件的testid
getTestId(data?: object) {
if (this.testId == null) {
return undefined;
}
return {
[TEST_ID_KEY]: data ? filter(this.testId, data) : this.testId
};
}
getTestIdValue(data?: object) {
if (this.testId == null) {
return undefined;
}
return data ? filter(this.testId, data) : this.testId;
}
return buildTestId(testid, data)[TEST_ID_KEY];
}

View File

@ -0,0 +1,76 @@
/**
* https://github.com/szepeshazi/print-elements 里的实现
*
*
*/
const hideFromPrintClass = 'pe-no-print';
const preservePrintClass = 'pe-preserve-print';
const preserveAncestorClass = 'pe-preserve-ancestor';
const bodyElementName = 'BODY';
function hide(element: Element) {
if (!element.classList.contains(preservePrintClass)) {
element.classList.add(hideFromPrintClass);
}
}
function preserve(element: Element, isStartingElement: boolean) {
element.classList.remove(hideFromPrintClass);
element.classList.add(preservePrintClass);
if (!isStartingElement) {
element.classList.add(preserveAncestorClass);
}
}
function clean(element: Element) {
element.classList.remove(hideFromPrintClass);
element.classList.remove(preservePrintClass);
element.classList.remove(preserveAncestorClass);
}
function walkSiblings(element: Element, callback: (element: Element) => void) {
let sibling = element.previousElementSibling;
while (sibling) {
callback(sibling);
sibling = sibling.previousElementSibling;
}
sibling = element.nextElementSibling;
while (sibling) {
callback(sibling);
sibling = sibling.nextElementSibling;
}
}
function attachPrintClasses(element: Element, isStartingElement: boolean) {
preserve(element, isStartingElement);
walkSiblings(element, hide);
}
function cleanup(element: Element, isStartingElement: boolean) {
clean(element);
walkSiblings(element, clean);
}
function walkTree(
element: Element,
callback: (element: Element, isStartingElement: boolean) => void
) {
let currentElement: Element | null = element;
callback(currentElement, true);
currentElement = currentElement.parentElement;
while (currentElement && currentElement.nodeName !== bodyElementName) {
callback(currentElement, false);
currentElement = currentElement.parentElement;
}
}
export function printElements(elements: Element[]) {
for (let i = 0; i < elements.length; i++) {
walkTree(elements[i], attachPrintClasses);
}
window.print();
for (let i = 0; i < elements.length; i++) {
walkTree(elements[i], cleanup);
}
}

View File

@ -1,3 +1,5 @@
import moment from 'moment';
/**
*
*
@ -142,7 +144,9 @@ function verifyRegion(province: string, city: string, country: string) {
}
function verifyBirthday(birthday: any) {
return !isNaN(+birthday);
const min = moment(new Date('1850-01-01')); // 还有1850年前的人活着吗
const max = moment().endOf('day'); // 最大值是今天
return !isNaN(+birthday) && moment(birthday).isBetween(min, max);
}
/**

View File

@ -1,6 +1,6 @@
{
"name": "amis-editor-core",
"version": "6.1.0",
"version": "6.2.1",
"description": "amis 可视化编辑器",
"main": "lib/index.js",
"module": "esm/index.js",

View File

@ -175,6 +175,11 @@
height: 48px;
color: #151b26;
}
&.editor-tab-s-icon > svg {
width: 16px;
height: 16px;
}
}
}

View File

@ -224,3 +224,59 @@
perspective: 1000px;
}
}
.ae-DialogList {
&-wrap {
padding: 0 10px;
}
list-style: none;
margin: 8px 0 0;
padding: 0;
li {
cursor: pointer;
margin: 4px 0;
padding: 4px 10px;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--colors-neutral-text-2);
a {
font-size: inherit;
color: var(--icon-color);
text-decoration: none;
cursor: pointer;
&:hover {
color: var(--icon-onHover-color);
background: var(--colors-neutral-bg-2);
}
}
}
// li + li {
// border-top: var(--borderWidth) solid var(--borderColor);
// }
li:hover {
color: var(--Layout-fontColor--onHover);
background: $hover-bg-color;
// border-top: var(--borderWidth) solid var(--borderColor);
// border-bottom: var(--borderWidth) solid var(--borderColor);
}
&-placeholder {
color: #b4b6ba;
padding-top: px2rem(10px);
text-align: center;
vertical-align: middle;
}
}

View File

@ -237,7 +237,7 @@ $tooltip-bottom: '[data-tooltip][data-position=' bottom ']:hover:after';
.config-form-content {
position: relative;
height: 100%;
padding: 16px 12px;
padding: 10px 12px;
// 带底部操作按钮的属性配置面板
&.with-actions {

View File

@ -309,6 +309,10 @@
height: calc(100% - 88px);
border-top: 1px solid #e8e9eb;
.action-tree-control {
& > div {
max-height: 100%;
}
height: 100%;
max-height: 100%;
padding: 0;

View File

@ -64,6 +64,7 @@
align-items: unset;
border-top: 1px solid #e5e5e5;
background: #f2f2f4;
border-radius: 0 !important;
@include collapse-header-text();
i {

View File

@ -54,6 +54,7 @@
@import './style-control/size';
@import './style-control/style-common';
@import './style-control/theme-css-code';
@import './style-control/flex-layout';
/* 组件样式 */
@import './components/button';
@ -1071,7 +1072,7 @@
.ae-CodePanel {
// 左侧面板Header
.panel-header {
margin: 12px 0;
margin: 10px 0;
flex: 0 0 22px;
padding: 0 12px;
font-family: PingFangSC-Medium;

View File

@ -0,0 +1,29 @@
.ae-FlexLayout {
&-wrap {
display: grid;
grid-row-gap: 10px;
grid-column-gap: 10px;
grid-template-columns: repeat(4, auto);
margin-bottom: 10px;
}
&-item {
display: flex;
flex-direction: row;
gap: 2px;
height: 36px;
border: 1px solid rgba(0, 0, 0, 0.2);
padding: 4px;
border-radius: 4px;
cursor: pointer;
&.active {
border-color: #528eff;
}
}
&-itemColumn {
height: 100%;
background-color: #999;
}
}

View File

@ -10,7 +10,7 @@ import {PluginEventListener, RendererPluginAction} from '../plugin';
import {reGenerateID} from '../util';
import {SubEditor} from './SubEditor';
import Breadcrumb from './Breadcrumb';
import {destroy} from 'mobx-state-tree';
import {destroy, isAlive} from 'mobx-state-tree';
import {ScaffoldModal} from './ScaffoldModal';
import {PopOverForm} from './PopOverForm';
import {ContextMenuPanel} from './Panel/ContextMenuPanel';
@ -255,7 +255,7 @@ export default class Editor extends Component<EditorProps> {
this.toDispose.forEach(fn => fn());
this.toDispose = [];
this.manager.dispose();
destroy(this.store);
setTimeout(() => destroy(this.store), 4);
}
// 快捷功能键

View File

@ -7,6 +7,7 @@ import {Icon} from 'amis';
import {autobind, noop} from '../util';
import {PluginEvent, ResizeMoveEventContext} from '../plugin';
import {EditorManager} from '../manager';
import {isAlive} from 'mobx-state-tree';
export interface HighlightBoxProps {
store: EditorStoreType;
@ -20,315 +21,271 @@ export interface HighlightBoxProps {
children?: React.ReactNode;
}
@observer
export default class HighlightBox extends React.Component<HighlightBoxProps> {
mainRef = React.createRef<HTMLDivElement>();
export default observer(function ({
className,
store,
id,
title,
children,
node,
toolbarContainer,
onSwitch,
manager
}: HighlightBoxProps) {
const handleWResizerMouseDown = React.useCallback(
(e: MouseEvent) => startResize(e, 'horizontal'),
[]
);
@autobind
handleWResizerMouseDown(e: MouseEvent) {
return this.startResize(e, 'horizontal');
}
const handleHResizerMouseDown = React.useCallback(
(e: MouseEvent) => startResize(e, 'vertical'),
[]
);
@autobind
handleHResizerMouseDown(e: MouseEvent) {
return this.startResize(e, 'vertical');
}
const handleResizerMouseDown = React.useCallback(
(e: MouseEvent) => startResize(e, 'both'),
[]
);
@autobind
handleResizerMouseDown(e: MouseEvent) {
return this.startResize(e, 'both');
}
const startResize = React.useCallback(
(e: MouseEvent, direction: 'horizontal' | 'vertical' | 'both') => {
const isLeftButton =
(e.button === 1 && window.event !== null) || e.button === 0;
if (!isLeftButton || e.defaultPrevented) return;
startResize(
e: MouseEvent,
direction: 'horizontal' | 'vertical' | 'both' = 'horizontal'
) {
const isLeftButton =
(e.button === 1 && window.event !== null) || e.button === 0;
if (!isLeftButton || e.defaultPrevented) return;
e.preventDefault();
const {manager, id, node, store} = this.props;
if (!node) {
return;
}
const target = document.querySelector(`[data-editor-id="${id}"]`);
if (!target) {
return;
}
manager.disableHover = true;
const event = manager[
direction === 'both'
? 'onSizeChangeStart'
: direction === 'vertical'
? 'onHeightChangeStart'
: 'onWidthChangeStart'
](e, {
dom: target as HTMLElement,
node: node,
store: store,
resizer:
direction === 'both'
? this.resizerDom
: direction === 'vertical'
? this.hResizerDom
: this.wResizerDom
}) as PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
e.preventDefault();
if (!node) {
return;
}
>;
const pluginOnMove = event.data?.onMove;
const pluginonEnd = event.data?.onEnd;
const target = document.querySelector(`[data-editor-id="${id}"]`);
if (!pluginOnMove && !pluginonEnd) {
return;
}
this.mainRef.current?.setAttribute('data-resizing', '');
if (!target) {
return;
}
manager.disableHover = true;
const onMove = (e: MouseEvent) => {
e.preventDefault();
pluginOnMove?.(e);
};
const event = manager[
direction === 'both'
? 'onSizeChangeStart'
: direction === 'vertical'
? 'onHeightChangeStart'
: 'onWidthChangeStart'
](e, {
dom: target as HTMLElement,
node: node,
store: store,
resizer:
direction === 'both'
? resizerDom.current!
: direction === 'vertical'
? hResizerDom.current!
: wResizerDom.current!
}) as PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
}
>;
const onUp = (e: MouseEvent) => {
e.preventDefault();
manager.disableHover = false;
this.mainRef.current?.removeAttribute('data-resizing');
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
document.body.style.cursor = 'default';
const pluginOnMove = event.data?.onMove;
const pluginonEnd = event.data?.onEnd;
// 阻止 click 事件触发。
let captureClick = (e: MouseEvent) => {
window.removeEventListener('click', captureClick, true);
if (!pluginOnMove && !pluginonEnd) {
return;
}
mainRef.current?.setAttribute('data-resizing', '');
const onMove = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
pluginOnMove?.(e);
};
window.addEventListener('click', captureClick, true);
setTimeout(
() => window.removeEventListener('click', captureClick, true),
350
);
pluginonEnd?.(e);
};
const onUp = (e: MouseEvent) => {
e.preventDefault();
manager.disableHover = false;
mainRef.current?.removeAttribute('data-resizing');
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
document.body.style.cursor = 'default';
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
document.body.style.cursor =
direction === 'both'
? 'nwse-resize'
: direction === 'vertical'
? 'ns-resize'
: 'ew-resize';
}
// 阻止 click 事件触发。
let captureClick = (e: MouseEvent) => {
window.removeEventListener('click', captureClick, true);
e.preventDefault();
e.stopPropagation();
};
window.addEventListener('click', captureClick, true);
setTimeout(
() => window.removeEventListener('click', captureClick, true),
350
);
wResizerDom: HTMLElement;
pluginonEnd?.(e);
};
@autobind
wResizerRef(ref: any) {
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
document.body.style.cursor =
direction === 'both'
? 'nwse-resize'
: direction === 'vertical'
? 'ns-resize'
: 'ew-resize';
},
[]
);
const wResizerDom = React.useRef<HTMLElement>();
const wResizerRef = React.useCallback((ref: HTMLElement) => {
if (ref) {
ref.addEventListener('mousedown', this.handleWResizerMouseDown);
ref.addEventListener('mousedown', handleWResizerMouseDown);
} else {
this.wResizerDom?.removeEventListener(
wResizerDom.current?.removeEventListener(
'mousedown',
this.handleWResizerMouseDown
handleWResizerMouseDown
);
}
this.wResizerDom = ref;
}
wResizerDom.current = ref;
}, []);
hResizerDom: HTMLElement;
@autobind
hResizerRef(ref: any) {
const hResizerDom = React.useRef<HTMLElement>();
const hResizerRef = React.useCallback((ref: HTMLElement) => {
if (ref) {
ref.addEventListener('mousedown', this.handleHResizerMouseDown);
ref.addEventListener('mousedown', handleHResizerMouseDown);
} else {
this.hResizerDom?.removeEventListener(
hResizerDom.current?.removeEventListener(
'mousedown',
this.handleHResizerMouseDown
handleHResizerMouseDown
);
}
this.hResizerDom = ref;
}
hResizerDom.current = ref;
}, []);
resizerDom: HTMLElement;
@autobind
resizerRef(ref: any) {
const resizerDom = React.useRef<HTMLElement>();
const resizerRef = React.useCallback((ref: HTMLElement) => {
if (ref) {
ref.addEventListener('mousedown', this.handleResizerMouseDown);
ref.addEventListener('mousedown', handleResizerMouseDown);
} else {
this.resizerDom?.removeEventListener(
resizerDom.current?.removeEventListener(
'mousedown',
this.handleResizerMouseDown
handleResizerMouseDown
);
}
this.resizerDom = ref;
}
@autobind
handleMouseEnter() {
const manager = this.props.manager;
resizerDom.current = ref;
}, []);
const handleMouseEnter = React.useCallback(() => {
if (manager.disableHover) {
return;
}
this.props.store.setHoverId(this.props.id);
}
// 特殊布局元素和自由容器直接子元素直接拖拽调整位置
@autobind
handleDragStart(e: React.DragEvent) {
const {manager, id} = this.props;
store.setHoverId(id);
}, []);
const handleDragStart = React.useCallback((e: React.DragEvent) => {
if (manager.disableHover) {
return;
}
manager.startDrag(id, e);
}, []);
const mainRef = React.createRef<HTMLDivElement>();
const toolbars = store.sortedToolbars;
const secondaryToolbars = store.sortedSecondaryToolbars;
const specialToolbars = store.sortedSpecialToolbars;
const isActive = store.isActive(id);
const curFreeContainerId = store.parentIsFreeContainer();
const isHover =
store.isHoved(id) ||
store.dropId === id ||
store.insertOrigId === id ||
curFreeContainerId === id;
const isDraggableContainer = store.draggableContainer(id);
// todo 干掉这个逻辑
// 获取当前高亮画布宽度
const aePreviewOffsetWidth = document.getElementById(
'aePreviewHighlightBox'
)!.offsetWidth;
if (!isAlive(node)) {
return <div />;
}
// @autobind
// handleMouseLeave() {
// this.props.store.setHoverId(this.props.id);
// }
// 判断是否在最右侧(考虑组件头部工具栏被遮挡的问题)
const isRightElem = aePreviewOffsetWidth - node.x < 176; // 跳过icode代码检查
render() {
const {
className,
store,
id,
title,
children,
node,
toolbarContainer,
onSwitch
} = this.props;
const toolbars = store.sortedToolbars;
const secondaryToolbars = store.sortedSecondaryToolbars;
const specialToolbars = store.sortedSpecialToolbars;
const isActive = store.isActive(id);
const curFreeContainerId = store.parentIsFreeContainer();
const isHover =
store.isHoved(id) ||
store.dropId === id ||
store.insertOrigId === id ||
curFreeContainerId === id;
const isDraggableContainer = store.draggableContainer(id);
// 获取当前高亮画布宽度
const aePreviewOffsetWidth = document.getElementById(
'aePreviewHighlightBox'
)!.offsetWidth;
// 判断是否在最右侧(考虑组件头部工具栏被遮挡的问题)
const isRightElem = aePreviewOffsetWidth - node.x < 176; // 跳过icode代码检查
/* bca-disable */ return (
<div
className={cx(
'ae-Editor-hlbox',
{
shake: id === store.insertOrigId,
selected: isActive || ~store.selections.indexOf(id),
hover: isHover,
regionOn: node.childRegions.some(region =>
store.isRegionHighlighted(region.id, region.region)
),
isFreeContainerElem: !!curFreeContainerId || isDraggableContainer
},
className
)}
data-hlbox-id={id}
style={{
display: node.w && node.h ? 'block' : 'none',
top: node.y,
left: node.x,
width: node.w,
height: node.h
}}
ref={this.mainRef}
onMouseEnter={this.handleMouseEnter}
draggable={!!curFreeContainerId || isDraggableContainer}
onDragStart={this.handleDragStart}
>
{isActive ? (
<div
className={`ae-Editor-toolbarPopover ${
isRightElem ? 'is-right-elem' : ''
}`}
>
<div className="ae-Editor-nav">
{node.host ? (
<div
className="ae-Editor-tip parent"
onClick={() => onSwitch?.(node.host.id)}
>
{node.host.label}
</div>
) : null}
<div key="tip" className="ae-Editor-tip current">
{title}
/* bca-disable */
return (
<div
className={cx(
'ae-Editor-hlbox',
{
shake: id === store.insertOrigId,
selected: isActive || ~store.selections.indexOf(id),
hover: isHover,
regionOn: node.childRegions.some(region =>
store.isRegionHighlighted(region.id, region.region)
),
isFreeContainerElem: !!curFreeContainerId || isDraggableContainer
},
className
)}
data-hlbox-id={id}
style={{
display: node.w && node.h ? 'block' : 'none',
top: node.y,
left: node.x,
width: node.w,
height: node.h
}}
ref={mainRef}
onMouseEnter={handleMouseEnter}
draggable={!!curFreeContainerId || isDraggableContainer}
onDragStart={handleDragStart}
>
{isActive ? (
<div
className={`ae-Editor-toolbarPopover ${
isRightElem ? 'is-right-elem' : ''
}`}
>
<div className="ae-Editor-nav">
{node.host ? (
<div
className="ae-Editor-tip parent"
onClick={() => onSwitch?.(node.host.id)}
>
{node.host.label}
</div>
) : null}
{node.firstChild ? (
<div
className="ae-Editor-tip child"
onClick={() => onSwitch?.(node.firstChild.id)}
>
{node.firstChild.label}
</div>
) : null}
<div key="tip" className="ae-Editor-tip current">
{title}
</div>
<div className="ae-Editor-toolbar" key="toolbar">
{toolbars.map(item => (
<button
key={item.id}
type="button"
draggable={item.draggable}
onDragStart={item.onDragStart}
data-id={item.id}
data-tooltip={item.tooltip || undefined}
data-position={item.placement || 'top'}
onClick={item.onClick}
>
{item.iconSvg ? (
<Icon className="icon" icon={item.iconSvg} />
) : ~item.icon!.indexOf('<') ? (
<span dangerouslySetInnerHTML={{__html: item.icon!}} />
) : (
<i className={item.icon} />
)}
</button>
))}
</div>
{node.firstChild ? (
<div
className="ae-Editor-tip child"
onClick={() => onSwitch?.(node.firstChild.id)}
>
{node.firstChild.label}
</div>
) : null}
</div>
) : null}
{isActive && secondaryToolbars.length ? (
<div
className="ae-Editor-toolbar sencondary"
key="sencondary-toolbar"
>
{secondaryToolbars.map(item => (
<div className="ae-Editor-toolbar" key="toolbar">
{toolbars.map(item => (
<button
key={item.id}
type="button"
className={item.className}
draggable={item.draggable}
onDragStart={item.onDragStart}
data-id={item.id}
data-tooltip={item.tooltip || undefined}
data-position={item.placement || 'top'}
@ -344,52 +301,76 @@ export default class HighlightBox extends React.Component<HighlightBoxProps> {
</button>
))}
</div>
) : null}
</div>
) : null}
{isActive && specialToolbars.length ? (
<div className="ae-Editor-toolbar special" key="special-toolbar">
{specialToolbars.map(item => (
<button
key={item.id}
type="button"
className={item.className}
data-id={item.id}
data-tooltip={item.tooltip || undefined}
data-position={item.placement || 'top'}
onClick={item.onClick}
>
{item.iconSvg ? (
<Icon className="icon" icon={item.iconSvg} />
) : ~item.icon!.indexOf('<') ? (
<span dangerouslySetInnerHTML={{__html: item.icon!}} />
) : (
<i className={item.icon} />
)}
</button>
))}
</div>
) : null}
{isActive && secondaryToolbars.length ? (
<div className="ae-Editor-toolbar sencondary" key="sencondary-toolbar">
{secondaryToolbars.map(item => (
<button
key={item.id}
type="button"
className={item.className}
data-id={item.id}
data-tooltip={item.tooltip || undefined}
data-position={item.placement || 'top'}
onClick={item.onClick}
>
{item.iconSvg ? (
<Icon className="icon" icon={item.iconSvg} />
) : ~item.icon!.indexOf('<') ? (
<span dangerouslySetInnerHTML={{__html: item.icon!}} />
) : (
<i className={item.icon} />
)}
</button>
))}
</div>
) : null}
{children}
{isActive && specialToolbars.length ? (
<div className="ae-Editor-toolbar special" key="special-toolbar">
{specialToolbars.map(item => (
<button
key={item.id}
type="button"
className={item.className}
data-id={item.id}
data-tooltip={item.tooltip || undefined}
data-position={item.placement || 'top'}
onClick={item.onClick}
>
{item.iconSvg ? (
<Icon className="icon" icon={item.iconSvg} />
) : ~item.icon!.indexOf('<') ? (
<span dangerouslySetInnerHTML={{__html: item.icon!}} />
) : (
<i className={item.icon} />
)}
</button>
))}
</div>
) : null}
{node.widthMutable ? (
<>
<span className="ae-border-WResizer" ref={this.wResizerRef}></span>
<span className="ae-WResizer" ref={this.wResizerRef}></span>
</>
) : null}
{children}
{node.heightMutable ? (
<>
<span className="ae-border-HResizer" ref={this.hResizerRef}></span>
<span className="ae-HResizer" ref={this.hResizerRef}></span>
</>
) : null}
{node.widthMutable ? (
<>
<span className="ae-border-WResizer" ref={wResizerRef}></span>
<span className="ae-WResizer" ref={wResizerRef}></span>
</>
) : null}
{node.widthMutable && node.heightMutable ? (
<span className="ae-Resizer" ref={this.resizerRef}></span>
) : null}
</div>
);
}
}
{node.heightMutable ? (
<>
<span className="ae-border-HResizer" ref={hResizerRef}></span>
<span className="ae-HResizer" ref={hResizerRef}></span>
</>
) : null}
{node.widthMutable && node.heightMutable ? (
<span className="ae-Resizer" ref={resizerRef}></span>
) : null}
</div>
);
});

View File

@ -80,7 +80,7 @@ export class NodeWrapper extends React.Component<NodeWrapperProps> {
// 自动合并假数据
if (isObject(mockProps) && !isEmpty(mockProps)) {
rest = merge({}, rest, mockProps);
rest = merge(rest, mockProps);
}
if ($$editor.renderRenderer) {

View File

@ -0,0 +1,117 @@
import {ClassNamesFn} from 'amis-core';
import {observer} from 'mobx-react';
import React from 'react';
import {EditorStoreType} from '../../store/editor';
import {modalsToDefinitions, translateSchema} from '../../util';
import {Button, Icon, ListMenu, PopOverContainer, confirm} from 'amis';
export interface DialogListProps {
classnames: ClassNamesFn;
store: EditorStoreType;
}
export default observer(function DialogList({
classnames: cx,
store
}: DialogListProps) {
const modals = store.modals;
const handleAddDialog = React.useCallback(() => {
const modal = {
type: 'dialog',
title: '未命名弹窗',
definitions: modalsToDefinitions(store.modals),
body: [
{
type: 'tpl',
tpl: '弹窗内容'
}
]
};
store.openSubEditor({
title: '编辑弹窗',
value: modal,
onChange: ({definitions, ...modal}: any, diff: any) => {
store.addModal(modal, definitions);
}
});
}, []);
const handleEditDialog = React.useCallback((event: React.UIEvent<any>) => {
const index = parseInt(event.currentTarget.getAttribute('data-index')!, 10);
const dialog = store.modals[index];
store.openSubEditor({
title: '编辑弹窗',
value: {
type: 'dialog',
...(dialog as any),
definitions: modalsToDefinitions(store.modals)
},
onChange: ({definitions, ...modal}: any, diff: any) => {
store.updateModal(dialog.$$id!, modal, definitions);
}
});
}, []);
const handleDelDialog = React.useCallback(
async (event: React.UIEvent<any>) => {
event.stopPropagation();
event.preventDefault();
const index = parseInt(
event.currentTarget
.closest('[data-index]')!
.getAttribute('data-index')!,
10
);
const dialog = store.modals[index];
const refsCount = store.countModalActionRefs(dialog.$$id!);
const confirmed = await confirm(
refsCount
? `当前弹窗已关联 ${refsCount} 个事件,删除后,所配置的事件动作将一起被删除。`
: '',
`确认删除弹窗「${dialog.editorSetting?.displayName || dialog.title}」?`
);
if (confirmed) {
store.removeModal(dialog.$$id!);
}
},
[]
);
return (
<div className={cx('ae-DialogList-wrap', 'hoverShowScrollBar')}>
<Button size="sm" level="enhance" block onClick={handleAddDialog}>
</Button>
{modals.length ? (
<ul className="ae-DialogList">
{modals.map((modal, index) => (
<li
className="ae-DialogList-item"
data-index={index}
key={modal.$$id || index}
onClick={handleEditDialog}
>
<span>
{`${
modal.editorSetting?.displayName ||
modal.title ||
'未命名弹窗'
}`}
</span>
<a onClick={handleDelDialog} className="ae-DialogList-iconBtn">
<Icon className="icon" icon="delete-bold-btn" />
</a>
</li>
))}
</ul>
) : (
<div className="ae-DialogList-placeholder"></div>
)}
</div>
);
});

View File

@ -7,6 +7,7 @@ import {Icon, InputBox, Tab, Tabs} from 'amis';
import {EditorNodeType} from '../../store/node';
import {isAlive} from 'mobx-state-tree';
import type {Schema} from 'amis';
import DialogList from './DialogList';
@observer
export class OutlinePanel extends React.Component<PanelProps> {
@ -42,8 +43,8 @@ export class OutlinePanel extends React.Component<PanelProps> {
e: React.MouseEvent<HTMLAnchorElement>,
option: Schema
) {
const store = this.props.store;
store.setPreviewDialogId(option.$$id);
// const store = this.props.store;
// store.setPreviewDialogId(option.$$id);
}
@autobind
@ -61,9 +62,9 @@ export class OutlinePanel extends React.Component<PanelProps> {
const store = this.props.store;
if (key && isAlive(store)) {
store.changeOutlineTabsKey(key);
if (key === 'component-outline') {
store.setPreviewDialogId();
}
// if (key === 'component-outline') {
// store.setPreviewDialogId();
// }
}
}
@ -250,15 +251,13 @@ export class OutlinePanel extends React.Component<PanelProps> {
}
renderDialogItem(option: any, index: number) {
const store = this.props.store;
const children = store.root.children;
const isSelectedDialog = option.$$id === store.previewDialogId;
// const store = this.props.store;
// const children = store.root.children;
// const isSelectedDialog = option.$$id === store.previewDialogId;
const dialogLabel = this.getDialogLabel(option, false);
return children?.length && isSelectedDialog ? (
this.renderItem(children[0], index, 'dialog')
) : (
return (
<li className={cx('ae-Outline-node')} key={index}>
<a onClick={e => this.handleDialogNodeClick(e, option)}>
<span className="ae-Outline-node-text">
@ -277,7 +276,6 @@ export class OutlinePanel extends React.Component<PanelProps> {
const {store} = this.props;
const outlineTabsKey = store.outlineTabsKey || 'component-outline';
const options = store.outline;
const dialogOptions = store.dialogOutlineList;
return (
<div className="ae-Outline-panel">
@ -336,46 +334,16 @@ export class OutlinePanel extends React.Component<PanelProps> {
)}
</div>
</Tab>
<Tab
className={'ae-outline-tabs-panel'}
key={'dialog-outline'}
eventKey={'dialog-outline'}
title={'弹窗大纲'}
>
<InputBox
className="editor-InputSearch"
value={curSearchElemKey}
onChange={this.handleSearchElemKeyChange}
placeholder={'查询页面元素'}
clearable={false}
{store.isSubEditor ? null : (
<Tab
className={'ae-outline-tabs-panel'}
key={'dialog-outline'}
eventKey={'dialog-outline'}
title={'弹窗列表'}
>
{curSearchElemKey ? (
<a onClick={this.clearSearchElemKey}>
<Icon icon="close" className="icon" />
</a>
) : (
<Icon icon="editor-search" className="icon" />
)}
</InputBox>
<hr className="margin-top" />
<div
className={cx('ae-Outline', 'hoverShowScrollBar', {
'ae-Outline--draging': store.dragging
})}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
>
{dialogOptions.length ? (
<ul className="ae-Outline-list">
{dialogOptions.map((option, index) =>
this.renderDialogItem(option, index)
)}
</ul>
) : (
<div></div>
)}
</div>
</Tab>
<DialogList store={store} classnames={cx} />
</Tab>
)}
</Tabs>
</div>
);

View File

@ -1,4 +1,4 @@
import {Html, render, TooltipWrapper, buildTestId} from 'amis';
import {Html, render, TestIdBuilder, TooltipWrapper} from 'amis';
import {observer} from 'mobx-react';
import React from 'react';
import cx from 'classnames';
@ -18,6 +18,7 @@ type PanelProps = {
};
searchRendererType: string;
className?: string;
testIdBuilder?: TestIdBuilder;
};
type PanelStates = {
@ -98,7 +99,7 @@ export default class RenderersPanel extends React.Component<
}
render() {
const {store, searchRendererType, className} = this.props;
const {store, searchRendererType, className, testIdBuilder} = this.props;
const grouped = this.props.groupedRenderers || {};
const keys = Object.keys(grouped);
@ -189,7 +190,7 @@ export default class RenderersPanel extends React.Component<
onDragStart={(e: React.DragEvent) =>
this.handleDragStart(e, item.name)
}
{...buildTestId(testid)}
{...testIdBuilder?.getChild(testid).getTestId()}
>
<div
className="icon-box"

View File

@ -79,29 +79,6 @@ export default class Preview extends Component<PreviewProps> {
this.currentDom.addEventListener('mousedown', this.handeMouseDown);
this.props.manager.on('after-update', this.handlePanelChange);
const store = this.props.store;
// 添加弹窗事件或弹窗列表进行弹窗切换后自动选中对应的弹窗
this.dialogReaction = reactionWithOldValue(
() =>
store.root.children?.length
? `${store.root.children[0]?.type}:${store.root.children[0]?.id}`
: '',
(info, preInfo) => {
if (preInfo !== '') {
// 如果为'' 说明是从预览切换回来的不需要调整activId
const type = info.split(':')[0];
if (type === 'dialog' || type === 'drawer') {
const dialogId = info.split(':')[1];
store.changeOutlineTabsKey('dialog-outline');
store.setPreviewDialogId(dialogId);
store.setActiveId(dialogId);
} else {
store.setActiveId(store.getRootId());
}
}
}
);
}
componentWillUnmount() {

View File

@ -6,6 +6,7 @@ import {observer} from 'mobx-react';
import {EditorManager} from '../manager';
import {EditorNodeType} from '../store/node';
import {autobind} from '../util';
import {isAlive} from 'mobx-state-tree';
export const AddBTNSvg = `<svg viewBox="0 0 12 12">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
@ -27,66 +28,62 @@ export interface HighlightBoxProps {
isOnlyChildRegion: boolean;
}
@observer
export default class RegionHighlightBox extends React.Component<HighlightBoxProps> {
// 点击清空当前区域中的所有元素
@autobind
handleClick() {
const {manager, id, name} = this.props;
// 改成 sfc 函数组件 可以消除一个警告 但是不知道为什么
// 可以消除 Can't perform a React state update on an unmounted component
export default observer(function (props: HighlightBoxProps) {
const {manager, store, id, name, title, node, isOnlyChildRegion} = props;
const handleClick = React.useCallback(() => {
manager.emptyRegion(id, name);
}
}, [id, name, manager]);
render() {
const {store, id, name, title, node, isOnlyChildRegion} = this.props;
let isHiglight = store.isRegionHighlighted(id, name);
let isHiglightHover = store.isRegionHighlightHover(id, name);
let isDragEnter = store.isRegionDragEnter(id, name);
const host = store.getNodeById(id)!;
const dx = node.x - host.x;
const dy = node.y - host.y;
let isHiglight = store.isRegionHighlighted(id, name);
let isHiglightHover = store.isRegionHighlightHover(id, name);
let isDragEnter = store.isRegionDragEnter(id, name);
const host = store.getNodeById(id)!;
const dx = node.x - host.x;
const dy = node.y - host.y;
return (
return (
<div
data-renderer={node.host.info.renderer.name}
data-region={name}
className={cx(
'ae-Editor-rhlbox',
isDragEnter ? 'is-dragenter' : '',
!isOnlyChildRegion && isHiglightHover ? 'region-hover' : '',
isOnlyChildRegion || isHiglight ? 'is-highlight' : '',
dx < 87 && dy < 21 && node.x < 190 ? 'region-label-within' : ''
)}
style={{
width: node.w,
height: node.h,
borderWidth: `${Math.max(0, dy)}px ${Math.max(
0,
host.w - dx - node.w
)}px ${Math.max(0, host.h - dy - node.h)}px ${Math.max(0, dx)}px`
}}
>
<div
data-renderer={node.host.info.renderer.name}
data-region={name}
className={cx(
'ae-Editor-rhlbox',
isDragEnter ? 'is-dragenter' : '',
!isOnlyChildRegion && isHiglightHover ? 'region-hover' : '',
isOnlyChildRegion || isHiglight ? 'is-highlight' : '',
dx < 87 && dy < 21 && node.x < 190 ? 'region-label-within' : ''
)}
style={{
width: node.w,
height: node.h,
borderWidth: `${Math.max(0, dy)}px ${Math.max(
0,
host.w - dx - node.w
)}px ${Math.max(0, host.h - dy - node.h)}px ${Math.max(0, dx)}px`
}}
data-node-id={id}
data-node-region={name}
className={`region-tip ${
isOnlyChildRegion ? 'is-only-child-region' : ''
} ignore-hover-elem`}
>
<div
data-node-id={id}
data-node-region={name}
className={`region-tip ${
isOnlyChildRegion ? 'is-only-child-region' : ''
} ignore-hover-elem`}
>
{title}
<span className="margin-space">|</span>
{title}
<span className="margin-space">|</span>
<button
type="button"
className="clear-icon-btn"
data-tooltip={'点击清空当前区域'}
data-position={'bottom'}
onClick={this.handleClick}
>
<Icon icon="clear-btn" />
</button>
</div>
<button
type="button"
className="clear-icon-btn"
title={''}
data-tooltip={'点击清空当前区域'}
data-position={'bottom'}
onClick={handleClick}
>
<Icon icon="clear-btn" />
</button>
</div>
);
}
}
</div>
);
});

View File

@ -224,7 +224,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
},
{
type: 'submit',
label: '确认',
label: '保存',
level: 'primary'
},
{

View File

@ -0,0 +1,157 @@
import React from 'react';
import {EditorNodeType} from '../../store/node';
import {EditorManager} from '../../manager';
import {diff, getThemeConfig} from '../../util';
import {createObjectFromChain, render} from 'amis';
import omit from 'lodash/omit';
import cx from 'classnames';
export function SchemaFrom({
propKey,
body,
definitions,
controls,
onChange,
value,
env,
api,
popOverContainer,
submitOnChange,
node,
manager,
justify,
ctx,
pipeIn,
pipeOut
}: {
propKey?: string;
env: any;
body?: Array<any>;
/**
* @deprecated body
*/
controls?: Array<any>;
definitions?: any;
value: any;
api?: any;
onChange: (
value: any,
diff: any,
filter: (schema: any, value: any, id: string, diff?: any) => any
) => void;
popOverContainer?: () => HTMLElement | void;
submitOnChange?: boolean;
node?: EditorNodeType;
manager: EditorManager;
panelById?: string;
justify?: boolean;
ctx?: any;
pipeIn?: (value: any) => any;
pipeOut?: (value: any, oldValue: any) => any;
}) {
const schema = React.useMemo(() => {
let containerKey = 'body';
if (Array.isArray(controls)) {
body = controls;
containerKey = 'controls';
}
body = Array.isArray(body) ? body.concat() : [];
if (submitOnChange === false) {
body.push({
type: 'submit',
label: '保存',
level: 'primary',
block: true,
className: 'ae-Settings-actions'
});
}
const schema = {
key: propKey,
definitions,
[containerKey]: body,
className: cx(
'config-form-content',
'ae-Settings-content',
'hoverShowScrollBar',
submitOnChange === false ? 'with-actions' : ''
),
wrapperComponent: 'div',
type: 'form',
title: '',
mode: 'normal',
api,
wrapWithPanel: false,
submitOnChange: submitOnChange !== false,
messages: {
validateFailed: ''
}
};
if (justify) {
schema.mode = 'horizontal';
schema.horizontal = {
left: 4,
right: 8,
justify: true
};
}
return schema;
}, [body, controls, submitOnChange]);
value = value || {};
const finalValue = pipeIn ? pipeIn(value) : value;
const themeConfig = React.useMemo(() => getThemeConfig(), []);
const submitSubscribers = React.useRef<Array<Function>>([]);
const subscribeSubmit = React.useCallback(
(
fn: (schema: any, value: any, id: string, diff?: any) => any,
once = false
) => {
let raw = fn;
const unsubscribe = () => {
submitSubscribers.current = submitSubscribers.current.filter(
item => ((item as any).__raw ?? item) !== raw
);
};
if (once) {
fn = (schema: any, value: any, id: string, diff?: any) => {
const ret = raw(schema, value, id, diff);
unsubscribe();
return ret;
};
(fn as any).__raw = raw;
}
submitSubscribers.current.push(fn);
return unsubscribe;
},
[]
);
return render(
schema,
{
onFinished: async (newValue: any) => {
newValue = pipeOut ? await pipeOut(newValue, value) : newValue;
const diffValue = diff(value, newValue);
onChange(newValue, diffValue, (schema, value, id, diff) => {
return submitSubscribers.current.reduce((schema, fn) => {
return fn(schema, value, id, diff);
}, schema);
});
},
data: createObjectFromChain([ctx, themeConfig, finalValue]),
node: node,
manager: manager,
popOverContainer,
subscribeSchemaSubmit: subscribeSubmit
},
{
...omit(env, 'replaceText')
// theme: 'cxd' // 右侧属性配置面板固定使用cxd主题展示
}
);
}

View File

@ -3,7 +3,6 @@ import {isAlive} from 'mobx-state-tree';
import React from 'react';
import {NodeWrapper} from './NodeWrapper';
import {PanelProps, RegionConfig, RendererInfo} from '../plugin';
import cx from 'classnames';
import groupBy from 'lodash/groupBy';
import {RegionWrapper} from './RegionWrapper';
import find from 'lodash/find';
@ -13,25 +12,14 @@ import {EditorNodeContext, EditorNodeType} from '../store/node';
import {EditorManager} from '../manager';
import flatten from 'lodash/flatten';
import {render as reactRender, unmountComponentAtNode} from 'react-dom';
import {
autobind,
diff,
getThemeConfig,
JSONGetById,
JSONPipeIn,
JSONPipeOut,
JSONUpdate,
getFixDialogType,
appTranslate
} from '../util';
import {createObjectFromChain} from 'amis-core';
import {autobind, JSONGetById, JSONUpdate, appTranslate} from '../util';
import {ErrorBoundary} from 'amis-core';
import {CommonConfigWrapper} from './CommonConfigWrapper';
import type {Schema} from 'amis';
import type {DataScope} from 'amis-core';
import type {RendererConfig} from 'amis-core';
import type {SchemaCollection} from 'amis';
import omit from 'lodash/omit';
import {SchemaFrom} from './base/SchemaForm';
// 创建 Node Store 并构建成树
export function makeWrapper(
@ -233,288 +221,6 @@ function replaceDialogtoRef(
return replacedSchema;
}
// 添加definitions
function addDefinitions(
schema: Schema,
definitions: Schema,
dialogMaxIndex: number,
selectDialog: any
) {
let newSchema;
let dialogRefsName = '';
if (dialogMaxIndex) {
Object.keys(definitions).forEach(ref => {
const dialog = definitions[ref];
if (dialog.$$id === selectDialog) {
dialogRefsName = ref;
}
});
}
let dialogType = getFixDialogType(schema, selectDialog);
let newDefinitions = {...definitions};
if (!dialogRefsName) {
dialogRefsName = dialogMaxIndex
? `${dialogType}-ref-${dialogMaxIndex + 1}`
: `${dialogType}-ref-1`;
}
let dialogBody = JSONGetById(schema, selectDialog);
// 防止definition被查找到替换为$ref重新生成一下
newDefinitions[dialogRefsName] = JSONPipeIn(
JSONPipeOut({
...dialogBody,
type: dialogType
})
);
newSchema = {
...schema,
definitions: newDefinitions
};
return {
dialogRefsName,
newSchema
};
}
// 选择现有弹窗后definitions设置和弹窗schema的同步
function currentDialogOnchagne(
manager: EditorManager,
diffs: any,
newValue?: any
) {
const {store} = manager;
let schema = store.schema;
let definitions = schema.definitions || {};
let dialogMaxIndex: number = 0;
Object.keys(definitions).forEach(k => {
if (k.includes('ref-')) {
let index = Number(k.split('-')[2]);
dialogMaxIndex = Math.max(dialogMaxIndex, index);
}
});
if (diffs?.length) {
let replacedSchema = null;
let editRefsName = '';
for (const diff of diffs) {
const {path, kind, item, rhs} = diff;
// 添加选择现有弹窗事件
if (
kind === 'A' &&
path.length > 1 &&
path?.[path.length - 1] === 'actions' &&
item.kind === 'N' &&
item.rhs?.__selectDialog
) {
const {newSchema, dialogRefsName} = addDefinitions(
schema,
definitions,
dialogMaxIndex,
item.rhs?.__selectDialog
);
replacedSchema = replaceDialogtoRef(
newSchema,
item.rhs?.__selectDialog,
dialogRefsName
);
if (item.rhs?.__relatedDialogId) {
replacedSchema = replaceDialogtoRef(
replacedSchema,
item.rhs?.__relatedDialogId,
dialogRefsName
);
}
return replacedSchema;
}
// 编辑弹窗,从新建弹窗切换到现有弹窗,原始弹窗id
else if (
kind === 'N' &&
path?.length > 1 &&
path?.[path.length - 1] === '__selectDialog'
) {
const {newSchema, dialogRefsName} = addDefinitions(
schema,
definitions,
dialogMaxIndex,
rhs
);
editRefsName = dialogRefsName;
replacedSchema = replaceDialogtoRef(newSchema, rhs, dialogRefsName);
}
// 编辑弹窗,从新建弹窗切换到现有弹窗,新生成弹窗id
else if (
kind === 'N' &&
path?.length > 1 &&
path?.[path.length - 1] === '__relatedDialogId'
) {
replacedSchema = replaceDialogtoRef(
replacedSchema!,
rhs,
editRefsName!
);
return replacedSchema;
}
// 编辑弹窗,选择了其他现有弹窗原始弹窗id
else if (
kind === 'E' &&
path?.length > 1 &&
path?.[path.length - 1] === '__selectDialog'
) {
const {newSchema, dialogRefsName} = addDefinitions(
schema,
definitions,
dialogMaxIndex,
rhs
);
editRefsName = dialogRefsName;
replacedSchema = replaceDialogtoRef(newSchema, rhs, dialogRefsName);
}
// 编辑弹窗,选择了其他现有弹窗新生成弹窗id
else if (
kind === 'E' &&
path?.length > 1 &&
path?.[path.length - 1] === '__relatedDialogId'
) {
replacedSchema = replaceDialogtoRef(
replacedSchema!,
rhs,
editRefsName!
);
return replacedSchema;
}
}
return replacedSchema;
}
return null;
}
function SchemaFrom({
propKey,
body,
definitions,
controls,
onChange,
value,
env,
api,
popOverContainer,
submitOnChange,
node,
manager,
justify,
ctx,
pipeIn,
pipeOut
}: {
propKey: string;
env: any;
body?: Array<any>;
/**
* @deprecated body
*/
controls?: Array<any>;
definitions?: any;
value: any;
api?: any;
onChange: (value: any, diff: any) => void;
popOverContainer?: () => HTMLElement | void;
submitOnChange?: boolean;
node?: EditorNodeType;
manager: EditorManager;
panelById?: string;
justify?: boolean;
ctx?: any;
pipeIn?: (value: any) => any;
pipeOut?: (value: any, oldValue: any) => any;
}) {
let containerKey = 'body';
if (Array.isArray(controls)) {
body = controls;
containerKey = 'controls';
}
body = Array.isArray(body) ? body.concat() : [];
if (submitOnChange === false) {
body.push({
type: 'submit',
label: '保存',
level: 'primary',
block: true,
className: 'ae-Settings-actions'
});
}
const schema = {
key: propKey,
definitions,
[containerKey]: body,
className: cx(
'config-form-content',
'ae-Settings-content',
'hoverShowScrollBar',
submitOnChange === false ? 'with-actions' : ''
),
wrapperComponent: 'div',
type: 'form',
title: '',
mode: 'normal',
api,
wrapWithPanel: false,
submitOnChange: submitOnChange !== false,
messages: {
validateFailed: ''
}
};
if (justify) {
schema.mode = 'horizontal';
schema.horizontal = {
left: 4,
right: 8,
justify: true
};
}
value = value || {};
const finalValue = pipeIn ? pipeIn(value) : value;
const themeConfig = getThemeConfig();
return render(
schema,
{
onFinished: async (newValue: any) => {
newValue = pipeOut ? await pipeOut(newValue, value) : newValue;
const diffValue = diff(value, newValue);
onChange(newValue, diffValue);
// 如果是选择现有弹窗需要提取Definitions在这里一起做变更
const store = manager.store;
const schema = store.schema;
let newSchema = currentDialogOnchagne(manager, diffValue, newValue);
if (newSchema) {
const schemaDiff = diff(schema, newSchema);
store.definitionOnchangeValue(newSchema, schemaDiff);
}
// 添加弹窗事件后自动选中弹窗
if (store.activeDialogPath) {
let activeId = store.getSchemaByPath(
store.activeDialogPath.split('/').filter(item => item !== '')
)?.$$id;
activeId && store.setPreviewDialogId(activeId);
store.setActiveDialogPath('');
}
},
data: createObjectFromChain([ctx, themeConfig, finalValue]),
node: node,
manager: manager,
popOverContainer
},
{
...omit(env, 'replaceText')
// theme: 'cxd' // 右侧属性配置面板固定使用cxd主题展示
}
);
}
export function makeSchemaFormRender(
manager: EditorManager,
schema: {

View File

@ -1,15 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg width="16px" height="16px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="页面编辑器" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="补充icon" transform="translate(-20.000000, -35.000000)">
<g id="组件" transform="translate(20.000000, 35.000000)">
<rect id="矩形" x="0" y="0" width="48" height="48"></rect>
<g id="4.图标元件/7.通用/7.分类/分类-线备份-3" transform="translate(14.000000, 14.000000)" stroke-width="1.25">
<rect id="矩形" stroke="currentColor" fill="currentColor" opacity="0" x="0.625" y="0.625" width="18.75" height="18.75"></rect>
<rect id="矩形" stroke="currentColor" stroke-linejoin="round" x="2.70106592" y="2.70231592" width="5.84786816" height="5.84786816"></rect>
<rect id="矩形备份-3" stroke="currentColor" stroke-linejoin="round" x="11.4563618" y="2.70231592" width="5.84786816" height="5.84786816"></rect>
<rect id="矩形备份-5" stroke="currentColor" stroke-linejoin="round" x="2.70106592" y="11.4510659" width="5.84786816" height="5.84786816"></rect>
<rect id="矩形备份-4" stroke="currentColor" stroke-linejoin="round" x="11.4563618" y="11.4510659" width="5.84786816" height="5.84786816"></rect>
<g id="4.图标元件/7.通用/7.分类/分类-线备份-3" transform="translate(14.000000, 14.000000)"
stroke-width="1.25">
<rect id="矩形" stroke="currentColor" fill="currentColor" opacity="0" x="0.625"
y="0.625" width="18.75" height="18.75"></rect>
<rect id="矩形" stroke="currentColor" stroke-linejoin="round" x="2.70106592"
y="2.70231592" width="5.84786816" height="5.84786816"></rect>
<rect id="矩形备份-3" stroke="currentColor" stroke-linejoin="round" x="11.4563618"
y="2.70231592" width="5.84786816" height="5.84786816"></rect>
<rect id="矩形备份-5" stroke="currentColor" stroke-linejoin="round" x="2.70106592"
y="11.4510659" width="5.84786816" height="5.84786816"></rect>
<rect id="矩形备份-4" stroke="currentColor" stroke-linejoin="round" x="11.4563618"
y="11.4510659" width="5.84786816" height="5.84786816"></rect>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -36,6 +36,7 @@ import type {EditorStoreType} from './store/editor';
import {AvailableRenderersPlugin} from './plugin/AvailableRenderers';
import ShortcutKey from './component/base/ShortcutKey';
import WidthDraggableContainer from './component/base/WidthDraggableContainer';
import {SchemaFrom} from './component/base/SchemaForm';
export const version = '__buildVersion';
@ -59,5 +60,6 @@ export {
ContainerWrapper,
AvailableRenderersPlugin,
ShortcutKey,
SchemaFrom,
WidthDraggableContainer
};

View File

@ -873,7 +873,11 @@ export class EditorManager {
* @param rendererIdOrSchema
* ID添加新元素schema片段添加新元素
*/
async addElem(rendererIdOrSchema: string | any, reGenerateId?: boolean) {
async addElem(
rendererIdOrSchema: string | any,
reGenerateId?: boolean,
activeChild: boolean = true
) {
if (!rendererIdOrSchema) {
return;
}
@ -1029,7 +1033,7 @@ export class EditorManager {
},
reGenerateId
);
if (child) {
if (child && activeChild) {
// mobx 修改数据是异步的
setTimeout(() => {
store.setActiveId(child.$$id);
@ -1323,10 +1327,15 @@ export class EditorManager {
* @param diff
*/
@autobind
panelChangeValue(value: any, diff?: any) {
panelChangeValue(
value: any,
diff?: any,
changeFilter?: (schema: any, value: any, id: string, diff?: any) => any,
id = this.store.activeId
) {
const store = this.store;
const context: ChangeEventContext = {
...this.buildEventContext(store.activeId),
...this.buildEventContext(id),
value,
diff
};
@ -1336,9 +1345,12 @@ export class EditorManager {
return;
}
store.changeValue(value, diff);
store.changeValue(value, diff, changeFilter, id);
this.trigger('after-update', context);
this.trigger('after-update', {
...context,
schema: context.node.schema // schema 是新的,因为修改完了
});
}
/**

View File

@ -983,7 +983,7 @@ export interface RendererPluginAction {
schema?: any; // 动作配置schema
supportComponents?: string[] | string; // 如果schema中包含选择组件可以指定该动作支持的组件类型用于组件数树过滤
innerArgs?: string[]; // 动作专属配置参数,主要是为了区分特性字段和附加参数
descDetail?: (info: any) => string | JSX.Element; // 动作详细描述
descDetail?: (info: any, context: any, props: any) => string | JSX.Element; // 动作详细描述
outputVarDataSchema?: any | any[]; // 动作出参的结构定义
actions?: SubRendererPluginAction[]; // 分支动作(配置面板包含多种动作的情况)
children?: RendererPluginAction[]; // 子类型for动作树

View File

@ -21,8 +21,8 @@ import {
guid,
appTranslate,
JSONGetByPath,
getDialogActions,
getFixDialogType
addModal,
mergeDefinitions
} from '../../src/util';
import {
InsertEventContext,
@ -56,6 +56,8 @@ import {EditorNode, EditorNodeType} from './node';
import findIndex from 'lodash/findIndex';
import {matchSorter} from 'match-sorter';
import debounce from 'lodash/debounce';
import type {DialogSchema} from '../../../amis/src/renderers/Dialog';
import type {DrawerSchema} from '../../../amis/src/renderers/Drawer';
export interface SchemaHistory {
versionId: number;
@ -124,6 +126,16 @@ export interface TargetName {
editorId: string;
}
export type EditorModalBody = (DialogSchema | DrawerSchema) & {
// 节点 ID
$$id?: string;
// 如果是公共弹窗,在 definitions 中的 key
$$ref?: string;
// 弹出方式
actionType?: string;
};
export const MainStore = types
.model('EditorRoot', {
isMobile: false,
@ -138,8 +150,6 @@ export const MainStore = types
hoverId: '',
hoverRegion: '',
activeId: '',
previewDialogId: '', // 选择要进行编辑的弹窗id
activeDialogPath: '', // 记录选中设计的弹窗path
activeRegion: '', // 记录当前激活的子区域
mouseMoveRegion: '', // 记录当前鼠标hover到的区域后续需要优化合并MouseMoveRegion和hoverRegion
@ -236,15 +246,6 @@ export const MainStore = types
// 给编辑状态时的
get filteredSchema() {
let schema = self.schema;
if (self.previewDialogId) {
let originDialogSchema = this.getSchema(self.previewDialogId);
schema = {
...originDialogSchema,
type:
originDialogSchema.type ||
getFixDialogType(self.schema, self.previewDialogId)
};
}
return filterSchemaForEditor(
getEnv(self).schemaFilter?.(schema) ?? schema
);
@ -1011,10 +1012,53 @@ export const MainStore = types
},
// 获取弹窗大纲列表
get dialogOutlineList() {
get modals(): Array<EditorModalBody> {
const schema = self.schema;
let actions = getDialogActions(schema, 'list');
return actions;
const modals: Array<DialogSchema | DrawerSchema> = [];
Object.keys(schema.definitions || {}).forEach(key => {
const definition = schema.definitions[key];
if (['dialog', 'drawer'].includes(definition.type)) {
modals.push({
...definition,
$$ref: key
});
}
});
JSONTraverse(schema, (value: any, key: string, host: any) => {
if (
key === 'actionType' &&
['dialog', 'drawer', 'confirmDialog'].includes(value)
) {
const key = value === 'drawer' ? 'drawer' : 'dialog';
const body = host[key] || host['args'];
if (body && !body.$ref) {
modals.push({
...body,
actionType: value
});
}
}
return value;
});
return modals;
},
get modalOptions() {
return this.modals.map((modal: EditorModalBody) => {
return {
label: `${
modal.editorSetting?.displayName || modal.title || '未命名弹窗'
}`,
tip:
modal.actionType === 'confirmDialog'
? '确认框'
: modal.type === 'drawer'
? '抽屉弹窗'
: '弹窗',
value: modal.$$id,
$$ref: modal.$$ref
};
});
}
};
})
@ -1235,14 +1279,6 @@ export const MainStore = types
// }
},
setActiveDialogPath(path: string) {
self.activeDialogPath = path;
},
setPreviewDialogId(id?: string) {
self.previewDialogId = id ? id : '';
},
setSelections(ids: Array<string>) {
self.activeId = '';
self.activeRegion = '';
@ -1400,11 +1436,23 @@ export const MainStore = types
this.changeLeftPanelOpenStatus(true);
},
changeValue(value: Schema, diff?: any) {
if (!self.activeId) {
changeValue(
value: Schema,
diff?: any,
changeFilter?: (schema: any, value: any, id: string, diff?: any) => any,
id = self.activeId
) {
if (!id) {
return;
}
this.changeValueById(self.activeId, value, diff);
this.changeValueById(
id,
value,
diff,
undefined,
undefined,
changeFilter
);
},
definitionOnchangeValue(value: Schema, diff?: any) {
@ -1416,7 +1464,8 @@ export const MainStore = types
value: Schema,
diff?: any,
replace?: boolean,
noTrace?: boolean
noTrace?: boolean,
changeFilter?: (schema: any, value: any, id: string, diff?: any) => any
) {
const origin = JSONGetById(self.schema, id);
@ -1427,11 +1476,12 @@ export const MainStore = types
// 通常 Panel 和 codeEditor 过来都有 diff 信息
if (diff) {
const result = patchDiff(origin, diff);
this.traceableSetSchema(
JSONUpdate(self.schema, id, JSONPipeIn(result), true),
noTrace
);
let schema = JSONUpdate(self.schema, id, JSONPipeIn(result), true);
schema = changeFilter?.(schema, value, id, diff) || schema;
this.traceableSetSchema(schema, noTrace);
} else {
let schema = JSONUpdate(self.schema, id, JSONPipeIn(value), replace);
schema = changeFilter?.(schema, value, id) || schema;
this.traceableSetSchema(
JSONUpdate(self.schema, id, JSONPipeIn(value), replace),
noTrace
@ -1627,6 +1677,132 @@ export const MainStore = types
self.jsonSchemaUri = schemaUri;
},
addModal(modal?: DialogSchema | DrawerSchema, definitions?: any) {
const [schema] = addModal(self.schema, modal, definitions);
this.traceableSetSchema(schema);
},
/**
* ID的模态动作引用的数量
*
* @param id ID
* @returns
*/
countModalActionRefs(id: string) {
let count = 0;
const host = JSONGetParentById(self.schema, id);
if (host?.actionType) {
// 有 type 说明是旧的动作按钮,按钮本身就是某种动作,不需要再计算
// 没有 type 说明是 onEvent 里面的动作,引用的数量也就是自己是 1
return host.type ? 0 : 1;
} else if (host !== self.schema.definitions) {
return count;
}
const modalKey = Object.keys(host).find(key => host[key]?.$$id === id);
JSONTraverse(self.schema, (value: any, key: string, host: any) => {
if (
key === 'actionType' &&
['dialog', 'drawer', 'confirmDialog'].includes(value) &&
host[value === 'drawer' ? 'drawer' : 'dialog']?.$ref === modalKey
) {
count++;
}
return value;
});
return count;
},
removeModal(id: string) {
let schema = self.schema;
const host = JSONGetParentById(schema, id);
if (host === schema.definitions) {
const modalKey = Object.keys(host).find(
key => host[key]?.$$id === id
);
JSONTraverse(schema, (value: any, key: string, host: any) => {
if (
key === 'actionType' &&
['dialog', 'drawer', 'confirmDialog'].includes(value) &&
host[value === 'drawer' ? 'drawer' : 'dialog']?.$ref === modalKey
) {
schema = JSONDelete(schema, host.$$id);
}
return value;
});
schema = JSONDelete(schema, id);
} else {
schema = JSONDelete(schema, host.$$id);
}
this.traceableSetSchema(schema);
},
updateModal(
id: string,
modal: DialogSchema | DrawerSchema,
definitions?: any
) {
let schema = self.schema;
const parent = JSONGetParentById(schema, id);
if (!parent) {
throw new Error('modal not found');
}
if (definitions && isPlainObject(definitions)) {
schema = mergeDefinitions(schema, definitions, modal);
}
const newHostKey =
((modal as any).actionType || modal.type) === 'drawer'
? 'drawer'
: 'dialog';
schema = JSONUpdate(schema, id, modal);
// 如果编辑的是公共弹窗
if (!parent.actionType) {
const modalKey = Object.keys(parent).find(
key => parent[key]?.$$id === id
);
// 所有引用的地方都要更新
JSONTraverse(schema, (value: any, key: string, host: any) => {
if (
key === 'actionType' &&
['dialog', 'drawer', 'confirmDialog'].includes(value) &&
host[value === 'drawer' ? 'drawer' : 'dialog']?.$ref ===
modalKey &&
newHostKey !== (value === 'drawer' ? 'drawer' : 'dialog')
) {
schema = JSONUpdate(schema, host.$$id, {
actionType: (modal as any).actionType || modal.type,
args: undefined,
dialog: undefined,
drawer: undefined,
[newHostKey]: host[value === 'drawer' ? 'drawer' : 'dialog']
});
}
return value;
});
} else {
// 内嵌弹窗只用改自己就行了
schema = JSONUpdate(schema, parent.$$id, {
actionType: (modal as any).actionType || modal.type,
args: undefined,
dialog: undefined,
drawer: undefined,
[newHostKey]: JSONPipeIn(modal)
});
}
this.traceableSetSchema(schema);
// todo 更新弹出方式的配置
},
openSubEditor(context: SubEditorContext) {
const activeId = self.activeId;

View File

@ -15,6 +15,7 @@ import isEqual from 'lodash/isEqual';
import isNumber from 'lodash/isNumber';
import debounce from 'lodash/debounce';
import merge from 'lodash/merge';
import {EditorModalBody} from './store/editor';
const {
guid,
@ -1393,129 +1394,116 @@ export const scrollToActive = debounce((selector: string) => {
}
}, 200);
/**
*
* @param schema schema
* @param listType list或label value形式的数据源
* @param filterId id
*/
export const getDialogActions = (
schema: Schema,
listType: 'list' | 'source',
filterId?: string
) => {
let dialogActions: any[] = [];
JSONTraverse(
schema,
(value: any, key: string, object: any) => {
// definitions中的弹窗
if (key === 'type' && value === 'page') {
const definitions = object.definitions;
if (definitions) {
Object.keys(definitions).forEach(key => {
if (key.includes('ref-')) {
if (listType === 'list') {
dialogActions.push(definitions[key]);
} else {
const dialog = definitions[key];
const dialogTypeName =
dialog.type === 'drawer'
? '抽屉式弹窗'
: dialog.dialogType
? '确认对话框'
: '弹窗';
dialogActions.push({
label: `${dialog.title || '-'}${dialogTypeName}`,
value: dialog.$$id
});
}
}
});
}
}
if (
(key === 'actionType' && value === 'dialog') ||
(key === 'actionType' && value === 'drawer') ||
(key === 'actionType' && value === 'confirmDialog')
) {
const dialogBodyMap = new Map([
[
'dialog',
{
title: '弹窗',
body: 'dialog'
}
],
[
'drawer',
{
title: '抽屉式弹窗',
body: 'drawer'
}
],
[
'confirmDialog',
{
title: '确认对话框',
// 兼容历史args参数
body: ['dialog', 'args']
}
]
]);
let dialogBody = dialogBodyMap.get(value)?.body!;
let dialogBodyContent = Array.isArray(dialogBody)
? object[dialogBody[0]] || object[dialogBody[1]]
: object[dialogBody];
export function addModal(schema: any, modal: any, definitions?: any) {
schema = {...schema, definitions: {...schema.definitions}};
if (
dialogBodyMap.has(value) &&
dialogBodyContent &&
!dialogBodyContent.$ref
) {
if (listType == 'list') {
// 没有 type: dialog的历史数据兼容一下
dialogActions.push({
...dialogBodyContent,
type: Array.isArray(dialogBody) ? 'dialog' : dialogBody
});
} else {
// 新建弹窗切换到现有弹窗把自身过滤掉
if (!filterId || (filterId && filterId !== dialogBodyContent.id)) {
dialogActions.push({
label: `${dialogBodyContent?.title || '-'}${
dialogBodyMap.get(value)?.title
}`,
value: dialogBodyContent.$$id
});
}
}
}
}
},
(value, key) => key.toString().startsWith('__')
);
return dialogActions;
};
// 如果有传入definitions则合并到schema中
if (definitions && isPlainObject(definitions)) {
schema = mergeDefinitions(schema, definitions, modal);
}
let idx = 1;
while (true) {
if (!schema.definitions[`modal-ref-${idx}`]) {
break;
}
idx++;
}
modal = {
type: 'dialog',
body: [{type: 'tpl', tpl: '这是一个弹窗'}],
title: `未命名弹窗${idx}`,
...modal,
$$id: guid()
} as any;
schema.definitions[`modal-ref-${idx}`] = JSONPipeIn(modal);
return [schema, `modal-ref-${idx}`];
}
/**
* definitions,由于历史数据可能没有type: dialog,
* @param json
* @param previewDialogId
* definitions
*
*
*
* @param modals
* @param definitions
* @returns
*/
export const getFixDialogType = (json: Schema, dialogId: string) => {
const dialogBodyMap = {
dialog: 'dialog',
drawer: 'drawer',
confirmDialog: 'dialog'
export function modalsToDefinitions(
modals: Array<EditorModalBody>,
definitions: any = {}
) {
let schema = {
definitions
};
let parentSchema = JSONGetParentById(json, dialogId);
// 事件中的弹窗
if (parentSchema.actionType) {
return dialogBodyMap[parentSchema.actionType as keyof typeof dialogBodyMap];
}
// definitions中的弹窗
else {
let dialogRefSchema = JSONGetById(parentSchema, dialogId);
return dialogRefSchema.type;
}
};
modals.forEach((modal, idx) => {
if (modal.$$ref) {
schema.definitions[modal.$$ref] = JSONPipeIn(modal);
} else {
[schema] = addModal(schema, {...modal, $$originId: modal.$$id});
}
});
return schema.definitions;
}
/**
* definitions definitions
*
* @param originSchema
* @param definitions
* @param modal
* @returns
*/
export function mergeDefinitions(
originSchema: any,
definitions: any,
modal: any
) {
const refs: Array<string> = [];
JSONTraverse(modal, (value, key) => {
if (key === '$ref') {
refs.push(value);
}
});
let schema = originSchema;
Object.keys(definitions).forEach(key => {
// 弹窗里面用到了才更新
if (!refs.includes(key)) {
return;
}
// 要修改就复制一份,避免污染原始数据
if (schema === originSchema) {
schema = {...schema, definitions: {...schema.definitions}};
}
const {$$originId, ...def} = definitions[key];
if ($$originId) {
const parent = JSONGetParentById(schema, $$originId);
if (!parent) {
throw new Error('Can not find modal action.');
}
const modalType = def.type === 'drawer' ? 'drawer' : 'dialog';
schema = JSONUpdate(schema, parent.$$id, {
...parent,
__actionModals: undefined,
args: undefined,
dialog: undefined,
drawer: undefined,
actionType: def.actionType ?? modalType,
[modalType]: JSONPipeIn({
$ref: key
})
});
schema.definitions[key] = JSONPipeIn(def);
} else {
schema.definitions[key] = JSONPipeIn(def);
}
});
return schema;
}

View File

@ -15,19 +15,18 @@ import {InputBoxProps} from '../../../amis-ui/src/components/InputBox';
import cx from 'classnames';
import {getEnv} from 'mobx-state-tree';
import {pick} from 'lodash';
import {LocaleProps} from 'amis-core';
/** 语料 key 正则表达式 */
export const corpusKeyReg =
/^i18n:[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/;
interface InputTextI18nProps extends InputBoxProps {
interface InputTextI18nProps extends InputBoxProps, LocaleProps {
classPrefix: string;
i18nEnabled?: boolean;
disabled?: boolean;
locale?: string;
maxLength?: number;
minLength?: number;
translate?: (value: any) => any;
onI18nChange?: (value: string) => void;
}

View File

@ -1,6 +1,6 @@
{
"name": "amis-editor",
"version": "6.1.0",
"version": "6.2.1",
"description": "amis 可视化编辑器",
"main": "lib/index.js",
"module": "esm/index.js",
@ -41,7 +41,7 @@
],
"dependencies": {
"@webcomponents/webcomponentsjs": "^2.6.0",
"amis-editor-core": "^6.1.0",
"amis-editor-core": "^6.2.1",
"amis-postcss": "1.0.0",
"amis-theme-editor-helper": "*",
"i18n-runtime": "*",

View File

@ -0,0 +1,69 @@
import {
EditorManager,
EditorStoreType,
SchemaFrom,
getSchemaTpl
} from 'amis-editor-core';
import {observer} from 'mobx-react';
import React from 'react';
export interface ModalSettingPanelProps {
store: EditorStoreType;
manager: EditorManager;
popOverContainer: any;
}
export default observer(function ({
store,
manager,
popOverContainer
}: ModalSettingPanelProps) {
const body = React.useMemo(() => {
return [
getSchemaTpl('collapseGroup', [
{
title: '弹窗入参',
body: [
{
type: 'json-schema-editor',
name: 'inputParams',
label: false,
mini: true,
disabledTypes: ['array'],
evalMode: true,
// variables: '${variables}',
addButtonText: '添加入参'
}
]
}
])
];
}, []);
const node = store.root.firstChild;
const value = store.getValueOf(node.id);
const onChange = React.useCallback((value: any, diff: any) => {
manager.panelChangeValue(value, diff, undefined, node.id);
}, []);
const env = React.useMemo(
() => ({...manager.env, session: 'left-panel-form'}),
[]
);
return (
<div className="ae-Outline-panel">
<div className="panel-header"></div>
<SchemaFrom
body={body}
value={value}
onChange={onChange}
submitOnChange={true}
env={env}
// popOverContainer={popOverContainer}
node={node}
manager={manager}
justify={true}
/>
</div>
);
});

View File

@ -0,0 +1,3 @@
<svg t="1709803052887" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1488" width="16px" height="16px">
<path d="M793.6 960H230.4C140.8 960 64 883.2 64 793.6V230.4C64 140.8 140.8 64 230.4 64h563.2C883.2 64 960 140.8 960 230.4v563.2c0 89.6-76.8 166.4-166.4 166.4z m108.8-729.6c0-64-51.2-115.2-115.2-115.2H230.4c-64 0-115.2 51.2-115.2 115.2v563.2c0 64 51.2 115.2 115.2 115.2h563.2c64 0 115.2-51.2 115.2-115.2V230.4z m-499.2 563.2h390.4v57.6H403.2v-57.6zM512 678.4v57.6H288v-19.2l-19.2 19.2-38.4-38.4 467.2-467.2 38.4 38.4-409.6 409.6H512z" fill="currentColor" p-id="1489"></path>
</svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -105,6 +105,7 @@ import inputRepeat from './form/input-repeat.svg';
import inputRichText from './form/input-rich-text.svg';
import inputTag from './form/input-tag.svg';
import inputText from './form/input-text.svg';
import InputSignature from './form/input-signature.svg';
import inputTime from './form/input-time.svg';
import inputTree from './form/input-tree.svg';
@ -158,6 +159,7 @@ import layout_fixed_top from './layout/layout-fixed-top.svg';
// 其他类 icon
import inputAddFx from './other/+fx.svg';
import inputFx from './other/fx.svg';
import modalSetting from './other/modal-setting.svg';
// 属性配置面板/显示类型
import block from './display/block.svg';
@ -282,6 +284,7 @@ registerIcon('input-rich-text-plugin', inputRichText);
registerIcon('input-tag-plugin', inputTag);
registerIcon('input-text-plugin', inputText);
registerIcon('input-time-range-plugin', inputTimeRange);
registerIcon('input-signature-plugin', InputSignature);
registerIcon('input-time-plugin', inputTime);
registerIcon('input-tree-plugin', inputTree);
@ -313,6 +316,7 @@ registerIcon('formula-plugin', formula);
registerIcon('property-sheet-plugin', propertySheet);
registerIcon('tooltip-plugin', tooltip);
registerIcon('divider-plugin', divider);
registerIcon('modal-setting', modalSetting);
// 常见布局组件 icon x 13
registerIcon('layout-absolute-plugin', layout_absolute);

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -32,6 +32,7 @@ import './renderer/style-control/BoxShadow';
import './renderer/style-control/Background';
import './renderer/style-control/Display';
import './renderer/style-control/InsetBoxModel';
import './renderer/style-control/FlexLayout';
import './renderer/RangePartsControl';
import './renderer/DataBindingControl';
import './renderer/DataMappingControl';

View File

@ -17,6 +17,7 @@ import {
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
import set from 'lodash/set';
import merge from 'lodash/merge';
import {escapeFormula, resolveArrayDatasource} from '../util';
export class CardsPlugin extends BasePlugin {
@ -574,7 +575,12 @@ export class CardsPlugin extends BasePlugin {
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.card = merge(
{
className: `${props.card.className || ''} ae-Editor-listItem`
},
props.card
);
}
// 列表类型内的文本元素显示原始公式

View File

@ -1,3 +1,6 @@
import React from 'react';
import {Button} from 'amis';
import {Icon} from 'amis-editor-core';
import {
ActiveEventContext,
BaseEventContext,
@ -11,6 +14,8 @@ import {
RendererPluginEvent
} from 'amis-editor-core';
import {getEventControlConfig} from '../renderer/event-control';
import {EditorNodeType} from 'packages/amis-editor-core/lib';
import {defaultFlexColumnSchema} from './Layout/FlexPluginBase';
export class ContainerPlugin extends LayoutBasePlugin {
static id = 'ContainerPlugin';
@ -32,8 +37,12 @@ export class ContainerPlugin extends LayoutBasePlugin {
type: 'container',
body: [],
style: {
position: 'static',
display: 'block'
position: 'relative',
display: 'flex',
inset: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
alignItems: 'flex-start'
},
size: 'none',
wrapperBody: false
@ -123,6 +132,183 @@ export class ContainerPlugin extends LayoutBasePlugin {
}
];
onActive(event: PluginEvent<ActiveEventContext>) {
const context = event.context;
if (context.info?.plugin !== this || !context.node) {
return;
}
const node = context.node!;
const isFlexItem = this.manager?.isFlexItem(node.id);
if (isFlexItem && context.node.parent?.children?.length > 1) {
let isColumnFlex = String(node.schema?.style?.flexDirection).includes(
'column'
);
console.log(
'isColumnFlex',
isColumnFlex,
node.schema?.style?.flexDirection
);
// context?.node.setHeightMutable(isColumnFlex);
context?.node.setWidthMutable(!isColumnFlex);
}
}
afterUpdate(event: PluginEvent<ActiveEventContext>) {
const node = event.context?.node;
}
onWidthChangeStart(
event: PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
}
>
) {
const context = event.context;
const node = context.node;
const host = node.host;
const dom = context.dom;
const parent = dom.parentElement as HTMLElement;
if (!parent) {
return;
}
console.log('on width change start');
const resizer = context.resizer;
const frameRect = parent.getBoundingClientRect();
const rect = dom.getBoundingClientRect();
const isFlexItem = this.manager?.isFlexItem(node.id);
const schema = node.schema;
const index = node.index;
const isFlexSize =
schema.style?.flex === '1 1 auto' &&
(schema.style?.position === 'static' ||
schema.style?.position === 'relative');
let flexGrow = 1;
let width = 0;
event.setData({
onMove: (e: MouseEvent) => {
const children = parent.children;
width = e.pageX - rect.left;
flexGrow = Math.max(
1,
Math.min(12, Math.round((12 * width) / frameRect.width))
);
resizer.setAttribute(
'data-value',
isFlexSize ? `${flexGrow}` : width + 'px'
);
if (isFlexSize) {
// 需重新计算flex下各子组件占比按照12等分计算
for (let i = 0; i < children.length; i++) {
if (i !== index) {
let width = children[i].clientWidth;
if (width > 0) {
let grow = Math.max(
1,
Math.min(12, Math.round((12 * width) / frameRect.width))
);
host.children[i]?.updateState({
style: {
...host.children[i].schema.style,
flexGrow: grow
}
});
}
} else {
node.updateState({
style: {
...node.schema.style,
flexGrow: +flexGrow
}
});
}
}
} else {
if (isFlexItem) {
node.updateState({
style: {
...node.schema.style,
flex: '0 0 150px',
flexBasis: `${width}px`
}
});
} else {
node.updateState({
style: {
...node.schema.style,
width: `${width}px`
}
});
}
}
requestAnimationFrame(() => {
node.calculateHighlightBox();
});
},
onEnd: () => {
resizer.removeAttribute('data-value');
if (isFlexSize) {
host?.children.forEach((item: EditorNodeType) => {
item.updateSchema({
style: {
...node.schema.style,
flexGrow: item.state.style?.flexGrow ?? 1
}
});
item.updateState({}, true);
});
} else {
if (isFlexItem) {
node.updateSchema({
style: {
...node.schema.style,
flex: `0 0 150px`,
flexBasis: `${width}px`
}
});
} else {
node.updateSchema({
style: {
...node.schema.style,
width: `${width}px`
}
});
}
node.updateState({}, true);
}
requestAnimationFrame(() => {
node.calculateHighlightBox();
});
}
});
}
onHeightChangeStart(
event: PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
}
>
) {
console.log('on height change start');
// return this.onSizeChangeStart(event, 'vertical');
}
panelBodyCreator = (context: BaseEventContext) => {
const curRendererSchema = context?.schema;
const isRowContent =
@ -132,6 +318,19 @@ export class ContainerPlugin extends LayoutBasePlugin {
const isFreeContainer = curRendererSchema?.isFreeContainer || false;
const isFlexItem = this.manager?.isFlexItem(context?.id);
const isFlexColumnItem = this.manager?.isFlexColumnItem(context?.id);
const node = context.node;
const parent = node.parent?.schema;
const draggableContainer = this.manager.draggableContainer(context?.id);
const canAppendSiblings =
parent &&
isFlexItem &&
!draggableContainer &&
this.manager?.canAppendSiblings();
const newItemSchema = isFlexColumnItem
? defaultFlexColumnSchema('', false)
: defaultFlexColumnSchema();
const displayTpl = [
getSchemaTpl('layout:display'),
@ -154,53 +353,122 @@ export class ContainerPlugin extends LayoutBasePlugin {
{
title: '属性',
body: getSchemaTpl('collapseGroup', [
// {
// title: '基本',
// body: [
// {
// name: 'wrapperComponent',
// label: '容器标签',
// type: 'select',
// searchable: true,
// options: [
// 'div',
// 'p',
// 'h1',
// 'h2',
// 'h3',
// 'h4',
// 'h5',
// 'h6',
// 'article',
// 'aside',
// 'code',
// 'footer',
// 'header',
// 'section'
// ],
// pipeIn: defaultValue('div'),
// validations: {
// isAlphanumeric: true,
// matchRegexp: '/^(?!.*script).*$/' // 禁用一下script标签
// },
// validationErrors: {
// isAlpha: 'HTML标签不合法请重新输入',
// matchRegexp: 'HTML标签不合法请重新输入'
// },
// validateOnChange: false
// },
// getSchemaTpl('layout:padding')
// ]
// },
{
title: '基本',
body: [
{
name: 'wrapperComponent',
label: '容器标签',
type: 'select',
searchable: true,
options: [
'div',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'article',
'aside',
'code',
'footer',
'header',
'section'
],
pipeIn: defaultValue('div'),
validations: {
isAlphanumeric: true,
matchRegexp: '/^(?!.*script).*$/' // 禁用一下script标签
},
validationErrors: {
isAlpha: 'HTML标签不合法请重新输入',
matchRegexp: 'HTML标签不合法请重新输入'
},
validateOnChange: false
canAppendSiblings && {
type: 'wrapper',
size: 'none',
className: 'grid grid-cols-2 gap-4 mb-4',
body: [
{
children: (
<Button
size="sm"
onClick={() =>
this.manager.appendSiblingSchema(
newItemSchema,
true,
true
)
}
>
<Icon
className="icon"
icon={
isFlexColumnItem
? 'top-arrow-to-top'
: 'left-arrow-to-left'
}
/>
<span>
{isFlexColumnItem ? '上方' : '左侧'}
{isFlexColumnItem ? '行' : '列'}
</span>
</Button>
)
},
{
children: (
<Button
size="sm"
onClick={() =>
this.manager.appendSiblingSchema(
newItemSchema,
false,
true
)
}
>
<Icon
className="icon"
icon={
isFlexColumnItem
? 'arrow-to-bottom'
: 'arrow-to-right'
}
/>
<span>
{isFlexColumnItem ? '下方' : '右侧'}
{isFlexColumnItem ? '行' : '列'}
</span>
</Button>
)
}
]
},
getSchemaTpl('layout:padding')
]
},
{
title: '布局',
body: [
getSchemaTpl('layout:position', {
visibleOn: '!data.stickyStatus'
getSchemaTpl('theme:paddingAndMargin', {
name: 'themeCss.baseControlClassName.padding-and-margin:default'
}),
getSchemaTpl('layout:originPosition'),
getSchemaTpl('layout:inset', {
mode: 'vertical'
getSchemaTpl('theme:border', {
name: `themeCss.baseControlClassName.border:default`
}),
getSchemaTpl('theme:colorPicker', {
name: 'themeCss.baseControlClassName.background:default',
label: '背景',
needCustom: true,
needGradient: true,
needImage: true,
labelMode: 'input'
}),
// 自由容器不需要 display 相关配置项
@ -263,7 +531,12 @@ export class ContainerPlugin extends LayoutBasePlugin {
getSchemaTpl('layout:isFixedWidth', {
visibleOn: `${!isFlexItem || isFlexColumnItem}`,
onChange: (value: boolean) => {
context?.node.setWidthMutable(value);
if (
!isFlexItem ||
context.node.parent?.children?.length > 1
) {
context?.node.setWidthMutable(value);
}
}
}),
getSchemaTpl('layout:width', {
@ -295,15 +568,27 @@ export class ContainerPlugin extends LayoutBasePlugin {
'data.style && data.style.display !== "flex" && data.style.display !== "inline-flex"'
})
: null,
getSchemaTpl('layout:z-index'),
getSchemaTpl('layout:z-index')
]
},
getSchemaTpl('status'),
{
title: '高级',
body: [
getSchemaTpl('layout:position', {
visibleOn: '!data.stickyStatus'
}),
getSchemaTpl('layout:originPosition'),
getSchemaTpl('layout:inset', {
mode: 'vertical'
}),
getSchemaTpl('layout:sticky', {
visibleOn:
'data.style && (data.style.position !== "fixed" && data.style.position !== "absolute")'
}),
getSchemaTpl('layout:stickyPosition')
]
},
getSchemaTpl('status')
}
])
},
{

View File

@ -1,5 +1,5 @@
import React from 'react';
import {Button, Drawer, Modal} from 'amis-ui';
import {Button, Drawer, Icon, Modal} from 'amis-ui';
import {
registerEditorPlugin,
BaseEventContext,
@ -11,12 +11,19 @@ import {
defaultValue,
EditorNodeType,
isEmpty,
getI18nEnabled
getI18nEnabled,
BuildPanelEventContext,
BasicPanelItem,
PluginEvent,
ChangeEventContext,
JSONPipeOut
} from 'amis-editor-core';
import {getEventControlConfig} from '../renderer/event-control/helper';
import omit from 'lodash/omit';
import type {RendererConfig, Schema} from 'amis-core';
import {ModalProps} from 'amis-ui/src/components/Modal';
import ModalSettingPanel from '../component/ModalSettingPanel';
import find from 'lodash/find';
interface InlineModalProps extends ModalProps {
type: string;
@ -139,19 +146,45 @@ export class DialogPlugin extends BasePlugin {
{
title: '基本',
body: [
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
type: 'input-text',
label: '组件名称',
name: 'editorSetting.displayName'
},
{
type: 'radios',
label: '弹出方式',
name: 'actionType',
pipeIn: (value: any, store: any, data: any) =>
value ?? data.type,
inline: false,
options: [
{
label: '弹窗',
value: 'dialog'
},
{
label: '抽屉',
value: 'drawer'
},
{
label: '确认对话框',
value: 'confirmDialog'
}
]
},
{
label: '标题',
type: 'input-text',
name: 'title'
},
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
label: '确认按钮文案',
type: 'input-text',
name: 'confirmText'
},
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
label: '取消按钮文案',
type: 'input-text',
@ -223,12 +256,43 @@ export class DialogPlugin extends BasePlugin {
{
title: '基本',
body: [
{
type: 'input-text',
label: '组件名称',
name: 'editorSetting.displayName'
},
{
type: 'radios',
label: '弹出方式',
name: 'actionType',
pipeIn: (value: any, store: any, data: any) =>
value ?? data.type,
inline: false,
options: [
{
label: '弹窗',
value: 'dialog'
},
{
label: '抽屉',
value: 'drawer'
},
{
label: '确认对话框',
value: 'confirmDialog'
}
]
},
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
label: '标题',
type: i18nEnabled ? 'input-text-i18n' : 'input-text',
name: 'title'
},
getSchemaTpl('switch', {
label: '展示关闭按钮',
name: 'showCloseButton',
@ -474,6 +538,37 @@ export class DialogPlugin extends BasePlugin {
]);
};
afterUpdate(event: PluginEvent<ChangeEventContext>) {
const context = event.context;
// 当弹出方式改变的时候,切换渲染器类型
if (
context.info.renderer.type &&
['dialog', 'drawer'].includes(context.info.renderer.type) &&
context.diff?.some(change => change.path?.join('.') === 'actionType')
) {
const change: any = find(
context.diff,
change => change.path?.join('.') === 'actionType'
)!;
let value = change?.rhs;
const newType = value === 'drawer' ? 'drawer' : 'dialog';
if (
newType !== context.schema.type &&
this.manager.replaceChild(context.id, {
...context.schema,
type: value === 'drawer' ? 'drawer' : 'dialog'
})
) {
setTimeout(() => {
this.manager.rebuild();
}, 4);
}
}
}
buildSubRenderers() {}
async buildDataSchemas(
@ -483,7 +578,10 @@ export class DialogPlugin extends BasePlugin {
) {
const renderer = this.manager.store.getNodeById(node.id)?.getComponent();
const data = omit(renderer.props.$schema.data, '$$id');
let dataSchema: any = {};
const inputParams = JSONPipeOut(renderer.props.$schema.inputParams);
let dataSchema: any = {
...inputParams?.properties
};
if (renderer.props.$schema.data === undefined || !isEmpty(data)) {
// 静态数据
@ -515,6 +613,7 @@ export class DialogPlugin extends BasePlugin {
return {
$id: 'dialog',
type: 'object',
...inputParams,
title: node.schema?.label || node.schema?.name,
properties: dataSchema
};
@ -548,6 +647,33 @@ export class DialogPlugin extends BasePlugin {
].filter((item: any) => item)
};
}
buildEditorPanel(
context: BuildPanelEventContext,
panels: Array<BasicPanelItem>
) {
if (
this.manager.store.isSubEditor &&
['dialog', 'drawer'].includes(this.manager.store.schema?.type)
) {
panels.push({
key: 'modal-setting',
icon: '', // 'fa fa-code',
title: (
<span
className="editor-tab-icon editor-tab-s-icon"
editor-tooltip="弹窗参数"
>
<Icon icon="modal-setting" />
</span>
),
position: 'left',
component: ModalSettingPanel,
order: -99999
});
}
super.buildEditorPanel(context, panels);
}
}
registerEditorPlugin(DialogPlugin);

View File

@ -9,7 +9,8 @@ import {
noop,
EditorNodeType,
isEmpty,
getI18nEnabled
getI18nEnabled,
JSONPipeOut
} from 'amis-editor-core';
import {getEventControlConfig} from '../renderer/event-control/helper';
import {tipedLabel} from 'amis-editor-core';
@ -124,6 +125,35 @@ export class DrawerPlugin extends BasePlugin {
{
title: '基本',
body: [
{
type: 'input-text',
label: '组件名称',
name: 'editorSetting.displayName'
},
{
type: 'radios',
label: '弹出方式',
name: 'actionType',
pipeIn: (value: any, store: any, data: any) =>
value ?? data.type,
inline: false,
options: [
{
label: '弹窗',
value: 'dialog'
},
{
label: '抽屉',
value: 'drawer'
},
{
label: '确认对话框',
value: 'confirmDialog'
}
]
},
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
label: '标题',
@ -355,7 +385,10 @@ export class DrawerPlugin extends BasePlugin {
) {
const renderer = this.manager.store.getNodeById(node.id)?.getComponent();
const data = omit(renderer.props.$schema.data, '$$id');
let dataSchema: any = {};
const inputParams = JSONPipeOut(renderer.props.$schema.inputParams);
let dataSchema: any = {
...inputParams?.properties
};
if (renderer.props.$schema.data === undefined || !isEmpty(data)) {
// 静态数据
@ -387,6 +420,7 @@ export class DrawerPlugin extends BasePlugin {
return {
$id: 'drawer',
type: 'object',
...inputParams,
title: node.schema?.label || node.schema?.name,
properties: dataSchema
};

View File

@ -4,7 +4,8 @@ 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';
import merge from 'lodash/merge';
import set from 'lodash/set';
export class EachPlugin extends BasePlugin {
static id = 'EachPlugin';
@ -362,10 +363,13 @@ export class EachPlugin extends BasePlugin {
props.value = [{}, {}];
props.className = `${props.className || ''} ae-Editor-list`;
if (props.items && !props.items.className?.includes('listItem')) {
props.items.className = `${
props.items.className || ''
} ae-Editor-eachItem`;
if (props.items && !props.items.className?.includes('eachItem')) {
props.items = merge(
{
className: `${props.items.className || ''} ae-Editor-eachItem`
},
props.items
);
}
return props;

View File

@ -0,0 +1,155 @@
import {EditorNodeType, getSchemaTpl} from 'amis-editor-core';
import {registerEditorPlugin} from 'amis-editor-core';
import {BasePlugin, BaseEventContext} from 'amis-editor-core';
import {ValidatorTag} from '../../validator';
import {getEventControlConfig} from '../../renderer/event-control/helper';
import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core';
export class SignaturePlugin extends BasePlugin {
static id = 'SignaturePlugin';
// 关联渲染器名字
rendererName = 'input-signature';
$schema = '/schemas/InputSignatureSchema.json';
// 组件名称
name = '签名面板';
isBaseComponent = true;
icon = 'fa fa-star-o';
pluginIcon = 'input-signature-plugin';
description = '手写签名面板';
docLink = '/amis/zh-CN/components/form/input-signature';
tags = ['表单项'];
scaffold = {
type: 'input-signature',
label: '签名',
name: 'signature'
};
previewSchema: any = {
type: 'form',
className: 'text-left',
mode: 'horizontal',
wrapWithPanel: false,
body: [
{
...this.scaffold,
embed: true
}
]
};
notRenderFormZone = true;
panelTitle = '签名面板';
// 事件定义
events: RendererPluginEvent[] = [];
// 动作定义
actions: RendererPluginAction[] = [];
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => {
return getSchemaTpl('tabs', [
{
title: '属性',
body: getSchemaTpl('collapseGroup', [
{
title: '基本',
body: [
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
getSchemaTpl('formItemName', {
required: true
}),
getSchemaTpl('label'),
getSchemaTpl('labelRemark'),
{
name: 'embed',
label: '弹窗展示',
type: 'ae-switch-more',
mode: 'normal',
formType: 'extend',
title: '弹窗展示',
bulk: true,
form: {
body: [
{
label: '按钮文案',
name: 'embedBtnLabel',
type: 'input-text'
},
getSchemaTpl('icon', {
label: '按钮图标',
name: 'embedBtnIcon'
}),
{
label: '确认按钮',
name: 'embedConfirmLabel',
type: 'input-text'
},
{
label: '确认按钮',
name: 'ebmedCancelLabel',
type: 'input-text'
}
]
}
},
{
label: '确认按钮',
name: 'confirmBtnLabel',
type: 'input-text'
},
{
label: '撤销按钮',
name: 'undoBtnLabel',
type: 'input-text'
},
{
label: '清空按钮',
name: 'clearBtnLabel',
type: 'input-text'
}
]
},
getSchemaTpl('status', {
isFormItem: true,
unsupportStatic: true
}),
getSchemaTpl('validation', {
tag: ValidatorTag.Check
})
])
},
{
title: '外观',
body: [
getSchemaTpl('collapseGroup', [
getSchemaTpl('style:formItem', {
renderer: context.info.renderer
}),
getSchemaTpl('style:classNames', {unsupportStatic: true})
])
]
},
{
title: '事件',
className: 'p-none',
body: [
getSchemaTpl('eventControl', {
name: 'onEvent',
...getEventControlConfig(this.manager, context)
})
]
}
]);
};
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
return {
type: 'number',
title: node.schema?.label || node.schema?.name,
originalValue: node.schema?.value // 记录原始值,循环引用检测需要
};
}
}
registerEditorPlugin(SignaturePlugin);

View File

@ -1,4 +1,9 @@
import {getSchemaTpl, valuePipeOut} from 'amis-editor-core';
import {
defaultValue,
getSchemaTpl,
undefinedPipeOut,
valuePipeOut
} from 'amis-editor-core';
import {registerEditorPlugin, tipedLabel} from 'amis-editor-core';
import {BasePlugin, BaseEventContext} from 'amis-editor-core';
import {ValidatorTag} from '../../validator';
@ -9,7 +14,8 @@ import type {
RendererPluginAction,
RendererPluginEvent
} from 'amis-editor-core';
import {isExpression} from 'amis-core';
import {isExpression, isPureVariable} from 'amis-core';
import omit from 'lodash/omit';
export class SwitchControlPlugin extends BasePlugin {
static id = 'SwitchControlPlugin';
@ -123,49 +129,58 @@ export class SwitchControlPlugin extends BasePlugin {
body: [getSchemaTpl('onText'), getSchemaTpl('offText')]
}
},
{
type: 'ae-switch-more',
bulk: true,
hiddenOnDefault: false,
mode: 'normal',
label: tipedLabel(
'值格式',
'默认勾选后的值 true未勾选的值 false'
),
label: '值格式',
formType: 'extend',
form: {
body: [
{
type: 'input-text',
label: '勾选后的值',
type: 'ae-valueFormat',
name: 'trueValue',
value: true,
pipeOut: valuePipeOut,
label: '开启时',
pipeIn: defaultValue(true),
pipeOut: undefinedPipeOut,
onChange: (
value: string,
oldValue: string,
value: any,
oldValue: any,
model: any,
form: any
) => {
if (oldValue === form.getValueByName('value')) {
form.setValueByName('value', value);
const {value: defaultValue, trueValue} =
form?.data || {};
if (isPureVariable(defaultValue)) {
return;
}
if (trueValue === defaultValue && trueValue !== value) {
form.setValues({value});
}
}
},
{
type: 'input-text',
label: '未勾选的值',
type: 'ae-valueFormat',
name: 'falseValue',
value: false,
pipeOut: valuePipeOut,
label: '关闭时',
pipeIn: defaultValue(false),
pipeOut: undefinedPipeOut,
onChange: (
value: string,
oldValue: string,
value: any,
oldValue: any,
model: any,
form: any
) => {
if (oldValue === form.getValueByName('value')) {
form.setValueByName('value', value);
const {value: defaultValue, falseValue} =
form?.data || {};
if (isPureVariable(defaultValue)) {
return;
}
if (
falseValue === defaultValue &&
falseValue !== value
) {
form.setValues({value});
}
}
}
@ -189,18 +204,18 @@ export class SwitchControlPlugin extends BasePlugin {
}),
*/
getSchemaTpl('valueFormula', {
rendererSchema: context?.schema,
rendererSchema: {
...omit(context?.schema, ['trueValue', 'falseValue']),
type: 'switch'
},
needDeleteProps: ['option'],
rendererWrapper: true, // 浅色线框包裹一下,增加边界感
// valueType: 'boolean',
valueType: 'boolean',
pipeIn: (value: any, data: any) => {
const {trueValue = true, falseValue = false} =
data.data || {};
return value === trueValue
? true
: value === falseValue
? false
: value;
if (isPureVariable(value)) {
return value;
}
return value === (data?.data?.trueValue ?? true);
},
pipeOut: (value: any, origin: any, data: any) => {
// 如果是表达式,直接返回

View File

@ -90,7 +90,8 @@ export class TreeSelectControlPlugin extends BasePlugin {
eventLabel: '获取焦点',
description: '输入框获取焦点时触发',
dataSchema: (manager: EditorManager) => {
const {value, items} = resolveOptionEventDataSchame(manager);
const {value, items, itemSchema} =
resolveOptionEventDataSchame(manager);
return [
{
@ -101,6 +102,11 @@ export class TreeSelectControlPlugin extends BasePlugin {
title: '数据',
properties: {
value,
item: {
type: 'object',
title: '选中的项',
properties: itemSchema
},
items
}
}
@ -114,7 +120,8 @@ export class TreeSelectControlPlugin extends BasePlugin {
eventLabel: '失去焦点',
description: '输入框失去焦点时触发',
dataSchema: (manager: EditorManager) => {
const {value, items} = resolveOptionEventDataSchame(manager);
const {value, items, itemSchema} =
resolveOptionEventDataSchame(manager);
return [
{
@ -125,6 +132,11 @@ export class TreeSelectControlPlugin extends BasePlugin {
title: '数据',
properties: {
value,
item: {
type: 'object',
title: '选中的项',
properties: itemSchema
},
items
}
}

View File

@ -514,6 +514,7 @@ export class ImagePlugin extends BasePlugin {
resizer.removeAttribute('data-value');
node.updateSchema(state);
node.updateState({}, true);
requestAnimationFrame(() => {
node.calculateHighlightBox();
});

View File

@ -1,9 +1,15 @@
/**
* @file Flex 1:3
*/
import {PlainObject} from 'amis-core';
import {LayoutBasePlugin, PluginEvent} from 'amis-editor-core';
import {getSchemaTpl, tipedLabel} from 'amis-editor-core';
import React from 'react';
import {
JSONPipeOut,
LayoutBasePlugin,
PluginEvent,
reGenerateID
} from 'amis-editor-core';
import {getSchemaTpl} from 'amis-editor-core';
import {Button, PlainObject} from 'amis';
import type {
BaseEventContext,
EditorNodeType,
@ -12,20 +18,29 @@ import type {
BasicToolbarItem,
PluginInterface
} from 'amis-editor-core';
import {Icon} from 'amis-editor-core';
import {JSONChangeInArray, JSONPipeIn, repeatArray} from 'amis-editor-core';
import {isAlive} from 'mobx-state-tree';
// 默认的列容器Schema
export const defaultFlexColumnSchema = (title?: string) => {
export const defaultFlexColumnSchema = (
title?: string,
disableFlexBasis: boolean = true
) => {
let style: PlainObject = {
position: 'static',
display: 'block',
flex: '1 1 auto',
flexGrow: 1
};
if (disableFlexBasis) {
style.flexBasis = 0;
}
return {
type: 'container',
body: [],
size: 'none',
style: {
position: 'static',
display: 'block',
flex: '1 1 auto',
flexGrow: 1,
flexBasis: 0
},
style,
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
@ -88,9 +103,66 @@ export class FlexPluginBase extends LayoutBasePlugin {
panelJustify = true; // 右侧配置项默认左右展示
// 设置分栏的默认布局比例
setFlexLayout = (node: EditorNodeType, value: string) => {
if (/^[\d:]+$/.test(value) && isAlive(node)) {
let list = value.trim().split(':');
let children = node.children || [];
if (String(node.schema?.style?.flexDirection).includes('column')) {
list = list.reverse();
node.updateSchemaStyle({
flexDirection: 'row'
});
}
// 更新flex布局
for (let i = 0; i < children.length; i++) {
let child = children[i];
child.updateSchemaStyle({
flexGrow: +list[i],
width: undefined,
flexBasis: 0,
flex: '1 1 auto'
});
}
// 增加或删除列
if (children.length < list.length) {
for (let i = 0; i < list.length - children.length; i++) {
let newColumnSchema = defaultFlexColumnSchema();
newColumnSchema.style.flexGrow = +list[i];
this.manager.addElem(newColumnSchema, true, false);
}
} else if (children.length > list.length) {
// 如果删除的列里面存在元素截断生成新的flex放在组件后面
const newSchema = JSONPipeIn(JSONPipeOut(node.schema));
newSchema.items = newSchema.items.slice(list.length);
node.updateSchema({
items: node.schema.items.slice(0, list.length)
});
if (
(newSchema.items as PlainObject[]).some(
(item, index) => item.body?.length
)
) {
const parent = node.parent;
this.manager.addChild(
parent.id,
parent.region,
newSchema,
parent?.children?.[node.index + 1]?.id
);
}
}
}
return undefined;
};
resetFlexBasis = (node: EditorNodeType, flexSetting: PlainObject = {}) => {
let schema = node.schema;
if (
String(flexSetting.flexDirection).includes('column') &&
!schema?.style?.height
@ -108,6 +180,30 @@ export class FlexPluginBase extends LayoutBasePlugin {
}
};
insertItem = (node: EditorNodeType, direction: string) => {
if (node.info?.plugin !== this) {
return;
}
const store = this.manager.store;
const newSchema = JSONPipeIn(JSONPipeOut(node.schema));
const parent = node.parent;
const nextId =
direction === 'upper' ? node.id : parent?.children?.[node.index + 1]?.id;
const child = this.manager.addChild(
parent.id,
parent.region,
newSchema,
nextId
);
if (child) {
// mobx 修改数据是异步的
setTimeout(() => {
store.setActiveId(child.$$id);
}, 100);
}
};
panelBodyCreator = (context: BaseEventContext) => {
const curRendererSchema = context?.schema || {};
const isRowContent =
@ -137,15 +233,82 @@ export class FlexPluginBase extends LayoutBasePlugin {
body: [
getSchemaTpl('collapseGroup', [
{
title: '布局',
title: '基础',
body: [
isSorptionContainer ? getSchemaTpl('layout:sorption') : null,
context.node &&
getSchemaTpl('layout:flex-layout', {
name: 'layout',
label: '快捷版式设置',
pipeIn: () => {
if (isAlive(context.node)) {
let children = context.node?.children || [];
if (
children.every(
item => item.schema?.style?.flex === '1 1 auto'
)
) {
return children
.map(item => item.schema?.style?.flexGrow || 1)
.join(':');
}
}
return undefined;
},
pipeOut: (value: string) =>
this.setFlexLayout(context.node, value)
}),
// 吸附容器不显示定位相关配置项
...(isSorptionContainer ? [] : positionTpl),
{
type: 'wrapper',
size: 'none',
className: 'grid grid-cols-2 gap-4 mb-4',
body: [
{
children: (
<Button
size="sm"
onClick={() =>
this.insertItem(context.node, 'under')
}
>
<Icon className="icon" icon="arrow-to-bottom" />
<span></span>
</Button>
)
},
{
children: (
<Button
size="sm"
onClick={() =>
this.insertItem(context.node, 'upper')
}
>
<Icon className="icon" icon="top-arrow-to-top" />
<span></span>
</Button>
)
}
]
},
getSchemaTpl('theme:paddingAndMargin', {
name: 'themeCss.baseControlClassName.padding-and-margin:default'
}),
getSchemaTpl('theme:border', {
name: `themeCss.baseControlClassName.border:default`
}),
getSchemaTpl('theme:colorPicker', {
name: 'themeCss.baseControlClassName.background:default',
label: '背景',
needCustom: true,
needGradient: true,
needImage: true,
labelMode: 'input'
}),
getSchemaTpl('layout:flex-setting', {
label: '弹性布局设置',
label: '内部对齐设置',
direction: curRendererSchema.direction,
justify: curRendererSchema.justify || 'center',
alignItems: curRendererSchema.alignItems,
@ -258,7 +421,15 @@ export class FlexPluginBase extends LayoutBasePlugin {
getSchemaTpl('layout:stickyPosition')
]
},
getSchemaTpl('status')
getSchemaTpl('status'),
{
title: '高级',
body: [
isSorptionContainer ? getSchemaTpl('layout:sorption') : null,
// 吸附容器不显示定位相关配置项
...(isSorptionContainer ? [] : positionTpl)
]
}
])
]
},
@ -289,7 +460,9 @@ export class FlexPluginBase extends LayoutBasePlugin {
const draggableContainer = this.manager.draggableContainer(id);
const isFlexItem = this.manager?.isFlexItem(id);
const isFlexColumnItem = this.manager?.isFlexColumnItem(id);
const newColumnSchema = defaultFlexColumnSchema('新的一列');
const newColumnSchema = isFlexColumnItem
? defaultFlexColumnSchema('', false)
: defaultFlexColumnSchema();
const canAppendSiblings = this.manager?.canAppendSiblings();
const toolbarsTooltips: any = {};
toolbars.forEach(toolbar => {
@ -350,7 +523,8 @@ export class FlexPluginBase extends LayoutBasePlugin {
level: 'special',
placement: 'bottom',
className: 'ae-AppendChild',
onClick: () => this.manager.addElem(newColumnSchema)
onClick: () =>
this.manager.addElem(defaultFlexColumnSchema('', true))
});
}
}

View File

@ -12,6 +12,7 @@ import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {repeatArray} from 'amis-editor-core';
import set from 'lodash/set';
import {escapeFormula, resolveArrayDatasource} from '../util';
import merge from 'lodash/merge';
export class List2Plugin extends BasePlugin {
static id = 'List2Plugin';
@ -450,7 +451,12 @@ export class List2Plugin extends BasePlugin {
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.card = merge(
{
className: `${props.card.className || ''} ae-Editor-listItem`
},
props.card
);
}
// 列表类型内的文本元素显示原始公式

View File

@ -158,52 +158,51 @@ export class ActionPlugin extends BasePlugin {
showLoading: true
}),
asFormItem: true,
children: ({value, onChange, data}: any) =>
data.actionType === 'dialog' ? (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
this.manager.openSubEditor({
title: '配置弹框内容',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
) : null
visibleOn: '${actionType == "dialog"}',
children: ({value, onChange, data}: any) => (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
this.manager.openSubEditor({
title: '配置弹框内容',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
)
},
{
visibleOn: 'data.actionType == "drawer"',
name: 'drawer',
pipeIn: defaultValue({
title: '弹框标题',
body: '对,你刚刚点击了'
}),
asFormItem: true,
children: ({value, onChange, data}: any) =>
data.actionType == 'drawer' ? (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
this.manager.openSubEditor({
title: '配置抽出式弹框内容',
value: {type: 'drawer', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
) : null
visibleOn: '${actionType == "drawer"}',
children: ({value, onChange, data}: any) => (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
this.manager.openSubEditor({
title: '配置抽出式弹框内容',
value: {type: 'drawer', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
)
},
getSchemaTpl('api', {
@ -218,35 +217,35 @@ export class ActionPlugin extends BasePlugin {
body: '内容'
}),
asFormItem: true,
children: ({onChange, value, data}: any) =>
data.actionType == 'ajax' ? (
<div className="m-b">
visibleOn: '${actionType == "ajax"}',
children: ({onChange, value, data}: any) => (
<div className="m-b">
<Button
size="sm"
level={value ? 'danger' : 'info'}
onClick={() =>
this.manager.openSubEditor({
title: '配置反馈弹框详情',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
>
</Button>
{value ? (
<Button
size="sm"
level={value ? 'danger' : 'info'}
onClick={() =>
this.manager.openSubEditor({
title: '配置反馈弹框详情',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
level="link"
className="m-l"
onClick={() => onChange('')}
>
</Button>
{value ? (
<Button
size="sm"
level="link"
className="m-l"
onClick={() => onChange('')}
>
</Button>
) : null}
</div>
) : null
) : null}
</div>
)
},
{

View File

@ -63,6 +63,7 @@ export * from './Form/UUID'; // UUID
export * from './Form/LocationPicker'; // 地理位置
export * from './Form/InputSubForm'; // 子表单项
export * from './Form/Hidden'; // 隐藏域
export * from './Form/InputSignature'; // 签名面板
export * from './Form/Static'; // 静态展示框
// 功能

View File

@ -177,7 +177,7 @@ export default class FlexSettingControl extends React.Component<FlexSettingContr
return (
<div className="ap-Flex">
{!label && <div className="ap-Flex-label"></div>}
{!label && <div className="ap-Flex-label"></div>}
{flexItems.map(item => (
<div
className={`ap-Flex-item ap-Flex-${item.field}`}

View File

@ -324,7 +324,7 @@ export default class TransferTableOption extends React.Component<
return {
type: 'action',
actionType: 'dialog',
label: '添加表格列',
label: '设置表格列',
level: 'enhance',
dialog: {
title: '设置表格列选项',
@ -348,12 +348,14 @@ export default class TransferTableOption extends React.Component<
{
type: 'input-text',
name: 'label',
placeholder: '标题'
placeholder: '标题',
required: true
},
{
type: 'input-text',
name: 'name',
placeholder: '绑定字段名'
placeholder: '绑定字段名',
required: true
},
{
type: 'select',
@ -417,7 +419,7 @@ export default class TransferTableOption extends React.Component<
{
type: 'action',
actionType: 'dialog',
label: '添加表格行',
label: '设置表格行',
level: 'enhance',
disabled: columns && columns.length === 0,
block: true,

View File

@ -0,0 +1,661 @@
import {
EditorManager,
JSONGetById,
JSONGetParentById,
JSONPipeIn,
JSONPipeOut,
JSONUpdate,
addModal,
modalsToDefinitions
} from 'amis-editor-core';
import React from 'react';
import {observer} from 'mobx-react';
import {JSONTraverse, JSONValueMap, RendererProps} from 'amis-core';
import {Button, FormField, InputJSONSchema, Select, Switch} from 'amis-ui';
import type {EditorModalBody} from '../../../../amis-editor-core/src/store/editor';
export interface DialogActionPanelProps extends RendererProps {
manager: EditorManager;
subscribeSchemaSubmit: (
fn: (schema: any, value: any, id: string, diff?: any) => any,
once?: boolean
) => () => void;
subscribeActionSubmit: (fn: (value: any) => any) => () => void;
addHook: (fn: Function, type?: 'validate' | 'init' | 'flush') => () => void;
}
export interface LocalModal {
label: string;
value: any;
tip: string;
modal: EditorModalBody;
isNew?: boolean;
isModified?: boolean;
isActive?: boolean;
// 是否为当前动作内嵌的弹窗
isCurrentActionModal?: boolean;
/**
*
*/
data?: any;
}
function DialogActionPanel({
classnames: cx,
render,
data,
manager,
onChange,
onBulkChange,
node,
addHook,
subscribeSchemaSubmit
}: DialogActionPanelProps) {
const eventKey = data.eventKey;
if (!eventKey) {
return <div></div>;
}
const actionIndex = data.actionIndex;
const store = manager.store;
const [modals, setModals] = React.useState<Array<LocalModal>>([]);
const currentModal = modals.find(item => item.isActive);
// 订阅由面板触发的 schema 变跟事件
// 写入 store 之前执行,可以对 schema 进行修改
React.useEffect(() => {
subscribeSchemaSubmit((schema: any, nodeSchema: any, id: string) => {
const rawActions = JSONGetById(schema, id)?.onEvent[eventKey]?.actions;
if (!rawActions || !Array.isArray(rawActions)) {
throw new Error('动作配置错误');
}
const actionSchema =
rawActions[
typeof actionIndex === 'undefined'
? rawActions.length - 1
: actionIndex
];
const modals: Array<LocalModal> = actionSchema.__actionModals;
const currentModal = modals.find(item => item.isActive)!;
schema = {...schema, definitions: {...schema.definitions}};
// 可能编辑了其他弹窗,同时所选弹窗里面如果公用了弹窗
// 会标记为 isModified
modals
.filter(
item => item.isModified && item !== currentModal && item.modal.$$ref
)
.forEach(({modal}) => {
const {$$originId: originId, ...def} = modal as any;
if (originId) {
const parent = JSONGetParentById(schema, originId);
if (!parent) {
// 找不到就丢回去,上层去处理
def.$$originId = originId;
} else {
// TODO 这里要不要再加个判断?
// 只更新当前动作中关联的弹窗?
const modalType = def.type === 'drawer' ? 'drawer' : 'dialog';
schema = JSONUpdate(schema, parent.$$id, {
...parent,
__actionModals: undefined,
args: undefined,
dialog: undefined,
drawer: undefined,
actionType: def.actionType ?? modalType,
[modalType]: JSONPipeIn({
$ref: modal.$$ref!
})
});
}
}
schema.definitions[modal.$$ref!] = JSONPipeIn(def);
});
// 处理当前选中的弹窗
let newActionSchema: any = null;
const modalType =
currentModal.modal.type === 'drawer' ? 'drawer' : 'dialog';
let originActionId = null;
let newRefName = '';
if (currentModal.isCurrentActionModal) {
// 选中的是当前动作内嵌的弹窗
// 直接更新当前动作即可
newActionSchema = {
...actionSchema,
__actionModals: undefined,
args: undefined,
dialog: undefined,
drawer: undefined,
actionType: currentModal.modal.actionType ?? modalType,
data: currentModal.data,
[modalType]: {
...currentModal.modal,
data: undefined
}
};
} else if (currentModal.modal.$$ref) {
// 选中的是引用的弹窗
newActionSchema = {
...actionSchema,
__actionModals: undefined,
args: undefined,
dialog: undefined,
drawer: undefined,
actionType: currentModal.modal.actionType ?? modalType,
data: currentModal.data,
[modalType]: {
$ref: currentModal.modal.$$ref
}
};
const originInd = (currentModal.modal as any).$$originId;
// 可能弹窗内容更新了
schema.definitions[currentModal.modal.$$ref] = JSONPipeIn({
...currentModal.modal,
$$originId: undefined,
$$ref: undefined
});
if (originInd) {
const parent = JSONGetParentById(schema, originInd);
if (parent && parent.actionType) {
originActionId = parent.$$id;
newRefName = currentModal.modal.$$ref;
} else {
// 没找到很可能是在主页面里面的弹窗
// 还得继续把 originId 给到上一层去处理
schema.definitions[currentModal.modal.$$ref].$$originId = originInd;
}
}
} else {
// 选的是别的工作内嵌的弹窗
// 需要把目标弹窗转成 definition
// 然后都引用这个 definition
let refKey: string = '';
[schema, refKey] = addModal(schema, currentModal.modal);
newActionSchema = {
...actionSchema,
__actionModals: undefined,
args: undefined,
dialog: undefined,
drawer: undefined,
actionType: currentModal.modal.actionType ?? modalType,
data: currentModal.data,
[modalType]: JSONPipeIn({
$ref: refKey
})
};
originActionId = currentModal.value;
newRefName = refKey;
}
schema = JSONUpdate(
schema,
actionSchema.$$id,
JSONPipeIn(newActionSchema),
true
);
// 原来的动作也要更新
if (originActionId && newRefName) {
schema = JSONUpdate(
schema,
currentModal.value,
JSONPipeIn({
$ref: newRefName
}),
true
);
}
return schema;
}, true);
}, []);
const [errors, setErrors] = React.useState<{
dialog?: string;
data?: string;
}>({});
React.useEffect(() => {
const unHook = addHook((data: any): any => {
const modals = data.__actionModals;
if (!modals || !Array.isArray(modals)) {
throw new Error('程序异常');
}
const currentModal = modals.find((item: any) => item.isActive);
if (!currentModal) {
setErrors({
...errors,
dialog: '请选择一个弹窗'
});
return false;
}
const required = currentModal.modal.inputParams?.required;
if (Array.isArray(required) && required.length) {
if (!currentModal.data) {
setErrors({
...errors,
data: '参数不能为空'
});
return false;
} else if (required.some(key => !currentModal.data[key])) {
setErrors({
...errors,
data: '参数中存在必填参数未赋值'
});
return false;
}
}
// TODO 校验参数赋值是否满足了弹窗的参数要求
}, 'validate');
return () => unHook();
}, []);
// 初始化弹窗列表
React.useEffect(() => {
const actionSchema =
typeof actionIndex === 'undefined'
? {}
: node.schema?.onEvent[eventKey]?.actions?.[actionIndex];
const dialogBody =
actionSchema[
actionSchema.actionType === 'drawer' ? 'drawer' : 'dialog'
] || actionSchema.args;
const modals: Array<LocalModal> = store.modals.map(modal => {
const isCurrentActionModal = modal.$$id === dialogBody?.$$id;
return {
label: `${
modal.editorSetting?.displayName || modal.title || '未命名弹窗'
}${
isCurrentActionModal
? '<当前动作内嵌弹窗>'
: modal.$$ref
? ''
: '<内嵌弹窗>'
}`,
tip:
(modal as any).actionType === 'confirmDialog'
? '确认框'
: modal.type === 'drawer'
? '抽屉弹窗'
: '弹窗',
value: modal.$$id,
modal: modal,
isCurrentActionModal,
data: modal.data
};
});
let dialogId = dialogBody?.$$id || '';
const ref = dialogBody?.$ref;
if (ref) {
dialogId = modals.find(item => item.modal.$$ref === ref)?.value || '';
}
// 初始化有问题的情况
const newData: any = {};
// if (!dialogId) {
// dialogId = guid();
// const placeholder = {
// $$id: dialogId,
// type: 'dialog',
// title: '未命名弹窗',
// body: [
// {
// type: 'tpl',
// tpl: '弹窗内容'
// }
// ]
// };
// modals.push({
// label: '未命名弹窗<当前动作内嵌弹窗>',
// tip: '弹窗',
// value: dialogId,
// isCurrentActionModal: true,
// modal: placeholder
// });
// newData['dialog'] = placeholder;
// }
const arr = modals.map(item => ({
...item,
isActive: dialogId === item.value,
data:
dialogId === item.value
? JSONPipeOut(actionSchema.data ?? item.data)
: JSONPipeOut(item.data)
}));
setModals(arr);
newData.__actionModals = arr;
onBulkChange(newData);
}, []);
// 处理弹窗切换
const handleDialogChange = React.useCallback(
(option: any) => {
const arr = modals.map(item => ({
...item,
isActive: item.value === option.value
}));
onBulkChange({
__actionModals: arr
});
setModals(arr);
setErrors({
...errors,
dialog: '',
data: ''
});
},
[modals]
);
// 打开子弹窗后,因为子弹窗里面可能会创建新弹窗,会在 defintions 里面
// 所以需要合并一下
const mergeDefinitions = React.useCallback(
(members: Array<LocalModal>, definitions: any, modal: any) => {
const refs: Array<string> = [];
JSONTraverse(modal, (value, key) => {
if (key === '$ref') {
refs.push(value);
}
});
let arr = members;
Object.keys(definitions).forEach(key => {
// 弹窗里面用到了才更新
if (!refs.includes(key)) {
return;
}
// 要修改就复制一份,避免污染原始数据
if (arr === members) {
arr = members.concat();
}
const {$$originId, ...modal} = definitions[key];
const idx = arr.findIndex(item =>
$$originId ? item.value === $$originId : item.modal.$$ref === key
);
const label = `${
modal.editorSetting?.displayName || modal.title || '未命名弹窗'
}`;
const tip =
(modal as any).actionType === 'confirmDialog'
? '确认框'
: modal.type === 'drawer'
? '抽屉弹窗'
: '弹窗';
if (~idx) {
arr.splice(idx, 1, {
...arr[idx],
label: label,
tip: tip,
modal: {...modal, $$ref: key, $$originId},
isModified: true
});
} else {
if ($$originId) {
throw new Error('Definition merge exception');
}
arr.push({
label,
tip,
value: modal.$$id,
modal: JSONPipeIn({
...modal,
$$ref: key
}),
isModified: true
});
}
});
return arr;
},
[]
);
// 处理新建弹窗
const handleDialogAdd = React.useCallback(
(
idx?: number | Array<number>,
value?: any,
skipForm?: boolean,
closePopOver?: () => void
) => {
store.openSubEditor({
title: '新建弹窗',
value: {
type: 'dialog',
title: '未命名弹窗',
body: [
{
type: 'tpl',
tpl: '弹窗内容'
}
],
definitions: modalsToDefinitions(modals.map(item => item.modal))
},
onChange: ({definitions, ...modal}: any, diff: any) => {
modal = JSONPipeIn(modal);
let arr = modals.concat();
if (!arr.some(item => item.isNew)) {
arr.push({
label: `${
modal.editorSetting?.displayName || modal.title || '未命名弹窗'
}`,
tip:
(modal as any).actionType === 'confirmDialog'
? '确认框'
: modal.type === 'drawer'
? '抽屉弹窗'
: '弹窗',
isNew: true,
isCurrentActionModal: true,
value: modal.$$id,
modal: modal
});
arr = mergeDefinitions(arr, definitions, modal);
arr = arr.map(item => ({
...item,
isActive: item.value === modal.$$id
}));
}
setModals(arr);
onBulkChange({__actionModals: arr});
}
});
closePopOver?.();
},
[modals]
);
// 处理编辑弹窗
const handleDialogEdit = React.useCallback(() => {
const currentModal = modals.find(item => item.isActive);
if (!currentModal) {
return;
}
store.openSubEditor({
title: '编辑弹窗',
value: {
type: 'dialog',
title: '弹窗标题',
body: [
{
type: 'tpl',
tpl: '弹窗内容'
}
],
...(currentModal.modal as any),
definitions: modalsToDefinitions(modals.map(item => item.modal))
},
onChange: ({definitions, ...modal}: any, diff: any) => {
// 编辑的时候不要修改 $$id
modal = JSONPipeIn({...modal, $$id: currentModal.modal.$$id});
let arr = modals.map(item =>
item.value === currentModal.value
? {
...item,
modal: modal,
isModified: true,
label: `${
modal.editorSetting?.displayName ||
modal.title ||
'未命名弹窗'
}${item.isCurrentActionModal ? '<当前动作内嵌弹窗>' : ''}`,
tip:
(modal as any).actionType === 'confirmDialog'
? '确认框'
: modal.type === 'drawer'
? '抽屉弹窗'
: '弹窗'
}
: item
);
arr = mergeDefinitions(arr, definitions, modal);
setModals(arr);
onBulkChange({__actionModals: arr});
}
});
}, [modals]);
const handleDataSwitchChange = React.useCallback(
(value: any) => {
handleDataChange(value ? {} : undefined);
},
[modals]
);
const handleDataChange = React.useCallback(
(value: any) => {
let arr = modals.map(modal =>
modal.isActive
? {
...modal,
data: value
}
: modal
);
setModals(arr);
onBulkChange({__actionModals: arr});
setErrors({
...errors,
data: ''
});
},
[modals]
);
const hasRequired =
Array.isArray(currentModal?.modal.inputParams?.required) &&
currentModal!.modal.inputParams.required.length;
React.useEffect(() => {
if (hasRequired && !currentModal?.data) {
handleDataChange({});
}
}, [hasRequired]);
// 渲染弹窗下拉选项
const renderMenu = React.useCallback((option: any, stats: any) => {
return (
<div className="flex w-full justify-between">
<span>{option.label}</span>
<span className="text-muted">{option.tip}</span>
</div>
);
}, []);
return (
<div className={cx('ae-DialogActionPanel')}>
<FormField
label="选择弹窗"
mode="horizontal"
isRequired
hasError={!!errors.dialog}
errors={errors.dialog}
>
<div
className={cx(
'Form-control Form-control--withSize Form-control--sizeLg'
)}
>
<Select
createBtnLabel="新建弹窗"
value={currentModal?.value || ''}
onChange={handleDialogChange}
options={modals}
creatable={!modals.some(item => item.isNew)}
clearable={false}
onAdd={handleDialogAdd}
renderMenu={renderMenu}
/>
{currentModal ? (
<div className="m-t-sm">
<Button size="sm" level="enhance" onClick={handleDialogEdit}>
</Button>
</div>
) : null}
</div>
</FormField>
{currentModal ? (
<FormField
label="参数赋值"
mode="horizontal"
hasError={!!errors.data}
errors={errors.data}
description={
!currentModal.data
? '不设置参数,打开弹窗将自动传递所有上下文数据'
: ''
}
>
<div
className={cx(
'Form-control Form-control--withSize Form-control--sizeLg'
)}
>
<Switch
className="mt-2 m-b-xs"
value={!!currentModal.data}
onChange={handleDataSwitchChange}
disabled={hasRequired}
/>
{currentModal.data ? (
<InputJSONSchema
className="m-t-sm"
value={currentModal.data}
onChange={handleDataChange}
schema={JSONPipeOut(currentModal.modal.inputParams)}
addButtonText="添加参数"
/>
) : null}
</div>
</FormField>
) : null}
</div>
);
}
export default observer(DialogActionPanel);

View File

@ -38,6 +38,11 @@ interface ActionDialogProp {
node: SchemaNode,
props?: PlainObject
) => JSX.Element;
subscribeSchemaSubmit: (
fn: (schema: any, value: any, id: string, diff?: any) => any
) => () => void;
subscribeActionSubmit: (fn: (value: any) => any) => () => void;
}
export default class ActionDialog extends React.Component<ActionDialogProp> {
@ -209,6 +214,8 @@ export default class ActionDialog extends React.Component<ActionDialogProp> {
render() {
const {
data,
subscribeSchemaSubmit,
subscribeActionSubmit,
show,
type,
actionTree,
@ -426,7 +433,10 @@ export default class ActionDialog extends React.Component<ActionDialogProp> {
onClose
},
{
data // 必须这样,不然变量会被当作数据映射处理掉
data, // 必须这样,不然变量会被当作数据映射处理掉
subscribeActionSubmit,
subscribeSchemaSubmit
}
);
// : null;

View File

@ -6,10 +6,10 @@ import {
BaseEventContext,
defaultValue,
EditorManager,
getFixDialogType,
getSchemaTpl,
JsonGenerateID,
JSONGetById,
modalsToDefinitions,
persistGet,
persistSet,
PluginActions,
@ -35,6 +35,7 @@ import without from 'lodash/without';
import {ActionConfig, ComponentInfo, ContextVariables} from './types';
import CmptActionSelect from './comp-action-select';
import {ActionData} from '.';
import DialogActionPanel from './DialogActionPanel';
export const getArgsWrapper = (
items: any,
@ -281,6 +282,73 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
const variableOptions = variableManager?.getVariableOptions() || [];
const pageVariableOptions = variableManager?.getPageVariablesOptions() || [];
const modalDescDetail: (info: any, context: any, props: any) => any = (
info,
{eventKey, actionIndex},
props: any
) => {
const {
actionTree,
actions: pluginActions,
commonActions,
allComponents,
node,
manager
} = props;
const store = manager.store;
const modals = store.modals;
const onEvent = node.schema?.onEvent;
const action = onEvent?.[eventKey].actions?.[actionIndex];
const actionBody =
action?.[action?.actionType === 'drawer' ? 'drawer' : 'dialog'];
let modalId = actionBody?.$$id;
if (actionBody?.$ref) {
modalId =
modals.find((item: any) => item.$$ref === actionBody.$ref)?.$$id || '';
}
const modal = modalId
? manager.store.modals.find((item: any) => item.$$id === modalId)
: '';
if (modal) {
return (
<>
<div>
&nbsp;
<a
href="#"
onClick={(e: React.UIEvent<any>) => {
e.preventDefault();
e.stopPropagation();
store.openSubEditor({
title: '编辑弹窗',
value: {
type: 'dialog',
...modal,
definitions: modalsToDefinitions(store.modals)
},
onChange: ({definitions, ...modal}: any, diff: any) => {
store.updateModal(modal.$$id!, modal, definitions);
}
});
}}
>
{modal.editorSetting?.displayName || modal.title || '未命名弹窗'}
</a>
&nbsp;
{(modal as any).actionType === 'confirmDialog'
? '确认框'
: modal.type === 'drawer'
? '抽屉弹窗'
: '弹窗'}
</div>
</>
);
}
return null;
};
return [
{
actionLabel: '页面',
@ -369,84 +437,30 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
description: '打开弹窗,弹窗内支持复杂的交互设计',
actions: [
{
actionType: 'dialog'
actionType: 'dialog',
descDetail: modalDescDetail
},
{
actionType: 'drawer'
actionType: 'drawer',
descDetail: modalDescDetail
},
{
actionType: 'confirmDialog'
actionType: 'confirmDialog',
descDetail: modalDescDetail
}
],
schema: [
{
type: 'radios',
label: '弹窗来源',
name: '__dialogSource',
required: true,
mode: 'horizontal',
inputClassName: 'event-action-radio',
value: 'new',
options: [
{
label: '选择页面内已有弹窗',
value: 'current'
},
{
label: '新建弹窗',
value: 'new'
}
]
},
{
name: '__dialogTitle',
type: 'input-text',
label: '弹窗标题',
placeholder: '请输入弹窗标题',
mode: 'horizontal',
size: 'lg',
visibleOn: '__dialogSource === "new"'
},
{
name: '__selectDialog',
type: 'select',
label: '选择弹窗',
source: '${__dialogActions}',
mode: 'horizontal',
size: 'lg',
visibleOn: '__dialogSource === "current"',
onChange: (value: any, oldValue: any, model: any, form: any) => {
form.setValueByName('args', {
fromCurrentDialog: true
});
}
},
{
type: 'radios',
label: '弹窗类型',
name: 'groupType',
mode: 'horizontal',
value: 'dialog',
required: true,
pipeIn: defaultValue('dialog'),
inputClassName: 'event-action-radio',
options: [
{
label: '弹窗',
value: 'dialog'
},
{
label: '抽屉',
value: 'drawer'
},
{
label: '确认对话框',
value: 'confirmDialog'
}
],
visibleOn:
'data.actionType === "openDialog" && __dialogSource === "new"'
component: DialogActionPanel
}
// {
// name: '__selectDialog',
// type: 'select',
// label: '选择弹窗',
// source: '${__dialogActions}',
// mode: 'horizontal',
// size: 'lg'
// }
]
},
{
@ -1236,19 +1250,19 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
{/* 只要path字段存在就认为是应用变量赋值无论是否有值 */}
{typeof info?.args?.path === 'string' && !info?.componentId ? (
<>
<span className="variable-left variable-right">
{variableManager.getNameByPath(info.args.path)}
</span>
</>
) : (
<>
<span className="variable-left variable-right">
{info?.rendererLabel || info.componentId || '-'}
</span>
</>
)}
{/*
@ -2645,52 +2659,51 @@ export const getOldActionSchema = (
showLoading: true
}),
asFormItem: true,
children: ({value, onChange, data}: any) =>
data.actionType === 'dialog' ? (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
manager.openSubEditor({
title: '配置弹框内容',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
) : null
visibleOn: '${actionType === "dialog"}',
children: ({value, onChange, data}: any) => (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
manager.openSubEditor({
title: '配置弹框内容',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
)
},
{
visibleOn: 'data.actionType == "drawer"',
name: 'drawer',
pipeIn: defaultValue({
title: '抽屉标题',
body: '对,你刚刚点击了'
}),
asFormItem: true,
children: ({value, onChange, data}: any) =>
data.actionType == 'drawer' ? (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
manager.openSubEditor({
title: '配置抽出式弹框内容',
value: {type: 'drawer', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
) : null
visibleOn: '${actionType == "drawer"}',
children: ({value, onChange, data}: any) => (
<Button
size="sm"
level="danger"
className="m-b"
onClick={() =>
manager.openSubEditor({
title: '配置抽出式弹框内容',
value: {type: 'drawer', ...value},
onChange: value => onChange(value)
})
}
block
>
</Button>
)
},
getSchemaTpl('api', {
@ -2705,35 +2718,35 @@ export const getOldActionSchema = (
body: '内容'
}),
asFormItem: true,
children: ({onChange, value, data}: any) =>
data.actionType == 'ajax' ? (
<div className="m-b">
visibleOn: '${actionType == "ajax"}',
children: ({onChange, value, data}: any) => (
<div className="m-b">
<Button
size="sm"
level={value ? 'danger' : 'info'}
onClick={() =>
manager.openSubEditor({
title: '配置反馈弹框详情',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
>
</Button>
{value ? (
<Button
size="sm"
level={value ? 'danger' : 'info'}
onClick={() =>
manager.openSubEditor({
title: '配置反馈弹框详情',
value: {type: 'dialog', ...value},
onChange: value => onChange(value)
})
}
level="link"
className="m-l"
onClick={() => onChange('')}
>
</Button>
{value ? (
<Button
size="sm"
level="link"
className="m-l"
onClick={() => onChange('')}
>
</Button>
) : null}
</div>
) : null
) : null}
</div>
)
},
{
@ -3228,137 +3241,10 @@ export const getEventControlConfig = (
delete action.addOnArgs;
}
if (config.actionType === 'openDialog') {
// 初始化弹窗schema
const dialogInitSchema = {
type: 'dialog',
title: action.__dialogTitle,
body: [
{
type: 'tpl',
tpl: '对,你刚刚点击了',
wrapperComponent: '',
inline: false
}
],
showCloseButton: true,
showErrorMsg: true,
showLoading: true,
className: 'app-popover :AMISCSSWrapper',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消'
},
{
type: 'button',
actionType: 'confirm',
label: '确认',
primary: true
}
]
};
const drawerInitSchema = {
type: 'drawer',
title: action.__dialogTitle,
body: [
{
type: 'tpl',
tpl: '对,你刚刚点击了',
wrapperComponent: '',
inline: false
}
],
className: 'app-popover :AMISCSSWrapper',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消'
},
{
type: 'button',
actionType: 'confirm',
label: '确认',
primary: true
}
]
};
const confirmDialogInitSchema = {
type: 'dialog',
title: action.__dialogTitle,
body: [
{
type: 'tpl',
tpl: '对,你刚刚点击了',
wrapperComponent: '',
inline: false
}
],
dialogType: 'confirm',
confirmText: '确认',
cancelText: '取消',
confirmBtnLevel: 'primary'
};
const setInitSchema = (groupType: string, action: ActionConfig) => {
if (groupType === 'dialog') {
JsonGenerateID(dialogInitSchema);
action.dialog = dialogInitSchema;
} else if (groupType === 'drawer') {
JsonGenerateID(drawerInitSchema);
action.drawer = drawerInitSchema;
} else if (groupType === 'confirmDialog') {
JsonGenerateID(confirmDialogInitSchema);
action.dialog = confirmDialogInitSchema;
}
};
const chooseCurrentDialog = (action: ActionConfig, schema: Schema) => {
const selectDialog = action.__selectDialog;
let dialogType = getFixDialogType(schema, selectDialog);
// 选择现有弹窗后为了使之前的弹窗和现有弹窗$$id唯一这里重新生成一下
let newDialogId = guid();
action.actionType = dialogType;
action[dialogType] = {
$$id: newDialogId,
type: dialogType
};
// 在这里记录一下新生成的弹窗id
action.__relatedDialogId = newDialogId;
};
if (type === 'add') {
if (config.__dialogSource === 'new') {
setInitSchema(config.groupType, action);
} else if (config.__dialogSource === 'current') {
chooseCurrentDialog(action, shcema!);
}
}
// 编辑
else if (type === 'update') {
if (config.__dialogSource === 'new') {
// 如果切换了弹窗类型或切换了弹窗来源则初始化schema
if (
config.groupType !== actionData?.groupType ||
(config.__dialogSource === 'new' &&
actionData?.__dialogSource === 'current')
) {
setInitSchema(config.groupType, action);
} else {
action[config.groupType] = {
...actionData![config.groupType],
title: config.__dialogTitle
};
}
} else if (config.__dialogSource === 'current') {
chooseCurrentDialog(action, shcema!);
}
}
}
// 不加回来可能数据会丢失
['drawer', 'dialog', 'args'].forEach(key => {
action[key] = action[key] ?? actionData?.[key];
});
// 刷新组件时,处理是否追加事件变量
if (config.actionType === 'reload') {

View File

@ -43,9 +43,7 @@ import {
PluginEvents,
RendererPluginAction,
RendererPluginEvent,
SubRendererPluginAction,
getDialogActions,
getFixDialogType
SubRendererPluginAction
} from 'amis-editor-core';
export * from './helper';
import {i18n as _i18n} from 'i18n-runtime';
@ -76,6 +74,12 @@ interface EventControlProps extends FormControlProps {
schema?: Schema
) => ActionConfig; // 动作配置提交时格式化
owner?: string; // 组件标识
// 监听面板提交事件
// 更改后写入 store 前触发
subscribeSchemaSubmit: (
fn: (schema: any, value: any, id: string, diff?: any) => any
) => () => void;
}
interface EventDialogData {
@ -141,6 +145,7 @@ export class EventControl extends React.Component<
} = {};
drag?: HTMLElement | null;
unReaction: any;
submitSubscribers: Array<(value: any) => any> = [];
constructor(props: EventControlProps) {
super(props);
@ -187,6 +192,7 @@ export class EventControl extends React.Component<
componentWillUnmount() {
this.unReaction?.();
this.submitSubscribers = [];
}
componentDidUpdate(
@ -224,6 +230,16 @@ export class EventControl extends React.Component<
}
}
@autobind
subscribeSubmit(subscriber: (value: any) => any) {
const fn = (value: any) => subscriber?.(value) || value;
this.submitSubscribers.push(fn);
return () => {
const idx = this.submitSubscribers.indexOf(fn);
this.submitSubscribers.splice(idx, 1);
};
}
generateEmptyDefault(events: RendererPluginEvent[]) {
const onEvent: ActionEventConfig = {};
events.forEach((event: RendererPluginEvent) => {
@ -782,26 +798,6 @@ export class EventControl extends React.Component<
return updateComponentContext(variables);
}
// 获取现有弹窗列表
getDialogList(
manager: EditorManager,
action?: ActionConfig,
actionType?: keyof typeof dialogObjMap
) {
if (
action &&
actionType &&
dialogObjMap[actionType] &&
!action?.args?.fromCurrentDialog
) {
let dialogBodyContent = dialogObjMap[actionType];
let filterId = Array.isArray(dialogBodyContent)
? action[dialogBodyContent[0]].id || action[dialogBodyContent[1]].id
: action[dialogBodyContent].id;
return getDialogActions(manager.store.schema, 'source', filterId);
} else return getDialogActions(manager.store.schema, 'source');
}
// 唤起动作配置弹窗
async activeActionDialog(
data: Pick<EventControlState, 'showAcionDialog' | 'type' | 'actionData'>
@ -866,38 +862,13 @@ export class EventControl extends React.Component<
__actionSchema: actionNode!.schema, // 树节点schema
__subActions: hasSubActionNode?.actions, // 树节点子动作
__cmptTreeSource: supportComponents ?? [],
__dialogActions: this.getDialogList(manager, action, actionGroupType),
// __dialogActions: manager.store.modalOptions,
__superCmptTreeSource: allComponents,
// __supersCmptTreeSource: '',
__setValueDs: setValueDs
// broadcastId: action.actionType === 'broadcast' ? action.eventName : ''
};
// 编辑时准备已选的弹窗来源和标题
if (actionConfig?.actionType == 'openDialog') {
const definitions = manager.store.schema.definitions;
let dialogBody =
dialogObjMap[actionGroupType as keyof typeof dialogObjMap];
const dialogObj = Array.isArray(dialogBody)
? dialogBody[0] || dialogBody[1]
: dialogBody;
const dialogRef = actionConfig?.[dialogObj!]?.$ref;
if (dialogRef) {
data.actionData!.__dialogTitle = definitions[dialogRef].title;
} else {
data.actionData!.__dialogTitle = actionConfig?.[dialogObj!]?.title;
}
if (actionConfig.args?.fromCurrentDialog) {
data.actionData!.__dialogSource = 'current';
data.actionData!.__selectDialog = definitions[dialogRef].$$id;
} else {
data.actionData!.__dialogSource = 'new';
}
}
// 选中项自动滚动至可见位置
setTimeout(
() =>
@ -913,14 +884,14 @@ export class EventControl extends React.Component<
pluginActions,
getContextSchemas,
__superCmptTreeSource: allComponents,
__dialogActions: this.getDialogList(manager)
__dialogActions: manager.store.modalOptions
};
}
this.setState(data);
}
// 渲染描述信息
renderDesc(action: ActionConfig) {
renderDesc(action: ActionConfig, actionIndex: number, eventKey: string) {
const {
actions: pluginActions,
actionTree,
@ -957,91 +928,39 @@ export class EventControl extends React.Component<
}
return typeof desc === 'function' ? (
<div className="action-control-content">{desc?.(info) || '-'}</div>
<div className="action-control-content">
{desc?.(
info,
{
actionIndex,
eventKey
},
this.props
) || '-'}
</div>
) : null;
}
getRefsFromCurrentDialog(store: any, action: any) {
let definitions = store.schema.definitions;
let dialogMaxIndex: number = 0;
let dialogRefsName = '';
if (definitions) {
Object.keys(definitions).forEach(k => {
const dialog = definitions[k];
if (dialog.$$id === action.__selectDialog) {
dialogRefsName = k;
}
if (k.includes('ref-')) {
let index = Number(k.split('-')[2]);
dialogMaxIndex = Math.max(dialogMaxIndex, index);
}
});
}
let dialogType = getFixDialogType(store.schema, action.__selectDialog);
if (!dialogRefsName) {
dialogRefsName = dialogMaxIndex
? `${dialogType}-ref-${dialogMaxIndex + 1}`
: `${dialogType}-ref-1`;
}
return dialogRefsName;
}
@autobind
onSubmit(type: string, config: any) {
const {actionConfigSubmitFormatter, manager} = this.props;
const {actionData} = this.state;
const store = manager.store;
const action =
let action =
actionConfigSubmitFormatter?.(config, type, actionData, store.schema) ??
config;
action = this.submitSubscribers.reduce(
(action, fn) => fn(action) || action,
action
);
delete action.__actionSchema;
if (type === 'add') {
if (['dialog', 'drawer', 'confirmDialog'].includes(action.actionType)) {
let args =
action.actionType === 'dialog'
? 'dialog'
: action.actionType === 'drawer'
? 'drawer'
: 'dialog';
if (!config?.__dialogSource || config?.__dialogSource === 'new') {
let actionLength = this.state.onEvent[config.eventKey].actions.length;
let path = `${store.getSchemaPath(store.activeId)}/onEvent/${
config.eventKey
}/actions/${actionLength}/${args}`;
store.setActiveDialogPath(path);
} else if (config?.__dialogSource === 'current') {
let dialogRefsName = this.getRefsFromCurrentDialog(store, action);
let path = `definitions/${dialogRefsName}`;
store.setActiveDialogPath(path);
}
this.addAction?.(config.eventKey, action);
} else {
this.addAction?.(config.eventKey, action);
}
this.addAction?.(config.eventKey, action);
} else if (type === 'update') {
if (['dialog', 'drawer', 'confirmDialog'].includes(action.actionType)) {
let args =
action.actionType === 'dialog'
? 'dialog'
: action.actionType === 'drawer'
? 'drawer'
: 'dialog';
if (config.__dialogSource === 'new') {
let path = `${store.getSchemaPath(store.activeId)}/onEvent/${
config.eventKey
}/actions/${config.actionIndex}/${args}`;
store.setActiveDialogPath(path);
} else if (config.__dialogSource === 'current') {
let dialogRefsName = this.getRefsFromCurrentDialog(store, action);
let path = `definitions/${dialogRefsName}`;
store.setActiveDialogPath(path);
}
this.updateAction?.(config.eventKey, config.actionIndex, action);
} else {
this.updateAction?.(config.eventKey, config.actionIndex, action);
}
this.updateAction?.(config.eventKey, config.actionIndex, action);
}
updateCommonUseActions({
@ -1079,6 +998,30 @@ export class EventControl extends React.Component<
// });
}
renderActionType(action: any, actionIndex: number, eventKey: string) {
const {
actionTree,
actions: pluginActions,
commonActions,
allComponents,
node,
manager
} = this.props;
return (
<span>
{getPropOfAcion(
action,
'actionLabel',
actionTree,
pluginActions,
commonActions,
allComponents
) || action.actionType}
</span>
);
}
render() {
const {
actionTree,
@ -1086,7 +1029,8 @@ export class EventControl extends React.Component<
commonActions,
getComponents,
allComponents,
render
render,
subscribeSchemaSubmit
} = this.props;
const {
onEvent,
@ -1282,14 +1226,11 @@ export class EventControl extends React.Component<
/>
</div>
<div className="action-item-actiontype">
{getPropOfAcion(
{this.renderActionType(
action,
'actionLabel',
actionTree,
pluginActions,
commonActions,
allComponents
) || action.actionType}
actionIndex,
eventKey
)}
</div>
</div>
<div className="action-control-header-right">
@ -1327,7 +1268,7 @@ export class EventControl extends React.Component<
</div>
</div>
</div>
{this.renderDesc(action)}
{this.renderDesc(action, actionIndex, eventKey)}
</li>
);
}
@ -1447,6 +1388,8 @@ export class EventControl extends React.Component<
onSubmit={this.onSubmit}
onClose={this.onClose}
render={this.props.render}
subscribeSchemaSubmit={subscribeSchemaSubmit}
subscribeActionSubmit={this.subscribeSubmit}
/>
</div>
);

View File

@ -0,0 +1,101 @@
/**
* @file flex
*/
import React from 'react';
import {InputBox, TooltipWrapper} from 'amis-ui';
import {FormControlProps, FormItem} from 'amis-core';
import cx from 'classnames';
function LayoutItem({
value,
onSelect,
active,
tip
}: {
value: string;
onSelect: () => void;
active?: boolean;
tip?: string;
}) {
const items = String(value).split(':');
return (
<TooltipWrapper key="TooltipWrapper" tooltip={tip}>
<div
className={cx('ae-FlexLayout-item', {
active
})}
style={{flex: value}}
onClick={onSelect}
>
{items.map(val => (
<div className="ae-FlexLayout-itemColumn" style={{flex: val}}></div>
))}
</div>
</TooltipWrapper>
);
}
function FlexLayouts({
onChange,
value
}: {
onChange: (value: string) => void;
value?: string;
}) {
const presetLayouts = [
'1',
'1:1',
'1:2',
'2:1',
'1:3',
'1:1:1',
'1:2:1',
'1:1:1:1'
];
let currentLayout = value;
if (value) {
// 转换成1:x格式
let items = String(value).split(':');
const min = Math.min.apply(null, items);
if (items.every(item => +item % min === 0)) {
items = items.map(item => String(+item / min));
currentLayout = items.join(':');
}
}
return (
<div className="ae-FlexLayout">
<div className="ae-FlexLayout-wrap">
{presetLayouts.map(item => (
<LayoutItem
key={item}
value={item}
tip={`排列${item}`}
onSelect={() => onChange(item)}
active={item === currentLayout}
/>
))}
</div>
<div className="flex items-center">
<span className="mr-2 text-gray-500"></span>
<InputBox
className="ae-FlexLayout-input"
clearable={false}
value={value}
placeholder="例如 1:3:2"
onChange={val => (currentLayout = val)}
onBlur={() => currentLayout && onChange(currentLayout)}
/>
</div>
</div>
);
}
@FormItem({type: 'flex-layout'})
export class FlexLayoutRenderer extends React.Component<FormControlProps> {
render() {
return <FlexLayouts {...this.props} />;
}
}

View File

@ -77,15 +77,15 @@ setSchemaTpl(
value: 'static'
},
{
label: '相对(relative)',
label: '相对原位置定位(relative)',
value: 'relative'
},
{
label: '固定(fixed)',
label: '视窗中悬浮(fixed)',
value: 'fixed'
},
{
label: '绝对(absolute)',
label: '绝对定位(absolute)',
value: 'absolute'
}
]
@ -238,6 +238,11 @@ setSchemaTpl(
flexHide?: boolean;
}) => {
const configOptions = compact([
!config?.flexHide && {
label: '弹性布局(flex)',
icon: 'flex-display',
value: 'flex'
},
{
label: '块级(block)',
icon: 'block-display',
@ -252,11 +257,6 @@ setSchemaTpl(
label: '行内元素(inline)',
icon: 'inline-display',
value: 'inline'
},
!config?.flexHide && {
label: '弹性布局(flex)',
icon: 'flex-display',
value: 'flex'
}
]);
const configSchema = {
@ -898,7 +898,7 @@ setSchemaTpl(
type: 'select',
label:
config?.label ||
tipedLabel(' x轴滚动模式', '用于设置水平方向的滚动模式'),
tipedLabel('水平内容超出', '用于设置水平方向的滚动模式'),
name: config?.name || 'style.overflowX',
value: config?.value || 'visible',
visibleOn: config?.visibleOn,
@ -914,7 +914,7 @@ setSchemaTpl(
value: 'hidden'
},
{
label: '滚动显示',
label: '水平滚动',
value: 'scroll'
},
{
@ -1104,7 +1104,7 @@ setSchemaTpl(
type: 'select',
label:
config?.label ||
tipedLabel(' y轴滚动模式', '用于设置垂直方向的滚动模式'),
tipedLabel('垂直内容超出', '用于设置垂直方向的滚动模式'),
name: config?.name || 'style.overflowY',
value: config?.value || 'visible',
visibleOn: config?.visibleOn,
@ -1120,7 +1120,7 @@ setSchemaTpl(
value: 'hidden'
},
{
label: '滚动显示',
label: '垂直滚动',
value: 'scroll'
},
{
@ -1349,8 +1349,12 @@ setSchemaTpl('layout:sticky', {
inputClassName: 'inline-flex justify-between',
onChange: (value: boolean, oldValue: boolean, model: any, form: any) => {
if (value) {
const inset = form.getValueByName('style.inset');
if (!inset || inset === 'auto') {
form.setValueByName('stickyPosition', 'auto');
form.setValueByName('style.inset', '0px auto 0px auto');
}
form.setValueByName('style.position', 'sticky');
form.setValueByName('style.inset', '0px auto auto auto');
form.setValueByName('style.zIndex', 10);
} else {
form.setValueByName('style.position', 'static');
@ -1488,6 +1492,26 @@ setSchemaTpl(
}
);
setSchemaTpl(
'layout:flex-layout',
(config?: {
name?: string;
label?: string;
visibleOn?: string;
pipeIn?: (value: any, data: any) => void;
pipeOut?: (value: any, data: any) => void;
}) => {
return {
type: 'flex-layout',
mode: 'default',
name: config?.name || 'layout',
label: config?.label ?? false,
visibleOn: config?.visibleOn,
pipeIn: config?.pipeIn,
pipeOut: config?.pipeOut
};
}
);
// flex相关配置项整合版
setSchemaTpl(
'layout:flex-setting',

View File

@ -592,20 +592,16 @@ setSchemaTpl(
const curHidePaddingAndMargin = hidePaddingAndMargin ?? false;
const styleStateFunc = (visibleOn: string, state: string) => {
return [
getSchemaTpl('theme:border', {
visibleOn: visibleOn,
name: `themeCss.${classname}.border:${state}`
}),
getSchemaTpl('theme:radius', {
visibleOn: visibleOn,
name: `themeCss.${classname}.radius:${state}`
}),
!curHidePaddingAndMargin
? getSchemaTpl('theme:paddingAndMargin', {
visibleOn: visibleOn,
name: `themeCss.${classname}.padding-and-margin:${state}`
})
: null,
getSchemaTpl('theme:border', {
visibleOn: visibleOn,
name: `themeCss.${classname}.border:${state}`
}),
getSchemaTpl('theme:colorPicker', {
visibleOn: visibleOn,
name: `themeCss.${classname}.background:${state}`,
@ -615,6 +611,10 @@ setSchemaTpl(
needImage: true,
labelMode: 'input'
}),
getSchemaTpl('theme:radius', {
visibleOn: visibleOn,
name: `themeCss.${classname}.radius:${state}`
}),
getSchemaTpl('theme:shadow', {
visibleOn: visibleOn,
name: `themeCss.${classname}.boxShadow:${state}`

View File

@ -341,7 +341,8 @@ export const TREE_BASE_EVENTS = (schema: any) => {
eventLabel: '值变化',
description: '选中值变化时触发',
dataSchema: (manager: EditorManager) => {
const {value, items} = resolveOptionEventDataSchame(manager);
const {value, items, itemSchema} =
resolveOptionEventDataSchame(manager);
return [
{
@ -352,6 +353,11 @@ export const TREE_BASE_EVENTS = (schema: any) => {
title: '数据',
properties: {
value,
item: {
type: 'object',
title: '选中的项',
properties: itemSchema
},
items
}
}

View File

@ -1,6 +1,6 @@
{
"name": "amis-formula",
"version": "6.1.0",
"version": "6.2.1",
"description": "负责 amis 里面的表达式实现,内置公式,编辑器等",
"main": "lib/index.js",
"module": "esm/index.js",
@ -114,4 +114,4 @@
}
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}
}

View File

@ -270,16 +270,18 @@ function BoxBorder(props: BorderProps & FormControlProps) {
}-border-width`}
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.[getKey('width')]}
placeholder={editorDefaultValue?.[getKey('width')] || '边框粗细'}
/>
<div className="Theme-Border-settings-style-color">
<Select
options={borderStyleOptions}
value={borderData[getKey('style')]}
placeholder={getLabel(
editorDefaultValue?.[getKey('style')],
borderStyleOptions
)}
placeholder={
getLabel(
editorDefaultValue?.[getKey('style')],
borderStyleOptions
) || '边框样式'
}
onChange={(item: any) => changeItem('style')(item.value)}
clearable={!!editorDefaultValue}
renderMenu={(item: Options) => {
@ -322,7 +324,7 @@ function BoxBorder(props: BorderProps & FormControlProps) {
itemName={`${
borderType === 'all' ? 'top' : borderType
}-border-color`}
placeholder={editorDefaultValue?.[getKey('color')]}
placeholder={editorDefaultValue?.[getKey('color')] || '边框颜色'}
/>
</div>
</div>

View File

@ -1001,7 +1001,7 @@ function FontEditor(props: FontEditorProps) {
}}
itemName="color"
state={state}
placeholder={editorDefaultValue?.color}
placeholder={editorDefaultValue?.color || '字体颜色'}
editorInheritValue={editorInheritValue?.color}
/>
</div>
@ -1019,7 +1019,7 @@ function FontEditor(props: FontEditorProps) {
menuTpl="label"
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.fontSize}
placeholder={editorDefaultValue?.fontSize || '字体大小'}
/>
</div>
)}
@ -1038,7 +1038,7 @@ function FontEditor(props: FontEditorProps) {
menuTpl="label"
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.fontWeight}
placeholder={editorDefaultValue?.fontWeight || '字体字重'}
/>
{(!hideLineHeight || !hideFontFamily) && (
<div className="Theme-FontEditor-item-label"></div>
@ -1058,7 +1058,7 @@ function FontEditor(props: FontEditorProps) {
menuTpl="label"
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.lineHeight}
placeholder={editorDefaultValue?.lineHeight || '字体行高'}
/>
<div className="Theme-FontEditor-item-label"></div>
</div>

View File

@ -231,7 +231,7 @@ function PaddingAndMarginDialog(props: PaddingAndMarginProps) {
itemName="margin-all"
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.margin}
placeholder={editorDefaultValue?.margin || '外边距'}
/>
<div className="Theme-PaddingAndMargin-input-label"></div>
</div>
@ -250,7 +250,7 @@ function PaddingAndMarginDialog(props: PaddingAndMarginProps) {
itemName="padding-all"
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.padding}
placeholder={editorDefaultValue?.padding || '内边距'}
/>
<div className="Theme-PaddingAndMargin-input-label"></div>
</div>

View File

@ -196,7 +196,7 @@ function BoxRadius(props: RadiusProps & RendererProps) {
itemName={'all-border-radius'}
state={state}
inheritValue={editorThemePath ? 'inherit' : ''}
placeholder={editorDefaultValue?.[getKey('all')]}
placeholder={editorDefaultValue?.[getKey('all')] || '圆角'}
/>
</div>
</div>

View File

@ -3,7 +3,7 @@
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"version": "6.1.0",
"version": "6.2.1",
"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": "^6.1.0",
"amis-formula": "^6.1.0",
"amis-core": "^6.2.1",
"amis-formula": "^6.2.1",
"classnames": "2.3.2",
"codemirror": "^5.63.0",
"downshift": "6.1.12",
@ -65,6 +65,7 @@
"react-intersection-observer": "9.5.2",
"react-json-view": "1.21.3",
"react-overlays": "5.1.1",
"react-pdf": "^7.7.1",
"react-textarea-autosize": "8.3.3",
"react-transition-group": "4.4.2",
"react-visibility-sensor": "5.1.1",
@ -143,4 +144,4 @@
]
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}
}

Some files were not shown because too many files have changed in this diff Show More