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`后支持变量 | | step | `number \| string` | `1` | 步长,支持变量 | `3.3.0`后支持变量 |
| showSteps | `boolean` | `false` | 是否显示步长 | | showSteps | `boolean` | `false` | 是否显示步长 |
| parts | `number` or `number[]` | `1` | 分割的块数<br/>主持数组传入分块的节点 | | 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` | 是否显示滑块标签 | | tooltipVisible | `boolean` | `false` | 是否显示滑块标签 |
| tooltipPlacement | `auto` or `bottom` or `left` or `right` | `top` | 滑块标签的位置,默认`auto`,方向自适应<br/>前置条件tooltipVisible 不为 false 时有效 | | tooltipPlacement | `auto` or `bottom` or `left` or `right` | `top` | 滑块标签的位置,默认`auto`,方向自适应<br/>前置条件tooltipVisible 不为 false 时有效 |
| tipFormatter | `function` | | 控制滑块标签显隐函数<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`,调用接口成功时触发 | | deleteSuccess | `index: number` 所在行记录索引 <br /> `item: object` 所在行记录 <br/> `[name]: object[]`列表记录 | 配置了`deleteApi`,调用接口成功时触发 |
| deleteFail | `index: number` 所在行记录索引 <br /> `item: object` 所在行记录 <br/> `[name]: object[]`列表记录<br />`error: object` `deleteApi`请求失败后返回的错误信息 | 配置了`deleteApi`,调用接口失败时触发 | | deleteFail | `index: number` 所在行记录索引 <br /> `item: object` 所在行记录 <br/> `[name]: object[]`列表记录<br />`error: object` `deleteApi`请求失败后返回的错误信息 | 配置了`deleteApi`,调用接口失败时触发 |
| change | `[name]: object[]` 列表记录 | 组件数据发生改变时触发 | | 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 ### 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}来配置具体的参数,详细请查看事件动作。 当前组件对外暴露以下特性动作,其他组件可以通过指定 actionType: 动作名称、componentId: 该组件 id 来触发这些动作,动作配置可以通过 args: {动作配置项名称: xxx}来配置具体的参数,详细请查看事件动作。
@ -1574,6 +1860,7 @@ order: 54
| setValue | `value: object \| Array<object>` 替换的值<br /> `index?: number` 可选,替换第几行数据,如果没有指定,则替换全部表格数据 | 替换表格数据 | | setValue | `value: object \| Array<object>` 替换的值<br /> `index?: number` 可选,替换第几行数据,如果没有指定,则替换全部表格数据 | 替换表格数据 |
| clear | - | 清空表格数据 | | clear | - | 清空表格数据 |
| reset | - | 将表格数据重置为`resetValue`,若没有配置`resetValue`,则清空表格数据 | | reset | - | 将表格数据重置为`resetValue`,若没有配置`resetValue`,则清空表格数据 |
| initDrag | - | 开启表格拖拽排序功能 |
### addItem ### 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) 中的配置以外,还支持下面一些配置 除了支持 [普通表单项属性表](./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) | | 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) | | 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) | | 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) | | 用来自定义值的展示 | | valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
| itemHeight | `number` | `32` | 每个选项的高度,用于虚拟渲染 | | itemHeight | `number` | `32` | 每个选项的高度,用于虚拟渲染 |
| virtualThreshold | `number` | `100` | 在选项数量超过多少时开启虚拟渲染 | | 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"` | | type | `string` | | 如果在 Table、Card 和 List 中,为`"image"`;在 Form 中用作静态展示,为`"static-image"` |
| className | `string` | | 外层 CSS 类名 | | className | `string` | | 外层 CSS 类名 |
| innerClassName | `string` | | 组件内层 CSS 类名 | | innerClassName | `string` | | 组件内层 CSS 类名 |
| imageClassName | `string` | | 图片 CSS 类名 | | imageClassName | `string` | | 图片 CSS 类名 |
| thumbClassName | `string` | | 图片缩率图 CSS 类名 | | thumbClassName | `string` | | 图片缩率图 CSS 类名 |
| height | `string` | | 图片缩率高度 | | height | `string` | | 图片缩率高度 |
| width | `string` | | 图片缩率宽度 | | width | `string` | | 图片缩率宽度 |
| title | `string` | | 标题 | | title | `string` | | 标题 |
| imageCaption | `string` | | 描述 | | imageCaption | `string` | | 描述 |
| placeholder | `string` | | 占位文本 | | placeholder | `string` | | 占位文本 |
| defaultImage | `string` | | 无数据时显示的图片 | | defaultImage | `string` | | 无数据时显示的图片 |
| src | `string` | | 缩略图地址 | | src | `string` | | 缩略图地址 |
| href | [模板](../../docs/concepts/template) | | 外部链接地址 | | href | [模板](../../docs/concepts/template) | | 外部链接地址 |
| originalSrc | `string` | | 原图地址 | | originalSrc | `string` | | 原图地址 |
| enlargeAble | `boolean` | | 支持放大预览 | | enlargeAble | `boolean` | | 支持放大预览 |
| enlargeTitle | `string` | | 放大预览的标题 | | enlargeTitle | `string` | | 放大预览的标题 |
| enlargeCaption | `string` | | 放大预览的描述 | | enlargeCaption | `string` | | 放大预览的描述 |
| enlargeWithGallary | `string` | `true` | 在表格中,图片的放大功能会默认展示所有图片信息,设置为`false`将关闭放大模式下图片集列表的展示 | | enlargeWithGallary | `string` | `true` | 在表格中,图片的放大功能会默认展示所有图片信息,设置为`false`将关闭放大模式下图片集列表的展示 |
| thumbMode | `string` | `contain` | 预览图模式,可选:`'w-full'`, `'h-full'`, `'contain'`, `'cover'` | | thumbMode | `string` | `contain` | 预览图模式,可选:`'w-full'`, `'h-full'`, `'contain'`, `'cover'` |
| thumbRatio | `string` | `1:1` | 预览图比例,可选:`'1:1'`, `'4:3'`, `'16:9'` | | thumbRatio | `string` | `1:1` | 预览图比例,可选:`'1:1'`, `'4:3'`, `'16:9'` |
| imageMode | `string` | `thumb` | 图片展示模式,可选:`'thumb'`, `'original'` 即:缩略图模式 或者 原图模式 | | imageMode | `string` | `thumb` | 图片展示模式,可选:`'thumb'`, `'original'` 即:缩略图模式 或者 原图模式 |
| showToolbar | `boolean` | `false` | 放大模式下是否展示图片的工具栏 | `2.2.0` | | showToolbar | `boolean` | `false` | 放大模式下是否展示图片的工具栏 | `2.2.0` |
| toolbarActions | `ImageAction[]` | | 图片工具栏,支持旋转,缩放,默认操作全部开启 | `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 #### ImageAction
@ -475,3 +477,170 @@ interface ImageAction {
disabled?: boolean; 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)` 语法来嵌入视频。 可以使用 `![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 及以上版本 > 1.8.1 及以上版本

View File

@ -68,9 +68,111 @@ order: 73
| disabled | `boolean` | false | 是否禁用 | | disabled | `boolean` | false | 是否禁用 |
| onPageChange | page、perPage 改变时会触发 | (page: number, perPage: number) => void; | 分页改变触发 | | onPageChange | page、perPage 改变时会触发 | (page: number, perPage: number) => void; | 分页改变触发 |
## 事件表
当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细请查看[事件动作](../../docs/concepts/event-action)。 当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细请查看[事件动作](../../docs/concepts/event-action)。
> `[name]`表示当前组件绑定的名称,即`name`属性,如果没有配置`name`属性,则通过`value`取值。 > `[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 ### rowMouseEnter
鼠标移入行记录。 鼠标移入行记录。

View File

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

View File

@ -24,6 +24,7 @@
rel="stylesheet" rel="stylesheet"
href="/node_modules/@fortawesome/fontawesome-free/css/v4-shims.css" 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="/node_modules/prismjs/themes/prism.css" />
<link rel="stylesheet" href="/examples/doc.css" /> <link rel="stylesheet" href="/examples/doc.css" />
@ -93,5 +94,10 @@
const initialState = {}; const initialState = {};
bootstrap(document.getElementById('root'), initialState); bootstrap(document.getElementById('root'), initialState);
</script> </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> </body>
</html> </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": { "dependencies": {
"path-to-regexp": "^6.2.0",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"qs": "6.9.7", "qs": "6.9.7"
"path-to-regexp": "^6.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/generator": "^7.22.9", "@babel/generator": "^7.22.9",
@ -79,6 +79,7 @@
"jest": "^29.0.3", "jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3", "jest-environment-jsdom": "^29.0.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"katex": "^0.16.9",
"lerna": "^6.6.2", "lerna": "^6.6.2",
"lint-staged": "^12.1.2", "lint-staged": "^12.1.2",
"magic-string": "^0.26.7", "magic-string": "^0.26.7",

View File

@ -36,6 +36,19 @@ export class CmptAction implements RendererAction {
*/ */
const key = action.componentId || action.componentName; const key = action.componentId || action.componentName;
const dataMergeMode = action.dataMergeMode || 'merge'; 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) { if (!key) {
console.warn('请提供目标组件的componentId或componentName'); console.warn('请提供目标组件的componentId或componentName');
@ -59,23 +72,6 @@ export class CmptAction implements RendererAction {
} }
if (action.actionType === 'setValue') { 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) { if (component?.setData) {
return component?.setData( return component?.setData(
action.args?.value, action.args?.value,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -304,7 +304,9 @@ export interface RendererInfo extends RendererScaffoldInfo {
sharedContext?: Record<string, any>; sharedContext?: Record<string, any>;
dialogTitle?: string; //弹窗标题用于弹窗大纲的展示 dialogTitle?: string; //弹窗标题用于弹窗大纲的展示
dialogType?: string; //区分确认对话框类型 dialogType?: string; //区分确认对话框类型
subEditorVariable?: Array<{label: string; children: any}>; // 传递给子编辑器的组件自定义变量如listSelect的选项名称和值 getSubEditorVariable?: (
schema?: any
) => Array<{label: string; children: any}>; // 传递给子编辑器的组件自定义变量如listSelect的选项名称和值
} }
export type BasicRendererInfo = Omit< export type BasicRendererInfo = Omit<
@ -1051,7 +1053,7 @@ export abstract class BasePlugin implements PluginInterface {
isListComponent: plugin.isListComponent, isListComponent: plugin.isListComponent,
rendererName: plugin.rendererName, rendererName: plugin.rendererName,
memberImmutable: plugin.memberImmutable, 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)) { if (Array.isArray(obj)) {
return obj.map((item, index) => JSONPipeIn(item, generateId)); return obj.map((item, index) => JSONPipeIn(item, generateId, idMap));
} }
let toUpdate: any = {}; let toUpdate: any = {};
@ -1224,7 +1224,10 @@ export async function resolveVariablesFromScope(node: any, manager: any) {
// 子编辑器内读取的host节点自定义变量非数据域方式如listSelect的选项值 // 子编辑器内读取的host节点自定义变量非数据域方式如listSelect的选项值
let hostNodeVaraibles = []; let hostNodeVaraibles = [];
if (manager?.store?.isSubEditor) { if (manager?.store?.isSubEditor) {
hostNodeVaraibles = manager.config?.hostNode?.info?.subEditorVariable || []; hostNodeVaraibles =
manager.config?.hostNode?.info?.getSubEditorVariable?.(
manager.config?.hostNode.schema
) || [];
} }
const variables: VariableItem[] = const variables: VariableItem[] =

View File

@ -36,7 +36,7 @@ import type {
} from 'amis-editor-core'; } from 'amis-editor-core';
import {normalizeApi} from 'amis-core'; import {normalizeApi} from 'amis-core';
import isPlainObject from 'lodash/isPlainObject'; import isPlainObject from 'lodash/isPlainObject';
import omit from 'lodash/omit'; import findLastIndex from 'lodash/findLastIndex';
interface ColumnItem { interface ColumnItem {
label: string; label: string;
@ -371,6 +371,9 @@ export class CRUDPlugin extends BasePlugin {
type: 'button', type: 'button',
actionType: 'dialog', actionType: 'dialog',
level: 'primary', level: 'primary',
editorSetting: {
behavior: 'create'
},
dialog: { dialog: {
title: '新增', title: '新增',
body: { body: {
@ -385,6 +388,9 @@ export class CRUDPlugin extends BasePlugin {
type: 'button', type: 'button',
actionType: 'dialog', actionType: 'dialog',
level: 'link', level: 'link',
editorSetting: {
behavior: 'update'
},
dialog: { dialog: {
title: '编辑', title: '编辑',
body: { body: {
@ -399,6 +405,9 @@ export class CRUDPlugin extends BasePlugin {
type: 'button', type: 'button',
actionType: 'dialog', actionType: 'dialog',
level: 'link', level: 'link',
editorSetting: {
behavior: 'view'
},
dialog: { dialog: {
title: '查看详情', title: '查看详情',
body: { body: {
@ -415,7 +424,10 @@ export class CRUDPlugin extends BasePlugin {
level: 'link', level: 'link',
className: 'text-danger', className: 'text-danger',
confirmText: '确定要删除?', confirmText: '确定要删除?',
api: 'delete:/xxx/delete' api: 'delete:/xxx/delete',
editorSetting: {
behavior: 'delete'
}
}, },
bulkDelete: { bulkDelete: {
type: 'button', type: 'button',
@ -423,12 +435,18 @@ export class CRUDPlugin extends BasePlugin {
label: '批量删除', label: '批量删除',
actionType: 'ajax', actionType: 'ajax',
confirmText: '确定要删除?', confirmText: '确定要删除?',
api: '/xxx/batch-delete' api: '/xxx/batch-delete',
editorSetting: {
behavior: 'bulkDelete'
}
}, },
bulkUpdate: { bulkUpdate: {
type: 'button', type: 'button',
label: '批量编辑', label: '批量编辑',
actionType: 'dialog', actionType: 'dialog',
editorSetting: {
behavior: 'bulkUpdate'
},
dialog: { dialog: {
title: '批量编辑', title: '批量编辑',
size: 'md', size: 'md',
@ -543,7 +561,7 @@ export class CRUDPlugin extends BasePlugin {
} }
}, },
{ {
name: 'features', name: '__features',
label: '启用功能', label: '启用功能',
type: 'checkboxes', type: 'checkboxes',
joinValues: false, joinValues: false,
@ -575,10 +593,10 @@ export class CRUDPlugin extends BasePlugin {
type: 'input-number', type: 'input-number',
label: '每列显示几个字段', label: '每列显示几个字段',
value: 3, value: 3,
name: 'filterColumnCount' name: '__filterColumnCount'
} }
], ],
visibleOn: 'data.features && data.features.includes("filter")' visibleOn: "${__features && CONTAINS(__features, 'filter')}"
}, },
{ {
name: 'columns', 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) => { pipeOut: (value: any) => {
let valueSchema = cloneDeep(value); 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格式 */ /** 统一api格式 */
valueSchema.api = valueSchema.api =
typeof valueSchema.api === 'string' typeof valueSchema.api === 'string'
? normalizeApi(valueSchema.api) ? normalizeApi(valueSchema.api)
: 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') { if (item === 'update') {
schema = cloneDeep(this.btnSchemas.update); schema = cloneDeep(this.btnSchemas.update);
schema.dialog.body.body = value.columns schema.dialog.body.body = value.columns
@ -692,9 +776,7 @@ export class CRUDPlugin extends BasePlugin {
? valueSchema.api ? valueSchema.api
: {...valueSchema.api, method: 'post'}; : {...valueSchema.api, method: 'post'};
} }
schema && operButtons.push(schema);
// 添加操作按钮
this.addItem(oper.buttons, schema);
} else { } else {
// 批量操作 // 批量操作
if (item === 'bulkUpdate') { if (item === 'bulkUpdate') {
@ -730,13 +812,14 @@ export class CRUDPlugin extends BasePlugin {
}; };
valueSchema.headerToolbar = [createSchemaBase, 'bulkActions']; valueSchema.headerToolbar = [createSchemaBase, 'bulkActions'];
} }
// 查询
let keysFilter = Object.keys(valueSchema.filter || {}); let keysFilter = Object.keys(valueSchema.filter || {});
if (item === 'filter' && !keysFilter.length) { if (item === 'filter' && !keysFilter.length) {
if (valueSchema.filterEnabledList) { if (valueSchema.filterEnabledList) {
valueSchema.filter = { valueSchema.filter = {
title: '查询条件' title: '查询条件'
}; };
valueSchema.filter.columnCount = value.filterColumnCount; valueSchema.filter.columnCount = value.__filterColumnCount;
valueSchema.filter.mode = 'horizontal'; valueSchema.filter.mode = 'horizontal';
valueSchema.filter.body = valueSchema.filterEnabledList.map( valueSchema.filter.body = valueSchema.filterEnabledList.map(
(item: any) => { (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' (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; return valueSchema;
}, },
canRebuild: true canRebuild: true

View File

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

View File

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

View File

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

View File

@ -1,15 +1,21 @@
import {isObject} from 'amis';
import { import {
EditorNodeType, BasePlugin,
RendererPluginAction, defaultValue,
RendererPluginEvent getSchemaTpl,
tipedLabel,
registerEditorPlugin
} from 'amis-editor-core'; } 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 {ValidatorTag} from '../../validator';
import {getEventControlConfig} from '../../renderer/event-control/helper'; import {getEventControlConfig} from '../../renderer/event-control/helper';
import type {IFormStore, IFormItemStore} from 'amis-core'; import type {IFormStore, IFormItemStore} from 'amis-core';
import type {
EditorNodeType,
RendererPluginAction,
RendererPluginEvent,
BaseEventContext
} from 'amis-editor-core';
export class RangeControlPlugin extends BasePlugin { export class RangeControlPlugin extends BasePlugin {
static id = 'RangeControlPlugin'; static id = 'RangeControlPlugin';
@ -135,6 +141,19 @@ export class RangeControlPlugin extends BasePlugin {
panelTitle = '滑块'; panelTitle = '滑块';
panelJustify = true; 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) => { panelBodyCreator = (context: BaseEventContext) => {
return getSchemaTpl('tabs', [ 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', actionType: 'clear',
actionLabel: '清空', actionLabel: '清空',
description: '清空组件数据' description: '清空组件数据'
},
{
actionType: 'initDrag',
actionLabel: '开启排序',
description: '开启表格拖拽排序功能'
} }
]; ];
@ -1071,6 +1202,16 @@ export class TableControlPlugin extends BasePlugin {
name: 'affixHeader', name: 'affixHeader',
label: '是否固定表头', label: '是否固定表头',
pipeIn: defaultValue(false) 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', { getSchemaTpl('className', {
name: 'rowClassName', name: 'rowClassName',
label: '行样式' label: '行样式'
}),
getSchemaTpl('className', {
name: 'toolbarClassName',
label: '工具栏'
}) })
] ]
}) })

View File

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

View File

@ -50,7 +50,7 @@ export class PickerControlPlugin extends BasePlugin {
value: 'B' value: 'B'
} }
], ],
modalClassName: 'app-popover' modalClassName: 'app-popover :AMISCSSWrapper'
}; };
previewSchema: any = { previewSchema: any = {
type: 'form', 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 { import {
ActiveEventContext, ActiveEventContext,
BaseEventContext, BaseEventContext,
@ -8,6 +13,10 @@ import {
} from 'amis-editor-core'; } from 'amis-editor-core';
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core'; import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {mockValue} from 'amis-editor-core'; import {mockValue} from 'amis-editor-core';
import {
getArgsWrapper,
getEventControlConfig
} from '../renderer/event-control/helper';
export class ImagePlugin extends BasePlugin { export class ImagePlugin extends BasePlugin {
static id = 'ImagePlugin'; static id = 'ImagePlugin';
@ -34,6 +43,112 @@ export class ImagePlugin extends BasePlugin {
value: mockValue({type: 'image'}) 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 = '图片'; panelTitle = '图片';
panelJustify = true; panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => { panelBodyCreator = (context: BaseEventContext) => {
@ -63,7 +178,7 @@ export class ImagePlugin extends BasePlugin {
pipeIn: defaultValue('thumb'), pipeIn: defaultValue('thumb'),
options: [ options: [
{ {
label: '缩图', label: '缩图',
value: 'thumb' value: 'thumb'
}, },
{ {
@ -130,6 +245,24 @@ export class ImagePlugin extends BasePlugin {
getSchemaTpl('imageUrl', { getSchemaTpl('imageUrl', {
name: 'defaultImage', name: 'defaultImage',
label: tipedLabel('占位图', '无数据时显示的图片') 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') 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'; pluginIcon = 'images-plugin';
scaffold = { scaffold = {
type: 'images', type: 'images',
imageGallaryClassName: 'app-popover' imageGallaryClassName: 'app-popover :AMISCSSWrapper'
}; };
previewSchema = { previewSchema = {
...this.scaffold, ...this.scaffold,

View File

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

View File

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

View File

@ -321,6 +321,7 @@ export class SwitchContainerPlugin extends LayoutBasePlugin {
name: 'items', name: 'items',
label: '状态列表', label: '状态列表',
addTip: '新增组件状态', addTip: '新增组件状态',
minLength: 1,
items: [ items: [
{ {
type: 'input-text', type: 'input-text',
@ -356,6 +357,10 @@ export class SwitchContainerPlugin extends LayoutBasePlugin {
title: '外观', title: '外观',
className: 'p-none', className: 'p-none',
body: getSchemaTpl('collapseGroup', [ body: getSchemaTpl('collapseGroup', [
getSchemaTpl('theme:base', {
collapsed: false,
extra: []
}),
{ {
title: '布局', title: '布局',
body: [ body: [
@ -460,7 +465,15 @@ export class SwitchContainerPlugin extends LayoutBasePlugin {
getSchemaTpl('layout:stickyPosition') 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', eventName: 'rowMouseEnter',
eventLabel: '鼠标移入行事件', eventLabel: '鼠标移入行事件',

View File

@ -7,7 +7,7 @@ import {findDOMNode} from 'react-dom';
import cx from 'classnames'; import cx from 'classnames';
import get from 'lodash/get'; import get from 'lodash/get';
import Sortable from 'sortablejs'; 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 {autobind} from 'amis-editor-core';
import type {Option} from 'amis'; import type {Option} from 'amis';
import {createObject, FormControlProps} from 'amis-core'; import {createObject, FormControlProps} from 'amis-core';
@ -30,7 +30,6 @@ export type SourceType = 'custom' | 'api' | 'apicenter' | 'variable';
export interface OptionControlState { export interface OptionControlState {
items: Array<PlainObject>; items: Array<PlainObject>;
api: SchemaApi;
labelField: string; labelField: string;
valueField: string; valueField: string;
} }
@ -50,7 +49,6 @@ export default class ListItemControl extends React.Component<
this.state = { this.state = {
items: this.transformOptions(props), items: this.transformOptions(props),
api: props.data.source,
labelField: props.data.labelField || 'title', labelField: props.data.labelField || 'title',
valueField: props.data.valueField valueField: props.data.valueField
}; };
@ -173,6 +171,12 @@ export default class ListItemControl extends React.Component<
*/ */
handleDelete(index: number) { handleDelete(index: number) {
const items = this.state.items.concat(); 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); items.splice(index, 1);
this.setState({items}, () => this.onChange()); this.setState({items}, () => this.onChange());

View File

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

View File

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

View File

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

View File

@ -563,6 +563,18 @@ test('evalute:Math', () => {
expect(evaluate('${POW(2, infinity)}', data)).toBe(data.infinity); 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', () => { test('evalute:namespace', () => {
localStorage.setItem('a', '1'); localStorage.setItem('a', '1');
localStorage.setItem('b', '2'); localStorage.setItem('b', '2');

View File

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

View File

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

View File

@ -1514,6 +1514,21 @@ export class Evaluator {
return text.split(/[\\/]/).pop(); 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; 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-paddingBottom: var(--sizes-size-5);
--transfer-base-header-paddingLeft: var(--sizes-size-8); --transfer-base-header-paddingLeft: var(--sizes-size-8);
--transfer-base-header-paddingRight: 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-paddingTop: var(--sizes-size-0);
--transfer-base-body-paddingBottom: var(--sizes-size-0); --transfer-base-body-paddingBottom: var(--sizes-size-0);
--transfer-base-body-paddingLeft: var(--sizes-size-0); --transfer-base-body-paddingLeft: var(--sizes-size-0);
@ -2052,6 +2053,7 @@
--Tabs-onActive-bg: var(--background); --Tabs-onActive-bg: var(--background);
--Tabs-onActive-borderColor: var(--borderColor); --Tabs-onActive-borderColor: var(--borderColor);
--Tabs-onActive-color: var(--colors-neutral-text-2); --Tabs-onActive-color: var(--colors-neutral-text-2);
--Tabs-onError-color: var(--colors-error-5);
--Tabs-onDisabled-color: var(--colors-neutral-text-7); --Tabs-onDisabled-color: var(--colors-neutral-text-7);
--Tabs-onHover-borderColor: var(--colors-neutral-line-8); --Tabs-onHover-borderColor: var(--colors-neutral-line-8);
--Tabs-add-icon-size: #{px2rem(15px)}; --Tabs-add-icon-size: #{px2rem(15px)};
@ -4130,6 +4132,7 @@
var(--combo-vertical-right-border-color) var(--combo-vertical-right-border-color)
var(--combo-vertical-bottom-border-color) var(--combo-vertical-bottom-border-color)
var(--combo-vertical-left-border-color); var(--combo-vertical-left-border-color);
--Combo--vertical-item--onError-borderColor: var(--colors-error-5);
--Combo--vertical-item-borderRadius: var( --Combo--vertical-item-borderRadius: var(
--combo-vertical-top-left-border-radius --combo-vertical-top-left-border-radius
) )

View File

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

View File

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

View File

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

View File

@ -74,7 +74,7 @@
} }
} }
&-simple { &-simple {
>ul >li { > ul > li {
&:hover, &:hover,
&:focus { &:focus {
outline: none; outline: none;
@ -94,8 +94,6 @@
} }
} }
&-next { &-next {
> span { > span {
cursor: pointer; cursor: pointer;
@ -113,47 +111,47 @@
align-items: center; align-items: center;
height: var(--Pagination-height); height: var(--Pagination-height);
&-left { &-left {
color: var(--Pagination-light-color); color: var(--Pagination-light-color);
} }
input { input {
min-width: px2rem(50px); min-width: px2rem(50px);
width: px2rem(50px); width: px2rem(50px);
height: var(--Pagination-height); height: var(--Pagination-height);
line-height: var(--Pagination-height); line-height: var(--Pagination-height);
// height: var(--Pagination-height); // height: var(--Pagination-height);
border: none; border: none;
border: var(--borderWidth) solid var(--borderColor); border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--borderRadius) 0 0 var(--borderRadius); border-radius: var(--borderRadius) 0 0 var(--borderRadius);
padding: var(--Pagination-padding); padding: var(--Pagination-padding);
margin-left: px2rem(8px); margin-left: px2rem(8px);
text-align: center; text-align: center;
&:focus, &:focus,
&:hover { &:hover {
outline: none; outline: none;
// border: var(--borderWidth) solid var(--primary); // border: var(--borderWidth) solid var(--primary);
border-color: var(--primary); border-color: var(--primary);
}
} }
&-right { }
display: inline-block; &-right {
width: px2rem(32px); display: inline-block;
cursor: pointer; width: px2rem(32px);
text-align: center; cursor: pointer;
height: var(--Pagination-height); text-align: center;
line-height: var(--Pagination-height); height: var(--Pagination-height);
border: var(--borderWidth) solid var(--borderColor); line-height: var(--Pagination-height);
border-left: none; border: var(--borderWidth) solid var(--borderColor);
border-radius: 0 var(--borderRadius) var(--borderRadius) 0; border-left: none;
font-size: var(--fontSizeSm); border-radius: 0 var(--borderRadius) var(--borderRadius) 0;
&:hover { font-size: var(--fontSizeSm);
color: var(--primary); &:hover {
border-color: var(--primary); color: var(--primary);
border-left: var(--borderWidth) solid var(--primary); border-color: var(--primary);
margin-left: -1px; border-left: var(--borderWidth) solid var(--primary);
} margin-left: -1px;
} }
}
} }
} }
@ -161,7 +159,6 @@
line-height: px2rem(30px); line-height: px2rem(30px);
.#{$ns}Pagination-item { .#{$ns}Pagination-item {
margin-left: px2rem(8px);
&:nth-child(1) { &:nth-child(1) {
margin-left: 0; margin-left: 0;
} }
@ -198,4 +195,4 @@
} }
text-align: right; 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 { .#{$ns}Tabs-addable {
display: flex; display: flex;
margin-left: var(--Tabs-add-margin);
align-items: center; align-items: center;
justify-content: flex-start; margin-left: var(--Tabs-add-margin);
padding: var(--Tabs--line-addPadding); margin-bottom: px2rem(3px);
padding-bottom: px2rem(8px);
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
@ -60,16 +60,19 @@
// } // }
&-arrow { &-arrow {
margin: var(--Tabs--line-addPadding);
width: 16px; width: 16px;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
box-sizing: content-box; box-sizing: content-box;
margin-bottom: px2rem(3px);
padding-bottom: px2rem(8px);
.iconfont { .icon {
font-size: var(--Remark-icon-fontSize); top: 0;
width: var(--Remark-icon-fontSize);
height: var(--Remark-icon-fontSize);
} }
&:hover { &:hover {
color: var(--icon-onHover-color); color: var(--icon-onHover-color);
@ -77,6 +80,9 @@
&--left { &--left {
padding-right: 16px; padding-right: 16px;
svg {
transform: rotate(180deg);
}
} }
&--right { &--right {
@ -242,6 +248,10 @@
border-color: var(--Tabs-onActive-borderColor); border-color: var(--Tabs-onActive-borderColor);
border-bottom-color: transparent; border-bottom-color: transparent;
} }
&.has-error > a:first-child {
color: var(--Tabs-onError-color) !important;
}
} }
} }
@ -672,6 +682,7 @@
.#{$ns}Tabs-addable { .#{$ns}Tabs-addable {
padding: 0 var(--Tabs--tiled-add-gap); padding: 0 var(--Tabs--tiled-add-gap);
margin-left: 0; margin-left: 0;
margin-bottom: 0;
white-space: nowrap; white-space: nowrap;
border-style: solid; border-style: solid;
border-color: var(--Tabs-borderColor); border-color: var(--Tabs-borderColor);
@ -1112,6 +1123,7 @@
& > .#{$ns}Tabs-linksContainer { & > .#{$ns}Tabs-linksContainer {
> .#{$ns}Tabs-linksContainer-arrow { > .#{$ns}Tabs-linksContainer-arrow {
margin-bottom: 0; margin-bottom: 0;
padding: 0;
} }
> .#{$ns}Tabs-linksContainer-main > .#{$ns}Tabs-links { > .#{$ns}Tabs-linksContainer-main > .#{$ns}Tabs-links {
@ -1224,21 +1236,6 @@
> .#{$ns}Tabs-linksContainer { > .#{$ns}Tabs-linksContainer {
margin-bottom: calc(var(--Tabs-borderWidth) * -1); 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 { .#{$ns}Tabs-linksContainer-arrow {
width: var(--Tabs--strong-arrow-size); width: var(--Tabs--strong-arrow-size);
margin-bottom: 0; margin-bottom: 0;
@ -1250,14 +1247,14 @@
&--left { &--left {
padding-right: 0; padding-right: 0;
border-right-width: 0;
border-top-left-radius: var(--Tabs-borderRadius); border-top-left-radius: var(--Tabs-borderRadius);
margin-right: px2rem(8px);
} }
&--right { &--right {
padding-left: 0; padding-left: 0;
border-left-width: 0; border-top-left-radius: var(--Tabs-borderRadius);
border-top-right-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-paddingRight) var(--combo-vertical-paddingBottom)
var(--combo-vertical-paddingLeft); var(--combo-vertical-paddingLeft);
position: relative; 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 { > .#{$ns}Combo-items > .#{$ns}Combo-item {

View File

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

View File

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

View File

@ -132,6 +132,13 @@
max-width: 100%; max-width: 100%;
margin-top: -1px; margin-top: -1px;
border-color: var(--Form-input-onFocused-borderColor); 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 { .#{$ns}MapPicker {

View File

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

View File

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

View File

@ -319,7 +319,10 @@
background: var(--Form-select-menu-bg); background: var(--Form-select-menu-bg);
color: var(--Form-select-menu-color); color: var(--Form-select-menu-color);
border-radius: px2rem(2px); 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 { &-sugs {
@ -328,13 +331,11 @@
} }
&-sugItem { &-sugItem {
padding: calc( padding: var(--select-base-default-option-paddingTop)
( var(--select-base-default-option-paddingRight)
var(--Form-selectOption-height) - var(--Form-input-lineHeight) * var(--select-base-default-option-paddingBottom)
var(--Form-input-fontSize) - #{px2rem(2px)} var(--select-base-default-option-paddingLeft);
) / 2 line-height: var(--select-base-default-option-line-height);
)
px2rem(12px);
svg { svg {
width: px2rem(16px); 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, &-select,
&-result { &-result {
overflow: hidden; overflow: hidden;
@ -64,6 +100,10 @@
var(--transfer-base-top-right-border-radius) var(--transfer-base-top-right-border-radius)
var(--transfer-base-bottom-right-border-radius) var(--transfer-base-bottom-right-border-radius)
var(--transfer-base-bottom-left-border-radius); var(--transfer-base-bottom-left-border-radius);
&--pagination {
max-height: px2rem(475px);
}
} }
&-select > &-selection, &-select > &-selection,

View File

@ -55,6 +55,17 @@ export default class Markdown extends React.Component<MarkdownProps> {
async _render() { async _render() {
const {content, options} = this.props; const {content, options} = this.props;
this.dom.innerHTML = markdown(content, options); 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() { render() {

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import includes from 'lodash/includes';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import unionWith from 'lodash/unionWith'; import unionWith from 'lodash/unionWith';
import {ThemeProps, themeable, findTree, differenceFromAll} from 'amis-core'; import {ThemeProps, themeable, findTree, differenceFromAll} from 'amis-core';
import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection'; import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection';
import {Options, Option} from './Select'; import {Options, Option} from './Select';
@ -24,6 +23,7 @@ import {ItemRenderStates as ResultItemRenderStates} from './ResultList';
import ResultTableList from './ResultTableList'; import ResultTableList from './ResultTableList';
import ResultTreeList from './ResultTreeList'; import ResultTreeList from './ResultTreeList';
import {SpinnerExtraProps} from './Spinner'; import {SpinnerExtraProps} from './Spinner';
import Pagination from './Pagination';
export type SelectMode = export type SelectMode =
| 'table' | 'table'
@ -113,6 +113,44 @@ export interface TransferProps
checkAllLabel?: string; checkAllLabel?: string;
/** 树形模式下,给 tree 的属性 */ /** 树形模式下,给 tree 的属性 */
onlyChildren?: boolean; 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 { export interface TransferState {
@ -549,10 +587,33 @@ export class Transfer<
{this.state.searchResult !== null {this.state.searchResult !== null
? this.renderSearchResult(props) ? this.renderSearchResult(props)
: this.renderOptions(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) { renderSearchResult(props: TransferProps) {
const { const {
searchResultMode, searchResultMode,
@ -827,9 +888,10 @@ export class Transfer<
virtualThreshold, virtualThreshold,
itemHeight, itemHeight,
loadingConfig, loadingConfig,
showInvalidMatch showInvalidMatch,
pagination,
accumulatedOptions
} = this.props; } = this.props;
const {resultSelectMode, isTreeDeferLoad} = this.state; const {resultSelectMode, isTreeDeferLoad} = this.state;
const searchable = !isTreeDeferLoad && resultSearchable; const searchable = !isTreeDeferLoad && resultSearchable;
@ -840,7 +902,7 @@ export class Transfer<
ref={this.domResultRef} ref={this.domResultRef}
classnames={cx} classnames={cx}
columns={columns!} columns={columns!}
options={options || []} options={(pagination?.enable ? accumulatedOptions : options) || []}
value={value} value={value}
disabled={disabled} disabled={disabled}
option2value={option2value} option2value={option2value}
@ -862,7 +924,7 @@ export class Transfer<
loadingConfig={loadingConfig} loadingConfig={loadingConfig}
classnames={cx} classnames={cx}
className={cx('Transfer-value')} className={cx('Transfer-value')}
options={options} options={(pagination?.enable ? accumulatedOptions : options) || []}
valueField={'value'} valueField={'value'}
value={value || []} value={value || []}
onChange={onChange!} onChange={onChange!}
@ -915,7 +977,8 @@ export class Transfer<
selectMode = 'list', selectMode = 'list',
translate: __, translate: __,
valueField = 'value', valueField = 'value',
mobileUI mobileUI,
pagination
} = this.props as any; } = this.props as any;
const {searchResult} = this.state; const {searchResult} = this.state;
@ -939,7 +1002,11 @@ export class Transfer<
<div <div
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')} 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)} {this.renderSelect(this.props)}
</div> </div>
<div className={cx('Transfer-mid', {'is-mobile': mobileUI})}> <div className={cx('Transfer-mid', {'is-mobile': mobileUI})}>
@ -949,7 +1016,12 @@ export class Transfer<
</div> </div>
) : null} ) : null}
</div> </div>
<div className={cx('Transfer-result', {'is-mobile': mobileUI})}> <div
className={cx('Transfer-result', {
'is-mobile': mobileUI,
'Transfer-select--pagination': !!pagination?.enable
})}
>
<div <div
className={cx( className={cx(
'Transfer-title', 'Transfer-title',

View File

@ -20,6 +20,7 @@
* 17. api * 17. api
* 18. CRUD * 18. CRUD
* 19. fetchInitData silent * 19. fetchInitData silent
* 20. CRUD表头查询字段更新后严格比较场景
*/ */
import { import {
@ -1046,9 +1047,8 @@ test('17. should use the first array item in the response if provided', async ()
) )
); );
waitFor(() => { await wait(200);
expect(container.querySelectorAll('tbody>tr').length).toBe(2); expect(container.querySelectorAll('tbody>tr').length).toBe(2);
});
}); });
describe('18. inner events', () => { describe('18. inner events', () => {
@ -1149,3 +1149,169 @@ test('19. fetchInitData silent true', async () => {
expect(notify).toBeCalledTimes(1); 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> </div>
`; `;
exports[`Renderer:static 2`] = ` exports[`Renderer:static2 1`] = `
<div> <div>
<div <div
class="cxd-Panel cxd-Panel--default cxd-Panel--form" class="cxd-Panel cxd-Panel--default cxd-Panel--form"

View File

@ -2,12 +2,13 @@
* NestedSelect * NestedSelect
* *
* 01. maxTagLength * 01. maxTagLength
* 02. onlyLeaf
*/ */
import {render, cleanup, waitFor} from '@testing-library/react'; import {render, cleanup, waitFor, fireEvent} from '@testing-library/react';
import '../../../src'; import '../../../src';
import {render as amisRender} from '../../../src'; import {render as amisRender} from '../../../src';
import {makeEnv} from '../../helper'; import {makeEnv, wait} from '../../helper';
import {clearStoresCache} from '../../../src'; import {clearStoresCache} from '../../../src';
afterEach(() => { afterEach(() => {
@ -113,3 +114,107 @@ describe('Renderer:NestedSelect', () => {
expect(container).toMatchSnapshot(); 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(); expect(container).toMatchSnapshot();
}); });
test('Renderer:static', async () => { test('Renderer:static2', async () => {
const {container} = render( const {container} = render(
amisRender( amisRender(
{ {
@ -189,3 +189,39 @@ test('Renderer:staticOn', async () => {
const text = getByText('123'); const text = getByText('123');
expect(text).toBeInTheDocument(); 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 () => { test('Renderer:transfer tree search', async () => {
const onSubmit = jest.fn(); const onSubmit = jest.fn();
const {container, findByText, getByText} = render( const {container, findByText, getByText} = render(
amisRender( amisRender(
@ -1486,7 +1485,7 @@ test('Renderer:transfer tree search', async () => {
}); });
await(300); await(300);
const libai = getByText('李白'); const libai = getByText('李白');
expect(libai).not.toBeNull(); expect(libai).not.toBeNull();
fireEvent.click(libai); fireEvent.click(libai);
@ -1501,4 +1500,310 @@ test('Renderer:transfer tree search', async () => {
expect(onSubmit.mock.calls[0][0]).toEqual({ expect(onSubmit.mock.calls[0][0]).toEqual({
transfer: "caocao,libai" 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 * 9. href
* 10. * 10.
* 11. clickAction * 11. clickAction
* 12. click事件
* 13. mouseenter / mouseleave
* 14. preview
* 15. zoom & maxScale & minScale
* *
* * Images * * Images
* images image 使 * images image 使
@ -20,7 +24,7 @@
* 2. enlargeAble & originalSrc & source & title & description * 2. enlargeAble & originalSrc & source & title & description
*/ */
import {fireEvent, render} from '@testing-library/react'; import {fireEvent, render, waitFor} from '@testing-library/react';
import '../../src'; import '../../src';
import {render as amisRender} from '../../src'; import {render as amisRender} from '../../src';
import {makeEnv, wait} from '../helper'; import {makeEnv, wait} from '../helper';
@ -334,7 +338,228 @@ describe('Renderer:image', () => {
}) })
); );
fireEvent.click(container.querySelector('.cxd-Image-thumbWrap')!); 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 <div
class="cxd-ImageField cxd-ImageField--thumb" class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
> >
<div <div
class="cxd-Image cxd-Image--thumb no-border" class="cxd-Image cxd-Image--thumb no-border"
@ -103,6 +104,7 @@ exports[`Renderer:image image:basic 1`] = `
<div> <div>
<div <div
class="cxd-ImageField cxd-ImageField--thumb show" class="cxd-ImageField cxd-ImageField--thumb show"
style="transform: scale(1);"
> >
<div <div
class="cxd-Image cxd-Image--thumb" class="cxd-Image cxd-Image--thumb"
@ -138,6 +140,7 @@ exports[`Renderer:image image:enlargeAble & originalSrc & enlargeTitle & showToo
<div> <div>
<div <div
class="cxd-ImageField cxd-ImageField--thumb" class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
> >
<div <div
class="cxd-Image cxd-Image--thumb" class="cxd-Image cxd-Image--thumb"
@ -178,6 +181,7 @@ exports[`Renderer:image image:enlargeAble & originalSrc & enlargeTitle & showToo
<div> <div>
<div <div
class="cxd-ImageField cxd-ImageField--thumb" class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
> >
<div <div
class="cxd-Image cxd-Image--thumb" class="cxd-Image cxd-Image--thumb"
@ -231,6 +235,7 @@ exports[`Renderer:image image:href 1`] = `
> >
<div <div
class="cxd-ImageField cxd-ImageField--thumb" class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
> >
<a <a
class="cxd-Link" class="cxd-Link"
@ -263,10 +268,62 @@ exports[`Renderer:image image:href 1`] = `
</div> </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`] = ` exports[`Renderer:image image:title & imageCaption 1`] = `
<div> <div>
<div <div
class="cxd-ImageField cxd-ImageField--thumb" class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
> >
<div <div
class="cxd-Image cxd-Image--thumb" class="cxd-Image cxd-Image--thumb"
@ -308,6 +365,7 @@ exports[`Renderer:image image:width & height 1`] = `
<div> <div>
<div <div
class="cxd-ImageField cxd-ImageField--thumb" class="cxd-ImageField cxd-ImageField--thumb"
style="transform: scale(1);"
> >
<div <div
class="cxd-Image cxd-Image--thumb" class="cxd-Image cxd-Image--thumb"
@ -330,6 +388,65 @@ exports[`Renderer:image image:width & height 1`] = `
</div> </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`] = ` exports[`Renderer:images images:basic 1`] = `
<div> <div>
<div <div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import find from 'lodash/find'; import find from 'lodash/find';
import pick from 'lodash/pick';
import {isAlive} from 'mobx-state-tree';
import {matchSorter} from 'match-sorter';
import { import {
OptionsControlProps, OptionsControlProps,
OptionsControl, OptionsControl,
FormOptionsControl,
resolveEventData, resolveEventData,
str2function, str2function,
getOptionValueBindField getOptionValueBindField,
} from 'amis-core'; isEffectiveApi,
import {SpinnerExtraProps, Transfer} from 'amis-ui'; isPureVariable,
import type {Option} from 'amis-core'; resolveVariableAndFilter,
import {
autobind, autobind,
filterTree, filterTree,
string2regExp, string2regExp,
@ -20,18 +20,25 @@ import {
findTreeIndex, findTreeIndex,
getTree, getTree,
spliceTree, spliceTree,
mapTree mapTree,
optionValueCompare,
resolveVariable,
ActionObject,
toNumber
} from 'amis-core'; } from 'amis-core';
import {Spinner} from 'amis-ui'; import {SpinnerExtraProps, Transfer, Spinner, ResultList} from 'amis-ui';
import {optionValueCompare} from 'amis-core'; import {
import {resolveVariable} from 'amis-core'; FormOptionsSchema,
import {FormOptionsSchema, SchemaApi, SchemaObject} from '../../Schema'; SchemaApi,
import {Selection as BaseSelection} from 'amis-ui'; SchemaObject,
import {ResultList} from 'amis-ui'; SchemaExpression,
import {ActionObject, toNumber} from 'amis-core'; SchemaClassName
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection'; } from '../../Schema';
import {supportStatic} from './StaticHoc'; 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 * Transfer
@ -161,6 +168,22 @@ export interface TransferControlSchema
* *
*/ */
onlyChildren?: boolean; 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 export interface BaseTransferProps
@ -427,6 +450,30 @@ export class BaseTransferRenderer<
return regexp.test(labelTest) || regexp.test(valueTest); 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 @autobind
optionItemRender(option: Option, states: ItemRenderStates) { optionItemRender(option: Option, states: ItemRenderStates) {
const {menuTpl, render, data} = this.props; const {menuTpl, render, data} = this.props;
@ -544,7 +591,11 @@ export class BaseTransferRenderer<
showInvalidMatch, showInvalidMatch,
onlyChildren, onlyChildren,
mobileUI, mobileUI,
noResultsText noResultsText,
pagination,
formItem,
env,
popOverContainer
} = this.props; } = this.props;
// 目前 LeftOptions 没有接口可以动态加载 // 目前 LeftOptions 没有接口可以动态加载
@ -570,6 +621,7 @@ export class BaseTransferRenderer<
onlyChildren={onlyChildren} onlyChildren={onlyChildren}
value={selectedOptions} value={selectedOptions}
options={options} options={options}
accumulatedOptions={formItem?.accumulatedOptions ?? []}
disabled={disabled} disabled={disabled}
onChange={this.handleChange} onChange={this.handleChange}
option2value={this.option2value} option2value={this.option2value}
@ -607,6 +659,28 @@ export class BaseTransferRenderer<
showInvalidMatch={showInvalidMatch} showInvalidMatch={showInvalidMatch}
mobileUI={mobileUI} mobileUI={mobileUI}
noResultsText={noResultsText} 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 <Spinner

View File

@ -3,7 +3,13 @@ import {
Renderer, Renderer,
RendererProps, RendererProps,
CustomStyle, CustomStyle,
setThemeClassName setThemeClassName,
ActionObject,
IScopedContext,
ScopedContext,
createObject,
resolveVariableAndFilter,
isPureVariable
} from 'amis-core'; } from 'amis-core';
import {filter} from 'amis-core'; import {filter} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core'; import {themeable, ThemeProps} from 'amis-core';
@ -398,6 +404,8 @@ export interface ImageFieldProps extends RendererProps {
enlargeWithGallary?: boolean; enlargeWithGallary?: boolean;
showToolbar?: boolean; showToolbar?: boolean;
toolbarActions?: ImageAction[]; toolbarActions?: ImageAction[];
maxScale?: number;
minScale?: number;
onImageEnlarge?: ( onImageEnlarge?: (
info: { info: {
src: string; src: string;
@ -414,9 +422,21 @@ export interface ImageFieldProps extends RendererProps {
target: any target: any
) => void; ) => void;
imageGallaryClassName?: string; 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< static defaultProps: Pick<
ImageFieldProps, ImageFieldProps,
'defaultImage' | 'thumbMode' | 'thumbRatio' 'defaultImage' | 'thumbMode' | 'thumbRatio'
@ -426,6 +446,10 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
thumbRatio: '1:1' thumbRatio: '1:1'
}; };
state: ImageFieldState = {
scale: 1
};
@autobind @autobind
handleEnlarge({ handleEnlarge({
src, src,
@ -468,13 +492,73 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
} }
@autobind @autobind
handleClick(e: React.MouseEvent<HTMLElement>) { async handleClick(e: React.MouseEvent<HTMLElement>) {
const {dispatchEvent, data} = this.props;
const clickAction = this.props.clickAction; const clickAction = this.props.clickAction;
const rendererEvent = await dispatchEvent(
e,
createObject(data, {
nativeEvent: e
})
);
if (rendererEvent?.prevented) {
return;
}
if (clickAction) { if (clickAction) {
handleAction(e, clickAction, this.props); 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() { render() {
const { const {
className, className,
@ -510,6 +594,7 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
defaultImage && !value defaultImage && !value
? filter(defaultImage, data, '| raw') ? filter(defaultImage, data, '| raw')
: imagePlaceholder; : imagePlaceholder;
return ( return (
<div <div
className={cx( className={cx(
@ -520,8 +605,10 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
className, className,
setThemeClassName('wrapperCustomStyle', id, wrapperCustomStyle) setThemeClassName('wrapperCustomStyle', id, wrapperCustomStyle)
)} )}
style={style} style={{...style, transform: `scale(${this.state.scale})`}}
onClick={this.handleClick} onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
> >
{value || (!value && !placeholder) ? ( {value || (!value && !placeholder) ? (
<ThemedImageThumb <ThemedImageThumb
@ -537,7 +624,7 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
thumbMode={thumbMode} thumbMode={thumbMode}
thumbRatio={thumbRatio} thumbRatio={thumbRatio}
originalSrc={filter(originalSrc, data, '| raw') ?? value} originalSrc={filter(originalSrc, data, '| raw') ?? value}
enlargeAble={enlargeAble && value !== defaultValue} enlargeAble={enlargeAble && value && value !== defaultValue}
onEnlarge={this.handleEnlarge} onEnlarge={this.handleEnlarge}
imageMode={imageMode} imageMode={imageMode}
imageControlClassName={setThemeClassName( imageControlClassName={setThemeClassName(
@ -597,4 +684,26 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
@Renderer({ @Renderer({
type: 'image' 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 { import {
Renderer, Renderer,
RendererProps, RendererProps,
autobind,
createObject,
isPureVariable, isPureVariable,
resolveEventData,
resolveVariableAndFilter resolveVariableAndFilter
} from 'amis-core'; } from 'amis-core';
import {BaseSchema} from '../Schema'; import {BaseSchema} from '../Schema';
@ -103,11 +106,31 @@ export default class Pagination extends React.Component<PaginationProps> {
return result ?? defaultValue; 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() { render() {
const {maxButtons, activePage, total, perPage} = this.props; const {maxButtons, activePage, total, perPage} = this.props;
return ( return (
<BasicPagination <BasicPagination
{...this.props} {...this.props}
onPageChange={this.onPageChange}
maxButtons={this.formatNumber(maxButtons)} maxButtons={this.formatNumber(maxButtons)}
activePage={this.formatNumber(activePage)} activePage={this.formatNumber(activePage)}
total={this.formatNumber(total)} total={this.formatNumber(total)}

View File

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

View File

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

View File

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

View File

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