Merge branch 'baidu:master' into master

This commit is contained in:
zhou999 2021-12-30 16:39:58 +08:00 committed by GitHub
commit d3dfb0da7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 8498 additions and 1379 deletions

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

View File

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx pretty-quick --staged
npx lint-staged

View File

@ -80,6 +80,11 @@ npm run update-snapshot
- [RickCole21](https://github.com/RickCole21)
- [catchonme](https://github.com/catchonme)
- [nwind](https://github.com/nwind)
- [zhangtao07](https://github.com/zhangtao07)
- [hsm-lv](https://github.com/hsm-lv)
- [RUNZE LU](https://github.com/lurunze1226)
- [ucasliyuan](https://github.com/ucasliyuan)
- [yangwei9012](https://github.com/yangwei9012)
## 低代码平台

View File

@ -362,6 +362,72 @@ exports[`Renderer:formula 1`] = `
</div>
</div>
</div>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
sum4
</span>
</span>
</span>
</label>
<div
class="cxd-NumberControl cxd-Form-control"
>
<div
class="cxd-Number cxd-Number--borderFull"
>
<div
class="cxd-Number-handler-wrap"
>
<span
aria-disabled="false"
aria-label="Increase Value"
class="cxd-Number-handler cxd-Number-handler-up"
role="button"
unselectable="on"
>
<span
class="cxd-Number-handler-up-inner"
unselectable="on"
/>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="cxd-Number-handler cxd-Number-handler-down"
role="button"
unselectable="on"
>
<span
class="cxd-Number-handler-down-inner"
unselectable="on"
/>
</span>
</div>
<div
class="cxd-Number-input-wrap"
>
<input
aria-valuenow="5"
autocomplete="off"
class="cxd-Number-input"
role="spinbutton"
step="1"
value="5"
/>
</div>
</div>
</div>
</div>
</form>
</div>
<div

View File

@ -53,43 +53,53 @@ exports[`Renderer:rating 1`] = `
class="cxd-RatingControl cxd-Form-control"
>
<div
class=""
class="cxd-Rating"
>
<span
class="cxd-Rating is-active"
data-forhalf="★"
data-index="0"
>
</span>
<span
class="cxd-Rating is-active"
data-forhalf="★"
data-index="1"
>
</span>
<span
class="cxd-Rating is-active"
data-forhalf="★"
data-index="2"
>
</span>
<span
class="cxd-Rating"
data-forhalf="★"
data-index="3"
>
</span>
<span
class="cxd-Rating"
data-forhalf="★"
data-index="4"
>
</span>
<ul>
<li
class="cxd-Rating-star is-active"
style="color: rgb(120, 123, 129);"
>
<icon-mock
classname="icon icon-star"
icon="star"
/>
</li>
<li
class="cxd-Rating-star is-active"
style="color: rgb(120, 123, 129);"
>
<icon-mock
classname="icon icon-star"
icon="star"
/>
</li>
<li
class="cxd-Rating-star is-active"
style="color: rgb(120, 123, 129);"
>
<icon-mock
classname="icon icon-star"
icon="star"
/>
</li>
<li
class="cxd-Rating-star"
>
<icon-mock
classname="icon icon-star"
icon="star"
/>
</li>
<li
class="cxd-Rating-star"
>
<icon-mock
classname="icon icon-star"
icon="star"
/>
</li>
</ul>
</div>
</div>
</div>

View File

@ -36,6 +36,11 @@ test('Renderer:formula', async () => {
name: 'sum3',
label: 'sum3'
},
{
type: 'number',
name: 'sum4',
label: 'sum4'
},
{
type: 'formula',
name: 'sum1',
@ -55,6 +60,13 @@ test('Renderer:formula', async () => {
condition: 'data.b',
value: 0,
formula: 'a + b + 2'
},
{
type: 'formula',
name: 'sum4',
condition: 'data.b',
value: 0,
formula: '${a + b + 2}'
}
],
title: 'The form',
@ -80,6 +92,7 @@ test('Renderer:formula', async () => {
expect(inputs[2].value).toBe('3');
expect(inputs[3].value).toBe('4');
expect(inputs[4].value).toBe('5');
expect(inputs[5].value).toBe('5');
expect(container).toMatchSnapshot();
});

View File

@ -0,0 +1,46 @@
import React = require('react');
import {render, cleanup} from '@testing-library/react';
import '../../src/themes/default';
import {render as amisRender} from '../../src/index';
import {makeEnv} from '../helper';
import {clearStoresCache} from '../../src/factory';
afterEach(() => {
cleanup();
clearStoresCache();
});
test('Renderer:timeline', async () => {
const {container} = render(
amisRender(
{
type: 'timeline',
items: [
{
time: "2019-02-07",
title: "节点数据",
color: "#ffb200",
},
{
time: "2019-02-08",
title: "节点数据",
color: "#4F86F4",
},
{
time: "2019-02-09",
title: "节点数据",
color: "success",
},
{
time: "2019-02-09",
title: "节点数据",
color: "warning",
}
]
},
{},
makeEnv()
)
);
expect(container).toMatchSnapshot();
});

View File

@ -55,6 +55,26 @@ test('api:buildApi', () => {
});
});
test('api:buildApi2', () => {
expect(
buildApi('http://domain.com/#/subpath?a=1&b=2', {
a: 1
}).url
).toBe('http://domain.com/#/subpath?a=1&b=2');
expect(
buildApi('http://domain.com/subpath?a=1&b=2#233', {
a: 1
}).url
).toBe('http://domain.com/subpath?a=1&b=2#233');
expect(
buildApi('http://domain.com/subpath?a=1&b=${a}#233', {
a: 1
}).url
).toBe('http://domain.com/subpath?a=1&b=1#233');
});
test('api:buildApi:dataMapping', () => {
expect(
buildApi(

View File

@ -0,0 +1,9 @@
import {registerFilter, resolveVariableAndFilter} from '../../src';
test('filter:customFilter', () => {
registerFilter('customFilter', input => `233`);
expect(resolveVariableAndFilter('${a | customFilter}', {a: 'abc'})).toEqual(
'233'
);
});

View File

@ -12,7 +12,7 @@ order: 99
## 基本用法
类型定义为 `app`,通过 pages 定义页面,支持层级,支持内嵌 schema或者 通过 schemaApi 远程拉取页面,完整用法请参考 [amis-admin](https://github.com/aisuda/amis-admin) 项目
类型定义为 `app`,通过 pages 定义页面,支持层级,支持内嵌 schema或者 通过 schemaApi 远程拉取页面,完整用法请参考 [amis-admin](https://github.com/aisuda/amis-admin) 项目里的代码示例,需要修改 `env`
```json
{
@ -21,7 +21,6 @@ order: 99
"pages": [
{
"label": "分组1",
"children": [
{
"label": "父页面",

View File

@ -43,7 +43,10 @@ order: 27
## 动态图片或文字
src、text 都支持变量,可以从上下文中动态获取图片或文字,下面的例子中第一个获取到了,而第二个没获取到,因此降级为显示 icon
src、text 都支持变量,可以从上下文中动态获取图片或文字,下面的例子中:
- 第一个获取到了,显示正常
- 第二个没获取到,因此降级为显示 icon
- 第三个图片没获取到,由于 text 优先级比 icon 高,所以显示 text
```schema
{
@ -61,6 +64,12 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
"type": "avatar",
"icon": "fa fa-user",
"src": "$other"
},
{
"type": "avatar",
"src": "$other",
"icon": "fa fa-user",
"text": "avatar"
}
]
}
@ -97,13 +106,52 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
[
{
"type": "avatar",
"size": 20,
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
"size": 'large',
"icon": "fa fa-user"
},
{
"type": "avatar",
"size": 'default',
"icon": "fa fa-user"
},
{
"type": "avatar",
"size": 'small',
"icon": "fa fa-user"
},
{
"type": "avatar",
"size": 60,
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
},
{
"type": "avatar",
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
},
{
"type": "avatar",
"size": 20,
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
},
]
```
## 控制字符类型距离左右两侧边界单位像素
通过 gap 可以控制字符类型距离左右两侧边界单位像素
```schema: scope="body"
[
{
"type": "avatar",
"text": 'ejson',
"gap": 2
},
{
"type": "avatar",
"text": "ejson",
"gap": 7
}
]
@ -111,7 +159,7 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
## 图片拉伸方式
通过 `fit` 可以控制图片拉伸方式,默认是 `cover`,具体细节可以参考 MDN [文档](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit)
通过 `fit` 可以控制图片拉伸方式,默认是 `'cover'`
```schema: scope="body"
[
@ -143,6 +191,39 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
]
```
## 控制图片是否允许拖动
通过 draggable 可以控制图片是否允许拖动
```schema: scope="body"
[
{
"type": "avatar",
"fit": "cover",
"src": "https://suda.cdn.bcebos.com/images/amis/plumeria.jpeg",
"draggable": false
},
{
"type": "avatar",
"fit": "cover",
"src": "https://suda.cdn.bcebos.com/images/amis/plumeria.jpeg",
"draggable": true
}
]
```
## 图片加载失败后,通过 onError 控制是否进行 text、icon 置换
> 如果同时存在 text 和 icon会优先用 text、接着 icon
```schema: scope="body"
{
"type": "avatar",
"src": "empty",
"text": "avatar",
"onError": "return true;"
},
```
## 样式
可以通过 style 来控制背景及文字颜色
@ -161,12 +242,17 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | -------- | ------ | --------------------- |
| className | `string` | | 外层 dom 的类名 |
| fit | `string` | cover | 图片缩放类型 |
| src | `string` | | 图片地址 |
| text | `string` | | 文字 |
| icon | `string` | | 图标 |
| shape | `string` | circle | 形状,也可以是 square |
| size | `number` | 40 | 大小 |
| style | `object` | | 外层 dom 的样式 |
| --------- | ----------- | ------ | --------------------- |
| className | `string` | | 外层 dom 的类名 |
| style | `object` | | 外层 dom 的样式 |
| fit |`'contain'` \| `'cover'` \| `'fill'` \| `'none'` \| `'scale-down'` | `'cover'` | 具体细节可以参考 MDN [文档](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit) |
| src | `string` | | 图片地址 |
| text | `string` | | 文字 |
| icon | `string` | `'fa fa-user'` | 图标 |
| shape | `'circle'` \| `'square'` \| `'rounded'` | `'circle'` | 形状,有三种 `'circle'` (圆形)、`'square'`(正方形)、`'rounded'`(圆角) |
| size | `number` \| `'default'` \| `'normal'` \| `'small'` | `'default'` | `'default' \| 'normal' \| 'small'`三种字符串类型代表不同大小分别是48、40、32也可以直接数字表示 |
| gap | `number` | 4 | 控制字符类型距离左右两侧边界单位像素 |
| alt | `number` | | 图像无法显示时的替代文本 |
| draggable | `boolean` | | 图片是否允许拖动 |
| crossOrigin | `'anonymous'` \| `'use-credentials'` \| `''` | | 图片的 `CORS` 属性设置 |
| onError | `string` | | 图片加载失败的字符串这个字符串是一个New Function内部执行的字符串参数是event使用event.nativeEvent获取原生dom事件这个字符串需要返回boolean值。设置 `"return ture;"` 会在图片加载失败后,使用 `text` 或者 `icon` 代表的信息来进行替换。目前图片加载失败默认是不进行置换。注意:图片加载失败,不包括$获取数据为空情况 |

View File

@ -29,7 +29,7 @@ order: 29
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------- |
| className | `string` | | 指定添加 button 类名 |
| href | `string` | | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 |
| url | `string` | | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 |
| size | `'xs' \| 'sm' \| 'md' \| 'lg' ` | | 设置按钮大小 |
| actionType | `'button' \| 'reset' \| 'submit'\| 'clear'\| 'url'` | button | 设置按钮类型 |
| level | `'link' \| 'primary' \| 'enhance' \| 'secondary' \| 'info'\|'success' \| 'warning' \| 'danger' \| 'light'\| 'dark' \| 'default'` | default | 设置按钮样式 |

View File

@ -175,6 +175,30 @@ order: 22
}
```
## 使用新表达式语法
> 1.5.0 及以上版本
通过新的[表达式](../../../docs/concepts/expression)语法,可以调用其中的函数,比如
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "static",
"value": "这个表单没有内容,通过上面的 debug 可以看到输出当前日期"
},
{
"type": "formula",
"name": "date",
"formula": "${DATETOSTR(NOW(), 'YYYY-MM-DD')}"
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |

View File

@ -0,0 +1,68 @@
---
title: InputFormula 公式编辑器
description:
type: 0
group: null
menuName: InputFormula
icon:
order: 21
---
## 基本用法
用来输入公式。还是 beta 版本,整体待优化。
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "input-formula",
"name": "formula",
"label": "公式",
"variableMode": "tabs",
"evalMode": false,
"value": "SUM(1 + 2)",
"variables": [
{
"label": "表单字段",
"children": [
{
"label": "ID",
"value": "id"
},
{
"label": "ID2",
"value": "id2"
}
]
},
{
"label": "流程字段",
"children": [
{
"label": "ID",
"value": "id"
},
{
"label": "ID2",
"value": "id2"
}
]
}
],
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ------------ | --------------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
| header | string | | 弹出来的弹框标题 |
| evalMode | Boolean | true | 表达式模式 或者 模板模式,模板模式则需要将表达式写在 `${``}` 中间。 |
| variables | {label: string; value: string; children?: any[];}[] | [] | 可用变量 |
| variableMode | string | `list` | 可配置成 `tabs` 或者 `tree` 默认为列表,支持分组。 |
| functions | Object[] | | 可以不设置,默认就是 amis-formula 里面定义的函数,如果扩充了新的函数则需要指定 |

View File

@ -10,6 +10,8 @@ order: 37
## 基本用法
默认颜色
```schema: scope="body"
{
"type": "form",
@ -18,7 +20,121 @@ order: 37
{
"type": "input-rating",
"name": "rating",
"label": "评分"
"label": "评分",
"count": 5,
"value": 3
}
]
}
```
自定义颜色。支持各种颜色形式,如 CSS 预定义颜色十六进制颜色RGB 颜色HSL 颜色。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-rating",
"name": "rating",
"label": "评分",
"count": 5,
"value": 3,
"colors": {
"1": "gray",
"2": "#678f8d",
"3": "rgb(119, 168, 141)",
"4": "hsl(147, 22%, 56%)",
"5": "#ff6670"
}
}
]
}
```
## 半星
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-rating",
"name": "rating",
"label": "评分",
"count": 5,
"value": 3.5,
"half": true
}
]
}
```
## 带有文字
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-rating",
"name": "rating",
"label": "评分",
"count": 5,
"value": 3,
"texts": {
"1": "很差",
"2": "较差",
"3": "一般",
"4": "较好",
"5": "很好"
},
"textClassName": "okde"
}
]
}
```
## 自定义字符
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-rating",
"name": "rating",
"label": "评分",
"count": 5,
"value": 3,
"char": "好"
}
]
}
```
## 只读
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-rating",
"name": "rating",
"label": "评分",
"count": 5,
"value": 3.6,
"half": true,
"readOnly": true,
"texts": {
"5": "3.6"
}
}
]
}
@ -28,9 +144,18 @@ order: 37
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ---------- | --------- | ------- | ---------------------- |
| half | `boolean` | `false` | 是否使用半星选择 |
| count | `number` | `5` | 共有多少星可供选择 |
| readOnly | `boolean` | `false` | 只读 |
| allowClear | `boolean` | `true` | 是否允许再次点击后清除 |
| 属性名 | 类型 | 默认值 | 说明 |
| ------------- | ------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| value | `number` | - | 当前值 |
| half | `boolean` | `false` | 是否使用半星选择 |
| count | `number` | `5` | 总星数 |
| readOnly | `boolean` | `false` | 只读 |
| allowClear | `boolean` | `true` | 是否允许再次点击后清除 |
| colors | `string` / `object` | `{'2': '#abadb1', '3': '#787b81', '5': '#ffa900' }` | 星星被选中的颜色。 若传入字符串,则只有一种颜色。若传入对象,可自定义分段,键名为分段的界限值,键值为对应的类名 |
| inactiveColor | `string` | `#e7e7e8` | 未被选中的星星的颜色 |
| texts | `object` | - | 星星被选中时的提示文字。可自定义分段,键名为分段的界限值,键值为对应的类名 |
| textPosition | `right` / `left` | `right` | 文字的位置 |
| char | `string` | `★` | 自定义字符 |
| className | `string` | - | 自定义样式类名 |
| charClassName | `string` | - | 自定义字符类名 |
| textClassName | `string` | - | 自定义文字类名 |

View File

@ -1278,7 +1278,8 @@ order: 2
"label": "选项",
"name": "select",
"autoFill": {
"option": "${label}"
"option.instantValidate": "${label}",
"option.submitValidate": "${label}",
},
"clearable": true,
"options": [
@ -1293,15 +1294,38 @@ order: 2
]
},
{
"type": "text",
"name": "option",
"label": "选中项"
"type": "input-text",
"name": "option.instantValidate",
"label": "选中项",
"description": "填充后立即校验",
"required": true,
"validateOnChange": true,
"validations": {
"equals": "Option B"
},
"validationErrors": {
"equals": "校验失败数据必须为Option B"
}
},
{
"type": "input-text",
"name": "option.submitValidate",
"label": "选中项1",
"description": "填充后提交表单时才校验",
"required": true,
"validations": {
"equals": "Option B"
},
"validationErrors": {
"equals": "校验失败数据必须为Option B"
}
}
]
}
```
上例中我们配置了`"autoFill": {"option": "${label}"}`,表示将选中项中的`label`的值,自动填充到当前表单项中`name`为`option`的文本框中。
上例中我们配置了`"autoFill": {"option.instantValidate": "${label}"}`,表示将选中项中的`label`的值,自动填充到当前表单项中`name`为`option.instantValidate`的文本框中。可以额外配置`"validateOnChange": true`,实现自动填充后立即校验填充项。
**多选模式**

View File

@ -93,6 +93,5 @@ order: 51
| option | `string` | | 选项说明 |
| onText | `string` | | 开启时的文本 |
| offText | `string` | | 关闭时的文本 |
| trueValue | `any` | `true` | 标识真值 |
| falseValue | `any` | `"false"` | 标识假值 |
| option | `string` | | 选项说明 |
| trueValue | `boolean / string / number` | `true` | 标识真值 |
| falseValue | `boolean / string / number` | `"false"` | 标识假值 |

View File

@ -17,106 +17,131 @@ icon:
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "组合穿梭器",
"type": "tabs-transfer-picker",
"name": "a",
"sortable": true,
"selectMode": "tree",
"searchable": true,
"pickerSize": "md",
"options": [
{
"label": "成员",
"selectMode": "tree",
"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": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
"label": "选人",
"type": "tabs-transfer-picker",
"name": "a",
"sortable": true,
"selectMode": "tree",
"searchable": true,
"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",
"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",
}
]
}
]
}
]
}
]
}
```

View File

@ -118,6 +118,101 @@ icon:
}
```
## 自定义选项展示
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "组合穿梭器",
"type": "tabs-transfer",
"name": "a",
"sortable": true,
"selectMode": "tree",
"searchable": true,
"menuTpl": "<div class='flex justify-between'><span>${label}</span><span class='text-muted m-r text-sm'>${tag}</span></div>",
"valueTpl": "${label}(${value})",
"options": [
{
"label": "成员",
"selectMode": "list",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang",
"tag": "法师",
},
{
"label": "曹操",
"value": "caocao",
"tag": "战士",
},
{
"label": "钟无艳",
"value": "zhongwuyan",
"tag": "战士",
},
{
"label": "李白",
"value": "libai",
"tag": "打野"
},
{
"label": "韩信",
"value": "hanxin",
"tag": "打野"
},
{
"label": "云中君",
"value": "yunzhongjun",
"tag": "打野"
}
]
},
{
"label": "用户",
"selectMode": "list",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang",
"tag": "法师",
},
{
"label": "曹操",
"value": "caocao",
"tag": "战士",
},
{
"label": "钟无艳",
"value": "zhongwuyan",
"tag": "战士",
},
{
"label": "李白",
"value": "libai",
"tag": "打野"
},
{
"label": "韩信",
"value": "hanxin",
"tag": "打野"
},
{
"label": "云中君",
"value": "yunzhongjun",
"tag": "打野"
}
]
}
]
}
]
}
```
## 属性表
更多配置请参考[穿梭器Transfer](./transfer)。

View File

@ -67,6 +67,55 @@ icon:
}
```
## 自定义选项展示
```schema: scope="body"
{
"type": "form",
"body": [
{
"label": "默认",
"type": "transfer-picker",
"name": "transfer",
"menuTpl": "<div class='flex justify-between'><span>${label}</span><span class='text-muted m-r text-sm'>${tag}</span></div>",
"valueTpl": "${label}(${value})",
"options": [
{
"label": "诸葛亮",
"value": "zhugeliang",
"tag": "法师",
},
{
"label": "曹操",
"value": "caocao",
"tag": "战士",
},
{
"label": "钟无艳",
"value": "zhongwuyan",
"tag": "战士",
},
{
"label": "李白",
"value": "libai",
"tag": "打野"
},
{
"label": "韩信",
"value": "hanxin",
"tag": "打野"
},
{
"label": "云中君",
"value": "yunzhongjun",
"tag": "打野"
}
]
}
]
}
```
## 属性表
更多配置请参考[穿梭器Transfer](./transfer)。

View File

@ -559,26 +559,77 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
适用于需选择的数据/信息源较多时,用户可直观的知道自己所选择的数据/信息的场景,一般左侧框为数据/信息源,右侧为已选数据/信息,被选中信息同时存在于 2 个框内。
## 自定义选项展示
```schema: scope="body"
{
"type": "form",
"body": [
{
"label": "默认",
"type": "transfer",
"name": "transfer",
"menuTpl": "<div class='flex justify-between'><span>${label}</span><span class='text-muted m-r text-sm'>${tag}</span></div>",
"valueTpl": "${label}(${value})",
"options": [
{
"label": "诸葛亮",
"value": "zhugeliang",
"tag": "法师",
},
{
"label": "曹操",
"value": "caocao",
"tag": "战士",
},
{
"label": "钟无艳",
"value": "zhongwuyan",
"tag": "战士",
},
{
"label": "李白",
"value": "libai",
"tag": "打野"
},
{
"label": "韩信",
"value": "hanxin",
"tag": "打野"
},
{
"label": "云中君",
"value": "yunzhongjun",
"tag": "打野"
}
]
}
]
}
```
## 属性表
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | ----------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
| delimeter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
| 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` | 结果可以进行拖拽排序 |
| 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`。 |
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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) |
| searchable | `boolean` | `false` | 当设置为 `true` 时表示可以通过输入部分内容检索出选项。 |
| searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索,可以设置个 api。 |
| 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) | | 用来自定义值的展示 |

View File

@ -1,5 +1,5 @@
---
title: Grid 水平布局
title: Grid 水平分栏
description:
type: 0
group: ⚙ 组件
@ -10,6 +10,73 @@ order: 46
## 基本用法
默认会水平均分宽度
```schema: scope="body"
[
{
"type": "grid",
"columns": [
{
"columnClassName": "bg-green-300",
"body": [
{
"type": "plain",
"text": "第一栏"
}
]
},
{
"columnClassName": "bg-blue-300",
"body": [
{
"type": "plain",
"text": "第二栏"
}
]
}
]
},
{
"type": "grid",
"className": "m-t",
"columns": [
{
"columnClassName": "bg-green-300",
"body": [
{
"type": "plain",
"text": "第一栏"
}
]
},
{
"columnClassName": "bg-blue-300",
"body": [
{
"type": "plain",
"text": "第二栏"
}
]
},
{
"columnClassName": "bg-red-300",
"body": [
{
"type": "plain",
"text": "第三栏"
}
]
}
]
}
]
```
## 响应式
通过 `md` 设置屏幕中等宽度768px情况下的分栏
```schema: scope="body"
[
{

View File

@ -0,0 +1,390 @@
---
title: Timeline 时间轴
description:
type: 0
group: ⚙ 组件
menuName: Timeline
icon:
order: 73
---
时间轴组件
## 基本用法
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
"detail": "error",
},
{
"time": "2019-02-08",
"title": "节点数据",
"detail": "success",
},
{
"time": "2019-02-09",
"title": "节点数据",
"detail": "error",
}
]
}
]
}
```
## 时间轴节点颜色设置
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
"color": "#ffb200",
},
{
"time": "2019-02-08",
"title": "节点数据",
"color": "#4F86F4",
},
{
"time": "2019-02-09",
"title": "节点数据",
"color": "success",
},
{
"time": "2019-02-09",
"title": "节点数据",
"color": "warning",
}
]
}
]
}
```
## 时间轴节点图标设置
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"items": [
{
"time": "2019-02-07",
"title": "节点数据error",
"detail": "error",
"icon": "status-fail"
},
{
"time": "2019-02-08",
"title": "节点数据success",
"detail": "success",
"icon": "status-success"
},
{
"time": "2019-02-09",
"title": "节点数据warning",
"detail": "warning",
"icon": "status-warning"
}
]
}
]
}
```
## 节点标题自定义
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"items": [
{
"time": "2019-02-07",
"title": [
{
"type": "text",
"value": "2019年02月7日"
},
{
"type": "button",
"label": "查看",
"actionType": "dialog",
"level": "link",
"dialog": {
"title": "查看详情",
"body": "这是详细内容。"
}
},
{
"type": "button",
"label": "删除",
"level": "link",
"actionType": "dialog",
"dialog": {
"title": "删除",
"body": "确认删除吗?"
}
}
]
},
{
"time": "2019-02-10",
"title": [
{
"type": "text",
"value": "2019年02月10日"
},
{
"type": "button",
"label": "查看",
"actionType": "dialog",
"level": "link",
"dialog": {
"title": "查看详情",
"body": "这是详细内容。"
}
},
{
"type": "button",
"label": "删除",
"level": "link",
"actionType": "dialog",
"dialog": {
"title": "删除",
"body": "确认删除吗?"
}
}
]
},
]
}
]
}
```
## 设置节点数据倒序
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
direction: "vertical",
reverse: true,
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
},
{
"time": "2019-02-08",
"title": "节点数据",
},
{
"time": "2019-02-09",
"title": "节点数据",
},
{
"time": "2019-02-10",
"title": "节点数据",
},
]
}
]
}
```
## 设置时间轴方向
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
direction: "horizontal",
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
},
{
"time": "2019-02-08",
"title": "节点数据",
},
{
"time": "2019-02-09",
"title": "节点数据",
},
{
"time": "2019-02-10",
"title": "节点数据",
},
]
}
]
}
```
## 设置文字相对时间轴方向(时间轴横向时不支持)
### 文字位于时间轴左侧
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"mode": "left",
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
"detail": "error",
},
{
"time": "2019-02-08",
"title": "节点数据",
"detail": "success",
}
]
},
]
}
```
### 文字交替位于时间轴两侧
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"mode": "alternate",
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
"detail": "error",
},
{
"time": "2019-02-08",
"title": "节点数据",
"detail": "success",
}
]
}
]
}
```
### 文字位于时间轴右侧
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"mode": "right",
"items": [
{
"time": "2019-02-07",
"title": "节点数据",
"detail": "error",
},
{
"time": "2019-02-08",
"title": "节点数据",
"detail": "success",
}
]
},
]
}
```
## 远程数据
```schema
{
"type": "page",
"body": [
{
"type": "timeline",
"source": {
"method": "get",
"url": "/api/mock2/timeline/timelineItems"
}
}
]
}
```
"source": "/api/mock2/timeline/timelineItems",
远程拉取接口时,返回的数据结构除了需要满足 amis 接口要求的基本数据结构 以外,必须用"items"作为时间轴数据的 key 值,如下:
```json
{
"status": 0,
"msg": "",
"data": {
"items": [
{"time": "2019-02-07", "title": "数据开发", "detail": "2019-02-07detail", "color":"#ffb200", "icon": "close"},
{"time": "2019-02-08", "title": "管理中心", "detail": "2019-02-08detail" },
{"time": "2019-02-09", "title": "SQL语句", "detail": "2019-02-09detail", "color":"warning"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-10detail", "icon": "compress-alt"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-11detail"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-12detail", "icon": "close"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-13detail"}
]
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | --------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------- |
| type | `string` | | `"timeline"` 指定为 时间轴 渲染器 |
| items | Array<[timelineItem](#timeline.item)> | [] | 配置节点数据 |
| source | [API](../../../docs/types/api) | | 数据源,可通过数据映射获取当前数据域变量、或者配置 API 对象 |
| mode | `left` \| `right` \| `alternate` | `right` | 指定文字相对于时间轴的位置,仅 direction=vertical时支持 |
| direction | `vertical` \| `horizontal` | `vertical` | 时间轴方向 | |
| reverse | `boolean` | `false` | 根据时间倒序显示
### timeline.item
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | ----------------------------------------------------- | ------ | --------------------------------------- |
| time | `string ` | | 节点时间 |
| title | `string \| [SchemaNode](../../docs/types/schemanode)` | | 节点标题 |
| detail | `string` | | 节点详细描述(折叠) |
| detailCollapsedText | `string` | `展开` | 详细内容折叠时按钮文案 |
| detailExpandedText | `string` | `折叠` | 详细内容展开时按钮文案 |
| color | `string \| level样式info、success、warning、danger` | `#DADBDD` | 时间轴节点颜色 |
| icon | `string` | | icon 名,支持 fontawesome v4 或使用 url优先级高于color |

View File

@ -23,8 +23,6 @@ order: 13
## 表达式语法
> 表达式语法实际上是 JavaScript 代码,更多 JavaScript 知识查看 [这里](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript)。
>
> 表达式中不要使用`${xxx}`语法,这个是数据映射的语法规则,不要搞混淆了!
在 amis 的实现过程中,当正则匹配到某个组件存在`xxxOn`语法的属性名时,会尝试进行下面步骤(以上面配置为例):
@ -67,7 +65,7 @@ order: 13
"obj[key] is ${obj[key]} <br />",
"arr[0] is ${arr[0]} <br />",
"arr[a] is ${arr[a]} <br />",
"arr[a + 1] is ${arr[a + 1]} <br />",
"arr[a + 1] is ${arr[a + 1]} <br />"
]
}
```
@ -92,3 +90,54 @@ order: 13
```
!!!include(amis-formula/dist/doc.md)!!!
## 函数调用示例
```schema
{
"type": "page",
"body": [
{
"type": "form",
"wrapWithPanel": false,
"body": [
{
"type": "static",
"label": "IF(true, 2, 3)",
"tpl": "${IF(true, 2, 3)}"
},
{
"type": "static",
"label": "MAX(1, -1, 2, 3, 5, -9)",
"tpl": "${MAX(1, -1, 2, 3, 5, -9)}"
},
{
"type": "static",
"label": "ROUND(3.5)",
"tpl": "${ROUND(3.5)}"
},
{
"type": "static",
"label": "AVG(4, 6, 10, 10, 10)",
"tpl": "${AVG(4, 6, 10, 10, 10)}"
},
{
"type": "static",
"label": "UPPERMONEY(7682.01)",
"tpl": "${UPPERMONEY(7682.01)}"
},
{
"type": "static",
"label": "TIMESTAMP(DATE(2021, 11, 21, 0, 0, 0), 'x')",
"tpl": "${TIMESTAMP(DATE(2021, 11, 21, 0, 0, 0), 'x')}"
},
{
"type": "static",
"label": "DATETOSTR(NOW(), 'YYYY-MM-DD')",
"tpl": "${DATETOSTR(NOW(), 'YYYY-MM-DD')}"
}
]
}
]
}
```

View File

@ -1,7 +1,42 @@
---
title: 移动端定制
title: 移动端展现
---
## 移动端原生 UI
从 1.6.0 版本开始amis 会默认在移动端下使用仿原生 UI 的展现,比如日期选择会从底部弹出。
由于这个仿原生 UI 是新开发的组件,有些 amis PC 版本的高级配置功能还不支持,比如 select 下的搜索过滤等,如果需要这些功能,可以先通过 props 里的 `useMobileUI` 属性关闭。
方法 1全局关闭
```js
amis.embed(
'#root',
{
// amis schema
},
{
// 这里是初始 props
},
{
theme: 'antd',
useMobileUI: false
}
);
```
方法 2针对某个组件进行关闭
```json
{
"type": "select",
"useMobileUI": false
}
```
## 移动端定制配置
有时候我们需要在移动端下展示不同效果,可以通过 `mobile` 属性来在移动端下覆盖部分属性。
```schema: scope="body"

View File

@ -160,7 +160,7 @@ let amisScoped = amis.embed(
// 全局 api 请求适配器
// 另外在 amis 配置项中的 api 也可以配置适配器,针对某个特定接口单独处理。
//
// responseAdaptor(api) {
// requestAdaptor(api) {
// return api;
// }
//
@ -276,7 +276,7 @@ amis.embed(
默认 JSSDK 不是 hash 路由,如果你想改成 hash 路由模式,请查看此处代码实现。只需要修改 env.isCurrentUrl、env.jumpTo 和 env.updateLocation 这几个方法即可。
参考https://github.com/baidu/amis/blob/master/examples/components/Example.tsx#L551-L575
参考https://github.com/baidu/amis/blob/master/examples/components/Example.jsx#L551-L575
### 销毁
@ -436,6 +436,8 @@ class MyComponent extends React.Component<any, any> {
render() {
let amisScoped;
let theme = 'cxd';
// 请勿使用 React.StrictMode目前还不支持
return (
<div>
<p>通过 amis 渲染页面</p>

View File

@ -7,8 +7,7 @@ export default {
type: 'tpl',
inline: false,
className: 'w-full',
tpl:
'<div class="flex justify-between"><div>顶部区域左侧</div><div>顶部区域右侧</div></div>'
tpl: '<div class="flex justify-between"><div>顶部区域左侧</div><div>顶部区域右侧</div></div>'
},
// footer: '<div class="p-2 text-center bg-light">底部区域</div>',
// asideBefore: '<div class="p-2 text-center">菜单前面区域</div>',

96
examples/app/index.html Normal file
View File

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>amis app 模式</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link type="image/x-icon" rel="shortcut icon" href="./static/favicon.png" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<link rel="stylesheet" href="../static/iconfont.css" />
<link rel="stylesheet" href="@fortawesome/fontawesome-free/css/all.css" />
<link
rel="stylesheet"
href="@fortawesome/fontawesome-free/css/v4-shims.css"
/>
<!--DEPENDENCIES_INJECT_PLACEHOLDER-->
<!--STYLE_PLACEHOLDER-->
<style>
.app-wrapper,
.schema-wrapper {
position: relative;
width: 100%;
height: 100%;
}
</style>
<script type="text/x-jsx">
let theme = localStorage.getItem('amis-theme') || 'cxd';
if (theme === 'default') {
theme = 'cxd';
}
// 非 IE 模式
if (window.navigator.userAgent.indexOf('Trident') === -1) {
document.write(
`<link rel="stylesheet" title="ang" ${
theme !== 'ang' ? 'disabled' : ''
} href="${__uri('../../scss/themes/ang.scss')}" />`
);
document.write(
`<link rel="stylesheet" title="cxd" ${
theme !== 'cxd' ? 'disabled' : ''
} href="${__uri('../../scss/themes/cxd.scss')}" />`
);
document.write(
`<link rel="stylesheet" title="dark" ${
theme !== 'dark' ? 'disabled' : ''
} href="${__uri('../../scss/themes/dark.scss')}" />`
);
document.write(
`<link rel="stylesheet" title="antd" ${
theme !== 'antd' ? 'disabled' : ''
} href="${__uri('../../scss/themes/antd.scss')}" />`
);
} else {
document.write(
`<link rel="stylesheet" title="ang" ${
theme !== 'ang' ? 'disabled' : ''
} href="${__uri('../../scss/themes/ang-ie11.scss')}" />`
);
document.write(
`<link rel="stylesheet" title="cxd" ${
theme !== 'cxd' ? 'disabled' : ''
} href="${__uri('../../scss/themes/cxd-ie11.scss')}" />`
);
document.write(
`<link rel="stylesheet" title="dark" ${
theme !== 'dark' ? 'disabled' : ''
} href="${__uri('../../scss/themes/dark-ie11.scss')}" />`
);
document.write(
`<link rel="stylesheet" title="antd" ${
theme !== 'antd' ? 'disabled' : ''
} href="${__uri('../../scss/themes/antd-ie11.scss')}" />`
);
}
</script>
<!--ignore-->
<link rel="stylesheet" href="../../scss/helper.scss" />
<!--ignore-->
</head>
<body>
<div id="root" class="app-wrapper"></div>
<script src="../mod.js"></script>
<script type="text/javascript">
/* @require ./index.jsx 标记为同步依赖,提前加载 */
amis.require(['./index.jsx'], function (app) {
var initialState = {};
app.bootstrap(document.getElementById('root'), initialState);
});
</script>
</body>
</html>

140
examples/app/index.jsx Normal file
View File

@ -0,0 +1,140 @@
/**
* @file app 模式示例
*/
import APPSchema from './app';
import {createHashHistory} from 'history';
import {embed} from '../embed';
const history = createHashHistory({});
function normalizeLink(to, location = history.location) {
to = to || '';
if (to && to[0] === '#') {
to = location.pathname + location.search + to;
} else if (to && to[0] === '?') {
to = location.pathname + to;
}
const idx = to.indexOf('?');
const idx2 = to.indexOf('#');
let pathname = ~idx
? to.substring(0, idx)
: ~idx2
? to.substring(0, idx2)
: to;
let search = ~idx ? to.substring(idx, ~idx2 ? idx2 : undefined) : '';
let hash = ~idx2 ? to.substring(idx2) : location.hash;
if (!pathname) {
pathname = location.pathname;
} else if (pathname[0] != '/' && !/^https?\:\/\//.test(pathname)) {
let relativeBase = location.pathname;
const paths = relativeBase.split('/');
paths.pop();
let m;
while ((m = /^\.\.?\//.exec(pathname))) {
if (m[0] === '../') {
paths.pop();
}
pathname = pathname.substring(m[0].length);
}
pathname = paths.concat(pathname).join('/');
}
return pathname + search + hash;
}
function isCurrentUrl(to, ctx) {
if (!to) {
return false;
}
const pathname = history.location.pathname;
const link = normalizeLink(to, {
...location,
pathname,
hash: ''
});
if (!~link.indexOf('http') && ~link.indexOf(':')) {
let strict = ctx && ctx.strict;
return match(link, {
decode: decodeURIComponent,
strict: typeof strict !== 'undefined' ? strict : true
})(pathname);
}
return decodeURI(pathname) === link;
}
const APPENV = {
updateLocation: (location, replace) => {
location = normalizeLink(location);
if (location === 'goBack') {
return history.goBack();
} else if (
(!/^https?\:\/\//.test(location) &&
location === history.location.pathname + history.location.search) ||
location === history.location.href
) {
//
return;
} else if (/^https?\:\/\//.test(location) || !history) {
return (window.location.href = location);
}
history[replace ? 'replace' : 'push'](location);
},
jumpTo: (to, action) => {
if (to === 'goBack') {
return history.goBack();
}
to = normalizeLink(to);
if (isCurrentUrl(to)) {
return;
}
if (action && action.actionType === 'url') {
action.blank === false
? (window.location.href = to)
: window.open(to, '_blank');
return;
} else if (action && action.blank) {
window.open(to, '_blank');
return;
}
if (/^https?:\/\//.test(to)) {
window.location.href = to;
} else if (
(!/^https?\:\/\//.test(to) &&
to === history.pathname + history.location.search) ||
to === history.location.href
) {
// do nothing
} else {
history.push(to);
}
},
isCurrentUrl: isCurrentUrl
};
export function bootstrap(mountTo) {
const amisInstance = embed(
mountTo,
APPSchema,
{
location: history.location
},
APPENV
);
history.listen(state => {
amisInstance.updateProps({
location: state.location || state
});
});
}

View File

@ -0,0 +1,60 @@
export default {
title: '点击左侧 crud 会触发右侧 crud 刷新',
subTitle: '需要配置 syncLocation: false 避免左侧也刷新',
body: {
type: 'grid',
columns: [
{
body: [
{
type: 'crud',
api: '/api/sample',
headerToolbar: [],
perPage: 10,
syncLocation: false,
itemAction: {
actionType: 'reload',
target: 'detailCRUD?id=${id}'
},
columns: [
{
name: 'id',
label: 'ID',
width: 20,
type: 'text'
},
{
name: 'platform',
label: 'Platform(s)',
type: 'text'
}
]
}
]
},
{
body: [
{
type: 'crud',
name: 'detailCRUD',
headerToolbar: [],
syncLocation: false,
api: '/api/sample?perPage=10&id=${id}&waitSeconds=1',
columns: [
{
name: 'engine',
label: 'Rendering engine',
type: 'text'
},
{
name: 'version',
label: 'Engine version',
type: 'text'
}
]
}
]
}
]
}
};

View File

@ -54,7 +54,7 @@ export const components = [
)
},
{
label: 'Grid 水平布局',
label: 'Grid 水平分栏',
path: '/zh-CN/components/grid',
getComponent: () =>
import('../../docs/zh-CN/components/grid.md').then(
@ -370,6 +370,14 @@ export const components = [
makeMarkdownRenderer
)
},
{
label: 'InputFormula 公式编辑器',
path: '/zh-CN/components/form/input-formula',
getComponent: () =>
import('../../docs/zh-CN/components/form/input-formula.md').then(
makeMarkdownRenderer
)
},
{
label: 'DiffEditor 对比编辑器',
path: '/zh-CN/components/form/diff-editor',
@ -991,7 +999,15 @@ export const components = [
import('../../docs/zh-CN/components/video.md').then(
makeMarkdownRenderer
)
}
},
{
label: 'Timeline 时间轴',
path: '/zh-CN/components/timeline',
getComponent: () =>
import('../../docs/zh-CN/components/timeline.md').then(
makeMarkdownRenderer
)
},
]
},

View File

@ -90,6 +90,7 @@ export default {
label: '弹个表单',
actionType: 'dialog',
dialog: {
size: 'lg',
title: '在弹框中的表单',
closeOnEsc: true,
actions: [

View File

@ -53,6 +53,7 @@ import HeaderHideSchema from './CRUD/HeaderHide';
import LoadOnceTableCrudSchema from './CRUD/LoadOnce';
import ExportCSVExcelSchema from './CRUD/ExportCSVExcel';
import CRUDDynamicSchema from './CRUD/Dynamic';
import ItemActionchema from './CRUD/ItemAction';
import SdkTest from './Sdk/Test';
import JSONSchemaForm from './Form/Schem';
import SimpleDialogSchema from './Dialog/Simple';
@ -84,7 +85,7 @@ import Tab1Schema from './Tabs/Tab1';
import Tab2Schema from './Tabs/Tab2';
import Tab3Schema from './Tabs/Tab3';
import TestComponent from './Test';
import APP from './APP/index';
import {normalizeLink} from '../../src/utils/normalizeLink';
export const examples = [
@ -376,6 +377,11 @@ export const examples = [
path: '/examples/crud/load-once',
component: makeSchemaRenderer(LoadOnceTableCrudSchema)
},
{
label: '点击联动',
path: '/examples/crud/item-action',
component: makeSchemaRenderer(ItemActionchema)
},
{
label: '导出 Excel/CSV',
path: '/examples/crud/export-excel-csv',
@ -577,49 +583,17 @@ export const examples = [
{
label: 'APP 多页应用',
icon: 'fa fa-cubes',
path: '/examples/app',
component: makeSchemaRenderer(APP, false, {
session: 'app',
jumpTo: (to: string) => {
location.hash = to;
},
updateLocation: (to, replace) => {
if (to === 'goBack') {
return window.history.back();
}
if (replace && window.history.replaceState) {
window.history.replaceState(
'',
document.title,
normalizeLink(to)
);
return;
}
window.history.pushState('', document.title, normalizeLink(to));
},
isCurrentUrl: (to: string, ctx: any) => {
if (!to) {
return false;
}
const pathname = location.hash ? location.hash.substring(1) : '/';
const link = normalizeLink(to, {
...location,
pathname,
hash: ''
});
if (!~link.indexOf('http') && ~link.indexOf(':')) {
return match(link, {
decode: decodeURIComponent,
strict: ctx?.strict ?? true
})(pathname);
}
return pathname === encodeURI(link);
path: '/app/',
component: () => {
// gh-pages
if (/^\/amis/.test(window.location.pathname)) {
window.open(`/amis/app/`, '_blank');
} else {
window.open(`/examples/app/`, '_blank');
}
})
return null;
}
}
// {

View File

@ -2,6 +2,7 @@ import React from 'react';
import {toast} from '../../src/components/Toast';
import {render, makeTranslator} from '../../src/index';
import {normalizeLink} from '../../src/utils/normalizeLink';
import {isMobile} from '../../src/utils/helper';
import attachmentAdpator from '../../src/utils/attachmentAdpator';
import {alert, confirm} from '../../src/components/Alert';
import axios from 'axios';
@ -324,11 +325,10 @@ export default class PlayGround extends React.Component {
theme: this.props.theme,
locale: this.props.locale,
affixHeader: false,
affixFooter: false,
useMobileUI: true
affixFooter: false
};
if (this.props.viewMode === 'mobile') {
if (this.props.viewMode === 'mobile' && !isMobile()) {
return (
<iframe
width="375"

View File

@ -7,12 +7,15 @@ import './polyfills/index';
import React from 'react';
import {render} from 'react-dom';
import axios from 'axios';
import TouchEmulator from 'hammer-touchemulator';
import copy from 'copy-to-clipboard';
import {toast} from '../src/components/Toast';
import '../src/locale/en-US';
import {render as renderAmis} from '../src/index';
TouchEmulator();
class AMISComponent extends React.Component {
state = {
schema: null,
@ -23,7 +26,10 @@ class AMISComponent extends React.Component {
window.addEventListener('message', event => {
const data = event.data;
if (data && data.schema) {
this.setState({schema: data.schema, props: data.props});
this.setState({
schema: data.schema,
props: data.props
});
}
});
window.parent.postMessage('amisReady', '*');
@ -41,7 +47,12 @@ class AMISComponent extends React.Component {
headers //
}) => {
config = {
url,
dataType: 'json',
method,
data,
headers,
responseType,
...config
};
@ -55,12 +66,7 @@ class AMISComponent extends React.Component {
config.validateStatus = function () {
return true;
};
const response = await axios[config.method](
config.url,
config.data,
config
);
const response = await axios(config);
if (response.status >= 400) {
if (response.data) {

View File

@ -54,6 +54,7 @@ fis.set('project.files', [
'/scss/helper.scss',
'/scss/themes/*.scss',
'/examples/*.html',
'/examples/app/*.html',
'/examples/*.tpl',
'/examples/static/*.png',
'/examples/static/*.svg',
@ -495,6 +496,7 @@ if (fis.project.currentMedia() === 'publish') {
'!mpegts.js/**',
'!hls.js/**',
'!froala-editor/**',
'!codemirror/**',
'!tinymce/**',
'!zrender/**',
@ -530,6 +532,7 @@ if (fis.project.currentMedia() === 'publish') {
'tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
'codemirror.js': ['codemirror/**'],
'papaparse.js': ['papaparse/**'],
'exceljs.js': ['exceljs/**'],
@ -562,6 +565,7 @@ if (fis.project.currentMedia() === 'publish') {
'rest.js': [
'*.js',
'!monaco-editor/**',
'!codemirror/**',
'!mpegts.js/**',
'!hls.js/**',
'!froala-editor/**',
@ -770,6 +774,7 @@ if (fis.project.currentMedia() === 'publish') {
'/examples/mod.js',
'node_modules/**.js',
'!monaco-editor/**',
'!codemirror/**',
'!mpegts.js/**',
'!hls.js/**',
'!froala-editor/**',
@ -808,6 +813,8 @@ if (fis.project.currentMedia() === 'publish') {
'pkg/tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
'pkg/codemirror.js': ['codemirror/**'],
'pkg/papaparse.js': ['papaparse/**'],
'pkg/exceljs.js': ['exceljs/**'],
@ -900,7 +907,7 @@ if (fis.project.currentMedia() === 'publish') {
const DocNavCN = ret.src['/examples/components/DocNavCN.ts'];
const Components = ret.src['/examples/components/Components.tsx'];
const DocCSS = ret.src['/examples/components/CssDocs.tsx'];
const ExampleJs = ret.src['/examples/components/Example.tsx'];
const ExampleJs = ret.src['/examples/components/Example.jsx'];
const pages = [];
const source = [

View File

@ -0,0 +1,12 @@
module.exports = function (req, res) {
const ret = {
status: 0,
msg: '',
data: {
count: 0,
rows: []
}
};
res.json(ret);
};

View File

@ -0,0 +1,6 @@
{
"status": 422,
"msg": "",
"errors": "当前用户已存在",
"data": null
}

View File

@ -0,0 +1,3 @@
{
"status": 0
}

View File

@ -0,0 +1,15 @@
{
"status": 0,
"msg": "",
"data": {
"items": [
{"time": "2019-02-07", "title": "数据开发", "detail": "2019-02-07detail", "color":"#ffb200", "icon": "close"},
{"time": "2019-02-08", "title": "管理中心", "detail": "2019-02-08detail" },
{"time": "2019-02-09", "title": "SQL语句", "detail": "2019-02-09detail", "color":"warning"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-10detail", "icon": "compress-alt"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-11detail"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-12detail", "icon": "close"},
{"time": "2019-02-10", "title": "一键部署", "detail": "2019-02-13detail"}
]
}
}

View File

@ -1,3 +1,5 @@
rewrite ^\/examples\/app\/$ /examples/app/index.html
rewrite ^\/(?:zh-CN|en-US)?\/?(?:examples|docs|components|style)(?:\/[a-z0-9\-_\/]+)?$ /examples/index.html
rewrite ^\/play$ /examples/index.html

View File

@ -1,6 +1,6 @@
{
"name": "amis",
"version": "1.5.3",
"version": "1.5.7",
"description": "一种MIS页面生成工具",
"main": "lib/index.js",
"scripts": {
@ -13,7 +13,7 @@
"dev": "fis3 release -cwd ./public",
"publish-to-internal": "sh build.sh && sh publish.sh",
"build": "sh build.sh",
"prettier": "prettier --write '{src,examples,scss}/**/*.{tsx,ts,jsx,scss}'",
"prettier": "prettier --write '{src,scss,examples}/**/**/*.{js,jsx,ts,tsx,scss,json}'",
"deploy-gh-page": "sh ./deploy-gh-pages.sh",
"build-schemas": "ts-node -O '{\"target\":\"es6\"}' scripts/build-schemas.ts"
},
@ -37,13 +37,19 @@
"url": "http://www.apache.org/licenses/LICENSE-2.0"
}
],
"lint-staged": {
"{src,examples}/**/**/*.{tsx,jsx,ts}": [
"prettier --write"
]
},
"dependencies": {
"amis-formula": "^1.2.7",
"amis-formula": "^1.3.5",
"ansi-to-react": "^6.1.6",
"async": "2.6.0",
"attr-accept": "2.2.2",
"blueimp-canvastoblob": "2.1.0",
"classnames": "2.3.1",
"codemirror": "^5.63.0",
"downshift": "6.1.7",
"echarts": "5.2.2",
"echarts-stat": "^1.2.0",
@ -94,6 +100,7 @@
"@fortawesome/fontawesome-free": "^5.15.4",
"@testing-library/react": "^12.0.0",
"@types/async": "^2.0.45",
"@types/codemirror": "^5.60.3",
"@types/echarts": "^4.9.2",
"@types/file-saver": "^2.0.1",
"@types/history": "^4.6.0",
@ -140,12 +147,14 @@
"fis3-preprocessor-js-require-file": "^0.1.3",
"fs-walk": "0.0.2",
"glob": "^7.2.0",
"hammer-touchemulator": "^0.0.2",
"history": "^4.7.2",
"husky": "^7.0.4",
"jest": "^27.4.2",
"jest-canvas-mock": "^2.3.0",
"js-yaml": "^4.1.0",
"json5": "^2.2.0",
"lint-staged": "^12.1.4",
"marked": "^3.0.4",
"mkdirp": "^1.0.4",
"moment-timezone": "^0.5.33",

View File

@ -164,6 +164,14 @@
--Audio-volumeControl-width: #{px2rem(110px)};
--Avatar-bg: #{$gray300};
--Avatar-width: #{px2rem(40px)};
--Avatar-size-large: #{px2rem(48px)};
// 兼容旧的size大小写法
--Avatar-size-default: var(--Avatar-width);
--Avatar-size-small: #{px2rem(32px)};
--Avatar-icon-size-large: #{px2rem(20px)};
// 兼容旧的icon大小写法
--Avatar-icon-size-default: var(--fontSizeLg);
--Avatar-icon-size-small: #{px2rem(12px)};
--Badge-size: var(--gap-md);
--Badge-color: var(--white);
@ -1091,8 +1099,8 @@
--PickerColumns-title-lineHeight: 1.5;
--PickerColumns-action-padding: 0 var(--gap-sm);
--PickerColumns-action-fontSize: var(--fontSizeMd);
--PickerColumns-confirmAction-color: var(--Button--info-bg);
--PickerColumns-cancelAction-color: var(--Button--light-bg);
--PickerColumns-confirmAction-color: #{lighten($text-color, 25%)};
--PickerColumns-cancelAction-color: #{lighten($text-color, 50%)};
--PickerColumns-option-fontSize: var(--fontSizeLg);
--PickerColumns-optionText-color: var(--text-color);
--PickerColumns-optionDisabled-opacity: 0.3;
@ -1144,7 +1152,9 @@
--ResultBox-value-bg: #f5f5f5;
--ResultBox-value-color: #000;
--Rating-onActive-color: var(--info);
--Rating-inactive-color: #e6e6e8;
--Rating-star-margin: #{px2rem(8px)};
--Rating-star-size: #{px2rem(24px)};
--Satus-icon-width: var(--gap-lg);
--Satus-icon-height: var(--Satus-icon-width);

View File

@ -32,7 +32,7 @@ $colors: (
primary: #007bff,
secondary: #6c757d,
success: #28a745,
info: #17a2b8,
info: #007bff,
warning: #fad733,
danger: #dc3545,
light: #f8f9fa,

View File

@ -1,14 +1,37 @@
@mixin avatar-size($size, $fontSize) {
width: $size;
height: $size;
line-height: $size;
i {
font-size: $fontSize;
}
}
.#{$ns}Avatar {
background: var(--Avatar-bg);
width: var(--Avatar-width);
height: var(--Avatar-width);
line-height: var(--Avatar-width);
@include avatar-size(var(--Avatar-size-default), var(--Avatar-icon-size-default));
position: relative;
display: inline-block;
overflow: hidden;
flex-shrink: 0;
border-radius: 50%;
text-align: center;
&--lg {
@include avatar-size(var(--Avatar-size-large), var(--Avatar-icon-size-large));
}
&--sm {
@include avatar-size(var(--Avatar-size-small), var(--Avatar-icon-size-small));
}
&--text {
position: absolute;
left: 50%;
transform-origin: 0 center;
}
&--square {
border-radius: 0%;
}
@ -17,18 +40,13 @@
border-radius: 10%;
}
i {
font-size: var(--fontSizeLg);
}
img {
color: transparent;
width: 100%;
height: 100%;
object-fit: cover;
}
&:hover {
img,
i {
transform: scale(1.1);

View File

@ -79,3 +79,283 @@
position: relative;
}
}
.#{$ns}CalendarMobile {
height: 100%;
overflow: scroll;
&-pop {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 90vh;
background: #fff;
border-radius: px2rem(16px) px2rem(16px) 0 0;
overflow: hidden;
border-width: 0;
}
&-wrap {
display: flex;
flex-direction: column;
height: 100%;
}
&-header {
flex-shrink: 0;
box-shadow: 0 2px 10px rgb(125 126 128 / 16%);
position: relative;
.subtitle-text {
display: inline-block;
width: px2rem(110px);
position: relative;
}
.rdtPrev {
width: px2rem(20px);
height: px2rem(44px);
display: block;
position: absolute;
top: 0;
left: px2rem(-20px);
}
.rdtNext {
width: px2rem(20px);
height: px2rem(44px);
display: block;
position: absolute;
top: 0;
right: px2rem(-20px);
}
}
&-title,
&-subtitle {
height: px2rem(44px);
font-weight: 500;
line-height: px2rem(44px);
text-align: center;
}
&-title {
font-size: var(--fontSizeLg);
}
&-weekdays {
display: flex;
.weekday {
flex: 1;
line-height: px2rem(30px);
text-align: center;
}
}
&-close {
position: absolute;
z-index: 1;
color: #c8c9cc;
font-size: px2rem(16px);
cursor: pointer;
top: px2rem(11px);
right: px2rem(16px);
}
&-body::-webkit-scrollbar {
width: 0;
}
&-body {
flex: 1;
overflow-y: scroll;
table {
border-spacing: 0 px2rem(4px);
}
.rdt .rdtPicker td.rdtActive, .rdt .rdtPicker td.rdtActive:hover {
background: transparent;
color: var(--Calendar-color);
text-shadow: none;
}
.rdt .rdtPicker tr td.rdtDisabled, .rdt .rdtPicker tr td.rdtDisabled:hover {
color: #999;
}
.rdtOldNone td.rdtOld {
display: none;
}
.rdtPicker td {
height: px2rem(56px);
position: relative;
}
.rdt .rdtPicker {
td.rdtDay,
td.rdtDay:hover,
td.rdtDisabled,
td.rdtDisabled:hover {
background: transparent;
}
}
.calendar-wrap {
position: relative;
width: px2rem(50px);
height: 100%;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: px2rem(4px);
}
}
&-embed &-body {
.calendar-wrap {
width: auto;
max-width: px2rem(50px);
}
}
&-footer {
flex-shrink: 0;
.date-range-confirm {
height: px2rem(36px);
margin: px2rem(7px) 0;
border-radius: var(--borderRadiusMd);
display: block;
}
.is-disabled {
opacity: 0.5;
color: var(--Button--primary-color);
background: var(--Button--primary-bg);
border-color: var(--Button--primary-bg);
filter: none;
}
&-toolbar {
padding: 0 px2rem(16px);
}
&-ranges {
background: #fff;
box-shadow: 0 0 2px 2px rgba(0,0,0,0.02);
border-radius: 24px;
overflow-x: scroll;
position: relative;
height: px2rem(48px);
line-height: px2rem(48px);
}
.#{$ns}DateRangePicker-rangers {
position: absolute;
white-space: nowrap;
.#{$ns}DateRangePicker-ranger {
margin: 0 px2rem(25px);
}
}
}
&-calendar-wrap {
padding: var(--gap-sm) 0;
}
& &-calendar-wrap &-calendar .rdtPicker {
width: 100%;
padding: 0;
.rdtOld {
visibility: hidden;
}
.rdtNew {
display: none;
}
.rdtBetween {
background: var(--Calendar-cell-onBetween-bg) !important;
color: var(--Button--primary-bg);
}
.rdtRangeStart,
.rdtRangeStart:hover,
.rdtRangeEnd,
.rdtRangeEnd:hover {
.calendar-wrap {
background: var(--Calendar-cell-onActive-bg) !important;
color: #fff;
text-shadow: 0 -1px 0 rgb(0 0 0 / 25%);
}
}
.rdtRangeHasEnd,
.rdtRangeHasEnd:hover {
background: linear-gradient(to right, transparent 0%, transparent 50%, var(--Calendar-cell-onBetween-bg) 51%, var(--Calendar-cell-onBetween-bg) 100%) !important;
}
.rdtRangeEnd,
.rdtRangeEnd:hover {
background: linear-gradient(to right, var(--Calendar-cell-onBetween-bg) 0%, var(--Calendar-cell-onBetween-bg) 50%, transparent 51%, transparent 100%) !important;
}
}
&-calendar-header {
height: px2rem(30px);
line-height: px2rem(30px);
text-align: center;
}
&-range-text {
position: absolute;
right: 0;
left: 0;
bottom: 0;
color: #fff;
font-size: var(--fontSizeSm);
white-space: nowrap;
}
&-calendar-wrap {
position: relative;
}
&-calendar-mark {
position: absolute;
top: 50%;
left: 50%;
z-index: 0;
color: rgba(242, 243, 245, 0.8);
font-size: px2rem(160px);
transform: translate(-50%, -50%);
pointer-events: none;
}
&-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: px2rem(136px);
height: px2rem(36px);
background: rgba(0, 0, 0, .9);
border-radius: 4px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
&-time {
height: px2rem(200px);
&-title {
border: var(--Calendar-borderWidth) solid var(--borderColorDarken);
border-left: none;
border-right: none;
text-align: center;
height: px2rem(30px);
line-height: px2rem(30px);
}
.rdtPicker {
margin: 0 auto;
}
}
}

View File

@ -331,7 +331,7 @@
}
&-multiMedia-img {
display: block;
width: 100%;
width: auto;
height: auto;
object-fit: cover;
border-radius: var(--Card-borderRadius);

View File

@ -0,0 +1,98 @@
.#{$ns}Cascader-tabs {
display: flex;
&.scrollable {
display: block;
overflow-x: auto;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
}
}
.#{$ns}Cascader-tab {
flex: 1;
width: calc((100vw - 20px) / 3);
height: px2rem(370px);
overflow-y: auto;
display: inline-block;
&::-webkit-scrollbar {
display: none;
}
}
.#{$ns}Cascader {
width: 100%;
padding: 0 10px;
&-Nav {
overflow-x: auto;
&Item {
display: inline-block;
margin-right: px2rem(10px);
list-style: none;
cursor: pointer;
user-select: none;
padding: 0 px2rem(6px);
}
}
&-btnGroup {
display: flex;
justify-content: space-between;
align-items: center;
height: px2rem(60px);
}
&-options {
box-sizing: border-box;
height: var(--Cascader-option-height);
padding-top: px2rem(6px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
margin: 0;
padding: 0;
}
&-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: px2rem(6px) 0;
font-size: var(--fontSizeMd);
line-height: var(--Cascader-option-lineHeight);
cursor: pointer;
position: relative;
&.selected {
span {
color: var(--primary);
}
}
&.disabled {
span {
color: gray;
}
}
&--text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
&-selectedNum {
min-width: px2rem(16px);
height: px2rem(16px);
line-height: px2rem(16px);
border-radius: 100%;
text-align: center;
background: var(--Form-select-menu-onActive-color);
color: var(--white) !important;
font-size: var(--fontSizeSm);
display: inline-block;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
&-icon {
color: var(--primary);
}
&-tab {
padding: 0;
}
}

View File

@ -1,5 +1,4 @@
.#{$ns}CollapseGroup {
.#{$ns}Collapse:not(:last-child) {
border-bottom: none;
}
@ -11,5 +10,4 @@
}
}
}
}
}

View File

@ -0,0 +1,146 @@
.#{$ns}FormulaEditor {
overflow: visible;
max-width: 100%;
box-sizing: content-box;
&-content {
border-radius: var(--borderRadius);
border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor);
}
&-header {
width: 100%;
height: px2rem(30px);
line-height: px2rem(30px);
padding: 0 #{px2rem(10px)};
box-sizing: border-box;
background: var(--Formula-header-bgColor);
border-radius: var(--borderRadius) var(--borderRadius) 0 0;
border-bottom: var(--Form-input-borderWidth) solid
var(--Form-input-borderColor);
font-weight: 500;
}
&-editor {
min-height: px2rem(238px);
max-height: px2rem(320px);
height: auto;
padding: #{px2rem(10px)};
padding-right: 0;
}
&.is-error &-editor {
border-color: var(--Form-input-onError-borderColor);
}
&.is-focused &-editor {
border-color: var(--Form-input-onFocused-borderColor);
}
&-settings {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: space-between;
max-height: px2rem(350px);
margin: 0 -5px;
> div {
flex: 1;
padding: 0 5px;
display: flex;
flex-direction: column;
> h3 {
padding: 10px 0;
margin: 0;
flex-shrink: 0;
}
> div {
flex: 1;
min-height: 0;
}
}
}
.cm-field,
.cm-func {
border-radius: 2px;
color: #fff;
margin: 0 1px;
padding: 0 2px;
}
.cm-field {
background: #007bff;
}
.cm-func {
background: #17a2b8;
}
}
.#{$ns}FormulaFuncList {
display: flex;
flex-direction: column;
& > &-searchBox {
display: flex;
width: auto;
flex-shrink: 0;
margin-bottom: px2rem(8px);
}
&-columns {
flex: 1;
min-height: 0;
overflow: auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
> div:first-child {
min-width: 200px;
flex-shrink: 0;
}
}
&-funcItem {
padding: 0 10px;
cursor: pointer;
&.is-active {
background: var(--Formula-funcItem-bgColor-onActive);
}
}
&-groupTitle {
padding: 5px 0;
background: transparent;
}
&-groupBody {
> div {
padding: 5px 0;
}
}
&-funcDetail {
padding: 10px 20px;
pre {
white-space: pre-wrap;
word-wrap: break-word;
background: var(--Formula-header-bgColor);
padding: #{px2rem(10px)};
border-radius: var(--borderRadius);
margin-top: 0;
}
div {
color: var(--text--loud-color);
}
}
}
.#{$ns}FormulaPicker {
&-icon {
margin-left: auto;
margin-right: #{px2rem(5px)};
}
}

View File

@ -231,3 +231,21 @@
}
}
}
/* 移动端样式调整 */
@include media-breakpoint-down(sm) {
.#{$ns}Modal {
.#{$ns}Modal-footer {
display: flex;
> .#{$ns}Button {
flex: 1;
height: px2rem(44px);
&:first-child {
margin-left: 0;
}
}
}
}
}

View File

@ -182,3 +182,48 @@
}
}
}
/* 移动端样式调整 */
@include media-breakpoint-down(sm) {
.#{$ns}Panel--form {
border: none;
box-shadow: none;
margin: 0 calc(var(--Panel-bodyPadding) * -1)
calc(var(--Panel-marginBottom) / 2);
.#{$ns}Panel-body {
padding: 0 var(--gap-md) var(--gap-md);
}
> .#{$ns}Panel-heading {
background: none;
border: none;
border-radius: 0;
.#{$ns}Panel-title {
padding-left: var(--Panel-bodyPadding);
border-left: px2rem(3px) solid var(--primary);
font-size: var(--fontSizeLg);
}
}
.#{$ns}Panel-footerWrap {
padding-bottom: var(--Panel-bodyPadding);
}
.#{$ns}Panel-footer {
border-top: none;
display: flex;
padding: 0 var(--Panel-bodyPadding);
> .#{$ns}Button {
flex: 1;
&:first-child {
margin-left: 0;
}
}
}
}
}

View File

@ -2,14 +2,9 @@
position: relative;
background-color: var(--PickerColumns-bg);
user-select: none;
&-popOver {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 0 var(--gap-sm);
}
flex: 1;
overflow: hidden;
font-size: var(--PickerColumns-option-fontSize);
&-toolbar {
display: flex;
@ -30,6 +25,9 @@
&:active {
opacity: 0.7;
}
&:hover {
background-color: none !important;
}
}
&-confirm {
@ -77,8 +75,22 @@
z-index: 2;
transform: translateY(-50%);
pointer-events: none;
&::after {
position: absolute;
content: '';
border-bottom: 1px solid var(--borderColor);
border-top: 1px solid var(--borderColor);
top: -50%;
right: -50%;
left: -50%;
bottom: -50%;
transform: scale(0.5);
}
}
&-mask {
position: absolute;
top: 0;
@ -98,12 +110,6 @@
pointer-events: none;
}
&-column {
flex: 1;
overflow: hidden;
font-size: var(--PickerColumns-option-fontSize);
}
&-columnWrapper {
margin: 0;
padding: 0;

149
scss/components/_popup.scss Normal file
View File

@ -0,0 +1,149 @@
@keyframes PopUpIn {
from {
height: 0;
}
}
@keyframes PopUpOut {
to {
height: 0;
}
}
@keyframes PopUpOpacityIn {
from {
opacity: 0;
}
}
@keyframes PopUpOpacityOut {
to {
opacity: 0;
}
}
.#{$ns}PopUp {
width: 100%;
height: px2rem(400px);
position: fixed;
background: var(--PopOver-bg);
left: 0;
bottom: 0;
z-index: $zindex-popover;
padding: 0;
margin: 0;
font-weight: var(--fontWeightNormal);
letter-spacing: normal;
line-height: var(--lineHeightBase);
text-align: left;
text-align: start;
text-decoration: none;
text-shadow: none;
text-transform: none;
white-space: normal;
word-break: normal;
word-spacing: normal;
word-wrap: normal;
font-size: var(--fontSizeBase);
box-shadow: var(--boxShadow);
border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--borderRadius);
overflow: hidden;
&.in,
&.out {
animation-duration: var(--animation-duration);
animation-fill-mode: both;
}
&.in {
animation-name: PopUpIn;
.#{$ns}PopUp-overlay {
animation-name: PopUpOpacityIn;
}
}
&.out {
animation-name: PopUpOut;
.#{$ns}PopUp-overlay {
animation-name: PopUpOpacityOut;
}
}
&-inner {
position: relative;
overflow: hidden;
height: 100%;
box-sizing: border-box;
background: $white;
display: flex;
flex-direction: column;
}
&-closeWrap {
position: relative;
text-align: center;
height: px2rem(48px);
line-height: px2rem(48px);
}
&-closeWrap &-close {
position: absolute;
z-index: 1;
color: var(--icon-color);
cursor: pointer;
top: px2rem(15px);
right: px2rem(15px);
}
&-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: px2rem(60px);
}
&-title {
font-size: var(--fontSizeMd);
}
&-cancel {
margin-left: var(--gap-sm);
}
&-confirm {
margin-right: var(--gap-sm);
}
&-content {
overflow-y: auto;
height: 100%;
display: flex;
flex: 1;
}
& > * {
position: relative;
z-index: 2;
}
&-overlay {
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 1;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
opacity: 1;
animation-duration: var(--animation-duration);
animation-fill-mode: both;
}
&--leftBottomLeftTop {
margin-top: px2rem(4px);
}
&--leftTopLeftBottom {
margin-top: px2rem(-4px);
}
&-safearea {
height: px2rem(16px);
}
}

View File

@ -126,4 +126,35 @@
padding-left: 8px;
min-height: 24px;
}
&.is-mobile {
min-height: calc(var(--Form-input-lineHeight) * var(--fontSizeLg));
border: none;
padding: 0;
font-size: var(--fontSizeLg);
border: none;
justify-content: flex-end;
.#{$ns}ResultBox-arrow {
margin-right: var(--gap-xs);
// margin-left: var(--gap-xs);
width: var(--gap-md);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
margin-left: 4px;
> svg {
transition: transform var(--animation-duration);
display: inline-block;
color: var(--Form-select-caret-iconColor);
width: 10px;
height: 10px;
top: 0;
transform: rotate(-90deg);
}
}
}
}

View File

@ -140,3 +140,63 @@
}
}
}
.#{$ns}Steps-mobile.#{$ns}Steps--horizontal {
.#{$ns}StepsItem {
&-container {
&Wrapper {
.#{$ns}StepsItem-body {
.#{$ns}StepsItem-title {
&::after {
display: none !important;
}
}
}
.#{$ns}StepsItem-body{
.#{$ns}StepsItem-title {
.#{$ns}StepsItem-subTitle {
padding-left: px2rem(5px);
}
}
}
}
&Icon {
position: relative;
display: block;
&:after {
content: '';
position: absolute;
right: 0;
top: px2rem(15px);
height: 1px;
left: px2rem(40px);
width: 99999px;
background-color: var(--Steps-line-bg);
}
}
&Icon.is-success {
&:after {
background-color: var(--Steps-line-success-bg);
}
}
}
&:last-child {
flex: 1;
.#{$ns}StepsItem-container {
&Icon {
&:after {
display: none !important;
}
}
}
}
}
}

View File

@ -0,0 +1,198 @@
.#{$ns}Timeline-vertical {
display: flex;
flex-flow: column;
.#{$ns}TimelineItem {
display: flex;
flex: 1;
flex-direction: row;
&:last-of-type {
.#{$ns}TimelineItem-axle .#{$ns}TimelineItem-line {
display: none;
}
}
&-axle {
position: relative;
flex: var(--TimelineItem--axle-flex);
.#{$ns}TimelineItem-line {
position: absolute;
height: calc(100% - var(--TimelineItem--left-line-top));
width: var(--TimelineItem--left-line-width);
left: var(--TimelineItem--left-line-left);
top: var(--TimelineItem--left-line-top);
background-color: var(--TimelineItem--line-bg);
}
.#{$ns}TimelineItem-round {
position: absolute;
width: var(--TimelineItem--round-width);
height: var(--TimelineItem--round-height);
left: var(--TimelineItem--round-left);
top: var(--TimelineItem--round-top);
background: var(--TimelineItem-round-bg);
border-radius: var(--TimelineItem--round-radius);
&--danger {
background: var(--Timeline--danger-bg);
}
&--info {
background: var(--Timeline--info-bg);
}
&--success {
background: var(--Timeline--success-bg);
}
&--warning {
background: var(--Timeline--warning-bg);
}
}
.#{$ns}TimelineItem-icon {
position: absolute;
width: var(--TimelineItem--icon-width);
height: var(--TimelineItem--icon-height);
left: var(--TimelineItem--icon-left);
border-radius: var(--TimelineItem--icon-radius);
}
}
&-content {
padding-bottom: var(--TimelineItem--content-padding-bottom);
margin-left: var(--TimelineItem--content-margin-left);
.#{$ns}TimelineItem-time {
color: var(--TimelineItem--text-secondary-color);
font-size: var(--Timeline--font-size);
margin-bottom: var(--TimelineItem--content-time-margin-bottom);
}
.#{$ns}TimelineItem-title {
color: var(--TimelineItem--text-primary-color);
font-size: var(--Timeline--font-size);
margin-bottom: var(--TimelineItem--content-title-margin-bottom);
}
.#{$ns}TimelineItem-detail {
.#{$ns}TimelineItem-detail-button {
display: flex;
cursor: pointer;
align-items: center;
font-size: var(--Timeline--font-size);
color: var(--TimelineItem--detail-button-color);
margin-bottom: var(--TimelineItem--detail-button-margin-bottom);
}
.#{$ns}TimelineItem-detail-arrow {
width: var(--TimelineItem-detail-arrow-width);
height: var(--TimelineItem-detail-arrow-width);
}
.#{$ns}TimelineItem-detail-arrow-top {
transform: rotateX(180deg);
}
.#{$ns}TimelineItem-detail-visible {
display: block;
max-width: var(--TimelineItem-detail-visible-max-width);
font-size: var(--Timeline--font-size);
padding: var(--TimelineItem-detail-visible-padding);
box-shadow: var(--TimelineItem-detail-visible-shadow);
}
.#{$ns}TimelineItem-detail-invisible {
display: none;
}
}
}
}
&.#{$ns}Timeline-left {
.#{$ns}TimelineItem {
flex-direction: row-reverse;
}
}
&.#{$ns}Timeline-alternate {
.#{$ns}TimelineItem:nth-child(odd) {
flex-direction: row-reverse;
max-width: 50%;
}
.#{$ns}TimelineItem:nth-child(even) {
margin-left: calc(50% - var(--Timeline-alternate-margin-left));
max-width: calc(50% + var(--Timeline-alternate-margin-left));
}
}
}
.#{$ns}Timeline-horizontal {
display: flex;
flex-flow: row;
margin-left: 50%;
transform: translateX(-50%);
.#{$ns}TimelineItem {
display: flex;
width: -webkit-fill-available;
flex-flow: column;
&:last-of-type {
.#{$ns}TimelineItem-axle .#{$ns}TimelineItem-line {
display: none;
}
}
&-axle {
position: relative;
flex: var(--TimelineItem--axle-flex);
.#{$ns}TimelineItem-line {
position: absolute;
height: var(--TimelineItem--left-line-width);
width: calc(100% - var(--TimelineItem--left-line-left));
left: var(--TimelineItem--left-line-top);
top: var(--TimelineItem--left-line-left);
background-color: var(--TimelineItem--line-bg);
}
.#{$ns}TimelineItem-round {
position: absolute;
width: var(--TimelineItem--round-width);
height: var(--TimelineItem--round-height);
left: var(--TimelineItem--round-top);
top: var(--TimelineItem--round-left);
background: var(--TimelineItem-round-bg);
border-radius: var(--TimelineItem--round-radius);
&--danger {
background: var(--Timeline--danger-bg);
}
&--info {
background: var(--Timeline--info-bg);
}
&--success {
background: var(--Timeline--success-bg);
}
&--warning {
background: var(--Timeline--warning-bg);
}
}
.#{$ns}TimelineItem-icon {
position: absolute;
width: var(--TimelineItem--icon-width);
height: var(--TimelineItem--icon-height);
left: var(--TimelineItem--icon-left);
border-radius: var(--TimelineItem--icon-radius);
}
}
}
}

View File

@ -29,6 +29,8 @@
pointer-events: all;
margin-left: var(--Checkbox-gap);
cursor: pointer;
display: inline-block;
vertical-align: middle;
> a {
// float: right;

View File

@ -10,6 +10,10 @@
color: var(--ColorPicker-color);
border-radius: var(--borderRadius);
&-popup{
height: 80vh;
}
&:not(.is-disabled) {
cursor: pointer;

View File

@ -127,6 +127,10 @@
}
}
.#{$ns}DateRangePicker-popup {
height: 90vh;
}
@include media-breakpoint-up(sm) {
.#{$ns}DateRangePicker-wrap {
white-space: nowrap;

View File

@ -122,6 +122,9 @@
}
}
.#{$ns}DatePicker-popup {
height: 80vh;
}
// override third-party styles
.rdt {
user-select: none;

View File

@ -15,8 +15,9 @@
}
&-selectBtn {
display: flex;
display: inline-flex;
align-items: center;
margin-right: 10px;
> svg {
width: px2rem(14px);
@ -40,7 +41,6 @@
// }
&-description {
margin-left: 10px;
color: #999;
font-size: 12px;
}

View File

@ -406,3 +406,171 @@
.#{$ns}Form-column-10 > .#{$ns}Form-item {
width: 10%;
}
/* 移动端样式调整 */
@include media-breakpoint-down(sm) {
.#{$ns}Form {
.#{$ns}Form-item {
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
padding: var(--Form-item-gap) 0;
position: relative;
&::after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0;
bottom: 0;
left: 0;
border-bottom: var(--Form-input-borderWidth) solid var(--borderColor);
transform: scaleY(0.5);
}
.#{$ns}InputGroup-addOn,
.#{$ns}TextControl-addOn {
border: none;
}
> .#{$ns}Form-label {
flex: 0 0 28%;
max-width: 28%;
min-height: 1px;
text-align: left;
padding-right: calc(var(--Form--horizontal-gutterWidth) / 2);
overflow-wrap: break-word;
margin-right: 0;
margin-bottom: 0;
font-size: var(--fontSizeLg);
}
.#{$ns}Form-description {
font-size: var(--fontSizeBase);
}
.#{$ns}TextControl-input {
font-size: var(--fontSizeLg);
input {
height: calc(var(--Form-input-lineHeight) * var(--fontSizeLg));
}
}
.#{$ns}Form-value,
.#{$ns}Form-control {
flex: 1;
flex-wrap: wrap;
font-size: var(--fontSizeLg);
&.is-disabled > .#{$ns}TextControl-input {
background: transparent;
}
}
.#{$ns}Form-hint,
.#{$ns}Form-remark,
.#{$ns}Form-static,
.#{$ns}Form-group--hor .#{$ns}Form-item,
.#{$ns}SwitchControl,
.#{$ns}CheckboxControl,
.#{$ns}RadiosControl,
.#{$ns}CheckboxesControl {
padding-top: 0;
padding-bottom: 0;
}
.#{$ns}Form-group--horizontal .#{$ns}TextControl-input input {
height: var(--Form-input-height);
}
.#{$ns}Form-hint {
font-size: var(--fontSizeBase);
margin-left: 0;
color: var(--text--muted-color);
}
.#{$ns}TextControl-placeholder {
top: 0;
}
.#{$ns}Form-static {
min-height: 0;
}
.#{$ns}Form-description,
.#{$ns}Form-feedback {
font-size: var(--fontSizeBase);
}
.#{$ns}InputGroup {
.#{$ns}Select,
.#{$ns}InputGroup-btn .#{$ns}Button {
border: none;
}
> .#{$ns}TextControl-input input {
height: var(--Form-input-height);
}
}
.#{$ns}ColorPicker {
padding: 0;
border: none;
.#{$ns}ColorPicker-arrow {
display: none;
}
}
.#{$ns}Form-group--hor .#{$ns}Form-item .#{$ns}Button {
margin-bottom: var(--gap-xs);
}
.#{$ns}TextareaControl > textarea,
.#{$ns}Form-control > .#{$ns}TextControl-input,
.#{$ns}TextControl.is-focused > .#{$ns}TextControl-input {
border: none;
padding: 0 var(--Form-input-paddingX) 0 0;
box-shadow: none;
&:hover,
&:focus,
&.active {
border: none;
outline: none;
outline-style: none;
}
}
.#{$ns}Form-control > .#{$ns}TextControl-input--multiple {
padding: 0;
min-height: 0;
}
}
.#{$ns}Form-groupColumn {
margin-bottom: 0;
}
.#{$ns}Divider {
display: none;
}
.#{$ns}Tabs-pane {
padding: 0;
}
> .#{$ns}Form-item {
&:last-child::after,
&:last-of-type::after {
display: none;
}
}
.#{$ns}Form-item .#{$ns}Form-groupColumn > .#{$ns}Form-item {
padding-bottom: var(--Form-input-paddingX);
}
}
}

View File

@ -87,4 +87,7 @@
}
}
}
&-popup {
height: px2rem(460px);
}
}

View File

@ -1,31 +1,70 @@
.#{$ns}Rating {
display: flex;
position: relative;
overflow: hidden;
display: block;
float: left;
font-size: px2rem(24px);
color: var(--dark);
cursor: pointer;
align-items: center;
justify-content: flex-start;
flex-flow: row wrap;
&.is-active {
color: var(--Rating-onActive-color);
& > ul {
display: flex;
padding: unset;
margin: unset;
align-items: center;
justify-content: flex-start;
flex-flow: row wrap;
}
&.is-disabled {
cursor: not-allowed;
pointer-events: none;
}
&-half:before {
position: absolute;
&-star {
position: relative;
margin-right: var(--Rating-star-margin);
overflow: hidden;
display: block;
z-index: 1;
top: 0;
left: 0;
width: 50%;
content: attr(data-forhalf);
color: var(--Rating-onActive-color);
font-size: var(--Rating-star-size);
line-height: 1;
cursor: pointer;
user-select: none;
color: var(--Rating-inactive-color);
&-half > svg.icon,
& > svg.icon {
display: block;
width: px2rem(24px);
height: px2rem(24px);
top: 0;
}
&:last-of-type {
margin-right: 0;
}
&.is-disabled {
cursor: not-allowed;
pointer-events: none;
}
&-half {
position: absolute;
overflow: hidden;
display: block;
z-index: 1;
top: 0;
left: 0;
width: 50%;
}
}
&-text {
font-size: var(--fontSizeSm);
color: var(--text-color);
font-weight: var(--fontWeightNormal);
&--left {
margin-right: var(--Rating-star-margin);
}
&--right {
margin-left: var(--Rating-star-margin);
}
}
}

View File

@ -40,6 +40,7 @@
height: var(--Form-input-height);
line-height: var(--Form-input-lineHeight);
font-size: var(--Form-input-fontSize);
align-items: center;
padding: calc(
(
var(--Form-input-height) - var(--Form-input-lineHeight) *
@ -54,6 +55,7 @@
}
> .#{$ns}Selections-dragbar {
top: 0;
width: var(--gap-lg);
position: relative;
left: calc(var(--gap-xs) * -1);

View File

@ -2,6 +2,7 @@
display: inline-flex;
vertical-align: middle;
text-align: left;
align-items: center;
outline: none;
position: relative;
font-size: var(--Form-input-fontSize);
@ -148,10 +149,27 @@
}
}
&.is-opened &-arrow > svg {
&.is-opened:not(.is-mobile) &-arrow > svg {
transform: rotate(180deg);
}
&.is-mobile {
min-height: calc(var(--Form-input-lineHeight) * var(--fontSizeLg));
border: none;
padding: 0;
font-size: var(--fontSizeLg);
.#{$ns}Select-valueWrap {
text-align: right;
padding-right: 4px;
}
.#{$ns}Select-arrow {
> svg {
transform: rotate(-90deg);
}
}
}
&-menu {
max-height: px2rem(300px);
overflow: auto;
@ -159,6 +177,13 @@
.#{$ns}Checkbox--sm > i {
margin-top: px2rem(-3px);
}
&.is-mobile {
width: 100%;
text-align: center;
.#{$ns}Select-option {
line-height: px2rem(36px);
}
}
}
&--longlist {
overflow: hidden;
@ -282,8 +307,8 @@
}
}
&.is-focused,
&.is-opened {
&.is-focused:not(.is-mobile),
&.is-opened:not(.is-mobile) {
border-color: var(--Form-input-onFocused-borderColor);
color: var(--Form-select-onFocused-color);
}
@ -314,6 +339,10 @@
fill: var(--Form-input-onHover-iconColor);
}
}
&-popup {
height: px2rem(320px);
}
}
.#{$ns}Select-popover {

View File

@ -1,7 +1,7 @@
.#{$ns}Selection {
> .#{$ns}Checkbox {
display: block;
height: var(--Form-input-height);
// height: var(--Form-input-height);
line-height: var(--Form-input-lineHeight);
font-size: var(--Form-input-fontSize);
padding: calc(
@ -41,6 +41,10 @@
}
.#{$ns}GroupedSelection {
max-height: px2rem(300px);
overflow: auto;
user-select: none;
&-group:not(:first-child) > &-itemLabel {
border-top: px2rem(1px) solid var(--ListMenu-divider-color);
}
@ -53,7 +57,7 @@
&-item {
display: flex;
height: var(--Form-input-height);
// height: var(--Form-input-height);
line-height: var(--Form-input-lineHeight);
font-size: var(--Form-input-fontSize);
padding: calc(
@ -189,7 +193,7 @@
&-itemInner {
display: flex;
align-items: center;
height: var(--Form-input-height);
// height: var(--Form-input-height);
line-height: var(--Form-input-lineHeight);
font-size: var(--Form-input-fontSize);
padding: calc(
@ -247,6 +251,7 @@
.#{$ns}ChainedSelection {
display: flex;
flex-direction: row;
min-height: 100%;
&-col {
flex-grow: 1;
@ -265,7 +270,7 @@
&-item {
display: flex;
height: var(--Form-input-height);
// height: var(--Form-input-height);
line-height: var(--Form-input-lineHeight);
font-size: var(--Form-input-fontSize);
padding: calc(

View File

@ -26,6 +26,12 @@
font-style: normal;
font-weight: bold;
transition: all var(--animation-duration);
> svg {
width: var(--fontSizeSm);
height: var(--fontSizeSm);
margin-top: calc((var(--Switch-height) - var(--fontSizeSm)) / 2);
}
}
.slider {

View File

@ -235,6 +235,9 @@
display: flex;
flex-direction: column;
&.is-mobile {
width: 100%;
}
& > .#{$ns}Transfer-selection {
flex-grow: 1;
max-height: 100%;

View File

@ -15,6 +15,10 @@
box-shadow: var(--Form-input-boxShadow);
background: var(--Form-input-onFocused-bg);
}
&-popup {
height: 80vh;
}
}
.#{$ns}TreeSelect-popover {

View File

@ -15,7 +15,7 @@ title: 背景色
| bg-primary | background: #007bff |
| bg-secondary | background: #6c757d |
| bg-success | background: #28a745 |
| bg-info | background: #17a2b8 |
| bg-info | background: #007bff |
| bg-warning | background: #28a745 |
| bg-danger | background: #dc3545 |
| bg-light | background: #f8f9fa |

View File

@ -11,7 +11,7 @@ title: 边框颜色
| border-primary | border-color: #007bff |
| border-secondary | border-color: #6c757d |
| border-success | border-color: #28a745 |
| border-info | border-color: #17a2b8 |
| border-info | border-color: #007bff |
| border-warning | border-color: #28a745 |
| border-danger | border-color: #dc3545 |
| border-light | border-color: #f8f9fa |

View File

@ -13,7 +13,7 @@ title: Text Color
| text-primary | color: #007bff |
| text-secondary | color: #6c757d |
| text-success | color: #28a745 |
| text-info | color: #17a2b8 |
| text-info | color: #007bff |
| text-warning | color: #fad733 |
| text-danger | color: #dc3545 |
| text-light | color: #f8f9fa |

View File

@ -166,4 +166,46 @@ $link-color: $info;
--Table-onChecked-borderColor: var(--Table-borderColor);
--Switch-bgColor: #bfbfbf;
// timeline
--TimelineItem--axle-flex: 0 0 #{px2rem(24px)};
--TimelineItem--left-line-width: #{px2rem(2px)};
--TimelineItem--left-line-left: #{px2rem(13px)};
--TimelineItem--left-line-top: #{px2rem(20px)};
--TimelineItem--round-width: #{px2rem(8px)};
--TimelineItem--round-height: #{px2rem(8px)};
--TimelineItem--round-left: #{px2rem(10px)};
--TimelineItem--round-top: #{px2rem(8px)};
--TimelineItem--icon-width: #{px2rem(16px)};
--TimelineItem--icon-height: #{px2rem(16px)};
--TimelineItem--icon-left: #{px2rem(6px)};
--TimelineItem--content-padding-bottom: #{px2rem(16px)};
--TimelineItem--content-margin-left: #{px2rem(8px)};
--TimelineItem--content-time-margin-bottom: #{px2rem(4px)};
--TimelineItem--content-title-margin-bottom: #{px2rem(4px)};
--TimelineItem--detail-button-margin-bottom: #{px2rem(8px)};
--TimelineItem-detail-arrow-width: #{px2rem(16px)};
--TimelineItem-detail-visible-padding: #{px2rem(10px)};
--TimelineItem-detail-visible-max-width: #{px2rem(300px)};
--Timeline-alternate-margin-left: #{px2rem(24px)};
--TimelineItem--icon-radius: 50%;
--TimelineItem--round-radius: 50%;
--TimelineItem--content-radius: #{px2rem(2px)};
--TimelineItem-detail-visible-shadow: 0 #{px2rem(1px)} #{px2rem(10px)} 0 rgba(0 0 0 / 10%);
--TimelineItem--font-size: #{px2rem(12px)};
--TimelineItem--text-primary-color: #151a26;
--TimelineItem--text-secondary-color: #83868c;
--TimelineItem--detail-button-color: var(--primary);
--TimelineItem--line-bg: #e6e6e8;
--TimelineItem--content-bg: #f2f2f4;
--TimelineItem-round-bg: #dadbdd;
--Timeline--success-bg: var(--success);
--Timeline--info-bg: var(--info);
--Timeline--warning-bg: var(--warning);
--Timeline--danger-bg: var(--danger);
}

View File

@ -18,6 +18,7 @@
@import '../components/tooltip';
@import '../components/tpl';
@import '../components/popover';
@import '../components/popup';
@import '../components/picker-columns';
@import '../components/toast';
@import '../components/alert';
@ -111,11 +112,14 @@
@import '../components/form/rating';
@import '../components/form/transfer';
@import '../components/form/nested-select';
@import '../components/cascader';
@import '../components/form/icon-picker';
@import '../components/form/form';
@import '../components/anchor-nav';
@import '../components/markdown';
@import '../components/link';
@import '../components/mapping';
@import '../components/formula';
@import '../components/timeline';
@import '../utilities';

View File

@ -364,7 +364,7 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Table-onHover-bg: #f5f5f5;
--Table-onHover-bg-rgb: 245, 251, 255;
--Table-onHover-borderColor: #eceff8;
--Table-onChecked-bg: transparent;
--Table-onChecked-bg: #f0faff;
--Table-onChecked-borderColor: #eceff8;
--Table-onChecked-color: #333;
--Table-onChecked-onHover-bg: #f5f5f5;
@ -610,4 +610,53 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Tree-itemHeight: #{px2rem(32px)};
--Progress-borderRadius: #{$R7};
--Rating-inactive-color: #{$G9};
// timeline
--TimelineItem--axle-flex: 0 0 #{px2rem(24px)};
--TimelineItem--left-line-width: #{px2rem(2px)};
--TimelineItem--left-line-left: #{px2rem(13px)};
--TimelineItem--left-line-top: #{px2rem(20px)};
--TimelineItem--round-width: #{px2rem(8px)};
--TimelineItem--round-height: #{px2rem(8px)};
--TimelineItem--round-left: #{px2rem(10px)};
--TimelineItem--round-top: #{px2rem(8px)};
--TimelineItem--icon-width: #{px2rem(16px)};
--TimelineItem--icon-height: #{px2rem(16px)};
--TimelineItem--icon-left: #{px2rem(6px)};
--TimelineItem--content-padding-bottom: #{px2rem(16px)};
--TimelineItem--content-margin-left: #{px2rem(8px)};
--TimelineItem--content-time-margin-bottom: #{px2rem(4px)};
--TimelineItem--content-title-margin-bottom: #{px2rem(4px)};
--TimelineItem--detail-button-margin-bottom: #{px2rem(8px)};
--TimelineItem-detail-arrow-width: #{px2rem(16px)};
--TimelineItem-detail-visible-padding: #{px2rem(10px)};
--TimelineItem-detail-visible-max-width: #{px2rem(300px)};
--Timeline-alternate-margin-left: #{px2rem(24px)};
--TimelineItem--icon-radius: #{$R8};
--TimelineItem--round-radius: #{$R8};
--TimelineItem--content-radius: #{$R2};
--TimelineItem-detail-visible-shadow: 0 #{px2rem(1px)} #{px2rem(10px)} 0 rgba(0 0 0 / 10%);
--TimelineItem--font-size: #{$T2};
--TimelineItem--text-primary-color: #{$text-color};
--TimelineItem--text-secondary-color: #{$G5};
--TimelineItem--detail-button-color: var(--primary);
--TimelineItem--line-bg: #{$G9};
--TimelineItem--content-bg: #{$G10};
--TimelineItem-round-bg: #{$G8};
--Timeline--success-bg: var(--success);
--Timeline--info-bg: var(--info);
--Timeline--warning-bg: var(--warning);
--Timeline--danger-bg: var(--danger);
// Formula
--Formula-header-bgColor: #{$G10};
--Formula-funcItem-bgColor-onActive: #{$light};
}

View File

@ -57,6 +57,7 @@ $link-color: $info;
--DatePicker-bg: var(--background);
--DatePicker-header-select-borderColor: var(--background);
--DropDown-menu-bg: var(--background);
--Drawer-header-bg: var(--background);
--Fieldset-legend-bgColor: var(--background);
--Form-input-addOnBg: var(--Form-input-bg);
--Form-input-bg: #3c3c3c;

View File

@ -150,6 +150,7 @@ export function renderChild(
<SchemaRenderer
{...props}
schema={schema}
propKey={schema.key}
$path={`${prefix ? `${prefix}/` : ''}${(schema && schema.type) || ''}`}
/>
);

View File

@ -184,12 +184,12 @@ export class RootRenderer extends React.Component<RootRendererProps> {
return;
}
store.closeDialog();
store.closeDialog(true);
}
handleDialogClose() {
handleDialogClose(confirmed = false) {
const store = this.store;
store.closeDialog();
store.closeDialog(confirmed);
}
handleDrawerConfirm(values: object[], action: Action, ...args: Array<any>) {

View File

@ -56,6 +56,7 @@ import {PaginationSchema} from './renderers/Pagination';
import {AnchorNavSchema} from './renderers/AnchorNav';
import {AvatarSchema} from './renderers/Avatar';
import {StepsSchema} from './renderers/Steps';
import {TimelineSchema} from './renderers/Timeline';
import {ArrayControlSchema} from './renderers/Form/InputArray';
import {ButtonGroupControlSchema} from './renderers/Form/ButtonGroupSelect';
import {ChainedSelectControlSchema} from './renderers/Form/ChainedSelect';
@ -201,6 +202,7 @@ export type SchemaType =
| 'web-component'
| 'anchor-nav'
| 'steps'
| 'timeline'
| 'control'
| 'input-array'
| 'button'
@ -227,6 +229,7 @@ export type SchemaType =
| 'input-time-range'
| 'input-datetime-range'
| 'input-excel'
| 'input-formula'
| 'diff-editor'
// editor 系列
@ -386,6 +389,7 @@ export type SchemaObject =
| AnchorNavSchema
| StepsSchema
| PortletSchema
| TimelineSchema
// 表单项
| FormControlSchema

View File

@ -242,6 +242,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
data: defaultData,
value: defaultValue,
activeKey: defaultActiveKey,
key: propKey,
...restSchema
} = schema;
return rest.invisible
@ -253,6 +254,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
defaultData,
defaultValue,
defaultActiveKey,
propKey,
$path: $path,
$schema: schema,
ref: isSFC ? undefined : this.refFn,
@ -293,6 +295,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
const {
data: defaultData,
value: defaultValue,
key: propKey,
activeKey: defaultActiveKey,
...restSchema
} = schema;
@ -321,6 +324,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
defaultData={restSchema.defaultData ?? defaultData}
defaultValue={restSchema.defaultValue ?? defaultValue}
defaultActiveKey={defaultActiveKey}
propKey={propKey}
$path={$path}
$schema={{...schema, ...exprProps}}
ref={this.refFn}

View File

@ -24,7 +24,7 @@ interface LinkItemProps {
export interface Navigation {
label: string;
children: Array<LinkItem>;
children?: Array<LinkItem>;
prefix?: JSX.Element;
affix?: JSX.Element;
className?: string;

View File

@ -106,7 +106,8 @@ export class AssociatedSelection extends BaseSelection<
leftMode,
cellRender,
multiple,
onDeferLoad
onDeferLoad,
itemRender
} = this.props;
const selectdOption = BaseSelection.resolveSelected(
@ -186,6 +187,7 @@ export class AssociatedSelection extends BaseSelection<
onChange={onChange}
option2value={option2value}
multiple={multiple}
itemRender={itemRender}
/>
) : rightMode === 'chained' ? (
<ChainedSelection
@ -195,6 +197,7 @@ export class AssociatedSelection extends BaseSelection<
onChange={onChange}
option2value={option2value}
multiple={multiple}
itemRender={itemRender}
/>
) : (
<GroupedSelection
@ -204,6 +207,7 @@ export class AssociatedSelection extends BaseSelection<
onChange={onChange}
option2value={option2value}
multiple={multiple}
itemRender={itemRender}
/>
)
) : (

253
src/components/Avatar.tsx Normal file
View File

@ -0,0 +1,253 @@
import * as React from 'react';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
/**
* Avatar
*/
interface AvatarCmptProps extends ThemeProps {
style?: {
[prop: string]: any
};
className?: string;
classnames: ClassNamesFn;
/**
*
*/
src?: string | React.ReactNode;
/**
*
*/
icon?: string | React.ReactNode;
/**
*
*/
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
/**
*
*/
shape?: 'circle' | 'square' | 'rounded';
/**
*
*/
size?: number | 'small' | 'default' | 'large';
/**
*
*/
text?: string;
/**
*
*/
gap?: number;
/**
*
*/
alt?: string;
/**
*
*/
draggable?: boolean;
/**
* CORS属性
*/
crossOrigin?: 'anonymous' | 'use-credentials' | '';
/**
* false
*/
onError?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => boolean;
/**
*
*/
children?: JSX.Element | ((props?: any) => JSX.Element)
}
const prefix = 'Avatar--';
const childPrefix = prefix + 'text';
export interface AvatarState {
scale: number;
hasImg: boolean;
}
export class Avatar extends React.Component<AvatarCmptProps, AvatarState> {
static defaultProps: Partial<AvatarCmptProps> = {
shape: 'circle',
size: 'default',
fit: 'cover',
gap: 4
};
state: AvatarState = {
scale: 1,
hasImg: true
};
avatarChildrenRef: React.RefObject<HTMLElement>;
avatarRef: React.RefObject<HTMLElement>;
constructor(props: AvatarCmptProps) {
super(props);
this.avatarChildrenRef = React.createRef();
this.avatarRef = React.createRef();
this.handleImageLoadError = this.handleImageLoadError.bind(this);
}
componentDidMount() {
this.setScaleByGap();
}
componentDidUpdate(prevProps: AvatarCmptProps, prevState: AvatarState) {
const {src, gap, text, children} = this.props;
const {hasImg} = this.state;
if (prevProps.src !== src) {
this.setState({
hasImg: !!src
});
}
if ((prevState.hasImg && !hasImg)
|| (prevProps.text !== text)
|| (prevProps.children !== children)
|| (prevProps.gap !== gap)) {
this.setScaleByGap();
}
}
handleImageLoadError(event: React.SyntheticEvent<HTMLImageElement, Event>) {
const {onError} = this.props;
this.setState({
hasImg: onError ? !onError(event) : false
});
}
setScaleByGap() {
const {gap = 4} = this.props;
if (!this.avatarChildrenRef.current || !this.avatarRef.current) {
return;
}
const childrenWidth = this.avatarChildrenRef.current.offsetWidth;
const nodeWidth = this.avatarRef.current.offsetWidth;
if (childrenWidth && nodeWidth) {
if (gap * 2 < nodeWidth) {
const diff = nodeWidth - gap * 2;
this.setState({
scale: diff < childrenWidth ? diff / childrenWidth : 1
});
}
}
};
render() {
let {
style = {},
className,
shape,
size,
src,
icon,
alt,
draggable,
crossOrigin,
fit,
text,
children,
classnames: cx
} = this.props;
const {scale, hasImg} = this.state;
const isImgRender = React.isValidElement(src);
const isIconRender = React.isValidElement(icon);
let childrenRender;
let sizeStyle = {};
let sizeClass = '';
if (typeof size === 'number') {
sizeStyle = {
height: size,
width: size,
lineHeight: size + 'px'
};
}
else if (typeof size === 'string') {
sizeClass = size === 'large'
? `${prefix}lg`
: size === 'small' ? `${prefix}sm` : '';
}
const scaleX = `scale(${scale}) translateX(-50%)`;
const scaleStyle = {
msTransform: scaleX,
WebkitTransform: scaleX,
transform: scaleX
};
if (typeof src === 'string' && hasImg) {
const imgStyle = fit ? {objectFit: fit} : {};
childrenRender = (
<img
style={imgStyle}
src={src}
alt={alt}
draggable={draggable}
onError={this.handleImageLoadError}
crossOrigin={crossOrigin}
/>
);
}
else if (isImgRender) {
childrenRender = src;
}
else if (typeof text === 'string' || typeof text === 'number') {
childrenRender = (
<span
className={cx(childPrefix)}
ref={this.avatarChildrenRef}
style={scaleStyle}>
{text}
</span>
);
}
else if (typeof icon === 'string') {
childrenRender = (<i className={icon} />);
}
else if (isIconRender) {
childrenRender = icon;
}
else {
childrenRender = (
<span
className={cx(childPrefix)}
ref={this.avatarChildrenRef}
style={scaleStyle}>
{children}
</span>
);
}
return (
<span
className={cx(`Avatar`, className, prefix + shape, sizeClass)}
style={{...sizeStyle, ...style}}
ref={this.avatarRef}>
{childrenRender}
</span>
);
}
}
export default themeable(Avatar);

View File

@ -120,7 +120,11 @@ export class BaiduMapPicker extends React.Component<
? new BMap.Point(value.lng, value.lat)
: new BMap.Point(116.404, 39.915);
if (this.props.coordinatesType == 'gcj02') {
point = await this.covertPoint(point, COORDINATES_GCJ02, COORDINATES_BD09);
point = await this.covertPoint(
point,
COORDINATES_GCJ02,
COORDINATES_BD09
);
map.centerAndZoom(point, 15);
} else {
map.centerAndZoom(point, 15);
@ -269,27 +273,29 @@ export class BaiduMapPicker extends React.Component<
covertPoint(point: any, from: number, to: number) {
return new Promise((resolve, reject) => {
this.convertor.translate([point], from, to, (res:any)=> {
this.convertor.translate([point], from, to, (res: any) => {
if (res.status === 0 && res.points.length) {
resolve(new BMap.Point(res.points[0].lng, res.points[0].lat));
} else {
} else {
reject();
}
})
})
});
});
}
triggerOnChange(loc: LocationItem) {
const point = new BMap.Point(loc.lng, loc.lat);
if (this.props.coordinatesType == 'gcj02') {
this.covertPoint(point, COORDINATES_BD09, COORDINATES_GCJ02).then((convertedPoint:any)=>{
this.props?.onChange({
address: loc.address.trim() || loc.title,
lat: convertedPoint.lat,
lng: convertedPoint.lng,
city: loc.city
});
})
this.covertPoint(point, COORDINATES_BD09, COORDINATES_GCJ02).then(
(convertedPoint: any) => {
this.props?.onChange({
address: loc.address.trim() || loc.title,
lat: convertedPoint.lat,
lng: convertedPoint.lng,
city: loc.city
});
}
);
} else {
this.props?.onChange({
address: loc.address.trim() || loc.title,
@ -298,7 +304,6 @@ export class BaiduMapPicker extends React.Component<
city: loc.city
});
}
}
@autobind

View File

@ -0,0 +1,563 @@
/**
* @file CalendarMobile
* @description
* @author hongyang03
*/
import React from 'react';
import moment from 'moment';
import Calendar from './calendar/Calendar';
import {themeable, ThemeProps} from '../theme';
import {LocaleProps, localeable} from '../locale';
import {autobind} from '../utils/helper';
export interface CalendarMobileProps extends ThemeProps, LocaleProps {
className?: string;
timeFormat?: string;
inputFormat?: string;
startDate?: moment.Moment;
endDate?: moment.Moment;
minDate?: moment.Moment;
maxDate?: moment.Moment;
minDuration?: moment.Duration;
maxDuration?: moment.Duration;
dateFormat?: string;
embed?: boolean;
viewMode?: 'days' | 'months' | 'years' | 'time' | 'quarters';
close?: () => void;
confirm?: () => void;
onChange?: (data: any, callback?: () => void) => void;
footerExtra?: JSX.Element | null;
showViewMode?: 'years' | 'months';
}
export interface CalendarMobileState {
startDate?: moment.Moment;
endDate?: moment.Moment;
monthHeights?: number[];
currentDate: moment.Moment;
showToast: boolean;
isScrollToBottom: boolean;
dateTime: any;
}
export class CalendarMobile extends React.Component<
CalendarMobileProps,
CalendarMobileState
> {
mobileBody: any;
mobileHeader: any;
timer: any;
static defaultProps: Pick<CalendarMobileProps, 'showViewMode' | 'minDate' | 'maxDate'> = {
showViewMode: 'months',
minDate: moment().subtract(1, 'year').startOf('months'),
maxDate: moment().add(1, 'year').endOf('months'),
};
constructor(props: CalendarMobileProps) {
super(props);
this.mobileBody = React.createRef();
this.mobileHeader = React.createRef();
const {startDate, endDate, viewMode} = this.props;
this.state = {
startDate,
endDate,
showToast: false,
currentDate: moment(),
isScrollToBottom: false,
dateTime: endDate ? [endDate.hour(), endDate.minute()] : [0, 0]
};
}
componentDidMount() {
this.initMonths();
}
componentDidUpdate(prevProps: CalendarMobileProps) {
const props = this.props;
if (prevProps.minDate !== props.minDate || prevProps.maxDate !== props.maxDate) {
this.initMonths();
}
}
componentWillUnmount() {
this.setState({showToast: false});
clearTimeout(this.timer);
}
initMonths() {
if (this.mobileBody.current) {
const header = this.mobileHeader.current;
let monthHeights: number[] = [];
const monthCollection = this.mobileBody.current.children;
for (let i = 0; i < monthCollection.length; i++) {
monthHeights[i] = monthCollection[i].offsetTop - header.clientHeight;
}
this.setState({
monthHeights
});
this.scollToDate(moment());
}
}
scollToDate(date: moment.Moment) {
const {minDate, showViewMode} = this.props;
const index = date.diff(minDate, showViewMode);
const currentEl = this.mobileBody.current.children[index];
const header = this.mobileHeader.current;
this.mobileBody.current.scrollBy(0, currentEl.offsetTop - this.mobileBody.current.scrollTop - header.clientHeight);
}
@autobind
onMobileBodyScroll(e: any) {
const {showViewMode} = this.props;
const {monthHeights} = this.state;
let minDate = this.props.minDate?.clone();
if (!this.mobileBody?.current || !monthHeights || !minDate) {
return;
}
const scrollTop = this.mobileBody.current.scrollTop;
const clientHeight = this.mobileBody.current.clientHeight;
const scrollHeight = this.mobileBody.current.scrollHeight;
let i = 0;
for(i; i < monthHeights.length; i++) {
if (scrollTop < monthHeights[i]) {
break;
}
}
i--;
i < 0 && (i = 0);
const currentDate = minDate.add(i, showViewMode);
this.setState({
currentDate,
isScrollToBottom: scrollTop + clientHeight === scrollHeight
});
}
@autobind
scrollPreYear() {
if (!this.state.currentDate) {
return;
}
const {minDate} = this.props;
let {currentDate} = this.state;
currentDate = currentDate.clone().subtract(1, 'years');
if (minDate && currentDate.isBefore(minDate)) {
currentDate = minDate;
}
this.setState({
currentDate
});
this.scollToDate(currentDate);
}
@autobind
scrollAfterYear() {
if (!this.state.currentDate) {
return;
}
const {maxDate} = this.props;
let {currentDate} = this.state;
currentDate = currentDate.clone().add(1, 'years');
if (maxDate && currentDate.isAfter(maxDate)) {
currentDate = maxDate;
}
this.setState({
currentDate
});
this.scollToDate(currentDate);
}
getDaysOfWeek() {
const locale = moment().localeData();
const days = locale.weekdaysMin();
const first = locale.firstDayOfWeek();
const dow: string[] = [];
let i = 0;
days.forEach((day: string) => {
dow[ (7 + ( i++ ) - first) % 7 ] = day;
});
return dow;
}
@autobind
handleCalendarClick(isDisabled: boolean) {
if (isDisabled) {
this.setState({showToast: true});
this.timer = setTimeout(() => {
this.setState({showToast: false});
}, 2000);
}
}
getRenderProps(props: any, currentDate: moment.Moment) {
let {startDate, endDate} = this.state;
const {translate: __, viewMode} = this.props;
const precision = viewMode === 'time' ? 'hours' : viewMode || 'day';
let footerText = '';
if (startDate &&
endDate &&
currentDate.isBetween(startDate, endDate, precision, '()')) {
props.className += ' rdtBetween';
}
else if (startDate
&& endDate
&& startDate.isSame(endDate, precision)
&& currentDate.isSame(startDate, precision)) {
props.className += ' rdtRangeStart';
footerText = __('Calendar.beginAndEnd');
}
else if (startDate && currentDate.isSame(startDate, precision)) {
props.className += ' rdtRangeStart';
footerText = __('Calendar.begin');
if (endDate) {
props.className += ' rdtRangeHasEnd';
}
}
else if (endDate && currentDate.isSame(endDate, precision)) {
props.className += ' rdtRangeEnd';
footerText = __('Calendar.end');
}
if (precision === 'day' && currentDate.date() === 1 && currentDate.day() === 1) {
props.className += ' rdtOldNone';
}
const rdtDisabled = props.className.indexOf('rdtDisabled') > -1;
return {
props,
footerText,
rdtDisabled
};
}
@autobind
handleTimeChange(newTime: any) {
if (!newTime) {
return;
}
const {onChange} = this.props;
let {startDate, endDate} = this.state;
if (startDate) {
let obj = {
dateTime: newTime,
startDate: endDate ? startDate : startDate?.clone().set({hour: newTime[0], minute: newTime[1], second: 0}),
endDate: !endDate ? endDate : endDate?.clone().set({hour: newTime[0], minute: newTime[1], second: 0})
};
this.setState(obj, () => {
onChange && onChange(this.state);
});
}
}
@autobind
checkIsValidDate(currentDate: moment.Moment) {
const {minDate, maxDate} = this.props;
let {startDate, endDate} = this.state;
let {minDuration, maxDuration, viewMode} = this.props;
const precision = viewMode === 'time' ? 'hours' : viewMode || 'day';
if (minDate && currentDate.isBefore(minDate, precision)) {
return false;
}
else if (maxDate && currentDate.isAfter(maxDate, precision)) {
return false;
}
else if (startDate && !endDate) {
if (minDuration
&& currentDate.isBefore(startDate.clone().add(minDuration))
&& currentDate.isSameOrAfter(startDate)) {
return false;
}
else if (maxDuration && currentDate.isAfter(startDate.clone().add(maxDuration))) {
return false;
}
}
return true;
}
@autobind
renderMobileDay(props: any, currentDate: moment.Moment) {
const cx = this.props.classnames;
const renderProps = this.getRenderProps(props, currentDate);
return <td {...renderProps.props}>
<div className="calendar-wrap" onClick={() => this.handleCalendarClick(renderProps.rdtDisabled)}>
{currentDate.date()}
<div className={cx('CalendarMobile-range-text')}>{renderProps.footerText}</div>
</div>
</td>;
}
@autobind
renderMonth(props: any, month: number, year: number) {
const cx = this.props.classnames;
const currentDate = moment().year(year).month(month);
const monthStr = currentDate
.localeData()
.monthsShort(currentDate.month(month));
const strLength = 3;
const monthStrFixedLength = monthStr.substring(0, strLength);
const renderProps = this.getRenderProps(props, currentDate);
return (
<td {...renderProps.props}>
<div className="calendar-wrap" onClick={() => this.handleCalendarClick(renderProps.rdtDisabled)}>
{monthStrFixedLength}
<div className={cx('CalendarMobile-range-text')}>{renderProps.footerText}</div>
</div>
</td>
);
}
@autobind
renderQuarter(props: any, quarter: number, year: number) {
const cx = this.props.classnames;
const currentDate = moment().year(year).quarter(quarter);
const renderProps = this.getRenderProps(props, currentDate);
return (
<td {...props}>
<div className="calendar-wrap" onClick={() => this.handleCalendarClick(renderProps.rdtDisabled)}>
Q{quarter}
<div className={cx('CalendarMobile-range-text')}>{renderProps.footerText}</div>
</div>
</td>
);
}
@autobind
handleMobileChange(newValue: moment.Moment) {
const {embed, minDuration, maxDuration, confirm, onChange, viewMode, minDate, maxDate} = this.props;
const {startDate, endDate, dateTime} = this.state;
const precision = viewMode === 'time' ? 'hours' : viewMode || 'day';
if (minDate && newValue && newValue.isBefore(minDate, 'second')) {
newValue = minDate;
}
if (maxDate && newValue && newValue.isAfter(maxDate, 'second')) {
newValue = maxDate;
}
if (
startDate &&
!endDate &&
newValue.isSameOrAfter(startDate) &&
(!minDuration || newValue.isSameOrAfter(startDate.clone().add(minDuration))) &&
(!maxDuration || newValue.isSameOrBefore(startDate.clone().add(maxDuration)))
) {
return this.setState(
{
endDate: newValue.clone().endOf(precision).set({hour: dateTime[0], minute: dateTime[1], second: 0})
},
() => {
onChange && onChange(this.state, () => embed && confirm && confirm());
}
);
}
this.setState(
{
startDate: newValue.clone().startOf(precision).set({hour: dateTime[0], minute: dateTime[1], second: 0}),
endDate: undefined
},
() => {
onChange && onChange(this.state);
}
);
}
@autobind
renderMobileCalendarBody() {
const {
classnames: cx,
dateFormat,
timeFormat,
inputFormat,
locale,
viewMode = 'days',
close
} = this.props;
const __ = this.props.translate;
const {minDate, maxDate, showViewMode} = this.props;
if (!minDate || !maxDate) {
return;
}
let calendarDates: moment.Moment[] = [];
for(let minDateClone = minDate.clone(); minDateClone.isSameOrBefore(maxDate); minDateClone.add(1, showViewMode)) {
calendarDates.push(minDateClone.clone());
}
return (
<div className={cx('CalendarMobile-body')} ref={this.mobileBody}
onScroll={this.onMobileBodyScroll}>
{calendarDates.map((calendarDate: moment.Moment, index: number) => {
const rdtOldNone = showViewMode === 'months'
&& calendarDate.clone().startOf('month').day() === 1
? 'rdtOldNone' : '';
return <div className={cx('CalendarMobile-calendar-wrap', rdtOldNone)} key={'calendar-wrap' + index}>
{showViewMode === 'months' && <div className={cx('CalendarMobile-calendar-mark')} key={'calendar-mark' + index}>
{calendarDate.month() + 1}
</div>}
<div className={cx('CalendarMobile-calendar-header')}>
<span className="rdtSwitch">
{calendarDate.format(__('dateformat.year'))}
</span>
{showViewMode === 'months' && <span className="rdtSwitch">
{calendarDate.format(__('MMM'))}
</span>}
</div>
<Calendar
className={cx('CalendarMobile-calendar', rdtOldNone)}
viewDate={calendarDate}
value={calendarDate}
onChange={this.handleMobileChange}
requiredConfirm={false}
dateFormat={dateFormat}
inputFormat={inputFormat}
timeFormat=''
isValidDate={this.checkIsValidDate}
viewMode={viewMode}
input={false}
onClose={close}
renderDay={this.renderMobileDay}
renderMonth={this.renderMonth}
renderQuarter={this.renderQuarter}
locale={locale}
hideHeader={true}
updateOn={viewMode}
key={'calendar' + index}
/>
</div>
})}
</div>
);
}
@autobind
renderMobileTimePicker() {
const {
classnames: cx,
timeFormat,
locale,
close
} = this.props;
const __ = this.props.translate;
const {startDate, endDate, dateTime} = this.state;
return (
<div className={cx('CalendarMobile-time')}>
<div className={cx('CalendarMobile-time-title')}>
{startDate && endDate ? __('Calendar.endPick') : __('Calendar.startPick')}
</div>
<Calendar
className={cx('CalendarMobile-time-calendar')}
onChange={this.handleTimeChange}
requiredConfirm={false}
timeFormat={timeFormat}
viewMode="time"
input={false}
onClose={close}
locale={locale}
useMobileUI={true}
showToolbar={false}
viewDate={moment().set({hour: dateTime[0], minute: dateTime[1], second: 0})}
/>
</div>
);
}
render() {
const {
className,
classnames: cx,
embed,
confirm,
footerExtra,
timeFormat,
minDate,
maxDate,
showViewMode
} = this.props;
const __ = this.props.translate;
const {startDate, endDate, currentDate, showToast, isScrollToBottom} = this.state;
let dateNow = currentDate
? currentDate.format(__(`Calendar.${showViewMode === 'months' ? 'yearmonth' : 'year'}`))
: moment().format(__(`Calendar.${showViewMode === 'months' ? 'yearmonth' : 'year'}`));
const header = (
<div className={cx('CalendarMobile-header')} ref={this.mobileHeader}>
<div className={cx('CalendarMobile-subtitle')}>
<span className="subtitle-text">
{currentDate && currentDate.isSameOrBefore(minDate, showViewMode)
? null
: <a className="rdtPrev" onClick={this.scrollPreYear}>&lsaquo;</a>}
{dateNow}
{currentDate && currentDate.isSameOrAfter(maxDate, showViewMode) || isScrollToBottom
? null
: <a className="rdtNext" onClick={this.scrollAfterYear}>&rsaquo;</a>}
</span>
</div>
{showViewMode === 'months' ? <div className={cx('CalendarMobile-weekdays')}>
{this.getDaysOfWeek().map((day: string, index: number) => (
<span key={day + index} className="weekday">
{day}
</span>
))}
</div> : null}
</div>
);
const footer = (
<div className={cx('CalendarMobile-footer')}>
{timeFormat && startDate && this.renderMobileTimePicker()}
<div className={cx('CalendarMobile-footer-toolbar')}>
<div className={cx('CalendarMobile-footer-ranges')}>
{footerExtra}
</div>
{confirm && !embed && <a
className={cx('Button', 'Button--primary', 'date-range-confirm', {
'is-disabled': !startDate || !endDate
})}
onClick={confirm}
>
{__('confirm')}
</a>}
</div>
</div>
);
return (
<div className={cx('CalendarMobile',
embed ? 'CalendarMobile-embed' : '',
className)}>
<div className={cx('CalendarMobile-wrap')}>
{header}
{this.renderMobileCalendarBody()}
{footer}
</div>
{showToast? <div className={cx('CalendarMobile-toast')}>{__('Calendar.toast')}</div> : null}
</div>
);
}
};
export default themeable(localeable(CalendarMobile));

View File

@ -176,7 +176,7 @@ export class Card extends React.Component<CardProps> {
</div>
</div>
) : (
<div>
<>
{heading}
{body ? (
<div className={cx('Card-body', bodyClassName)}>{body}</div>
@ -193,7 +193,7 @@ export class Card extends React.Component<CardProps> {
) : null}
</div>
) : null}
</div>
</>
)}
</div>
);

565
src/components/Cascader.tsx Normal file
View File

@ -0,0 +1,565 @@
/**
* @file Cascader
* @author fex
*/
import React from 'react';
import {autobind, getTreeAncestors} from '../utils/helper';
import {themeable} from '../theme';
import {NestedSelectProps} from '../renderers/Form/NestedSelect';
import {Option, Options} from './Select';
import intersectionBy from 'lodash/intersectionBy';
import compact from 'lodash/compact';
import find from 'lodash/find';
import uniqBy from 'lodash/uniqBy';
import Button from './Button';
import {flattenTree, findTree, getTreeDepth} from '../utils/helper';
export type CascaderOption = {
text?: string;
value?: string | number;
color?: string;
disabled?: boolean;
children?: Options;
className?: string;
[key: string]: any;
};
export interface CascaderProps extends NestedSelectProps {
value?: (number | string)[];
activeColor?: string;
optionRender?: ({
option,
selected
}: {
option: CascaderOption;
selected: boolean;
}) => React.ReactNode;
onClose?: () => void;
onConfirm?: (param: any) => void;
multiple?: boolean;
}
export type CascaderTab = {
options: Options;
};
export interface CascaderState {
selectedOptions: Options;
activeTab: number;
tabs: Array<{
options: Options;
}>;
}
export class Cascader extends React.Component<CascaderProps, CascaderState> {
static defaultProps = {
labelField: 'label',
valueField: 'value'
};
tabsRef: React.RefObject<HTMLDivElement> = React.createRef();
tabRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: CascaderProps) {
super(props);
this.state = {
selectedOptions: this.props.selectedOptions || [],
activeTab: 0,
tabs: [
{
options: this.props.options.slice() || []
}
]
};
}
componentDidMount() {
const {multiple, options, valueField = 'value', cascade} = this.props;
let selectedOptions = this.props.selectedOptions.slice();
let parentsCount = 0;
let parentTree: Options = [];
selectedOptions.forEach((item: Option) => {
const parents = getTreeAncestors(options, item as any);
// 获取最长路径
if (parents && parents?.length > parentsCount) {
parentTree = parents;
parentsCount = parentTree.length;
}
});
const selectedValues = selectedOptions.map(
(option: Option) => option[valueField]
);
const tabs = parentTree.map((option: Option) => {
if (multiple && !cascade) {
if (
selectedValues.includes(option[valueField]) &&
option?.children?.length
) {
option.children.forEach((option: Option) => (option.disabled = true));
}
}
return multiple
? {
options: [
{
...option,
isCheckAll: true
},
...(option.children ? option.children : [])
]
}
: {
options: option.children ? option.children : []
};
});
this.setState({
selectedOptions,
tabs: [...this.state.tabs, ...tabs]
});
}
@autobind
handleTabSelect(index: number) {
const tabs = this.state.tabs.slice(0, index + 1);
this.setState({
activeTab: index,
tabs
});
}
@autobind
getOptionParent(option: Option) {
const {options, valueField = 'value'} = this.props;
let ancestors: any[] = [];
findTree(options, (item, index, level, paths) => {
if (item[valueField] === option[valueField]) {
ancestors = paths;
return true;
}
return false;
});
return ancestors.length ? ancestors[ancestors.length - 1] : null;
}
@autobind
dealParentSelect(option: Option, selectedOptions: Options): Options {
const {valueField = 'value'} = this.props;
const parentOption = this.getOptionParent(option);
if (parentOption) {
const parentChildren = parentOption?.children;
const equalOption = intersectionBy(
selectedOptions,
parentChildren,
valueField
);
// 包含则选中父节点
const isParentSelected = find(selectedOptions, {
[valueField]: parentOption[valueField]
});
if (equalOption.length === parentChildren?.length && !isParentSelected) {
selectedOptions.push(parentOption);
}
if (equalOption.length !== parentChildren?.length && isParentSelected) {
const index = selectedOptions.findIndex(
(item: Option) => item[valueField] === parentOption[valueField]
);
selectedOptions.splice(index, 1);
}
return this.dealParentSelect(parentOption, selectedOptions);
} else {
return selectedOptions;
}
}
@autobind
flattenTreeWithLeafNodes(option: Option) {
return compact(
flattenTree(Array.isArray(option) ? option : [option], node => node)
);
}
@autobind
adjustOptionSelect(option: Option): boolean {
const {valueField = 'value'} = this.props;
const {selectedOptions} = this.state;
function loop(arr: any[]): boolean {
if (!arr.length) {
return false;
}
return arr.some((item: any) => item[valueField] === option[valueField]);
}
return loop(selectedOptions);
}
@autobind
getSelectedChildNum(option: Option): number {
let count = 0;
const loop = (arr: any[]) => {
if (!arr || !arr.length) {
return;
}
for (let item of arr) {
if (item.children) {
loop(item.children || []);
} else {
if (this.adjustOptionSelect(item)) {
count++;
}
}
}
};
loop(option.children || []);
return count;
}
@autobind
dealOptionDisable(selectedOptions: Options) {
const {
valueField = 'value',
options,
cascade,
multiple,
onlyChildren // 子节点可点击
} = this.props;
if (!multiple || cascade || onlyChildren) {
return;
}
const selectedValues = selectedOptions.map(
(option: Option) => option[valueField]
);
const loop = (option: Option) => {
if (!option.children) {
return;
}
option.children &&
option.children.forEach((childOption: Option) => {
if (
!selectedValues.includes(option[valueField]) &&
!option.disabled
) {
childOption.disabled = false;
}
if (selectedValues.includes(option[valueField]) || option.disabled) {
childOption.disabled = true;
}
loop(childOption);
});
};
options.forEach((option: Option) => loop(option));
}
@autobind
dealChildrenSelect(option: Option, selectedOptions: Options) {
const {valueField = 'value'} = this.props;
let index = selectedOptions.findIndex(
(item: Option) => item[valueField] === option[valueField]
);
if (index !== -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
function loop(option: Option) {
if (!option.children) {
return;
}
option.children.forEach((item: Option) => {
if (index !== -1) {
// 删除选中节点及其子节点
selectedOptions = selectedOptions.filter(
(sItem: Option) => sItem[valueField] !== item[valueField]
);
} else {
// 添加节点及其子节点
selectedOptions.push(item);
}
loop(item);
});
}
loop(option);
return selectedOptions;
}
getParentTree = (option: Option, arr: Options): Options => {
const parentOption = this.getOptionParent(option);
if (parentOption) {
arr.push(parentOption);
return this.getParentTree(parentOption, arr);
}
return arr;
};
@autobind
onSelect(option: CascaderOption, tabIndex: number) {
const {multiple, valueField = 'value', cascade} = this.props;
let tabs = this.state.tabs.slice();
let {activeTab} = this.state;
let selectedOptions = this.state.selectedOptions;
const isDisable = option.disabled;
if (!isDisable) {
if (multiple) {
// 父子级分离
if (cascade) {
if (
option.isCheckAll ||
!option.children ||
!option.children.length
) {
let index = selectedOptions.findIndex(
(item: Option) => item[valueField] === option[valueField]
);
if (index !== -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
}
} else {
if (
option.isCheckAll ||
!option.children ||
!option.children.length
) {
selectedOptions = this.dealChildrenSelect(option, selectedOptions);
selectedOptions = this.dealParentSelect(option, selectedOptions);
}
}
} else {
// 单选
selectedOptions = this.getParentTree(option, [option]);
}
}
this.dealOptionDisable(selectedOptions);
if (tabs.length > tabIndex + 1) {
tabs = tabs.slice(0, tabIndex + 1);
}
requestAnimationFrame(() => {
const tabWidth = this.tabRef.current?.offsetWidth || 1;
const parentTree = this.getParentTree(option, [option]);
const scrollLeft = (parentTree.length - 2) * tabWidth;
if (scrollLeft !== 0) {
(this.tabsRef.current as HTMLElement).scrollTo(scrollLeft, 0);
}
});
if (option?.children && !option.isCheckAll) {
const nextTab = multiple
? {
options: [
{
...option,
isCheckAll: true
},
...option.children
]
}
: {
options: option.children
};
if (tabs[tabIndex + 1]) {
tabs[tabIndex + 1] = nextTab;
} else {
tabs.push(nextTab);
}
activeTab += 1;
}
this.setState({
tabs,
activeTab,
selectedOptions
});
}
@autobind
onNextClick(option: CascaderOption, tabIndex: number) {
let {activeTab} = this.state;
let tabs = this.state.tabs.slice();
if (option.c)
if (option?.children) {
const nextTab = {
options: option.children
};
if (tabs[tabIndex + 1]) {
tabs[tabIndex + 1] = nextTab;
} else {
tabs.push(nextTab);
}
activeTab += 1;
}
this.setState({
tabs,
activeTab
});
}
@autobind
getSubmitOptions(selectedOptions: Options): Options {
const _selectedOptions: Options = [];
const {
multiple,
options,
valueField = 'value',
cascade,
onlyChildren,
withChildren
} = this.props;
if (cascade || onlyChildren || withChildren || !multiple) {
return selectedOptions;
}
const selectedValues = selectedOptions.map(
(option: Option) => option[valueField]
);
function loop(options: Options) {
if (!options || !options.length) {
return;
}
options.forEach((option: Option) => {
if (selectedValues.includes(option[valueField])) {
_selectedOptions.push(option);
} else {
loop(option.children ? option.children : []);
}
});
}
loop(options);
return _selectedOptions;
}
@autobind
confirm() {
const {onChange, joinValues, delimiter, extractValue, valueField, onClose} =
this.props;
let {selectedOptions} = this.state;
let _selectedOptions = this.getSubmitOptions(selectedOptions);
_selectedOptions = uniqBy(_selectedOptions, valueField);
onChange(
joinValues
? _selectedOptions
.map(item => item[valueField as string])
.join(delimiter)
: extractValue
? _selectedOptions.map(item => item[valueField as string])
: _selectedOptions
);
onClose && onClose();
}
@autobind
renderOption(option: CascaderOption, tabIndex: number) {
const {
activeColor,
optionRender,
labelField,
valueField = 'value',
classnames: cx,
cascade,
multiple
} = this.props;
const {selectedOptions} = this.state;
const selectedValueArr = selectedOptions.map(item => item[valueField]);
let selfChecked = selectedValueArr.includes(option[valueField]);
const color = option.color || (selfChecked ? activeColor : undefined);
const Text = optionRender ? (
optionRender({option, selected: selfChecked})
) : (
<span>{option[labelField]}</span>
);
return (
<li
className={cx(
'Cascader-option',
{
selected: selfChecked,
disabled: option.disabled
},
option.className
)}
style={{color}}
onClick={() => this.onSelect(option, tabIndex)}
key={tabIndex + '-' + option[valueField]}
>
<span className={cx('Cascader-option--text')}>{Text}</span>
</li>
);
}
@autobind
renderOptions(options: Options, tabIndex: number) {
const {classnames: cx} = this.props;
return (
<ul key={tabIndex} className={cx('Cascader-options')}>
{options.map(option => this.renderOption(option, tabIndex))}
</ul>
);
}
@autobind
renderTabs() {
const {classnames: cx, options} = this.props;
const {tabs} = this.state;
const depth = getTreeDepth(options);
return (
<div
className={cx(`Cascader-tabs`, depth > 3 ? 'scrollable' : '')}
ref={this.tabsRef}
>
{tabs.map((tab: CascaderTab, tabIndex: number) => {
const {options} = tab;
return (
<div
className={cx(`Cascader-tab`)}
ref={this.tabRef}
key={tabIndex}
>
{this.renderOptions(options, tabIndex)}
</div>
);
})}
{depth <= 3 && options.length
? Array(getTreeDepth(options) - tabs.length)
.fill(1)
.map((item: number, index: number) => (
<div className={cx(`Cascader-tab`)} key={index}></div>
))
: null}
</div>
);
}
render() {
const {
classPrefix: ns,
classnames: cx,
className,
onClose,
translate: __
} = this.props;
return (
<div className={cx(`Cascader`, className)}>
<div className={cx(`Cascader-btnGroup`)}>
<Button
className={cx(`Cascader-btnCancel`)}
level="default"
onClick={onClose}
>
{__('cancel')}
</Button>
<Button
className={cx(`Cascader-btnConfirm`)}
level="primary"
onClick={this.confirm}
>
{__('confirm')}
</Button>
</div>
{this.renderTabs()}
</div>
);
}
}
export default themeable(Cascader);

View File

@ -78,7 +78,13 @@ export class ChainedSelection extends BaseSelection<
onClick={() => this.toggleOption(option)}
>
<div className={cx('ChainedSelection-itemLabel')}>
{itemRender(option)}
{itemRender(option, {
index: index,
multiple: multiple,
checked: !!~valueArray.indexOf(option),
onChange: () => this.toggleOption(option),
disabled: disabled || option.disabled
})}
</div>
{multiple ? (
@ -100,7 +106,8 @@ export class ChainedSelection extends BaseSelection<
disabled,
classnames: cx,
itemClassName,
itemRender
itemRender,
multiple
} = this.props;
const valueArray = this.valueArray;
@ -118,7 +125,13 @@ export class ChainedSelection extends BaseSelection<
onClick={() => this.selectOption(option, depth, id)}
>
<div className={cx('ChainedSelection-itemLabel')}>
{itemRender(option)}
{itemRender(option, {
index: index,
multiple: multiple,
checked: !!~this.state.selected.indexOf(id),
onChange: () => this.selectOption(option, depth, id),
disabled: disabled || option.disabled
})}
</div>
{option.defer && option.loading ? <Spinner size="sm" show /> : null}

View File

@ -0,0 +1,99 @@
import React from 'react';
import 'codemirror/lib/codemirror.css';
import type CodeMirror from 'codemirror';
import {autobind} from '../utils/helper';
import {resizeSensor} from '../utils/resize-sensor';
export interface CodeMirrorEditorProps {
className?: string;
value?: string;
onChange?: (value: string) => void;
onFocus?: (e: any) => void;
onBlur?: (e: any) => void;
editorFactory?: (
dom: HTMLElement,
cm: typeof CodeMirror,
props?: any
) => CodeMirror.Editor;
editorDidMount?: (cm: typeof CodeMirror, editor: CodeMirror.Editor) => void;
editorWillUnMount?: (
cm: typeof CodeMirror,
editor: CodeMirror.Editor
) => void;
}
export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
dom = React.createRef<HTMLDivElement>();
editor?: CodeMirror.Editor;
toDispose: Array<() => void> = [];
unmounted = false;
async componentDidMount() {
const cm = (await import('codemirror')).default;
// @ts-ignore
await import('codemirror/mode/javascript/javascript');
// @ts-ignore
await import('codemirror/mode/htmlmixed/htmlmixed');
await import('codemirror/addon/mode/simple');
await import('codemirror/addon/mode/multiplex');
if (this.unmounted) {
return;
}
this.editor =
this.props.editorFactory?.(this.dom.current!, cm, this.props) ??
cm(this.dom.current!, {
value: this.props.value || ''
});
this.props.editorDidMount?.(cm, this.editor);
this.editor.on('change', this.handleChange);
this.toDispose.push(
resizeSensor(this.dom.current as HTMLElement, () =>
this.editor?.refresh()
)
);
// todo 以后优化这个,解决弹窗里面默认光标太小的问题
setTimeout(() => this.editor?.refresh(), 350);
this.toDispose.push(() => {
this.props.editorWillUnMount?.(cm, this.editor!);
});
}
componentDidUpdate(prevProps: CodeMirrorEditorProps) {
const props = this.props;
if (props.value !== prevProps.value) {
this.editor && this.setValue(props.value);
}
}
componentWillUnmount() {
this.unmounted = true;
this.editor?.off('change', this.handleChange);
this.toDispose.forEach(fn => fn());
this.toDispose = [];
}
@autobind
handleChange(editor: any) {
this.props.onChange?.(editor.getValue());
}
setValue(value?: string) {
const doc = this.editor!.getDoc();
if (value && value !== doc.getValue()) {
const cursor = doc.getCursor();
doc.setValue(value);
doc.setCursor(cursor);
}
}
render() {
const {className} = this.props;
return <div className={className} ref={this.dom}></div>;
}
}
export default CodeMirrorEditor;

View File

@ -26,13 +26,14 @@ const collapseStyles: {
export interface CollapseProps {
key?: string;
id?: string;
propKey?: string;
mountOnEnter?: boolean;
unmountOnExit?: boolean;
className?: string;
classPrefix: string;
classnames: ClassNamesFn;
headerPosition?: 'top' | 'bottom';
header?: React.ReactElement;
header?: React.ReactNode;
body: any;
bodyClassName?: string;
disabled?: boolean;
@ -55,7 +56,6 @@ export interface CollapseState {
}
export class Collapse extends React.Component<CollapseProps, CollapseState> {
static defaultProps: Partial<CollapseProps> = {
mountOnEnter: false,
unmountOnExit: false,
@ -79,10 +79,13 @@ export class Collapse extends React.Component<CollapseProps, CollapseState> {
super(props);
this.toggleCollapsed = this.toggleCollapsed.bind(this);
this.state.collapsed = !!props.collapsed;
this.state.collapsed = props.collapsable ? !!props.collapsed : false;
}
static getDerivedStateFromProps(nextProps: CollapseProps, preState: CollapseState) {
static getDerivedStateFromProps(
nextProps: CollapseProps,
preState: CollapseState
) {
if (nextProps.propsUpdate && nextProps.collapsed !== preState.collapsed) {
return {
collapsed: !!nextProps.collapsed
@ -164,7 +167,9 @@ export class Collapse extends React.Component<CollapseProps, CollapseState> {
children
} = this.props;
const finalHeader = this.state.collapsed ? header : collapseHeader || header;
const finalHeader = this.state.collapsed
? header
: collapseHeader || header;
let dom = [
finalHeader ? (
@ -173,14 +178,18 @@ export class Collapse extends React.Component<CollapseProps, CollapseState> {
onClick={this.toggleCollapsed}
className={cx(`Collapse-header`, headingClassName)}
>
{showArrow && collapsable
? expandIcon
? React.cloneElement(expandIcon, {
...expandIcon.props,
className: cx('Collapse-icon-tranform')
})
: <span className={cx('Collapse-arrow')} />
: ''}
{showArrow && collapsable ? (
expandIcon ? (
React.cloneElement(expandIcon, {
...expandIcon.props,
className: cx('Collapse-icon-tranform')
})
) : (
<span className={cx('Collapse-arrow')} />
)
) : (
''
)}
{finalHeader}
</HeadingComponent>
) : null,
@ -213,7 +222,6 @@ export class Collapse extends React.Component<CollapseProps, CollapseState> {
);
}}
</Transition>
];
if (headerPosition === 'bottom') {

View File

@ -28,7 +28,6 @@ class CollapseGroup extends React.Component<
CollapseGroupProps,
CollapseGroupState
> {
static defaultProps: Partial<CollapseGroupProps> = {
className: '',
accordion: false,
@ -58,21 +57,18 @@ class CollapseGroup extends React.Component<
if (collapsed) {
if (this.props.accordion) {
activeKey = [];
}
else {
for(let i = 0; i < activeKey.length; i++) {
} else {
for (let i = 0; i < activeKey.length; i++) {
if (activeKey[i] === item.id) {
activeKey.splice(i, 1);
break;
}
}
}
}
else {
} else {
if (this.props.accordion) {
activeKey = [item.id];
}
else {
} else {
activeKey.push(item.id);
}
}
@ -88,7 +84,8 @@ class CollapseGroup extends React.Component<
return children.map((child: React.ReactElement, index: number) => {
let props = child.props;
const id = props.schema.key || String(index);
const id = props.propKey || String(index);
const collapsed = this.state.activeKey.indexOf(id) === -1;
return React.cloneElement(child as any, {
@ -98,7 +95,8 @@ class CollapseGroup extends React.Component<
collapsed,
expandIcon: this.props.expandIcon,
propsUpdate: true,
onCollapse: (item: CollapseProps, collapsed: boolean) => this.collapseChange(item, collapsed)
onCollapse: (item: CollapseProps, collapsed: boolean) =>
this.collapseChange(item, collapsed)
});
});
};
@ -116,7 +114,7 @@ class CollapseGroup extends React.Component<
className={cx(
`CollapseGroup`,
{
'icon-position-right': expandIconPosition === 'right',
'icon-position-right': expandIconPosition === 'right'
},
className
)}

View File

@ -11,8 +11,9 @@ import {Icon} from './icons';
import Overlay from './Overlay';
import {uncontrollable} from 'uncontrollable';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {autobind, isObject} from '../utils/helper';
import {autobind, isMobile, isObject} from '../utils/helper';
import {localeable, LocaleProps} from '../locale';
export type PresetColor = {color: string; title: string} | string;
@ -32,6 +33,7 @@ export interface ColorProps extends LocaleProps, ThemeProps {
presetColors?: PresetColor[];
resetValue?: string;
allowCustomColor?: boolean;
useMobileUI?: boolean;
}
export interface ColorControlState {
@ -218,7 +220,8 @@ export class ColorControl extends React.PureComponent<
placement,
classnames: cx,
presetColors,
allowCustomColor
allowCustomColor,
useMobileUI
} = this.props;
const __ = this.props.translate;
@ -270,7 +273,7 @@ export class ColorControl extends React.PureComponent<
<Icon icon="caret" className="icon" onClick={this.handleClick} />
</span>
{isOpened ? (
{!(useMobileUI && isMobile()) && isOpened ? (
<Overlay
placement={placement || 'auto'}
target={() => findDOMNode(this)}
@ -317,6 +320,43 @@ export class ColorControl extends React.PureComponent<
</PopOver>
</Overlay>
) : null}
{useMobileUI && isMobile() && (
<PopUp
className={cx(`${ns}ColorPicker-popup`)}
isShow={isOpened}
onHide={this.handleClick}
>
{allowCustomColor ? (
<SketchPicker
styles={{}}
disableAlpha={!!~['rgb', 'hex'].indexOf(format as string)}
color={value}
presetColors={presetColors}
onChangeComplete={this.handleChange}
/>
) : (
<GithubPicker
color={value}
colors={
Array.isArray(presetColors)
? (presetColors
.filter(
item => typeof item === 'string' || isObject(item)
)
.map(item =>
typeof item === 'string'
? item
: isObject(item)
? item?.color
: item
) as string[])
: undefined
}
onChangeComplete={this.handleChange}
/>
)}
</PopUp>
)}
</div>
);
}

View File

@ -9,13 +9,14 @@ import moment from 'moment';
import 'moment/locale/zh-cn';
import {Icon} from './icons';
import PopOver from './PopOver';
import PopUp from './PopUp';
import Overlay from './Overlay';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {PlainObject} from '../types';
import Calendar from './calendar/Calendar';
import 'react-datetime/css/react-datetime.css';
import {localeable, LocaleProps, TranslateFn} from '../locale';
import {ucFirst} from '../utils/helper';
import {isMobile, ucFirst} from '../utils/helper';
const availableShortcuts: {[propName: string]: any} = {
now: {
@ -279,15 +280,17 @@ export interface DateProps extends LocaleProps, ThemeProps {
// 是否为内嵌模式,如果开启就不是 picker 了,直接页面点选。
embed?: boolean;
schedules?: Array<{
startTime: Date,
endTime: Date,
content: any,
className?: string
startTime: Date;
endTime: Date;
content: any;
className?: string;
}>;
scheduleClassNames?: Array<string>;
largeMode?: boolean;
onScheduleClick?: (scheduleData: any) => void;
useMobileUI?: boolean;
// 下面那个千万不要写,写了就会导致 keyof DateProps 得到的结果是 string | number;
// [propName: string]: any;
}
@ -312,7 +315,13 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
shortcuts: '',
closeOnSelect: true,
overlayPlacement: 'auto',
scheduleClassNames: ['bg-warning', 'bg-danger', 'bg-success', 'bg-info', 'bg-secondary']
scheduleClassNames: [
'bg-warning',
'bg-danger',
'bg-success',
'bg-info',
'bg-secondary'
]
};
state: DatePickerState = {
isOpened: false,
@ -557,6 +566,8 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
borderMode,
embed,
minDate,
useMobileUI,
maxDate,
schedules,
largeMode,
scheduleClassNames,
@ -612,6 +623,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
onClose={this.close}
locale={locale}
minDate={minDate}
maxDate={maxDate}
// utc={utc}
schedules={schedulesData}
largeMode={largeMode}
@ -659,7 +671,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
<Icon icon="clock" className="icon" />
</a>
{isOpened ? (
{!(useMobileUI && isMobile()) && isOpened ? (
<Overlay
target={this.getTarget}
container={popOverContainer || this.getParent}
@ -690,11 +702,38 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
onClose={this.close}
locale={locale}
minDate={minDate}
maxDate={maxDate}
// utc={utc}
/>
</PopOver>
</Overlay>
) : null}
{useMobileUI && isMobile() ? (
<PopUp
className={cx(`${ns}DatePicker-popup`)}
isShow={isOpened}
onHide={this.handleClick}
>
{this.renderShortCuts(shortcuts)}
<Calendar
value={date}
onChange={this.handleChange}
requiredConfirm={!!(dateFormat && timeFormat)}
dateFormat={dateFormat}
inputFormat={inputFormat}
timeFormat={timeFormat}
isValidDate={this.checkIsValidDate}
viewMode={viewMode}
timeConstraints={timeConstraints}
input={false}
onClose={this.close}
locale={locale}
minDate={minDate}
// utc={utc}
/>
</PopUp>
) : null}
</div>
);
}

View File

@ -13,10 +13,12 @@ import Overlay from './Overlay';
import {ShortCuts, ShortCutDateRange} from './DatePicker';
import Calendar from './calendar/Calendar';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {PlainObject} from '../types';
import {noop, ucFirst} from '../utils/helper';
import {isMobile, noop, ucFirst} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
import CalendarMobile from './CalendarMobile';
export interface DateRangePickerProps extends ThemeProps, LocaleProps {
className?: string;
@ -47,6 +49,7 @@ export interface DateRangePickerProps extends ThemeProps, LocaleProps {
embed?: boolean;
viewMode?: 'days' | 'months' | 'years' | 'time' | 'quarters';
borderMode?: 'full' | 'half' | 'none';
useMobileUI?: boolean;
}
export interface DateRangePickerState {
@ -285,6 +288,7 @@ export class DateRangePicker extends React.Component<
this.handlePopOverClick = this.handlePopOverClick.bind(this);
this.renderDay = this.renderDay.bind(this);
this.renderQuarter = this.renderQuarter.bind(this);
this.handleMobileChange = this.handleMobileChange.bind(this);
const {format, joinValues, delimiter, value} = this.props;
this.state = {
@ -497,6 +501,16 @@ export class DateRangePicker extends React.Component<
);
}
handleMobileChange(data: any, callback?: () => void) {
this.setState(
{
startDate: data.startDate,
endDate: data.endDate
},
callback
);
}
selectRannge(range: PlainObject) {
const {closeOnSelect, minDate, maxDate} = this.props;
const now = moment();
@ -760,10 +774,20 @@ export class DateRangePicker extends React.Component<
disabled,
embed,
overlayPlacement,
borderMode
borderMode,
useMobileUI,
timeFormat,
minDate,
maxDate,
minDuration,
maxDuration,
dateFormat,
viewMode = 'days',
ranges
} = this.props;
const useCalendarMobile = useMobileUI && isMobile() && ['days', 'months', 'quarters'].indexOf(viewMode) > -1;
const {isOpened, isFocused} = this.state;
const {isOpened, isFocused, startDate, endDate} = this.state;
const selectedDate = DateRangePicker.unFormatValue(
value,
@ -782,6 +806,25 @@ export class DateRangePicker extends React.Component<
endViewValue && arr.push(endViewValue);
const __ = this.props.translate;
const calendarMobile = <CalendarMobile
timeFormat={timeFormat}
inputFormat={inputFormat}
startDate={startDate}
endDate={endDate}
minDate={minDate}
maxDate={maxDate}
minDuration={minDuration}
maxDuration={maxDuration}
dateFormat={dateFormat}
embed={embed}
viewMode={viewMode}
close={this.close}
confirm={this.confirm}
onChange={this.handleMobileChange}
footerExtra={this.renderRanges(ranges)}
showViewMode={viewMode === 'quarters' || viewMode === 'months' ? 'years' : 'months'}
/>;
if (embed) {
return (
<div
@ -793,11 +836,15 @@ export class DateRangePicker extends React.Component<
className
)}
>
{this.renderCalendar()}
{useCalendarMobile
? calendarMobile
: this.renderCalendar()}
</div>
);
}
const CalendarMobileTitle = <div className={`${ns}CalendarMobile-title`}>{__('Calendar.datepicker')}</div>;
return (
<div
tabIndex={0}
@ -837,7 +884,19 @@ export class DateRangePicker extends React.Component<
</a>
{isOpened ? (
<Overlay
useMobileUI && isMobile() ? (
<PopUp
isShow={isOpened}
className={cx(`${ns}CalendarMobile-pop`)}
onHide={this.close}
header={CalendarMobileTitle}
>
{useCalendarMobile
? calendarMobile
: this.renderCalendar()}
</PopUp>
)
: <Overlay
target={() => this.dom.current}
onHide={this.close}
container={popOverContainer || (() => findDOMNode(this))}

View File

@ -27,7 +27,13 @@ export class GroupedSelection extends BaseSelection {
className={cx('GroupedSelection-group', option.className)}
>
<div className={cx('GroupedSelection-itemLabel')}>
{itemRender(option)}
{itemRender(option, {
index: index,
multiple: multiple,
checked: false,
onChange: () => undefined,
disabled: disabled || option.disabled
})}
</div>
<div className={cx('GroupedSelection-items', option.className)}>
@ -52,7 +58,13 @@ export class GroupedSelection extends BaseSelection {
onClick={() => this.toggleOption(option)}
>
<div className={cx('GroupedSelection-itemLabel')}>
{itemRender(option)}
{itemRender(option, {
index: index,
multiple: multiple,
checked: !!~valueArray.indexOf(option),
onChange: () => this.toggleOption(option),
disabled: disabled || option.disabled
})}
</div>
{multiple ? (

View File

@ -12,14 +12,16 @@ import {Icon} from './icons';
import Overlay from './Overlay';
import Calendar from './calendar/Calendar';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {themeable, ThemeProps} from '../theme';
import {PlainObject} from '../types';
import {noop} from '../utils/helper';
import {isMobile, noop} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
import {DateRangePicker} from './DateRangePicker';
import capitalize from 'lodash/capitalize';
import {ShortCuts, ShortCutDateRange} from './DatePicker';
import {availableRanges} from './DateRangePicker';
import CalendarMobile from './CalendarMobile';
export interface MonthRangePickerProps extends ThemeProps, LocaleProps {
className?: string;
@ -47,6 +49,7 @@ export interface MonthRangePickerProps extends ThemeProps, LocaleProps {
resetValue?: any;
popOverContainer?: any;
embed?: boolean;
useMobileUI?: boolean;
}
export interface MonthRangePickerState {
@ -97,6 +100,7 @@ export class MonthRangePicker extends React.Component<
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handlePopOverClick = this.handlePopOverClick.bind(this);
this.renderMonth = this.renderMonth.bind(this);
this.handleMobileChange = this.handleMobileChange.bind(this);
const {format, joinValues, delimiter, value} = this.props;
this.state = {
@ -278,6 +282,16 @@ export class MonthRangePicker extends React.Component<
);
}
handleMobileChange(data: any, callback?: () => void) {
this.setState(
{
startDate: data.startDate,
endDate: data.endDate
},
callback
);
}
selectRannge(range: PlainObject) {
const {closeOnSelect, minDate, maxDate} = this.props;
this.setState(
@ -528,10 +542,18 @@ export class MonthRangePicker extends React.Component<
clearable,
disabled,
embed,
overlayPlacement
overlayPlacement,
useMobileUI,
timeFormat,
minDate,
maxDate,
minDuration,
maxDuration,
ranges
} = this.props;
const mobileUI = isMobile() && useMobileUI;
const {isOpened, isFocused} = this.state;
const {isOpened, isFocused, startDate, endDate} = this.state;
const selectedDate = DateRangePicker.unFormatValue(
value,
@ -550,6 +572,24 @@ export class MonthRangePicker extends React.Component<
endViewValue && arr.push(endViewValue);
const __ = this.props.translate;
const calendarMobile = <CalendarMobile
timeFormat={timeFormat}
inputFormat={inputFormat}
startDate={startDate}
endDate={endDate}
minDate={minDate}
maxDate={maxDate}
minDuration={minDuration}
maxDuration={maxDuration}
embed={embed}
viewMode="months"
close={this.close}
confirm={this.confirm}
onChange={this.handleMobileChange}
footerExtra={this.renderRanges(ranges)}
showViewMode="years"
/>;
if (embed) {
return (
<div
@ -561,11 +601,15 @@ export class MonthRangePicker extends React.Component<
className
)}
>
{this.renderCalendar()}
{mobileUI
? calendarMobile
: this.renderCalendar()}
</div>
);
}
const CalendarMobileTitle = <div className={`${ns}CalendarMobile-title`}>{__('Calendar.datepicker')}</div>;
return (
<div
tabIndex={0}
@ -604,7 +648,17 @@ export class MonthRangePicker extends React.Component<
</a>
{isOpened ? (
<Overlay
mobileUI ? (
<PopUp
isShow={isOpened}
className={cx(`${ns}CalendarMobile-pop`)}
onHide={this.close}
header={CalendarMobileTitle}
>
{calendarMobile}
</PopUp>
)
: <Overlay
target={() => this.dom.current}
onHide={this.close}
container={popOverContainer || (() => findDOMNode(this))}

View File

@ -165,7 +165,7 @@ class Position extends React.Component<any, any> {
interface OverlayProps {
placement?: string;
show?: boolean;
transition?: React.ReactType;
transition?: React.ElementType;
containerPadding?: number;
shouldUpdatePosition?: boolean;
rootClose?: boolean;

150
src/components/Picker.tsx Normal file
View File

@ -0,0 +1,150 @@
/**
* @file Picker
* @description
*/
import React, {memo, ReactNode, useState, useEffect} from 'react';
import {uncontrollable} from 'uncontrollable';
import {themeable, ThemeProps} from '../theme';
import {localeable, LocaleProps} from '../locale';
import Button from './Button';
import {PickerColumnItem, default as Column} from './PickerColumn';
export type PickerValue = string | number;
export interface PickerProps extends ThemeProps, LocaleProps {
title?: String | ReactNode;
labelField?: string;
className?: string;
showToolbar?: boolean;
defaultValue?: PickerValue[];
value?: PickerValue[];
swipeDuration?: number;
visibleItemCount?: number;
itemHeight?: number;
columns: PickerColumnItem[] | PickerColumnItem;
onChange?: (value?: PickerValue[], index?: number, confirm?: boolean) => void;
onClose?: (value?: PickerValue[]) => void;
onConfirm?: (value?: PickerValue[]) => void;
}
function fixToArray(data: any) {
if (!Array.isArray(data)) {
return [data];
}
return data;
}
const Picker = memo<PickerProps>(props => {
const {
labelField,
visibleItemCount = 5,
value = [],
swipeDuration = 1000,
columns = [],
itemHeight = 30,
showToolbar = true,
className = '',
classnames: cx,
classPrefix: ns,
translate: __
} = props;
const _columns = fixToArray(columns);
const [innerValue, setInnerValue] = useState<PickerValue[]>(
fixToArray(props.value === undefined ? props.defaultValue || [] : value)
);
useEffect(() => {
setInnerValue(value);
}, [value]);
const close = () => {
if (props.onClose) {
props.onClose(innerValue);
}
};
const confirm = () => {
if (props.onConfirm) {
props.onConfirm(innerValue);
}
};
const onChange = (
itemValue: PickerValue,
columnIndex: number,
confirm?: boolean
) => {
const nextInnerValue = [...innerValue];
nextInnerValue[columnIndex] = itemValue;
setInnerValue(nextInnerValue);
if (props.onChange) {
props.onChange(nextInnerValue, columnIndex, confirm);
}
};
const renderColumnItem = (item: PickerColumnItem, index: number) => {
return (
<Column
{...item}
classnames={cx}
classPrefix={ns}
labelField={labelField}
itemHeight={itemHeight}
swipeDuration={swipeDuration}
visibleItemCount={visibleItemCount}
value={innerValue[index]}
onChange={(val: string | number, i, confirm) => {
onChange(val, index, confirm);
}}
key={`column${index}`}
/>
);
};
const wrapHeight = itemHeight * +visibleItemCount;
const frameStyle = {height: `${itemHeight}px`};
const columnsStyle = {height: `${wrapHeight}px`};
const maskStyle = {
backgroundSize: `100% ${(wrapHeight - itemHeight) / 2}px`
};
return (
<div className={cx(className, 'PickerColumns', 'PickerColumns-popOver')}>
{showToolbar && (
<div className={cx('PickerColumns-toolbar')}>
<Button
className="PickerColumns-cancel"
level="default"
onClick={close}
>
{__('cancel')}
</Button>
<Button
className="PickerColumns-confirm"
level="primary"
onClick={confirm}
>
{__('confirm')}
</Button>
</div>
)}
<div className={cx('PickerColumns-columns')} style={columnsStyle}>
{_columns.map((column: PickerColumnItem, index: number) =>
renderColumnItem(column, index)
)}
<div className={cx('PickerColumns-mask')} style={maskStyle}></div>
<div className={cx('PickerColumns-frame')} style={frameStyle}></div>
</div>
</div>
);
});
export default themeable(
localeable(
uncontrollable(Picker, {
value: 'onChange'
})
)
);

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