mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:48:13 +08:00
feat:transfer组件功能完善 (#3696)
* feat:transfer组件功能完善 * update-snapshot * feat:transfer组件功能完善 * update-snapshot * feat:transfer组件功能完善 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * update-snapshot * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * update-snapshot Co-authored-by: sqzhou <zhoushengqiang01@baidu.com>
This commit is contained in:
parent
8829f6346f
commit
e22e2f80ce
@ -959,40 +959,11 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
||||
style="position: relative;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="cxd-ResultBox-value"
|
||||
<span
|
||||
class="cxd-ResultBox-placeholder"
|
||||
>
|
||||
<span
|
||||
class="cxd-ResultBox-valueLabel"
|
||||
>
|
||||
诸葛亮
|
||||
</span>
|
||||
<a
|
||||
data-index="0"
|
||||
>
|
||||
<icon-mock
|
||||
classname="icon icon-close"
|
||||
icon="close"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-ResultBox-value"
|
||||
>
|
||||
<span
|
||||
class="cxd-ResultBox-valueLabel"
|
||||
>
|
||||
李白
|
||||
</span>
|
||||
<a
|
||||
data-index="1"
|
||||
>
|
||||
<icon-mock
|
||||
classname="icon icon-close"
|
||||
icon="close"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
请选择
|
||||
</span>
|
||||
<span
|
||||
class="cxd-TransferDropDown-icon"
|
||||
>
|
||||
@ -1067,7 +1038,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
||||
class="cxd-Table-checkCell"
|
||||
>
|
||||
<label
|
||||
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--sm"
|
||||
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -1088,7 +1059,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="is-active"
|
||||
class=""
|
||||
>
|
||||
<td
|
||||
class="cxd-Table-checkCell"
|
||||
@ -1175,7 +1146,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="is-active"
|
||||
class=""
|
||||
>
|
||||
<td
|
||||
class="cxd-Table-checkCell"
|
||||
@ -1369,11 +1340,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Renderer:select table with labelField & valueField 2`] = `
|
||||
Object {
|
||||
"a": "zhugeliang,libai",
|
||||
}
|
||||
`;
|
||||
exports[`Renderer:select table with labelField & valueField 2`] = `Object {}`;
|
||||
|
||||
exports[`Renderer:select tree 1`] = `
|
||||
<div>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -179,6 +179,7 @@ icon:
|
||||
"type": "transfer",
|
||||
"name": "transfer4",
|
||||
"selectMode": "tree",
|
||||
"searchable": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
@ -283,116 +284,6 @@ icon:
|
||||
}
|
||||
```
|
||||
|
||||
### 支持搜索
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "带搜索",
|
||||
"type": "transfer",
|
||||
"name": "transfer6",
|
||||
"selectMode": "chained",
|
||||
"searchable": true,
|
||||
"sortable": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
"children": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战士",
|
||||
"children": [
|
||||
{
|
||||
"label": "曹操",
|
||||
"value": "caocao"
|
||||
},
|
||||
{
|
||||
"label": "钟无艳",
|
||||
"value": "zhongwuyan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "打野",
|
||||
"children": [
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 延时加载
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "延时加载",
|
||||
"type": "transfer",
|
||||
"name": "transfer7",
|
||||
"selectMode": "tree",
|
||||
"deferApi": "/api/mock2/form/deferOptions?label=${label}&waitSeconds=2",
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
"children": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战士",
|
||||
"defer": true
|
||||
},
|
||||
{
|
||||
"label": "打野",
|
||||
"children": [
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 关联选择模式
|
||||
|
||||
```schema: scope="body"
|
||||
@ -506,7 +397,7 @@ icon:
|
||||
}
|
||||
```
|
||||
|
||||
leftOptions 动态加载,默认 source 接口是返回 options 部分,而 leftOptions 是没有对应的接口可以动态返回了。为了方便,目前如果 source 接口返回的选中中,第一个 option 是以下这种格式则也会把 options[0].leftOptions 当成 leftOptions, options[0].children 当 options。同时 options[0].leftDefaultValue 可以用来配置左侧选项的默认值。
|
||||
`leftOptions` 动态加载,默认 `source` 接口是返回 options 部分,而 `leftOptions` 是没有对应的接口可以动态返回了。为了方便,目前如果 `source` 接口返回的选中中,第一个 option 是以下这种格式则也会把 options[0].leftOptions 当成 `leftOptions`, options[0].children 当 options。同时 options[0].leftDefaultValue 可以用来配置左侧选项的默认值。
|
||||
|
||||
```
|
||||
{
|
||||
@ -524,13 +415,200 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
|
||||
}
|
||||
```
|
||||
|
||||
## searchApi
|
||||
|
||||
**发送**
|
||||
### 延时加载
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "延时加载",
|
||||
"type": "transfer",
|
||||
"name": "transfer7",
|
||||
"selectMode": "tree",
|
||||
"deferApi": "/api/mock2/form/deferOptions?label=${label}&waitSeconds=2",
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
"children": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战士",
|
||||
"defer": true
|
||||
},
|
||||
{
|
||||
"label": "打野",
|
||||
"children": [
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 支持搜索
|
||||
#### 左侧搜索功能
|
||||
通过 `searchable` 字段来控制左侧选项栏的搜索功能。
|
||||
|
||||
在不设置 `searchApi` 情况下,对输入框内容和对应列表项的value、label进行匹配,匹配成功就会左侧面板中显示。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "带搜索",
|
||||
"type": "transfer",
|
||||
"name": "transfer6",
|
||||
"selectMode": "chained",
|
||||
"searchable": true,
|
||||
"sortable": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
"children": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战士",
|
||||
"children": [
|
||||
{
|
||||
"label": "曹操",
|
||||
"value": "caocao"
|
||||
},
|
||||
{
|
||||
"label": "钟无艳",
|
||||
"value": "zhongwuyan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "打野",
|
||||
"children": [
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 右侧结果搜索功能
|
||||
|
||||
右侧结果搜索是通过`resultSearchable`字段开启,设置该字段为true时开启。
|
||||
|
||||
开启结果搜索后,目前默认通过value、label对输入内容进行模糊匹配。
|
||||
|
||||
目前树的延时加载不支持结果搜索功能。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "page",
|
||||
"body": {
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "树型展示",
|
||||
"type": "transfer",
|
||||
"name": "transfer4",
|
||||
"selectMode": "tree",
|
||||
"searchable": true,
|
||||
"resultListModeFollowSelect": true,
|
||||
"resultSearchable": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
"children": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战士",
|
||||
"children": [
|
||||
{
|
||||
"label": "曹操",
|
||||
"value": "caocao"
|
||||
},
|
||||
{
|
||||
"label": "钟无艳",
|
||||
"value": "zhongwuyan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "打野",
|
||||
"children": [
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### searchApi
|
||||
|
||||
设置这个api,可以实现左侧选项搜索结果的检索。
|
||||
|
||||
##### 发送
|
||||
|
||||
默认 GET,携带 term 变量,值为搜索框输入的文字,可从上下文中取数据设置进去。
|
||||
|
||||
**响应**
|
||||
##### 响应
|
||||
|
||||
格式要求如下:
|
||||
|
||||
@ -559,7 +637,132 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
|
||||
|
||||
适用于需选择的数据/信息源较多时,用户可直观的知道自己所选择的数据/信息的场景,一般左侧框为数据/信息源,右侧为已选数据/信息,被选中信息同时存在于 2 个框内。
|
||||
|
||||
## 自定义选项展示
|
||||
### 结果面板跟随模式
|
||||
|
||||
`resultListModeFollowSelect` 开启结果面板跟随模式。
|
||||
|
||||
#### 表格跟随模式
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "表格形式",
|
||||
"type": "transfer",
|
||||
"name": "transfer",
|
||||
"selectMode": "table",
|
||||
"resultListModeFollowSelect": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "label",
|
||||
"label": "英雄"
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"label": "位置"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang",
|
||||
"position": "中单"
|
||||
},
|
||||
{
|
||||
"label": "曹操",
|
||||
"value": "caocao",
|
||||
"position": "上单"
|
||||
},
|
||||
{
|
||||
"label": "钟无艳",
|
||||
"value": "zhongwuyan",
|
||||
"position": "上单"
|
||||
},
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai",
|
||||
"position": "打野"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin",
|
||||
"position": "打野"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun",
|
||||
"position": "打野"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 树形跟随模式
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "page",
|
||||
"body": {
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"label": "树型展示",
|
||||
"type": "transfer",
|
||||
"name": "transfer4",
|
||||
"selectMode": "tree",
|
||||
"searchable": true,
|
||||
"resultListModeFollowSelect": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "法师",
|
||||
"children": [
|
||||
{
|
||||
"label": "诸葛亮",
|
||||
"value": "zhugeliang"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战士",
|
||||
"children": [
|
||||
{
|
||||
"label": "曹操",
|
||||
"value": "caocao"
|
||||
},
|
||||
{
|
||||
"label": "钟无艳",
|
||||
"value": "zhongwuyan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "打野",
|
||||
"children": [
|
||||
{
|
||||
"label": "李白",
|
||||
"value": "libai"
|
||||
},
|
||||
{
|
||||
"label": "韩信",
|
||||
"value": "hanxin"
|
||||
},
|
||||
{
|
||||
"label": "云中君",
|
||||
"value": "yunzhongjun"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义选项展示
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
@ -612,27 +815,31 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
|
||||
|
||||
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ---------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
|
||||
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
|
||||
| delimiter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
|
||||
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
|
||||
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
|
||||
| searchable | `boolean` | `false` | 当设置为 `true` 时表示可以通过输入部分内容检索出选项。 |
|
||||
| searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索,可以设置个 api。 |
|
||||
| statistics | `boolean` | `true` | 是否显示统计数据 |
|
||||
| selectTitle | `string` | `"请选择"` | 左侧的标题文字 |
|
||||
| resultTitle | `string` | `"当前选择"` | 右侧结果的标题文字 |
|
||||
| sortable | `boolean` | `false` | 结果可以进行拖拽排序 |
|
||||
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
|
||||
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
|
||||
| delimeter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
|
||||
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
|
||||
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
|
||||
| searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索,可以设置这个api。|
|
||||
| resultListModeFollowSelect | `boolean` | `false` | 结果面板跟随模式,目前只支持`list`、`table`、`tree`(tree目前只支持非延时加载的`tree`) |
|
||||
| statistics | `boolean` | `true` | 是否显示统计数据|
|
||||
| selectTitle | `string` | `"请选择"` | 左侧的标题文字 |
|
||||
| resultTitle | `string` | `"当前选择"` | 右侧结果的标题文字|
|
||||
| sortable | `boolean` | `false` | 结果可以进行拖拽排序(结果列表为树时,不支持排序)|
|
||||
| selectMode | `string` | `list` | 可选:`list`、`table`、`tree`、`chained`、`associated`。分别为:列表形式、表格形式、树形选择形式、级联选择形式,关联选择形式(与级联选择的区别在于,级联是无限极,而关联只有一级,关联左边可以是个 tree)。 |
|
||||
| searchResultMode | `string` | | 如果不设置将采用 `selectMode` 的值,可以单独配置,参考 `selectMode`,决定搜索结果的展示形式。 |
|
||||
| columns | `Array<Object>` | | 当展示形式为 `table` 可以用来配置展示哪些列,跟 table 中的 columns 配置相似,只是只有展示功能。 |
|
||||
| leftOptions | `Array<Object>` | | 当展示形式为 `associated` 时用来配置左边的选项集。 |
|
||||
| leftMode | `string` | | 当展示形式为 `associated` 时用来配置左边的选择形式,支持 `list` 或者 `tree`。默认为 `list`。 |
|
||||
| rightMode | `string` | | 当展示形式为 `associated` 时用来配置右边的选择形式,可选:`list`、`table`、`tree`、`chained`。 |
|
||||
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 |
|
||||
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
|
||||
| searchResultMode | `string` | | 如果不设置将采用 `selectMode` 的值,可以单独配置,参考 `selectMode`,决定搜索结果的展示形式。 |
|
||||
| searchable | `boolean` | `false` | 左侧列表搜索功能,当设置为 true 时表示可以通过输入部分内容检索出选项项。 |
|
||||
| searchPlaceholder | `string` | | 左侧列表搜索框提示 |
|
||||
| columns | `Array<Object>` | | 当展示形式为 `table` 可以用来配置展示哪些列,跟 table 中的 columns 配置相似,只是只有展示功能。 |
|
||||
| leftOptions | `Array<Object>` | | 当展示形式为 `associated` 时用来配置左边的选项集。|
|
||||
| leftMode | `string` | | 当展示形式为 `associated` 时用来配置左边的选择形式,支持 `list` 或者 `tree`。默认为 `list`。|
|
||||
| rightMode | `string` | | 当展示形式为 `associated` 时用来配置右边的选择形式,可选:`list`、`table`、`tree`、`chained`。 |
|
||||
| resultSearchable | `boolean` | `false` | 结果(右则)列表的检索功能,当设置为true时,可以通过输入检索模糊匹配检索内容(目前树的延时加载不支持结果搜索功能) |
|
||||
| resultSearchPlaceholder | `string` | | 右侧列表搜索框提示 |
|
||||
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 |
|
||||
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
|
||||
|
||||
## 事件表
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
color: var(--Form-input-placeholderColor);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-basis: var(--Form-input-height);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
@ -69,7 +69,7 @@
|
||||
flex-direction: row;
|
||||
|
||||
> .#{$ns}Checkbox {
|
||||
margin-right: px2rem(8px);
|
||||
margin-right: px2rem(10px);
|
||||
}
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@ -104,6 +104,10 @@
|
||||
|
||||
&-placeholder {
|
||||
@include checkboxes-placeholder();
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,10 +139,6 @@
|
||||
padding-right: var(--gap-md);
|
||||
}
|
||||
|
||||
.#{$ns}Table-table > tbody > tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.#{$ns}Table-table > tbody > tr.is-active {
|
||||
color: var(--Form-select-menu-onActive-color);
|
||||
background: var(--Form-select-menu-onActive-bg);
|
||||
@ -148,24 +148,13 @@
|
||||
.#{$ns}TreeSelection {
|
||||
.#{$ns}Table-expandBtn {
|
||||
color: var(--icon-color);
|
||||
margin-right: 5px;
|
||||
margin-right: var(--gap-xs);
|
||||
}
|
||||
|
||||
&-sublist {
|
||||
position: relative;
|
||||
margin: 0 0 0 px2rem(35px);
|
||||
display: none;
|
||||
|
||||
&:before {
|
||||
width: 1px;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--gap-xs) * -1);
|
||||
bottom: calc(var(--Form-input-height) / 2);
|
||||
left: -19px;
|
||||
border-left: dashed 1px var(--icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
@ -183,35 +172,17 @@
|
||||
color: var(--text--muted-color);
|
||||
}
|
||||
|
||||
&-sublist &-item:before {
|
||||
height: 1px;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--Form-input-height) / 2);
|
||||
width: 19px;
|
||||
left: -19px;
|
||||
border-top: dashed 1px var(--icon-color);
|
||||
}
|
||||
|
||||
&-itemInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// height: var(--Form-input-height);
|
||||
line-height: var(--Form-input-lineHeight);
|
||||
line-height: var(--Tree-itemHeight);
|
||||
position: relative;
|
||||
font-size: var(--Form-input-fontSize);
|
||||
padding: calc(
|
||||
(
|
||||
var(--Form-input-height) - var(--Form-input-lineHeight) *
|
||||
var(--Form-input-fontSize)
|
||||
) / 2
|
||||
)
|
||||
var(--gap-sm);
|
||||
padding: 0 var(--gap-sm);
|
||||
flex-direction: row;
|
||||
|
||||
> .#{$ns}Checkbox {
|
||||
margin-right: 0;
|
||||
margin-left: var(--gap-sm);
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@ -234,7 +205,6 @@
|
||||
flex-grow: 1;
|
||||
|
||||
span {
|
||||
margin-left: var(--gap-xs);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@ -303,7 +273,7 @@
|
||||
flex-grow: 1;
|
||||
|
||||
span {
|
||||
margin-left: var(--gap-xs);
|
||||
margin-left: px2rem(10px);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@ -358,3 +328,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}ResultTreeList {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.#{$ns}ResultTableList {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&-close-btn {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
@ -5,6 +5,10 @@
|
||||
min-height: px2rem(300px);
|
||||
position: relative;
|
||||
|
||||
&-searchbox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--inline {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
@ -70,6 +74,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}-ResultTreeList {
|
||||
border-top: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.#{$ns}AssociatedSelection {
|
||||
overflow: hidden;
|
||||
|
||||
@ -79,6 +87,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-select {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-search + &-selection {
|
||||
border-top: 1px solid var(--borderColor);
|
||||
}
|
||||
@ -129,6 +141,14 @@
|
||||
color: var(--text--muted-color);
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}Tree {
|
||||
padding: px2rem(2px) px2rem(10px);
|
||||
|
||||
&-itemLabel:hover::after {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}TabsTransfer {
|
||||
@ -230,6 +250,7 @@
|
||||
|
||||
.#{$ns}TransferControl {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-inline {
|
||||
display: inline-block;
|
||||
|
@ -155,16 +155,65 @@
|
||||
|
||||
.#{$ns}Transfer {
|
||||
&-title {
|
||||
height: var(--Form-input-height);
|
||||
padding-left: #{px2rem(16px)};
|
||||
padding-right: #{px2rem(16px)};
|
||||
flex: 0 0 #{px2rem(38px)};
|
||||
height: #{px2rem(38px)};
|
||||
padding-left: var(--gap-base);
|
||||
padding-right: var(--gap-base);
|
||||
}
|
||||
|
||||
&-result {
|
||||
|
||||
>.#{$ns}Selections {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-table-title {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.#{$ns}ListCheckboxes {
|
||||
.#{$ns}ListCheckboxes-item {
|
||||
padding-left: #{px2rem(16px)};
|
||||
padding-right: #{px2rem(16px)};
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}TableSelection {
|
||||
.#{$ns}Table-table {
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&.is-active {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}GroupedSelection {
|
||||
&-item {
|
||||
padding-left: var(--gap-base);
|
||||
padding-right: var(--gap-base);
|
||||
|
||||
&.is-active {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}Selections {
|
||||
height: auto;
|
||||
|
||||
&-item {
|
||||
padding-left: var(--gap-base);
|
||||
padding-right: var(--gap-base);
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}Modal {
|
||||
|
@ -12,11 +12,11 @@ import {themeable} from '../theme';
|
||||
import {uncontrollable} from 'uncontrollable';
|
||||
import GroupedSelection from './GroupedSelection';
|
||||
import TableSelection from './TableSelection';
|
||||
import TreeSelection from './TreeSelection';
|
||||
import GroupedSelecton from './GroupedSelection';
|
||||
import ChainedSelection from './ChainedSelection';
|
||||
import {Icon} from './icons';
|
||||
import {localeable} from '../locale';
|
||||
import Tree from './Tree';
|
||||
|
||||
export interface AssociatedSelectionProps extends BaseSelectionProps {
|
||||
leftOptions: Options;
|
||||
@ -44,6 +44,7 @@ export class AssociatedSelection extends BaseSelection<
|
||||
AssociatedSelectionProps,
|
||||
AssociatedSelectionState
|
||||
> {
|
||||
|
||||
state: AssociatedSelectionState = {
|
||||
leftValue: this.props.leftDefaultValue
|
||||
};
|
||||
@ -133,14 +134,12 @@ export class AssociatedSelection extends BaseSelection<
|
||||
<div className={cx('AssociatedSelection', className)}>
|
||||
<div className={cx('AssociatedSelection-left')}>
|
||||
{leftMode === 'tree' ? (
|
||||
<TreeSelection
|
||||
option2value={this.leftOption2Value}
|
||||
options={leftOptions}
|
||||
value={this.state.leftValue}
|
||||
disabled={disabled}
|
||||
onChange={this.handleLeftSelect}
|
||||
<Tree
|
||||
multiple={false}
|
||||
clearable={false}
|
||||
disabled={disabled}
|
||||
value={this.state.leftValue}
|
||||
options={leftOptions}
|
||||
onChange={this.handleLeftSelect}
|
||||
onDeferLoad={this.handleLeftDeferLoad}
|
||||
/>
|
||||
) : (
|
||||
@ -192,14 +191,12 @@ export class AssociatedSelection extends BaseSelection<
|
||||
multiple={multiple}
|
||||
/>
|
||||
) : rightMode === 'tree' ? (
|
||||
<TreeSelection
|
||||
<Tree
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
options={selectdOption.children || []}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onChange={onChange!}
|
||||
multiple={multiple}
|
||||
itemRender={itemRender}
|
||||
/>
|
||||
) : rightMode === 'chained' ? (
|
||||
<ChainedSelection
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {BaseSelection} from './Selection';
|
||||
import {themeable} from '../theme';
|
||||
import React from 'react';
|
||||
import {uncontrollable} from 'uncontrollable';
|
||||
|
||||
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||
import {themeable} from '../theme';
|
||||
import Checkbox from './Checkbox';
|
||||
import {Option} from './Select';
|
||||
import {localeable} from '../locale';
|
||||
|
||||
export class GroupedSelection extends BaseSelection {
|
||||
export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
|
||||
valueArray: Array<Option>;
|
||||
|
||||
renderOption(option: Option, index: number) {
|
||||
@ -18,6 +19,7 @@ export class GroupedSelection extends BaseSelection {
|
||||
itemRender,
|
||||
multiple
|
||||
} = this.props;
|
||||
|
||||
const valueArray = this.valueArray;
|
||||
|
||||
if (Array.isArray(option.children)) {
|
||||
|
@ -2,15 +2,21 @@
|
||||
* 用来显示选择结果,垂直显示。支持移出、排序等操作。
|
||||
*/
|
||||
import React from 'react';
|
||||
import {Option} from './Select';
|
||||
import Sortable from 'sortablejs';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
|
||||
import {Option, Options} from './Select';
|
||||
import {ThemeProps, themeable} from '../theme';
|
||||
import {Icon} from './icons';
|
||||
import {autobind, guid} from '../utils/helper';
|
||||
import Sortable from 'sortablejs';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||
import TransferSearch from './TransferSearch';
|
||||
|
||||
export interface ResultListProps extends ThemeProps, LocaleProps {
|
||||
export interface ResultListProps
|
||||
extends ThemeProps,
|
||||
LocaleProps,
|
||||
BaseSelectionProps {
|
||||
className?: string;
|
||||
value?: Array<Option>;
|
||||
onChange?: (value: Array<Option>, optionModified?: boolean) => void;
|
||||
@ -20,6 +26,23 @@ export interface ResultListProps extends ThemeProps, LocaleProps {
|
||||
placeholder: string;
|
||||
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||
itemClassName?: string;
|
||||
columns: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
}>;
|
||||
cellRender?: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
searchable?: boolean;
|
||||
onSearch?: Function;
|
||||
}
|
||||
|
||||
export interface ItemRenderStates {
|
||||
@ -28,17 +51,35 @@ export interface ItemRenderStates {
|
||||
onChange: (value: any, name: string) => void;
|
||||
}
|
||||
|
||||
export class ResultList extends React.Component<ResultListProps> {
|
||||
interface ResultListState {
|
||||
searchResult: Options | null;
|
||||
}
|
||||
|
||||
export class ResultList extends React.Component<
|
||||
ResultListProps,
|
||||
ResultListState
|
||||
> {
|
||||
|
||||
static itemRender(option: any) {
|
||||
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
||||
}
|
||||
static defaultProps: Pick<ResultListProps, 'placeholder' | 'itemRender'> = {
|
||||
|
||||
static defaultProps: Pick<
|
||||
ResultListProps,
|
||||
'placeholder' | 'itemRender'
|
||||
> = {
|
||||
placeholder: 'placeholder.selectData',
|
||||
itemRender: ResultList.itemRender
|
||||
};
|
||||
|
||||
state: ResultListState = {
|
||||
searchResult: null
|
||||
};
|
||||
|
||||
cancelSearch?: () => void;
|
||||
id = guid();
|
||||
sortable?: Sortable;
|
||||
unmounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.sortable && this.initSortable();
|
||||
@ -54,20 +95,7 @@ export class ResultList extends React.Component<ResultListProps> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.desposeSortable();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleRemove(e: React.MouseEvent<HTMLElement>) {
|
||||
const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10);
|
||||
const {value, onChange} = this.props;
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = value.concat();
|
||||
newValue.splice(index, 1);
|
||||
onChange?.(newValue);
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
@ -138,24 +166,65 @@ export class ResultList extends React.Component<ResultListProps> {
|
||||
onChange?.(result, true);
|
||||
}
|
||||
|
||||
render() {
|
||||
@autobind
|
||||
search(inputValue: string) {
|
||||
const {onSearch, value} = this.props;
|
||||
const searchResult = (value || []).filter(
|
||||
item => onSearch && onSearch(inputValue, item)
|
||||
);
|
||||
this.setState({searchResult})
|
||||
}
|
||||
|
||||
@autobind
|
||||
clearSearch() {
|
||||
this.setState({searchResult: null})
|
||||
}
|
||||
|
||||
// 关闭表格最后一项
|
||||
@autobind
|
||||
handleCloseItem(option: Option) {
|
||||
const {value, onChange, option2value, options, disabled} = this.props;
|
||||
|
||||
if (disabled || option.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除普通值
|
||||
let valueArray = BaseSelection.value2array(value, options, option2value);
|
||||
|
||||
let idx = valueArray.indexOf(option);
|
||||
valueArray.splice(idx, 1);
|
||||
let newValue: string | Array<Option> = option2value
|
||||
? valueArray.map(item => option2value(item))
|
||||
: valueArray;
|
||||
onChange && onChange(newValue);
|
||||
|
||||
const {searchResult} = this.state;
|
||||
if (searchResult) {
|
||||
const searchArray = BaseSelection.value2array(
|
||||
searchResult,
|
||||
options,
|
||||
option2value
|
||||
);
|
||||
const searchIdx = searchArray.indexOf(option);
|
||||
searchResult.splice(searchIdx, 1);
|
||||
this.setState({searchResult});
|
||||
}
|
||||
}
|
||||
|
||||
renderNormalList(value?: Options) {
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
value,
|
||||
placeholder,
|
||||
itemRender,
|
||||
disabled,
|
||||
title,
|
||||
itemClassName,
|
||||
sortable,
|
||||
translate: __
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('Selections', className)}>
|
||||
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||
|
||||
<>
|
||||
{Array.isArray(value) && value.length ? (
|
||||
<div className={cx('Selections-items')}>
|
||||
{value.map((option, index) => (
|
||||
@ -186,7 +255,9 @@ export class ResultList extends React.Component<ResultListProps> {
|
||||
<a
|
||||
className={cx('Selections-delBtn')}
|
||||
data-index={index}
|
||||
onClick={this.handleRemove}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) =>
|
||||
this.handleCloseItem(option)
|
||||
}
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
@ -194,9 +265,36 @@ export class ResultList extends React.Component<ResultListProps> {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>
|
||||
)}
|
||||
) :
|
||||
(<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
title,
|
||||
searchable,
|
||||
value,
|
||||
translate: __,
|
||||
placeholder = __('Transfer.searchKeyword')
|
||||
} = this.props;
|
||||
|
||||
const {searchResult} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('Selections', className)}>
|
||||
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||
{searchable ? (
|
||||
<TransferSearch
|
||||
placeholder={placeholder}
|
||||
onSearch={this.search}
|
||||
onCancelSearch={this.clearSearch}
|
||||
/>
|
||||
) : null}
|
||||
{this.renderNormalList(searchResult !== null ? searchResult : value)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
231
src/components/ResultTableList.tsx
Normal file
231
src/components/ResultTableList.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 结果表格(暂时不支持结果排序)
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||
import {themeable} from '../theme';
|
||||
import {Option, Options} from './Select';
|
||||
import {resolveVariable} from '../utils/tpl-builtin';
|
||||
import {localeable} from '../locale';
|
||||
import {autobind} from '../utils/helper';
|
||||
import TransferSearch from './TransferSearch';
|
||||
|
||||
import {CloseIcon} from './icons';
|
||||
import TableSelection from './TableSelection';
|
||||
|
||||
export interface ResultTableSelectionProps extends BaseSelectionProps {
|
||||
title?: string;
|
||||
placeholder?: string;
|
||||
searchable?: boolean;
|
||||
onSearch?: Function;
|
||||
columns: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
}>;
|
||||
cellRender: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface ResultTableSelectionState {
|
||||
tableOptions: Options;
|
||||
searching: Boolean;
|
||||
searchTableOptions: Options;
|
||||
}
|
||||
|
||||
export class BaseResultTableSelection extends BaseSelection<ResultTableSelectionProps, ResultTableSelectionState> {
|
||||
static defaultProps = {
|
||||
...BaseSelection.defaultProps,
|
||||
cellRender: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => <span>{resolveVariable(column.name, option)}</span>,
|
||||
};
|
||||
|
||||
state: ResultTableSelectionState = {
|
||||
tableOptions: [],
|
||||
searching: false,
|
||||
searchTableOptions: []
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: ResultTableSelectionProps) {
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
option2value,
|
||||
} = props;
|
||||
const valueArray = BaseSelection.value2array(value, options, option2value);
|
||||
return {
|
||||
tableOptions: valueArray
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleCloseItem(option: Option) {
|
||||
const {value, onChange, option2value, options, disabled} = this.props;
|
||||
|
||||
const {searching, searchTableOptions} = this.state;
|
||||
|
||||
if (disabled || option.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除普通值
|
||||
let valueArray = BaseSelection.value2array(value, options, option2value);
|
||||
|
||||
let idx = valueArray.indexOf(option);
|
||||
valueArray.splice(idx, 1);
|
||||
let newValue: string | Array<Option> = option2value
|
||||
? valueArray.map(item => option2value(item))
|
||||
: valueArray;
|
||||
onChange && onChange(newValue);
|
||||
|
||||
if (searching) {
|
||||
const searchArray = BaseSelection.value2array(
|
||||
searchTableOptions,
|
||||
options,
|
||||
option2value
|
||||
);
|
||||
const searchIdx = searchArray.indexOf(option);
|
||||
searchTableOptions.splice(searchIdx, 1);
|
||||
this.setState({searchTableOptions});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
search(inputValue: string) {
|
||||
// 结果为空,直接清空
|
||||
if (!inputValue) {
|
||||
this.clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const {value, onSearch} = this.props;
|
||||
const searchOptions = ((value || []) as Options)
|
||||
.filter(item => onSearch?.(inputValue, item));
|
||||
|
||||
this.setState({
|
||||
searching: true,
|
||||
searchTableOptions: searchOptions
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
clearSearch() {
|
||||
this.setState({
|
||||
searching: false,
|
||||
searchTableOptions: []
|
||||
});
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
columns,
|
||||
cellRender,
|
||||
value,
|
||||
disabled,
|
||||
option2value,
|
||||
onChange,
|
||||
translate: __,
|
||||
placeholder
|
||||
} = this.props;
|
||||
|
||||
const {searching, tableOptions, searchTableOptions} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('ResultTableList', className)}>
|
||||
{
|
||||
Array.isArray(value) && value.length ?
|
||||
(
|
||||
<TableSelection
|
||||
columns={columns}
|
||||
options={!searching ? tableOptions : searchTableOptions}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
option2value={option2value}
|
||||
onChange={onChange}
|
||||
multiple={false}
|
||||
cellRender={(
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => {
|
||||
const raw = cellRender(column, option, colIndex, rowIndex);
|
||||
if (colIndex === columns.length - 1) {
|
||||
return (
|
||||
<>
|
||||
{raw}
|
||||
{
|
||||
<span
|
||||
className={cx('ResultTableList-close-btn')}
|
||||
onClick={(e: React.SyntheticEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
this.handleCloseItem(option);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</span>
|
||||
}
|
||||
</>)
|
||||
}
|
||||
return raw;
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
title,
|
||||
searchable,
|
||||
translate: __,
|
||||
placeholder = __('Transfer.searchKeyword')
|
||||
} = this.props;
|
||||
|
||||
|
||||
return (
|
||||
<div className={cx('Selections', className)}>
|
||||
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||
{searchable ? (
|
||||
<TransferSearch
|
||||
placeholder={placeholder}
|
||||
onSearch={this.search}
|
||||
onCancelSearch={this.clearSearch}
|
||||
/>
|
||||
) : null}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(localeable(BaseResultTableSelection));
|
298
src/components/ResultTreeList.tsx
Normal file
298
src/components/ResultTreeList.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 结果树(暂时不支持结果排序)
|
||||
*/
|
||||
import React from 'react';
|
||||
import {cloneDeep, isEqual, omit} from 'lodash';
|
||||
|
||||
import {Option, Options} from './Select';
|
||||
import {ThemeProps, themeable} from '../theme';
|
||||
import {autobind, noop} from '../utils/helper';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import {BaseSelectionProps} from './Selection';
|
||||
import Tree from './Tree';
|
||||
import TransferSearch from './TransferSearch';
|
||||
|
||||
export interface ResultTreeListProps extends ThemeProps, LocaleProps, BaseSelectionProps {
|
||||
className?: string;
|
||||
title?: string;
|
||||
searchable?: boolean;
|
||||
value: Array<Option>;
|
||||
valueField: string;
|
||||
onSearch?: Function;
|
||||
onChange: (value: Array<Option>, optionModified?: boolean) => void;
|
||||
placeholder: string;
|
||||
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||
itemClassName?: string;
|
||||
cellRender?: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface ItemRenderStates {
|
||||
index: number;
|
||||
disabled?: boolean;
|
||||
onChange: (value: any, name: string) => void;
|
||||
}
|
||||
|
||||
interface ResultTreeListState {
|
||||
searching: Boolean;
|
||||
treeOptions: Options;
|
||||
searchTreeOptions: Options;
|
||||
}
|
||||
|
||||
// 递归找到对应选中节点(dfs)
|
||||
function getDeep(
|
||||
node: Option,
|
||||
cb: (node: Option) => boolean,
|
||||
pathNodes: Array<Option>,
|
||||
valueField: string
|
||||
) {
|
||||
if (node[valueField] && cb(node)) {
|
||||
node.isChecked = true;
|
||||
for (let i = pathNodes.length - 2; i >= 0; i--) {
|
||||
if (!pathNodes[i].isChecked) {
|
||||
pathNodes[i].isChecked = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (node.children && Array.isArray(node.children)) {
|
||||
node.children.forEach(n => {
|
||||
pathNodes.push(n);
|
||||
getDeep(n, cb, pathNodes, valueField);
|
||||
pathNodes.pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 递归删除树多余的节点
|
||||
function deepCheckedTreeNode(nodes: Array<Option>) {
|
||||
let arr: Array<Option> = [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node.isChecked) {
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
node.children = deepCheckedTreeNode(node.children);
|
||||
}
|
||||
arr.push(node);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// 根据选项获取到结果
|
||||
function getResultOptions(value: Options = [], options: Options, valueField: string) {
|
||||
const newOptions = cloneDeep(options) as Array<Option>;
|
||||
const callBack = (node: Option) =>
|
||||
!!(value || []).find(target => target[valueField] === node[valueField]);
|
||||
newOptions &&
|
||||
newOptions.forEach(op => {
|
||||
getDeep(op, callBack, [op], valueField);
|
||||
});
|
||||
return deepCheckedTreeNode(newOptions);
|
||||
}
|
||||
|
||||
// 在包含回调函数情况下,遍历树
|
||||
function deepTree(nodes: Options, cb: (node: Option) => void) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
cb(node);
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
deepTree(node.children, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 树的节点删除
|
||||
function deepDeleteTree(nodes: Options, option: Option, valueField: string) {
|
||||
let arr: Array<Option> = [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (isEqual(node, option)) {
|
||||
continue;
|
||||
}
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
node.children = deepDeleteTree(node.children, option, valueField);
|
||||
}
|
||||
if (
|
||||
(node.children && node.children.length > 0) ||
|
||||
node[valueField] !== undefined
|
||||
) {
|
||||
arr.push(node);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
export class BaseResultTreeList extends React.Component<ResultTreeListProps, ResultTreeListState> {
|
||||
|
||||
static itemRender(option: any) {
|
||||
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
||||
}
|
||||
|
||||
static defaultProps: Pick<ResultTreeListProps, 'placeholder' | 'itemRender'> = {
|
||||
placeholder: 'placeholder.selectData',
|
||||
itemRender: BaseResultTreeList.itemRender
|
||||
};
|
||||
|
||||
state: ResultTreeListState = {
|
||||
searching: false,
|
||||
treeOptions: [],
|
||||
searchTreeOptions: []
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: ResultTreeListProps) {
|
||||
const newOptions = getResultOptions(props.value, props.options, props.valueField);
|
||||
return {
|
||||
treeOptions: cloneDeep(newOptions)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除非选中节点
|
||||
@autobind
|
||||
deleteTreeChecked(option: Option) {
|
||||
const {value = [], onChange, valueField} = this.props;
|
||||
const {searching, treeOptions} = this.state;
|
||||
let temNode: Options = [];
|
||||
const cb = (node: Option) => {
|
||||
if (isEqual(node, option)) {
|
||||
temNode = [node];
|
||||
}
|
||||
};
|
||||
deepTree(treeOptions || [], cb);
|
||||
let arr: Options = [];
|
||||
const cb2 = (node: Option) => {
|
||||
if (node.isChecked && node[valueField]) {
|
||||
arr.push(node);
|
||||
}
|
||||
};
|
||||
deepTree(temNode, cb2);
|
||||
onChange &&
|
||||
onChange(
|
||||
value.filter(
|
||||
item =>
|
||||
!arr.find(arrItem =>
|
||||
isEqual(omit(arrItem, ['isChecked', 'childrens']), item)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// 搜索时,重新生成树
|
||||
searching && this.deleteResultTreeNode(option);
|
||||
}
|
||||
|
||||
// 搜索树点击删除时,删除对应节点
|
||||
deleteResultTreeNode(option: Option) {
|
||||
const arr = deepDeleteTree(
|
||||
cloneDeep(this.state.searchTreeOptions) || [],
|
||||
option,
|
||||
this.props.valueField
|
||||
);
|
||||
this.setState({searchTreeOptions: arr});
|
||||
}
|
||||
|
||||
@autobind
|
||||
search(inputValue: string) {
|
||||
// 结果为空,直接清空
|
||||
if (!inputValue) {
|
||||
this.clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const {valueField, onSearch} = this.props;
|
||||
let temOptions: Array<Option> = this.state.treeOptions || [];
|
||||
const cb = (node: Option) => {
|
||||
node.isChecked = false;
|
||||
return true;
|
||||
};
|
||||
deepTree(temOptions, cb);
|
||||
|
||||
const callBack = (node: Option) => onSearch?.(inputValue, node);
|
||||
|
||||
temOptions &&
|
||||
temOptions.forEach(op => {
|
||||
getDeep(op, callBack, [op], valueField);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
searching: true,
|
||||
searchTreeOptions: deepCheckedTreeNode(temOptions)
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
clearSearch() {
|
||||
this.setState({
|
||||
searching: false,
|
||||
searchTreeOptions: []
|
||||
});
|
||||
}
|
||||
|
||||
renderTree() {
|
||||
const {
|
||||
className,
|
||||
classnames: cx,
|
||||
value,
|
||||
placeholder,
|
||||
valueField,
|
||||
itemRender,
|
||||
translate: __
|
||||
} = this.props;
|
||||
|
||||
const {treeOptions, searching, searchTreeOptions} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('ResultTreeList', className)}>
|
||||
{
|
||||
Array.isArray(value) && value.length ?
|
||||
(<Tree
|
||||
className={cx('Transfer-tree')}
|
||||
options={!searching ? treeOptions : searchTreeOptions}
|
||||
valueField={valueField}
|
||||
value={[]}
|
||||
onChange={noop}
|
||||
showIcon={false}
|
||||
itemRender={itemRender}
|
||||
removable
|
||||
onDelete={(option: Option) => this.deleteTreeChecked(option)}
|
||||
/>) :
|
||||
(<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
title,
|
||||
searchable,
|
||||
translate: __,
|
||||
placeholder = __('Transfer.searchKeyword')
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('Selections', className)}>
|
||||
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||
{searchable ? (
|
||||
<TransferSearch
|
||||
placeholder={placeholder}
|
||||
onSearch={this.search}
|
||||
onCancelSearch={this.clearSearch}
|
||||
/>
|
||||
) : null}
|
||||
{this.renderTree()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(localeable(BaseResultTreeList));
|
@ -122,7 +122,6 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
|
||||
return (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
onClick={e => e.defaultPrevented || this.toggleOption(option)}
|
||||
className={cx(
|
||||
itemClassName,
|
||||
option.className,
|
||||
@ -131,8 +130,18 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
|
||||
)}
|
||||
>
|
||||
{multiple ? (
|
||||
<td className={cx('Table-checkCell')} key="checkbox">
|
||||
<Checkbox size="sm" checked={checked} disabled={disabled} />
|
||||
<td className={cx('Table-checkCell')}
|
||||
key="checkbox"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
this.toggleOption(option);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
{columns.map((column, colIndex) => (
|
||||
@ -174,4 +183,4 @@ export default themeable(
|
||||
value: 'onChange'
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
@ -1,20 +1,32 @@
|
||||
import React from 'react';
|
||||
import {intersectionWith, differenceWith, includes, debounce, result} from 'lodash';
|
||||
|
||||
import {ThemeProps, themeable} from '../theme';
|
||||
import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection';
|
||||
import {Options, Option} from './Select';
|
||||
import {uncontrollable} from 'uncontrollable';
|
||||
import ResultList from './ResultList';
|
||||
import TableSelection from './TableSelection';
|
||||
import TreeSelection from './TreeSelection';
|
||||
import {autobind, flattenTree} from '../utils/helper';
|
||||
import InputBox from './InputBox';
|
||||
import Checkbox from './Checkbox';
|
||||
import Tree from './Tree';
|
||||
import {Icon} from './icons';
|
||||
import debounce from 'lodash/debounce';
|
||||
import AssociatedSelection from './AssociatedSelection';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import GroupedSelection from './GroupedSelection';
|
||||
import ChainedSelection from './ChainedSelection';
|
||||
import {ItemRenderStates as ResultItemRenderStates} from './ResultList';
|
||||
import ResultTableList from './ResultTableList';
|
||||
import ResultTreeList from './ResultTreeList';
|
||||
|
||||
export type SelectMode =
|
||||
'table'
|
||||
| 'group'
|
||||
| 'list'
|
||||
| 'tree'
|
||||
| 'chained'
|
||||
| 'associated';
|
||||
|
||||
export interface TransferProps
|
||||
extends ThemeProps,
|
||||
@ -26,7 +38,7 @@ export interface TransferProps
|
||||
multiple?: boolean;
|
||||
|
||||
selectTitle?: string;
|
||||
selectMode?: 'table' | 'group' | 'list' | 'tree' | 'chained' | 'associated';
|
||||
selectMode?: SelectMode;
|
||||
columns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
@ -72,11 +84,16 @@ export interface TransferProps
|
||||
) => JSX.Element;
|
||||
|
||||
resultTitle?: string;
|
||||
// 结果提示语
|
||||
resultListModeFollowSelect?: boolean;
|
||||
resultSearchPlaceholder?: string;
|
||||
optionItemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||
resultItemRender?: (
|
||||
option: Option,
|
||||
states: ResultItemRenderStates
|
||||
) => JSX.Element;
|
||||
resultSearchable?: boolean;
|
||||
onResultSearch?: (text: string, item: Option) => boolean;
|
||||
sortable?: boolean;
|
||||
onRef?: (ref: Transfer) => void;
|
||||
onSelectAll?: (options: Options) => void;
|
||||
@ -85,34 +102,76 @@ export interface TransferProps
|
||||
export interface TransferState {
|
||||
inputValue: string;
|
||||
searchResult: Options | null;
|
||||
isTreeDeferLoad: boolean;
|
||||
resultSelectMode: 'list' | 'tree' | 'table'
|
||||
}
|
||||
|
||||
export class Transfer<
|
||||
T extends TransferProps = TransferProps
|
||||
> extends React.Component<T, TransferState> {
|
||||
static defaultProps: Pick<TransferProps, 'multiple'> = {
|
||||
multiple: true
|
||||
|
||||
static defaultProps: Pick<
|
||||
TransferProps,
|
||||
'multiple' | 'resultListModeFollowSelect' | 'selectMode'
|
||||
> = {
|
||||
multiple: true,
|
||||
resultListModeFollowSelect: false,
|
||||
selectMode: 'list'
|
||||
};
|
||||
|
||||
state = {
|
||||
state: TransferState = {
|
||||
inputValue: '',
|
||||
searchResult: null
|
||||
searchResult: null,
|
||||
isTreeDeferLoad: false,
|
||||
resultSelectMode: 'list'
|
||||
};
|
||||
|
||||
valueArray: Options;
|
||||
availableOptions: Options;
|
||||
unmounted = false;
|
||||
cancelSearch?: () => void;
|
||||
treeRef: any;
|
||||
|
||||
componentDidMount() {
|
||||
this.props?.onRef?.(this);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: TransferProps) {
|
||||
// 计算是否是懒加载模式
|
||||
let isTreeDeferLoad: boolean = false;
|
||||
props.selectMode === 'tree' &&
|
||||
props.options.forEach(item => {
|
||||
if (item.defer) {
|
||||
isTreeDeferLoad = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算结果的selectMode
|
||||
let resultSelectMode = 'list';
|
||||
if (props.selectMode === 'tree' && props.resultListModeFollowSelect && !isTreeDeferLoad) {
|
||||
resultSelectMode = 'tree';
|
||||
}
|
||||
|
||||
if (props.selectMode === 'table' && props.resultListModeFollowSelect) {
|
||||
resultSelectMode = 'table';
|
||||
}
|
||||
|
||||
return {
|
||||
isTreeDeferLoad,
|
||||
resultSelectMode
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.lazySearch.cancel();
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
@autobind
|
||||
domRef(ref: any) {
|
||||
this.treeRef = ref;
|
||||
}
|
||||
|
||||
@autobind
|
||||
toggleAll() {
|
||||
const {options, option2value, onChange, value, onSelectAll} = this.props;
|
||||
@ -142,7 +201,7 @@ export class Transfer<
|
||||
|
||||
// 全选,给予动作全选使用
|
||||
selectAll() {
|
||||
const {options, option2value, onChange} = this.props;;
|
||||
const {options, option2value, onChange} = this.props;
|
||||
const availableOptions = flattenTree(options).filter(
|
||||
(option, index, list) =>
|
||||
!option.disabled &&
|
||||
@ -169,22 +228,17 @@ export class Transfer<
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSearch(text: string) {
|
||||
handleSearch(inputValue: string) {
|
||||
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
|
||||
if (text) {
|
||||
this.setState(
|
||||
{
|
||||
inputValue: text
|
||||
},
|
||||
() => {
|
||||
// 如果有取消搜索,先取消掉。
|
||||
this.cancelSearch && this.cancelSearch();
|
||||
this.lazySearch(text);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.handleSeachCancel();
|
||||
}
|
||||
this.setState({inputValue}, () => {
|
||||
if (inputValue) {
|
||||
// 如果有取消搜索,先取消掉。
|
||||
this.cancelSearch && this.cancelSearch();
|
||||
this.lazySearch();
|
||||
} else {
|
||||
this.handleSeachCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -196,34 +250,75 @@ export class Transfer<
|
||||
}
|
||||
|
||||
lazySearch = debounce(
|
||||
(text: string) => {
|
||||
(async (text: string) => {
|
||||
const onSearch = this.props.onSearch!;
|
||||
let result = await onSearch(
|
||||
text,
|
||||
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor)
|
||||
);
|
||||
async () => {
|
||||
const {inputValue} = this.state;
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
const onSearch = this.props.onSearch!;
|
||||
let result = await onSearch(
|
||||
inputValue,
|
||||
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor)
|
||||
);
|
||||
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(result)) {
|
||||
throw new Error('onSearch 需要返回数组');
|
||||
}
|
||||
if (!Array.isArray(result)) {
|
||||
throw new Error('onSearch 需要返回数组');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
searchResult: result
|
||||
});
|
||||
})(text).catch(e => console.error(e));
|
||||
this.setState({
|
||||
searchResult: result
|
||||
});
|
||||
},
|
||||
250,
|
||||
{
|
||||
trailing: true,
|
||||
leading: false
|
||||
}
|
||||
{trailing: true, leading: false}
|
||||
);
|
||||
|
||||
getFlattenArr(options: Array<Option>) {
|
||||
return flattenTree(options).filter(
|
||||
(option, index, list) =>
|
||||
!option.disabled &&
|
||||
option.value !== void 0 &&
|
||||
list.indexOf(option) === index
|
||||
);
|
||||
}
|
||||
|
||||
// 树搜索处理
|
||||
@autobind
|
||||
handleSearchTreeChange(values: Array<Option>, searchOptions: Array<Option>) {
|
||||
const {onChange, value} = this.props;
|
||||
const searchAvailableOptions = this.getFlattenArr(searchOptions);
|
||||
|
||||
const useArr = intersectionWith(
|
||||
searchAvailableOptions,
|
||||
values,
|
||||
(a, b) => a.value === b.value
|
||||
);
|
||||
const unuseArr = differenceWith(
|
||||
searchAvailableOptions,
|
||||
values,
|
||||
(a, b) => a.value === b.value
|
||||
);
|
||||
|
||||
const newArr: Array<Option> = [];
|
||||
Array.isArray(value) &&
|
||||
value.forEach(item => {
|
||||
if (!unuseArr.find(v => v.value === item.value)) {
|
||||
newArr.push(item);
|
||||
}
|
||||
});
|
||||
useArr.forEach(item => {
|
||||
if (!newArr.find(v => v.value === item.value)) {
|
||||
newArr.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
onChange && onChange(newArr);
|
||||
}
|
||||
|
||||
renderSelect(
|
||||
props: TransferProps & {
|
||||
onToggleAll?: () => void;
|
||||
@ -238,7 +333,8 @@ export class Transfer<
|
||||
disabled,
|
||||
options,
|
||||
statistics,
|
||||
translate: __
|
||||
translate: __,
|
||||
searchPlaceholder = __('Transfer.searchKeyword')
|
||||
} = props;
|
||||
|
||||
if (selectRender) {
|
||||
@ -250,6 +346,16 @@ export class Transfer<
|
||||
});
|
||||
}
|
||||
|
||||
let checkedPartial = false;
|
||||
let checkedAll = false;
|
||||
|
||||
checkedAll = this.availableOptions.every(
|
||||
option => this.valueArray.indexOf(option) > -1
|
||||
);
|
||||
checkedPartial = this.availableOptions.some(
|
||||
option => this.valueArray.indexOf(option) > -1
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -259,14 +365,23 @@ export class Transfer<
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{includes(['list', 'tree'], selectMode) ? (
|
||||
<Checkbox
|
||||
checked={checkedPartial}
|
||||
partial={checkedPartial && !checkedAll}
|
||||
onChange={props.onToggleAll || this.toggleAll}
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
{__(selectTitle || 'Transfer.available')}
|
||||
{statistics !== false ? (
|
||||
<span>
|
||||
({this.valueArray.length}/{this.availableOptions.length})
|
||||
({this.availableOptions.length - this.valueArray.length}/
|
||||
{this.availableOptions.length})
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{selectMode !== 'table' ? (
|
||||
{includes(['chained', 'associated'], selectMode) ? (
|
||||
<a
|
||||
onClick={props.onToggleAll || this.toggleAll}
|
||||
className={cx(
|
||||
@ -284,9 +399,9 @@ export class Transfer<
|
||||
<InputBox
|
||||
value={this.state.inputValue}
|
||||
onChange={this.handleSearch}
|
||||
placeholder={__('Transfer.searchKeyword')}
|
||||
clearable={false}
|
||||
onKeyDown={this.handleSearchKeyDown}
|
||||
placeholder={searchPlaceholder}
|
||||
>
|
||||
{this.state.searchResult !== null ? (
|
||||
<a onClick={this.handleSeachCancel}>
|
||||
@ -322,7 +437,8 @@ export class Transfer<
|
||||
cellRender,
|
||||
multiple
|
||||
} = props;
|
||||
const options = this.state.searchResult || [];
|
||||
const {isTreeDeferLoad, searchResult} = this.state;
|
||||
const options = searchResult ?? [];
|
||||
const mode = searchResultMode || selectMode;
|
||||
const resultColumns = searchResultColumns || columns;
|
||||
|
||||
@ -341,16 +457,22 @@ export class Transfer<
|
||||
multiple={multiple}
|
||||
/>
|
||||
) : mode === 'tree' ? (
|
||||
<TreeSelection
|
||||
<Tree
|
||||
onRef={this.domRef}
|
||||
placeholder={noResultsText}
|
||||
className={cx('Transfer-selection')}
|
||||
options={options}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onChange={(value: Array<any>) =>
|
||||
this.handleSearchTreeChange(value, options)
|
||||
}
|
||||
joinValues={false}
|
||||
showIcon={false}
|
||||
multiple={true}
|
||||
cascade={true}
|
||||
onlyChildren={!isTreeDeferLoad}
|
||||
itemRender={optionItemRender}
|
||||
multiple={multiple}
|
||||
/>
|
||||
) : mode === 'chained' ? (
|
||||
<ChainedSelection
|
||||
@ -396,7 +518,8 @@ export class Transfer<
|
||||
cellRender,
|
||||
leftDefaultValue,
|
||||
optionItemRender,
|
||||
multiple
|
||||
multiple,
|
||||
noResultsText
|
||||
} = props;
|
||||
|
||||
return selectMode === 'table' ? (
|
||||
@ -413,16 +536,20 @@ export class Transfer<
|
||||
multiple={multiple}
|
||||
/>
|
||||
) : selectMode === 'tree' ? (
|
||||
<TreeSelection
|
||||
<Tree
|
||||
onRef={this.domRef}
|
||||
placeholder={noResultsText}
|
||||
className={cx('Transfer-selection')}
|
||||
options={options || []}
|
||||
options={options}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
onChange={onChange!}
|
||||
onlyChildren={!this.state.isTreeDeferLoad}
|
||||
itemRender={optionItemRender}
|
||||
multiple={multiple}
|
||||
onDeferLoad={onDeferLoad}
|
||||
joinValues={false}
|
||||
showIcon={false}
|
||||
multiple={true}
|
||||
cascade={true}
|
||||
/>
|
||||
) : selectMode === 'chained' ? (
|
||||
<ChainedSelection
|
||||
@ -468,21 +595,90 @@ export class Transfer<
|
||||
);
|
||||
}
|
||||
|
||||
renderResult() {
|
||||
const {
|
||||
columns,
|
||||
options,
|
||||
disabled,
|
||||
option2value,
|
||||
classnames: cx,
|
||||
cellRender,
|
||||
onChange,
|
||||
value,
|
||||
resultItemRender,
|
||||
resultSearchable,
|
||||
resultSearchPlaceholder,
|
||||
onResultSearch,
|
||||
sortable,
|
||||
translate: __
|
||||
} = this.props;
|
||||
|
||||
const {resultSelectMode, isTreeDeferLoad} = this.state;
|
||||
const searchable = !isTreeDeferLoad && resultSearchable;
|
||||
|
||||
const placeholder = resultSearchPlaceholder || __('Transfer.selectFromLeft');
|
||||
|
||||
return resultSelectMode === 'table'
|
||||
? (
|
||||
<ResultTableList
|
||||
classnames={cx}
|
||||
columns={columns!}
|
||||
options={options || []}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
option2value={option2value}
|
||||
cellRender={cellRender}
|
||||
onChange={onChange}
|
||||
multiple={false}
|
||||
searchable={searchable}
|
||||
placeholder={placeholder}
|
||||
onSearch={onResultSearch}
|
||||
/>)
|
||||
: resultSelectMode === 'tree'
|
||||
? (
|
||||
<ResultTreeList
|
||||
classnames={cx}
|
||||
options={options}
|
||||
valueField={'value'}
|
||||
value={value || []}
|
||||
onChange={onChange!}
|
||||
itemRender={resultItemRender}
|
||||
searchable={searchable}
|
||||
placeholder={placeholder}
|
||||
onSearch={onResultSearch}
|
||||
/>
|
||||
): (
|
||||
<ResultList
|
||||
className={cx('Transfer-value')}
|
||||
sortable={sortable}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
itemRender={resultItemRender}
|
||||
columns={columns!}
|
||||
options={options || []}
|
||||
option2value={option2value}
|
||||
cellRender={cellRender}
|
||||
searchable={searchable}
|
||||
onSearch={onResultSearch}
|
||||
/>);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
inline,
|
||||
classnames: cx,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
resultTitle,
|
||||
sortable,
|
||||
options,
|
||||
option2value,
|
||||
disabled,
|
||||
statistics,
|
||||
showArrow,
|
||||
resultItemRender,
|
||||
resultListModeFollowSelect,
|
||||
selectMode = 'list',
|
||||
translate: __
|
||||
} = this.props;
|
||||
|
||||
@ -494,6 +690,8 @@ export class Transfer<
|
||||
list.indexOf(option) === index
|
||||
);
|
||||
|
||||
const tableType = resultListModeFollowSelect && selectMode === 'table';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')}
|
||||
@ -509,13 +707,16 @@ export class Transfer<
|
||||
) : null}
|
||||
</div>
|
||||
<div className={cx('Transfer-result')}>
|
||||
<div className={cx('Transfer-title')}>
|
||||
<div
|
||||
className={cx(
|
||||
'Transfer-title',
|
||||
tableType ? 'Transfer-table-title' : ''
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{__(resultTitle || 'Transfer.selectd')}
|
||||
{statistics !== false ? (
|
||||
<span>
|
||||
({this.valueArray.length}/{this.availableOptions.length})
|
||||
</span>
|
||||
<span>({this.valueArray.length})</span>
|
||||
) : null}
|
||||
</span>
|
||||
<a
|
||||
@ -528,15 +729,7 @@ export class Transfer<
|
||||
{__('clear')}
|
||||
</a>
|
||||
</div>
|
||||
<ResultList
|
||||
className={cx('Transfer-value')}
|
||||
sortable={sortable}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={__('Transfer.selectFromLeft')}
|
||||
itemRender={resultItemRender}
|
||||
/>
|
||||
{this.renderResult()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
129
src/components/TransferSearch.tsx
Normal file
129
src/components/TransferSearch.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Tranasfer搜索
|
||||
*/
|
||||
import React from 'react';
|
||||
import {debounce} from 'lodash';
|
||||
|
||||
import {ThemeProps, themeable} from '../theme';
|
||||
import {Icon} from './icons';
|
||||
import {autobind} from '../utils/helper';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import InputBox from './InputBox';
|
||||
|
||||
export interface TransferSearchProps
|
||||
extends ThemeProps,
|
||||
LocaleProps {
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
onSearch: Function;
|
||||
onCancelSearch: Function
|
||||
}
|
||||
|
||||
export interface ItemRenderStates {
|
||||
index: number;
|
||||
disabled?: boolean;
|
||||
onChange: (value: any, name: string) => void;
|
||||
}
|
||||
|
||||
interface TransferSearchState {
|
||||
inputValue: string;
|
||||
}
|
||||
|
||||
export class TransferSearch extends React.Component<
|
||||
TransferSearchProps,
|
||||
TransferSearchState
|
||||
> {
|
||||
|
||||
static itemRender(option: any) {
|
||||
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
||||
}
|
||||
|
||||
static defaultProps: Pick<TransferSearchProps, 'placeholder'> = {
|
||||
placeholder: 'placeholder.selectData'
|
||||
};
|
||||
|
||||
state: TransferSearchState = {
|
||||
inputValue: ''
|
||||
};
|
||||
|
||||
cancelSearch?: () => void;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.lazySearch.cancel();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSearch(inputValue: string) {
|
||||
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
|
||||
this.setState({inputValue}, () => {
|
||||
if (inputValue) {
|
||||
// 如果有取消搜索,先取消掉。
|
||||
this.cancelSearch && this.cancelSearch();
|
||||
this.lazySearch();
|
||||
} else {
|
||||
this.handleSeachCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazySearch = debounce(
|
||||
() => {
|
||||
const {inputValue} = this.state;
|
||||
// 防止由于防抖导致空值问题
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
const {onSearch} = this.props;
|
||||
onSearch!(inputValue);
|
||||
},
|
||||
250,
|
||||
{trailing: true, leading: false}
|
||||
);
|
||||
|
||||
@autobind
|
||||
handleSearchKeyDown(e: React.KeyboardEvent<any>) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSeachCancel() {
|
||||
this.props.onCancelSearch?.();
|
||||
this.setState({
|
||||
inputValue: ''
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
translate: __,
|
||||
placeholder = __('Transfer.searchKeyword')
|
||||
} = this.props;
|
||||
|
||||
const {inputValue} = this.state;
|
||||
|
||||
return (
|
||||
<div className={cx('Transfer-search')}>
|
||||
<InputBox
|
||||
value={inputValue}
|
||||
onChange={this.handleSearch}
|
||||
clearable={false}
|
||||
onKeyDown={this.handleSearchKeyDown}
|
||||
placeholder={placeholder}
|
||||
>
|
||||
{!!inputValue ? (
|
||||
<a onClick={this.handleSeachCancel}>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
) : (
|
||||
<Icon icon="search" className="icon" />
|
||||
)}
|
||||
</InputBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(localeable(TransferSearch));
|
@ -23,6 +23,7 @@ import {Icon, getIcon} from './icons';
|
||||
import Checkbox from './Checkbox';
|
||||
import {LocaleProps, localeable} from '../locale';
|
||||
import Spinner from './Spinner';
|
||||
import {ItemRenderStates} from './Selection';
|
||||
|
||||
interface IDropIndicator {
|
||||
left: number;
|
||||
@ -41,7 +42,7 @@ export interface IDropInfo {
|
||||
interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
||||
highlightTxt?: string;
|
||||
|
||||
onRef: any;
|
||||
onRef?: any;
|
||||
|
||||
showIcon?: boolean;
|
||||
// 是否默认都展开
|
||||
@ -124,6 +125,7 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
||||
onExpandTree?: (nodePathArr: any[]) => void;
|
||||
draggable?: boolean;
|
||||
onMove?: (dropInfo: IDropInfo) => void;
|
||||
itemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||
}
|
||||
|
||||
interface TreeSelectorState {
|
||||
@ -806,6 +808,7 @@ export class TreeSelector extends React.Component<
|
||||
editTip,
|
||||
removeTip,
|
||||
translate: __,
|
||||
itemRender,
|
||||
draggable
|
||||
} = this.props;
|
||||
const {
|
||||
@ -967,7 +970,15 @@ export class TreeSelector extends React.Component<
|
||||
>
|
||||
{highlightTxt
|
||||
? highlight(`${item[labelField]}`, highlightTxt)
|
||||
: `${item[labelField]}`}
|
||||
: itemRender ? (
|
||||
itemRender(item, {
|
||||
index: key,
|
||||
multiple: multiple,
|
||||
checked: checked,
|
||||
onChange: () => this.handleCheck(item, !selfChecked),
|
||||
disabled: disabled || item.disabled
|
||||
}))
|
||||
: `${item[labelField]}`}
|
||||
</span>
|
||||
|
||||
{!nodeDisabled &&
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
|
||||
import {
|
||||
OptionsControlProps,
|
||||
OptionsControl,
|
||||
FormOptionsControl
|
||||
} from './Options';
|
||||
import React from 'react';
|
||||
import Transfer, {Transfer as BaseTransfer} from '../../components/Transfer';
|
||||
import type {Option} from './Options';
|
||||
import {
|
||||
@ -16,9 +18,7 @@ import {
|
||||
getTree,
|
||||
spliceTree
|
||||
} from '../../utils/helper';
|
||||
import {Api} from '../../types';
|
||||
import Spinner from '../../components/Spinner';
|
||||
import find from 'lodash/find';
|
||||
import {optionValueCompare} from '../../components/Select';
|
||||
import {resolveVariable} from '../../utils/tpl-builtin';
|
||||
import {SchemaApi, SchemaObject} from '../../Schema';
|
||||
@ -51,6 +51,11 @@ export interface TransferControlSchema extends FormOptionsControl {
|
||||
*/
|
||||
selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated';
|
||||
|
||||
/**
|
||||
* 结果面板是否追踪显示
|
||||
*/
|
||||
resultListModeFollowSelect?: boolean;
|
||||
|
||||
/**
|
||||
* 当 selectMode 为 associated 时用来定义左侧的选项
|
||||
*/
|
||||
@ -86,6 +91,11 @@ export interface TransferControlSchema extends FormOptionsControl {
|
||||
*/
|
||||
searchable?: boolean;
|
||||
|
||||
/**
|
||||
* 结果(右则)列表的检索功能,当设置为true时,可以通过输入检索模糊匹配检索内容
|
||||
*/
|
||||
resultSearchable?: boolean;
|
||||
|
||||
/**
|
||||
* 搜索 API
|
||||
*/
|
||||
@ -110,6 +120,16 @@ export interface TransferControlSchema extends FormOptionsControl {
|
||||
* 用来丰富值的展示
|
||||
*/
|
||||
valueTpl?: SchemaObject;
|
||||
|
||||
/**
|
||||
* 左侧列表搜索框提示
|
||||
*/
|
||||
searchPlaceholder?: string;
|
||||
|
||||
/**
|
||||
* 右侧列表搜索框提示
|
||||
*/
|
||||
resultSearchPlaceholder?: string;
|
||||
}
|
||||
|
||||
export interface BaseTransferProps
|
||||
@ -271,6 +291,13 @@ export class BaseTransferRenderer<
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleResultSearch(term: string, item: Option) {
|
||||
const {valueField} = this.props;
|
||||
const regexp = string2regExp(term);
|
||||
return regexp.test(item[(valueField as string) || 'value']);
|
||||
}
|
||||
|
||||
@autobind
|
||||
optionItemRender(option: Option, states: ItemRenderStates) {
|
||||
const {menuTpl, render, data} = this.props;
|
||||
@ -351,7 +378,7 @@ export class BaseTransferRenderer<
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
let {
|
||||
className,
|
||||
classnames: cx,
|
||||
selectedOptions,
|
||||
@ -370,7 +397,10 @@ export class BaseTransferRenderer<
|
||||
selectTitle,
|
||||
resultTitle,
|
||||
menuTpl,
|
||||
resultItemRender
|
||||
searchPlaceholder,
|
||||
resultListModeFollowSelect = false,
|
||||
resultSearchPlaceholder,
|
||||
resultSearchable = false
|
||||
} = this.props;
|
||||
|
||||
// 目前 LeftOptions 没有接口可以动态加载
|
||||
@ -412,6 +442,11 @@ export class BaseTransferRenderer<
|
||||
cellRender={this.renderCell}
|
||||
selectTitle={selectTitle}
|
||||
resultTitle={resultTitle}
|
||||
resultListModeFollowSelect={resultListModeFollowSelect}
|
||||
onResultSearch={this.handleResultSearch}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
resultSearchable={resultSearchable}
|
||||
resultSearchPlaceholder={resultSearchPlaceholder}
|
||||
optionItemRender={this.optionItemRender}
|
||||
resultItemRender={this.resultItemRender}
|
||||
onSelectAll={this.onSelectAll}
|
||||
|
Loading…
Reference in New Issue
Block a user