feat: 表单项增加展示态 (#5391)

* feat: 表单项增加展示态
包括: 多选、单选、下拉列表、按钮选择、标签选择、时间选择类组件、进度条选择、时间范围选择类组件、列表选择器、数字输入框、评分、矩阵输入框、文本输入器、combo、inputKv、inputKvs、树选择器、级联选择器、穿梭器、城市选择器

* docs: 表单和表单项支持静态展示

* feat: 表单和表单项支持静态展示 - 微调

Co-authored-by: jinye <jinye@baidu.com>
This commit is contained in:
lmaomaoz 2022-10-19 09:37:01 +08:00 committed by GitHub
parent fd02fcd7f4
commit 13b0fdf04d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2352 additions and 416 deletions

View File

@ -305,6 +305,181 @@ order: 1
> `visible`和`hidden``visibleOn`和`hiddenOn`除了判断逻辑相反以外,没有任何区别
### 配置静态展示
##### 静态配置
通过配置`"static": true`来将表单项以静态形式展示
可以在[示例页](../../../examples/form/switchDisplay)查看支持静态展示的表单项的展示方式
```schema: scope="body"
{
"type": "form",
"body": [
{
"type": "input-text",
"label": "静态",
"name": "text1",
"value": "text1的值",
"static": true
},
{
"type": "input-text",
"label": "输入态",
"name": "text2",
"value": "text2的值"
}
]
}
```
##### 通过条件配置静态/输入态
也可以通过[表达式](../../../docs/concepts/expression)配置`staticOn`,来实现在某个条件下将当前表单项状态的的自动切换.
```schema: scope="body"
{
"type": "form",
"body": [
{
"type": "input-number",
"label": "数量",
"name": "number",
"value": 0,
"description": "调整数量大小查看效果吧!"
},
{
"type": "input-text",
"label": "文本",
"name": "text",
"staticOn": "this.number > 1",
"value": "text value",
"description": "当数量大于1的时候该文本框会变成静态"
}
]
}
```
##### 自定义展示态的展示方式
通过配置`staticSchema`,可以自定义静态展示时的展示方式
```schema: scope="body"
{
"type": "form",
"body": [
{
"type": "input-text",
"name": "var3",
"label": "自定义展示态schema",
"value": "表单项value",
"static": true,
"staticSchema": [
"自定义前缀 | ",
{
"type": "tpl",
"tpl": "${var3}"
},
" | 自定义后缀",
]
}
]
}
```
##### 限制选择器类组件的展示数量
下拉选择器、多选框等组件当选项过多静态展示时若全部展示会占用页面很多空间所以默认进行了限制10个
可以通过配置`staticSchema.limit`,可以自定义静态展示时的数量
```schema: scope="body"
{
"type": "form",
"body": [
{
"type": "input-tag",
"name": "tags",
"label": "自定义展示数量",
"value": "1,2,3,4,5,6,7,8",
"options": [
{"label": "选项1", "value": "选项1"},
{"label": "选项2", "value": "选项2"},
{"label": "选项3", "value": "选项3"},
{"label": "选项4", "value": "选项4"},
{"label": "选项5", "value": "选项5"},
{"label": "选项6", "value": "选项6"},
{"label": "选项7", "value": "选项7"},
{"label": "选项8", "value": "选项8"}
],
"static": true,
"staticSchema": {
"limit": 3
}
}
]
}
```
##### 通过事件动作切换表单项状态
也支持使用 事件动作 切换表单项的 输入态和展示态(静态),也可以使用动作对整个表单进行状态切换
```schema: scope="body"
{
"type": "form",
"title": "单个表单项状态切换",
"mode": "horizontal",
"labelWidth": 150,
"body": [
{
"type": "input-text",
"id": "formItemSwitch",
"name": "var1",
"label": "使用事件动作状态切换",
"value": "text"
},
{
"type": 'button-toolbar',
"name": 'button-toolbar',
"buttons": [
{
"type": "button",
"label": "输入态",
"level": "primary",
"onEvent": {
"click": {
"actions": [
{
"actionType": "nonstatic",
"componentId": "formItemSwitch"
}
]
}
}
},
{
"type": "button",
"label": "展示态",
"level": "primary",
"onEvent": {
"click": {
"actions": [
{
"actionType": "static",
"componentId": "formItemSwitch"
}
]
}
}
}
],
"className": 'show'
},
],
"actions": []
}
```
## 表单项值
表单项值,即表单项通过用户交互发生变化后,更新表单数据域中同`name`变量值.
@ -1344,3 +1519,59 @@ fillMapping 配置 支持变量取值和表达式;
| autoFill.size | `string` | | showSuggestion 为 true 时,参照录入 mode 为 dialog 时,可设置大小 |
| autoFill.columns | `Array<Column>` | | showSuggestion 为 true 时,数据展示列配置 |
| autoFill.filter | [SchemaNode](../../docs/types/schemanode) | | showSuggestion 为 true 时,数据查询过滤条件 |
| static | `boolean` | | 当前表单项是否是静态展示,目前支持静[支持静态展示的表单项](#支持静态展示的表单项) |
| staticClassName | `string` | | 静态展示时的类名 |
| staticLabelClassName | `string` | | 静态展示时的Label的类名 |
| staticInputClassName | `string` | | 静态展示时的value的类名 |
| staticSchema | `string`|`Array`|[SchemaNode](../../docs/types/schemanode) | | 自定义静态展示方式 |
| staticSchema.limit | `number` | 10 | select、checkboxes等选择类组件多选时展示态展示的数量 |
## 支持静态展示的表单项
可以在[示例页](../../../examples/form/switchDisplay)查看支持静态展示的表单项的展示方式
- form 表单
- button-group-select 按钮点选
- chained-select 链式下拉框
- chart-radios 图表单选框
- checkbox 勾选框
- checkboxes 复选框
- combo 组合
- input-kv 键值对
- input-array 数组输入框
- input-city 城市选择器
- input-color 颜色选择器
- input-date 日期选择器
- input-date-range 日期范围选择器
- input-datetime-range 日期时间选择器
- input-time-range 时间范围选择器
- input-group 输入框组合
- input-month-range 月份范围
- input-number 数字输入
- input-quarter-range 季度范围
- input-range 滑块
- input-rating 评分
- input-tag 标签选择器
- input-text 输入框
- input-password 密码输入框
- input-email 邮箱输入框
- input-url url输入框
- native-date native日期选择器
- native-time native时间选择器
- native-number native数字输入
- input-tree 树形选择器
- input-year-range 年份范围
- list-select 列表选择器
- location-picker 地理位置
- matrix-checkboxes 矩阵勾选
- nested-select 级联选择器
- radios 单选框
- select 下拉框
- multi-select 多选下拉框
- switch 开关
- tabs-transfer 组合穿梭器
- tabs-transfer-picker 组合穿梭选择器
- textarea 多行输入框
- transfer 穿梭器
- transfer-picker 穿梭选择器
- tree-select 属性选择器

View File

@ -568,6 +568,144 @@ Form 默认会在底部渲染一个提交按钮,用于执行表单的提交行
如果表单项较多导致表单过长,而不方便操作底部的按钮栏,可以配置`"affixFooter": true`属性,将底部按钮栏固定在浏览器底部
## 表单静态展示
在一些场景,表单提交后需要将填写的内容静态展示
### 设置初始状态
通过配置`static: true`将整个表单设置为静态展示,单个表单项也支持此配置
```schema: scope="body"
{
"type": "form",
"title": "表单状态切换",
"mode": "horizontal",
"labelWidth": 150,
"id": "allFormSwitch",
"static": true,
"body": [
{
"type": "input-text",
"name": "var1",
"label": "输入框",
"value": "text"
},
{
"type": "input-color",
"name": "var2",
"label": "颜色选择",
"value": "#F0F"
},
{
"type": "switch",
"name": "switch",
"label": "开关",
"option": "开关说明",
"value": true
}
],
"actions": []
}
```
### 切换输入态和展示态
也支持使用[动作](#动作表)切换表单的 输入态和展示态(静态),也可以使用动作对单个表单项进行状态切换
可以在[示例页](../../../examples/form/switchDisplay)查看表单项的静态展示方式
```schema: scope="body"
{
"type": "form",
"title": "表单状态切换",
"mode": "horizontal",
"labelWidth": 150,
"id": "allFormSwitch",
"static": true,
"data": {
"isStatic": false
},
"body": [
{
"type": "input-text",
"name": "var1",
"label": "输入框",
"value": "text"
},
{
"type": "input-color",
"name": "var2",
"label": "颜色选择",
"value": "#F0F"
},
{
"type": "switch",
"name": "switch",
"label": "开关",
"option": "开关说明",
"value": true
},
{
"type": 'button-toolbar',
"name": 'button-toolbar',
"buttons": [
{
"type": "button",
"label": "提交",
"level": "primary",
"visibleOn": "${!isStatic}",
"onEvent": {
"click": {
"actions": [
{
"actionType": "setValue",
"componentId": "allFormSwitch",
"args": {
"value": {
"isStatic": true
}
}
},
{
"actionType": "static",
"componentId": "allFormSwitch"
}
]
}
}
},
{
"type": "button",
"label": "编辑",
"level": "primary",
"visibleOn": "${isStatic}",
"onEvent": {
"click": {
"actions": [
{
"actionType": "setValue",
"componentId": "allFormSwitch",
"args": {
"value": {
"isStatic": false
}
}
},
{
"actionType": "nonstatic",
"componentId": "allFormSwitch"
}
]
}
}
}
]
},
],
"actions": []
}
```
## 表单项数据初始化
表单可以通过配置`initApi`,实现表单初始化时请求接口,用于展示数据或初始化表单项。
@ -1297,7 +1435,9 @@ Form 支持轮询初始化接口,步骤如下:
| trimValues | `boolean` | `false` | trim 当前表单项的每一个值 |
| promptPageLeave | `boolean` | `false` | form 还没保存,即将离开页面前是否弹框确认。 |
| columnCount | `number` | 0 | 表单项显示为几列 |
| inheritData | `booelan` | `true` | 默认表单是采用数据链的形式创建个自己的数据域,表单提交的时候只会发送自己这个数据域的数据,如果希望共用上层数据域可以设置这个属性为 false这样上层数据域的数据不需要在表单中用隐藏域或者显式映射才能发送了。 |
| inheritData | `boolean` | `true` | 默认表单是采用数据链的形式创建个自己的数据域,表单提交的时候只会发送自己这个数据域的数据,如果希望共用上层数据域可以设置这个属性为 false这样上层数据域的数据不需要在表单中用隐藏域或者显式映射才能发送了。 |
| static | `boolean` | | 整个表单静态方式展示,详情请查看[示例页](../../../examples/form/switchDisplay) |
| staticClassName | `string` | | 表单静态展示时使用的类名 |
## 事件表
@ -1324,5 +1464,7 @@ Form 支持轮询初始化接口,步骤如下:
| reset | - | 重置表单 |
| clear | - | 清空表单 |
| validate | - | 校验表单 |
| reload | - | 刷新(重新加载) |
| setValue | `value: object` 更新的表单数据 | 更新数据,对数据进行 merge |
| reload | - | 刷新(重新加载) |
| setValue | `value: object` 更新的表单数据 | 更新数据,对数据进行 merge |
| static | - | 表单切换为静态展示 |
| nonstatic | - | 表单切换为普通输入态 |

View File

@ -175,7 +175,7 @@ export const examples = [
},
{
label: '编辑态、展示态切换',
label: '输入态、展示态切换',
path: '/examples/form/switchDisplay',
component: makeSchemaRenderer(SwitchFormDisplay)
},

View File

@ -1,5 +1,13 @@
const renderOptions = (
count = 3,
render = (index) => ({label: `选项${index}`, value: index})
) => [...Array(count)].map((item, index) => render(index));
const renderSelectValues = (count) =>
[...Array(count)].map((item, index) => index).join(',');
export default {
"title": "表单及表单项切换编辑态展示态",
"title": "表单及表单项切换输入态展示态",
"data": {
"id": 1
},
@ -10,6 +18,9 @@ export default {
"mode": "horizontal",
"labelWidth": 150,
"id": "allFormSwitch",
"data": {
"isStatic": false
},
"body": [
{
"type": "input-text",
@ -27,61 +38,46 @@ export default {
"type": "switch",
"name": "switch",
"label": "开关",
"option": "开关说明",
"value": true
},
{
"type": "checkboxes",
"name": "checkboxes",
"label": "多选框",
"value": "1,2",
"value": renderSelectValues(12),
"multiple": true,
"options": [
{
"label": "选项1",
"value": 1
},
{
"label": "选项2",
"value": 2
},
{
"label": "选项3",
"value": 3
}
]
"options": renderOptions(12)
},
{
"type": "select",
"name": "type",
"label": "下拉单选",
"type": "input-tag",
"name": "select11",
"label": "标签选择",
"inline": true,
"value": 1,
"options": [
{
"label": "选项1",
"value": 1
},
{
"label": "选项2",
"value": 2
},
{
"label": "内容很长很长的选项,内容很长很长的选项",
"value": 3
}
]
"value": renderSelectValues(12),
"options": renderOptions(12)
},
{
type: 'button-toolbar',
name: 'button-toolbar',
buttons: [
"type": 'button-toolbar',
"name": 'button-toolbar',
"buttons": [
{
"type": "button",
"label": "提交",
"level": "primary",
"visibleOn": "${!isStatic}",
"onEvent": {
"click": {
"actions": [
{
"actionType": "setValue",
"componentId": "allFormSwitch",
"args": {
"value": {
"isStatic": true
}
}
},
{
"actionType": "static",
"componentId": "allFormSwitch"
@ -94,9 +90,19 @@ export default {
"type": "button",
"label": "编辑",
"level": "primary",
"visibleOn": "${isStatic}",
"onEvent": {
"click": {
"actions": [
{
"actionType": "setValue",
"componentId": "allFormSwitch",
"args": {
"value": {
"isStatic": false
}
}
},
{
"actionType": "nonstatic",
"componentId": "allFormSwitch"
@ -106,7 +112,7 @@ export default {
}
}
],
className: 'show'
"className": 'show'
},
],
"actions": []
@ -126,12 +132,12 @@ export default {
"value": "text"
},
{
type: 'button-toolbar',
name: 'button-toolbar',
buttons: [
"type": 'button-toolbar',
"name": 'button-toolbar',
"buttons": [
{
"type": "button",
"label": "编辑态",
"label": "输入态",
"level": "primary",
"onEvent": {
"click": {
@ -160,7 +166,7 @@ export default {
}
}
],
className: 'show'
"className": 'show'
},
],
"actions": []
@ -175,7 +181,7 @@ export default {
"type": "input-text",
"id": "formItemInputText",
"name": "var1",
"label": "编辑态<br />不设置<br />或static: false",
"label": "输入态<br />不设置<br />或static: false",
"value": "text",
"static": false,
"desc": "使用staticOn 支持表达式控制,用法类似"
@ -201,7 +207,6 @@ export default {
"name": "var3",
"label": "自定义展示态schema",
"value": "表单项value",
"desc": "通过\\${name}可获取到当前值",
"static": true,
"staticSchema": [
"自定义前缀 | ",
@ -223,7 +228,7 @@ export default {
"autoFocus": true,
"panel": false,
"debug": false,
"title": "目前支持编辑态展示态切换的表单项",
"title": "目前支持输入态展示态切换的表单项",
"labelWidth": 150,
"staticClassName": "now-is-static",
"body": [
@ -233,7 +238,7 @@ export default {
"buttons": [
{
"type": "button",
"label": "切换为编辑态",
"label": "切换为输入态",
"level": "primary",
"visibleOn": "${static}",
"onEvent": {
@ -471,21 +476,8 @@ export default {
"label": "List",
"desc": "可多选",
"multiple": true,
"value": "1,2",
"options": [
{
"label": "选项 A",
"value": 1
},
{
"label": "选项 B",
"value": 2
},
{
"label": "选项 C",
"value": 3
}
]
"value": renderSelectValues(2),
"options": renderOptions(3)
},
{
"type": "divider"
@ -496,24 +488,12 @@ export default {
"label": "List",
"imageClassName": "thumb-lg",
"desc": "支持放张图片",
"value": 1,
"options": [
{
"image": "/examples/static/photo/3893101144.jpg",
"value": 1,
"label": "图片1"
},
{
"image": "/examples/static/photo/3893101144.jpg",
"value": 2,
"label": "图片2"
},
{
"image": "/examples/static/photo/3893101144.jpg",
"value": 3,
"label": "图片3"
}
]
"value": renderSelectValues(2),
"options": renderOptions(3, (index) => ({
"image": "/examples/static/photo/3893101144.jpg",
"value": index,
"label": `图片${index}`
}))
},
{
"type": "divider"
@ -523,22 +503,12 @@ export default {
"name": "list5",
"label": "List",
"desc": "支持文字排版",
"multiple": true,
"value": 1,
"options": [
{
"value": 1,
"body": "<div class=\"m-l-sm m-r-sm m-b-sm m-t-xs\">\n <div class=\"text-md p-b-xs b-inherit b-b m-b-xs\">套餐C01</div>\n <div class=\"text-sm\">CPU22核</div>\n <div class=\"text-sm\">内存10GB</div>\n <div class=\"text-sm\">SSD盘1024GB</div>\n </div>"
},
{
"value": 2,
"body": "<div class=\"m-l-sm m-r-sm m-b-sm m-t-xs\">\n <div class=\"text-md p-b-xs b-inherit b-b m-b-xs\">套餐C02</div>\n <div class=\"text-sm\">CPU23核</div>\n <div class=\"text-sm\">内存11GB</div>\n <div class=\"text-sm\">SSD盘1025GB</div>\n </div>"
},
{
"value": 3,
"disabled": true,
"body": "<div class=\"m-l-sm m-r-sm m-b-sm m-t-xs\">\n <div class=\"text-md p-b-xs b-inherit b-b m-b-xs\">套餐C03</div>\n <div class=\"text-sm\">CPU24核</div>\n <div class=\"text-sm\">内存12GB</div>\n <div class=\"text-sm\">SSD盘1026GB</div>\n </div>"
}
]
"options": renderOptions(3, (index) => ({
"value": index,
"body": "<div class=\"m-l-sm m-r-sm m-b-sm m-t-xs\">\n <div class=\"text-md p-b-xs b-inherit b-b m-b-xs\">套餐C01</div>\n <div class=\"text-sm\">CPU22核</div>\n <div class=\"text-sm\">内存10GB</div>\n <div class=\"text-sm\">SSD盘1024GB</div>\n </div>"
}))
},
{
"type": "divider"
@ -589,7 +559,7 @@ export default {
"type": "checkbox",
"name": "checkbox",
"label": "勾选框",
"options": "勾选说明",
"option": "同意协议",
"value": true
},
{
@ -755,9 +725,6 @@ export default {
}
]
},
{
"type": "divider"
},
{
"type": "input-group",
"label": "各种组合",
@ -819,7 +786,10 @@ export default {
"type": "textarea",
"name": "textarea",
"label": "多行文本",
"value": "这是一段多行文本文字\n第二行内容"
"value": "这是一段多行文本文字\n第二行内容\n2222\n333",
"staticSchema": {
"limit": 3
}
},
{
"type": "divider"
@ -836,6 +806,332 @@ export default {
"inline": true,
"value": "A,B,C"
},
{
"label": '组合穿梭器',
"type": 'tabs-transfer',
"name": 'tabs-transfer',
"sortable": true,
"selectMode": 'tree',
"id": 'tab-transfer-receiver',
"resetValue": 'zhugeliang',
"value": "zhugeliang,caocao",
"enableNodePath": true,
"options": [
{
"label": '成员',
"selectMode": 'tree',
"searchable": true,
"children": [
{
"label": '法师',
"children": [
{
"label": '诸葛亮',
"value": 'zhugeliang'
}
]
},
{
"label": '战士',
"children": [
{
"label": '曹操',
"value": 'caocao'
},
{
"label": '钟无艳',
"value": 'zhongwuyan'
}
]
},
{
"label": '打野',
"children": [
{
"label": '李白',
"value": 'libai'
},
{
"label": '韩信',
"value": 'hanxin'
},
{
"label": '云中君',
"value": 'yunzhongjun'
}
]
}
]
},
{
"label": '用户',
"selectMode": 'chained',
"children": [
{
"label": '法师',
"children": [
{
"label": '诸葛亮',
"value": 'zhugeliang2'
}
]
},
{
"label": '战士',
"children": [
{
"label": '曹操',
"value": 'caocao2'
},
{
"label": '钟无艳',
"value": 'zhongwuyan2'
}
]
},
{
"label": '打野',
"children": [
{
"label": '李白',
"value": 'libai2'
},
{
"label": '韩信',
"value": 'hanxin2'
},
{
"label": '云中君',
"value": 'yunzhongjun2'
}
]
}
]
}
],
"onEvent": {
"change": {
"actions": [
{
"actionType": 'toast',
"args": {
"msgType": 'info',
"msg": '${event.data.value|json}'
}
}
]
}
}
},
{
"label": '穿梭器picker',
"type": 'transfer-picker',
"name": 'transfer-picker',
"id": 'transfer-picker-receiver',
"resetValue": 'zhugeliang',
"sortable": true,
"selectMode": 'tree',
"searchable": true,
"value": "zhugeliang,zhongwuyan",
"enableNodePath": 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'
}
]
}
],
"onEvent": {
"change": {
"actions": [
{
"actionType": 'toast',
"args": {
"msgType": 'info',
"msg": '${event.data.value|json}'
}
}
]
}
}
},
{
"label": '组合穿梭器picker',
"type": 'tabs-transfer-picker',
"name": 'tabs-transfer-picker',
"id": 'tabs-transfer-picker-receiver',
"resetValue": 'zhugeliang',
"value": "caocao,zhongwuyan",
"sortable": true,
"selectMode": 'tree',
"pickerSize": 'md',
"menuTpl":
"<div class='flex justify-between'><span>${label}</span>${email ? `<div class='text-muted m-r-xs text-sm text-right'>${email}<br />${phone}</div>`: ''}</div>",
"valueTpl": '${label}(${value})',
"options": [
{
"label": '成员',
"selectMode": 'tree',
"searchable": true,
"children": [
{
"label": '法师',
"children": [
{
"label": '诸葛亮',
"value": 'zhugeliang',
"email": 'zhugeliang@timi.com',
"phone": 13111111111
}
]
},
{
"label": '战士',
"children": [
{
"label": '曹操',
"value": 'caocao',
"email": 'caocao@timi.com',
"phone": 13111111111
},
{
"label": '钟无艳',
"value": 'zhongwuyan',
"email": 'zhongwuyan@timi.com',
"phone": 13111111111
}
]
},
{
"label": '打野',
"children": [
{
"label": '李白',
"value": 'libai',
"email": 'libai@timi.com',
"phone": 13111111111
},
{
"label": '韩信',
"value": 'hanxin',
"email": 'hanxin@timi.com',
"phone": 13111111111
},
{
"label": '云中君',
"value": 'yunzhongjun',
"email": 'yunzhongjun@timi.com',
"phone": 13111111111
}
]
}
]
},
{
"label": '角色',
"selectMode": 'list',
"children": [
{
"label": '角色 1',
"value": 'role1'
},
{
"label": '角色 2',
"value": 'role2'
},
{
"label": '角色 3',
"value": 'role3'
},
{
"label": '角色 4',
"value": 'role4'
}
]
},
{
"label": '部门',
"selectMode": 'tree',
"children": [
{
"label": '总部',
"value": 'dep0',
"children": [
{
"label": '部门 1',
"value": 'dep1',
"children": [
{
"label": '部门 4',
"value": 'dep4'
},
{
"label": '部门 5',
"value": 'dep5'
}
]
},
{
"label": '部门 2',
"value": 'dep2'
},
{
"label": '部门 3',
"value": 'dep3'
}
]
}
]
}
],
"onEvent": {
"change": {
"actions": [
{
"actionType": 'toast',
"args": {
"msgType": 'info',
"msg": '${event.data.value|json}'
}
}
]
}
}
},
{
"type": "divider"
},
@ -872,9 +1168,6 @@ export default {
}
]
},
{
"type": "divider"
},
{
"type": "input-tree",
"name": "trees",
@ -906,9 +1199,6 @@ export default {
}
]
},
{
"type": "divider"
},
{
"type": "tree-select",
"name": "selecttree",
@ -939,13 +1229,11 @@ export default {
}
]
},
{
"type": "divider"
},
{
"type": "tree-select",
"name": "selecttrees",
"label": "树多选选择器",
"enableNodePath": true,
"multiple": true,
"value": "1-2,5",
"options": [
@ -973,6 +1261,9 @@ export default {
}
]
},
{
"type": "divider"
},
{
"type": "nested-select",
"name": "nestedSelect",
@ -1268,7 +1559,11 @@ export default {
]
},
{
"type": "divider"
"name": "select3",
"type": "chained-select",
"label": "链式下拉选择器",
"source": "/api/mock2/options/chainedOptions?waitSeconds=1&parentId=$parentId&level=$level&maxLevel=4",
"value": "a,b"
},
{
"type": "divider"
@ -1488,7 +1783,56 @@ export default {
"type": "input-range",
"name": "range",
"label": "范围",
"value": "50"
"value": 50
},
{
"name": "city",
"type": "input-city",
"label": "城市",
"searchable": true,
"value": 210727
},
{
"type": "location-picker",
"label": "地理位置",
"name": "location",
"ak": "LiZT5dVbGTsPI91tFGcOlSpe5FDehpf7",
"label": "地址"
},
{
"type": "divider"
},
{
"type": "chart-radios",
"label": "图表单选框",
"name": "main",
"chartValueField": "num",
"value": "a",
"options": [
{
"label": "A",
"num": 100,
"value": "a"
},
{
"label": "B",
"num": 120,
"value": "b"
},
{
"label": "C",
"num": 30,
"value": "c"
},
{
"label": "D",
"num": 40,
"value": "d"
}
]
},
{
"type": "divider"
},
{
"type": "button-toolbar",
@ -1496,7 +1840,7 @@ export default {
"buttons": [
{
"type": "button",
"label": "切换为编辑态",
"label": "切换为输入态",
"level": "primary",
"visibleOn": "${static}",
"onEvent": {
@ -1525,6 +1869,20 @@ export default {
]
}
}
},
{
"type": "button",
"label": "清空",
"onEvent": {
"click": {
"actions": [
{
"actionType": "clear",
"componentId": "myform"
}
]
}
}
}
]
}

View File

@ -9,7 +9,9 @@ import {
ActionObject,
Payload,
ClassName,
BaseApiObject
BaseApiObject,
SchemaExpression,
SchemaClassName
} from '../types';
import {filter, evalExpression} from '../utils/tpl';
import getExprProperties from '../utils/filter-schema';
@ -334,7 +336,9 @@ export interface FormSchemaBase {
/**
* className
*/
staticClassName?: string;
static?: boolean;
staticOn?: SchemaExpression;
staticClassName?: SchemaClassName;
}
export type FormGroup = FormSchemaBase & {
@ -1529,7 +1533,7 @@ export default class Form extends React.Component<FormProps, object> {
formLabelWidth: labelWidth,
controlWidth,
disabled: disabled || (control as Schema).disabled || form.loading,
static: (control as Schema).static ?? isStatic,
static: (control as Schema).static || isStatic,
btnDisabled: disabled || form.loading || form.validating,
onAction: this.handleAction,
onQuery: this.handleQuery,
@ -1601,9 +1605,8 @@ export default class Form extends React.Component<FormProps, object> {
`Form`,
`Form--${mode || 'normal'}`,
columnCount ? `Form--column Form--column-${columnCount}` : null,
className,
isStatic ? 'Form--isStatic' : null,
staticClassName && isStatic ? staticClassName : null
staticClassName && isStatic ? staticClassName : className,
isStatic ? 'Form--isStatic' : null
)}
onSubmit={this.handleFormSubmit}
noValidate
@ -1704,9 +1707,7 @@ export default class Form extends React.Component<FormProps, object> {
affixFooter,
lazyLoad,
translate: __,
footer,
static: isStatic = false,
formStore
footer
} = this.props;
let body: JSX.Element = this.renderBody();

View File

@ -25,7 +25,9 @@ import {
BaseApiObject,
BaseSchemaWithoutType,
ClassName,
Schema
Schema,
SchemaClassName,
SchemaExpression
} from '../types';
import {filter} from '../utils/tpl';
import {HocStoreFactory} from '../WithStore';
@ -466,10 +468,12 @@ export interface FormItemProps extends RendererProps {
showErrorMsg?: boolean;
// 展示态 相关
static?: boolean;
staticOn?: SchemaExpression;
staticPlaceholder?: string;
staticClassName?: string;
staticLabelClassName?: string;
staticValueClassName?: string;
staticClassName?: SchemaClassName;
staticLabelClassName?: SchemaClassName;
staticInputClassName?: SchemaClassName;
staticSchema?: any;
}
// 下发下去的属性
@ -499,6 +503,16 @@ export interface FormItemConfig extends FormItemBasicConfig {
component: FormControlComponent;
}
const getItemLabelClassName = (props: FormItemProps) => {
const {staticLabelClassName, labelClassName} = props;
return props.static && staticLabelClassName ? staticLabelClassName : labelClassName;
};
const getItemInputClassName = (props: FormItemProps) => {
const {staticInputClassName, inputClassName} = props;
return props.static && staticInputClassName ? staticInputClassName : inputClassName;
};
export class FormItemWrap extends React.Component<FormItemProps> {
reaction: Array<() => void> = [];
lastSearchTerm: any;
@ -844,7 +858,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
renderControl(): JSX.Element | null {
const {
inputClassName,
formItem: model,
classnames: cx,
children,
@ -882,7 +895,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
controlSize !== 'full'
},
model?.errClassNames,
inputClassName
getItemInputClassName(this.props)
)
});
}
@ -908,7 +921,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
captionClassName,
desc,
label,
labelClassName,
render,
required,
caption,
@ -924,9 +936,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
useMobileUI,
translate: __,
static: isStatic,
staticClassName,
staticLabelClassName,
staticValueClassName
staticClassName
} = props;
// 强制不渲染 label 的话
@ -946,13 +956,12 @@ export class FormItemWrap extends React.Component<FormItemProps> {
data-role="form-item"
className={cx(
`Form-item Form-item--horizontal`,
className,
isStatic && staticClassName ? staticClassName : className,
{
'Form-item--horizontal-justify': horizontal.justify,
[`is-error`]: model && !model.valid,
[`is-required`]: required
},
isStatic && staticClassName,
model?.errClassNames
)}
>
@ -969,8 +978,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
[`Form-itemColumn--${left}`]: !horizontal.leftFixed,
'Form-label--left': labelAlign === 'left'
},
labelClassName,
isStatic && staticLabelClassName,
getItemLabelClassName(props)
)}
style={labelWidth != null ? {width: labelWidth} : undefined}
>
@ -1003,9 +1011,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
) : null}
<div
className={cx(`Form-value`,
isStatic && staticValueClassName,
{
className={cx(`Form-value`,{
// [`Form-itemColumn--offset${getWidthRate(horizontal.offset)}`]: !label && label !== false,
[`Form-itemColumn--${right}`]:
!horizontal.leftFixed && !!right && right !== 12 - left
@ -1069,7 +1075,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
desc,
description,
label,
labelClassName,
render,
required,
caption,
@ -1085,9 +1090,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
data,
showErrorMsg,
useMobileUI,
translate: __,
static: isStatic,
staticLabelClassName
translate: __
} = props;
description = description || desc;
@ -1106,7 +1109,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
)}
>
{label && renderLabel !== false ? (
<label className={cx(`Form-label`, labelClassName, isStatic && staticLabelClassName)}>
<label className={cx(`Form-label`, getItemLabelClassName(props))}>
<span>
{label
? render(
@ -1190,7 +1193,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
desc,
description,
label,
labelClassName,
render,
required,
caption,
@ -1206,9 +1208,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
data,
showErrorMsg,
useMobileUI,
static: isStatic,
staticLabelClassName,
staticValueClassName,
translate: __
} = props;
const labelWidth = props.labelWidth || props.formLabelWidth;
@ -1229,7 +1228,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
>
{label && renderLabel !== false ? (
<label
className={cx(`Form-label`, labelClassName, isStatic && staticLabelClassName)}
className={cx(`Form-label`, getItemLabelClassName(props))}
style={labelWidth != null ? {width: labelWidth} : undefined}
>
<span>
@ -1260,7 +1259,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
</label>
) : null}
<div className={cx(`Form-value`, isStatic && staticValueClassName)}>
<div className={cx(`Form-value`)}>
{renderControl()}
{caption
@ -1318,7 +1317,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
desc,
description,
label,
labelClassName,
render,
required,
caption,
@ -1334,8 +1332,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
data,
showErrorMsg,
useMobileUI,
static: isStatic,
staticLabelClassName,
translate: __
} = props;
const labelWidth = props.labelWidth || props.formLabelWidth;
@ -1357,7 +1353,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
<div className={cx('Form-rowInner')}>
{label && renderLabel !== false ? (
<label
className={cx(`Form-label`, labelClassName, isStatic && staticLabelClassName)}
className={cx(`Form-label`, getItemLabelClassName(props))}
style={labelWidth != null ? {width: labelWidth} : undefined}
>
<span>
@ -1643,7 +1639,6 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
renderControl() {
const {
inputClassName,
formItem: model,
classnames: cx,
children,
@ -1686,7 +1681,7 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
controlSize !== 'full'
},
model?.errClassNames,
inputClassName
getItemInputClassName(this.props)
)}
></Control>
{isOpened ? this.buildSchema() : null}

View File

@ -1,4 +1,5 @@
.#{$ns}ListControl {
.#{$ns}ListControl,
.#{$ns}ListControl-static {
&-items {
display: block;
margin: calc(var(--ListControl-gutterWidth) / -2);
@ -24,13 +25,39 @@
max-width: calc(#{px2rem(200px)} + 2 * var(--ListControl-item-paddingX));
border-radius: var(--ListControl-item-borderRadius);
&:not(.is-disabled) {
cursor: pointer;
}
.b-inherit {
border-color: var(--ListControl-item-color);
}
}
&-itemImage {
margin: calc(var(--ListControl-item-paddingY) * -1)
calc(var(--ListControl-item-paddingX) * -1);
img {
display: block;
max-width: 100%;
}
}
&-itemLabel {
text-align: center;
}
&-itemImage + &-itemLabel {
margin-top: var(--ListControl-item-paddingY);
}
&-placeholder {
color: var(--Form-input-placeholderColor);
}
}
.#{$ns}ListControl {
&-item {
&:not(.is-disabled) {
cursor: pointer;
}
@include hover {
background: var(--ListControl-item-onHover-bg);
@ -97,38 +124,4 @@
}
}
}
&-itemImage {
margin: calc(var(--ListControl-item-paddingY) * -1)
calc(var(--ListControl-item-paddingX) * -1);
img {
display: block;
max-width: 100%;
}
}
&-itemLabel {
text-align: center;
}
&-itemImage + &-itemLabel {
margin-top: var(--ListControl-item-paddingY);
}
&-placeholder {
color: var(--Form-input-placeholderColor);
}
}
.#{$ns}Form-static {
.#{$ns}ListControl-item.is-disabled {
opacity: 1;
background: var(--ListControl-item-onActive-bg);
border-color: var(--ListControl-item-onDisabled-borderColor);
color: var(--ListControl-item-color);
&::before, &::before {
display: none;
}
}
}
}

View File

@ -129,6 +129,13 @@
}
}
.#{$ns}Form-static {
.#{$ns}Switch-option {
vertical-align: initial;
color: var(--text--muted-color);
}
}
.#{$ns}SwitchControl {
padding-top: calc((var(--Form-input-height) - var(--Switch-height)) / 2);

View File

@ -64,3 +64,11 @@
top: var(--Form-input-paddingY);
}
}
.#{$ns}Form-static {
.#{$ns}TextareaControl > textarea {
border: 0;
padding: 0;
resize: none;
}
}

View File

@ -37,7 +37,7 @@ interface MapPickerProps {
lng: number;
city?: string;
};
onChange: (value: any) => void;
onChange?: (value: any) => void;
}
interface LocationItem {
@ -288,7 +288,7 @@ export class BaiduMapPicker extends React.Component<
if (this.props.coordinatesType == 'gcj02') {
this.covertPoint(point, COORDINATES_BD09, COORDINATES_GCJ02).then(
(convertedPoint: any) => {
this.props?.onChange({
(typeof this.props?.onChange === 'function') && this.props.onChange({
address: loc.address.trim() || loc.title,
lat: convertedPoint.lat,
lng: convertedPoint.lng,
@ -297,7 +297,7 @@ export class BaiduMapPicker extends React.Component<
}
);
} else {
this.props?.onChange({
(typeof this.props?.onChange === 'function') && this.props?.onChange({
address: loc.address.trim() || loc.title,
lat: loc.lat,
lng: loc.lng,

View File

@ -0,0 +1,153 @@
import React from 'react';
import {anyChanged, autobind, localeable, LocaleProps} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core';
import Button from './Button';
export interface MultilineTextProps extends ThemeProps, LocaleProps {
/**
*
*/
maxRows?: number;
/**
*
*/
text: string;
/**
*
*/
expendButtonText?: string;
/**
*
*/
collapseButtonText?: string;
}
export interface MultilineTextState {
isExpend: boolean;
showBtn: boolean;
}
export class MultilineText extends React.Component<MultilineTextProps, MultilineTextState> {
static defaultProps = {
maxRows: 5,
expendButtonText: '展开',
collapseButtonText: '收起'
};
state = {
isExpend: false,
showBtn: false
};
ref?: React.RefObject<HTMLDivElement>;
constructor(props: MultilineTextProps) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
if (this.ref && this.ref.current) {
if (this.ref.current.scrollHeight > this.ref.current.clientHeight) {
this.setState({
showBtn: true
});
}
}
}
shouldComponentUpdate(nextProps: Readonly<MultilineTextProps>, nextState: Readonly<MultilineTextState>, nextContext: any): boolean {
if (
anyChanged(
['text', 'maxRows', 'expendButtonText', 'collapseButtonText', 'className'],
this.props,
nextProps
)
|| anyChanged(['isExpend', 'showBtn'], this.state, nextState)
) {
return true;
}
return false;
}
componentDidUpdate(oldProps: any, oldState: any) {
const {text, maxRows} = this.props;
if (text !== oldProps.text || maxRows !== oldProps) {
if (this.ref && this.ref.current) {
this.setState({
showBtn: this.ref.current.scrollHeight > this.ref.current.clientHeight
});
}
}
}
@autobind
toggleExpend() {
this.setState({
isExpend: !this.state.isExpend
});
}
render() {
const {
className,
text,
classnames: cx,
maxRows = 5,
expendButtonText,
collapseButtonText
} = this.props;
if (!text) {
return null;
}
const {
showBtn,
isExpend
} = this.state;
return (
<div
className={cx(
`MultilineText`,
className
)}
>
{/* 用于计算高度 */}
<div
ref={this.ref}
className={cx('white-space-pre-line', 'overflow-hidden')}
style={{
height: `${maxRows * 20}px`,
visibility: 'hidden',
position: 'absolute',
zIndex: -99
}}
>{text}</div>
{/* 用于展示 */}
<div
className={cx('white-space-pre-line', 'overflow-hidden')}
style={{
height: (showBtn && !isExpend) ? `${maxRows * 20}px` : 'auto'
}}
>{text}</div>
{showBtn &&
<div className="text-right">
<Button
className="mt-1"
level="link"
onClick={this.toggleExpend}
>{!isExpend ? expendButtonText : collapseButtonText}</Button>
</div>
}
</div>
);
}
}
export default themeable(localeable(MultilineText));

View File

@ -108,6 +108,8 @@ import Steps, {StepStatus} from './Steps';
import Tag from './Tag';
import Timeline from './Timeline';
import ImageGallery from './ImageGallery';
import BaiduMapPicker from './BaiduMapPicker';
import MultilineText from './MultilineText';
export {
NotFound,
@ -220,5 +222,7 @@ export {
StepStatus,
Tag,
Timeline,
ImageGallery
ImageGallery,
BaiduMapPicker,
MultilineText
};

View File

@ -128,6 +128,10 @@ import {
SchemaExpression
} from 'amis-core';
import type {FormSchemaBase} from 'amis-core/lib/renderers/Form';
import {WordsSchema} from './renderers/Words';
import {MultilineTextSchema} from './renderers/MultilineText';
import {DateRangeSchema} from './renderers/DateRange';
import {PasswordSchema} from './renderers/Password';
// 每加个类型,这补充一下。
export type SchemaType =
@ -163,6 +167,7 @@ export type SchemaType =
| 'static-time' // 这个几个跟表单项同名再form下面用必须带前缀 static-
| 'month'
| 'static-month' // 这个几个跟表单项同名再form下面用必须带前缀 static-
| 'date-range'
| 'dialog'
| 'spinner'
| 'divider'
@ -347,6 +352,10 @@ export type SchemaType =
| 'grid-nav'
| 'users-select'
| 'tag'
| 'tags'
| 'words'
| 'password'
| 'multiline-text'
// 原生 input 类型
| 'native-date'
@ -479,7 +488,11 @@ export type SchemaObject =
| TabsTransferPickerControlSchema
| TreeControlSchema
| TreeSelectControlSchema
| UserSelectControlSchema;
| UserSelectControlSchema
| DateRangeSchema
| MultilineTextSchema
| PasswordSchema
| WordsSchema;
export type SchemaCollection =
| SchemaObject

View File

@ -144,6 +144,10 @@ import './renderers/GridNav';
import './renderers/TooltipWrapper';
import './renderers/Tag';
import './renderers/Table2/index';
import './renderers/Words';
import './renderers/Password';
import './renderers/DateRange';
import './renderers/MultilineText';
import './compat';
import './schemaExtend';

View File

@ -0,0 +1,98 @@
import React from 'react';
import {Renderer, RendererProps} from 'amis-core';
import moment from 'moment';
import {BaseSchema} from '../Schema';
import {getPropValue} from 'amis-core';
/**
* DateRange
*/
export interface DateRangeSchema extends BaseSchema {
/**
*
*/
type: 'date-range';
/**
* moment
*/
valueFormat?: string;
/**
* moment
*/
format?: string;
/**
*
*/
delimiter?: string;
/**
*
*/
connector?: string;
}
export interface DateRangeProps
extends RendererProps,
Omit<DateRangeSchema, 'type' | 'className'> {}
export class DateRangeField extends React.Component<DateRangeProps, Object> {
refreshInterval: ReturnType<typeof setTimeout>;
static defaultProps: Pick<
DateRangeProps,
'valueFormat'| 'format' | 'connector'
> = {
format: 'YYYY-MM-DD',
valueFormat: 'X',
connector: '~'
};
render() {
let {
delimiter = ',',
connector = '~',
value,
valueFormat,
format = 'YYYY-MM-DD',
classnames: cx,
className
} = this.props;
if (!value) {
return null;
}
if (typeof value === 'string') {
value = value.split(delimiter);
}
let [startTime = '', endTime = ''] = value;
if (valueFormat) {
startTime = moment(startTime, valueFormat);
endTime = moment(endTime, valueFormat);
}
else {
startTime = moment(startTime * 1000);
endTime = moment(endTime * 1000);
}
startTime = startTime.isValid() ? startTime.format(format) : '';
endTime = endTime.isValid() ? endTime.format(format) : '';
return (
<span
className={cx('DateRangeField', className)}
>
{[startTime, endTime].join(` ${connector} `)}
</span>
);
}
}
@Renderer({
type: 'date-range'
})
export class DateRangeFieldRenderer extends DateRangeField {};

View File

@ -8,6 +8,7 @@ import type {Option} from 'amis-core';
import {ActionObject} from 'amis-core';
import {getLevelFromClassName, autobind, isEmpty} from 'amis-core';
import {ButtonGroupSchema} from '../ButtonGroup';
import {supportStatic} from './StaticHoc';
/**
*
@ -67,6 +68,7 @@ export default class ButtonGroupControl extends React.Component<
reload && reload();
}
@supportStatic()
render(props = this.props) {
const {
render,

View File

@ -12,6 +12,8 @@ import {isEffectiveApi} from 'amis-core';
import {isMobile, createObject} from 'amis-core';
import {ActionObject} from 'amis-core';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
import find from 'lodash/find';
/**
*
@ -257,6 +259,49 @@ export default class ChainedSelectControl extends React.Component<
reload && reload();
}
renderStatic(displayValue = '-') {
const {
options = [],
labelField = 'label',
valueField = 'value',
classPrefix,
classnames: cx,
className,
value,
delimiter
} = this.props;
const allOptions = [
{options, visible: true},
...(this.state.stack || [])
];
const valueArr = Array.isArray(value)
? value.concat()
: value && typeof value === 'string'
? value.split(delimiter || ',')
: [];
if (valueArr?.length > 0) {
displayValue = valueArr
.map((value: any, index) => {
const {options, visible} = allOptions[index] || {};
if (visible === false) {
return null;
}
if (!options || !options.length) {
return value;
}
const selectedOption = find(options, (o) => value === o[valueField]) || {};
return selectedOption[labelField] ?? value;
})
.filter(v => v != null)
.join(' > ');
}
return <div className={cx(`${classPrefix}SelectStaticControl`, className)}>{displayValue}</div>;
}
@supportStatic()
render() {
const {
options,

View File

@ -6,6 +6,7 @@ import {
} from 'amis-core';
import {autobind} from 'amis-core';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Radio
@ -46,6 +47,10 @@ export default class ChartRadiosControl extends React.Component<
}
highlight(index: number = this.highlightIndex) {
if (this.props.static) {
return;
}
this.highlightIndex = index;
if (!this.chart || this.prevIndex === index) {
@ -80,7 +85,8 @@ export default class ChartRadiosControl extends React.Component<
this.prevIndex = index;
}
compoonentDidMount() {
componentDidMount() {
// to do 初始化有值的情况暂时无法生效
if (this.props.selectedOptions.length) {
this.highlight(this.props.options.indexOf(this.props.selectedOptions[0]));
}
@ -92,6 +98,30 @@ export default class ChartRadiosControl extends React.Component<
}
}
renderStatic(displayValue = '-') {
this.prevIndex = -1;
this.highlightIndex = -1;
const {
options = [],
selectedOptions,
labelField = 'label',
valueField = 'value',
chartValueField
} = this.props;
if (options.length && selectedOptions.length) {
const count = options.reduce((all, cur) => {
return all + cur[chartValueField || valueField]
}, 0);
if (count > 0) {
const percent = (+selectedOptions[0][chartValueField || valueField] / count * 100).toFixed(2);
displayValue = `${selectedOptions[0][labelField]}${percent}%`;
}
}
return <>{displayValue}</>;
}
@supportStatic()
render() {
const {options, labelField, chartValueField, valueField, render} =
this.props;

View File

@ -6,6 +6,7 @@ import {withBadge, BadgeObject} from 'amis-ui';
import {autobind, createObject} from 'amis-core';
import {ActionObject} from 'amis-core';
import {BaseSchema, FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
export interface SchemaMap {
checkbox: CheckboxControlSchema;
@ -89,6 +90,37 @@ export default class CheckboxControl extends React.Component<
onChange && onChange(eventData);
}
renderStatic() {
const {
value,
trueValue,
falseValue,
option,
render,
partial,
optionType,
checked,
labelClassName
} = this.props;
return (
<Checkbox
inline
value={value || ''}
trueValue={trueValue}
falseValue={falseValue}
disabled={true}
partial={partial}
optionType={optionType}
checked={checked}
labelClassName={labelClassName}
>
{option ? render('option', option) : null}
</Checkbox>
);
}
@supportStatic()
render() {
const {
className,

View File

@ -11,6 +11,7 @@ import {
import type {ActionObject, Api, OptionsControlProps, Option} from 'amis-core';
import {Checkbox, Icon} from 'amis-ui';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
*
@ -322,6 +323,7 @@ export default class CheckboxesControl extends React.Component<
return result;
}
@supportStatic()
render() {
const {
className,

View File

@ -1009,6 +1009,7 @@ export default class ComboControl extends React.Component<ComboProps> {
conditions,
changeImmediately,
addBtnText,
static: isStatic,
translate: __
} = this.props;
@ -1164,6 +1165,10 @@ export default class ComboControl extends React.Component<ComboProps> {
}
renderDelBtn(value: any, index: number) {
if (this.props.static) {
return null;
}
const {
classPrefix: ns,
classnames: cx,
@ -1257,6 +1262,10 @@ export default class ComboControl extends React.Component<ComboProps> {
}
renderAddBtn() {
if (this.props.static) {
return null;
}
const {
classPrefix: ns,
classnames: cx,
@ -1355,7 +1364,8 @@ export default class ComboControl extends React.Component<ComboProps> {
placeholder,
translate: __,
itemClassName,
itemsWrapperClassName
itemsWrapperClassName,
static: isStatic
} = this.props;
let items = this.props.items;
@ -1372,7 +1382,7 @@ export default class ComboControl extends React.Component<ComboProps> {
multiLine ? `Combo--ver` : `Combo--hor`,
noBorder ? `Combo--noBorder` : '',
disabled ? 'is-disabled' : '',
!disabled && draggable && Array.isArray(value) && value.length > 1
!isStatic && !disabled && draggable && Array.isArray(value) && value.length > 1
? 'is-draggable'
: ''
)}
@ -1405,7 +1415,7 @@ export default class ComboControl extends React.Component<ComboProps> {
className={cx(`Combo-item`, itemClassName)}
key={this.keys[index] || (this.keys[index] = guid())}
>
{!disabled && draggable && thelist.length > 1 ? (
{!isStatic && !disabled && draggable && thelist.length > 1 ? (
<div className={cx('Combo-itemDrager')}>
<a
key="drag"
@ -1479,7 +1489,7 @@ export default class ComboControl extends React.Component<ComboProps> {
<div className={cx(`Combo-placeholder`)}>{__(placeholder)}</div>
) : null}
</div>
{!disabled ? (
{!isStatic && !disabled ? (
<div className={cx(`Combo-toolbar`)}>
{this.renderAddBtn()}
{draggable ? (
@ -1584,16 +1594,39 @@ export default class ComboControl extends React.Component<ComboProps> {
);
}
renderStatic(displayValue = '-') {
// 如有 staticSchema 会被拦截渲染schema, 不会走到这里
return this.props.render(
'static-input-kv',
{
type: 'json'
},
this.props
);
}
render() {
const {
type,
formInited,
multiple,
className,
classPrefix: ns,
classnames: cx,
disabled
static: isStatic,
staticSchema
} = this.props;
// 静态展示时
// 当有staticSchema 或 type = input-kv | input-kvs
// 才拦截处理,其他情况交给子表单项处理即可
if (
isStatic
&& (staticSchema || ['input-kv', 'input-kvs'].includes(type))
) {
return this.renderStatic();
}
return formInited || typeof formInited === 'undefined' ? (
<div className={cx(`ComboControl`, className)}>
{multiple ? this.renderMultipe() : this.renderSingle()}

View File

@ -9,6 +9,7 @@ import {ActionObject} from 'amis-core';
import {Option} from 'amis-core';
import {localeable, LocaleProps} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* City
@ -71,6 +72,21 @@ export interface CityPickerProps
useMobileUI?: boolean;
}
export interface CityDb {
province: Array<string>;
city: {
[propName: number]: Array<number>;
};
district: {
[propName: number]:
| {
[propName: number]: Array<number>;
}
| Array<number>;
};
[propName: string]: any;
}
export interface CityPickerState {
code: number;
province: string;
@ -80,21 +96,76 @@ export interface CityPickerState {
district: string;
districtCode: number;
street: string;
db?: CityDb;
}
db?: {
province: Array<string>;
city: {
[propName: number]: Array<number>;
};
district: {
[propName: number]:
| {
[propName: number]: Array<number>;
}
| Array<number>;
};
[propName: string]: any;
const getCityFromCode = ({
value,
db,
delimiter = ','
}:{
value: any;
db?: CityDb;
delimiter?: string;
}) => {
const result = {
code: 0,
province: '',
provinceCode: 0,
city: '',
cityCode: 0,
district: '',
districtCode: 0,
street: ''
};
if (!db || !value) {
return result;
}
let code =
(value && value.code) ||
(typeof value === 'number' && value) ||
(typeof value === 'string' && /(\d{6})/.test(value) && RegExp.$1);
if (code && db[code]) {
code = parseInt(code, 10);
result.code = code;
const provinceCode = code - (code % 10000);
if (db[provinceCode]) {
result.provinceCode = provinceCode;
result.province = db[provinceCode];
}
const cityCode = code - (code % 100);
if (cityCode !== provinceCode && db[cityCode]) {
result.cityCode = cityCode;
result.city = db[cityCode];
} else if (~db.city[provinceCode]?.indexOf(code)) {
result.cityCode = code;
result.city = db[code];
}
if (code % 100) {
result.district = db[code];
result.districtCode = code;
}
} else if (value) {
// todo 模糊查找
}
if (value && value.street) {
result.street = value.street;
} else if (typeof value === 'string' && ~value.indexOf(delimiter)) {
result.street = value.slice(value.indexOf(delimiter) + delimiter.length);
}
return result;
}
const loadDb = (callback: (db: any) => void): void => {
import('amis-ui/lib/components/CityDB').then(callback);
}
export class CityPicker extends React.Component<
@ -139,7 +210,7 @@ export class CityPicker extends React.Component<
return;
}
import('amis-ui/lib/components/CityDB').then(db => {
loadDb(db => {
this.setState(
{
db: {
@ -272,56 +343,11 @@ export class CityPicker extends React.Component<
return;
}
const state = {
code: 0,
province: '',
provinceCode: 0,
city: '',
cityCode: 0,
district: '',
districtCode: 0,
street: ''
};
let code =
(value && value.code) ||
(typeof value === 'number' && value) ||
(typeof value === 'string' && /(\d{6})/.test(value) && RegExp.$1);
if (code && db[code]) {
code = parseInt(code, 10);
state.code = code;
const provinceCode = code - (code % 10000);
if (db[provinceCode]) {
state.provinceCode = provinceCode;
state.province = db[provinceCode];
}
const cityCode = code - (code % 100);
if (cityCode !== provinceCode && db[cityCode]) {
state.cityCode = cityCode;
state.city = db[cityCode];
} else if (~db.city[provinceCode]?.indexOf(code)) {
state.cityCode = code;
state.city = db[code];
}
if (code % 100) {
state.district = db[code];
state.districtCode = code;
}
} else if (value) {
// todo 模糊查找
}
if (value && value.street) {
state.street = value.street;
} else if (typeof value === 'string' && ~value.indexOf(delimiter)) {
state.street = value.slice(value.indexOf(delimiter) + delimiter.length);
}
this.setState(state);
this.setState(getCityFromCode({
value,
delimiter,
db
}));
}
@autobind
@ -462,6 +488,10 @@ export interface LocationControlProps extends FormControlProps {
allowStreet?: boolean;
}
export class LocationControl extends React.Component<LocationControlProps> {
state = {
db: null
}
@autobind
doAction(action: ActionObject, data: object, throwErrors: boolean) {
const {resetValue, onChange} = this.props;
@ -492,6 +522,51 @@ export class LocationControl extends React.Component<LocationControlProps> {
onChange(value);
}
renderStatic(displayValue = '') {
const {value, delimiter} = this.props;
if (!this.state.db) {
loadDb(db => {
this.setState(
{
db: {
...db.default,
province: db.province as any,
city: db.city,
district: db.district
}
}
);
});
return <Spinner size='sm' />;
}
if (!value) {
return <>{displayValue}</>;
}
const {
province,
city,
district,
street
} = getCityFromCode({
value,
delimiter,
db: this.state.db
});
return (
<>
{
[province, city, district, street]
.filter(v => !!v)
.join(delimiter)
}
</>
);
}
@supportStatic()
render() {
const {
value,

View File

@ -5,7 +5,7 @@ import {FormItem, FormControlProps} from 'amis-core';
import type {PresetColor} from 'amis-ui';
import {isMobile} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import renderStaticHoc from './StaticHoc';
import {supportStatic} from './StaticHoc';
// todo amis-ui 里面组件直接改成按需加载
export const ColorPicker = React.lazy(
@ -71,15 +71,7 @@ export default class ColorControl extends React.PureComponent<
open: false
};
@renderStaticHoc()
renderStatic() {
return this.props.render(
'static-color',
{type: 'color'},
this.props
);
}
@supportStatic()
render() {
const {
className,
@ -93,25 +85,21 @@ export default class ColorControl extends React.PureComponent<
const mobileUI = useMobileUI && isMobile();
return (
<div className={cx(`${ns}ColorControl`, className)}>
{
isStatic
? this.renderStatic()
: <Suspense fallback={<div>...</div>}>
<ColorPicker
classPrefix={ns}
{...rest}
useMobileUI={useMobileUI}
popOverContainer={
mobileUI && env && env.getModalContainer
? env.getModalContainer
: mobileUI
? undefined
: rest.popOverContainer
}
value={value || ''}
/>
</Suspense>
}
<Suspense fallback={<div>...</div>}>
<ColorPicker
classPrefix={ns}
{...rest}
useMobileUI={useMobileUI}
popOverContainer={
mobileUI && env && env.getModalContainer
? env.getModalContainer
: mobileUI
? undefined
: rest.popOverContainer
}
value={value || ''}
/>
</Suspense>
</div>
);
}

View File

@ -8,6 +8,7 @@ import {DatePicker} from 'amis-ui';
import {FormBaseControlSchema, SchemaObject} from '../../Schema';
import {createObject, anyChanged, isMobile, autobind} from 'amis-core';
import {ActionObject} from 'amis-core';
import {supportStatic} from './StaticHoc';
export interface InputDateBaseControlSchema extends FormBaseControlSchema {
/**
@ -455,6 +456,7 @@ export default class DateControl extends React.PureComponent<
this.props.onChange(nextValue);
}
@supportStatic()
render() {
let {
className,

View File

@ -8,6 +8,7 @@ import {isMobile, createObject, autobind} from 'amis-core';
import {ActionObject} from 'amis-core';
import type {ShortCuts} from 'amis-ui/lib/components/DatePicker';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* DateRange
@ -232,6 +233,7 @@ export default class DateRangeControl extends React.Component<DateRangeProps> {
this.props.onChange(nextValue);
}
@supportStatic()
render() {
const {
className,

View File

@ -4,6 +4,7 @@ import cx from 'classnames';
import {filterDate, parseDuration} from 'amis-core';
import InputDateRange, {DateRangeControlSchema} from './InputDateRange';
import {DateRangePicker} from 'amis-ui';
import {supportStatic} from './StaticHoc';
/**
* MonthRange
@ -16,6 +17,7 @@ export interface MonthRangeControlSchema
}
export default class MonthRangeControl extends InputDateRange {
@supportStatic()
render() {
const {
className,

View File

@ -13,6 +13,7 @@ import {
ActionObject
} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
*
@ -296,7 +297,18 @@ export default class NumberControl extends React.Component<
}
this.input.focus();
}
render(): JSX.Element {
renderStatic(displayValue = '-') {
const {unit, value} = this.props;
const finalValue =
unit && value && typeof value === 'string'
? value.replace(unit, '')
: value;
return <>{finalValue || displayValue}</>;
}
@supportStatic()
render() {
const {
className,
classPrefix: ns,

View File

@ -4,6 +4,7 @@ import cx from 'classnames';
import {filterDate, parseDuration} from 'amis-core';
import InputDateRange, {DateRangeControlSchema} from './InputDateRange';
import {DateRangePicker} from 'amis-ui';
import {supportStatic} from './StaticHoc';
/**
* QuarterRange
* https://baidu.gitee.io/amis/docs/components/form/input-quarter-range
@ -14,6 +15,7 @@ export interface QuarterRangeControlSchema
}
export default class QuarterRangeControl extends InputDateRange {
@supportStatic()
render() {
const {
className,

View File

@ -13,6 +13,7 @@ import {autobind, createObject} from 'amis-core';
import {filter} from 'amis-core';
import {FormBaseControlSchema, SchemaObject} from '../../Schema';
import {ActionObject} from 'amis-core';
import {supportStatic} from './StaticHoc';
/**
* Range
@ -599,6 +600,7 @@ export default class RangeControl extends React.PureComponent<
: value;
}
@supportStatic()
render() {
const {value} = this.state;
const props: RangeItemProps = {

View File

@ -5,6 +5,7 @@ import {ActionObject} from 'amis-core';
import {Rating} from 'amis-ui';
import type {textPositionType} from 'amis-ui/lib/components/Rating';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Rating
@ -117,6 +118,43 @@ export default class RatingControl extends React.Component<RatingProps, any> {
onChange?.(value);
}
renderStatic() {
const {
className,
value,
count,
half,
char,
inactiveColor,
colors,
texts,
charClassName,
textClassName,
textPosition,
classnames: cx
} = this.props;
return (
<div className={cx('RatingControl', className)}>
<Rating
classnames={cx}
value={value}
disabled={true}
count={count}
half={half}
char={char}
inactiveColor={inactiveColor}
colors={colors}
texts={texts}
charClassName={charClassName}
textClassName={textClassName}
textPosition={textPosition}
/>
</div>
);
}
@supportStatic()
render() {
const {
className,

View File

@ -2,14 +2,12 @@ import React from 'react';
import {
OptionsControl,
OptionsControlProps,
Option,
FormOptionsControl
Option
} from 'amis-core';
import Downshift from 'downshift';
import find from 'lodash/find';
import isInteger from 'lodash/isInteger';
import unionWith from 'lodash/unionWith';
import isEqual from 'lodash/isEqual';
import {findDOMNode} from 'react-dom';
import {ResultBox} from 'amis-ui';
import {autobind, filterTree, createObject} from 'amis-core';
@ -19,6 +17,7 @@ import {PopOver} from 'amis-core';
import {ListMenu} from 'amis-ui';
import {ActionObject} from 'amis-core';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Tag
@ -425,6 +424,7 @@ export default class TagControl extends React.PureComponent<
return max != null && isInteger(max) && selectedOptions.length >= max;
}
@supportStatic()
render() {
const {
className,

View File

@ -19,7 +19,7 @@ import {ActionSchema} from '../Action';
import {FormOptionsSchema, SchemaApi} from '../../Schema';
import {generateIcon} from 'amis-core';
import {rendererEventDispatcher, bindRendererEvent} from 'amis-core';
import renderStaticHoc from './StaticHoc';
import {supportStatic} from './StaticHoc';
import type {Option} from 'amis-core';
import type {ListenerAction} from 'amis-core';
@ -892,27 +892,11 @@ export default class TextControl extends React.PureComponent<
);
}
@renderStaticHoc()
renderStatic(displayValue = '-'): JSX.Element {
return (
<>
{
this.props.type === 'input-password'
? '********'
: displayValue
}
</>
);
}
render(): JSX.Element {
renderBody(body: JSX.Element) {
const {
classnames: cx,
className,
classPrefix: ns,
options,
source,
autoComplete,
addOn: addOnRaw,
render,
data,
@ -929,12 +913,6 @@ export default class TextControl extends React.PureComponent<
}
: addOnRaw;
let input = isStatic
? this.renderStatic()
: autoComplete !== false && (source || options?.length || autoComplete)
? this.renderSugestMode()
: this.renderNormal();
const iconElement = generateIcon(cx, addOn?.icon, 'Icon');
let addOnDom = addOn && !isStatic ? (
@ -954,23 +932,42 @@ export default class TextControl extends React.PureComponent<
) : null;
if (inputOnly) {
return input;
return body;
}
return (
<div
className={cx(className, `${ns}TextControl`, {
const classNames = !isStatic
? cx(className, `${ns}TextControl`, {
[`${ns}TextControl--withAddOn`]: !!addOnDom,
'is-focused': this.state.isFocused,
'is-disabled': disabled
})}
>
})
: cx(`${ns}TextControl`, {
[`${ns}TextControl--withAddOn`]: !!addOnDom
});
return (
<div className={classNames}>
{addOn && addOn.position === 'left' ? addOnDom : null}
{input}
{body}
{addOn && addOn.position !== 'left' ? addOnDom : null}
</div>
);
}
@supportStatic()
render(): JSX.Element {
const {
options,
source,
autoComplete,
} = this.props;
let input = autoComplete !== false && (source || options?.length || autoComplete)
? this.renderSugestMode()
: this.renderNormal();
return this.renderBody(input);
}
}
export function mapItemIndex(

View File

@ -12,6 +12,7 @@ import {
} from 'amis-core';
import {Spinner} from 'amis-ui';
import {FormOptionsSchema, SchemaApi} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Tree
@ -179,6 +180,7 @@ export default class TreeControl extends React.Component<TreeProps> {
}
}
@supportStatic()
render() {
const {
className,

View File

@ -4,6 +4,7 @@ import cx from 'classnames';
import {filterDate, parseDuration} from 'amis-core';
import InputDateRange, {DateRangeControlSchema} from './InputDateRange';
import {DateRangePicker} from 'amis-ui';
import {supportStatic} from './StaticHoc';
/**
* YearRange
@ -15,6 +16,7 @@ export interface YearRangeControlSchema
}
export default class YearRangeControl extends InputDateRange {
@supportStatic()
render() {
const {
className,

View File

@ -12,6 +12,7 @@ import {
SchemaClassName,
SchemaCollection
} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* List
@ -94,6 +95,78 @@ export default class ListControl extends React.Component<ListProps, any> {
reload && reload();
}
renderStatic(displayValue = '-') {
const {
itemSchema,
labelField,
valueField,
imageClassName,
itemClassName,
selectedOptions,
classnames: cx,
render,
data
} = this.props;
if (!selectedOptions.length) {
return displayValue;
}
const itemRender = (option: Option, key: number) => {
let label = option[labelField || 'label'];
label = label || `选项${key + 1}`;
if (itemSchema || option.body || option.image) {
return (
<div
key={key}
className={cx(
'ListControl-static-item',
itemClassName
)}
>
{itemSchema
? render(`${key}/body`, itemSchema, {
data: createObject(data, option)
})
: option.body
? render(`${key}/body`, option.body)
: [(option.image
? <div key="image"
className={cx('ListControl-itemImage', imageClassName)}
>
<img src={option.image} alt={label} />
</div>
: null
),
(
<div key="label"
className={cx('ListControl-itemLabel')}
>
{label}
</div>
)
]
}
</div>
);
}
return (
<div
key={key}
className={cx(`ListControl-static-item`)}
>
{label}
</div>
);
}
return <div className={cx('StaticList')}>
{selectedOptions.map(itemRender)}
</div>
}
@supportStatic()
render() {
const {
render,

View File

@ -1,9 +1,10 @@
import React from 'react';
import {themeable, ClassNamesFn, ThemeProps} from 'amis-core';
import {themeable, ClassNamesFn, ThemeProps, Overlay, PopOver, autobind} from 'amis-core';
import {FormItem, FormBaseControl, FormControlProps} from 'amis-core';
import {LocationPicker} from 'amis-ui';
import {LocationPicker, Alert2, BaiduMapPicker, Icon} from 'amis-ui';
import {filter} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Location
* https://baidu.gitee.io/amis/docs/components/form/location
@ -41,7 +42,89 @@ export class LocationControl extends React.Component<LocationControlProps> {
vendor: 'baidu',
coordinatesType: 'bd09'
};
domRef: React.RefObject<HTMLDivElement> = React.createRef();
state = {
isOpened: false
};
@autobind
close() {
this.setState({
isOpened: false
});
}
@autobind
open() {
this.setState({
isOpened: true
});
}
@autobind
handleClick() {
this.state.isOpened ? this.close() : this.open();
}
@autobind
getParent() {
return this.domRef.current?.parentElement;
}
@autobind
getTarget() {
return this.domRef.current;
}
renderStatic(displayValue = '-') {
const {
classnames: cx,
value,
vendor,
ak,
coordinatesType,
popOverContainer,
} = this.props;
const __ = this.props.translate;
if (!value) {
return <>{displayValue}</>;
}
return (
<div className={this.props.classnames('LocationControl')} ref={this.domRef}>
<span>{value.address}</span>
<a className={cx('LocationPicker-toggler', 'ml-1')} onClick={this.handleClick}>
<Icon icon="location" className="icon" />
</a>
<Overlay
target={this.getTarget}
container={popOverContainer || this.getParent}
rootClose={false}
show={this.state.isOpened}
>
<PopOver
className={cx('LocationPicker-popover')}
onHide={this.close}
overlay
style={{width: this.getTarget()?.offsetWidth}}
>
{vendor === 'baidu' ? (
<BaiduMapPicker
ak={ak}
value={value}
coordinatesType={coordinatesType}
/>
) : (
<Alert2>{__('${vendor} 地图控件不支持', {vendor})}</Alert2>
)}
</PopOver>
</Overlay>
</div>
);
}
@supportStatic()
render() {
return (
<div className={this.props.classnames('LocationControl')}>

View File

@ -10,6 +10,7 @@ import {Checkbox, Spinner} from 'amis-ui';
import {autobind, setVariable, createObject} from 'amis-core';
import {ApiObject, ActionObject} from 'amis-core';
import {FormBaseControlSchema, SchemaApi} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Matrix
@ -286,7 +287,7 @@ export default class MatrixCheckbox extends React.Component<
this.props.onChange(value.concat());
}
renderInput() {
renderInput(forceDisabled = false) {
const {columns, rows} = this.state;
const {rowLabel, disabled, classnames: cx, multiple} = this.props;
@ -321,7 +322,7 @@ export default class MatrixCheckbox extends React.Component<
<td key={x} className="text-center">
<Checkbox
type={multiple ? 'checkbox' : 'radio'}
disabled={disabled}
disabled={forceDisabled || disabled}
checked={
!!(value[x] && value[x][y] && value[x][y].checked)
}
@ -340,9 +341,22 @@ export default class MatrixCheckbox extends React.Component<
);
}
renderStatic(displayValue = '-') {
const {className, render, classnames: cx} = this.props;
const {error} = this.state;
return (
<div key="input" className={cx('MatrixControl', className || '')}>
{error
? displayValue
: this.renderInput(true)
}
</div>
);
}
@supportStatic()
render() {
const {className, render, classnames: cx} = this.props;
const {error, loading} = this.state;
return (

View File

@ -30,6 +30,7 @@ import {RootClose} from 'amis-core';
import {Cascader} from 'amis-ui';
import {ActionObject} from 'amis-core';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Nested Select
@ -862,6 +863,7 @@ export default class NestedSelectControl extends React.Component<
);
}
@supportStatic()
render() {
const {
className,

View File

@ -10,6 +10,7 @@ import {
import {autobind, isEmpty, createObject} from 'amis-core';
import {ActionObject} from 'amis-core';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Radio
@ -87,6 +88,7 @@ export default class RadiosControl extends React.Component<RadiosProps, any> {
reload && reload();
}
@supportStatic()
render() {
const {
className,

View File

@ -20,6 +20,7 @@ import {TransferDropDown} from 'amis-ui';
import type {SchemaClassName} from '../../Schema';
import type {TooltipObject} from 'amis-ui/lib/components/TooltipWrapper';
import {supportStatic} from './StaticHoc';
/**
* Select
@ -398,6 +399,7 @@ export default class SelectControl extends React.Component<SelectProps, any> {
}
}
@supportStatic()
render() {
let {
autoComplete,

View File

@ -1,23 +1,72 @@
import React from "react";
import {getPropValue, FormControlProps} from "amis-core";
export interface renderStaticHocProps {
/**
* paddingY
*
*/
staticNoPaddingY?: boolean;
function renderCommonStatic(props: any, defaultValue: string) {
const {
type,
render,
staticSchema
} = props;
const staticProps = {
...props,
...staticSchema
};
switch(type) {
case 'select':
case 'checkboxes':
case 'button-group-select':
case 'input-tree':
case 'tree-select':
case 'nested-select':
case 'cascader-select':
case 'radios':
case 'multi-select':
case 'transfer':
case 'transfer-picker':
case 'tabs-transfer':
case 'tabs-transfer-picker':
return render('static-select', {type: 'words'}, staticProps);
case 'input-date':
case 'input-datetime':
case 'input-time':
case 'input-month':
case 'input-quarter':
case 'input-year':
return renderStaticDateTypes(staticProps);
case 'input-date-range':
case 'input-datetime-range':
case 'input-time-range':
case 'input-month-range':
case 'input-quarter-range':
case 'input-year-range':
return render('static-input-date-range', {type: 'date-range'}, {
...props,
valueFormat: props.format,
format: props.inputFormat,
...staticSchema
});
case 'input-password':
return render('static-input-password', {type: 'password'}, staticProps);
case 'input-color':
return render('static-color', {type: 'color'}, staticProps);
case 'input-tag':
return render('static-input-tag', {type: 'tags'}, staticProps);
default:
return defaultValue;
}
}
/**
*
* render支持静态展示装饰
*/
export default function renderStaticHoc<T extends FormControlProps>(
hocProps:renderStaticHocProps = {
staticNoPaddingY: false
}
) {
const {staticNoPaddingY} = hocProps;
export function supportStatic<T extends FormControlProps>() {
return function (
target: any,
name: string,
@ -26,27 +75,57 @@ export default function renderStaticHoc<T extends FormControlProps>(
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const props = (this as TypedPropertyDescriptor<any> & {props: T}).props;
const {
staticSchema,
render,
classPrefix: ns,
classnames: cx,
staticPlaceholder = '-'
} = props;
if (props.static) {
const {
render,
staticSchema,
classPrefix: ns,
classnames: cx,
className,
staticPlaceholder = '-'
} = props;
let body;
const displayValue = getPropValue(props) || staticPlaceholder;
const body = staticSchema
// 外部传入自定义展示态schema
? render('form-static-schema', staticSchema, props)
// 预置展示态
: original.apply(this, [...args, displayValue]);
const displayValue = getPropValue(props);
if (!displayValue) {
body = staticPlaceholder;
} else {
// 自定义了schema并且有type
if (staticSchema && (
staticSchema.type
|| Array.isArray(staticSchema)
|| typeof staticSchema === 'string'
|| typeof staticSchema === 'number'
)) {
body = render('form-static-schema', staticSchema, props);
} else if (target.renderStatic) {
// 特殊组件
body = target.renderStatic.apply(this, [...args, displayValue]);
} else {
// 可复用组件
body = renderCommonStatic(props, displayValue);
}
}
return <div className={cx(`${ns}Form-static`, {
'is-noPaddingY-static': staticNoPaddingY
})}>
{body}
</div>
return <div className={cx(`${ns}Form-static`, className)}>{body}</div>
}
return original.apply(this, args);
}
return descriptor;
}
}
function renderStaticDateTypes(props: any) {
const {render, type, inputFormat, timeFormat, format, value} = props;
return render(
'static-input-date',
{
type: 'date',
value,
format: type === 'time' && timeFormat ? timeFormat : inputFormat,
valueFormat: format
}
);
}

View File

@ -5,7 +5,7 @@ import {createObject, autobind, isObject} from 'amis-core';
import {generateIcon} from 'amis-core';
import {IconSchema} from '../Icon';
import {FormBaseControlSchema} from '../../Schema';
import renderStaticHoc from './StaticHoc';
import {supportStatic} from './StaticHoc';
/**
* Switch
@ -79,19 +79,50 @@ export default class SwitchControl extends React.Component<SwitchProps, any> {
onChange && onChange(checked);
}
@renderStaticHoc({
staticNoPaddingY: true
})
getResult() {
const {
classnames: cx,
onText,
offText,
} = this.props;
const on = isObject(onText)
? generateIcon(cx, onText.icon, 'Switch-icon')
: onText;
const off = isObject(offText)
? generateIcon(cx, offText.icon, 'Switch-icon')
: offText;
return {on, off};
}
renderBody(children: any) {
const {
classnames: cx,
option,
optionAtLeft
} = this.props;
const Option = <span className={cx('Switch-option')}>{option}</span>;
return (
<>
{optionAtLeft ? Option : null}
{children}
{optionAtLeft ? null : Option}
</>
);
}
renderStatic() {
const {
value,
trueValue,
on = '开',
off = '关'
} = this.props;
return <>{value === trueValue ? on : off}</>;
const {on = '开', off = '关'} = this.getResult();
const body = <span>{value === trueValue ? on : off}</span>;
return this.renderBody(body);
}
@supportStatic()
render() {
const {
size,
@ -101,46 +132,26 @@ export default class SwitchControl extends React.Component<SwitchProps, any> {
value,
trueValue,
falseValue,
onText,
offText,
option,
onChange,
disabled,
optionAtLeft,
static: isStatic
} = this.props;
const on = isObject(onText)
? generateIcon(cx, onText.icon, 'Switch-icon')
: onText;
const off = isObject(offText)
? generateIcon(cx, offText.icon, 'Switch-icon')
: offText;
const {on, off} = this.getResult();
return (
<div className={cx(`SwitchControl`, className)}>
{optionAtLeft ? (
<span className={cx('Switch-option')}>{option}</span>
) : null}
{
isStatic
? this.renderStatic()
: <Switch
classPrefix={ns}
value={value}
trueValue={trueValue}
falseValue={falseValue}
onText={on}
offText={off}
disabled={disabled}
onChange={this.handleChange}
size={size as any}
/>
}
{optionAtLeft ? null : (
<span className={cx('Switch-option')}>{option}</span>
{this.renderBody(
<Switch
classPrefix={ns}
value={value}
trueValue={trueValue}
falseValue={falseValue}
onText={on}
offText={off}
disabled={disabled}
onChange={this.handleChange}
size={size as any}
/>
)}
</div>
);

View File

@ -17,6 +17,7 @@ import {
import {Selection as BaseSelection} from 'amis-ui';
import {ActionObject} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {supportStatic} from './StaticHoc';
/**
* TabsTransfer
@ -268,6 +269,7 @@ export class TabsTransferRenderer extends BaseTabsTransferRenderer<TabsTransferP
}
}
@supportStatic()
render() {
const {
className,

View File

@ -8,6 +8,7 @@ import {autobind, createObject} from 'amis-core';
import {Selection as BaseSelection} from 'amis-ui';
import {ActionObject} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {supportStatic} from './StaticHoc';
/**
* TabsTransferPicker 穿
@ -80,6 +81,7 @@ export class TabsTransferPickerRenderer extends BaseTabsTransferRenderer<TabsTra
}
}
@supportStatic()
render() {
const {
className,

View File

@ -3,11 +3,12 @@ import {FormItem, FormControlProps, FormBaseControl} from 'amis-core';
import {Textarea} from 'amis-ui';
import {autobind, ucFirst} from 'amis-core';
import {autobind} from 'amis-core';
import {bindRendererEvent} from 'amis-core';
import type {ListenerAction} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* TextArea
@ -149,9 +150,22 @@ export default class TextAreaControl extends React.Component<
);
}
renderStatic(displayValue = '') {
const {
render,
staticSchema = {}
} = this.props;
return render('static-textarea', {
type: 'multiline-text',
text: displayValue,
maxRows: staticSchema.limit || 5
}, staticSchema);
}
@supportStatic()
render() {
const {...rest} = this.props;
return (
<Textarea
{...rest}

View File

@ -26,6 +26,7 @@ import {Selection as BaseSelection} from 'amis-ui';
import {ResultList} from 'amis-ui';
import {ActionObject} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {supportStatic} from './StaticHoc';
/**
* Transfer
@ -384,6 +385,7 @@ export class BaseTransferRenderer<
}
}
@supportStatic()
render() {
let {
className,

View File

@ -5,6 +5,7 @@ import {BaseTransferRenderer, TransferControlSchema} from './Transfer';
import {TransferPicker} from 'amis-ui';
import {autobind} from 'amis-core';
import {ActionObject} from 'amis-core';
import {supportStatic} from './StaticHoc';
/**
* TransferPicker 穿
@ -58,6 +59,7 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
}
}
@supportStatic()
render() {
const {
className,

View File

@ -23,6 +23,7 @@ import {findDOMNode} from 'react-dom';
import {normalizeOptions} from 'amis-core';
import {ActionObject} from 'amis-core';
import {FormOptionsSchema} from '../../Schema';
import {supportStatic} from './StaticHoc';
/**
* Tree
@ -598,6 +599,7 @@ export default class TreeSelectControl extends React.Component<
);
}
@supportStatic()
render() {
const {
className,

View File

@ -0,0 +1,51 @@
/**
* @file MultilineText
*/
import React from 'react';
import {Renderer, RendererProps, resolveVariableAndFilter} from 'amis-core';
import {BaseSchema} from '../Schema';
import {MultilineText} from 'amis-ui';
/**
* MultilineText
*/
export interface MultilineTextSchema extends BaseSchema {
type: 'multiline-text';
/**
*
*/
text?: string;
/**
*
*/
maxRows?: number;
/**
*
*/
expendButtonText?: string;
/**
*
*/
collapseButtonText?: string;
}
export interface MultilineTextProps
extends RendererProps,
Omit<MultilineTextSchema, 'type' | 'className'> {}
export class MultilineTextField extends React.Component<MultilineTextProps, object> {
render() {
const {data, text: originText} = this.props;
const text = resolveVariableAndFilter(originText, data, '| raw');
return <MultilineText {...this.props} text={text} />;
}
}
@Renderer({
type: 'multiline-text'
})
export class MultilineTextFieldRenderer extends MultilineTextField {}

View File

@ -0,0 +1,60 @@
/**
* @file Password
*/
import React from 'react';
import {autobind, Renderer, RendererProps} from 'amis-core';
import {BaseSchema} from '../Schema';
import {Icon} from 'amis-ui';
/**
* Password
*/
export interface PasswordSchema extends BaseSchema {
type: 'password';
/**
*
*/
mosaicText?: string;
}
export interface PasswordProps
extends RendererProps,
Omit<PasswordSchema, 'type' | 'className'> {}
export class PasswordField extends React.Component<PasswordProps, object> {
state = {
visible: false
};
@autobind
toggleVisible() {
this.setState({
visible: !this.state.visible
});
}
render() {
const {
classnames: cx,
className,
mosaicText = '********',
value
} = this.props;
return (
<span className={cx('Password-field', className)}>
{this.state.visible ? value : mosaicText}
{this.state.visible
? <Icon icon="view" className="icon" onClick={this.toggleVisible} />
: <Icon icon="invisible" className="icon" onClick={this.toggleVisible} />
}
</span>
);
}
}
@Renderer({
type: 'password'
})
export class PasswordFieldRenderer extends PasswordField {}

View File

@ -0,0 +1,260 @@
/**
* @file Words
*/
import React, {Fragment} from 'react';
import {autobind, Renderer, RendererProps, Option, getTreeAncestors, resolveVariableAndFilter} from 'amis-core';
import {BaseSchema} from '../Schema';
import {PlainObject} from '../types';
import {Tag} from 'amis-ui';
type Words = string | string[];
/**
* Words
*/
export interface WordsSchema extends BaseSchema {
type: 'words';
/**
* , 0
*/
limit?: number;
/**
*
*/
expendButtonText?: string;
/**
*
*/
expendButton?: PlainObject;
/**
*
*/
collapseButtonText?: string;
/**
*
*/
collapseButton?: PlainObject;
/**
* tags数据
*/
words: Words;
/**
* useTag 使tag的方式展示
*/
inTag?: boolean | PlainObject;
/**
*
*/
delimiter?: string | JSX.Element;
}
export interface WordsProps
extends RendererProps,
Omit<WordsSchema, 'type' | 'className'> {}
function getLabel(item: Option, index: number, {
type,
labelField = 'label',
options = [],
enableNodePath,
hideNodePathLabel,
pathSeparator = '/',
}: any): string {
if (enableNodePath
|| (type === 'nested-select' && !hideNodePathLabel)
){
// 将所有祖先节点也展现出来
const ancestors = getTreeAncestors(options, item, true);
return `${
ancestors
? ancestors.map(item => `${item[labelField || 'label']}`).join(` ${pathSeparator} `)
: item[labelField || 'label']
}`;
}
return item[labelField] || `选项${index}`;
}
export class WordsField extends React.Component<WordsProps, object> {
static defaultProps: Partial<WordsProps> = {
inTag: false
};
state = {
isExpend: false
};
@autobind
toggleExpend() {
this.setState({
isExpend: !this.state.isExpend
});
}
getLimit(words: Words) {
const {limit} = this.props;
return limit ?? (Array.isArray(words) ? 10 : 200);
}
renderContent(words: Words) {
const {
delimiter,
inTag
} = this.props;
// 纯文字展示
if (!Array.isArray(words)) {
return words;
}
// 不使用tag时默认用 逗号连接
if (!inTag) {
return words.map((item, key) => {
return <Fragment key={key}>
{item}
{delimiter ? delimiter : ' '}
</Fragment>
})
}
return words.map((label, key) => (
// 使用tag展示时默认不使用连接符
<Tag
key={key}
label={label}
className={'mb-1'}
{...typeof inTag === 'object' ? inTag : undefined}
></Tag>
));
}
renderAll(words: Words, hasBtn = false) {
const {
collapseButtonText = '收起',
collapseButton,
render
} = this.props;
return (
<>
{this.renderContent(words)}
{!hasBtn ? null :
render('collapseBtn', {
type: 'button',
level: 'link',
className: 'ml-1'
}, {
onClick: this.toggleExpend,
...collapseButton,
label: collapseButtonText
})
}
</>
);
}
renderPart(words: Words) {
const {
expendButtonText = '展开',
expendButton,
render
} = this.props;
const limit = this.getLimit(words);
let partContent = Array.isArray(words)
? words.slice(0, limit)
: words.toString().slice(0, limit);
return (
<>
{this.renderContent(partContent)}
&nbsp;...
{render('collapseBtn', {
type: 'button',
level: 'link',
className: 'ml-1'
}, {
onClick: this.toggleExpend,
...expendButton,
label: expendButtonText
})}
</>
)
}
getWords() {
const {
selectedOptions = [],
words: oldWords,
data
} = this.props;
let words;
if (typeof oldWords === 'string') {
words = resolveVariableAndFilter(oldWords, data, '| raw');
}
if (words) {
return words;
}
if (selectedOptions?.length > 0) {
return selectedOptions
.map((option: Option, index: number) => getLabel(option, index, this.props));
}
return null;
}
render() {
const {
classnames: cx,
className
} = this.props;
const words = this.getWords();
if (!words) {
return null;
}
const limit = this.getLimit(words);
let body;
if (
!limit
|| (Array.isArray(words) && words.length <= limit)
|| (!Array.isArray(words) && words.toString().length <= limit)
) {
// 渲染全部,且无展开收起按钮
body = this.renderAll(words);
}
else {
body = this.state.isExpend
? this.renderAll(words, true)
: this.renderPart(words);
}
return <div className={cx('Words-field', className)}>{body}</div>
}
}
@Renderer({
type: 'words'
})
export class WordsRenderer extends WordsField {}
@Renderer({
type: 'tags'
})
export class TagsRenderer extends WordsField {
static defaultProps: Partial<WordsProps> = {
inTag: true
}
}