Merge remote-tracking branch 'baidu/master' into feat/table-expandme-checkme-toggle-entire-tree

This commit is contained in:
ranqirong 2023-11-15 09:10:02 +08:00
commit 5108906e96
89 changed files with 4066 additions and 1959 deletions

View File

@ -287,7 +287,7 @@ order: 38
| step | `number \| string` | `1` | 步长,支持变量 | `3.3.0`后支持变量 |
| showSteps | `boolean` | `false` | 是否显示步长 |
| parts | `number` or `number[]` | `1` | 分割的块数<br/>主持数组传入分块的节点 |
| marks | <code>{ [number &#124; string]: ReactNode }</code> or <code>{ [number &#124; string]: { style: CSSProperties, label: ReactNode } }</code> | | 刻度标记<br/>- 支持自定义样式<br/>- 设置百分比 |
| marks | <code>{ [number &#124; string]: string &#124; number &#124; SchemaObject }</code> or <code>{ [number &#124; string]: { style: CSSProperties, label: string } }</code> | | 刻度标记<br/>- 支持自定义样式<br/>- 设置百分比 |
| tooltipVisible | `boolean` | `false` | 是否显示滑块标签 |
| tooltipPlacement | `auto` or `bottom` or `left` or `right` | `top` | 滑块标签的位置,默认`auto`,方向自适应<br/>前置条件tooltipVisible 不为 false 时有效 |
| tipFormatter | `function` | | 控制滑块标签显隐函数<br/>前置条件tooltipVisible 不为 false 时有效 |

View File

@ -917,6 +917,11 @@ order: 54
| deleteSuccess | `index: number` 所在行记录索引 <br /> `item: object` 所在行记录 <br/> `[name]: object[]`列表记录 | 配置了`deleteApi`,调用接口成功时触发 |
| deleteFail | `index: number` 所在行记录索引 <br /> `item: object` 所在行记录 <br/> `[name]: object[]`列表记录<br />`error: object` `deleteApi`请求失败后返回的错误信息 | 配置了`deleteApi`,调用接口失败时触发 |
| change | `[name]: object[]` 列表记录 | 组件数据发生改变时触发 |
| orderChange | `movedItems: item[]` 已排序数据 | 手动拖拽行排序时触发 |
| rowClick | `item: object` 行点击数据<br/>`index: number` 行索引 | 单击整行时触发 |
| rowDbClick | `item: object` 行点击数据<br/>`index: number` 行索引 | 双击整行时触发 |
| rowMouseEnter | `item: object` 行移入数据<br/>`index: number` 行索引 | 移入整行时触发 |
| rowMouseLeave | `item: object` 行移出数据<br/>`index: number` 行索引 | 移出整行时触发 |
### add
@ -1563,6 +1568,287 @@ order: 54
}
```
### orderChange
在开启拖拽排序行记录后才会用到,排序确认后触发。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"data": {
"table": [
{
"id": 1,
"a": "a1",
"b": "b1"
},
{
"id": 2,
"a": "a2",
"b": "b2"
}
]
},
"body": [
{
"showIndex": true,
"type": "input-table",
"name": "table",
"columns": [
{
"name": "a",
"label": "A"
},
{
"name": "b",
"label": "B"
}
],
"addable": true,
"draggable": true,
"onEvent": {
"orderChange": {
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "${event.data.movedItems.length|json}行发生移动"
}
}
]
}
}
}
]
}
```
### rowClick
点击行记录。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"data": {
"table": [
{
"id": 1,
"a": "a1",
"b": "b1"
},
{
"id": 2,
"a": "a2",
"b": "b2"
}
]
},
"body": [
{
"showIndex": true,
"type": "input-table",
"name": "table",
"columns": [
{
"name": "a",
"label": "A"
},
{
"name": "b",
"label": "B"
}
],
"addable": true,
"onEvent": {
"rowClick": {
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "行单击数据:${event.data.item|json};行索引:${event.data.index}"
}
}
]
}
}
}
]
}
```
### rowDbClick
双击行记录。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"data": {
"table": [
{
"id": 1,
"a": "a1",
"b": "b1"
},
{
"id": 2,
"a": "a2",
"b": "b2"
}
]
},
"body": [
{
"showIndex": true,
"type": "input-table",
"name": "table",
"columns": [
{
"name": "a",
"label": "A"
},
{
"name": "b",
"label": "B"
}
],
"addable": true,
"onEvent": {
"rowDbClick": {
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "行单击数据:${event.data.item|json};行索引:${event.data.index}"
}
}
]
}
}
}
]
}
```
### rowMouseEnter
鼠标移入行记录。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"data": {
"table": [
{
"id": 1,
"a": "a1",
"b": "b1"
},
{
"id": 2,
"a": "a2",
"b": "b2"
}
]
},
"body": [
{
"showIndex": true,
"type": "input-table",
"name": "table",
"columns": [
{
"name": "a",
"label": "A"
},
{
"name": "b",
"label": "B"
}
],
"addable": true,
"onEvent": {
"rowMouseEnter": {
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "行索引:${event.data.index}"
}
}
]
}
}
}
]
}
```
### rowMouseLeave
鼠标移出行记录。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"data": {
"table": [
{
"id": 1,
"a": "a1",
"b": "b1"
},
{
"id": 2,
"a": "a2",
"b": "b2"
}
]
},
"body": [
{
"showIndex": true,
"type": "input-table",
"name": "table",
"columns": [
{
"name": "a",
"label": "A"
},
{
"name": "b",
"label": "B"
}
],
"addable": true,
"onEvent": {
"rowMouseLeave": {
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "行索引:${event.data.index}"
}
}
]
}
}
}
]
}
```
## 动作表
当前组件对外暴露以下特性动作,其他组件可以通过指定 actionType: 动作名称、componentId: 该组件 id 来触发这些动作,动作配置可以通过 args: {动作配置项名称: xxx}来配置具体的参数,详细请查看事件动作。
@ -1574,6 +1860,7 @@ order: 54
| setValue | `value: object \| Array<object>` 替换的值<br /> `index?: number` 可选,替换第几行数据,如果没有指定,则替换全部表格数据 | 替换表格数据 |
| clear | - | 清空表格数据 |
| reset | - | 将表格数据重置为`resetValue`,若没有配置`resetValue`,则清空表格数据 |
| initDrag | - | 开启表格拖拽排序功能 |
### addItem
@ -2199,3 +2486,83 @@ order: 54
}
}
```
### initDrag
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "button",
"label": "开始表格排序",
"onEvent": {
"click": {
"actions": [
{
"componentId": "drag-input-table",
"actionType": "initDrag"
}
]
}
}
},
{
"type": "input-table",
"label": "表格表单",
"id": "drag-input-table",
"name": "table",
"columns": [
{
"name": "a",
"label": "A"
},
{
"name": "b",
"label": "B"
}
],
"addable": true,
"footerAddBtn": {
"label": "新增",
"icon": "fa fa-plus",
"hidden": true
},
"strictMode": true,
"minLength": 0,
"needConfirm": false,
"showTableAddBtn": false
}
],
"data": {
"table": [
{
"id": 1,
"a": "a1",
"b": "b1"
},
{
"id": 2,
"a": "a2",
"b": "b2"
},
{
"id": 3,
"a": "a3",
"b": "b3"
},
{
"id": 4,
"a": "a4",
"b": "b4"
},
{
"id": 5,
"a": "a5",
"b": "b5"
}
]
}
}
```

View File

@ -878,12 +878,95 @@ icon:
}
```
## 分页
> `3.6.0`及以上版本
当数据量庞大时,可以开启数据源分页,此时左侧列表底部会出现分页控件,相关配置参考属性表。通常在提交表单中使用分页场景,处理数据量较大的数据源。如果需要在表单中回显已选值,建议同时设置`{"joinValues": false, "extractValue": false}`因为已选数据可能位于不同的分页如果仅使用value值作为提交值可能会导致右侧结果区无法正确渲染。
> 仅列表list和表格table展示模式支持分页接口的数据结构参考[CRUD数据源接口格式](../crud#数据结构)
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"label": "默认",
"type": "transfer",
"name": "transfer",
"joinValues": false,
"extractValue": false,
"source": "/api/mock2/options/transfer?page=${page}&perPage=${perPage}",
"pagination": {
"enable": true,
"layout": ["pager", "perpage", "total"],
"popOverContainerSelector": ".cxd-Panel--form"
},
"value": [
{"label": "Laura Lewis", "value": "1", "id": 1},
{"label": "Christopher Rodriguez", "value": "3", "id": 3},
{"label": "Laura Miller", "value": "12", "id": 12},
{"label": "Patricia Robinson", "value": "14", "id": 14}
]
}
]
}
```
### 前端分页
> `3.6.0`及以上版本
当使用数据域变量作为数据源时,支持实现前端一次性加载并分页
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "service",
"api": {
"url": "/api/mock2/options/loadDataOnce",
"method": "get",
"responseData": {
"transferOptions": "${items}"
}
},
"body": [
{
"label": "默认",
"type": "transfer",
"name": "transfer",
"joinValues": false,
"extractValue": false,
"source": "${transferOptions}",
"pagination": {
"enable": true,
"layout": ["pager", "perpage", "total"],
"popOverContainerSelector": ".cxd-Panel--form"
},
"value": [
{"label": "Laura Lewis", "value": "1", "id": 1},
{"label": "Christopher Rodriguez", "value": "3", "id": 3},
{"label": "Laura Miller", "value": "12", "id": 12},
{"label": "Patricia Robinson", "value": "14", "id": 14}
]
}
]
}
]
}
```
## 属性表
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| -------------------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| -------------------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
| delimeter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
@ -909,6 +992,13 @@ icon:
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
| itemHeight | `number` | `32` | 每个选项的高度,用于虚拟渲染 |
| virtualThreshold | `number` | `100` | 在选项数量超过多少时开启虚拟渲染 |
| pagination | `object` | | 分页配置 | `3.6.0` |
| pagination.className | `string` | | 分页控件CSS类名 | `3.6.0` |
| pagination.enable | `boolean` | | 是否开启分页 | `3.6.0` |
| pagination.layout | `string` \| `string[]` | `["pager"]` | 通过控制 layout 属性的顺序,调整分页结构布局 | `3.6.0` |
| pagination.perPageAvailable | `number[]` | `[10, 20, 50, 100]` | 指定每页可以显示多少条 | `3.6.0` |
| pagination.maxButtons | `number` | `5` | 最多显示多少个分页按钮,最小为 5 | `3.6.0` |
| pagination.popOverContainerSelector | `string` | | 切换每页条数的控件挂载点 | `3.6.0` |
## 事件表

View File

@ -433,31 +433,33 @@ List 的内容、Card 卡片的内容配置同上
## 属性表
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ------------------ | ------------------------------------ | --------- | --------------------------------------------------------------------------------------------- | ------- |
| type | `string` | | 如果在 Table、Card 和 List 中,为`"image"`;在 Form 中用作静态展示,为`"static-image"` |
| className | `string` | | 外层 CSS 类名 |
| innerClassName | `string` | | 组件内层 CSS 类名 |
| imageClassName | `string` | | 图片 CSS 类名 |
| thumbClassName | `string` | | 图片缩率图 CSS 类名 |
| height | `string` | | 图片缩率高度 |
| width | `string` | | 图片缩率宽度 |
| title | `string` | | 标题 |
| imageCaption | `string` | | 描述 |
| placeholder | `string` | | 占位文本 |
| defaultImage | `string` | | 无数据时显示的图片 |
| src | `string` | | 缩略图地址 |
| href | [模板](../../docs/concepts/template) | | 外部链接地址 |
| originalSrc | `string` | | 原图地址 |
| enlargeAble | `boolean` | | 支持放大预览 |
| enlargeTitle | `string` | | 放大预览的标题 |
| enlargeCaption | `string` | | 放大预览的描述 |
| enlargeWithGallary | `string` | `true` | 在表格中,图片的放大功能会默认展示所有图片信息,设置为`false`将关闭放大模式下图片集列表的展示 |
| thumbMode | `string` | `contain` | 预览图模式,可选:`'w-full'`, `'h-full'`, `'contain'`, `'cover'` |
| thumbRatio | `string` | `1:1` | 预览图比例,可选:`'1:1'`, `'4:3'`, `'16:9'` |
| imageMode | `string` | `thumb` | 图片展示模式,可选:`'thumb'`, `'original'` 即:缩略图模式 或者 原图模式 |
| showToolbar | `boolean` | `false` | 放大模式下是否展示图片的工具栏 | `2.2.0` |
| toolbarActions | `ImageAction[]` | | 图片工具栏,支持旋转,缩放,默认操作全部开启 | `2.2.0` |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ------------------ | ------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------- | ------- |
| type | `string` | | 如果在 Table、Card 和 List 中,为`"image"`;在 Form 中用作静态展示,为`"static-image"` |
| className | `string` | | 外层 CSS 类名 |
| innerClassName | `string` | | 组件内层 CSS 类名 |
| imageClassName | `string` | | 图片 CSS 类名 |
| thumbClassName | `string` | | 图片缩率图 CSS 类名 |
| height | `string` | | 图片缩率高度 |
| width | `string` | | 图片缩率宽度 |
| title | `string` | | 标题 |
| imageCaption | `string` | | 描述 |
| placeholder | `string` | | 占位文本 |
| defaultImage | `string` | | 无数据时显示的图片 |
| src | `string` | | 缩略图地址 |
| href | [模板](../../docs/concepts/template) | | 外部链接地址 |
| originalSrc | `string` | | 原图地址 |
| enlargeAble | `boolean` | | 支持放大预览 |
| enlargeTitle | `string` | | 放大预览的标题 |
| enlargeCaption | `string` | | 放大预览的描述 |
| enlargeWithGallary | `string` | `true` | 在表格中,图片的放大功能会默认展示所有图片信息,设置为`false`将关闭放大模式下图片集列表的展示 |
| thumbMode | `string` | `contain` | 预览图模式,可选:`'w-full'`, `'h-full'`, `'contain'`, `'cover'` |
| thumbRatio | `string` | `1:1` | 预览图比例,可选:`'1:1'`, `'4:3'`, `'16:9'` |
| imageMode | `string` | `thumb` | 图片展示模式,可选:`'thumb'`, `'original'` 即:缩略图模式 或者 原图模式 |
| showToolbar | `boolean` | `false` | 放大模式下是否展示图片的工具栏 | `2.2.0` |
| toolbarActions | `ImageAction[]` | | 图片工具栏,支持旋转,缩放,默认操作全部开启 | `2.2.0` |
| maxScale | `number` 或 [模板](../../docs/concepts/template) | | 执行调整图片比例动作时的最大百分比 | `3.4.4` |
| minScale | `number` 或 [模板](../../docs/concepts/template) | | 执行调整图片比例动作时的最小百分比 | `3.4.4` |
#### ImageAction
@ -475,3 +477,170 @@ interface ImageAction {
disabled?: boolean;
}
```
## 事件表
当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细查看[事件动作](../../docs/concepts/event-action)。
| 事件名称 | 事件参数 | 说明 |
| ---------- | ---------- | -------------- |
| click | 上下文数据 | 点击图片时触发 |
| mouseenter | 上下文数据 | 鼠标移入时触发 |
| mouseleave | 上下文数据 | 鼠标移入时触发 |
### click / mouseenter / mouseleave
点击图片 / 鼠标移入图片 / 鼠标移出图片,可以尝试通过${event.context.nativeEvent}获取鼠标事件对象。
```schema: scope="body"
{
"type": "image",
"src": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"onEvent": {
"click": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "图片被点击了"
}
}
]
},
"mouseenter": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "鼠标移入图片"
}
}
]
},
"mouseleave": {
"actions": [
{
"actionType": "toast",
"args": {
"msg": "鼠标移出图片"
}
}
]
}
}
}
```
## 动作表
当前组件对外暴露以下特性动作,其他组件可以通过指定`actionType: 动作名称`、`componentId: 该组件id`来触发这些动作,动作配置可以通过`args: {动作配置项名称: xxx}`来配置具体的参数,详细请查看[事件动作](../../docs/concepts/event-action#触发其他组件的动作)。
| 动作名称 | 动作配置 | 说明 |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| preview | - | 预览图片 |
| zoom | `scale: number``scale: `[模板](../../docs/concepts/template),定义每次放大或缩小图片的百分比大小,正值为放大,负值为缩小,默认 50 | 调整图片比例,将图片等比例放大或缩小 |
### preview
预览图片,可以通过配置`originalSrc`来指定预览的原图地址。
```schema: scope="body"
{
"type": "page",
"body": {
"type": "container",
"body": [
{
"type": "container",
"body": [
{
"type": "image",
"className": "mb-1",
"src": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"originalSrc": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg",
"id": "previewImage"
}
]
},
{
"type": "action",
"label": "预览图片",
"onEvent": {
"click": {
"actions": [
{
"actionType": "preview",
"componentId": "previewImage"
}
]
}
}
}
]
}
}
```
### zoom
调整图片比例,将图片等比例放大或缩小。可以通过配置图片的`maxScale`和`minScale`来限制调整的比例。
```schema: scope="body"
{
"type": "page",
"body": {
"type": "container",
"body": [
{
"type": "flex",
"items": [
{
"type": "image",
"innerClassName": "no-border",
"className": "mt-5 mb-5",
"src": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"maxScale": 200,
"minScale": 20,
"id": "zoomImage"
}
]
},
{
"type": "action",
"label": "放大图片",
"onEvent": {
"click": {
"actions": [
{
"actionType": "zoom",
"args": {
"scale": 50,
},
"componentId": "zoomImage"
}
]
}
}
},
{
"type": "action",
"label": "缩小图片",
"className": "mx-1",
"onEvent": {
"click": {
"actions": [
{
"actionType": "zoom",
"args": {
"scale": -50,
},
"componentId": "zoomImage"
}
]
}
}
}
]
}
}
```

View File

@ -81,6 +81,30 @@ order: 58
可以使用 `![text](video.mp4)` 语法来嵌入视频。
## 支持 latex
> 3.6.0 及以上版本
公式渲染使用 KaTeX 实现,由于体积太大默认不提供,需要自己去[下载](https://github.com/KaTeX/KaTeX/releases),在页面中引入以下三个文件:
```
<link rel="stylesheet" href="katex/katex.min.css">
<script src="katex/katex.min.js"></script>
<script src="katex/contrib/auto-render.min.js"></script>
```
markdown 中的 `$``$$` 包裹的内容就能以公式展现,比如 `$\sqrt{a^2 + b^2}$`,如果是在代码中 `\` 要转义为 `\\`
```schema
{
"type": "page",
"body": {
"type": "markdown",
"value": "$$\\hat{f} (\\xi)=\\int_{-\\infty}^{\\infty}f(x)e^{-2\\pi ix\\xi}dx$$"
}
}
```
## 高级配置
> 1.8.1 及以上版本

View File

@ -68,9 +68,111 @@ order: 73
| disabled | `boolean` | false | 是否禁用 |
| onPageChange | page、perPage 改变时会触发 | (page: number, perPage: number) => void; | 分页改变触发 |
## 事件表
当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细请查看[事件动作](../../docs/concepts/event-action)。
> `[name]`表示当前组件绑定的名称,即`name`属性,如果没有配置`name`属性,则通过`value`取值。
> | 事件名称 | 事件参数 | 说明 |
> | -------- | ------------------------------------- | ------------------------------------------- |
> | change | `[value]: object` 当前页码的值<br/> | 当前页码值改变时触发 |
> | change | `page: number` 当前页码的值<br/>`perPage: number` 每页显示的记录数 | 当前页码值改变时触发 |
### change
切换页码时,通过更新 service 数据域中的 page 来实现联动刷新 table 表格数据。
```schema: scope="body"
{
"type": "service",
"id": "service_01",
"api": "/api/mock2/crud/table?page=${page}",
"data": {
"page": 1
},
"body": [
{
"type": "table",
"title": "表格1",
"source": "$rows",
"columns": [
{
"name": "engine",
"label": "Engine"
},
{
"name": "version",
"label": "Version"
}
]
},
{
"type": "pagination",
"activePage": "${page}",
"hasNext": true,
"onEvent": {
"change": {
"actions": [
{
"actionType": "setValue",
"componentId": "service_01",
"args": {
"value": {
"page": "${event.data.page}"
}
}
}
]
}
}
}
]
}
```
切换页码时,通过向 service 发送 page 并重新加载 service 数据来实现联动刷新 table 表格数据。
```schema: scope="body"
{
"type": "service",
"id": "service_02",
"api": "/api/mock2/crud/table?page=${page}",
"data": {
"page": 1
},
"body": [
{
"type": "table",
"title": "表格1",
"source": "$rows",
"columns": [
{
"name": "engine",
"label": "Engine"
},
{
"name": "version",
"label": "Version"
}
]
},
{
"type": "pagination",
"activePage": "${page}",
"hasNext": true,
"onEvent": {
"change": {
"actions": [
{
"actionType": "reload",
"componentId": "service_02",
"data": {
"page": "${event.data.page}"
}
}
]
}
}
}
]
}
```

View File

@ -2324,6 +2324,70 @@ popOver 的其它配置请参考 [popover](./popover)
}
```
### rowDbClick
双击整行时触发。
```schema: scope="body"
{
"type": "service",
"api": "/api/mock2/sample?perPage=10",
"body": [
{
"type": "table",
"source": "$rows",
"onEvent": {
"rowDbClick": {
"actions": [
{
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "行单击数据:${event.data.item|json};行索引:${event.data.index}"
}
}
]
}
},
"columns": [
{
"name": "id",
"label": "ID",
"searchable": true
},
{
"name": "engine",
"label": "Rendering engine",
"filterable": {
"options": [
"Internet Explorer 4.0",
"Internet Explorer 5.0"
]
}
},
{
"name": "browser",
"label": "Browser",
"sortable": true
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
]
}
```
### rowMouseEnter
鼠标移入行记录。

View File

@ -75,42 +75,42 @@ order: 9
```schema
{
type: 'page',
body: [
"type": "page",
"body": [
{
type: 'button',
label: '尝试点击、鼠标移入/移出',
level: 'primary',
onEvent: {
click: {
actions: [
"type": "button",
"label": "尝试点击、鼠标移入/移出",
"level": "primary",
"onEvent": {
"click": {
"actions": [
{
actionType: 'toast',
args: {
msgType: 'info',
msg: '派发点击事件'
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "派发点击事件"
}
}
]
},
mouseenter: {
actions: [
"mouseenter": {
"actions": [
{
actionType: 'toast',
args: {
msgType: 'info',
msg: '派发鼠标移入事件'
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "派发鼠标移入事件"
}
}
]
},
mouseleave: {
actions: [
"mouseleave": {
"actions": [
{
actionType: 'toast',
args: {
msgType: 'info',
msg: '派发鼠标移出事件'
"actionType": "toast",
"args": {
"msgType": "info",
"msg": "派发鼠标移出事件"
}
}
]

View File

@ -24,6 +24,7 @@
rel="stylesheet"
href="/node_modules/@fortawesome/fontawesome-free/css/v4-shims.css"
/>
<link rel="stylesheet" href="/node_modules/katex/dist/katex.min.css" />
<link rel="stylesheet" href="/node_modules/prismjs/themes/prism.css" />
<link rel="stylesheet" href="/examples/doc.css" />
@ -93,5 +94,10 @@
const initialState = {};
bootstrap(document.getElementById('root'), initialState);
</script>
<script defer src="/node_modules/katex/dist/katex.min.js"></script>
<script
defer
src="/node_modules/katex/dist/contrib/auto-render.min.js"
></script>
</body>
</html>

View File

@ -0,0 +1,238 @@
/** 前端分页的接口 */
module.exports = function (req, res) {
res.json({
status: 0,
msg: 'ok',
data: {
count: data.length,
items: data
}
});
};
const data = [
{
"label": "Laura Lewis",
"value": "1"
},
{
"label": "David Gonzalez",
"value": "2"
},
{
"label": "Christopher Rodriguez",
"value": "3"
},
{
"label": "Sarah Young",
"value": "4"
},
{
"label": "James Jones",
"value": "5"
},
{
"label": "Larry Robinson",
"value": "6"
},
{
"label": "Christopher Perez",
"value": "7"
},
{
"label": "Sharon Davis",
"value": "8"
},
{
"label": "Kenneth Anderson",
"value": "9"
},
{
"label": "Deborah Lewis",
"value": "10"
},
{
"label": "Jennifer Lewis",
"value": "11"
},
{
"label": "Laura Miller",
"value": "12"
},
{
"label": "Larry Harris",
"value": "13"
},
{
"label": "Patricia Robinson",
"value": "14"
},
{
"label": "Mark Davis",
"value": "15"
},
{
"label": "Jessica Harris",
"value": "16"
},
{
"label": "Anna Brown",
"value": "17"
},
{
"label": "Lisa Young",
"value": "18"
},
{
"label": "Donna Williams",
"value": "19"
},
{
"label": "Shirley Davis",
"value": "20"
},
{
"label": "Richard Clark",
"value": "21"
},
{
"label": "Cynthia Martinez",
"value": "22"
},
{
"label": "Kimberly Walker",
"value": "23"
},
{
"label": "Timothy Anderson",
"value": "24"
},
{
"label": "Betty Lee",
"value": "25"
},
{
"label": "Jeffrey Allen",
"value": "26"
},
{
"label": "Karen Martinez",
"value": "27"
},
{
"label": "Anna Lopez",
"value": "28"
},
{
"label": "Dorothy Anderson",
"value": "29"
},
{
"label": "David Perez",
"value": "30"
},
{
"label": "Dorothy Martin",
"value": "31"
},
{
"label": "George Johnson",
"value": "32"
},
{
"label": "Donald Jackson",
"value": "33"
},
{
"label": "Mary Brown",
"value": "34"
},
{
"label": "Deborah Martinez",
"value": "35"
},
{
"label": "Donald Jackson",
"value": "36"
},
{
"label": "Lisa Robinson",
"value": "37"
},
{
"label": "Laura Martinez",
"value": "38"
},
{
"label": "Timothy Taylor",
"value": "39"
},
{
"label": "Joseph Martinez",
"value": "40"
},
{
"label": "Karen Wilson",
"value": "41"
},
{
"label": "Karen Walker",
"value": "42"
},
{
"label": "William Martinez",
"value": "43"
},
{
"label": "Linda Brown",
"value": "44"
},
{
"label": "Elizabeth Brown",
"value": "45"
},
{
"label": "Anna Moore",
"value": "46"
},
{
"label": "Robert Martinez",
"value": "47"
},
{
"label": "Edward Hernandez",
"value": "48"
},
{
"label": "Elizabeth Hall",
"value": "49"
},
{
"label": "Linda Jackson",
"value": "50"
},
{
"label": "Brian Jones",
"value": "51"
},
{
"label": "Amy Thompson",
"value": "52"
},
{
"label": "Kimberly Wilson",
"value": "53"
},
{
"label": "Nancy Garcia",
"value": "54"
},
{
"label": "Mary Thompson",
"value": "55"
}
].map(function (item, index) {
return Object.assign({}, item, {
id: index + 1
});
});

View File

@ -0,0 +1,242 @@
/** Transfer分页接口 */
module.exports = function (req, res) {
const perPage = Number(req.query.perPage || 10);
const page = Number(req.query.page || 1);
res.json({
status: 0,
msg: 'ok',
data: {
count: data.length,
page: page,
items: data.concat().splice((page - 1) * perPage, perPage)
}
});
};
const data = [
{
"label": "Laura Lewis",
"value": "1"
},
{
"label": "David Gonzalez",
"value": "2"
},
{
"label": "Christopher Rodriguez",
"value": "3"
},
{
"label": "Sarah Young",
"value": "4"
},
{
"label": "James Jones",
"value": "5"
},
{
"label": "Larry Robinson",
"value": "6"
},
{
"label": "Christopher Perez",
"value": "7"
},
{
"label": "Sharon Davis",
"value": "8"
},
{
"label": "Kenneth Anderson",
"value": "9"
},
{
"label": "Deborah Lewis",
"value": "10"
},
{
"label": "Jennifer Lewis",
"value": "11"
},
{
"label": "Laura Miller",
"value": "12"
},
{
"label": "Larry Harris",
"value": "13"
},
{
"label": "Patricia Robinson",
"value": "14"
},
{
"label": "Mark Davis",
"value": "15"
},
{
"label": "Jessica Harris",
"value": "16"
},
{
"label": "Anna Brown",
"value": "17"
},
{
"label": "Lisa Young",
"value": "18"
},
{
"label": "Donna Williams",
"value": "19"
},
{
"label": "Shirley Davis",
"value": "20"
},
{
"label": "Richard Clark",
"value": "21"
},
{
"label": "Cynthia Martinez",
"value": "22"
},
{
"label": "Kimberly Walker",
"value": "23"
},
{
"label": "Timothy Anderson",
"value": "24"
},
{
"label": "Betty Lee",
"value": "25"
},
{
"label": "Jeffrey Allen",
"value": "26"
},
{
"label": "Karen Martinez",
"value": "27"
},
{
"label": "Anna Lopez",
"value": "28"
},
{
"label": "Dorothy Anderson",
"value": "29"
},
{
"label": "David Perez",
"value": "30"
},
{
"label": "Dorothy Martin",
"value": "31"
},
{
"label": "George Johnson",
"value": "32"
},
{
"label": "Donald Jackson",
"value": "33"
},
{
"label": "Mary Brown",
"value": "34"
},
{
"label": "Deborah Martinez",
"value": "35"
},
{
"label": "Donald Jackson",
"value": "36"
},
{
"label": "Lisa Robinson",
"value": "37"
},
{
"label": "Laura Martinez",
"value": "38"
},
{
"label": "Timothy Taylor",
"value": "39"
},
{
"label": "Joseph Martinez",
"value": "40"
},
{
"label": "Karen Wilson",
"value": "41"
},
{
"label": "Karen Walker",
"value": "42"
},
{
"label": "William Martinez",
"value": "43"
},
{
"label": "Linda Brown",
"value": "44"
},
{
"label": "Elizabeth Brown",
"value": "45"
},
{
"label": "Anna Moore",
"value": "46"
},
{
"label": "Robert Martinez",
"value": "47"
},
{
"label": "Edward Hernandez",
"value": "48"
},
{
"label": "Elizabeth Hall",
"value": "49"
},
{
"label": "Linda Jackson",
"value": "50"
},
{
"label": "Brian Jones",
"value": "51"
},
{
"label": "Amy Thompson",
"value": "52"
},
{
"label": "Kimberly Wilson",
"value": "53"
},
{
"label": "Nancy Garcia",
"value": "54"
},
{
"label": "Mary Thompson",
"value": "55"
}
].map(function (item, index) {
return Object.assign({}, item, {
id: index + 1
});
});

View File

@ -40,9 +40,9 @@
]
},
"dependencies": {
"path-to-regexp": "^6.2.0",
"postcss": "^8.4.14",
"qs": "6.9.7",
"path-to-regexp": "^6.2.0"
"qs": "6.9.7"
},
"devDependencies": {
"@babel/generator": "^7.22.9",
@ -79,6 +79,7 @@
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"js-yaml": "^4.1.0",
"katex": "^0.16.9",
"lerna": "^6.6.2",
"lint-staged": "^12.1.2",
"magic-string": "^0.26.7",

View File

@ -36,6 +36,19 @@ export class CmptAction implements RendererAction {
*/
const key = action.componentId || action.componentName;
const dataMergeMode = action.dataMergeMode || 'merge';
const path = action.args?.path;
/** 如果args中携带path参数, 则认为是全局变量赋值, 否则认为是组件变量赋值 */
if (action.actionType === 'setValue' && path && typeof path === 'string') {
const beforeSetData = renderer?.props?.env?.beforeSetData;
if (beforeSetData && typeof beforeSetData === 'function') {
const res = await beforeSetData(renderer, action, event);
if (res === false) {
return;
}
}
}
if (!key) {
console.warn('请提供目标组件的componentId或componentName');
@ -59,23 +72,6 @@ export class CmptAction implements RendererAction {
}
if (action.actionType === 'setValue') {
const beforeSetData = renderer?.props?.env?.beforeSetData;
const path = action.args?.path;
/** 如果args中携带path参数, 则认为是全局变量赋值, 否则认为是组件变量赋值 */
if (
path &&
typeof path === 'string' &&
beforeSetData &&
typeof beforeSetData === 'function'
) {
const res = await beforeSetData(renderer, action, event);
if (res === false) {
return;
}
}
if (component?.setData) {
return component?.setData(
action.args?.value,

View File

@ -87,6 +87,16 @@ export interface RendererEnv {
affixOffsetBottom?: number;
richTextToken: string;
/**
* baidu
*/
locationPickerVendor?: string;
/**
* ak
*/
locationPickerAK?: string;
loadRenderer: (
schema: Schema,
path: string,

View File

@ -50,6 +50,7 @@ import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item';
import {injectObjectChain} from '../utils';
import {reaction} from 'mobx';
export interface FormHorizontal {
left?: number;
@ -371,6 +372,7 @@ export interface FormProps
onFailed?: (reason: string, errors: any) => any;
onFinished: (values: object, action: any) => any;
onValidate: (values: object, form: any) => any;
onValidChange?: (valid: boolean, props: any) => void; // 表单数据合法性变更
messages: {
fetchSuccess?: string;
fetchFailed?: string;
@ -443,6 +445,8 @@ export default class Form extends React.Component<FormProps, object> {
'onChange',
'onFailed',
'onFinished',
'onValidate',
'onValidChange',
'onSaved',
'canAccessSuperData',
'lazyChange',
@ -460,8 +464,7 @@ export default class Form extends React.Component<FormProps, object> {
[propName: string]: Array<() => Promise<any>>;
} = {};
asyncCancel: () => void;
disposeOnValidate: () => void;
disposeRulesValidate: () => void;
toDispose: Array<() => void> = [];
shouldLoadInitApi: boolean = false;
timer: ReturnType<typeof setTimeout>;
mounted: boolean;
@ -532,6 +535,7 @@ export default class Form extends React.Component<FormProps, object> {
store,
messages: {fetchSuccess, fetchFailed},
onValidate,
onValidChange,
promptPageLeave,
env,
rules
@ -541,49 +545,63 @@ export default class Form extends React.Component<FormProps, object> {
if (onValidate) {
const finalValidate = promisify(onValidate);
this.disposeOnValidate = this.addHook(async () => {
const result = await finalValidate(store.data, store);
this.toDispose.push(
this.addHook(async () => {
const result = await finalValidate(store.data, store);
if (result && isObject(result)) {
Object.keys(result).forEach(key => {
let msg = result[key];
const items = store.getItemsByPath(key);
if (result && isObject(result)) {
Object.keys(result).forEach(key => {
let msg = result[key];
const items = store.getItemsByPath(key);
// 没有找到
if (!Array.isArray(items) || !items.length) {
return;
}
// 没有找到
if (!Array.isArray(items) || !items.length) {
return;
}
// 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去
items.forEach(item => item.clearError());
// 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去
items.forEach(item => item.clearError());
if (msg) {
msg = Array.isArray(msg) ? msg : [msg];
items.forEach(item => item.addError(msg));
}
if (msg) {
msg = Array.isArray(msg) ? msg : [msg];
items.forEach(item => item.addError(msg));
}
delete result[key];
});
delete result[key];
});
isEmpty(result)
? store.clearRestError()
: store.setRestError(Object.keys(result).map(key => result[key]));
}
});
isEmpty(result)
? store.clearRestError()
: store.setRestError(Object.keys(result).map(key => result[key]));
}
})
);
}
// 表单校验结果发生变化时,触发 onValidChange
if (onValidChange) {
this.toDispose.push(
reaction(
() => store.valid,
valid => onValidChange(valid, this.props)
)
);
}
if (Array.isArray(rules) && rules.length) {
this.disposeRulesValidate = this.addHook(() => {
if (!store.valid) {
return;
}
this.toDispose.push(
this.addHook(() => {
if (!store.valid) {
return;
}
rules.forEach(
item =>
!evalExpression(item.rule, store.data) &&
store.addRestError(item.message, item.name)
);
});
rules.forEach(
item =>
!evalExpression(item.rule, store.data) &&
store.addRestError(item.message, item.name)
);
})
);
}
if (isEffectiveApi(initApi, store.data, initFetch, initFetchOn)) {
@ -655,8 +673,8 @@ export default class Form extends React.Component<FormProps, object> {
// this.lazyHandleChange.flush();
this.lazyEmitChange.cancel();
this.asyncCancel && this.asyncCancel();
this.disposeOnValidate && this.disposeOnValidate();
this.disposeRulesValidate && this.disposeRulesValidate();
this.toDispose.forEach(fn => fn());
this.toDispose = [];
window.removeEventListener('beforeunload', this.beforePageUnload);
this.unBlockRouting?.();
}
@ -836,30 +854,27 @@ export default class Form extends React.Component<FormProps, object> {
return this.props.store.validated;
}
validate(
async validate(
forceValidate?: boolean,
throwErrors: boolean = false
throwErrors: boolean = false,
toastErrors: boolean = true
): Promise<boolean> {
const {store, dispatchEvent, data, messages, translate: __} = this.props;
this.flush();
return store
.validate(
this.hooks['validate'] || [],
forceValidate,
throwErrors,
typeof messages?.validateFailed === 'string'
? __(filter(messages.validateFailed, store.data))
: undefined
)
.then((result: boolean) => {
if (result) {
dispatchEvent('validateSucc', data);
} else {
dispatchEvent('validateError', data);
}
return result;
});
const result = await store.validate(
this.hooks['validate'] || [],
forceValidate,
throwErrors,
toastErrors === false
? ''
: typeof messages?.validateFailed === 'string'
? __(filter(messages.validateFailed, store.data))
: undefined
);
dispatchEvent(result ? 'validateSucc' : 'validateError', data);
return result;
}
setErrors(errors: {[propName: string]: string}, tag = 'remote') {

View File

@ -37,6 +37,7 @@ import {
FormBaseControl
} from './Item';
import {IFormItemStore} from '../store/formItem';
import {isObject} from 'amis-core';
export type OptionsControlComponent = React.ComponentType<FormControlProps>;
@ -230,7 +231,11 @@ export interface OptionsControlProps
selectedOptions: Array<Option>;
setOptions: (value: Array<any>, skipNormalize?: boolean) => void;
setLoading: (value: boolean) => void;
reloadOptions: (setError?: boolean) => void;
reloadOptions: (
setError?: boolean,
isInit?: boolean,
data?: Record<string, any>
) => void;
deferLoad: (option: Option) => void;
leftDeferLoad: (option: Option, leftOptions: Option) => void;
expandTreeOptions: (nodePathArr: any[]) => void;
@ -443,15 +448,12 @@ export function registerOptionsControl(config: OptionsConfig) {
);
if (prevOptions !== options) {
formItem.setOptions(
normalizeOptions(
options || [],
undefined,
props.valueField || 'value'
),
this.changeOptionValue,
props.data
formItem.loadOptionsFromDataScope(
props.source as string,
props.data,
this.changeOptionValue
);
this.normalizeValue();
}
} else if (
@ -792,20 +794,16 @@ export function registerOptionsControl(config: OptionsConfig) {
}
@autobind
reloadOptions(setError?: boolean, isInit = false) {
const {source, formItem, data, onChange, setPrinstineValue, valueField} =
reloadOptions(setError?: boolean, isInit = false, data = this.props.data) {
const {source, formItem, onChange, setPrinstineValue, valueField} =
this.props;
if (formItem && isPureVariable(source as string)) {
isAlive(formItem) &&
formItem.setOptions(
normalizeOptions(
resolveVariableAndFilter(source as string, data, '| raw') || [],
undefined,
valueField
),
this.changeOptionValue,
data
formItem.loadOptionsFromDataScope(
source as string,
data,
this.changeOptionValue
);
return;
} else if (!formItem || !isEffectiveApi(source, data)) {

View File

@ -156,7 +156,8 @@ export function wrapControl<
minLength,
maxLength,
validateOnChange,
label
label,
pagination
}
} = this.props;
@ -230,7 +231,8 @@ export function wrapControl<
validateOnChange,
label,
inputGroupControl,
extraName
extraName,
pagination
});
// issue 这个逻辑应该在 combo 里面自己实现。
@ -380,7 +382,8 @@ export function wrapControl<
'minLength',
'maxLength',
'label',
'extraName'
'extraName',
'pagination'
],
prevProps.$schema,
props.$schema,

View File

@ -35,7 +35,8 @@ export const ComboStore = iRendererStore
minLength: 0,
maxLength: 0,
length: 0,
activeKey: 0
activeKey: 0,
memberValidMap: types.optional(types.frozen(), {})
})
.views(self => {
function getForms() {
@ -170,13 +171,21 @@ export const ComboStore = iRendererStore
self.activeKey = key;
}
function setMemberValid(valid: boolean, index: number) {
self.memberValidMap = {
...self.memberValidMap,
[index]: valid
};
}
return {
config,
setActiveKey,
bindUniuqueItem,
unBindUniuqueItem,
addForm,
onChildStoreDispose
onChildStoreDispose,
setMemberValid
};
});

View File

@ -161,10 +161,10 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
rule => rule.includes(lhs) && rule.includes(rhs)
)
) {
return lhs === rhs;
return lhs !== rhs;
}
return lhs == rhs;
return lhs != rhs;
})
) {
if (query[pageField || 'page']) {

View File

@ -7,11 +7,13 @@ import {
Instance
} from 'mobx-state-tree';
import isEqualWith from 'lodash/isEqualWith';
import uniqWith from 'lodash/uniqWith';
import {FormStore, IFormStore} from './form';
import {str2rules, validate as doValidate} from '../utils/validations';
import {Api, Payload, fetchOptions, ApiObject} from '../types';
import {ComboStore, IComboStore, IUniqueGroup} from './combo';
import {evalExpression} from '../utils/tpl';
import {resolveVariableAndFilter} from '../utils/tpl-builtin';
import {buildApi, isEffectiveApi} from '../utils/api';
import findIndex from 'lodash/findIndex';
import {
@ -98,6 +100,7 @@ export const FormItemStore = StoreNode.named('FormItemStore')
joinValues: true,
extractValue: false,
options: types.optional(types.frozen<Array<any>>(), []),
optionsRaw: types.optional(types.frozen<Array<any>>(), []),
expressionsInOptions: false,
selectFirst: false,
autoFill: types.frozen(),
@ -113,7 +116,18 @@ export const FormItemStore = StoreNode.named('FormItemStore')
/** 当前表单项所属的InputGroup父元素, 用于收集InputGroup的子元素 */
inputGroupControl: types.optional(types.frozen(), {}),
colIndex: types.frozen(),
rowIndex: types.frozen()
rowIndex: types.frozen(),
/** Transfer组件分页模式 */
pagination: types.optional(types.frozen(), {
enable: false,
/** 当前页数 */
page: 1,
/** 每页显示条数 */
perPage: 10,
/** 总条数 */
total: 0
}),
accumulatedOptions: types.optional(types.frozen<Array<any>>(), [])
})
.views(self => {
function getForm(): any {
@ -175,6 +189,26 @@ export const FormItemStore = StoreNode.named('FormItemStore')
return getLastOptionValue();
},
/** 数据源接口数据是否开启分页 */
get enableSourcePagination(): boolean {
return !!self.pagination.enable;
},
/** 数据源接口开启分页时当前页码 */
get sourcePageNum(): number {
return self.pagination.page ?? 1;
},
/** 数据源接口开启分页时每页显示条数 */
get sourcePerPageNum(): number {
return self.pagination.perPage ?? 10;
},
/** 数据源接口开启分页时数据总条数 */
get sourceTotalNum(): number {
return self.pagination.total ?? 0;
},
getSelectedOptions: (
value: any = self.tmpValue,
nodeValueArray?: any[] | undefined
@ -308,7 +342,8 @@ export const FormItemStore = StoreNode.named('FormItemStore')
minLength,
validateOnChange,
label,
inputGroupControl
inputGroupControl,
pagination
}: {
extraName?: string;
required?: boolean;
@ -338,6 +373,11 @@ export const FormItemStore = StoreNode.named('FormItemStore')
path: string;
[propsName: string]: any;
};
pagination?: {
enable?: boolean;
page?: number;
perPage?: number;
};
}) {
if (typeof rules === 'string') {
rules = str2rules(rules);
@ -372,6 +412,15 @@ export const FormItemStore = StoreNode.named('FormItemStore')
inputGroupControl?.name != null &&
(self.inputGroupControl = inputGroupControl);
if (pagination && isObject(pagination) && !!pagination.enable) {
self.pagination = {
enable: true,
page: pagination.page ? pagination.page || 1 : 1,
perPage: pagination.perPage ? pagination.perPage || 10 : 10,
total: 0
};
}
if (
typeof rules !== 'undefined' ||
typeof required !== 'undefined' ||
@ -556,6 +605,23 @@ export const FormItemStore = StoreNode.named('FormItemStore')
}
}
function setPagination(params: {
page?: number;
perPage?: number;
total?: number;
}) {
const {page, perPage, total} = params || {};
if (self.enableSourcePagination) {
self.pagination = {
...self.pagination,
...(page != null && typeof page === 'number' ? {page} : {}),
...(perPage != null && typeof perPage === 'number' ? {perPage} : {}),
...(total != null && typeof total === 'number' ? {total} : {})
};
}
}
function setOptions(
options: Array<object>,
onChange?: (value: any) => void,
@ -567,6 +633,15 @@ export const FormItemStore = StoreNode.named('FormItemStore')
options = filterTree(options, item => item);
const originOptions = self.options.concat();
self.options = options;
/** 开启分页后当前选项内容需要累加 */
self.accumulatedOptions = self.enableSourcePagination
? uniqWith(
[...originOptions, ...options],
(lhs, rhs) =>
lhs[self.valueField ?? 'value'] ===
rhs[self.valueField ?? 'value']
)
: options;
syncOptions(originOptions, data);
let selectedOptions;
@ -722,6 +797,14 @@ export const FormItemStore = StoreNode.named('FormItemStore')
options = normalizeOptions(options as any, undefined, self.valueField);
if (self.enableSourcePagination) {
self.pagination = {
...self.pagination,
page: parseInt(json.data?.page, 10) || 1,
total: parseInt(json.data?.total ?? json.data?.count, 10) || 0
};
}
if (config?.extendsOptions && self.selectedOptions.length > 0) {
self.selectedOptions.forEach((item: any) => {
const exited = findTree(
@ -752,6 +835,41 @@ export const FormItemStore = StoreNode.named('FormItemStore')
return json;
});
/**
* source变量解析后是全量的数据源
*/
function loadOptionsFromDataScope(
source: string,
ctx: Record<string, any>,
onChange?: (value: any) => void
) {
let options: any[] = resolveVariableAndFilter(source, ctx, '| raw');
if (!Array.isArray(options)) {
return [];
}
options = normalizeOptions(options, undefined, self.valueField);
if (self.enableSourcePagination) {
self.pagination = {
...self.pagination,
...(ctx?.page ? {page: ctx?.page} : {}),
...(ctx?.perPage ? {perPage: ctx?.perPage} : {}),
total: options.length
};
}
options = options.slice(
(self.pagination.page - 1) * self.pagination.perPage,
self.pagination.page * self.pagination.perPage
);
setOptions(options, onChange, ctx);
return options;
}
const loadAutoUpdateData: (
api: Api,
data?: object,
@ -1377,8 +1495,10 @@ export const FormItemStore = StoreNode.named('FormItemStore')
setError,
addError,
clearError,
setPagination,
setOptions,
loadOptions,
loadOptionsFromDataScope,
deferLoadOptions,
deferLoadLeftOptions,
expandTreeOptions,

View File

@ -657,6 +657,11 @@ export interface BaseSchemaWithoutType {
*
*/
editorSetting?: {
/**
* createupdateremove
*/
behavior?: string;
/**
* 便
*/
@ -666,6 +671,8 @@ export interface BaseSchemaWithoutType {
* 便
*/
mock?: any;
[propName: string]: any;
};
/**

View File

@ -304,7 +304,9 @@ export interface RendererInfo extends RendererScaffoldInfo {
sharedContext?: Record<string, any>;
dialogTitle?: string; //弹窗标题用于弹窗大纲的展示
dialogType?: string; //区分确认对话框类型
subEditorVariable?: Array<{label: string; children: any}>; // 传递给子编辑器的组件自定义变量如listSelect的选项名称和值
getSubEditorVariable?: (
schema?: any
) => Array<{label: string; children: any}>; // 传递给子编辑器的组件自定义变量如listSelect的选项名称和值
}
export type BasicRendererInfo = Omit<
@ -1051,7 +1053,7 @@ export abstract class BasePlugin implements PluginInterface {
isListComponent: plugin.isListComponent,
rendererName: plugin.rendererName,
memberImmutable: plugin.memberImmutable,
subEditorVariable: plugin.subEditorVariable
getSubEditorVariable: plugin.getSubEditorVariable
};
}
}

View File

@ -79,7 +79,7 @@ export function JSONPipeIn(obj: any, generateId = false, idMap: any = {}): any {
}
if (Array.isArray(obj)) {
return obj.map((item, index) => JSONPipeIn(item, generateId));
return obj.map((item, index) => JSONPipeIn(item, generateId, idMap));
}
let toUpdate: any = {};
@ -1224,7 +1224,10 @@ export async function resolveVariablesFromScope(node: any, manager: any) {
// 子编辑器内读取的host节点自定义变量非数据域方式如listSelect的选项值
let hostNodeVaraibles = [];
if (manager?.store?.isSubEditor) {
hostNodeVaraibles = manager.config?.hostNode?.info?.subEditorVariable || [];
hostNodeVaraibles =
manager.config?.hostNode?.info?.getSubEditorVariable?.(
manager.config?.hostNode.schema
) || [];
}
const variables: VariableItem[] =

View File

@ -36,7 +36,7 @@ import type {
} from 'amis-editor-core';
import {normalizeApi} from 'amis-core';
import isPlainObject from 'lodash/isPlainObject';
import omit from 'lodash/omit';
import findLastIndex from 'lodash/findLastIndex';
interface ColumnItem {
label: string;
@ -371,6 +371,9 @@ export class CRUDPlugin extends BasePlugin {
type: 'button',
actionType: 'dialog',
level: 'primary',
editorSetting: {
behavior: 'create'
},
dialog: {
title: '新增',
body: {
@ -385,6 +388,9 @@ export class CRUDPlugin extends BasePlugin {
type: 'button',
actionType: 'dialog',
level: 'link',
editorSetting: {
behavior: 'update'
},
dialog: {
title: '编辑',
body: {
@ -399,6 +405,9 @@ export class CRUDPlugin extends BasePlugin {
type: 'button',
actionType: 'dialog',
level: 'link',
editorSetting: {
behavior: 'view'
},
dialog: {
title: '查看详情',
body: {
@ -415,7 +424,10 @@ export class CRUDPlugin extends BasePlugin {
level: 'link',
className: 'text-danger',
confirmText: '确定要删除?',
api: 'delete:/xxx/delete'
api: 'delete:/xxx/delete',
editorSetting: {
behavior: 'delete'
}
},
bulkDelete: {
type: 'button',
@ -423,12 +435,18 @@ export class CRUDPlugin extends BasePlugin {
label: '批量删除',
actionType: 'ajax',
confirmText: '确定要删除?',
api: '/xxx/batch-delete'
api: '/xxx/batch-delete',
editorSetting: {
behavior: 'bulkDelete'
}
},
bulkUpdate: {
type: 'button',
label: '批量编辑',
actionType: 'dialog',
editorSetting: {
behavior: 'bulkUpdate'
},
dialog: {
title: '批量编辑',
size: 'md',
@ -543,7 +561,7 @@ export class CRUDPlugin extends BasePlugin {
}
},
{
name: 'features',
name: '__features',
label: '启用功能',
type: 'checkboxes',
joinValues: false,
@ -575,10 +593,10 @@ export class CRUDPlugin extends BasePlugin {
type: 'input-number',
label: '每列显示几个字段',
value: 3,
name: 'filterColumnCount'
name: '__filterColumnCount'
}
],
visibleOn: 'data.features && data.features.includes("filter")'
visibleOn: "${__features && CONTAINS(__features, 'filter')}"
},
{
name: 'columns',
@ -641,33 +659,99 @@ export class CRUDPlugin extends BasePlugin {
]
}
],
pipeIn: (value: any) => {
const __features = [];
// 收集 filter
if (value.filter) {
__features.push('filter');
}
// 收集 列操作
const lastIndex = findLastIndex(
value.columns || [],
(item: any) => item.type === 'operation'
);
if (lastIndex !== -1) {
const operBtns: Array<string> = ['update', 'view', 'delete'];
(value.columns[lastIndex].buttons || []).forEach((btn: any) => {
if (operBtns.includes(btn.editorSetting?.behavior || '')) {
__features.push(btn.editorSetting?.behavior);
}
});
}
// 收集批量操作
if (Array.isArray(value.bulkActions)) {
value.bulkActions.forEach((item: any) => {
if (item.editorSetting?.behavior) {
__features.push(item.editorSetting?.behavior);
}
});
}
// 收集新增
if (
Array.isArray(value.headerToolbar) &&
value.headerToolbar.some(
(item: any) => item.editorSetting?.behavior === 'create'
)
) {
__features.push('create');
}
return {
...value,
__filterColumnCount: value?.filter?.columnCount || 3,
__features: __features,
__LastFeatures: [...__features]
};
},
pipeOut: (value: any) => {
let valueSchema = cloneDeep(value);
// 查看/删除 操作,可选择是否使用接口返回值预填充
const features: Array<any> = valueSchema.features;
const oper: {
type: 'operation';
label?: string;
buttons: Array<ActionSchema>;
} = {
type: 'operation',
label: '操作',
buttons: []
};
const itemBtns: Array<string> = ['update', 'view', 'delete'];
const hasFeatures = get(features, 'length');
valueSchema.bulkActions = [];
/** 统一api格式 */
valueSchema.api =
typeof valueSchema.api === 'string'
? normalizeApi(valueSchema.api)
: valueSchema.api;
hasFeatures &&
features.forEach((item: string) => {
if (itemBtns.includes(item)) {
let schema;
const features: string[] = valueSchema.__features;
const lastFeatures: string[] = valueSchema.__LastFeatures;
const willAddedList = features.filter(
item => !lastFeatures.includes(item)
);
const willRemoveList = lastFeatures.filter(
item => !features.includes(item)
);
const operButtons: any[] = [];
const operBtns: string[] = ['update', 'view', 'delete'];
if (!valueSchema.bulkActions) {
valueSchema.bulkActions = [];
} else {
// 删除 未勾选的批量操作
valueSchema.bulkActions = valueSchema.bulkActions.filter(
(item: any) =>
!willRemoveList.includes(item.editorSetting?.behavior)
);
}
// 删除 未勾选的 filter
if (willRemoveList.includes('filter') && valueSchema.filter) {
delete valueSchema.filter;
}
// 删除 未勾选的 新增
if (
willRemoveList.includes('create') &&
Array.isArray(valueSchema.headerToolbar)
) {
valueSchema.headerToolbar = valueSchema.headerToolbar.filter(
(item: any) => item.editorSetting?.behavior !== 'create'
);
}
willAddedList.length &&
willAddedList.forEach((item: string) => {
if (operBtns.includes(item)) {
// 列操作按钮
let schema;
if (item === 'update') {
schema = cloneDeep(this.btnSchemas.update);
schema.dialog.body.body = value.columns
@ -692,9 +776,7 @@ export class CRUDPlugin extends BasePlugin {
? valueSchema.api
: {...valueSchema.api, method: 'post'};
}
// 添加操作按钮
this.addItem(oper.buttons, schema);
schema && operButtons.push(schema);
} else {
// 批量操作
if (item === 'bulkUpdate') {
@ -730,13 +812,14 @@ export class CRUDPlugin extends BasePlugin {
};
valueSchema.headerToolbar = [createSchemaBase, 'bulkActions'];
}
// 查询
let keysFilter = Object.keys(valueSchema.filter || {});
if (item === 'filter' && !keysFilter.length) {
if (valueSchema.filterEnabledList) {
valueSchema.filter = {
title: '查询条件'
};
valueSchema.filter.columnCount = value.filterColumnCount;
valueSchema.filter.columnCount = value.__filterColumnCount;
valueSchema.filter.mode = 'horizontal';
valueSchema.filter.body = valueSchema.filterEnabledList.map(
(item: any) => {
@ -751,10 +834,30 @@ export class CRUDPlugin extends BasePlugin {
}
}
});
const hasOperate = valueSchema.columns.find(
// 处理列操作按钮
const lastIndex = findLastIndex(
value.columns || [],
(item: any) => item.type === 'operation'
);
hasFeatures && !hasOperate && valueSchema.columns.push(oper);
if (lastIndex === -1) {
if (operButtons.length) {
valueSchema.columns.push({
type: 'operation',
label: '操作',
buttons: operButtons
});
}
} else {
const operColumn = valueSchema.columns[lastIndex];
operColumn.buttons = (operColumn.buttons || [])
.filter(
(btn: any) =>
!willRemoveList.includes(btn.editorSetting?.behavior)
)
.concat(operButtons);
}
return valueSchema;
},
canRebuild: true

View File

@ -20,7 +20,7 @@ export class CalendarPlugin extends BasePlugin {
panelTitle = '日历日程';
description = '展示日历及日程。';
docLink = '/amis/zh-CN/components/calendor';
docLink = '/amis/zh-CN/components/calendar';
tags = ['展示'];
scaffold = {

View File

@ -679,7 +679,7 @@ export class ComboControlPlugin extends BasePlugin {
}`;
let isColumnChild = false;
if (trigger) {
if (trigger && items) {
isColumnChild = someTree(items.children, item => item.id === trigger?.id);
if (isColumnChild) {
@ -698,7 +698,7 @@ export class ComboControlPlugin extends BasePlugin {
}
}
const pool = items.children.concat();
const pool = items?.children?.concat() || [];
while (pool.length) {
const current = pool.shift() as EditorNodeType;

View File

@ -474,7 +474,7 @@ export class FormPlugin extends BasePlugin {
return {
type: 'container',
className: 'form-item-gap',
visibleOn: `data.feat === '${feat.value}' && (!data.dsType || data.dsType === '${builderKey}')`,
visibleOn: `$\{feat === '${feat.value}' && (!dsType || dsType === '${builderKey}')}`,
body: flatten([
builder.makeSourceSettingForm({
feat: feat.value,
@ -685,26 +685,28 @@ export class FormPlugin extends BasePlugin {
const dsSettings = flatten(
this.Features.map(feat =>
this.dsManager.buildCollectionFromBuilders(
(builder, builderKey, index) => ({
type: 'container',
className: 'form-item-gap',
visibleOn: `data.feat === '${
feat.value
}' && (data.dsType == null ? '${builderKey}' === '${
defaultDsType || ApiDSBuilderKey
}' : data.dsType === '${builderKey}')`,
body: flatten([
builder.makeSourceSettingForm({
feat: feat.value,
renderer: 'form',
inScaffold: false,
sourceSettings: {
renderLabel: true,
userOrders: false
}
})
])
})
(builder, builderKey, index) => {
return {
type: 'container',
className: 'form-item-gap',
visibleOn: `$\{feat === '${
feat.value
}' && (dsType == null ? '${builderKey}' === '${
defaultDsType || ApiDSBuilderKey
}' : dsType === '${builderKey}')}`,
body: flatten([
builder.makeSourceSettingForm({
feat: feat.value,
renderer: 'form',
inScaffold: false,
sourceSettings: {
renderLabel: true,
userOrders: false
}
})
])
};
}
)
)
);

View File

@ -1,15 +1,21 @@
import {isObject} from 'amis';
import {
EditorNodeType,
RendererPluginAction,
RendererPluginEvent
BasePlugin,
defaultValue,
getSchemaTpl,
tipedLabel,
registerEditorPlugin
} from 'amis-editor-core';
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {registerEditorPlugin} from 'amis-editor-core';
import {BasePlugin, BaseEventContext} from 'amis-editor-core';
import {ValidatorTag} from '../../validator';
import {getEventControlConfig} from '../../renderer/event-control/helper';
import type {IFormStore, IFormItemStore} from 'amis-core';
import type {
EditorNodeType,
RendererPluginAction,
RendererPluginEvent,
BaseEventContext
} from 'amis-editor-core';
export class RangeControlPlugin extends BasePlugin {
static id = 'RangeControlPlugin';
@ -135,6 +141,19 @@ export class RangeControlPlugin extends BasePlugin {
panelTitle = '滑块';
panelJustify = true;
filterProps(props: Record<string, any>, node: EditorNodeType) {
if (
props.marks &&
isObject(props.marks) &&
props.marks.hasOwnProperty('$$id')
) {
delete props.marks.$$id;
}
return props;
}
panelBodyCreator = (context: BaseEventContext) => {
return getSchemaTpl('tabs', [
{

View File

@ -653,6 +653,132 @@ export class TableControlPlugin extends BasePlugin {
}
}
]
},
{
eventName: 'orderChange',
eventLabel: '行排序',
description: '手动拖拽行排序事件',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
movedItems: {
type: 'array',
title: '已排序记录'
}
}
}
}
}
]
},
{
eventName: 'rowClick',
eventLabel: '行单击',
description: '点击整行事件',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
item: {
type: 'object',
title: '当前行记录'
},
index: {
type: 'number',
title: '当前行索引'
}
}
}
}
}
]
},
{
eventName: 'rowDbClick',
eventLabel: '行双击',
description: '双击整行事件',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
item: {
type: 'object',
title: '当前行记录'
},
index: {
type: 'number',
title: '当前行索引'
}
}
}
}
}
]
},
{
eventName: 'rowMouseEnter',
eventLabel: '鼠标移入行事件',
description: '移入整行时触发',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
item: {
type: 'object',
title: '当前行记录'
},
index: {
type: 'number',
title: '当前行索引'
}
}
}
}
}
]
},
{
eventName: 'rowMouseLeave',
eventLabel: '鼠标移出行事件',
description: '移出整行时触发',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
item: {
type: 'object',
title: '当前行记录'
},
index: {
type: 'number',
title: '当前行索引'
}
}
}
}
}
]
}
];
@ -811,6 +937,11 @@ export class TableControlPlugin extends BasePlugin {
actionType: 'clear',
actionLabel: '清空',
description: '清空组件数据'
},
{
actionType: 'initDrag',
actionLabel: '开启排序',
description: '开启表格拖拽排序功能'
}
];
@ -1071,6 +1202,16 @@ export class TableControlPlugin extends BasePlugin {
name: 'affixHeader',
label: '是否固定表头',
pipeIn: defaultValue(false)
}),
getSchemaTpl('switch', {
name: 'showFooterAddBtn',
label: '展示底部新增按钮',
pipeIn: defaultValue(true)
}),
getSchemaTpl('switch', {
name: 'showTableAddBtn',
label: '展示操作列新增按钮',
pipeIn: defaultValue(true)
})
]
},
@ -1080,6 +1221,10 @@ export class TableControlPlugin extends BasePlugin {
getSchemaTpl('className', {
name: 'rowClassName',
label: '行样式'
}),
getSchemaTpl('className', {
name: 'toolbarClassName',
label: '工具栏'
})
]
})

View File

@ -109,21 +109,26 @@ export class ListControlPlugin extends BasePlugin {
}
];
subEditorVariable: Array<{label: string; children: any}> = [
{
label: '当前选项',
children: [
{
label: '选项名称',
value: 'label'
},
{
label: '选项值',
value: 'value'
}
]
}
];
getSubEditorVariable(schema: any): Array<{label: string; children: any}> {
let labelField = schema?.labelField || 'label';
let valueField = schema?.valueField || 'value';
return [
{
label: '当前选项',
children: [
{
label: '选项名称',
value: labelField
},
{
label: '选项值',
value: valueField
}
]
}
];
}
panelBodyCreator = (context: BaseEventContext) => {
return formItemControl(
@ -201,7 +206,7 @@ export class ListControlPlugin extends BasePlugin {
body: [
{
type: 'tpl',
tpl: `\${${this.getDisplayField(value)}}`,
tpl: `\${${this.getDisplayField(data)}}`,
wrapperComponent: '',
inline: true
}
@ -275,16 +280,7 @@ export class ListControlPlugin extends BasePlugin {
}
getDisplayField(data: any) {
if (
data.source ||
(data.map &&
Array.isArray(data.map) &&
data.map[0] &&
Object.keys(data.map[0]).length > 1)
) {
return data.labelField ?? 'label';
}
return 'label';
return data?.labelField ?? 'label';
}
editDetail(id: string, field: string) {

View File

@ -50,7 +50,7 @@ export class PickerControlPlugin extends BasePlugin {
value: 'B'
}
],
modalClassName: 'app-popover'
modalClassName: 'app-popover :AMISCSSWrapper'
};
previewSchema: any = {
type: 'form',

View File

@ -1,4 +1,9 @@
import {getI18nEnabled, registerEditorPlugin} from 'amis-editor-core';
import {
RendererPluginAction,
RendererPluginEvent,
getI18nEnabled,
registerEditorPlugin
} from 'amis-editor-core';
import {
ActiveEventContext,
BaseEventContext,
@ -8,6 +13,10 @@ import {
} from 'amis-editor-core';
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {mockValue} from 'amis-editor-core';
import {
getArgsWrapper,
getEventControlConfig
} from '../renderer/event-control/helper';
export class ImagePlugin extends BasePlugin {
static id = 'ImagePlugin';
@ -34,6 +43,112 @@ export class ImagePlugin extends BasePlugin {
value: mockValue({type: 'image'})
};
// 事件定义
events: RendererPluginEvent[] = [
{
eventName: 'click',
eventLabel: '点击',
description: '点击时触发',
defaultShow: true,
dataSchema: [
{
type: 'object',
properties: {
context: {
type: 'object',
title: '上下文',
properties: {
nativeEvent: {
type: 'object',
title: '鼠标事件对象'
}
}
}
}
}
]
},
{
eventName: 'mouseenter',
eventLabel: '鼠标移入',
description: '鼠标移入时触发',
dataSchema: [
{
type: 'object',
properties: {
context: {
type: 'object',
title: '上下文',
properties: {
nativeEvent: {
type: 'object',
title: '鼠标事件对象'
}
}
}
}
}
]
},
{
eventName: 'mouseleave',
eventLabel: '鼠标移出',
description: '鼠标移出时触发',
dataSchema: [
{
type: 'object',
properties: {
context: {
type: 'object',
title: '上下文',
properties: {
nativeEvent: {
type: 'object',
title: '鼠标事件对象'
}
}
}
}
}
]
}
];
// 动作定义
actions: RendererPluginAction[] = [
{
actionType: 'preview',
actionLabel: '预览',
description: '预览图片'
},
{
actionType: 'zoom',
actionLabel: '调整图片比例',
description: '将图片等比例放大或缩小',
schema: {
type: 'container',
body: [
getArgsWrapper([
getSchemaTpl('formulaControl', {
name: 'scale',
mode: 'horizontal',
variables: '${variables}',
horizontal: {
leftFixed: 4 // 需要设置下leftFixed否则这个字段的控件没有与其他字段的控件左对齐
},
label: tipedLabel(
'调整比例',
'定义每次放大或缩小图片的百分比大小正值为放大负值为缩小默认50'
),
value: 50,
size: 'lg'
})
])
]
}
}
];
panelTitle = '图片';
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => {
@ -63,7 +178,7 @@ export class ImagePlugin extends BasePlugin {
pipeIn: defaultValue('thumb'),
options: [
{
label: '缩图',
label: '缩图',
value: 'thumb'
},
{
@ -130,6 +245,24 @@ export class ImagePlugin extends BasePlugin {
getSchemaTpl('imageUrl', {
name: 'defaultImage',
label: tipedLabel('占位图', '无数据时显示的图片')
}),
getSchemaTpl('formulaControl', {
name: 'maxScale',
mode: 'horizontal',
label: tipedLabel(
'放大极限',
'定义动作调整图片大小的最大百分比默认200'
),
value: 200
}),
getSchemaTpl('formulaControl', {
name: 'minScale',
mode: 'horizontal',
label: tipedLabel(
'缩小极限',
'定义动作调整图片大小的最小百分比默认50'
),
value: 50
})
]
},
@ -245,6 +378,16 @@ export class ImagePlugin extends BasePlugin {
},
getSchemaTpl('theme:cssCode')
])
},
{
title: '事件',
className: 'p-none',
body: [
getSchemaTpl('eventControl', {
name: 'onEvent',
...getEventControlConfig(this.manager, context)
})
]
}
]);
};

View File

@ -20,7 +20,7 @@ export class ImagesPlugin extends BasePlugin {
pluginIcon = 'images-plugin';
scaffold = {
type: 'images',
imageGallaryClassName: 'app-popover'
imageGallaryClassName: 'app-popover :AMISCSSWrapper'
};
previewSchema = {
...this.scaffold,

View File

@ -29,6 +29,7 @@ export class NavPlugin extends BasePlugin {
scaffold = {
type: 'nav',
stacked: true,
popupClassName: 'app-popover :AMISCSSWrapper',
links: [
{
label: '页面1',

View File

@ -62,9 +62,13 @@ export class PaginationPlugin extends BasePlugin {
type: 'object',
title: '数据',
properties: {
value: {
type: 'string',
page: {
type: 'number',
title: '当前页码值'
},
perPage: {
type: 'number',
title: '每页显示的记录数'
}
},
description: '当前数据域,可以通过.字段名读取对应的值'

View File

@ -321,6 +321,7 @@ export class SwitchContainerPlugin extends LayoutBasePlugin {
name: 'items',
label: '状态列表',
addTip: '新增组件状态',
minLength: 1,
items: [
{
type: 'input-text',
@ -356,6 +357,10 @@ export class SwitchContainerPlugin extends LayoutBasePlugin {
title: '外观',
className: 'p-none',
body: getSchemaTpl('collapseGroup', [
getSchemaTpl('theme:base', {
collapsed: false,
extra: []
}),
{
title: '布局',
body: [
@ -460,7 +465,15 @@ export class SwitchContainerPlugin extends LayoutBasePlugin {
getSchemaTpl('layout:stickyPosition')
]
},
...getSchemaTpl('theme:common', {exclude: ['layout']})
{
title: '自定义样式',
body: [
{
type: 'theme-cssCode',
label: false
}
]
}
])
},
{

View File

@ -349,6 +349,32 @@ export class TablePlugin extends BasePlugin {
}
]
},
{
eventName: 'rowDbClick',
eventLabel: '行双击',
description: '双击整行事件',
dataSchema: [
{
type: 'object',
properties: {
data: {
type: 'object',
title: '数据',
properties: {
item: {
type: 'object',
title: '当前行记录'
},
index: {
type: 'number',
title: '当前行索引'
}
}
}
}
}
]
},
{
eventName: 'rowMouseEnter',
eventLabel: '鼠标移入行事件',

View File

@ -7,7 +7,7 @@ import {findDOMNode} from 'react-dom';
import cx from 'classnames';
import get from 'lodash/get';
import Sortable from 'sortablejs';
import {FormItem, Button, Icon, render as amisRender} from 'amis';
import {FormItem, Button, Icon, render as amisRender, toast} from 'amis';
import {autobind} from 'amis-editor-core';
import type {Option} from 'amis';
import {createObject, FormControlProps} from 'amis-core';
@ -30,7 +30,6 @@ export type SourceType = 'custom' | 'api' | 'apicenter' | 'variable';
export interface OptionControlState {
items: Array<PlainObject>;
api: SchemaApi;
labelField: string;
valueField: string;
}
@ -50,7 +49,6 @@ export default class ListItemControl extends React.Component<
this.state = {
items: this.transformOptions(props),
api: props.data.source,
labelField: props.data.labelField || 'title',
valueField: props.data.valueField
};
@ -173,6 +171,12 @@ export default class ListItemControl extends React.Component<
*/
handleDelete(index: number) {
const items = this.state.items.concat();
const minLength = this.props.minLength;
if (minLength > 0 && items.length <= minLength) {
toast.warning(`列表项数目不能少于${minLength}`);
return;
}
items.splice(index, 1);
this.setState({items}, () => this.onChange());

View File

@ -449,19 +449,29 @@ export class CRUDColumnControl extends React.Component<
size="sm"
className={cx('flex')}
/>
) : Array.isArray(options) && options.length > 0 ? (
) : (
<>
{this.renderHeader()}
<ul className={cx('ae-CRUDConfigControl-list')} ref={this.dragRef}>
{options.map((item, index) => {
return this.renderOption(item, index);
})}
</ul>
{Array.isArray(options) && options.length > 0 ? (
<ul
className={cx('ae-CRUDConfigControl-list')}
ref={this.dragRef}
>
{options.map((item, index) => {
return this.renderOption(item, index);
})}
</ul>
) : (
<ul
className={cx('ae-CRUDConfigControl-list')}
ref={this.dragRef}
>
<p className={cx(`ae-CRUDConfigControl-placeholder`)}>
</p>
</ul>
)}
</>
) : (
<ul className={cx('ae-CRUDConfigControl-list')} ref={this.dragRef}>
<p className={cx(`ae-CRUDConfigControl-placeholder`)}></p>
</ul>
)}
{showAddModal ? (

View File

@ -1244,8 +1244,6 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
'path',
'value',
'index',
'fromPage',
'fromApp',
'__valueInput',
'__comboType',
'__containerType'
@ -1283,29 +1281,34 @@ export const ACTION_TYPE_TREE = (manager: any): RendererPluginAction[] => {
supportComponents: 'byComponent',
schema: [
{
name: '__actionSubType',
type: 'radios',
label: '动作类型',
mode: 'horizontal',
options: [
{label: '组件变量', value: 'cmpt'},
{label: '页面变量', value: 'page'},
{label: '内存变量', value: 'app'}
],
value:
'${args.fromApp ? "app" : args.fromPage ? "page" : "cmpt"}',
onChange: (value: string, oldVal: any, data: any, form: any) => {
form.setValueByName('__valueInput', undefined);
form.setValueByName('args.value', undefined);
form.deleteValueByName('args.path');
form.deleteValueByName('args.fromApp');
form.deleteValueByName('args.fromPage');
if (value === 'page') {
form.setValueByName('args.fromPage', true);
} else if (value === 'app') {
form.setValueByName('args.fromApp', true);
}
children: ({render, data}: any) => {
const path = data?.args?.path || '';
return render('setValueType', {
name: '__actionSubType',
type: 'radios',
label: '动作类型',
mode: 'horizontal',
options: [
{label: '组件变量', value: 'cmpt'},
{label: '页面变量', value: 'page'},
{label: '内存变量', value: 'app'}
],
value: /^appVariables/.test(path) // 只需要初始化时更新value
? 'app'
: /^(__page|__query)/.test(path)
? 'page'
: 'cmpt',
onChange: (
value: string,
oldVal: any,
data: any,
form: any
) => {
form.setValueByName('__valueInput', undefined);
form.setValueByName('args.value', undefined);
form.deleteValueByName('args.path');
}
});
}
},
// 组件变量
@ -2349,6 +2352,12 @@ export const COMMON_ACTION_SCHEMA_MAP: {
},
confirm: {
descDetail: (info: any) => <div></div>
},
preview: {
descDetail: (info: any) => <div></div>
},
zoom: {
descDetail: (info: any) => <div></div>
}
};
@ -3212,7 +3221,7 @@ export const getEventControlConfig = (
showCloseButton: true,
showErrorMsg: true,
showLoading: true,
className: 'app-popover',
className: 'app-popover :AMISCSSWrapper',
actions: [
{
type: 'button',
@ -3239,7 +3248,7 @@ export const getEventControlConfig = (
inline: false
}
],
className: 'app-popover',
className: 'app-popover :AMISCSSWrapper',
actions: [
{
type: 'button',
@ -3376,9 +3385,7 @@ export const getEventControlConfig = (
/** 应用变量赋值 */
action.args = {
path: config.args.path,
value: config.args?.value ?? '',
fromPage: action.args?.fromPage,
fromApp: action.args?.fromApp
value: config.args?.value ?? ''
};
action.hasOwnProperty('componentId') && delete action.componentId;

View File

@ -148,7 +148,7 @@ export class TextareaFormulaControl extends React.Component<
constructor(props: TextareaFormulaControlProps) {
super(props);
this.state = {
value: '',
value: this.props.value || '',
variables: [],
formulaPickerOpen: false,
formulaPickerValue: '',

View File

@ -563,6 +563,18 @@ test('evalute:Math', () => {
expect(evaluate('${POW(2, infinity)}', data)).toBe(data.infinity);
});
test('evalute:UUID', () => {
function isUUIDv4(value: string) {
return /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value
);
}
expect(isUUIDv4(evaluate('${UUID()}', {}))).toBe(true);
expect(evaluate('${UUID()}', {}).length).toBe(36);
expect(evaluate('${UUID(8)}', {}).length).toBe(8);
});
test('evalute:namespace', () => {
localStorage.setItem('a', '1');
localStorage.setItem('b', '2');

View File

@ -591,6 +591,16 @@
返回a.json`。
### UUID
用法:`UUID(8)`
* `length:number` 生成的UUID字符串长度默认为32位
返回:`string` 生成的UUID字符串
生成UUID字符串
## 日期函数
### DATE

View File

@ -1022,6 +1022,23 @@ export const doc: {
},
namespace: '文本函数'
},
{
name: 'UUID',
description: '生成UUID字符串',
example: 'UUID(8)',
params: [
{
type: 'number',
name: 'length',
description: '生成的UUID字符串长度默认为32位'
}
],
returns: {
type: 'string',
description: '生成的UUID字符串'
},
namespace: '文本函数'
},
{
name: 'DATE',
description:

View File

@ -1514,6 +1514,21 @@ export class Evaluator {
return text.split(/[\\/]/).pop();
}
/**
* UUID字符串
*
* @param {number} length - UUID字符串长度32
* @example UUID()
* @example UUID(8)
* @namespace
*
* @returns {string} UUID字符串
*/
fnUUID(length: number = 36) {
const len = Math.min(Math.max(length, 0), 36);
return uuidv4().slice(0, len);
}
// 日期函数
/**
@ -2414,3 +2429,25 @@ export function createObject(
return obj;
}
export function createStr() {
return (
'00000000000000000' + (Math.random() * 0xffffffffffffffff).toString(16)
).slice(-16);
}
export function uuidv4() {
const a = createStr();
const b = createStr();
return (
a.slice(0, 8) +
'-' +
a.slice(8, 12) +
'-4' +
a.slice(13) +
'-a' +
b.slice(1, 4) +
'-' +
b.slice(4)
);
}

View File

@ -786,6 +786,7 @@
--transfer-base-header-paddingBottom: var(--sizes-size-5);
--transfer-base-header-paddingLeft: var(--sizes-size-8);
--transfer-base-header-paddingRight: var(--sizes-size-8);
--transfer-base-footer-border-color: var(--colors-neutral-line-8);
--transfer-base-body-paddingTop: var(--sizes-size-0);
--transfer-base-body-paddingBottom: var(--sizes-size-0);
--transfer-base-body-paddingLeft: var(--sizes-size-0);
@ -2052,6 +2053,7 @@
--Tabs-onActive-bg: var(--background);
--Tabs-onActive-borderColor: var(--borderColor);
--Tabs-onActive-color: var(--colors-neutral-text-2);
--Tabs-onError-color: var(--colors-error-5);
--Tabs-onDisabled-color: var(--colors-neutral-text-7);
--Tabs-onHover-borderColor: var(--colors-neutral-line-8);
--Tabs-add-icon-size: #{px2rem(15px)};
@ -4130,6 +4132,7 @@
var(--combo-vertical-right-border-color)
var(--combo-vertical-bottom-border-color)
var(--combo-vertical-left-border-color);
--Combo--vertical-item--onError-borderColor: var(--colors-error-5);
--Combo--vertical-item-borderRadius: var(
--combo-vertical-top-left-border-radius
)

View File

@ -274,22 +274,18 @@ $Table-strip-bg: transparent;
--DropDown-menu-borderColor: var(--borderColor);
--DropDown-menu-borderRadius: var(--borderRadius);
--DropDown-menu-borderWidth: var(--borderWidth);
--DropDown-menu-boxShadow: var(--shadows-shadow-normal);
--DropDown-menu-height: #{px2rem(34px)};
--DropDown-menu-boxShadow: var(--Form-select-outer-boxShadow);
--DropDown-menu-height: #{px2rem(32px)};
--DropDown-menu-minWidth: #{px2rem(160px)};
--DropDown-menu-paddingX: 0;
--DropDown-menu-paddingY: var(--gap-xs);
--DropDown-menuItem-onHover-bg: var(--ListMenu-item--onHover-bg);
--DropDown-menuItem-onHover-bg: var(--Form-select-menu-onHover-bg);
--DropDown-group-color: #848b99;
--DropDown-menuItem-color: #151a26;
--DropDown-menuItem-onHover-color: var(--colors-brand-5);
--DropDown-menuItem-onHover-color: var(--Form-select-menu-onHover-color);
--DropDown-menuItem-onActive-color: var(--colors-brand-5);
--DropDown-menuItem-onDisabled-color: #b4b6ba;
--DropDown-menuItem-paddingX: var(--gap-sm);
--DropDown-menuItem-paddingY: calc(
(var(--DropDown-menu-height) - var(--fontSizeBase) * var(--lineHeightBase)) /
2
);
--DropDown-menuItem-paddingX: var(--select-base-default-option-paddingRight);
--Fieldset-legend-bgColor: var(--colors-neutral-fill-11);

View File

@ -57,6 +57,7 @@
overflow-y: auto;
overflow-x: hidden;
max-height: px2rem(300px);
margin-top: px2rem(4px);
}
}
@ -67,10 +68,11 @@
&-menuItem,
&-menu > li {
padding: var(--DropDown-menuItem-paddingY) var(--DropDown-menuItem-paddingX);
padding: 0 var(--DropDown-menuItem-paddingX);
white-space: nowrap;
box-sizing: border-box;
height: var(--DropDown-menu-height);
line-height: var(--DropDown-menu-height);
vertical-align: middle;
user-select: none;
color: var(--DropDown-menuItem-color);
@ -95,7 +97,7 @@
&.#{$ns}DropDown-divider {
height: px2rem(1px);
margin: px2rem(9px) 0;
margin: px2rem(4px) 0;
overflow: hidden;
background: var(--DropDown-menu-borderColor);
padding: 0;

View File

@ -519,14 +519,13 @@
.#{$ns}Nav-Menu-submenu-arrow {
display: inline-block;
font-size: px2rem(9px);
vertical-align: middle;
text-transform: none;
text-rendering: auto;
line-height: px2rem(20px);
margin-left: px2rem(5px);
& > svg {
top: auto;
top: 0;
}
}

View File

@ -74,7 +74,7 @@
}
}
&-simple {
>ul >li {
> ul > li {
&:hover,
&:focus {
outline: none;
@ -94,8 +94,6 @@
}
}
&-next {
> span {
cursor: pointer;
@ -113,47 +111,47 @@
align-items: center;
height: var(--Pagination-height);
&-left {
color: var(--Pagination-light-color);
}
input {
min-width: px2rem(50px);
width: px2rem(50px);
height: var(--Pagination-height);
line-height: var(--Pagination-height);
// height: var(--Pagination-height);
border: none;
border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--borderRadius) 0 0 var(--borderRadius);
padding: var(--Pagination-padding);
margin-left: px2rem(8px);
text-align: center;
&-left {
color: var(--Pagination-light-color);
}
input {
min-width: px2rem(50px);
width: px2rem(50px);
height: var(--Pagination-height);
line-height: var(--Pagination-height);
// height: var(--Pagination-height);
border: none;
border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--borderRadius) 0 0 var(--borderRadius);
padding: var(--Pagination-padding);
margin-left: px2rem(8px);
text-align: center;
&:focus,
&:hover {
outline: none;
// border: var(--borderWidth) solid var(--primary);
border-color: var(--primary);
}
&:focus,
&:hover {
outline: none;
// border: var(--borderWidth) solid var(--primary);
border-color: var(--primary);
}
&-right {
display: inline-block;
width: px2rem(32px);
cursor: pointer;
text-align: center;
height: var(--Pagination-height);
line-height: var(--Pagination-height);
border: var(--borderWidth) solid var(--borderColor);
border-left: none;
border-radius: 0 var(--borderRadius) var(--borderRadius) 0;
font-size: var(--fontSizeSm);
&:hover {
color: var(--primary);
border-color: var(--primary);
border-left: var(--borderWidth) solid var(--primary);
margin-left: -1px;
}
}
&-right {
display: inline-block;
width: px2rem(32px);
cursor: pointer;
text-align: center;
height: var(--Pagination-height);
line-height: var(--Pagination-height);
border: var(--borderWidth) solid var(--borderColor);
border-left: none;
border-radius: 0 var(--borderRadius) var(--borderRadius) 0;
font-size: var(--fontSizeSm);
&:hover {
color: var(--primary);
border-color: var(--primary);
border-left: var(--borderWidth) solid var(--primary);
margin-left: -1px;
}
}
}
}
@ -161,7 +159,6 @@
line-height: px2rem(30px);
.#{$ns}Pagination-item {
margin-left: px2rem(8px);
&:nth-child(1) {
margin-left: 0;
}
@ -198,4 +195,4 @@
}
text-align: right;
}
}

View File

@ -1108,3 +1108,13 @@
}
}
}
.#{$ns}AutoFilterToolbar {
display: block;
text-align: right;
white-space: nowrap;
> .#{$ns}Button {
margin-top: 0;
}
}

View File

@ -18,10 +18,10 @@
.#{$ns}Tabs-addable {
display: flex;
margin-left: var(--Tabs-add-margin);
align-items: center;
justify-content: flex-start;
padding: var(--Tabs--line-addPadding);
margin-left: var(--Tabs-add-margin);
margin-bottom: px2rem(3px);
padding-bottom: px2rem(8px);
white-space: nowrap;
cursor: pointer;
@ -60,16 +60,19 @@
// }
&-arrow {
margin: var(--Tabs--line-addPadding);
width: 16px;
height: 100%;
display: flex;
align-items: center;
cursor: pointer;
box-sizing: content-box;
margin-bottom: px2rem(3px);
padding-bottom: px2rem(8px);
.iconfont {
font-size: var(--Remark-icon-fontSize);
.icon {
top: 0;
width: var(--Remark-icon-fontSize);
height: var(--Remark-icon-fontSize);
}
&:hover {
color: var(--icon-onHover-color);
@ -77,6 +80,9 @@
&--left {
padding-right: 16px;
svg {
transform: rotate(180deg);
}
}
&--right {
@ -242,6 +248,10 @@
border-color: var(--Tabs-onActive-borderColor);
border-bottom-color: transparent;
}
&.has-error > a:first-child {
color: var(--Tabs-onError-color) !important;
}
}
}
@ -672,6 +682,7 @@
.#{$ns}Tabs-addable {
padding: 0 var(--Tabs--tiled-add-gap);
margin-left: 0;
margin-bottom: 0;
white-space: nowrap;
border-style: solid;
border-color: var(--Tabs-borderColor);
@ -1112,6 +1123,7 @@
& > .#{$ns}Tabs-linksContainer {
> .#{$ns}Tabs-linksContainer-arrow {
margin-bottom: 0;
padding: 0;
}
> .#{$ns}Tabs-linksContainer-main > .#{$ns}Tabs-links {
@ -1224,21 +1236,6 @@
> .#{$ns}Tabs-linksContainer {
margin-bottom: calc(var(--Tabs-borderWidth) * -1);
&.#{$ns}Tabs-linksContainer--overflow
> .#{$ns}Tabs-linksContainer-main
> .#{$ns}Tabs-links
> .#{$ns}Tabs-link {
&:first-of-type {
border-left-width: 0;
border-top-left-radius: 0;
}
&:last-of-type {
border-right-width: 0;
border-top-right-radius: 0;
}
}
.#{$ns}Tabs-linksContainer-arrow {
width: var(--Tabs--strong-arrow-size);
margin-bottom: 0;
@ -1250,14 +1247,14 @@
&--left {
padding-right: 0;
border-right-width: 0;
border-top-left-radius: var(--Tabs-borderRadius);
margin-right: px2rem(8px);
}
&--right {
padding-left: 0;
border-left-width: 0;
border-top-right-radius: var(--Tabs-borderRadius);
border-top-left-radius: var(--Tabs-borderRadius);
margin-left: px2rem(8px);
}
}

View File

@ -258,6 +258,12 @@
var(--combo-vertical-paddingRight) var(--combo-vertical-paddingBottom)
var(--combo-vertical-paddingLeft);
position: relative;
&.has-error {
border-color: var(
--Combo--vertical-item--onError-borderColor
) !important; // 因为下面的规则权重更高 &:not(.is-disabled) > .#{$ns}Combo-items > .#{$ns}Combo-item:hover
}
}
> .#{$ns}Combo-items > .#{$ns}Combo-item {

View File

@ -257,12 +257,9 @@
}
.#{$ns}DateRangePicker-popover {
margin: px2rem(2px) 0 0;
&.#{$ns}PopOver--leftTopLeftBottom,
&.#{$ns}PopOver--rightTopRightBottom {
margin: px2rem(-2px) 0 0;
}
border: var(--Form-select-outer-borderWidth) solid
var(--Form-input-onFocused-borderColor);
box-shadow: var(--Form-select-outer-boxShadow);
}
.#{$ns}DateRangePicker-popup {

View File

@ -200,11 +200,9 @@
.#{$ns}DatePicker-popover {
margin: px2rem(2px) 0 0;
&.#{$ns}PopOver--leftTopLeftBottom,
&.#{$ns}PopOver--rightTopRightBottom {
margin: px2rem(-2px) 0 0;
}
border: var(--Form-select-outer-borderWidth) solid
var(--Form-input-onFocused-borderColor);
box-shadow: var(--Form-select-outer-boxShadow);
}
// 移动端输入框样式

View File

@ -132,6 +132,13 @@
max-width: 100%;
margin-top: -1px;
border-color: var(--Form-input-onFocused-borderColor);
&.#{$ns}PopOver--v-top {
margin-top: px2rem(4px);
}
&.#{$ns}PopOver--v-bottom {
margin-bottom: px2rem(4px);
}
}
.#{$ns}MapPicker {

View File

@ -4,7 +4,7 @@
.#{$ns}NestedSelect-menu {
padding-top: px2rem(4px);
padding-bottom: px2rem(4px);
box-shadow: 0 px2rem(2px) px2rem(8px) 0 rgba(7, 12, 20, 0.12);
box-shadow: var(--Form-select-outer-boxShadow);
}
}
@ -18,8 +18,6 @@
&-optionArrowRight {
display: inline-block;
padding-right: var(--Form-select-icon-rigin);
svg {
width: px2rem(10px);
height: px2rem(10px);
@ -29,6 +27,12 @@
}
}
&-optionArrowRight.is-disabled {
svg {
color: var(--text--muted-color);
}
}
&-menuOuter {
display: flex;
}
@ -56,19 +60,23 @@
max-height: px2rem(175px);
background: var(--Form-select-menu-bg);
color: var(--Form-select-menu-color);
border: var(--Form-select-outer-borderWidth) solid
var(--Form-input-onFocused-borderColor);
border-radius: var(--borderRadius);
box-shadow: var(--Form-select-outer-boxShadow);
overflow-y: auto;
overflow-x: hidden;
border: var(--Form-select-outer-borderWidth) solid
var(--Form-input-onFocused-borderColor);
&:not(:first-child) {
border-left: 0;
margin-left: px2rem(4px);
}
.#{$ns}NestedSelect-option {
position: relative;
padding-left: var(--gap-md);
padding: var(--select-base-default-option-paddingTop)
var(--select-base-default-option-paddingRight)
var(--select-base-default-option-paddingBottom)
var(--select-base-default-option-paddingLeft);
min-height: var(--select-base-default-option-line-height);
line-height: var(--select-base-default-option-line-height);
cursor: pointer;
@ -122,3 +130,8 @@
height: px2rem(340px);
}
}
.#{$ns}NestedSelect-popover {
border: none;
box-shadow: none;
}

View File

@ -584,26 +584,15 @@
}
.#{$ns}Select-popover {
margin-top: calc(var(--Form-select-outer-borderWidth) * -1);
background: var(--Form-select-menu-bg);
color: var(--Form-select-menu-color);
border: var(--Form-select-outer-borderWidth) solid
var(--Form-input-onFocused-borderColor);
box-shadow: var(--Form-select-outer-boxShadow);
border-top-left-radius: 0;
border-top-right-radius: 0;
// min-width: px2rem(100px);
// PopOver 上已经配置了这个要是配置就会覆盖所以先干掉好了
// z-index: 10;
&.#{$ns}PopOver--leftTopLeftBottom {
margin-top: calc(
(var(--Form-select-popoverGap) - var(--Form-select-outer-borderWidth)) *
-1
);
}
}
.#{$ns}SelectControl {

View File

@ -319,7 +319,10 @@
background: var(--Form-select-menu-bg);
color: var(--Form-select-menu-color);
border-radius: px2rem(2px);
box-shadow: var(--menu-box-shadow);
box-shadow: var(--Form-select-outer-boxShadow);
border: var(--Form-select-outer-borderWidth) solid
var(--Form-input-onFocused-borderColor);
padding: px2rem(4px) 0;
}
&-sugs {
@ -328,13 +331,11 @@
}
&-sugItem {
padding: calc(
(
var(--Form-selectOption-height) - var(--Form-input-lineHeight) *
var(--Form-input-fontSize) - #{px2rem(2px)}
) / 2
)
px2rem(12px);
padding: var(--select-base-default-option-paddingTop)
var(--select-base-default-option-paddingRight)
var(--select-base-default-option-paddingBottom)
var(--select-base-default-option-paddingLeft);
line-height: var(--select-base-default-option-line-height);
svg {
width: px2rem(16px);

View File

@ -39,6 +39,42 @@
}
}
&-footer {
border-top: 1px solid var(--transfer-base-footer-border-color);
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
padding: var(--gap-sm);
/* 底部空间较小让Pagination紧凑一些 */
&-pagination {
& > ul {
&.#{$ns}Pagination-item {
margin-left: 0;
}
& > li {
--Pagination-minWidth: #{px2rem(22px)};
--Pagination-height: #{px2rem(22px)};
--Pagination-padding: 0 #{px2rem(6px)};
}
}
.#{$ns}Pagination-perpage {
--select-base-default-paddingTop: 0;
--select-base-default-paddingBottom: 0;
--select-base-default-paddingLeft: #{px2rem(6px)};
--select-base-default-paddingRight: #{px2rem(6px)};
margin-left: 0;
.#{$ns}Select-valueWrap {
line-height: #{px2rem(22px)};
}
}
}
}
&-select,
&-result {
overflow: hidden;
@ -64,6 +100,10 @@
var(--transfer-base-top-right-border-radius)
var(--transfer-base-bottom-right-border-radius)
var(--transfer-base-bottom-left-border-radius);
&--pagination {
max-height: px2rem(475px);
}
}
&-select > &-selection,

View File

@ -55,6 +55,17 @@ export default class Markdown extends React.Component<MarkdownProps> {
async _render() {
const {content, options} = this.props;
this.dom.innerHTML = markdown(content, options);
// @ts-ignore 需要用户手动加载 katex
if (typeof renderMathInElement === 'function') {
// @ts-ignore
renderMathInElement(this.dom, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false}
]
});
}
}
render() {

View File

@ -94,7 +94,6 @@ export interface BasicPaginationProps {
popOverContainerSelector?: string;
onPageChange?: (page: number, perPage?: number, dir?: string) => void;
dispatchEvent?: Function;
}
export interface PaginationProps
extends BasicPaginationProps,
@ -141,20 +140,13 @@ export class Pagination extends React.Component<
}
async handlePageNumChange(page: number, perPage?: number, dir?: string) {
const {disabled, onPageChange, dispatchEvent} = this.props;
const {disabled, onPageChange} = this.props;
const _page = isNaN(Number(page)) || Number(page) < 1 ? 1 : page;
if (disabled) {
return;
}
const rendererEvent = await dispatchEvent?.(
'change',
resolveEventData(this.props, {_page})
);
if (rendererEvent?.prevented) {
return;
}
onPageChange?.(_page, perPage, dir);
}

View File

@ -383,11 +383,16 @@ export class Range extends React.Component<RangeItemProps, any> {
getStepValue(value: number, step: number) {
const surplus = value % step;
let result = 0;
let closeNum = Math.floor(value - (value % step));
// 余数 >= 步长一半 -> 向上取
// 余数 < 步长一半 -> 向下取
const _value = surplus >= step / 2 ? value : safeSub(value, step);
while (result <= _value) {
result = safeAdd(result, step);
if (step < 1 || result === 0 || result === closeNum) {
result = safeAdd(result, step);
} else {
result = closeNum;
}
}
return result;
}
@ -502,7 +507,7 @@ export class Range extends React.Component<RangeItemProps, any> {
// 遍历刻度标记masks 寻找距离当前节点最近的刻度标记 并记录差值
keys(marks).forEach((mKey: keyof MarksType) => {
const mNum = isString(mKey) ? parseInt(mKey, 10) : mKey;
if (mKey !== value) {
if (mKey !== value && !isNaN(mNum)) {
maxWidth = Math.min(Math.abs(curNum - mNum), maxWidth);
}
});

View File

@ -50,6 +50,7 @@ export interface TabProps extends ThemeProps {
tip?: string;
tab?: Schema;
className?: string;
tabClassName?: string;
activeKey?: string | number;
reload?: boolean;
mountOnEnter?: boolean;
@ -738,7 +739,7 @@ export class Tabs extends React.Component<TabsProps, any> {
disabled && 'Tabs-linksContainer-arrow--disabled'
)}
>
<i className={'iconfont icon-arrow-' + type} />
<Icon icon="right-arrow-bold" className="icon" />
</div>
) : null;
}

View File

@ -4,7 +4,6 @@ import includes from 'lodash/includes';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';
import unionWith from 'lodash/unionWith';
import {ThemeProps, themeable, findTree, differenceFromAll} from 'amis-core';
import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection';
import {Options, Option} from './Select';
@ -24,6 +23,7 @@ import {ItemRenderStates as ResultItemRenderStates} from './ResultList';
import ResultTableList from './ResultTableList';
import ResultTreeList from './ResultTreeList';
import {SpinnerExtraProps} from './Spinner';
import Pagination from './Pagination';
export type SelectMode =
| 'table'
@ -113,6 +113,44 @@ export interface TransferProps
checkAllLabel?: string;
/** 树形模式下,给 tree 的属性 */
onlyChildren?: boolean;
/** 分页模式下累积的选项值,用于右侧回显 */
accumulatedOptions?: Option[];
/** 分页配置 */
pagination?: {
/** 是否开启分页 */
enable: boolean;
/** 分页组件CSS类名 */
className?: string;
/**
* layout属性的顺序 total,perPage,pager,go
* @default 'pager'
*/
layout?: string | Array<string>;
/**
*
* @default [10, 20, 50, 100]
*/
perPageAvailable?: Array<number>;
/**
*
*
* @default 5
*/
maxButtons?: number;
page?: number;
perPage?: number;
total?: number;
popOverContainer?: any;
popOverContainerSelector?: string;
};
/** 切换分页事件 */
onPageChange?: (
page: number,
perPage?: number,
direction?: 'forward' | 'backward'
) => void;
}
export interface TransferState {
@ -549,10 +587,33 @@ export class Transfer<
{this.state.searchResult !== null
? this.renderSearchResult(props)
: this.renderOptions(props)}
{this.renderFooter()}
</>
);
}
renderFooter() {
const {classnames: cx, pagination, onPageChange} = this.props;
return pagination?.enable ? (
<div className={cx('Transfer-footer')}>
<Pagination
className={cx('Transfer-footer-pagination', pagination.className)}
activePage={pagination.page}
perPage={pagination.perPage}
total={pagination.total}
layout={pagination.layout}
maxButtons={pagination.maxButtons}
perPageAvailable={pagination.perPageAvailable}
popOverContainer={pagination.popOverContainer}
popOverContainerSelector={pagination.popOverContainerSelector}
onPageChange={onPageChange}
/>
</div>
) : null;
}
renderSearchResult(props: TransferProps) {
const {
searchResultMode,
@ -827,9 +888,10 @@ export class Transfer<
virtualThreshold,
itemHeight,
loadingConfig,
showInvalidMatch
showInvalidMatch,
pagination,
accumulatedOptions
} = this.props;
const {resultSelectMode, isTreeDeferLoad} = this.state;
const searchable = !isTreeDeferLoad && resultSearchable;
@ -840,7 +902,7 @@ export class Transfer<
ref={this.domResultRef}
classnames={cx}
columns={columns!}
options={options || []}
options={(pagination?.enable ? accumulatedOptions : options) || []}
value={value}
disabled={disabled}
option2value={option2value}
@ -862,7 +924,7 @@ export class Transfer<
loadingConfig={loadingConfig}
classnames={cx}
className={cx('Transfer-value')}
options={options}
options={(pagination?.enable ? accumulatedOptions : options) || []}
valueField={'value'}
value={value || []}
onChange={onChange!}
@ -915,7 +977,8 @@ export class Transfer<
selectMode = 'list',
translate: __,
valueField = 'value',
mobileUI
mobileUI,
pagination
} = this.props as any;
const {searchResult} = this.state;
@ -939,7 +1002,11 @@ export class Transfer<
<div
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')}
>
<div className={cx('Transfer-select')}>
<div
className={cx('Transfer-select', {
'Transfer-select--pagination': !!pagination?.enable
})}
>
{this.renderSelect(this.props)}
</div>
<div className={cx('Transfer-mid', {'is-mobile': mobileUI})}>
@ -949,7 +1016,12 @@ export class Transfer<
</div>
) : null}
</div>
<div className={cx('Transfer-result', {'is-mobile': mobileUI})}>
<div
className={cx('Transfer-result', {
'is-mobile': mobileUI,
'Transfer-select--pagination': !!pagination?.enable
})}
>
<div
className={cx(
'Transfer-title',

View File

@ -20,6 +20,7 @@
* 17. api
* 18. CRUD
* 19. fetchInitData silent
* 20. CRUD表头查询字段更新后严格比较场景
*/
import {
@ -1046,9 +1047,8 @@ test('17. should use the first array item in the response if provided', async ()
)
);
waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length).toBe(2);
});
await wait(200);
expect(container.querySelectorAll('tbody>tr').length).toBe(2);
});
describe('18. inner events', () => {
@ -1149,3 +1149,169 @@ test('19. fetchInitData silent true', async () => {
expect(notify).toBeCalledTimes(1);
});
});
test('20. CRUD filters contain fields that modification inspection should use strict mode', async () => {
let keyword;
const mockFetcher = jest.fn().mockImplementation((req) => {
/** mock.calls[0][0]拿不到filter里的参数先用闭包测试吧 */
keyword = req.data.version;
return Promise.resolve({
data: {
status: 0,
msg: 'ok',
data: {
count: 0,
items: []
}
}
})
});
const {container} = render(
amisRender(
{
type: 'page',
body: [
{
"type": "crud",
"name": "crud",
"syncLocation": false,
"api": {
"method": "post",
"url": "/api/mock/crud"
},
"filter": {
"body": [
{
"type": "select",
"name": "version",
"label": "version",
"clearable": true,
"options": [
{"label": "0", "value": 0},
{"label": "1", "value": 1},
{"label": "true", "value": true},
{"label": "false", "value": false},
{"label": "emptyString", "value": ''},
{"label": "stringZero", "value": '0'},
{"label": "stringOne", "value": '1'}
]
}
],
"actions": [
{
"type": "submit",
"label": "SubmitBtn",
"primary": true
}
]
},
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "version",
"label": "Engine version engine"
}
],
}
]
},
{},
makeEnv({fetcher: mockFetcher})
)
);
const select = container.querySelector('.cxd-Select')!;
const submitBtn = container.querySelector("button[type='submit']")!;
fireEvent.click(select);
await wait(200);
let options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[0]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual(0);
/** 从 0 -> false 查询成功 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[3]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual(false);
/** 从 false -> '' 查询成功 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[4]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual('');
/** 从 '' -> 0 查询成功 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[0]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual(0);
/** 切换到1 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[1]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual(1);
/** 从 1 -> true 查询成功 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[2]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual(true);
/** 从 true -> '1' 查询成功 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[6]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual('1');
/** 切换到false */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[3]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual(false);
/** 从 false -> '0' 查询成功 */
fireEvent.click(select);
await wait(200);
options = container.querySelectorAll('.cxd-Select-option-content');
fireEvent.click(options[5]);
await wait(200);
fireEvent.click(submitBtn);
await wait(200);
expect(keyword).toEqual('0');
}, 7000);

View File

@ -119,7 +119,7 @@ exports[`Renderer:static 1`] = `
</div>
`;
exports[`Renderer:static 2`] = `
exports[`Renderer:static2 1`] = `
<div>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"

View File

@ -2,12 +2,13 @@
* NestedSelect
*
* 01. maxTagLength
* 02. onlyLeaf
*/
import {render, cleanup, waitFor} from '@testing-library/react';
import {render, cleanup, waitFor, fireEvent} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv} from '../../helper';
import {makeEnv, wait} from '../../helper';
import {clearStoresCache} from '../../../src';
afterEach(() => {
@ -113,3 +114,107 @@ describe('Renderer:NestedSelect', () => {
expect(container).toMatchSnapshot();
});
});
describe.only('Renderer:NestedSelect with onlyLeaf', () => {
test('single selection', async () => {
const optionWithNoChild = 'OptionWithNoChild';
const optionWithChild = 'OptionWithChild';
const {container, queryByText} = await setupNestedSelect({
"onlyLeaf": true,
"options": [
{"label": "选项A", "value": "A"},
{"label": optionWithNoChild, "value": "B", "children": []},
{
"label": optionWithChild,
"value": "C",
"children": [
{"label": "选项c1", "value": "c1"},
{"label": "选项c2", "value": "c2"}
]
}
]
});
const trigger = container.querySelector('.cxd-ResultBox');
expect(trigger).toBeInTheDocument();
fireEvent.click(trigger!);
await wait(200);
const parentNum = container.querySelectorAll('.cxd-NestedSelect-optionArrowRight')?.length ?? 0;
expect(parentNum).toEqual(1);
let options = container.querySelectorAll('.cxd-NestedSelect-optionLabel');
expect(options.length).toEqual(3);
/** onlyLeaf开启后children为空数组的选项也可以选择 */
fireEvent.click(options[1]);
await wait(300);
expect(queryByText(optionWithNoChild)!).toBeInTheDocument();
fireEvent.click(trigger!);
await wait(200);
options = container.querySelectorAll('.cxd-NestedSelect-optionLabel');
fireEvent.click(options[2]);
await wait(300);
fireEvent.click(trigger!);
await wait(200);
expect(queryByText(optionWithNoChild)!).toBeInTheDocument();
/** onlyLeaf开启后children非空的选项无法选择 */
expect(queryByText(optionWithChild)).toBeNull();
});
test('single selection', async () => {
const optionWithNoChild = 'OptionWithNoChild';
const optionWithChild = 'OptionWithChild';
const {container, queryByText} = await setupNestedSelect({
"onlyLeaf": true,
"multiple": true,
"options": [
{"label": "选项A", "value": "A"},
{"label": optionWithNoChild, "value": "B", "children": []},
{
"label": optionWithChild,
"value": "C",
"children": [
{"label": "选项c1", "value": "c1"},
{"label": "选项c2", "value": "c2"}
]
}
]
});
const trigger = container.querySelector('.cxd-ResultBox');
expect(trigger).toBeInTheDocument();
fireEvent.click(trigger!);
await wait(200);
const parentNum = container.querySelectorAll('.cxd-NestedSelect-optionArrowRight')?.length ?? 0;
expect(parentNum).toEqual(1);
let options = container.querySelectorAll('.cxd-NestedSelect-optionLabel');
expect(options.length).toEqual(3);
/** onlyLeaf开启后children为空数组的选项也可以选择 */
fireEvent.click(options[1]);
await wait(300);
fireEvent.click(trigger!);
await wait(200);
expect(queryByText(optionWithNoChild)!).toBeInTheDocument();
fireEvent.click(trigger!);
await wait(200);
options = container.querySelectorAll('.cxd-NestedSelect-optionLabel');
fireEvent.click(options[2]);
await wait(300);
fireEvent.click(trigger!);
await wait(200);
expect(queryByText(optionWithNoChild)!).toBeInTheDocument();
/** onlyLeaf开启后children非空的选项无法选择 */
expect(queryByText(optionWithChild)).toBeNull();
});
});

View File

@ -66,7 +66,7 @@ test('Renderer:static', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer:static', async () => {
test('Renderer:static2', async () => {
const {container} = render(
amisRender(
{
@ -189,3 +189,39 @@ test('Renderer:staticOn', async () => {
const text = getByText('123');
expect(text).toBeInTheDocument();
});
test('Renderer:staticInColumn', async () => {
const {container, getByText} = render(
amisRender(
{
type: 'crud',
source: '${items}',
columns: [
{
type: 'input-text',
name: 'a',
label: 'a',
static: true,
quickEdit: {
type: 'input-text',
mode: 'inline'
}
}
],
submitText: null,
actions: []
},
{
data: {
items: [{a: '1'}]
}
},
makeEnv()
)
);
await wait(200);
expect(container.querySelector('input[name="a"]')).toBeInTheDocument();
expect((container.querySelector('input[name="a"]') as any).value).toBe('1');
});

View File

@ -1392,7 +1392,6 @@ test('Renderer:transfer search highlight', async () => {
});
test('Renderer:transfer tree search', async () => {
const onSubmit = jest.fn();
const {container, findByText, getByText} = render(
amisRender(
@ -1486,7 +1485,7 @@ test('Renderer:transfer tree search', async () => {
});
await(300);
const libai = getByText('李白');
expect(libai).not.toBeNull();
fireEvent.click(libai);
@ -1501,4 +1500,310 @@ test('Renderer:transfer tree search', async () => {
expect(onSubmit.mock.calls[0][0]).toEqual({
transfer: "caocao,libai"
});
});
});
test('Renderer:Transfer with pagination', async () => {
const mockData = [
{
"label": "Laura Lewis",
"value": "1"
},
{
"label": "David Gonzalez",
"value": "2"
},
{
"label": "Christopher Rodriguez",
"value": "3"
},
{
"label": "Sarah Young",
"value": "4"
},
{
"label": "James Jones",
"value": "5"
},
{
"label": "Larry Robinson",
"value": "6"
},
{
"label": "Christopher Perez",
"value": "7"
},
{
"label": "Sharon Davis",
"value": "8"
},
{
"label": "Kenneth Anderson",
"value": "9"
},
{
"label": "Deborah Lewis",
"value": "10"
},
{
"label": "Jennifer Lewis",
"value": "11"
},
{
"label": "Laura Miller",
"value": "12"
},
{
"label": "Larry Harris",
"value": "13"
},
{
"label": "Patricia Robinson",
"value": "14"
},
{
"label": "Mark Davis",
"value": "15"
},
{
"label": "Jessica Harris",
"value": "16"
},
{
"label": "Anna Brown",
"value": "17"
},
{
"label": "Lisa Young",
"value": "18"
},
{
"label": "Donna Williams",
"value": "19"
},
{
"label": "Shirley Davis",
"value": "20"
}
];
const fetcher = jest.fn().mockImplementation((api) => {
const perPage = 10; /** 锁死10个方便测试 */
const page = Number(api.query.page || 1);
return Promise.resolve({
data: {
status: 0,
msg: 'ok',
data: {
count: mockData.length,
page: page,
items: mockData.concat().splice((page - 1) * perPage, perPage)
}
}
});
});
const {container} = render(
amisRender(
{
"type": "form",
"debug": true,
"body": [
{
"label": "默认",
"type": "transfer",
"name": "transfer",
"joinValues": false,
"extractValue": false,
"source": "/api/mock2/options/transfer?page=${page}&perPage=${perPage}",
"pagination": {
"enable": true,
"layout": ["pager", "perpage", "total"],
"popOverContainerSelector": ".cxd-Panel--form"
},
"value": [
{"label": "Laura Lewis", "value": "1", id: 1},
{"label": "Christopher Rodriguez", "value": "3", id: 3},
{"label": "Laura Miller", "value": "12", id: 12},
{"label": "Patricia Robinson", "value": "14", id: 14}
]
}
]
}, {}, makeEnv({fetcher})));
await wait(500);
expect(container.querySelector('.cxd-Transfer-footer-pagination')).toBeInTheDocument();
const checkboxes = container.querySelectorAll('input[type=checkbox]')!;
expect(checkboxes.length).toEqual(11); /** 包括顶部全选 */
expect((checkboxes[1] as HTMLInputElement)?.checked).toEqual(true);
expect((checkboxes[2] as HTMLInputElement)?.checked).toEqual(false);
expect((checkboxes[3] as HTMLInputElement)?.checked).toEqual(true);
expect((checkboxes[4] as HTMLInputElement)?.checked).toEqual(false);
const nextBtn = container.querySelector('.cxd-Pagination-next')!;
fireEvent.click(nextBtn);
await wait(500);
const checkboxes2 = container.querySelectorAll('input[type=checkbox]')!;
expect(checkboxes2.length).toEqual(11);
expect((checkboxes2[1] as HTMLInputElement)?.checked).toEqual(false);
expect((checkboxes2[2] as HTMLInputElement)?.checked).toEqual(true);
expect((checkboxes2[3] as HTMLInputElement)?.checked).toEqual(false);
expect((checkboxes2[4] as HTMLInputElement)?.checked).toEqual(true);
})
test.only('Renderer:Transfer with pagination and data source from data scope', async () => {
const mockData = [
{
"label": "Laura Lewis",
"value": "1"
},
{
"label": "David Gonzalez",
"value": "2"
},
{
"label": "Christopher Rodriguez",
"value": "3"
},
{
"label": "Sarah Young",
"value": "4"
},
{
"label": "James Jones",
"value": "5"
},
{
"label": "Larry Robinson",
"value": "6"
},
{
"label": "Christopher Perez",
"value": "7"
},
{
"label": "Sharon Davis",
"value": "8"
},
{
"label": "Kenneth Anderson",
"value": "9"
},
{
"label": "Deborah Lewis",
"value": "10"
},
{
"label": "Jennifer Lewis",
"value": "11"
},
{
"label": "Laura Miller",
"value": "12"
},
{
"label": "Larry Harris",
"value": "13"
},
{
"label": "Patricia Robinson",
"value": "14"
},
{
"label": "Mark Davis",
"value": "15"
},
{
"label": "Jessica Harris",
"value": "16"
},
{
"label": "Anna Brown",
"value": "17"
},
{
"label": "Lisa Young",
"value": "18"
},
{
"label": "Donna Williams",
"value": "19"
},
{
"label": "Shirley Davis",
"value": "20"
}
];
const fetcher = jest.fn().mockImplementation((api) => {
return Promise.resolve({
data: {
status: 0,
msg: 'ok',
data: {
count: mockData.length,
items: mockData
}
}
});
});
const {container} = render(
amisRender(
{
"type": "form",
"debug": true,
"body": [
{
"type": "service",
"api": {
"url": "/api/mock2/options/loadDataOnce",
"method": "get",
"responseData": {
"transferOptions": "${items}"
}
},
body: [
{
"label": "默认",
"type": "transfer",
"name": "transfer",
"joinValues": false,
"extractValue": false,
"source": "${transferOptions}",
"pagination": {
"enable": true,
"layout": ["pager", "perpage", "total"],
"popOverContainerSelector": ".cxd-Panel--form"
},
"value": [
{"label": "Laura Lewis", "value": "1", id: 1},
{"label": "Christopher Rodriguez", "value": "3", id: 3},
{"label": "Laura Miller", "value": "12", id: 12},
{"label": "Patricia Robinson", "value": "14", id: 14}
]
}
]
}
]
}, {}, makeEnv({fetcher})));
await wait(500);
expect(container.querySelector('.cxd-Transfer-footer-pagination')).toBeInTheDocument();
const checkboxes = container.querySelectorAll('input[type=checkbox]')!;
expect(checkboxes.length).toEqual(11); /** 包括顶部全选 */
expect((checkboxes[1] as HTMLInputElement)?.checked).toEqual(true);
expect((checkboxes[2] as HTMLInputElement)?.checked).toEqual(false);
expect((checkboxes[3] as HTMLInputElement)?.checked).toEqual(true);
expect((checkboxes[4] as HTMLInputElement)?.checked).toEqual(false);
const nextBtn = container.querySelector('.cxd-Pagination-next')!;
fireEvent.click(nextBtn);
await wait(500);
const checkboxes2 = container.querySelectorAll('input[type=checkbox]')!;
expect(checkboxes2.length).toEqual(11);
expect((checkboxes2[1] as HTMLInputElement)?.checked).toEqual(false);
expect((checkboxes2[2] as HTMLInputElement)?.checked).toEqual(true);
expect((checkboxes2[3] as HTMLInputElement)?.checked).toEqual(false);
expect((checkboxes2[4] as HTMLInputElement)?.checked).toEqual(true);
})

View File

@ -12,6 +12,10 @@
* 9. href
* 10.
* 11. clickAction
* 12. click事件
* 13. mouseenter / mouseleave
* 14. preview
* 15. zoom & maxScale & minScale
*
* * Images
* images image 使
@ -20,7 +24,7 @@
* 2. enlargeAble & originalSrc & source & title & description
*/
import {fireEvent, render} from '@testing-library/react';
import {fireEvent, render, waitFor} from '@testing-library/react';
import '../../src';
import {render as amisRender} from '../../src';
import {makeEnv, wait} from '../helper';
@ -334,7 +338,228 @@ describe('Renderer:image', () => {
})
);
fireEvent.click(container.querySelector('.cxd-Image-thumbWrap')!);
expect(getByText('这是一个弹框')!).toBeInTheDocument();
await waitFor(() => {
expect(getByText('这是一个弹框')!).toBeInTheDocument();
});
});
test('image:click', async () => {
const notify = jest.fn();
const {container, getByText} = render(
amisRender(
{
type: 'image',
src: 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80',
class: 'cursor-pointer',
onEvent: {
click: {
actions: [
{
actionType: 'toast',
args: {
msgType: 'info',
msg: '派发点击事件'
}
}
]
},
mouseenter: {
actions: [
{
actionType: 'toast',
args: {
msgType: 'info',
msg: '派发鼠标移入事件'
}
}
]
},
mouseleave: {
actions: [
{
actionType: 'toast',
args: {
msgType: 'info',
msg: '派发鼠标移出事件'
}
}
]
}
}
},
{},
makeEnv({
notify,
session: 'image-test-action-1'
})
)
);
fireEvent.click(container.querySelector('.cxd-Image-thumbWrap')!);
await waitFor(() => {
expect(notify).toHaveBeenCalledWith('info', '派发点击事件', {
msg: '派发点击事件',
msgType: 'info'
});
});
fireEvent.mouseEnter(container.querySelector('.cxd-Image-thumbWrap')!);
await waitFor(() => {
expect(notify).toHaveBeenCalledWith('info', '派发鼠标移入事件', {
msg: '派发鼠标移入事件',
msgType: 'info'
});
});
fireEvent.mouseLeave(container.querySelector('.cxd-Image-thumbWrap')!);
await waitFor(() => {
expect(notify).toHaveBeenCalledWith('info', '派发鼠标移出事件', {
msg: '派发鼠标移出事件',
msgType: 'info'
});
});
});
test('image:preview 预览动作', async () => {
const {container, getByText, baseElement} = render(
amisRender({
type: 'page',
body: [
{
type: 'image',
id: 'previewImage',
src: 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80',
originalSrc:
'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg'
},
{
type: 'button',
label: '预览图片',
onEvent: {
click: {
actions: [
{
actionType: 'preview',
componentId: 'previewImage'
}
]
}
}
}
]
})
);
expect(container).toMatchSnapshot();
fireEvent.click(getByText('预览图片'));
expect(baseElement.querySelector('.cxd-ImageGallery')!).toBeInTheDocument();
expect(
baseElement.querySelector('.cxd-ImageGallery .cxd-ImageGallery-main img')!
).toHaveAttribute(
'src',
'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg'
);
});
test('image:zoom & maxScale & minScale 调整图片比例动作', async () => {
const {container, getByText, baseElement} = render(
amisRender({
type: 'page',
body: [
{
type: 'image',
id: 'zoomImage',
src: 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80',
originalSrc:
'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg',
maxScale: 200,
minScale: 20
},
{
type: 'button',
label: '放大图片',
onEvent: {
click: {
actions: [
{
actionType: 'zoom',
args: {
scale: 50
},
componentId: 'zoomImage'
}
]
}
}
},
{
type: 'button',
label: '缩小图片',
onEvent: {
click: {
actions: [
{
actionType: 'zoom',
args: {
scale: -50
},
componentId: 'zoomImage'
}
]
}
}
}
]
})
);
expect(container).toMatchSnapshot();
const imgIns = baseElement.querySelector('.cxd-ImageField--thumb')!;
expect(imgIns).toHaveStyle({
transform: 'scale(1)'
});
fireEvent.click(getByText('放大图片'));
await waitFor(() => {
expect(imgIns).toHaveStyle({
transform: 'scale(1.5)'
});
});
fireEvent.click(getByText('缩小图片'));
await waitFor(() => {
expect(imgIns).toHaveStyle({
transform: 'scale(1)'
});
});
fireEvent.click(getByText('放大图片'));
fireEvent.click(getByText('放大图片'));
await waitFor(() => {
expect(imgIns).toHaveStyle({
transform: 'scale(2)'
});
});
fireEvent.click(getByText('放大图片'));
await waitFor(() => {
expect(imgIns).toHaveStyle({
transform: 'scale(2)'
});
});
fireEvent.click(getByText('缩小图片'));
fireEvent.click(getByText('缩小图片'));
fireEvent.click(getByText('缩小图片'));
fireEvent.click(getByText('缩小图片'));
await waitFor(() => {
expect(imgIns).toHaveStyle({
transform: 'scale(0.2)'
});
});
fireEvent.click(getByText('缩小图片'));
await waitFor(() => {
expect(imgIns).toHaveStyle({
transform: 'scale(0.2)'
});
});
});
});

View File

@ -56,6 +56,7 @@ exports[`Renderer:image image as form item 1`] = `
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb no-border"
@ -103,6 +104,7 @@ exports[`Renderer:image image:basic 1`] = `
<div>
<div
class="cxd-ImageField cxd-ImageField--thumb show"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -138,6 +140,7 @@ exports[`Renderer:image image:enlargeAble & originalSrc & enlargeTitle & showToo
<div>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -178,6 +181,7 @@ exports[`Renderer:image image:enlargeAble & originalSrc & enlargeTitle & showToo
<div>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -231,6 +235,7 @@ exports[`Renderer:image image:href 1`] = `
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<a
class="cxd-Link"
@ -263,10 +268,62 @@ exports[`Renderer:image image:href 1`] = `
</div>
`;
exports[`Renderer:image image:preview 预览动作 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
role="page-body"
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
>
<div
class="cxd-Image-thumbWrap"
>
<div
class="cxd-Image-thumb cxd-Image-thumb--contain cxd-Image-thumb--1-1"
>
<img
class="cxd-Image-image"
src="https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80"
/>
</div>
</div>
</div>
</div>
<button
class="cxd-Button cxd-Button--default cxd-Button--size-default"
type="button"
>
<span>
预览图片
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:image image:title & imageCaption 1`] = `
<div>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -308,6 +365,7 @@ exports[`Renderer:image image:width & height 1`] = `
<div>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -330,6 +388,65 @@ exports[`Renderer:image image:width & height 1`] = `
</div>
`;
exports[`Renderer:image image:zoom & maxScale & minScale 调整图片比例动作 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
role="page-body"
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
>
<div
class="cxd-Image-thumbWrap"
>
<div
class="cxd-Image-thumb cxd-Image-thumb--contain cxd-Image-thumb--1-1"
>
<img
class="cxd-Image-image"
src="https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80"
/>
</div>
</div>
</div>
</div>
<button
class="cxd-Button cxd-Button--default cxd-Button--size-default"
type="button"
>
<span>
放大图片
</span>
</button>
<button
class="cxd-Button cxd-Button--default cxd-Button--size-default"
type="button"
>
<span>
缩小图片
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:images images:basic 1`] = `
<div>
<div

View File

@ -7090,6 +7090,7 @@ exports[`Renderer:table list 1`] = `
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -7331,6 +7332,7 @@ exports[`Renderer:table list 1`] = `
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"
@ -7692,6 +7694,7 @@ exports[`Renderer:table list 1`] = `
>
<div
class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
>
<div
class="cxd-Image cxd-Image--thumb"

View File

@ -1949,7 +1949,13 @@ export default class CRUD extends React.Component<CRUDProps, any> {
}
renderPagination(toolbar: SchemaNode) {
const {store, render, classnames: cx, alwaysShowPagination} = this.props;
const {
store,
render,
classnames: cx,
alwaysShowPagination,
perPageAvailable
} = this.props;
const {page, lastPage} = store;
if (
@ -1971,6 +1977,11 @@ export default class CRUD extends React.Component<CRUDProps, any> {
| 'showPerPage'
> = {};
// 下发 perPageAvailable
if (Array.isArray(perPageAvailable)) {
extraProps.perPageAvailable = perPageAvailable;
}
/** 优先级showPageInput显性配置 > (lastPage > 9) */
if (typeof toolbar !== 'string') {
Object.assign(extraProps, toolbar);
@ -2218,7 +2229,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
toolbar.align || (type === 'pagination' ? 'right' : 'left');
return (
<div
key={index}
key={toolbar.id || index}
className={cx(
'Crud-toolbar-item',
align ? `Crud-toolbar-item--${align}` : '',

View File

@ -11,7 +11,9 @@ import {
difference,
ucFirst,
autobind,
createObject
createObject,
CustomStyle,
setThemeClassName
} from 'amis-core';
import {
isPureVariable,
@ -923,7 +925,10 @@ export default class Cards extends React.Component<GridProps, object> {
translate: __,
loading = false,
loadingConfig,
env
env,
id,
wrapperCustomStyle,
themeCss
} = this.props;
this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了,已经渲染了就不重复渲染了。
@ -973,9 +978,15 @@ export default class Cards extends React.Component<GridProps, object> {
return (
<div
ref={this.bodyRef}
className={cx('Cards', className, {
'Cards--unsaved': !!store.modified || !!store.moved
})}
className={cx(
'Cards',
className,
{
'Cards--unsaved': !!store.modified || !!store.moved
},
setThemeClassName('baseControlClassName', id, themeCss),
setThemeClassName('wrapperCustomStyle', id, wrapperCustomStyle)
)}
style={buildStyle(style, data)}
>
{affixHeader ? (
@ -1007,6 +1018,20 @@ export default class Cards extends React.Component<GridProps, object> {
{footer}
<Spinner loadingConfig={loadingConfig} overlay show={loading} />
<CustomStyle
config={{
wrapperCustomStyle,
id,
themeCss,
classNames: [
{
key: 'baseControlClassName'
}
]
}}
env={env}
/>
</div>
);
}

View File

@ -1,5 +1,12 @@
import React from 'react';
import {Renderer, RendererProps, buildStyle, isPureVariable} from 'amis-core';
import {
CustomStyle,
Renderer,
RendererProps,
buildStyle,
isPureVariable,
setThemeClassName
} from 'amis-core';
import {Schema} from 'amis-core';
import {resolveVariable, resolveVariableAndFilter} from 'amis-core';
import {createObject, getPropValue, isObject} from 'amis-core';
@ -96,7 +103,11 @@ export default class Each extends React.Component<EachProps> {
indexKeyName,
placeholder,
classnames: cx,
translate: __
translate: __,
env,
id,
wrapperCustomStyle,
themeCss
} = this.props;
const value = getPropValue(this.props, props =>
@ -124,7 +135,14 @@ export default class Each extends React.Component<EachProps> {
}
return (
<div className={cx('Each', className)} style={buildStyle(style, data)}>
<div
className={cx(
'Each',
className,
setThemeClassName('baseControlClassName', id, themeCss)
)}
style={buildStyle(style, data)}
>
{Array.isArray(arr) && arr.length && items ? (
arr.map((item: any, index: number) => (
<EachItem
@ -144,6 +162,20 @@ export default class Each extends React.Component<EachProps> {
{render('placeholder', __(placeholder))}
</div>
)}
<CustomStyle
config={{
wrapperCustomStyle,
id,
themeCss,
classNames: [
{
key: 'baseControlClassName'
}
]
}}
env={env}
/>
</div>
);
}

View File

@ -8,7 +8,10 @@ import {
resolveEventData,
ApiObject,
FormHorizontal,
evalExpressionWithConditionBuilder
evalExpressionWithConditionBuilder,
IFormStore,
getVariable,
IFormItemStore
} from 'amis-core';
import {ActionObject, Api} from 'amis-core';
import {ComboStore, IComboStore} from 'amis-core';
@ -37,7 +40,11 @@ import {isEffectiveApi, str2AsyncFunction} from 'amis-core';
import {Alert2} from 'amis-ui';
import memoize from 'lodash/memoize';
import {Icon} from 'amis-ui';
import {isAlive} from 'mobx-state-tree';
import {
isAlive,
clone as cloneModel,
destroy as destroyModel
} from 'mobx-state-tree';
import {
FormBaseControlSchema,
SchemaApi,
@ -48,7 +55,6 @@ import {
import {ListenerAction} from 'amis-core';
import type {SchemaTokenizeableString} from '../../Schema';
import isPlainObject from 'lodash/isPlainObject';
import {isMobile} from 'amis-core';
export type ComboCondition = {
test: string;
@ -395,6 +401,7 @@ export default class ComboControl extends React.Component<ComboProps> {
this.dragTipRef = this.dragTipRef.bind(this);
this.flush = this.flush.bind(this);
this.handleComboTypeChange = this.handleComboTypeChange.bind(this);
this.handleSubFormValid = this.handleSubFormValid.bind(this);
this.defaultValue = {
...props.scaffold
};
@ -797,6 +804,11 @@ export default class ComboControl extends React.Component<ComboProps> {
);
}
handleSubFormValid(valid: boolean, {index}: any) {
const {store} = this.props;
store.setMemberValid(valid, index);
}
handleFormInit(values: any, {index}: any) {
const {
syncDefaultValue,
@ -806,9 +818,15 @@ export default class ComboControl extends React.Component<ComboProps> {
formInited,
onChange,
submitOnChange,
setPrinstineValue
setPrinstineValue,
formItem
} = this.props;
// 已经开始验证了,那么打开成员的时候,就要验证一下。
if (formItem?.validated) {
this.subForms[index]?.validate(true, false, false);
}
this.subFormDefaultValues.push({
index,
values,
@ -881,7 +899,13 @@ export default class ComboControl extends React.Component<ComboProps> {
}
validate(): any {
const {messages, nullable, translate: __} = this.props;
const {
messages,
nullable,
value: rawValue,
translate: __,
store
} = this.props;
const value = this.getValueAsArray();
const minLength = this.resolveVariableProps(this.props, 'minLength');
const maxLength = this.resolveVariableProps(this.props, 'maxLength');
@ -896,18 +920,62 @@ export default class ComboControl extends React.Component<ComboProps> {
(messages && messages.maxLengthValidateFailed) || 'Combo.maxLength',
{maxLength}
);
} else if (this.subForms.length && (!nullable || value)) {
return Promise.all(this.subForms.map(item => item.validate())).then(
values => {
if (~values.indexOf(false)) {
return __(
(messages && messages.validateFailed) || 'validateFailed'
);
}
} else if (nullable && !rawValue) {
return; // 不校验
} else if (value.length) {
return Promise.all(
value.map(async (values: any, index: number) => {
const subForm = this.subForms[index];
if (subForm) {
return subForm.validate(true, false, false);
} else {
// 那些还没有渲染出来的数据
// 因为有可能存在分页,有可能存在懒加载,所以没办法直接用 subForm 去校验了
const subForm = this.subForms[Object.keys(this.subForms)[0] as any];
if (subForm) {
const form: IFormStore = subForm.props.store;
let valid = false;
for (let formitem of form.items) {
const cloned: IFormItemStore = cloneModel(formitem);
let value: any = getVariable(values, formitem.name, false);
return;
if (formitem.extraName) {
value = [
getVariable(values, formitem.name, false),
getVariable(values, formitem.extraName, false)
];
}
cloned.changeTmpValue(value, 'dataChanged');
valid = await cloned.validate(values);
destroyModel(cloned);
if (valid === false) {
break;
}
}
store.setMemberValid(valid, index);
return valid;
}
}
})
).then(values => {
if (~values.indexOf(false)) {
return __((messages && messages.validateFailed) || 'validateFailed');
}
);
return;
});
} else if (this.subForms.length) {
return Promise.all(
this.subForms.map(item => item.validate(true, false, false))
).then(values => {
if (~values.indexOf(false)) {
return __((messages && messages.validateFailed) || 'validateFailed');
}
return;
});
}
}
@ -1253,6 +1321,12 @@ export default class ComboControl extends React.Component<ComboProps> {
// 不能按需渲染,因为 unique 会失效。
mountOnEnter={!hasUnique}
unmountOnExit={false}
className={
store.memberValidMap[index] === false ? 'has-error' : ''
}
tabClassName={
store.memberValidMap[index] === false ? 'has-error' : ''
}
>
{condition && typeSwitchable !== false ? (
<div className={cx('Combo-itemTag')}>
@ -1485,7 +1559,8 @@ export default class ComboControl extends React.Component<ComboProps> {
itemClassName,
itemsWrapperClassName,
static: isStatic,
mobileUI
mobileUI,
store
} = this.props;
let items = this.props.items;
@ -1543,7 +1618,11 @@ export default class ComboControl extends React.Component<ComboProps> {
return (
<div
className={cx(`Combo-item`, itemClassName)}
className={cx(
`Combo-item`,
itemClassName,
store.memberValidMap[index] === false ? 'has-error' : ''
)}
key={this.keys[index]}
>
{!isStatic && !disabled && draggable && thelist.length > 1 ? (
@ -1622,7 +1701,8 @@ export default class ComboControl extends React.Component<ComboProps> {
nullable,
translate: __,
itemClassName,
mobileUI
mobileUI,
store
} = this.props;
let items = this.props.items;
@ -1646,7 +1726,13 @@ export default class ComboControl extends React.Component<ComboProps> {
disabled ? 'is-disabled' : ''
)}
>
<div className={cx(`Combo-item`, itemClassName)}>
<div
className={cx(
`Combo-item`,
itemClassName,
store.memberValidMap[0] === false ? 'has-error' : ''
)}
>
{condition && typeSwitchable !== false ? (
<div className={cx('Combo-itemTag')}>
<label>{__('Combo.type')}</label>
@ -1715,11 +1801,13 @@ export default class ComboControl extends React.Component<ComboProps> {
className: cx(`Combo-form`, formClassName)
},
{
index: 0,
disabled: disabled,
static: isStatic,
data,
onChange: this.handleSingleFormChange,
ref: this.makeFormRef(0),
onValidChange: this.handleSubFormValid,
onInit: this.handleSingleFormInit,
canAccessSuperData,
formStore: undefined,
@ -1749,6 +1837,7 @@ export default class ComboControl extends React.Component<ComboProps> {
onAction: this.handleAction,
onRadioChange: this.handleRadioChange,
ref: this.makeFormRef(index),
onValidChange: this.handleSubFormValid,
canAccessSuperData,
lazyChange: changeImmediately ? false : true,
formLazyChange: false,

View File

@ -14,7 +14,8 @@ import {
ActionObject,
isMobile,
isPureVariable,
resolveVariableAndFilter
resolveVariableAndFilter,
isNumeric
} from 'amis-core';
import {Range as InputRange, NumberInput, Icon} from 'amis-ui';
import {FormBaseControlSchema, SchemaObject} from '../../Schema';
@ -703,6 +704,11 @@ export default class RangeControl extends React.PureComponent<
renderMarks &&
(renderMarks[key] = render(region, item as SchemaObject));
}
/** 过滤掉不合法的值(合法的值是数字 & 百分数) */
if (renderMarks && !isNumeric(key.replace(/%$/, ''))) {
delete renderMarks[key];
}
});
return (

View File

@ -299,6 +299,7 @@ export default class FormTable extends React.Component<TableProps, TableState> {
subFormItems: any = {};
rowPrinstine: Array<any> = [];
editting: any = {};
table: any;
constructor(props: TableProps) {
super(props);
@ -587,6 +588,10 @@ export default class FormTable extends React.Component<TableProps, TableState> {
);
return;
} else if (actionType === 'initDrag') {
const tableStore = this.table?.props?.store;
tableStore?.stopDragging();
tableStore?.toggleDragging();
}
return onAction && onAction(action, ctx, ...rest);
}
@ -1550,6 +1555,7 @@ export default class FormTable extends React.Component<TableProps, TableState> {
while (ref && ref.getWrappedInstance) {
ref = ref.getWrappedInstance();
}
this.table = ref;
}
computedAddBtnDisabled() {
@ -1586,7 +1592,8 @@ export default class FormTable extends React.Component<TableProps, TableState> {
static: isStatic,
showFooterAddBtn,
footerAddBtn,
toolbarClassName
toolbarClassName,
onEvent
} = this.props;
const maxLength = this.resolveVariableProps(this.props, 'maxLength');
@ -1621,7 +1628,8 @@ export default class FormTable extends React.Component<TableProps, TableState> {
prefixRow,
affixRow,
autoFillHeight,
tableContentClassName
tableContentClassName,
onEvent
},
{
ref: this.tableRef.bind(this),

View File

@ -142,17 +142,15 @@ export class LocationControl extends React.Component<LocationControlProps> {
@supportStatic()
render() {
const {style} = this.props;
const {style, env} = this.props;
const ak = filter(this.props.ak, this.props.data) || env.locationPickerAK!;
return (
<div
className={this.props.classnames('LocationControl', {
'is-mobile': isMobile()
})}
>
<LocationPicker
{...this.props}
ak={filter(this.props.ak, this.props.data)}
/>
<LocationPicker {...this.props} ak={ak} />
</div>
);
}

View File

@ -172,6 +172,11 @@ export default class NestedSelectControl extends React.Component<
return !!rendererEvent?.prevented;
}
/** 是否为父节点 */
isParentNode(option: Option) {
return Array.isArray(option.children) && option.children.length > 0;
}
@autobind
handleOutClick(e: React.MouseEvent<any>) {
const {options} = this.props;
@ -295,7 +300,7 @@ export default class NestedSelectControl extends React.Component<
return;
}
if (onlyLeaf && option.children) {
if (onlyLeaf && this.isParentNode(option)) {
return;
}
@ -327,7 +332,7 @@ export default class NestedSelectControl extends React.Component<
let valueField = this.props.valueField || 'value';
if (onlyLeaf && !Array.isArray(option) && option.children) {
if (onlyLeaf && !Array.isArray(option) && this.isParentNode(option)) {
return;
}
@ -431,6 +436,7 @@ export default class NestedSelectControl extends React.Component<
allChecked(options: Options): boolean {
const {selectedOptions, withChildren, onlyChildren} = this.props;
return options.every(option => {
if ((withChildren || onlyChildren) && option.children) {
return this.allChecked(option.children);
@ -683,8 +689,8 @@ export default class NestedSelectControl extends React.Component<
if (
!selfChecked &&
onlyChildren &&
option.children &&
this.allChecked(option.children)
this.isParentNode(option) &&
this.allChecked(option.children!)
) {
selfChecked = true;
}
@ -728,7 +734,11 @@ export default class NestedSelectControl extends React.Component<
</div>
{option.children && option.children.length ? (
<div className={cx('NestedSelect-optionArrowRight')}>
<div
className={cx('NestedSelect-optionArrowRight', {
'is-disabled': nodeDisabled
})}
>
<Icon icon="right-arrow-bold" className="icon" />
</div>
) : null}
@ -799,8 +809,8 @@ export default class NestedSelectControl extends React.Component<
if (
!isChecked &&
onlyChildren &&
option.children &&
this.allChecked(option.children)
this.isParentNode(option) &&
this.allChecked(option.children!)
) {
isChecked = true;
}

View File

@ -1,17 +1,17 @@
import React from 'react';
import find from 'lodash/find';
import pick from 'lodash/pick';
import {isAlive} from 'mobx-state-tree';
import {matchSorter} from 'match-sorter';
import {
OptionsControlProps,
OptionsControl,
FormOptionsControl,
resolveEventData,
str2function,
getOptionValueBindField
} from 'amis-core';
import {SpinnerExtraProps, Transfer} from 'amis-ui';
import type {Option} from 'amis-core';
import {
getOptionValueBindField,
isEffectiveApi,
isPureVariable,
resolveVariableAndFilter,
autobind,
filterTree,
string2regExp,
@ -20,18 +20,25 @@ import {
findTreeIndex,
getTree,
spliceTree,
mapTree
mapTree,
optionValueCompare,
resolveVariable,
ActionObject,
toNumber
} from 'amis-core';
import {Spinner} from 'amis-ui';
import {optionValueCompare} from 'amis-core';
import {resolveVariable} from 'amis-core';
import {FormOptionsSchema, SchemaApi, SchemaObject} from '../../Schema';
import {Selection as BaseSelection} from 'amis-ui';
import {ResultList} from 'amis-ui';
import {ActionObject, toNumber} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {SpinnerExtraProps, Transfer, Spinner, ResultList} from 'amis-ui';
import {
FormOptionsSchema,
SchemaApi,
SchemaObject,
SchemaExpression,
SchemaClassName
} from '../../Schema';
import {supportStatic} from './StaticHoc';
import {matchSorter} from 'match-sorter';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import type {Option} from 'amis-core';
import type {PaginationSchema} from '../Pagination';
/**
* Transfer
@ -161,6 +168,22 @@ export interface TransferControlSchema
*
*/
onlyChildren?: boolean;
/**
* selectMode为默认和table才会生效
* @since 3.6.0
*/
pagination?: {
/** 是否左侧选项分页,默认不开启 */
enable: SchemaExpression;
/** 分页组件CSS类名 */
className?: SchemaClassName;
/** 是否开启前端分页 */
loadDataOnce?: boolean;
} & Pick<
PaginationSchema,
'layout' | 'maxButtons' | 'perPageAvailable' | 'popOverContainerSelector'
>;
}
export interface BaseTransferProps
@ -427,6 +450,30 @@ export class BaseTransferRenderer<
return regexp.test(labelTest) || regexp.test(valueTest);
}
@autobind
handlePageChange(
page: number,
perPage?: number,
direction?: 'forward' | 'backward'
) {
const {source, data, formItem, onChange} = this.props;
const ctx = createObject(data, {
page: page ?? 1,
perPage: perPage ?? 10,
...(direction ? {pageDir: direction} : {})
});
if (!formItem || !isAlive(formItem)) {
return;
}
if (isPureVariable(source)) {
formItem.loadOptionsFromDataScope(source, ctx, onChange);
} else if (isEffectiveApi(source, ctx)) {
formItem.loadOptions(source, ctx, undefined, false, onChange, false);
}
}
@autobind
optionItemRender(option: Option, states: ItemRenderStates) {
const {menuTpl, render, data} = this.props;
@ -544,7 +591,11 @@ export class BaseTransferRenderer<
showInvalidMatch,
onlyChildren,
mobileUI,
noResultsText
noResultsText,
pagination,
formItem,
env,
popOverContainer
} = this.props;
// 目前 LeftOptions 没有接口可以动态加载
@ -570,6 +621,7 @@ export class BaseTransferRenderer<
onlyChildren={onlyChildren}
value={selectedOptions}
options={options}
accumulatedOptions={formItem?.accumulatedOptions ?? []}
disabled={disabled}
onChange={this.handleChange}
option2value={this.option2value}
@ -607,6 +659,28 @@ export class BaseTransferRenderer<
showInvalidMatch={showInvalidMatch}
mobileUI={mobileUI}
noResultsText={noResultsText}
pagination={{
...pick(pagination, [
'className',
'layout',
'perPageAvailable',
'popOverContainerSelector'
]),
enable:
!!formItem?.enableSourcePagination &&
(!selectMode ||
selectMode === 'list' ||
selectMode === 'table') &&
options.length > 0,
maxButtons: Number.isInteger(pagination?.maxButtons)
? pagination.maxButtons
: 5,
page: formItem?.sourcePageNum,
perPage: formItem?.sourcePerPageNum,
total: formItem?.sourceTotalNum,
popOverContainer: popOverContainer ?? env?.getModalContainer
}}
onPageChange={this.handlePageChange}
/>
<Spinner

View File

@ -3,7 +3,13 @@ import {
Renderer,
RendererProps,
CustomStyle,
setThemeClassName
setThemeClassName,
ActionObject,
IScopedContext,
ScopedContext,
createObject,
resolveVariableAndFilter,
isPureVariable
} from 'amis-core';
import {filter} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core';
@ -398,6 +404,8 @@ export interface ImageFieldProps extends RendererProps {
enlargeWithGallary?: boolean;
showToolbar?: boolean;
toolbarActions?: ImageAction[];
maxScale?: number;
minScale?: number;
onImageEnlarge?: (
info: {
src: string;
@ -414,9 +422,21 @@ export interface ImageFieldProps extends RendererProps {
target: any
) => void;
imageGallaryClassName?: string;
onClick?:
| ((e: React.MouseEvent<any>, props: any) => void)
| string
| Function
| null;
}
export class ImageField extends React.Component<ImageFieldProps, object> {
interface ImageFieldState {
scale: number; // 放大倍率
}
export class ImageField extends React.Component<
ImageFieldProps,
ImageFieldState
> {
static defaultProps: Pick<
ImageFieldProps,
'defaultImage' | 'thumbMode' | 'thumbRatio'
@ -426,6 +446,10 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
thumbRatio: '1:1'
};
state: ImageFieldState = {
scale: 1
};
@autobind
handleEnlarge({
src,
@ -468,13 +492,73 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
}
@autobind
handleClick(e: React.MouseEvent<HTMLElement>) {
async handleClick(e: React.MouseEvent<HTMLElement>) {
const {dispatchEvent, data} = this.props;
const clickAction = this.props.clickAction;
const rendererEvent = await dispatchEvent(
e,
createObject(data, {
nativeEvent: e
})
);
if (rendererEvent?.prevented) {
return;
}
if (clickAction) {
handleAction(e, clickAction, this.props);
}
}
@autobind
handleMouseEnter(e: React.MouseEvent<any>) {
const {dispatchEvent, data} = this.props;
dispatchEvent(e, data);
}
@autobind
handleMouseLeave(e: React.MouseEvent<any>) {
const {dispatchEvent, data} = this.props;
dispatchEvent(e, data);
}
handleSelfAction(actionType: string, action: ActionObject) {
let {data, maxScale = 200, minScale = 50} = this.props;
let {scale = 50} = action.args;
if (actionType === 'zoom') {
if (isPureVariable(maxScale)) {
maxScale = isNaN(
resolveVariableAndFilter(maxScale, createObject(action.data, data))
)
? 200
: resolveVariableAndFilter(maxScale, createObject(action.data, data));
}
if (isPureVariable(minScale)) {
minScale = isNaN(
resolveVariableAndFilter(minScale, createObject(action.data, data))
)
? 50
: resolveVariableAndFilter(minScale, createObject(action.data, data));
}
if (scale >= 0) {
this.setState({
scale:
this.state.scale + scale / 100 < maxScale / 100
? this.state.scale + scale / 100
: maxScale / 100
});
} else {
this.setState({
scale:
this.state.scale + scale / 100 > minScale / 100
? this.state.scale + scale / 100
: minScale / 100
});
}
}
}
render() {
const {
className,
@ -510,6 +594,7 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
defaultImage && !value
? filter(defaultImage, data, '| raw')
: imagePlaceholder;
return (
<div
className={cx(
@ -520,8 +605,10 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
className,
setThemeClassName('wrapperCustomStyle', id, wrapperCustomStyle)
)}
style={style}
style={{...style, transform: `scale(${this.state.scale})`}}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{value || (!value && !placeholder) ? (
<ThemedImageThumb
@ -537,7 +624,7 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
thumbMode={thumbMode}
thumbRatio={thumbRatio}
originalSrc={filter(originalSrc, data, '| raw') ?? value}
enlargeAble={enlargeAble && value !== defaultValue}
enlargeAble={enlargeAble && value && value !== defaultValue}
onEnlarge={this.handleEnlarge}
imageMode={imageMode}
imageControlClassName={setThemeClassName(
@ -597,4 +684,26 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
@Renderer({
type: 'image'
})
export class ImageFieldRenderer extends ImageField {}
export class ImageFieldRenderer extends ImageField {
static contextType = ScopedContext;
constructor(props: ImageFieldProps, context: IScopedContext) {
super(props);
const scoped = context;
scoped.registerComponent(this);
}
componentWillUnmount() {
const scoped = this.context as IScopedContext;
scoped.unRegisterComponent(this);
}
doAction(action: ActionObject) {
const actionType = action?.actionType as string;
if (actionType === 'preview') {
this.handleEnlarge(this.props as ImageThumbProps);
} else {
this.handleSelfAction(actionType, action);
}
}
}

View File

@ -2,7 +2,10 @@ import React from 'react';
import {
Renderer,
RendererProps,
autobind,
createObject,
isPureVariable,
resolveEventData,
resolveVariableAndFilter
} from 'amis-core';
import {BaseSchema} from '../Schema';
@ -103,11 +106,31 @@ export default class Pagination extends React.Component<PaginationProps> {
return result ?? defaultValue;
}
@autobind
async onPageChange(page: number, perPage?: number, dir?: string) {
const {onPageChange, dispatchEvent, data} = this.props;
const rendererEvent = await dispatchEvent?.(
'change',
createObject(data, {
page: page,
perPage: perPage
})
);
if (rendererEvent?.prevented) {
return;
}
onPageChange?.(page, perPage, dir);
}
render() {
const {maxButtons, activePage, total, perPage} = this.props;
return (
<BasicPagination
{...this.props}
onPageChange={this.onPageChange}
maxButtons={this.formatNumber(maxButtons)}
activePage={this.formatNumber(activePage)}
total={this.formatNumber(total)}

View File

@ -512,7 +512,7 @@ export const HocQuickEdit =
>
{render('quick-edit-form', this.buildSchema(), {
value: undefined,
static: false,
defaultStatic: false,
onSubmit: this.handleSubmit,
onAction: this.handleAction,
onChange: null,
@ -577,7 +577,8 @@ export const HocQuickEdit =
mode: 'normal',
value: value ?? '',
onChange: this.handleFormItemChange,
ref: this.formItemRef
ref: this.formItemRef,
defaultStatic: false
});
}
@ -591,7 +592,8 @@ export const HocQuickEdit =
onChange: this.handleChange,
formLazyChange: false,
canAccessSuperData,
disabled
disabled,
defaultStatic: false
});
}

View File

@ -80,7 +80,7 @@ export default class SwitchContainer extends React.Component<
componentDidUpdate(preProps: SwitchContainerProps) {
const items = this.props.items || [];
if (this.state.activeIndex >= 0 && !items[this.state.activeIndex]) {
if (this.state.activeIndex > 0 && !items[this.state.activeIndex]) {
this.setState({
activeIndex: 0
});

View File

@ -71,7 +71,7 @@ export function AutoFilterForm({
})
);
let showExpander = searchableColumns.length >= columnsNum;
let showExpander = activedSearchableColumns.length >= columnsNum;
// todo 以后做动画
if (!searchFormExpanded && body.length) {
@ -100,7 +100,7 @@ export function AutoFilterForm({
}
lastGroup.body.push({
type: 'container',
className: 'ButtonToolbar text-right block',
className: 'AutoFilterToolbar',
wrapperBody: false,
body: [
{
@ -114,32 +114,31 @@ export function AutoFilterForm({
visible: showBtnToolbar,
buttons: searchableColumns.map(column => {
return {
type: 'checkbox',
label: false,
className: cx('Table-searchableForm-checkbox'),
inputClassName: cx('Table-searchableForm-checkbox-inner'),
name: `${
column.searchable.strategy === 'jsonql' ? '' : '__search_'
}${column.searchable?.name ?? column.name}`,
option: column.searchable?.label ?? column.label,
/**
* syncLocation开启后Query中二次同步到数据域中(boolean)
* eg:
* true ==> "true"
* false ==> "false"
*
*/
trueValue: '1',
falseValue: '0',
value: !!column.enableSearch ? '1' : '0',
badge: {
offset: [-10, 5],
visibleOn: `${
column.toggable && !column.toggled && column.enableSearch
}`
},
onChange: (value: '1' | '0') =>
onItemToggleExpanded?.(column, value === '1' ? true : false)
children: ({render}: any) =>
render(
`column-search-toggler-${column.id}`,
{
type: 'checkbox',
label: false,
className: cx('Table-searchableForm-checkbox'),
inputClassName: cx('Table-searchableForm-checkbox-inner'),
name: `__whatever_name`,
option: column.searchable?.label ?? column.label,
badge: {
offset: [-10, 5],
visibleOn: `${
column.toggable &&
!column.toggled &&
column.enableSearch
}`
}
},
{
value: activedSearchableColumns.includes(column),
onChange: (value: any) =>
onItemToggleExpanded?.(column, value)
}
)
};
})
},
@ -147,34 +146,35 @@ export function AutoFilterForm({
{
type: 'submit',
label: __('search'),
size: 'sm',
level: 'primary',
className: 'w-18'
className: 'w-18 mr-2'
},
{
type: 'reset',
label: __('reset'),
size: 'sm',
className: 'w-18'
},
showExpander
? {
children: () => (
<a
className={cx(
'Table-SFToggler',
searchFormExpanded ? 'is-expanded' : ''
)}
onClick={onToggleExpanded}
>
{__(searchFormExpanded ? 'collapse' : 'expand')}
<span className={cx('Table-SFToggler-arrow')}>
<Icon icon="right-arrow-bold" className="icon" />
</span>
</a>
)
}
: null
].filter(item => item)
{
children: () =>
showExpander ? (
<a
className={cx(
'Table-SFToggler',
searchFormExpanded ? 'is-expanded' : ''
)}
onClick={onToggleExpanded}
>
{__(searchFormExpanded ? 'collapse' : 'expand')}
<span className={cx('Table-SFToggler-arrow')}>
<Icon icon="right-arrow-bold" className="icon" />
</span>
</a>
) : null
}
]
});
return {
@ -227,7 +227,7 @@ export default observer(
const onItemToggleExpanded = React.useCallback(
(column: IColumn, value: boolean) => {
column.setEnableSearch(value);
store.setSearchFormExpanded(true);
value && store.setSearchFormExpanded(true);
},
[]
);

View File

@ -115,7 +115,9 @@ export interface Action extends Button {
| 'selectAll'
| 'changeTabKey'
| 'click'
| 'stopAutoRefresh';
| 'stopAutoRefresh'
| 'preview'
| 'zoom';
api?: SchemaApi;
asyncApi?: SchemaApi;
payload?: any;