Merge branch 'baidu:master' into master

This commit is contained in:
zhou999 2022-01-19 19:01:29 +08:00 committed by GitHub
commit e3f8c3762f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
159 changed files with 9518 additions and 1102 deletions

View File

@ -4,6 +4,7 @@
</p>
[文档(国内)](https://baidu.gitee.io/amis/) |
[文档(备用)](https://aisuda.bce.baidu.com/amis/) |
[文档(国外)](https://baidu.github.io/amis/) |
[可视化编辑器](https://aisuda.github.io/amis-editor-demo/) |
[amis-admin](https://github.com/aisuda/amis-admin) |

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ import React = require('react');
import {render, cleanup, fireEvent} from '@testing-library/react';
import '../../../src/themes/default';
import {render as amisRender} from '../../../src/index';
import {makeEnv} from '../../helper';
import {wait, makeEnv} from '../../helper';
import {clearStoresCache} from '../../../src/factory';
afterEach(() => {
@ -57,5 +57,6 @@ test('Renderer:button', async () => {
);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/OpenDialog/));
await wait(100);
expect(container).toMatchSnapshot();
});

View File

@ -2,7 +2,7 @@ import React = require('react');
import {render, cleanup, fireEvent} from '@testing-library/react';
import '../../../src/themes/default';
import {render as amisRender} from '../../../src/index';
import {makeEnv} from '../../helper';
import {makeEnv, wait} from '../../helper';
import {clearStoresCache} from '../../../src/factory';
afterEach(() => {
@ -52,5 +52,6 @@ test('Renderer:button-toolbar', async () => {
);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/OpenDialog/));
await wait(100);
expect(container).toMatchSnapshot();
});

View File

@ -0,0 +1,285 @@
import React = require('react');
import {render, cleanup, fireEvent} from '@testing-library/react';
import '../../../src/themes/default';
import {render as amisRender} from '../../../src/index';
import {makeEnv, wait} from '../../helper';
import {clearStoresCache} from '../../../src/factory';
afterEach(() => {
cleanup();
clearStoresCache();
});
const setup = (inputOptions: any = {}, formOptions: any = {}) => {
const utils = render(
amisRender(
{
type: 'form',
api: '/api/mock2/form/saveForm',
body: [
{
name: 'text',
label: 'text',
type: 'input-text',
changeImmediately: true,
...inputOptions
}
],
...formOptions
},
{},
makeEnv()
)
);
const input = utils.container.querySelector(
'input[name="text"]'
) as HTMLInputElement;
const submitBtn = utils.container.querySelector('button[type="submit"]');
return {
input,
submitBtn,
...utils
};
};
/**
* 使
*/
test('Renderer:text', () => {
const {container, input} = setup();
expect(container).toMatchSnapshot();
// 输入是否正常
fireEvent.change(input, {target: {value: 'AbCd'}});
expect(input.value).toBe('AbCd');
});
/**
* type url
*/
test('Renderer:text type is url', async () => {
const {container, input, submitBtn} = setup({
type: 'input-url'
});
fireEvent.change(input, {target: {value: 'abcd'}});
fireEvent.click(submitBtn);
await wait(200); // 表单校验是异步的,所以必须要等一段时间 @todo 感觉可能需要寻找更靠谱的办法
expect(container).toMatchSnapshot('validate fail');
fireEvent.change(input, {target: {value: 'https://www.baidu.com'}});
await wait(200);
expect(container).toMatchSnapshot('validate success');
});
/**
* type email
*/
test('Renderer:text type is email', async () => {
const {container, input, submitBtn} = setup({
type: 'input-email'
});
fireEvent.change(input, {target: {value: 'abcd'}});
fireEvent.click(submitBtn);
await wait(200);
expect(container).toMatchSnapshot('validate fail');
fireEvent.change(input, {target: {value: 'test@baidu.com'}});
await wait(200);
expect(container).toMatchSnapshot('validate success');
});
/**
* type password
*/
test('Renderer:text type is password', () => {
const {container, input} = setup({
type: 'input-password'
});
fireEvent.change(input, {target: {value: 'abcd'}});
expect(container).toMatchSnapshot();
});
/**
* addOn
*/
test('Renderer:text with addOn', () => {
const {container, input} = setup({
addOn: {
type: 'button',
label: '搜索'
}
});
expect(container).toMatchSnapshot();
});
/**
* clearable
*/
test('Renderer:text with clearable', async () => {
const {container, input} = setup({
clearable: true
});
fireEvent.change(input, {target: {value: 'abcd'}}); // 有值之后才会显示clear的icon
expect(container).toMatchSnapshot();
fireEvent.click(container.querySelector('a.cxd-TextControl-clear'));
await wait(100);
expect(input.value).toBe('');
});
/**
*
*/
test('Renderer:text with options', async () => {
const {container, input} = setup(
{
options: [
{
label: 'Option A',
value: 'a'
},
{
label: 'Option B',
value: 'b'
}
]
},
{debug: true}
);
expect(container).toMatchSnapshot();
// 展开 options
fireEvent.click(container.querySelector('.cxd-TextControl-input'));
expect(container).toMatchSnapshot('options is open');
// 选中一项
fireEvent.click(
container.querySelector('.cxd-TextControl-sugs .cxd-TextControl-sugItem')
);
// expect(input.value).toBe('a');
expect(container).toMatchSnapshot('select first option');
});
/**
* ,
*/
test('Renderer:text with options and multiple', async () => {
const {container, input} = setup(
{
multiple: true,
options: [
{
label: 'OptionA',
value: 'a'
},
{
label: 'OptionB',
value: 'b'
},
{
label: 'OptionC',
value: 'c'
},
{
label: 'OptionD',
value: 'd'
}
]
},
{debug: true}
);
const textControl = container.querySelector('.cxd-TextControl-input');
// 展开 options
fireEvent.click(textControl);
expect(container).toMatchSnapshot('options is opened');
// 选中第一项
fireEvent.click(
container.querySelector('.cxd-TextControl-sugs .cxd-TextControl-sugItem')
);
// expect(input.value).toBe('a');
expect(container).toMatchSnapshot('first option selected');
// 再次打开 options
fireEvent.click(textControl);
expect(container).toMatchSnapshot(
'options is opened again, and first option already selected'
);
// 选中 options 中的第一项
fireEvent.click(
container.querySelector('.cxd-TextControl-sugs .cxd-TextControl-sugItem')
);
// expect(input.value).toBe('a,b');
expect(container).toMatchSnapshot('second option selected');
});
/**
*
*/
test('Renderer:text with prefix and suffix', () => {
const {container} = setup({
prefix: '¥',
suffix: 'RMB'
});
expect(container).toMatchSnapshot();
});
/**
*
*/
test('Renderer:text with counter', () => {
const {container, input} = setup({
showCounter: true
});
expect(container).toMatchSnapshot();
fireEvent.change(input, {target: {value: 'abcd'}});
expect(container).toMatchSnapshot();
});
/**
*
*/
test('Renderer:text with counter and maxLength', () => {
const {container, input} = setup({
showCounter: true,
maxLength: 10
});
expect(container).toMatchSnapshot();
fireEvent.change(input, {target: {value: 'abcd'}});
expect(container).toMatchSnapshot();
});
/**
*
*/
test('Renderer:text with transform lowerCase', () => {
const {input} = setup({transform: {lowerCase: true}});
fireEvent.change(input, {target: {value: 'AbCd'}});
expect(input.value).toBe('abcd');
});
/**
*
*/
test('Renderer:text with transform upperCase', () => {
const {input} = setup({transform: {upperCase: true}});
fireEvent.change(input, {target: {value: 'AbCd'}});
expect(input.value).toBe('ABCD');
});

View File

@ -21,3 +21,43 @@ test('Renderer:iframe', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer:iframe-var', async () => {
const {container} = render(
amisRender(
{
type: 'page',
data: {url: 'https://www.baidu.com'},
body: {
type: 'iframe',
className: 'b-a',
src: '$url',
height: 500,
width: 500
}
},
{},
makeEnv({})
)
);
expect(container).toMatchSnapshot();
});
test('Renderer:iframe-escape', async () => {
const {container} = render(
amisRender(
{
type: 'iframe',
className: 'b-a',
src: 'https://www.baidu.com/?s=%25f',
height: 500,
width: 500
},
{},
makeEnv({})
)
);
expect(container).toMatchSnapshot();
});

View File

@ -640,7 +640,7 @@ test('Renderer:Page initFetchOn trigger initApi fetch when condition becomes tur
expect(component.toJSON()).toMatchSnapshot();
});
test('Renderer:Page handleAction actionType=url|link', () => {
test('Renderer:Page handleAction actionType=url|link', async () => {
const jumpTo = jest.fn();
const {getByText} = render(
amisRender(
@ -667,6 +667,7 @@ test('Renderer:Page handleAction actionType=url|link', () => {
);
fireEvent.click(getByText(/JumpTo/));
await wait(100);
expect(jumpTo).toHaveBeenCalled();
expect(jumpTo.mock.calls[0][0]).toEqual('/goToPath?a=1');
});
@ -695,6 +696,7 @@ test('Renderer:Page handleAction actionType=dialog', async () => {
);
fireEvent.click(getByText(/OpenDialog/));
await wait(100);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/取消/));
@ -744,6 +746,7 @@ test('Renderer:Page handleAction actionType=dialog mergeData', async () => {
);
fireEvent.click(getByText(/OpenDialog/));
await wait(100);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/确认/));
@ -775,6 +778,7 @@ test('Renderer:Page handleAction actionType=drawer', async () => {
);
fireEvent.click(getByText(/OpenDrawer/));
await wait(100);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/取消/));
@ -871,7 +875,7 @@ test('Renderer:Page handleAction actionType=ajax', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer:Page handleAction actionType=copy', () => {
test('Renderer:Page handleAction actionType=copy', async () => {
const copy = jest.fn();
const {getByText} = render(
amisRender(
@ -898,6 +902,7 @@ test('Renderer:Page handleAction actionType=copy', () => {
);
fireEvent.click(getByText(/CopyContent/));
await wait(100);
expect(copy).toHaveBeenCalled();
expect(copy.mock.calls[0][0]).toEqual('the content is 1');
});
@ -944,7 +949,7 @@ test('Renderer:Page handleAction actionType=ajax & feedback', async () => {
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/确认/));
await wait(500);
await wait(600);
expect(container).toMatchSnapshot();
});
@ -1058,7 +1063,7 @@ test('Renderer:Page initApi reload by action', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer:Page Tpl JumpTo', () => {
test('Renderer:Page Tpl JumpTo', async () => {
const jumpTo = jest.fn();
const {getByText} = render(
amisRender(
@ -1078,6 +1083,7 @@ test('Renderer:Page Tpl JumpTo', () => {
);
fireEvent.click(getByText(/JumpTo/));
await wait(100);
expect(jumpTo).toHaveBeenCalled();
expect(jumpTo.mock.calls[0][0]).toEqual('/goToPath?a=1');
});

View File

@ -601,6 +601,7 @@ test('Renderer:Wizard actionPrevLabel actionNextLabel actionFinishLabel classNam
expect(fetcher).toHaveBeenCalled();
fireEvent.click(getByText(/PrevStep/));
await wait(100);
expect(container).toMatchSnapshot();
});
@ -1377,6 +1378,7 @@ test('Renderer:Wizard dialog', async () => {
);
fireEvent.click(getByText(/OpenDialog/));
await wait(100);
expect(container).toMatchSnapshot();
fireEvent.click(getByText(/取消/));

View File

@ -18,10 +18,10 @@ exports[`Renderer:dropdown-button 1`] = `
</span>
</button>
<ul
class="cxd-DropDown-menu"
class="cxd-DropDown-menu-root cxd-DropDown-menu"
>
<li
class=""
class="cxd-DropDown-button"
>
<a
class=""
@ -32,7 +32,7 @@ exports[`Renderer:dropdown-button 1`] = `
</a>
</li>
<li
class=""
class="cxd-DropDown-button"
>
<a
class=""

View File

@ -10,3 +10,41 @@ exports[`Renderer:iframe 1`] = `
/>
</div>
`;
exports[`Renderer:iframe-escape 1`] = `
<div>
<iframe
class="b-a"
frameborder="0"
src="https://www.baidu.com/?s=%25f"
style="width: 500px; height: 500px;"
/>
</div>
`;
exports[`Renderer:iframe-var 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
>
<iframe
class="b-a"
frameborder="0"
src="https://www.baidu.com"
style="width: 500px; height: 500px;"
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,124 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renderer:timeline 1`] = `
<div>
<div
class="cxd-Timeline cxd-Timeline-vertical cxd-Timeline-right"
>
<div
class="cxd-TimelineItem"
>
<div
class="cxd-TimelineItem-axle"
>
<div
class="cxd-TimelineItem-line"
/>
<div
class="cxd-TimelineItem-round"
style="background-color: rgb(255, 178, 0);"
/>
</div>
<div
class="cxd-TimelineItem-content"
>
<div
class="cxd-TimelineItem-time"
>
2019-02-07
</div>
<div
class="cxd-TimelineItem-title"
>
节点数据
</div>
</div>
</div>
<div
class="cxd-TimelineItem"
>
<div
class="cxd-TimelineItem-axle"
>
<div
class="cxd-TimelineItem-line"
/>
<div
class="cxd-TimelineItem-round"
style="background-color: rgb(79, 134, 244);"
/>
</div>
<div
class="cxd-TimelineItem-content"
>
<div
class="cxd-TimelineItem-time"
>
2019-02-08
</div>
<div
class="cxd-TimelineItem-title"
>
节点数据
</div>
</div>
</div>
<div
class="cxd-TimelineItem"
>
<div
class="cxd-TimelineItem-axle"
>
<div
class="cxd-TimelineItem-line"
/>
<div
class="cxd-TimelineItem-round cxd-TimelineItem-round--success"
/>
</div>
<div
class="cxd-TimelineItem-content"
>
<div
class="cxd-TimelineItem-time"
>
2019-02-09
</div>
<div
class="cxd-TimelineItem-title"
>
节点数据
</div>
</div>
</div>
<div
class="cxd-TimelineItem"
>
<div
class="cxd-TimelineItem-axle"
>
<div
class="cxd-TimelineItem-line"
/>
<div
class="cxd-TimelineItem-round cxd-TimelineItem-round--warning"
/>
</div>
<div
class="cxd-TimelineItem-content"
>
<div
class="cxd-TimelineItem-time"
>
2019-02-09
</div>
<div
class="cxd-TimelineItem-title"
>
节点数据
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -348,6 +348,12 @@ Content-Type: application/pdf
Content-Disposition: attachment; filename="download.pdf"
```
如果接口存在跨域,除了常见的 cors header 外,还需要添加以下 header
```
Access-Control-Expose-Headers: Content-Disposition
```
## 倒计时
主要用于发验证码的场景,通过设置倒计时 `countDown`(单位是秒),让点击按钮后禁用一段时间:

View File

@ -62,7 +62,7 @@ order: 99
- `label` 菜单名称。
- `icon` 菜单图标比如fa fa-file.
- `url` 页面路由路径,当路由命中该路径时,启用当前页面。当路径不是 `/` 打头时,会连接父级路径。比如:父级的路径为 `folder`,而此时配置 `pageA`, 那么当页面地址为 `/folder/pageA` 时才会命中此页面。当路径是 `/` 开头如: `/crud/list` 时,则不会拼接父级路径。另外还支持 `/crud/view/:id` 这类带参数的路由,页面中可以通过 `${params.id}` 取到此值。
- `schema` 页面的配置,具体配置请前往 [Page 页面说明](./page.md)
- `schema` 页面的配置,具体配置请前往 [Page 页面说明](./page)
- `schemaApi` 如果想通过接口拉取,请配置。返回路径为 `json>data`。schema 和 schemaApi 只能二选一。
- `link` 如果想配置个外部链接菜单,只需要配置 link 即可。
- `redirect` 跳转,当命中当前页面时,跳转到目标页面。

View File

@ -0,0 +1,193 @@
---
title: Calendar 日历
description:
type: 0
group: ⚙ 组件
menuName: Calendar 日历
icon:
order: 36
---
## 基本用法
```schema: scope="body"
{
"type": "calendar",
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
}
```
## 自定义颜色
```schema: scope="body"
{
"type": "calendar",
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1",
"className": "bg-success"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2",
"className": "bg-info"
}
]
}
```
```schema: scope="body"
{
"type": "calendar",
"scheduleClassNames": ["bg-success", "bg-info"],
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
}
```
## 自定义日程展示
```schema: scope="body"
{
"type": "calendar",
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
],
"scheduleAction": {
"actionType": "drawer",
"drawer": {
"title": "日程",
"body": {
"type": "table",
"columns": [
{
"name": "time",
"label": "时间"
},
{
"name": "content",
"label": "内容"
}
],
"data": "${scheduleData}"
}
}
}
}
```
## 支持从数据源中获取日程
```schema
{
"type": "page",
"data": {
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
},
"body": [
{
"type": "calendar",
"schedules": "${schedules}"
}
]
}
```
## 放大模式
```schema: scope="body"
{
"type": "calendar",
"largeMode": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-12 02:14:00",
"endTime": "2021-12-13 05:14:00",
"content": "这是一个日程2"
},
{
"startTime": "2021-12-20 05:14:00",
"endTime": "2021-12-21 05:14:00",
"content": "这是一个日程3"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程4"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-23 05:14:00",
"content": "这是一个日程5"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程6"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程7"
}
]
}
```
## Calendar 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| - | - | - | - |
| type | `string` | `"calendar"` | 指定为 calendar 渲染器 |
| schedules | `Array<{startTime: string, endTime: string, content: any, className?: string}> \| string` | | 日历中展示日程可设置静态数据或从上下文中取数据startTime和endTime格式参考[文档](https://momentjs.com/docs/#/parsing/string/)className参考[背景色](https://baidu.gitee.io/amis/zh-CN/style/background/background-color) |
| scheduleClassNames | `Array<string>` | `['bg-warning', 'bg-danger', 'bg-success', 'bg-info', 'bg-secondary']` | 日历中展示日程的颜色,参考[背景色](https://baidu.gitee.io/amis/zh-CN/style/background/background-color) |
| scheduleAction | `SchemaNode` | | 自定义日程展示 |
| largeMode | `boolean` | `false` | 放大模式 |

View File

@ -96,6 +96,8 @@ CRUD即增删改查组件主要用来展现数据列表并支持各类
如果不需要分页,或者配置了 `loadDataOnce` 则可以忽略掉 `total``hasNext` 参数。
> 如果 api 地址中有变量,比如 `/api/mock2/sample/${id}`amis 就不会自动加上分页参数,需要自己加上,改成 `/api/mock2/sample/${id}?page=${page}&perPage=${perPage}`
## 功能
既然这个渲染器叫增删改查,那接下来分开介绍这几个功能吧。
@ -2538,12 +2540,12 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据,
| stopAutoRefreshWhenModalIsOpen | `boolean` | `false` | 当有弹框时关闭自动刷新,关闭弹框又恢复 |
| syncLocation | `boolean` | `true` | 是否将过滤条件的参数同步到地址栏 |
| draggable | `boolean` | `false` | 是否可通过拖拽排序 |
| resizable | `boolean` | `true` | 是否可以调整列宽度 |
| itemDraggableOn | `boolean` | | 用[表达式](../../docs/concepts/expression)来配置是否可拖拽排序 |
| [saveOrderApi](#saveOrderApi) | [API](../../docs/types/api) | | 保存排序的 api。 |
| [quickSaveApi](#quickSaveApi) | [API](../../docs/types/api) | | 快速编辑后用来批量保存的 API。 |
| [quickSaveItemApi](#quickSaveItemApi) | [API](../../docs/types/api) | | 快速编辑配置成及时保存时使用的 API。 |
| bulkActions | Array<[Action](./action)> | | 批量操作列表,配置后,表格可进行选中操作。 |
| defaultChecked | `boolean` | `false` | 当可批量操作时,默认是否全部勾选。 |
| messages | `Object` | | 覆盖消息提示,如果不指定,将采用 api 返回的 message |
| messages.fetchFailed | `string` | | 获取失败时提示 |
| messages.saveOrderFailed | `string` | | 保存顺序失败提示 |

View File

@ -35,6 +35,68 @@ order: 44
}
```
## 分组展示模式
配置`children`可以实现分组展示,分组标题支持配置`icon`。
> 1.5.7 及以上版本
```schema: scope="body"
{
"type": "dropdown-button",
"label": "下拉菜单",
"buttons": [
{
"label": "RD",
"icon": "fa fa-user",
"children": [
{
"type": "button",
"label": "前端FE"
},
{
"type": "button",
"label": "后端RD"
}
]
},
{
"label": "QA",
"icon": "fa fa-user",
"children": [
{
"type": "button",
"label": "测试QA",
},
{
"type": "button",
"label": "交付测试DQA",
"disabled": true
},
{
"type": "divider"
}
]
},
{
"label": "Manager",
"icon": "fa fa-user",
"children": [
{
"type": "button",
"label": "项目经理PM"
},
{
"type": "button",
"label": "项目管理中心PMO",
"visible": false
}
]
}
]
}
```
## 关闭下拉菜单
配置`"closeOnClick": true`可以实现点击按钮后自动关闭下拉菜单。
@ -197,18 +259,20 @@ order: 44
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------------- | ------------------ | ----------------- | ----------------------------------------- |
| type | `string` | `dropdown-button` | 类型 |
| label | `string` | | 按钮文本 |
| className | `string` | | 外层 CSS 类名 |
| block | `boolean` | | 块状样式 |
| size | `string` | | 尺寸,支持`'xs'`、`'sm'`、`'md'` 、`'lg'` |
| align | `string` | | 位置,可选`'left'`或`'right'` |
| buttons | `Array<action>` | | 配置下拉按钮 |
| iconOnly | `boolean` | | 只显示 icon |
| defaultIsOpened | `boolean` | | 默认是否打开 |
| closeOnOutside | `boolean` | `true` | 点击外侧区域是否收起 |
| closeOnClick | `boolean` | `false` | 点击按钮后自动关闭下拉菜单 |
| trigger | `click``hover` | `click` | 触发方式 |
| hideCaret | `boolean` | false | 隐藏下拉图标 |
| 属性名 | 类型 | 默认值 | 说明 |
| --------------- | ----------------------- | ----------------- | ----------------------------------------- |
| type | `string` | `dropdown-button` | 类型 |
| label | `string` | | 按钮文本 |
| className | `string` | | 外层 CSS 类名 |
| btnClassName | `string` | | 按钮 CSS 类名 |
| menuClassName | `string` | | 下拉菜单 CSS 类名 |
| block | `boolean` | | 块状样式 |
| size | `string` | | 尺寸,支持`'xs'`、`'sm'`、`'md'` 、`'lg'` |
| align | `string` | | 位置,可选`'left'`或`'right'` |
| buttons | `Array<DropdownButton>` | | 配置下拉按钮 |
| iconOnly | `boolean` | | 只显示 icon |
| defaultIsOpened | `boolean` | | 默认是否打开 |
| closeOnOutside | `boolean` | `true` | 点击外侧区域是否收起 |
| closeOnClick | `boolean` | `false` | 点击按钮后自动关闭下拉菜单 |
| trigger | `click``hover` | `click` | 触发方式 |
| hideCaret | `boolean` | false | 隐藏下拉图标 |

View File

@ -403,10 +403,192 @@ type Value = ValueGroup;
}
```
## 简易模式
通过 builderMode 配置为简易模式,在这个模式下将不开启树形分组功能,输出结果只有一层,方便后端实现简单的 SQL 生成。
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "condition-builder",
"label": "条件组件",
"builderMode": "simple",
"name": "conditions",
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
"fields": [
{
"label": "文本",
"type": "text",
"name": "text"
},
{
"label": "数字",
"type": "number",
"name": "number"
},
{
"label": "布尔",
"type": "boolean",
"name": "boolean"
},
{
"label": "选项",
"type": "select",
"name": "select",
"options": [
{
"label": "A",
"value": "a"
},
{
"label": "B",
"value": "b"
},
{
"label": "C",
"value": "c"
},
{
"label": "D",
"value": "d"
},
{
"label": "E",
"value": "e"
}
]
},
{
"label": "动态选项",
"type": "select",
"name": "select2",
"source": "/api/mock2/form/getOptions?waitSeconds=1"
},
{
"label": "日期",
"children": [
{
"label": "日期",
"type": "date",
"name": "date"
},
{
"label": "时间",
"type": "time",
"name": "time"
},
{
"label": "日期时间",
"type": "datetime",
"name": "datetime"
}
]
}
]
}
]
}
```
在这个模式下还可以通过 `showANDOR` 来显示顶部的条件类型切换
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "condition-builder",
"label": "条件组件",
"builderMode": "simple",
"showANDOR": true,
"name": "conditions",
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
"fields": [
{
"label": "文本",
"type": "text",
"name": "text"
},
{
"label": "数字",
"type": "number",
"name": "number"
},
{
"label": "布尔",
"type": "boolean",
"name": "boolean"
},
{
"label": "选项",
"type": "select",
"name": "select",
"options": [
{
"label": "A",
"value": "a"
},
{
"label": "B",
"value": "b"
},
{
"label": "C",
"value": "c"
},
{
"label": "D",
"value": "d"
},
{
"label": "E",
"value": "e"
}
]
},
{
"label": "动态选项",
"type": "select",
"name": "select2",
"source": "/api/mock2/form/getOptions?waitSeconds=1"
},
{
"label": "日期",
"children": [
{
"label": "日期",
"type": "date",
"name": "date"
},
{
"label": "时间",
"type": "time",
"name": "time"
},
{
"label": "日期时间",
"type": "datetime",
"name": "datetime"
}
]
}
]
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| -------------- | -------- | ------ | ------------------ |
| className | `string` | | 外层 dom 类名 |
| fieldClassName | `string` | | 输入字段的类名 |
| source | `string` | | 通过远程拉取配置项 |
| 属性名 | 类型 | 默认值 | 说明 |
| -------------- | --------- | ------ | ------------------------------ |
| className | `string` | | 外层 dom 类名 |
| fieldClassName | `string` | | 输入字段的类名 |
| source | `string` | | 通过远程拉取配置项 |
| fields | | | 字段配置 |
| showANDOR | `boolean` | | 用于 simple 模式下显示切换按钮 |
| showNot | `boolean` | | 是否显示「非」按钮 |

View File

@ -197,7 +197,7 @@ order: 13
也支持通过[模板](./template),设置自定义值。
来一个常见例子,配置两个选择`开始时间`和`结束时间`的时间选择器,需要满足:`开始时间`不能小于`结束时间``结束时间`也不能大于`开始时间`。
来一个常见例子,配置两个选择`开始时间`和`结束时间`的时间选择器,需要满足:`开始时间`不能大于`结束时间``结束时间`也不能小于`开始时间`。
```schema: scope="body"
{
@ -312,185 +312,6 @@ order: 13
}
```
## 日历日程
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
}
```
## 日历日程-自定义颜色
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1",
"className": "bg-success"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2",
"className": "bg-info"
}
]
}
```
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"scheduleClassNames": ["bg-success", "bg-info"],
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
}
```
## 日历日程-自定义日程展示
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
],
"scheduleAction": {
"actionType": "drawer",
"drawer": {
"title": "日程",
"body": {
"type": "table",
"columns": [
{
"name": "time",
"label": "时间"
},
{
"name": "content",
"label": "内容"
}
],
"data": "${scheduleData}"
}
}
}
}
```
## 日历日程-支持从数据源中获取日程
```schema
{
"type": "page",
"data": {
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
},
"body": [
{
"type": "input-date",
"embed": true,
"schedules": "${schedules}"
}
]
}
```
## 放大模式
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"largeMode": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-12 02:14:00",
"endTime": "2021-12-13 05:14:00",
"content": "这是一个日程2"
},
{
"startTime": "2021-12-20 05:14:00",
"endTime": "2021-12-21 05:14:00",
"content": "这是一个日程3"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程4"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-23 05:14:00",
"content": "这是一个日程5"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程6"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程7"
}
]
}
```
## 原生日期组件
原生数字日期将直接使用浏览器的实现,最终展现效果和浏览器有关,而且只支持 `min`、`max`、`step` 这几个属性设置。
@ -527,7 +348,3 @@ order: 13
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| timeConstraints | `object` | `true` | 请参考: [react-datetime](https://github.com/YouCanBookMe/react-datetime) |
| schedules | `Array<{startTime: Date, endTime: Date, content: any, className?: string}> \| string` | | 日历中展示日程可设置静态数据或从上下文中取数据className参考[背景色](https://baidu.gitee.io/amis/zh-CN/style/background/background-color) |
| scheduleClassNames | `Array<string>` | `['bg-warning', 'bg-danger', 'bg-success', 'bg-info', 'bg-secondary']` | 日历中展示日程的颜色,参考[背景色](https://baidu.gitee.io/amis/zh-CN/style/background/background-color) |
| scheduleAction | `SchemaNode` | | 自定义日程展示 |
| largeMode | `boolean` | `false` | 放大模式 |

View File

@ -21,7 +21,6 @@ order: 21
"type": "input-formula",
"name": "formula",
"label": "公式",
"variableMode": "tabs",
"evalMode": false,
"value": "SUM(1 + 2)",
"variables": [
@ -29,12 +28,34 @@ order: 21
"label": "表单字段",
"children": [
{
"label": "ID",
"value": "id"
"label": "文章名",
"value": "name",
"tag": "文本"
},
{
"label": "ID2",
"value": "id2"
"label": "作者",
"value": "author",
"tag": "文本"
},
{
"label": "售价",
"value": "price",
"tag": "数字"
},
{
"label": "出版时间",
"value": "time",
"tag": "时间"
},
{
"label": "版本号",
"value": "version",
"tag": "数字"
},
{
"label": "出版社",
"value": "publisher",
"tag": "文本"
}
]
},
@ -42,12 +63,222 @@ order: 21
"label": "流程字段",
"children": [
{
"label": "ID",
"value": "id"
"label": "联系电话",
"value": "telphone"
},
{
"label": "ID2",
"value": "id2"
"label": "地址",
"value": "addr"
}
]
}
],
}
]
}
```
## 展示模式
设置`"inputMode": "button"`可以切换编辑器的展示模式。
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "input-formula",
"name": "formula",
"label": "公式",
"variableMode": "tree",
"evalMode": false,
"value": "SUM(1 + 2)",
"inputMode": "button",
"variables": [
{
"label": "表单字段",
"children": [
{
"label": "文章名",
"value": "name",
"tag": "文本"
},
{
"label": "作者",
"value": "author",
"tag": "文本"
},
{
"label": "售价",
"value": "price",
"tag": "数字"
},
{
"label": "出版时间",
"value": "time",
"tag": "时间"
},
{
"label": "版本号",
"value": "version",
"tag": "数字"
},
{
"label": "出版社",
"value": "publisher",
"tag": "文本"
}
]
},
{
"label": "流程字段",
"children": [
{
"label": "联系电话",
"value": "telphone"
},
{
"label": "地址",
"value": "addr"
}
]
}
],
}
]
}
```
## 变量展示模式
设置不同`variableMode`字段切换变量展示模式,树形结构:
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "input-formula",
"name": "formula",
"label": "公式",
"variableMode": "tree",
"evalMode": false,
"variables": [
{
"label": "表单字段",
"children": [
{
"label": "文章名",
"value": "name",
"tag": "文本"
},
{
"label": "作者",
"value": "author",
"tag": "文本"
},
{
"label": "售价",
"value": "price",
"tag": "数字"
},
{
"label": "出版时间",
"value": "time",
"tag": "时间"
},
{
"label": "版本号",
"value": "version",
"tag": "数字"
},
{
"label": "出版社",
"value": "publisher",
"tag": "文本"
}
]
},
{
"label": "流程字段",
"children": [
{
"label": "联系电话",
"value": "telphone"
},
{
"label": "地址",
"value": "addr"
}
]
}
],
}
]
}
```
Tab 结构:
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "input-formula",
"name": "formula",
"label": "公式",
"variableMode": "tabs",
"evalMode": false,
"variables": [
{
"label": "表单字段",
"children": [
{
"label": "文章名",
"value": "name",
"tag": "文本"
},
{
"label": "作者",
"value": "author",
"tag": "文本"
},
{
"label": "售价",
"value": "price",
"tag": "数字"
},
{
"label": "出版时间",
"value": "time",
"tag": "时间"
},
{
"label": "版本号",
"value": "version",
"tag": "数字"
},
{
"label": "出版社",
"value": "publisher",
"tag": "文本"
}
]
},
{
"label": "流程字段",
"children": [
{
"label": "联系电话",
"value": "telphone"
},
{
"label": "地址",
"value": "addr"
}
]
}
@ -59,10 +290,21 @@ order: 21
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ------------ | --------------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
| header | string | | 弹出来的弹框标题 |
| evalMode | Boolean | true | 表达式模式 或者 模板模式,模板模式则需要将表达式写在 `${``}` 中间。 |
| variables | {label: string; value: string; children?: any[];}[] | [] | 可用变量 |
| variableMode | string | `list` | 可配置成 `tabs` 或者 `tree` 默认为列表,支持分组。 |
| functions | Object[] | | 可以不设置,默认就是 amis-formula 里面定义的函数,如果扩充了新的函数则需要指定 |
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | ------------------------------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------ |
| title | `string` | `'公式编辑器'` | 弹框标题 |
| header | `string` | - | 编辑器 header 标题,如果不设置,默认使用表单项`label`字段 |
| evalMode | `boolean` | `true` | 表达式模式 或者 模板模式,模板模式则需要将表达式写在 `${``}` 中间。 |
| variables | `{label: string; value: string; children?: any[]; tag?: string}[]` | `[]` | 可用变量 |
| variableMode | `string` | `list` | 可配置成 `tabs` 或者 `tree` 默认为列表,支持分组。 |
| functions | `Object[]` | - | 可以不设置,默认就是 amis-formula 里面定义的函数,如果扩充了新的函数则需要指定 |
| inputMode | `'button' \| 'input-button'` | - | 控件的展示模式 |
| icon | `string` | - | 按钮图标,例如`fa fa-list` |
| btnLabel | `string` | `'公示编辑'` | 按钮文本,`inputMode`为`button`时生效 |
| level | `'info' \| 'success' \| 'warning' \| 'danger' \| 'link' \| 'primary' \| 'dark' \| 'light'` | `default` | 按钮样式 |
| btnSize | `'xs' \| 'sm' \| 'md' \| 'lg'` | - | 按钮大小 |
| borderMode | `'full' \| 'half' \| 'none'` | - | 输入框边框模式 |
| placeholder | `string` | `'暂无数据'` | 输入框占位符 |
| className | `string` | - | 控件外层 CSS 样式类名 |
| variableClassName | `string` | - | 变量面板 CSS 样式类名 |
| functionClassName | `string` | - | 函数面板 CSS 样式类名 |

View File

@ -58,6 +58,42 @@ key 只能是字符串,因此输入格式是 `input-text`,但 value 格式
}
```
## 自定义 value 的默认值
通过 `defaultValue` 设置默认值
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"body": [
{
"type": "input-kv",
"name": "css",
"defaultValue": "1.0"
}
]
}
```
## 关闭可拖拽排序
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"body": [
{
"type": "input-kv",
"name": "css",
"draggable": false
}
]
}
```
## 自定义提示信息
```schema: scope="body"
@ -78,8 +114,10 @@ key 只能是字符串,因此输入格式是 `input-text`,但 value 格式
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | -------- | -------------- | ------------------ |
| valueType | `type` | `"input-text"` | 值类型 |
| keyPlaceholder | `string` | | key 的提示信息的 |
| valuePlaceholder | `string` | | value 的提示信息的 |
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | --------- | -------------- | ------------------ |
| valueType | `type` | `"input-text"` | 值类型 |
| keyPlaceholder | `string` | | key 的提示信息的 |
| valuePlaceholder | `string` | | value 的提示信息的 |
| draggable | `boolean` | true | 是否可拖拽排序 |
| defaultValue | | `''` | 默认值 |

View File

@ -28,7 +28,11 @@ order: 47
## 图片上传
通过设置 `receiver` 来支持文件上传,它的返回值类似如下:
通过设置 `receiver` 来支持文件上传,如果是 tinymce它会将图片放在 `file` 字段中
> 1.6.1 及以上版本可以通过 fileField 字段修改
它的返回值类似如下:
```json
{
@ -55,7 +59,7 @@ order: 47
"body": [
{
"type": "input-rich-text",
"receiver": "/api/mock2/sample/mirror?json={%22value%22:%22/amis/static/logo_c812f54.png%22}",
"receiver": "/api/mock2/sample/mirror?json={%22link%22:%22/amis/static/logo_c812f54.png%22}",
"name": "rich",
"label": "Rich Text"
}
@ -146,6 +150,7 @@ froala 可以通过设置 buttons 参数来控制显示哪些按钮,默认是
| saveAsUbb | `boolean` | | 是否保存为 ubb 格式 |
| receiver | [API](../../../docs/types/api) | | 默认的图片保存 API |
| videoReceiver | [API](../../../docs/types/api) | | 默认的视频保存 API |
| fileField | string | | 上传文件时的字段名 |
| size | `string` | | 框的大小,可设置为 `md` 或者 `lg` |
| options | `object` | | 需要参考 [tinymce](https://www.tiny.cloud/docs/configure/integration-and-setup/) 或 [froala](https://www.froala.com/wysiwyg-editor/docs/options) 的文档 |
| buttons | `Array<string>` | | froala 专用配置显示的按钮tinymce 可以通过前面的 options 设置 [toolbar](https://www.tiny.cloud/docs/demo/custom-toolbar-button/) 字符串 |

View File

@ -283,6 +283,36 @@ order: 56
}
```
## 自动转换值
可以配置 transform来自动转换值支持转小写或大写。
```schema: scope="body"
{
"type": "form",
"body": [
{
"name": "a",
"type": "input-text",
"label": "A",
"placeholder": "输入的英文自动转为小写",
"transform": {
"lowerCase": true
}
},
{
"name": "b",
"type": "input-text",
"label": "B",
"placeholder": "输入的英文自动转为大写",
"transform": {
"upperCase": true
}
}
]
}
```
## 属性表
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
@ -308,6 +338,7 @@ order: 56
| resetValue | `string` | `""` | 清除后设置此配置项给定的值。 |
| prefix | `string` | `""` | 前缀 |
| suffix | `string` | `""` | 后缀 |
| showCounter | `boolean` | `` | 是否显示计数器 |
| minLength | `number` | `` | 限制最小字数 |
| maxLength | `number` | `` | 限制最大字数 |
| showCounter | `boolean` | | 是否显示计数器 |
| minLength | `number` | | 限制最小字数 |
| maxLength | `number` | | 限制最大字数 |
| transform | `object` | | 自动转换值,可选 `transform: { lowerCase: true, upperCase: true }` |

View File

@ -117,6 +117,30 @@ public class StreamingResponseBodyController {
}
```
## source 支持高级配置
> 1.6.1 及以上版本
可以类似 api 那样自定义 header、method 等,比如:
```json
{
"type": "log",
"height": 300,
"source": {
"method": "post",
"url": "[/api/mock2/form/saveForm](http://localhost:3000/)",
"data": {
"myName": "${name}",
"myEmail": "${email}"
},
"headers": {
"my-header": "${myHeader}"
}
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
@ -126,3 +150,4 @@ public class StreamingResponseBodyController {
| autoScroll | `boolean` | true | 是否自动滚动 |
| placeholder | `string` | | 加载中的文字 |
| encoding | `string` | utf-8 | 返回内容的字符编码 |
| source | `string` | | 接口 |

View File

@ -263,7 +263,6 @@ List 的内容、Card 卡片的内容配置同上
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | ----------------- | ------ | -------------------------------------------------------------------------------------- |
| type | `string` | | 如果在 Table、Card 和 List 中,为`"color"`;在 Form 中用作静态展示,为`"static-color"` |
| className | `string` | | 外层 CSS 类名 |
| placeholder | `string` | | 占位文本 |
| map | `object` | | 映射配置 |

View File

@ -70,6 +70,12 @@ order: 13
}
```
_特殊字符变量名_
> 1.6.1 及以上版本
默认变量名不支持特殊字符比如 `${ xxx.yyy }` 意思取 xxx 变量的 yyy 属性,如果变量名就是 `xxx.yyy` 怎么获取?这个时候需要用到转义语法,如:`${ xxx\.yyy }`
### 公式
除了支持简单表达式外,还集成了很多公式(函数)如:

View File

@ -145,6 +145,8 @@ order: 14
> - `crud`组件中的`api`crud 默认是跟地址栏联动,如果要做请关闭同步地址栏 syncLocation: false
> - 等等...
> 如果 api 地址中有变量,比如 `/api/mock2/sample/${id}`amis 就不会自动加上分页参数,需要自己加上,改成 `/api/mock2/sample/${id}?page=${page}&perPage=${perPage}`
#### 配置请求条件
默认在变量变化时,总是会去请求联动的接口,你也可以配置请求条件,当只有当前数据域中某个值符合特定条件才去请求该接口。
@ -233,6 +235,59 @@ order: 14
2. 配置搜索按钮,并配置该行为是刷新目标组件,并配置目标组件`target`
3. 这样我们只有在点击搜索按钮的时候,才会将`keyword`值发送给`select`组件,重新拉取选项
### 表单提交返回数据
表单提交后会将返回结果合并到当前表单数据域,因此可以基于它实现提交按钮后显示结果,比如
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"title": "查询用户 ID",
"body": [
{
"type": "input-group",
"name": "input-group",
"body": [
{
"type": "input-text",
"name": "name",
"label": "姓名"
},
{
"type": "submit",
"label": "查询",
"level": "primary"
}
]
},
{
"type": "static",
"name": "id",
"visibleOn": "typeof data.id !== 'undefined'",
"label": "返回 ID"
}
],
"actions": []
}
```
上面的例子首先用 `"actions": []` 将表单默认的提交按钮去掉,然后在 `input-group` 里放一个 `submit` 类型的按钮来替代表单查询。
这个查询结果返回类似如下的数据
```json
{
"status": 0,
"msg": "保存成功",
"data": {
"id": 1
}
}
```
amis 会将返回的 `data` 写入表单数据域,因此下面的 `static` 组件就能显示了。
### 其他联动
还有一些组件特有的联动效果,例如 form 的 disabledOncrud 中的 itemDraggableOn 等等,可以参考相应的组件文档。

View File

@ -53,6 +53,18 @@ order: 11
}
```
如果是变量本身有 html则需要使用 raw 过滤
```schema
{
"data": {
"text": "<b>World!</b>"
},
"type": "page",
"body": "<h1>Hello</h1> <span>${text|raw}</span>"
}
```
### 表达式
> 1.5.0 及以上版本

View File

@ -0,0 +1,23 @@
---
title: Debug 工具
---
> 1.6.1 及以上版本
amis 内置了 Debug 功能,可以查看组件内部运行日志,方便分析问题,目前在文档右侧就有显示。
## 开启方法
默认不会开启这个功能,可以通过下面两种方式开启:
1. 配置全局变量 `enableAMISDebug` 的值为 `true`,比如 `window.enableAMISDebug = true`
2. 在页面 URL 参数中加上 `amisDebug=1`,比如 `http://xxx.com/?amisDebug=1`
开启之后,在页面右侧就会显示。
## 目前功能
目前 Debug 工具提供了两个功能:
1. 运行日志,主要是 api 及数据转换的日志
2. 查看组件数据链Debug 工具展开后,点击任意组件就能看到这个组件的数据链

View File

@ -55,7 +55,9 @@ interface EventTrack {
| 'reset'
| 'reset-and-submit'
| 'formItemChange'
| 'tabChange';
| 'tabChange'
| 'pageHidden'
| 'pageVisible';
/**
* 事件数据,根据不同事件有不同结构,下面会详细说明
@ -471,3 +473,13 @@ tab 切换事件,示例
默认情况下 `key` 的值从 `0` 开始,如果 tab 上设置了 `hash` 值就会用这个值。
同样,如果 tabs 设置了 id也会输出这个 id 值方便区分
### pageHidden
当 tab 切换或者页面关闭时触发,可以当成用户离开页面的时间。
### pageVisible
当用户又切换回当前页面的时间,可以当做是用户重新访问的开始时间。
由于 amis 可能被嵌入到页面中,所以 amis 无法知晓页面首次打开的时间,需要自行处理。

View File

@ -99,3 +99,7 @@ amisScoped.updateProps({
}
})
```
## CRUD api 分页功能失效
如果 api 地址中有变量,比如 `/api/mock2/sample/${id}`amis 就不会自动加上分页参数,需要自己加上,改成 `/api/mock2/sample/${id}?page=${page}&perPage=${perPage}`

View File

@ -85,18 +85,6 @@ SDK 版本适合对前端或 React 不了解的开发者,它不依赖 npm 及
</html>
```
### 更新属性
可以通过 amisScoped 对象的 updateProps 方法来更新下发到 amis 的属性。
```ts
amisScoped.updateProps(
{
// 新的属性对象
} /*, () => {} 更新回调 */
);
```
### 切换主题
jssdk 版本默认使用 `sdk.css` 即云舍主题,如果你想用使用仿 Antd请将 css 引用改成 `.antd.css`。同时 js 渲染地方第四个参数传入 `theme` 属性。如:
@ -120,6 +108,7 @@ amisScoped.updateProps({
theme: 'antd'
});
```
> 如果想使用 amis 1.2.2 之前的默认主题,名字是 ang
### 初始值
@ -239,35 +228,6 @@ amisScoped.updateProps(
);
```
### 销毁
如果是单页应用,在离开当前页面的时候通常需要销毁实例,可以通过 unmount 方法来完成。
```ts
amisScoped.unmount();
```
### 切换主题
jssdk 版本默认使用 `sdk.css` 即云舍主题,如果你想用使用仿 AntD 主题,请改成引用 `antd.css`。同时 js 渲染地方第四个参数传入 `theme` 属性。如:
```js
amis.embed(
'#root',
{
// amis schema
},
{
// 默认数据
},
{
theme: 'antd'
}
);
```
> 如果想使用 amis 1.2.2 之前的默认主题,名字是 ang
### 多页模式
默认 amis 渲染是单页模式,如果想实现多页应用,请使用 [app 渲染器](../../components/app)。

View File

@ -811,6 +811,8 @@ Content-Type: application/pdf
Content-Disposition: attachment; filename="download.pdf"
```
如果只有 `Content-Type`,比如 Excel 的 `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,则应该使用[页面跳转](../../components/action#直接跳转)的方式来实现下载。
### replaceData
返回的数据是否替换掉当前的数据,默认为 `false`(即追加),设置为`true`就是完全替换当前数据。

View File

@ -753,6 +753,14 @@ export const components = [
makeMarkdownRenderer
)
},
{
label: 'Calendar 日历',
path: '/zh-CN/components/calendar',
getComponent: () =>
import('../../docs/zh-CN/components/calendar.md').then(
makeMarkdownRenderer
)
},
{
label: 'Card 卡片',
path: '/zh-CN/components/card',

View File

@ -65,6 +65,8 @@ import Form2LinkPageSchema from './Linkage/Form2';
import CRUDLinkPageSchema from './Linkage/CRUD';
import OptionsPageSchema from './Linkage/Options';
import OptionsLocalPageSchema from './Linkage/OptionsLocal';
import FormSubmitSchema from './Linkage/FormSubmit';
import EventsSchema from './Linkage/Event';
import WizardSchema from './Wizard';
import ChartSchema from './Chart';
import EChartsEditorSchema from './ECharts';
@ -480,6 +482,11 @@ export const examples = [
path: '/examples/linkpage/form',
component: makeSchemaRenderer(FormLinkPageSchema)
},
{
label: '表单提交后显示结果',
path: '/examples/linkpage/form-submit',
component: makeSchemaRenderer(FormSubmitSchema)
},
{
label: '表单自动更新',
path: '/examples/linkpage/form2',
@ -489,6 +496,11 @@ export const examples = [
label: '表单和列表联动',
path: '/examples/linkpage/crud',
component: makeSchemaRenderer(CRUDLinkPageSchema)
},
{
label: '广播事件机制',
path: '/examples/linkpage/event',
component: makeSchemaRenderer(EventsSchema)
}
]
},

View File

@ -0,0 +1,358 @@
export default {
type: 'page',
title: '广播事件',
regions: ['body', 'toolbar', 'header'],
body: [
{
type: 'button',
id: 'b_001',
label: '发送广播事件1-表单1/2/3都在监听',
actionType: 'reload',
dialog: {
title: '系统提示',
body: '对你点击了'
},
// target: 'form?name=lvxj',
onEvent: {
click: {
actions: [
{
actionType: 'reload',
args: {
name: 'lvxj',
age: 18
},
preventDefault: true,
stopPropagation: false,
componentId: 'form_001'
// componentId: 'form_001_form_01_text_01'
},
{
actionType: 'broadcast',
eventName: 'broadcast_1',
args: {
name: 'lvxj',
age: 18,
ld: [
{
name: 'ld-lv',
age: 'ld-19'
},
{
name: 'ld-xj',
age: 'ld-21'
}
]
},
description: '一个按钮的点击事件'
}
]
}
}
},
{
type: 'button',
id: 'b_002',
label: '发送广播事件2-表单3在监听',
className: 'ml-2',
actionType: 'reload',
dialog: {
title: '系统提示',
body: '对你点击了'
},
// target: 'form?name=lvxj',
onEvent: {
click: {
actions: [
{
actionType: 'broadcast',
eventName: 'broadcast_2',
args: {
job: '拯救世界'
},
description: '一个按钮的点击事件'
}
]
}
}
},
{
type: 'form',
id: 'form_001',
title: '表单1(我的权重最低)-刷新',
name: 'form1',
debug: true,
data: {
selfname: 'selfname' //
},
body: [
{
type: 'form',
id: 'form_001_form_01',
title: '表单1(我的权重最低)-刷新',
name: 'sub-form1',
debug: true,
body: [
{
type: 'input-text',
id: 'form_001_form_01_text_01',
label: '名称',
name: 'name',
disabled: false,
mode: 'horizontal'
},
{
type: 'input-text',
id: 'form_001_form_01_text_02',
label: '等级',
name: 'level',
disabled: false,
mode: 'horizontal'
},
{
type: 'input-text',
id: 'form_001_form_01_text_03',
label: '昵称',
name: 'myname',
disabled: false,
mode: 'horizontal'
}
],
onEvent: {
broadcast_1: {
actions: [
{
actionType: 'reload',
args: {
level: 1,
myname: '${event.data.name}', //
name: '${selfname}' //
},
preventDefault: true,
stopPropagation: false
}
]
}
}
}
]
},
{
type: 'form',
name: 'form2',
id: 'form_002',
title: '表单2(权重2)-刷新+发Ajax',
debug: true,
body: [
{
type: 'input-text',
id: 'form_001_text_01',
label: '年龄',
name: 'age',
disabled: false,
mode: 'horizontal'
}
],
data: {
execOn: 'kkk',
param: '1'
},
onEvent: {
broadcast_1: {
weight: 2,
actions: [
{
actionType: 'reload',
args: {
age: '${event.data.age}'
},
preventDefault: false,
stopPropagation: false,
execOn: 'execOn === "kkk"' // or this.xxx
},
{
actionType: 'ajax',
args: {
param: '2'
},
api: 'https://api/form/form2-ajax?param=${param}', // param=2args
// api: 'https://api/form/form2-ajax?param=${name}', // param=lvxj
execOn: 'execOn === "kkk"',
preventDefault: false,
stopPropagation: false
},
{
actionType: 'broadcast',
eventName: 'broadcast_2',
args: {
job: '打怪兽'
},
description: '一个按钮的点击事件'
}
]
}
}
},
{
type: 'form',
name: 'form3',
id: 'form_003',
title: '表单3(权重3)-逻辑编排',
debug: true,
body: [
{
type: 'input-text',
id: 'form_003_text_01',
label: '职业',
name: 'job',
disabled: false,
mode: 'horizontal'
}
],
data: {
loopData: [
{
name: 'lv',
age: '19'
},
{
name: 'xj',
age: '21'
}
],
branchCont: 18
},
api: 'https://api/form/form3',
onEvent: {
broadcast_1: {
weight: 3,
actions: [
{
actionType: 'custom',
script:
"doAction({actionType: 'ajax',api: 'https://api/form/form3-custom-ajax-1'});\n //event.stopPropagation();"
},
{
actionType: 'parallel',
args: {
level: 3
},
children: [
{
actionType: 'ajax',
api: 'https://api/form/form3-parallel-ajax-1',
preventDefault: false
// stopPropagation: true
},
{
actionType: 'ajax',
api: 'https://api/form/form3-parallel-ajax-2'
}
]
},
{
actionType: 'switch',
preventDefault: false,
stopPropagation: false,
children: [
{
actionType: 'ajax',
api: 'https://api/form/form3-branch-ajax-1',
expression: 'this.branchCont > 19',
preventDefault: false,
stopPropagation: true //
},
{
actionType: 'ajax',
api: 'https://api/form/form3-branch-ajax-2',
expression: 'this.branchCont > 17',
preventDefault: false,
stopPropagation: false
},
{
actionType: 'ajax',
api: 'https://api/form/form3-branch-ajax-3',
expression: 'this.branchCont > 16'
}
]
},
{
actionType: 'loop',
loopName: 'loopData',
preventDefault: false,
stopPropagation: false,
args: {
level: 3
},
children: [
{
actionType: 'reload',
preventDefault: false,
stopPropagation: false
},
{
actionType: 'ajax',
api: 'https://api/form/form3-loop-ajax-1?name=${name}',
preventDefault: false,
stopPropagation: false
},
// {
// actionType: 'break'
// },
{
actionType: 'ajax',
api: 'https://api/form/form3-loop-ajax-2?age=${age}'
},
{
actionType: 'loop',
loopName: 'loopData',
args: {
level: 3
},
children: [
{
actionType: 'ajax',
api: 'https://api/form/form3-loop-loop-ajax-1'
},
{
actionType: 'ajax',
api: 'https://api/form/form3-loop-loop-ajax-2?age=${age}',
preventDefault: false,
stopPropagation: false
},
{
actionType: 'continue'
},
{
actionType: 'ajax',
api: 'https://api/form/form3-loop-loop-ajax-3'
}
]
}
]
}
]
},
broadcast_2: {
actions: [
{
actionType: 'reset-and-submit',
api: 'https://api/form/form3-reset-and-submit',
script: null, //
preventDefault: false,
stopPropagation: false
},
{
actionType: 'reload',
args: {
job: '${event.data.job}'
},
preventDefault: false,
stopPropagation: false
}
]
}
}
}
]
};

View File

@ -0,0 +1,34 @@
export default {
type: 'page',
title: '表单提交后显示结果',
body: {
type: 'form',
api: '/api/mock2/form/saveForm',
title: '查询用户 ID',
body: [
{
type: 'input-group',
name: 'input-group',
body: [
{
type: 'input-text',
name: 'name',
label: '姓名'
},
{
type: 'submit',
label: '查询',
level: 'primary'
}
]
},
{
type: 'static',
name: 'id',
visibleOn: "typeof data.id !== 'undefined'",
label: '返回 ID'
}
],
actions: []
}
};

View File

@ -12,7 +12,6 @@ export default {
type: 'input-text',
name: 'name'
},
{
label: 'Email',
type: 'input-email',

View File

@ -103,6 +103,8 @@
})();
}
window.enableAMISDebug = true;
/* @require ./index.jsx 标记为同步依赖,提前加载 */
amis.require(['./index.jsx'], function (app) {
var initialState = {};

View File

@ -7,15 +7,12 @@ 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,

View File

@ -1,6 +1,6 @@
{
"name": "amis",
"version": "1.5.7",
"version": "1.6.1",
"description": "一种MIS页面生成工具",
"main": "lib/index.js",
"scripts": {
@ -43,7 +43,7 @@
]
},
"dependencies": {
"amis-formula": "^1.3.5",
"amis-formula": "^1.3.6",
"ansi-to-react": "^6.1.6",
"async": "2.6.0",
"attr-accept": "2.2.2",
@ -132,7 +132,7 @@
"css": "3.0.0",
"faker": "^5.5.3",
"fis-optimizer-terser": "^1.0.1",
"fis-parser-sass": "^1.1.0",
"fis-parser-sass": "^1.1.1",
"fis-parser-svgr": "^1.0.0",
"fis3": "^3.4.41",
"fis3-deploy-skip-packed": "0.0.5",
@ -147,7 +147,6 @@
"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",
@ -155,7 +154,7 @@
"js-yaml": "^4.1.0",
"json5": "^2.2.0",
"lint-staged": "^12.1.4",
"marked": "^3.0.4",
"marked": ">=4.0.10",
"mkdirp": "^1.0.4",
"moment-timezone": "^0.5.33",
"path-to-regexp": "^6.2.0",

View File

@ -197,9 +197,11 @@ module.exports = function (content, file) {
}
);
content = marked(content).replace(/<p>\[\[(\d+)\]\]<\/p>/g, function (_, id) {
return placeholder[id] || '';
});
content = marked
.parse(content)
.replace(/<p>\[\[(\d+)\]\]<\/p>/g, function (_, id) {
return placeholder[id] || '';
});
content = fis.compile.partial(content, file, 'html');
// + `\n\n<div class="m-t-lg b-l b-info b-3x wrapper bg-light dk">文档内容有误?欢迎大家一起来编写,文档地址:<i class="fa fa-github"></i><a href="https://github.com/baidu/amis/tree/master${file.subpath}">${file.subpath}</a>。</div>`;

View File

@ -626,10 +626,11 @@
--DropDown-menu-paddingX: 0;
--DropDown-menu-paddingY: var(--gap-xs);
--DropDown-menuItem-onHover-bg: var(--ListMenu-item--onHover-bg);
--DropDown-menuItem-color: var(--text-color);
--DropDown-group-color: #848b99;
--DropDown-menuItem-color: #151a26;
--DropDown-menuItem-onHover-color: var(--primary);
--DropDown-menuItem-onActive-color: var(--primary);
--DropDown-menuItem-onDisabled-color: var(--text--muted-color);
--DropDown-menuItem-onDisabled-color: #b4b6ba;
--DropDown-menuItem-paddingX: var(--gap-sm);
--DropDown-menuItem-paddingY: calc(
(var(--DropDown-menu-height) - var(--fontSizeBase) * var(--lineHeightBase)) /
@ -657,6 +658,8 @@
--Form-description-fontSize: var(--fontSizeSm);
--Form-fontSize: var(--fontSizeBase);
--Form-item-fontSize: var(--Form-fontSize);
--Form-item-fontColor: #5e626a;
--Form-group--lg-gutterWidth: #{px2rem(40px)};
--Form-group--md-gutterWidth: #{px2rem(30px)};
@ -1096,11 +1099,12 @@
--PickerColumns-bg: white;
--PickerColumns-toolbar-height: #{px2rem(50px)};
--PickerColumns-title-fontSize: var(--fontSizeLg);
--PickerColumns-title-color: #222;
--PickerColumns-title-lineHeight: 1.5;
--PickerColumns-action-padding: 0 var(--gap-sm);
--PickerColumns-action-fontSize: var(--fontSizeMd);
--PickerColumns-confirmAction-color: #{lighten($text-color, 25%)};
--PickerColumns-cancelAction-color: #{lighten($text-color, 50%)};
--PickerColumns-action-padding: 0 var(--gap-md);
--PickerColumns-action-fontSize: var(--fontSizeLg);
--PickerColumns-confirmAction-color: #2468f2;
--PickerColumns-cancelAction-color: #666;
--PickerColumns-option-fontSize: var(--fontSizeLg);
--PickerColumns-optionText-color: var(--text-color);
--PickerColumns-optionDisabled-opacity: 0.3;
@ -1111,6 +1115,8 @@
--PopOverAble-iconColor: inherit;
--PopOverAble-onHover-iconColor: inherit;
--PopUp-cancelAction-color: #666;
--Property-title-bg: #f2f2f2;
--Property-label-bg: #f7f7f7;
@ -1437,4 +1443,9 @@
--ColumnToggler-fontColor: #151a26;
--ColumnToggler-item-backgroundColor: #f6f7f8;
--ColumnToggler-item-backgroundColor-onHover: rgba(36, 104, 242, 0.1);
--InputFormula-header-bgColor: #fafafa;
--InputFormula-icon-size: #{px2rem(24px)};
--InputFormula-icon-color-onActive: var(--primary);
--InputFormula-code-bgColor: #f2f2f4;
}

7
scss/_thirds.scss Normal file
View File

@ -0,0 +1,7 @@
@import url('../node_modules/react-datetime/css/react-datetime.css?__inline');
@import url('../node_modules/codemirror/lib/codemirror.css?__inline');
@import url('../node_modules/froala-editor/css/froala_style.min.css?__inline');
@import url('../node_modules/froala-editor/css/froala_editor.pkgd.min.css?__inline');
@import url('../node_modules/tinymce/skins/ui/oxide/skin.css?__inline');
@import url('../node_modules/video-react/dist/video-react.css?__inline');
@import url('../node_modules/cropperjs/dist/cropper.css?__inline');

View File

@ -409,6 +409,7 @@ $zindex-contextmenu: 1500 !default;
$zindex-tooltip: 1600 !default;
$zindex-toast: 2000 !default;
$zindex-top: 3000 !default;
$zindex-debug: 4000 !default;
$Form--horizontal-columns: 12;
$Table-strip-bg: lighten(#f6f8f8, 1%) !default;

View File

@ -82,6 +82,7 @@
.#{$ns}CalendarMobile {
height: 100%;
width: 100%;
overflow: scroll;
&-pop {
@ -245,7 +246,7 @@
background: #fff;
box-shadow: 0 0 2px 2px rgba(0,0,0,0.02);
border-radius: 24px;
overflow-x: scroll;
overflow-x: auto;
position: relative;
height: px2rem(48px);
line-height: px2rem(48px);
@ -257,6 +258,9 @@
margin: 0 px2rem(25px);
}
}
.#{$ns}DatePicker-shortcuts {
width: auto;
}
}
&-calendar-wrap {
@ -345,7 +349,7 @@
}
&-time {
height: px2rem(200px);
height: px2rem(180px);
&-title {
border: var(--Calendar-borderWidth) solid var(--borderColorDarken);
border-left: none;
@ -358,4 +362,11 @@
margin: 0 auto;
}
}
.#{$ns}CalendarTime {
height: px2rem(130px);
overflow: hidden;
}
.#{$ns}PickerColumns-header {
display: none;
}
}

View File

@ -95,4 +95,8 @@
&-tab {
padding: 0;
}
&-btnCancel {
color: var(--PopUp-cancelAction-color);
}
}

View File

@ -0,0 +1,27 @@
.#{$ns}CityArea {
&-popup {
height: px2rem(280px);
}
&-Input {
margin-top: var(--gap-xs);
outline: none;
vertical-align: top;
border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor);
border-radius: var(--Form-input-borderRadius);
line-height: var(--Form-input-lineHeight);
padding: var(--Form-input-paddingY) var(--Form-input-paddingX);
font-size: var(--Form-input-fontSize);
display: inline-flex !important;
&::placeholder {
color: var(--Form-input-placeholderColor);
user-select: none;
}
&:focus {
border-color: var(--Form-input-onFocused-borderColor);
box-shadow: var(--Form-input-boxShadow);
background: var(--Form-input-onFocused-bg);
}
}
}

View File

@ -8,6 +8,10 @@
.#{$ns}Collapse-arrow {
float: right;
}
.#{$ns}Collapse-icon-tranform {
float: right;
}
}
}
}

View File

@ -33,7 +33,6 @@
transition: opacity var(--animation-duration);
display: flex;
align-items: center;
margin-left: var(--gap-base);
.#{$ns}CBDelete {
margin-left: var(--gap-xs);
@ -112,6 +111,21 @@
display: none;
}
}
&.simple {
margin-left: 0;
&:before {
display: none;
}
&:after {
display: none;
}
}
}
&-toolbarCondition {
margin-right: var(--gap-base);
}
}
@ -217,6 +231,10 @@
display: none;
}
}
&-simple {
margin-bottom: var(--gap-sm);
}
}
.#{$ns}CBInputSwitch {

167
scss/components/_debug.scss Normal file
View File

@ -0,0 +1,167 @@
/**
* Debug 模块的 UI由于没法使用任何主题所以这里使用独立配色风格
*/
.AMISDebug {
position: fixed;
z-index: $zindex-debug;
top: 0;
right: 0;
height: 100vh;
width: 24px;
h3 {
color: inherit;
}
.primary {
color: #009fff;
}
&-header {
padding: var(--Drawer-header-padding);
background: var(--Drawer-header-bg);
border-bottom: var(--Drawer-content-borderWidth) solid
var(--Drawer-header-borderColor);
}
&-hoverBox {
pointer-events: none;
position: absolute;
outline: 1px dashed #1c76c4;
}
&-activeBox {
pointer-events: none;
position: absolute;
outline: 1px #1c76c4;
}
&-tab {
overflow: hidden;
}
&-tab > button {
color: inherit;
background: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: var(--gap-sm) var(--gap-md);
transition: 0.3s;
border-bottom: 1px solid transparent;
}
&-tab > button:hover {
color: #e7e7e7;
}
&-tab > button.active {
color: #e7e7e7;
border-bottom-color: #e7e7e7;
}
&-toggle {
background: var(--body-bg);
position: fixed;
top: 50%;
right: 0;
width: 24px;
height: 48px;
box-shadow: rgba(0, 0, 0, 0.24) -2px 0px 4px 0px;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
padding-top: 14px;
padding-left: 6px;
cursor: pointer;
i {
color: var(--text-color);
}
&:hover {
i {
color: var(--primary);
}
}
}
&-content {
display: none;
}
&-resize {
position: absolute;
width: 4px;
top: 0;
left: 0;
bottom: 0;
cursor: col-resize;
&:hover {
background: #75715e;
}
}
&-changePosition {
position: absolute;
font-size: 18px;
right: 40px;
top: var(--gap-sm);
cursor: pointer;
}
&-close {
position: absolute;
font-size: 18px;
right: var(--gap-sm);
top: var(--gap-sm);
cursor: pointer;
}
&.is-expanded {
width: 420px;
overflow: auto;
background: #272821;
color: #cccccc;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
.AMISDebug-toggle {
display: none;
}
.AMISDebug-content {
display: block;
}
}
&.is-left {
left: 0;
.AMISDebug-resize {
left: unset;
right: 0;
}
}
&-log {
padding: var(--gap-sm);
button {
cursor: pointer;
background: #0e639c;
flex-grow: 1;
box-sizing: border-box;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 6px 11px;
outline: none;
text-decoration: none;
color: inherit;
max-width: 300px;
border: none;
}
button:hover {
background: #1177bb;
}
}
&-inspect {
padding: var(--gap-sm);
}
}

View File

@ -34,20 +34,28 @@
}
&-menu {
position: absolute;
z-index: $zindex-dropdown;
top: 100%;
left: 0;
margin: px2rem(1px) 0 0;
background: var(--DropDown-menu-bg);
list-style: none;
padding: var(--DropDown-menu-paddingY) var(--DropDown-menu-paddingX);
border: var(--DropDown-menu-borderWidth) solid
var(--DropDown-menu-borderColor);
border-radius: var(--DropDown-menu-borderRadius);
box-shadow: var(--DropDown-menu-boxShadow);
min-width: var(--DropDown-menu-minWidth);
text-align: left;
border: none;
user-select: none;
&-root {
position: absolute;
z-index: $zindex-dropdown;
top: 100%;
left: 0;
margin: px2rem(1px) 0 0;
border: none;
border-radius: var(--DropDown-menu-borderRadius);
box-shadow: var(--DropDown-menu-boxShadow);
min-width: var(--DropDown-menu-minWidth);
overflow-y: auto;
overflow-x: hidden;
max-height: px2rem(300px);
}
}
&--alignRight &-menu {
@ -95,6 +103,28 @@
background: var(--DropDown-menu-borderColor);
padding: 0;
}
&.#{$ns}DropDown-groupTitle {
height: inherit;
font-size: var(--fontSizeSm);
padding: var(--gap-xs) var(--gap-xs);
padding-left: var(--gap-sm);
color: var(--DropDown-group-color);
flex-grow: 1;
cursor: default;
&:hover {
background: none;
}
span {
white-space: nowrap;
}
& ~ .#{$ns}DropDown-button {
padding-left: var(--gap-lg);
}
}
}
&-menu > li a {

View File

@ -3,30 +3,50 @@
max-width: 100%;
box-sizing: content-box;
@mixin scrollbar {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background: rgba(0, 0, 0, 0.3);
}
}
@mixin panel-header {
width: 100%;
height: px2rem(30px);
line-height: px2rem(30px);
padding: 0 #{px2rem(10px)};
box-sizing: border-box;
background: var(--InputFormula-header-bgColor);
border-radius: var(--borderRadius) var(--borderRadius) 0 0;
border-bottom: var(--Form-input-borderWidth) solid
var(--Form-input-borderColor);
font-weight: 500;
}
&-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;
@include panel-header();
}
&-editor {
min-height: px2rem(238px);
max-height: px2rem(320px);
height: auto;
padding: #{px2rem(10px)};
@include scrollbar();
height: px2rem(200px);
padding: #{px2rem(5px)};
padding-right: 0;
.CodeMirror {
width: 100%;
height: 100%;
}
}
&.is-error &-editor {
@ -39,108 +59,261 @@
&-settings {
display: flex;
flex-direction: row;
flex-flow: row nowrap;
align-items: stretch;
justify-content: space-between;
max-height: px2rem(350px);
margin: 0 -5px;
height: #{px2rem(250px)};
margin-top: #{px2rem(10px)};
}
> div {
&-panel {
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
align-items: stretch;
height: #{px2rem(250px)};
width: #{px2rem(220px)};
border-radius: var(--borderRadius);
border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor);
&:not(:last-child) {
margin-right: #{px2rem(10px)};
}
&:last-child {
flex: 1;
padding: 0 5px;
display: flex;
flex-direction: column;
}
> h3 {
padding: 10px 0;
margin: 0;
flex-shrink: 0;
&-header {
@include panel-header();
}
&-body {
flex: 1;
display: flex;
flex-flow: column nowrap;
max-height: #{px2rem(220px)};
}
}
/* 变量列表 */
&-VariableList {
&-root {
max-height: #{px2rem(220px)};
}
&-root.is-scrollable {
@include scrollbar();
overflow-x: hidden;
overflow-y: auto;
}
&-base {
--Form-input-fontSize: var(--fontSizeSm);
max-width: inherit;
overflow: hidden;
}
&-tabs {
--Tabs-linkFontSize: var(--fontSizeSm);
--Tabs--card-linkPadding: #{px2rem(5px)};
max-height: #{px2rem(220px)};
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
& > ul {
border-top: none;
}
> div {
& > div {
@include scrollbar();
overflow-x: hidden;
overflow-y: auto;
flex: 1;
border-radius: var(--borderRadius);
}
}
&-tab {
padding: 0;
}
&-item {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
white-space: nowrap;
& > label {
white-space: nowrap;
}
&-tag {
vertical-align: middle;
text-align: center;
padding: 0 #{px2rem(8px)};
line-height: 17px;
border-radius: var(--borderRadius);
background: #f5f5f5;
color: var(--black);
}
}
}
/* 函数列表 */
&-FuncList {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: stretch;
flex: 1;
&:not(:last-child) {
margin-right: #{px2rem(10px)};
}
&-searchBox {
display: flex;
width: auto;
flex-shrink: 0;
padding: #{px2rem(8px)};
& > div {
flex: 1;
font-size: var(--fontSizeSm);
height: var(--gap-xl);
}
}
&-body {
@include scrollbar();
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
&-collapseGroup {
.#{$ns}FormulaEditor-FuncList-collapse {
border: none;
.#{$ns}FormulaEditor-FuncList-expandIcon {
font-size: var(--fontSizeSm);
line-height: var(--fontSizeXl);
transform-origin: #{px2rem(7px)} #{px2rem(9px)};
transition: transform 0.2s;
}
}
}
&-groupTitle {
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
align-items: unset;
padding: #{px2rem(5px)} #{px2rem(10px)};
background: transparent;
font-size: var(--fontSizeSm);
font-weight: bold;
}
&-groupBody {
> div {
padding: 5px 0;
}
}
&-item {
cursor: pointer;
padding: 0 var(--gap-lg);
height: var(--gap-xl);
line-height: var(--gap-xl);
&.is-active {
background: var(--Tree-item-onHover-bg);
}
}
&-doc {
display: flex;
flex-flow: column nowrap;
padding: #{px2rem(10px)};
max-height: #{px2rem(200px)};
pre {
white-space: pre-wrap;
word-wrap: break-word;
background: var(--InputFormula-code-bgColor);
padding: #{px2rem(5px)} #{px2rem(10px)};
border-radius: var(--borderRadius);
margin-top: 0;
code {
color: #2468f2;
}
}
&-desc {
@include scrollbar();
color: var(--text--loud-color);
overflow-x: hidden;
overflow-y: auto;
flex: 1;
min-height: 0;
}
}
}
.cm-field,
.cm-func {
border-radius: 2px;
border-radius: 3px;
color: #fff;
margin: 0 1px;
padding: 0 2px;
}
.cm-field {
padding: 3px 5px;
}
.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);
}
color: #ae4597;
font-weight: bold;
line-height: 14px;
}
}
.#{$ns}FormulaPicker {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
&-input {
flex: 1;
margin-right: #{px2rem(10px)};
}
&-action {
display: flex;
justify-content: center;
align-items: center;
}
&-icon {
margin-left: auto;
margin-right: #{px2rem(5px)};
top: 0 !important;
font-size: var(--InputFormula-icon-size);
&:not(:last-child) {
margin-right: var(--fontSizeSm);
}
&.is-filled {
fill: var(--InputFormula-icon-color-onActive);
color: var(--InputFormula-icon-color-onActive);
}
}
}

View File

@ -6,7 +6,11 @@
overflow: hidden;
font-size: var(--PickerColumns-option-fontSize);
&-toolbar {
li:focus {
outline: none;
}
&-header {
display: flex;
align-items: center;
justify-content: space-between;
@ -18,12 +22,12 @@
height: 100%;
padding: var(--PickerColumns-action-padding);
font-size: var(--PickerColumns-action-fontSize);
background-color: transparent;
background-color: transparent !important;
border: none;
cursor: pointer;
&:active {
opacity: 0.7;
background-color: none !important;
}
&:hover {
background-color: none !important;
@ -31,11 +35,11 @@
}
&-confirm {
color: var(--PickerColumns-confirmAction-color);
color: var(--PickerColumns-confirmAction-color) !important;
}
&-cancel {
color: var(--PickerColumns-cancelAction-color);
color: var(--PickerColumns-cancelAction-color) !important;
}
&-title {
@ -44,6 +48,7 @@
font-size: var(--PickerColumns-title-fontSize);
line-height: var(--PickerColumns-title-lineHeight);
text-align: center;
color: var(--PickerColumns-title-color);
}
&-columns {
@ -127,4 +132,9 @@
cursor: not-allowed;
opacity: var(--PickerColumns-optionDisabled-opacity);
}
&-columnItemis-selected {
font-size: 18px;
color: --PickerColumns-title-color;
}
}

View File

@ -28,7 +28,7 @@
background: var(--PopOver-bg);
left: 0;
bottom: 0;
z-index: $zindex-popover;
z-index: $zindex-top;
padding: 0;
margin: 0;
font-weight: var(--fontWeightNormal);
@ -105,6 +105,7 @@
}
&-cancel {
color: var(--PopUp-cancelAction-color);
margin-left: var(--gap-sm);
}

View File

@ -10,8 +10,8 @@
color: var(--ColorPicker-color);
border-radius: var(--borderRadius);
&-popup{
height: 80vh;
&-popup {
height: px2rem(400px);
}
&:not(.is-disabled) {

View File

@ -128,7 +128,7 @@
}
.#{$ns}DateRangePicker-popup {
height: 90vh;
height: px2rem(400px);
}
@include media-breakpoint-up(sm) {
@ -148,3 +148,36 @@
background: var(--DatePicker-bg);
border-radius: var(--DatePicker-borderRadius);
}
// 移动端输入框样式
.#{$ns}DateRangePicker.is-mobile {
border: 0;
justify-content: flex-end;
span,
a {
&:focus {
outline: unset;
}
}
.#{$ns}DateRangePicker-value,
.#{$ns}DateRangePicker-clear {
display: inline-flex;
justify-content: flex-end;
padding: 0 0;
}
.#{$ns}DateRangePicker-value {
margin-right: var(--gap-xs);
}
.#{$ns}DateRangePicker-placeholder {
flex-grow: unset;
flex-basis: unset;
}
.#{$ns}DateRangePicker-toggler {
margin-top: -3px;
}
}

View File

@ -123,8 +123,53 @@
}
.#{$ns}DatePicker-popup {
height: 80vh;
height: px2rem(300px);
}
// 移动端输入框样式
.#{$ns}DatePicker.is-mobile {
border: 0;
justify-content: flex-end;
span,
a {
&:focus {
outline: unset;
}
}
.#{$ns}DatePicker-value,
.#{$ns}DatePicker-clear {
display: inline-flex;
justify-content: flex-end;
padding: 0 0;
}
.#{$ns}DatePicker-value {
margin-right: var(--gap-xs);
}
.#{$ns}DatePicker-placeholder {
flex-grow: unset;
flex-basis: unset;
}
.#{$ns}DatePicker-toggler {
margin-top: -3px;
}
}
.#{$ns}DatePicker-popup.#{$ns}DatePicker-mobile {
color: red;
.rdt {
width: 100%;
.rdtPicker {
width: 100%;
padding: unset;
}
}
}
// override third-party styles
.rdt {
user-select: none;

View File

@ -52,6 +52,8 @@
font-weight: var(--fontWeightNormal);
margin-bottom: var(--gap-xs);
position: relative;
font-size: var(--Form-item-fontSize);
color: var(--Form-item-fontColor);
> span {
position: relative;

View File

@ -17,7 +17,7 @@
}
&-popup {
height: 80vh;
height: px2rem(400px);
}
}

View File

@ -113,6 +113,8 @@ $link-color: $info;
--Modal-header-bg: #fff;
--Modal-title-fontWeight: 500;
--Form-item-fontSize: var(--fontSizeBase);
--Form-item-fontColor: var(--body-color);
--Form-input-onFocused-borderColor: #{$blue-5};
--Form-input-borderColor: #{$border-color-base};
--Form-input-boxShadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
@ -193,7 +195,8 @@ $link-color: $info;
--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-detail-visible-shadow: 0 #{px2rem(1px)} #{px2rem(10px)} 0
rgba(0 0 0 / 10%);
--TimelineItem--font-size: #{px2rem(12px)};

View File

@ -1,3 +1,5 @@
@import '../thirds';
@import '../mixins';
@import '../base/reset';
@import '../base/normalize';
@ -82,6 +84,7 @@
@import '../components/form/checks';
@import '../components/form/selection';
@import '../components/form/city';
@import '../components/city-area';
@import '../components/form/switch';
@import '../components/form/number';
@import '../components/form/select';
@ -122,4 +125,6 @@
@import '../components/formula';
@import '../components/timeline';
@import '../components/debug';
@import '../utilities';

View File

@ -119,6 +119,8 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Page-header-paddingX: var(--gap-md);
--Page-header-paddingY: #{px2rem(10px)};
--Form-item-fontSize: var(--fontSizeBase);
--Form-item-fontColor: #{$G4};
--Form-item-gap: var(--gap-base);
--Form-input-bg: #{$G11};
--Form-input-color: #{$G2};
@ -318,7 +320,7 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Button--primary-onActive-color: #{$G11};
--Button--light-border: var(--light);
--Button--light-color: var(--button-color);
--Button--light-color: var(--text-color);
--Button-onDisabled-borderColor: #{$G9};
--Button-onDisabled-opacity: 0.65;
@ -639,7 +641,8 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--TimelineItem--round-radius: #{$R8};
--TimelineItem--content-radius: #{$R2};
--TimelineItem-detail-visible-shadow: 0 #{px2rem(1px)} #{px2rem(10px)} 0 rgba(0 0 0 / 10%);
--TimelineItem-detail-visible-shadow: 0 #{px2rem(1px)} #{px2rem(10px)} 0
rgba(0 0 0 / 10%);
--TimelineItem--font-size: #{$T2};
@ -655,8 +658,6 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Timeline--warning-bg: var(--warning);
--Timeline--danger-bg: var(--danger);
// Formula
--Formula-header-bgColor: #{$G10};
--Formula-funcItem-bgColor-onActive: #{$light};
--InputFormula-code-bgColor: #{$G10};
}

View File

@ -59,6 +59,9 @@ $link-color: $info;
--DropDown-menu-bg: var(--background);
--Drawer-header-bg: var(--background);
--Fieldset-legend-bgColor: var(--background);
--Form-item-fontSize: var(--fontSizeBase);
--Form-item-fontColor: var(--body-color);
--Form-input-addOnBg: var(--Form-input-bg);
--Form-input-bg: #3c3c3c;
--Form-input-color: var(--text-color);

View File

@ -96,18 +96,6 @@
}
}
.#{$ns}DropDown {
.#{$ns}DropDown-menu {
border: none;
> li {
color: #{$G2};
}
> li.is-disabled {
color: #{$G6};
}
}
}
.#{$ns}Toast {
.#{$ns}Toast-icon {
margin-right: #{px2rem(8px)};
@ -338,17 +326,10 @@
padding-left: 0;
}
.#{$ns}Form-label {
font-size: #{$T3};
color: #{$G4};
}
.#{$ns}Form-item {
&--inline {
.#{$ns}Form-label {
margin-right: #{px2rem(16px)};
font-size: #{$T3};
color: #{$G4};
}
}
}

View File

@ -1,4 +1,5 @@
import {observer} from 'mobx-react';
import {getEnv} from 'mobx-state-tree';
import React from 'react';
import Alert from './components/Alert2';
import Spinner from './components/Spinner';
@ -39,10 +40,18 @@ export class RootRenderer extends React.Component<RootRendererProps> {
'handleDialogConfirm',
'handleDialogClose',
'handleDrawerConfirm',
'handleDrawerClose'
'handleDrawerClose',
'handlePageVisibilityChange'
]);
}
componentDidMount() {
document.addEventListener(
'visibilitychange',
this.handlePageVisibilityChange
);
}
componentDidUpdate(prevProps: RootRendererProps) {
const props = this.props;
@ -61,6 +70,23 @@ export class RootRenderer extends React.Component<RootRendererProps> {
componentWillUnmount() {
this.props.rootStore.removeStore(this.store);
document.removeEventListener(
'visibilitychange',
this.handlePageVisibilityChange
);
}
handlePageVisibilityChange() {
const env = this.props.env;
if (document.visibilityState === 'hidden') {
env?.tracker({
eventType: 'pageHidden'
});
} else if (document.visibilityState === 'visible') {
env?.tracker({
eventType: 'pageVisible'
});
}
}
handleAction(

View File

@ -9,6 +9,7 @@ import {ButtonToolbarSchema} from './renderers/Form/ButtonToolbar';
import {CardSchema} from './renderers/Card';
import {CardsSchema} from './renderers/Cards';
import {FormSchema} from './renderers/Form';
import {CalendarSchema} from './renderers/Calendar';
import {CarouselSchema} from './renderers/Carousel';
import {ChartSchema} from './renderers/Chart';
import {CollapseSchema} from './renderers/Collapse';
@ -132,6 +133,7 @@ export type SchemaType =
| 'cards'
| 'carousel'
| 'chart'
| 'calendar'
| 'collapse'
| 'collapse-group'
| 'color'
@ -340,6 +342,7 @@ export type SchemaObject =
| AvatarSchema
| ButtonGroupSchema
| ButtonToolbarSchema
| CalendarSchema
| CardSchema
| CardsSchema
| CarouselSchema

View File

@ -5,6 +5,7 @@ import LazyComponent from './components/LazyComponent';
import {
filterSchema,
loadRenderer,
RendererComponent,
RendererConfig,
RendererEnv,
RendererProps,
@ -12,9 +13,12 @@ import {
} from './factory';
import {asFormItem} from './renderers/Form/Item';
import {renderChild, renderChildren} from './Root';
import {IScopedContext, ScopedContext} from './Scoped';
import {Schema, SchemaNode} from './types';
import {DebugWrapper, enableAMISDebug} from './utils/debug';
import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents} from './utils/helper';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {RendererEvent} from './utils/renderer-event';
import {SimpleMap} from './utils/SimpleMap';
interface SchemaRendererProps extends Partial<RendererProps> {
@ -23,6 +27,10 @@ interface SchemaRendererProps extends Partial<RendererProps> {
env: RendererEnv;
}
interface BroadcastCmptProps extends RendererProps {
component: RendererComponent;
}
const defaultOmitList = [
'type',
'name',
@ -50,6 +58,64 @@ const defaultOmitList = [
const componentCache: SimpleMap = new SimpleMap();
class BroadcastCmpt extends React.Component<BroadcastCmptProps> {
ref: any;
unbindEvent: (() => void) | undefined = undefined;
static contextType = ScopedContext;
constructor(props: BroadcastCmptProps, context: IScopedContext) {
super(props);
this.triggerEvent = this.triggerEvent.bind(this);
}
componentDidMount() {
const {env} = this.props;
this.unbindEvent = env.bindEvent(this.ref);
}
componentWillUnmount() {
this.unbindEvent?.();
}
getWrappedInstance() {
return this.ref;
}
async triggerEvent(
e: React.MouseEvent<any>,
data: any
): Promise<RendererEvent<any> | undefined> {
return await this.props.env.dispatchEvent(e, this.ref, this.context, data);
}
@autobind
childRef(ref: any) {
while (ref && ref.getWrappedInstance) {
ref = ref.getWrappedInstance();
}
this.ref = ref;
}
render() {
const {component: Component, ...rest} = this.props;
const isClassComponent = Component.prototype?.isReactComponent;
// 函数组件不支持 ref https://reactjs.org/docs/refs-and-the-dom.html#refs-and-function-components
return isClassComponent ? (
<Component
ref={this.childRef}
{...rest}
dispatchEvent={this.triggerEvent}
/>
) : (
<Component {...rest} dispatchEvent={this.triggerEvent} />
);
}
}
export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
static displayName: string = 'Renderer';
@ -300,7 +366,6 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
...restSchema
} = schema;
const Component = renderer.component;
// 原来表单项的 visible: false 和 hidden: true 表单项的值和验证是有效的
// 而 visibleOn 和 hiddenOn 是无效的,
// 这个本来就是个bug但是已经被广泛使用了
@ -315,8 +380,8 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
return null;
}
return (
<Component
const component = (
<BroadcastCmpt
{...theme.getRendererConfig(renderer.name)}
{...restSchema}
{...chainEvents(rest, restSchema)}
@ -329,8 +394,15 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
$schema={{...schema, ...exprProps}}
ref={this.refFn}
render={this.renderChild}
component={Component}
/>
);
return enableAMISDebug ? (
<DebugWrapper renderer={renderer}>{component}</DebugWrapper>
) : (
component
);
}
}

View File

@ -8,7 +8,15 @@ import find from 'lodash/find';
import hoistNonReactStatic from 'hoist-non-react-statics';
import {dataMapping} from './utils/tpl-builtin';
import {RendererEnv, RendererProps} from './factory';
import {noop, autobind, qsstringify, qsparse} from './utils/helper';
import {
noop,
autobind,
qsstringify,
qsparse,
createObject,
findTree,
TreeItem
} from './utils/helper';
import {RendererData, Action} from './types';
export interface ScopedComponentType extends React.Component<RendererProps> {
@ -29,9 +37,11 @@ export interface ScopedComponentType extends React.Component<RendererProps> {
export interface IScopedContext {
parent?: AliasIScopedContext;
children?: AliasIScopedContext[];
registerComponent: (component: ScopedComponentType) => void;
unRegisterComponent: (component: ScopedComponentType) => void;
getComponentByName: (name: string) => ScopedComponentType;
getComponentById: (id: string) => ScopedComponentType | undefined;
getComponents: () => Array<ScopedComponentType>;
reload: (target: string, ctx: RendererData) => void;
send: (target: string, ctx: RendererData) => void;
@ -46,8 +56,7 @@ function createScopedTools(
env?: RendererEnv
): IScopedContext {
const components: Array<ScopedComponentType> = [];
return {
const self = {
parent,
registerComponent(component: ScopedComponentType) {
// 不要把自己注册在自己的 Scoped 上,自己的 Scoped 是给子节点们注册的。
@ -80,7 +89,7 @@ function createScopedTools(
return paths.reduce((scope, name, idx) => {
if (scope && scope.getComponentByName) {
const result = scope.getComponentByName(name);
const result: ScopedComponentType = scope.getComponentByName(name);
return result && idx < len - 1 ? result.context : result;
}
@ -96,6 +105,27 @@ function createScopedTools(
return resolved || (parent && parent.getComponentByName(name));
},
getComponentById(id: string) {
let root: AliasIScopedContext = this;
// 找到顶端scoped
while (root.parent) {
root = root.parent;
}
// 向下查找
let component = undefined;
findTree([root], (item: TreeItem) =>
item.getComponents().find((cmpt: ScopedComponentType) => {
if (cmpt.props.id === id) {
component = cmpt;
return true;
}
return false;
})
) as ScopedComponentType | undefined;
return component;
},
getComponents() {
return components.concat();
},
@ -208,6 +238,17 @@ function createScopedTools(
}
}
};
if (!parent) {
return self;
}
!parent.children && (parent.children = []);
// 把孩子带上
parent.children!.push(self);
return self;
}
function closeDialog(component: ScopedComponentType) {
@ -257,6 +298,7 @@ export function HocScoped<
context,
this.props.env
);
const scopeRef = props.scopeRef;
scopeRef && scopeRef(this.scoped);
}

138
src/actions/Action.ts Normal file
View File

@ -0,0 +1,138 @@
import {extendObject} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpression} from '../utils/tpl';
import {dataMapping} from '../utils/tpl-builtin';
// 逻辑动作类型支持并行、排他switch、循环支持continue和break
type LogicActionType = 'parallel' | 'switch' | 'loop' | 'continue' | 'break';
// 循环动作执行状态
export enum LoopStatus {
NORMAL,
BREAK,
CONTINUE
}
// 监听器动作定义
export interface ListenerAction {
actionType: 'broadcast' | LogicActionType | 'custom' | string; // 动作类型 逻辑动作|自定义(脚本支撑)|reload|url|ajax|dialog|drawer 其他扩充的组件动作
eventName?: string; // 事件名称actionType: broadcast
description?: string; // 事件描述actionType: broadcast
componentId?: string; // 组件ID用于直接执行指定组件的动作
args?: any; // 参数,可以配置数据映射
preventDefault?: boolean; // 阻止原有组件的动作行为
stopPropagation?: boolean; // 阻止后续的事件处理器执行
execOn?: string; // 执行条件
script?: string; // 自定义JSactionType: custom
[propName: string]: any; // 扩展各种Action
}
export interface LogicAction extends ListenerAction {
children?: ListenerAction[]; // 子动作
}
export interface ListenerContext {
[propName: string]: any;
}
// Action 基础接口
export interface Action {
// 运行这个 Action每个类型的 Action 都只有一个实例run 函数是个可重入的函数
run: (
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>,
mergeData?: any // 有些Action内部需要通过上下文数据处理专有逻辑这里的数据是事件数据+渲染器数据
) => Promise<void>;
}
// 存储 Action 和类型的映射关系,用于后续查找
const ActionTypeMap: {[key: string]: Action} = {};
// 注册 Action
export const registerAction = (type: string, action: Action) => {
ActionTypeMap[type] = action;
};
// 通过类型获取 Action 实例
export const getActionByType = (type: string) => {
return ActionTypeMap[type];
};
export const runActions = async (
actions: ListenerAction | ListenerAction[],
renderer: ListenerContext,
event: any
) => {
if (!Array.isArray(actions)) {
actions = [actions];
}
for (const actionConfig of actions) {
let actionInstrance = getActionByType(actionConfig.actionType);
// 如果存在指定组件ID说明是组件专有动作
if (actionConfig.componentId) {
actionInstrance = getActionByType('component');
} else if (
actionConfig.actionType === 'url' ||
actionConfig.actionType === 'link' ||
actionConfig.actionType === 'jump'
) {
// 打开页面动作
actionInstrance = getActionByType('openpage');
}
// 找不到就通过组件专有动作完成
if (!actionInstrance) {
actionInstrance = getActionByType('component');
}
// 这些节点的子节点运行逻辑由节点内部实现
await runAction(actionInstrance, actionConfig, renderer, event);
if (event.stoped) {
break;
}
}
};
// 执行动作,与原有动作处理打通
export const runAction = async (
actionInstrance: Action,
actionConfig: ListenerAction,
renderer: ListenerContext,
event: any
) => {
// 用户可能需要用到事件数据和当前域的数据因此merge事件数据和当前渲染器数据
// 需要保持渲染器数据链完整
const mergeData = extendObject(renderer.props.data, {
event
});
if (actionConfig.execOn && !evalExpression(actionConfig.execOn, mergeData)) {
return;
}
// 修正参数,处理数据映射
let args = event.data;
if (actionConfig.args) {
args = dataMapping(actionConfig.args, mergeData);
}
await actionInstrance.run(
{
...actionConfig,
args
},
renderer,
event,
mergeData
);
// 阻止原有动作执行
actionConfig.preventDefault && event.preventDefault();
// 阻止后续动作执行
actionConfig.stopPropagation && event.stopPropagation();
};

58
src/actions/AjaxAction.ts Normal file
View File

@ -0,0 +1,58 @@
import {IRootStore} from '../store/root';
import {isVisible} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class AjaxAction
* @implements {Action}
*/
export class AjaxAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const store = renderer.props.store;
store.setCurrentAction(action);
store
.saveRemote(action.api as string, action.args, {
successMessage: action.messages && action.messages.success,
errorMessage: action.messages && action.messages.failed
})
.then(async () => {
if (action.feedback && isVisible(action.feedback, store.data)) {
await this.openFeedback(action.feedback, store);
}
const redirect = action.redirect && filter(action.redirect, store.data);
redirect && renderer.env.jumpTo(redirect, action);
})
.catch(() => {});
}
openFeedback(dialog: any, store: IRootStore) {
return new Promise(resolve => {
store.setCurrentAction({
type: 'button',
actionType: 'dialog',
dialog: dialog
});
store.openDialog(store.data, undefined, confirmed => {
resolve(confirmed);
});
});
}
}
registerAction('ajax', new AjaxAction());

View File

@ -0,0 +1,27 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
LoopStatus,
registerAction
} from './Action';
/**
* breach
*
* @export
* @class BreakAction
* @implements {Action}
*/
export class BreakAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
renderer.loopStatus = LoopStatus.BREAK;
}
}
registerAction('break', new BreakAction());

View File

@ -0,0 +1,41 @@
import {createObject} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
* broadcast
*
* @export
* @class BroadcastAction
* @implements {Action}
*/
export class BroadcastAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (!action.eventName) {
console.warn('eventName 未定义,请定义事件名称');
return;
}
// 作为一个新的事件需要把广播动作的args参数追加到事件数据中
event.setData(createObject(event.data, action.args));
// 直接触发对应的动作
return await event.context.env.dispatchEvent(
action.eventName,
renderer,
action.args,
event
);
}
}
registerAction('broadcast', new BroadcastAction());

36
src/actions/CmptAction.ts Normal file
View File

@ -0,0 +1,36 @@
import {RendererEvent} from '../utils/renderer-event';
import {dataMapping} from '../utils/tpl-builtin';
import {
Action,
ListenerAction,
ListenerContext,
LoopStatus,
registerAction
} from './Action';
/**
*
*
* @export
* @class CmptAction
* @implements {Action}
*/
export class CmptAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
// 根据唯一ID查找指定组件
const component =
renderer.props.$schema.id !== action.componentId
? event.context.scoped?.getComponentById(action.componentId)
: renderer;
// 执行组件动作
(await component.props.onAction?.(event, action, action.args)) ||
component.doAction?.(action, action.args);
}
}
registerAction('component', new CmptAction());

View File

@ -0,0 +1,27 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
LoopStatus,
registerAction
} from './Action';
/**
* continue
*
* @export
* @class ContinueAction
* @implements {Action}
*/
export class ContinueAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
renderer.loopStatus = LoopStatus.CONTINUE;
}
}
registerAction('continue', new ContinueAction());

41
src/actions/CopyAction.ts Normal file
View File

@ -0,0 +1,41 @@
import {RendererEvent} from '../utils/renderer-event';
import {dataMapping} from '../utils/tpl-builtin';
import {filter} from '../utils/tpl';
import pick from 'lodash/pick';
import mapValues from 'lodash/mapValues';
import qs from 'qs';
import {
Action,
ListenerAction,
ListenerContext,
LoopStatus,
registerAction
} from './Action';
import {isVisible} from '../utils/helper';
/**
*
*
* @export
* @class CopyAction
* @implements {Action}
*/
export class CopyAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
debugger;
if (action.content || action.copy) {
renderer.props.env.copy?.(
filter(action.content || action.copy, action.args, '| raw'),
{
format: action.copyFormat
}
);
}
}
}
registerAction('copy', new CopyAction());

View File

@ -0,0 +1,46 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
LoopStatus,
registerAction
} from './Action';
/**
* JS脚本
*
* @export
* @class CustomAction
* @implements {Action}
*/
export class CustomAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
// 执行自定义编排脚本
let scriptFunc = action.script;
if (typeof scriptFunc === 'string') {
scriptFunc = new Function(
'context',
'doAction',
'event',
scriptFunc
) as any;
}
// 外部可以直接调用doAction来完成动作调用
// 可以通过上下文直接编排动作调用通过event来进行动作干预
await (scriptFunc as any)?.call(
null,
renderer,
renderer.doAction.bind(renderer),
event
);
}
}
registerAction('custom', new CustomAction());

View File

@ -0,0 +1,28 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class DialogAction
* @implements {Action}
*/
export class DialogAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const store = renderer.props.store;
store.setCurrentAction(action);
store.openDialog(action.args);
}
}
registerAction('dialog', new DialogAction());

View File

@ -0,0 +1,28 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class DrawerAction
* @implements {Action}
*/
export class DrawerAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const store = renderer.props.store;
store.setCurrentAction(action);
store.openDrawer(action.args);
}
}
registerAction('drawer', new DrawerAction());

View File

@ -0,0 +1,38 @@
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import pick from 'lodash/pick';
import mapValues from 'lodash/mapValues';
import qs from 'qs';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class EmailAction
* @implements {Action}
*/
export class EmailAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
const mailTo = filter(action.to, action.args);
const mailInfo = mapValues(
pick(action, 'to', 'cc', 'bcc', 'subject', 'body'),
val => filter(val, action.args)
);
const mailStr = qs.stringify(mailInfo);
const mailto = `mailto:${mailTo}?${mailStr}`;
window.open(mailto);
}
}
registerAction('email', new EmailAction());

77
src/actions/LoopAction.ts Normal file
View File

@ -0,0 +1,77 @@
import {RendererEvent} from '../utils/renderer-event';
import {createObject} from '../utils/helper';
import {
Action,
ListenerContext,
LogicAction,
LoopStatus,
registerAction,
runAction,
runActions
} from './Action';
import {resolveVariable} from '../utils/tpl-builtin';
/**
*
*
* @export
* @class LoopAction
* @implements {Action}
*/
export class LoopAction implements Action {
async run(
action: LogicAction,
renderer: ListenerContext,
event: RendererEvent<any>,
mergeData: any
) {
if (typeof action.loopName !== 'string') {
console.warn('loopName 必须是字符串类型');
return;
}
const loopData = resolveVariable(action.loopName, mergeData) || [];
// 必须是数组
if (!loopData) {
console.warn(`没有找到数据 ${action.loopName}`);
} else if (!Array.isArray(loopData)) {
console.warn(`${action.loopName} 数据不是数组`);
} else if (action.children?.length) {
// 暂存一下
const protoData = event.data;
for (const data of loopData) {
renderer.loopStatus = LoopStatus.NORMAL;
// 追加逻辑处理中的数据,事件数据优先,用完还要还原
event.setData(createObject(event.data, data));
for (const subAction of action.children) {
// @ts-ignore
if (renderer.loopStatus === LoopStatus.CONTINUE) {
continue;
}
await runActions(subAction, renderer, event);
// @ts-ignore
if (renderer.loopStatus === LoopStatus.BREAK || event.stoped) {
// 还原事件数据
event.setData(protoData);
break;
}
}
if (event.stoped) {
// 还原事件数据
event.setData(protoData);
break;
}
}
renderer.loopStatus = LoopStatus.NORMAL;
event.setData(protoData);
}
}
}
registerAction('loop', new LoopAction());

View File

@ -0,0 +1,39 @@
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
/**
*
*
* @export
* @class OpenPageAction
* @implements {Action}
*/
export class OpenPageAction implements Action {
async run(
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (!renderer.props.env?.jumpTo) {
throw new Error('env.jumpTo is required!');
}
renderer.props.env.jumpTo(
filter(
(action.to || action.url || action.link) as string,
action.args,
'| raw'
),
action,
action.args
);
}
}
registerAction('openpage', new OpenPageAction());

View File

@ -0,0 +1,26 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
ListenerContext,
LogicAction,
registerAction,
runActions
} from './Action';
export class ParallelAction implements Action {
async run(
action: LogicAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (action.children && action.children.length) {
const childActions = action.children.map((child: LogicAction) => {
// 并行动作互不干扰,但不管哪个存在干预都对后续动作生效
return runActions(child, renderer, event);
});
await Promise.all(childActions);
}
}
}
registerAction('parallel', new ParallelAction());

View File

@ -0,0 +1,35 @@
import {RendererEvent} from '../utils/renderer-event';
import {evalExpression} from '../utils/tpl';
import {
Action,
ListenerContext,
LogicAction,
registerAction,
runActions
} from './Action';
/**
*
*/
export class SwitchAction implements Action {
async run(
action: LogicAction,
renderer: ListenerContext,
event: RendererEvent<any>,
mergeData: any
) {
for (const branch of action.children || []) {
if (!branch.expression) {
continue;
}
if (evalExpression(branch.expression, mergeData)) {
await runActions(branch, renderer, event);
// 去掉runAllMatch这里只做排他多个可以直接通过execOn
break;
}
}
}
}
registerAction('switch', new SwitchAction());

20
src/actions/index.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* @file
*/
import './LoopAction';
import './BreakAction';
import './ContinueAction';
import './SwitchAction';
import './ParallelAction';
import './CustomAction';
import './BroadcastAction';
import './CmptAction';
import './AjaxAction';
import './CopyAction';
import './DialogAction';
import './DrawerAction';
import './EmailAction';
import './OpenPageAction';
export * from './Action';

View File

@ -25,10 +25,29 @@ export interface CalendarMobileProps extends ThemeProps, LocaleProps {
embed?: boolean;
viewMode?: 'days' | 'months' | 'years' | 'time' | 'quarters';
close?: () => void;
confirm?: () => void;
confirm?: (startDate?: any, endTime?: any) => void;
onChange?: (data: any, callback?: () => void) => void;
footerExtra?: JSX.Element | null;
showViewMode?: 'years' | 'months';
isDatePicker?: boolean;
timeConstraints?: {
hours?: {
min: number;
max: number;
step: number;
};
minutes?: {
min: number;
max: number;
step: number;
};
seconds: {
min: number;
max: number;
step: number;
};
};
defaultDate?: moment.Moment;
}
export interface CalendarMobileState {
@ -39,6 +58,8 @@ export interface CalendarMobileState {
showToast: boolean;
isScrollToBottom: boolean;
dateTime: any;
minDate?: moment.Moment;
maxDate?: moment.Moment;
}
export class CalendarMobile extends React.Component<
@ -50,10 +71,8 @@ export class CalendarMobile extends React.Component<
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'),
static defaultProps: Pick<CalendarMobileProps, 'showViewMode'> = {
showViewMode: 'months'
};
constructor(props: CalendarMobileProps) {
@ -62,18 +81,63 @@ export class CalendarMobile extends React.Component<
this.mobileBody = React.createRef();
this.mobileHeader = React.createRef();
const {startDate, endDate, viewMode} = this.props;
const {startDate, endDate, defaultDate, minDate, maxDate} = this.props;
const dateRange = this.getDateRange(minDate, maxDate, defaultDate);
this.state = {
minDate: dateRange.minDate,
maxDate: dateRange.maxDate,
startDate,
endDate,
showToast: false,
currentDate: moment(),
currentDate: dateRange.currentDate,
isScrollToBottom: false,
dateTime: endDate ? [endDate.hour(), endDate.minute()] : [0, 0]
};
}
getDateRange(minDate?: moment.Moment, maxDate?: moment.Moment, defaultDate?: moment.Moment) {
!moment.isMoment(minDate) || !minDate.isValid() && (minDate = undefined);
!moment.isMoment(maxDate) || !maxDate.isValid() && (maxDate = undefined);
let currentDate = defaultDate || moment();
let dateRange: {
minDate: moment.Moment,
maxDate: moment.Moment
} = {
minDate: currentDate.clone().subtract(1, 'year').startOf('months'),
maxDate: currentDate.clone().add(1, 'year').endOf('months')
};
if (minDate && maxDate) {
dateRange = {
minDate,
maxDate
};
}
else if (minDate && !maxDate) {
dateRange = {
minDate,
maxDate: moment(minDate).add(2, 'year')
};
currentDate = minDate.clone();
}
else if (!minDate && maxDate) {
dateRange = {
minDate: moment(maxDate).subtract(2, 'year'),
maxDate
};
currentDate = maxDate.clone();
}
if (!currentDate.isBetween(dateRange.minDate, dateRange.maxDate, 'days', '[]')) {
currentDate = dateRange.minDate.clone();
}
return {
...dateRange,
currentDate
};
}
componentDidMount() {
this.initMonths();
}
@ -82,7 +146,16 @@ export class CalendarMobile extends React.Component<
const props = this.props;
if (prevProps.minDate !== props.minDate || prevProps.maxDate !== props.maxDate) {
this.initMonths();
const currentDate = this.state.currentDate;
const dateRange = this.getDateRange(props.minDate, props.maxDate, moment(currentDate));
this.setState(
{
minDate: dateRange.minDate,
maxDate: dateRange.maxDate,
currentDate: dateRange.currentDate,
},
() => this.initMonths()
);
}
}
@ -102,14 +175,19 @@ export class CalendarMobile extends React.Component<
this.setState({
monthHeights
});
this.scollToDate(moment());
const defaultDate = this.props.defaultDate || this.state.currentDate;
this.scollToDate(defaultDate ? moment(defaultDate) : moment());
}
}
scollToDate(date: moment.Moment) {
const {minDate, showViewMode} = this.props;
const {showViewMode} = this.props;
const {minDate} = this.state;
const index = date.diff(minDate, showViewMode);
const currentEl = this.mobileBody.current.children[index];
if (!currentEl) {
return;
}
const header = this.mobileHeader.current;
this.mobileBody.current.scrollBy(0, currentEl.offsetTop - this.mobileBody.current.scrollTop - header.clientHeight);
}
@ -118,7 +196,7 @@ export class CalendarMobile extends React.Component<
onMobileBodyScroll(e: any) {
const {showViewMode} = this.props;
const {monthHeights} = this.state;
let minDate = this.props.minDate?.clone();
let minDate = this.state.minDate?.clone();
if (!this.mobileBody?.current || !monthHeights || !minDate) {
return;
}
@ -146,8 +224,7 @@ export class CalendarMobile extends React.Component<
if (!this.state.currentDate) {
return;
}
const {minDate} = this.props;
let {currentDate} = this.state;
let {currentDate, minDate} = this.state;
currentDate = currentDate.clone().subtract(1, 'years');
if (minDate && currentDate.isBefore(minDate)) {
currentDate = minDate;
@ -163,8 +240,7 @@ export class CalendarMobile extends React.Component<
if (!this.state.currentDate) {
return;
}
const {maxDate} = this.props;
let {currentDate} = this.state;
let {currentDate, maxDate} = this.state;
currentDate = currentDate.clone().add(1, 'years');
if (maxDate && currentDate.isAfter(maxDate)) {
currentDate = maxDate;
@ -201,7 +277,7 @@ export class CalendarMobile extends React.Component<
getRenderProps(props: any, currentDate: moment.Moment) {
let {startDate, endDate} = this.state;
const {translate: __, viewMode} = this.props;
const {translate: __, viewMode, isDatePicker} = this.props;
const precision = viewMode === 'time' ? 'hours' : viewMode || 'day';
let footerText = '';
@ -233,6 +309,10 @@ export class CalendarMobile extends React.Component<
props.className += ' rdtOldNone';
}
if (isDatePicker) {
footerText = '';
}
const rdtDisabled = props.className.indexOf('rdtDisabled') > -1;
return {
@ -252,8 +332,8 @@ export class CalendarMobile extends React.Component<
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})
startDate: endDate ? startDate : startDate?.clone().set({hour: newTime[0], minute: newTime[1], second: newTime[2] || 0}),
endDate: !endDate ? endDate : endDate?.clone().set({hour: newTime[0], minute: newTime[1], second: newTime[2] || 0})
};
this.setState(obj, () => {
onChange && onChange(this.state);
@ -263,8 +343,7 @@ export class CalendarMobile extends React.Component<
@autobind
checkIsValidDate(currentDate: moment.Moment) {
const {minDate, maxDate} = this.props;
let {startDate, endDate} = this.state;
const {startDate, endDate, minDate, maxDate} = this.state;
let {minDuration, maxDuration, viewMode} = this.props;
const precision = viewMode === 'time' ? 'hours' : viewMode || 'day';
@ -340,8 +419,8 @@ export class CalendarMobile extends React.Component<
@autobind
handleMobileChange(newValue: moment.Moment) {
const {embed, minDuration, maxDuration, confirm, onChange, viewMode, minDate, maxDate} = this.props;
const {startDate, endDate, dateTime} = this.state;
const {embed, minDuration, maxDuration, confirm, onChange, viewMode, isDatePicker} = this.props;
const {startDate, endDate, dateTime, minDate, maxDate} = this.state;
const precision = viewMode === 'time' ? 'hours' : viewMode || 'day';
if (minDate && newValue && newValue.isBefore(minDate, 'second')) {
@ -353,6 +432,7 @@ export class CalendarMobile extends React.Component<
}
if (
!isDatePicker &&
startDate &&
!endDate &&
newValue.isSameOrAfter(startDate) &&
@ -361,17 +441,17 @@ export class CalendarMobile extends React.Component<
) {
return this.setState(
{
endDate: newValue.clone().endOf(precision).set({hour: dateTime[0], minute: dateTime[1], second: 0})
endDate: newValue.clone().endOf(precision).set({hour: dateTime[0], minute: dateTime[1], second: dateTime[2] || 0})
},
() => {
onChange && onChange(this.state, () => embed && confirm && confirm());
onChange && onChange(this.state, () => embed && confirm && confirm(startDate, endDate));
}
);
}
this.setState(
{
startDate: newValue.clone().startOf(precision).set({hour: dateTime[0], minute: dateTime[1], second: 0}),
startDate: newValue.clone().startOf(precision).set({hour: dateTime[0], minute: dateTime[1], second: dateTime[2] || 0}),
endDate: undefined
},
() => {
@ -389,17 +469,23 @@ export class CalendarMobile extends React.Component<
inputFormat,
locale,
viewMode = 'days',
close
close,
defaultDate,
showViewMode
} = this.props;
const __ = this.props.translate;
const {minDate, maxDate, showViewMode} = this.props;
const {minDate, maxDate} = this.state;
if (!minDate || !maxDate) {
return;
}
let calendarDates: moment.Moment[] = [];
for(let minDateClone = minDate.clone(); minDateClone.isSameOrBefore(maxDate); minDateClone.add(1, showViewMode)) {
calendarDates.push(minDateClone.clone());
let date = minDateClone.clone();
if (defaultDate) {
date = moment(defaultDate).set({year: date.get('year'), month: date.get('month')});
}
calendarDates.push(date);
}
return (
@ -455,7 +541,10 @@ export class CalendarMobile extends React.Component<
classnames: cx,
timeFormat,
locale,
close
close,
timeConstraints,
defaultDate,
isDatePicker
} = this.props;
const __ = this.props.translate;
@ -464,10 +553,11 @@ export class CalendarMobile extends React.Component<
return (
<div className={cx('CalendarMobile-time')}>
<div className={cx('CalendarMobile-time-title')}>
{startDate && endDate ? __('Calendar.endPick') : __('Calendar.startPick')}
{isDatePicker ? __('Date.titleTime') : startDate && endDate ? __('Calendar.endPick') : __('Calendar.startPick')}
</div>
<Calendar
className={cx('CalendarMobile-time-calendar')}
value={defaultDate}
onChange={this.handleTimeChange}
requiredConfirm={false}
timeFormat={timeFormat}
@ -477,7 +567,9 @@ export class CalendarMobile extends React.Component<
locale={locale}
useMobileUI={true}
showToolbar={false}
viewDate={moment().set({hour: dateTime[0], minute: dateTime[1], second: 0})}
viewDate={moment().set({hour: dateTime[0], minute: dateTime[1], second: dateTime[2] || 0})}
timeConstraints={timeConstraints}
isValidDate={this.checkIsValidDate}
/>
</div>
);
@ -489,16 +581,16 @@ export class CalendarMobile extends React.Component<
className,
classnames: cx,
embed,
close,
confirm,
footerExtra,
timeFormat,
minDate,
maxDate,
showViewMode
showViewMode,
isDatePicker
} = this.props;
const __ = this.props.translate;
const {startDate, endDate, currentDate, showToast, isScrollToBottom} = this.state;
const {startDate, endDate, currentDate, showToast, isScrollToBottom, minDate, maxDate} = this.state;
let dateNow = currentDate
? currentDate.format(__(`Calendar.${showViewMode === 'months' ? 'yearmonth' : 'year'}`))
: moment().format(__(`Calendar.${showViewMode === 'months' ? 'yearmonth' : 'year'}`));
@ -535,9 +627,12 @@ export class CalendarMobile extends React.Component<
</div>
{confirm && !embed && <a
className={cx('Button', 'Button--primary', 'date-range-confirm', {
'is-disabled': !startDate || !endDate
'is-disabled': !startDate || !(endDate || isDatePicker)
})}
onClick={confirm}
onClick={() => {
confirm(startDate, endDate);
close && close();
}}
>
{__('confirm')}
</a>}

View File

@ -114,7 +114,6 @@ export class Cascader extends React.Component<CascaderProps, CascaderState> {
});
}
@autobind
handleTabSelect(index: number) {
const tabs = this.state.tabs.slice(0, index + 1);
@ -543,14 +542,14 @@ export class Cascader extends React.Component<CascaderProps, CascaderState> {
<div className={cx(`Cascader-btnGroup`)}>
<Button
className={cx(`Cascader-btnCancel`)}
level="default"
level="text"
onClick={onClose}
>
{__('cancel')}
</Button>
<Button
className={cx(`Cascader-btnConfirm`)}
level="primary"
level="text"
onClick={this.confirm}
>
{__('confirm')}

315
src/components/CityArea.tsx Normal file
View File

@ -0,0 +1,315 @@
/**
* @file
*/
import React, {useEffect, useState, memo} from 'react';
import Picker from './Picker';
import ResultBox from './ResultBox';
import {useSetState, useUpdateEffect} from '../hooks';
import {localeable, LocaleProps} from '../locale';
import {themeable, ThemeProps} from '../theme';
import {uncontrollable} from 'uncontrollable';
import PopUp from './PopUp';
import {PickerObjectOption} from './PickerColumn';
export type AreaColumnOption = {
text: string;
value: number;
};
export interface AreaProps extends LocaleProps, ThemeProps {
value: any;
/**
*
*/
allowCity?: boolean;
/**
*
*/
allowDistrict?: boolean;
/**
*
*/
allowStreet?: boolean;
/**
* code
*/
extractValue?: boolean;
/**
*
*/
joinValues?: boolean;
/**
*
*/
delimiter?: string;
/**
*
*/
disabled?: boolean;
useMobileUI?: boolean;
onChange: (value: any) => void;
/** 点击完成按钮时触发 */
onConfirm?: (result: AreaColumnOption[], index: number) => void;
/** 点击取消按钮时触发 */
onCancel?: (...args: unknown[]) => void;
popOverContainer?: any;
}
/**
*
*/
type district = {
[propName: number]: {
[propName: number]: Array<number>;
};
};
interface DbState {
province: number[];
district: district;
[key: number]: string;
city: {
[key: number]: number[];
};
}
interface StateObj {
columns: {options: Array<AreaColumnOption>}[];
}
const CityArea = memo<AreaProps>(props => {
const {
joinValues = true,
extractValue = true,
delimiter = ',',
allowCity = true,
allowDistrict = true,
allowStreet = false,
// 默认北京东城区
value = 110101,
classnames: cx,
translate: __,
disabled = false,
popOverContainer,
useMobileUI
} = props;
const [values, setValues] = useState<Array<number>>([]);
const [street, setStreet] = useState('');
const [confirmValues, setConfirmValues] =
useState<Array<PickerObjectOption>>();
const [db, updateDb] = useSetState<DbState>();
const [state, updateState] = useSetState<StateObj>({
columns: []
});
const [isOpened, setIsOpened] = useState(false);
const onChange = (columnValues: Array<number>, columnIndex: number) => {
// 清空后面的值
while (columnValues[columnIndex++]) {
columnValues[columnIndex++] = -1;
}
let [provience, city, district] = columnValues;
if (city === -1) {
city = db.city?.[provience]?.[0];
}
if (district === -1) {
district = db.district?.[provience]?.[city]?.[0];
}
let tempValues = [provience, city, district];
if (!allowDistrict) {
tempValues.splice(2, 1);
}
if (!allowCity) {
tempValues.splice(1, 1);
}
setValues(tempValues);
};
const propsChange = () => {
const {onChange} = props;
const [province, city, district] = values;
const code =
allowDistrict && district
? district
: allowCity && city
? city
: province;
if (typeof extractValue === 'undefined' ? joinValues : extractValue) {
code
? onChange(
allowStreet && street
? [code, street].join(delimiter)
: String(code)
)
: onChange('');
} else {
onChange({
code,
province: db[province],
city: db[city],
district: db[district],
street
});
}
};
const onConfirm = () => {
const confirmValues = values.map((item: number) => ({
text: db[item],
value: item
}));
setConfirmValues(confirmValues);
propsChange();
setIsOpened(false);
};
const onCancel = () => {
setIsOpened(false);
if (props.onCancel) props.onCancel();
};
const getPropsValue = () => {
// 最后一项的值
let code =
(value && value.code) ||
(typeof value === 'number' && value) ||
(typeof value === 'string' && /(\d{6})/.test(value) && RegExp.$1);
const values: Array<number> = [];
if (code && db[code]) {
code = parseInt(code, 10);
let provinceCode = code - (code % 10000);
let cityCode = code - (code % 100);
if (db[provinceCode]) {
values[0] = provinceCode;
}
if (db[cityCode] && allowCity) {
values[1] = cityCode;
} else if (~db.city[provinceCode]?.indexOf(code) && allowCity) {
values[1] = code;
}
if (code % 100 && allowDistrict) {
values[2] = code;
}
setValues(values);
}
};
const updateColumns = () => {
if (!db) {
return;
}
let [provience, city, district] = values;
const provienceColumn = db.province.map((code: number) => {
return {text: db[code], value: code, disabled};
});
const cityColumn = city
? db.city[provience].map((code: number) => {
return {text: db[code], value: code, disabled};
})
: [];
const districtColumn =
city && district
? db.district[provience][city].map((code: number) => {
return {text: db[code], value: code, disabled};
})
: [];
const columns = [
{options: provienceColumn},
{options: cityColumn},
{options: districtColumn}
];
if (!allowDistrict || !allowCity) {
columns.splice(2, 1);
}
if (!allowCity) {
columns.splice(1, 1);
}
updateState({columns});
};
const loadDb = () => {
import('../renderers/Form/CityDB').then(db => {
updateDb({
...db.default,
province: db.province as any,
city: db.city,
district: db.district as district
});
});
};
useEffect(() => {
loadDb();
}, []);
useEffect(() => {
isOpened && db && getPropsValue();
}, [db, isOpened]);
useEffect(() => {
street && propsChange();
}, [street]);
useUpdateEffect(() => {
values.length && updateColumns();
}, [values]);
const result = confirmValues
?.filter(item => item?.value)
?.map(item => item.text)
.join(delimiter);
return (
<div className={cx(`CityArea`)}>
<ResultBox
className={cx('CityArea-Input', isOpened ? 'is-active' : '')}
allowInput={false}
result={result}
onResultChange={() => {}}
onResultClick={() => setIsOpened(!isOpened)}
placeholder={__('Condition.cond_placeholder')}
useMobileUI={useMobileUI}
></ResultBox>
{allowStreet && values[0] ? (
<input
className={cx('CityArea-Input')}
value={street}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setStreet(e.currentTarget.value)
}
placeholder={__('City.street')}
disabled={disabled}
/>
) : null}
<PopUp
className={cx(`CityArea-popup`)}
container={popOverContainer}
isShow={isOpened}
showConfirm
onConfirm={onConfirm}
onHide={onCancel}
>
<Picker
className={'CityArea-picker'}
columns={state.columns}
onChange={onChange as any}
showToolbar={false}
labelField="text"
itemHeight={40}
value={values}
classnames={props.classnames}
classPrefix={props.classPrefix}
/>
</PopUp>
</div>
);
});
export default themeable(
localeable(
uncontrollable(CityArea, {
value: 'onChange'
})
)
);

View File

@ -1,5 +1,5 @@
import React from 'react';
import 'codemirror/lib/codemirror.css';
// import 'codemirror/lib/codemirror.css';
import type CodeMirror from 'codemirror';
import {autobind} from '../utils/helper';
import {resizeSensor} from '../utils/resize-sensor';

View File

@ -182,7 +182,10 @@ export class Collapse extends React.Component<CollapseProps, CollapseState> {
expandIcon ? (
React.cloneElement(expandIcon, {
...expandIcon.props,
className: cx('Collapse-icon-tranform')
className: cx(
'Collapse-icon-tranform',
expandIcon.props?.className
)
})
) : (
<span className={cx('Collapse-arrow')} />

View File

@ -227,6 +227,7 @@ export class ColorControl extends React.PureComponent<
const __ = this.props.translate;
const isOpened = this.state.isOpened;
const isFocused = this.state.isFocused;
const mobileUI = useMobileUI && isMobile();
return (
<div
@ -261,6 +262,7 @@ export class ColorControl extends React.PureComponent<
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onClick={this.handleClick}
readOnly={mobileUI}
/>
{clearable && !disabled && value ? (
@ -273,7 +275,7 @@ export class ColorControl extends React.PureComponent<
<Icon icon="caret" className="icon" onClick={this.handleClick} />
</span>
{!(useMobileUI && isMobile()) && isOpened ? (
{!mobileUI && isOpened ? (
<Overlay
placement={placement || 'auto'}
target={() => findDOMNode(this)}
@ -320,9 +322,10 @@ export class ColorControl extends React.PureComponent<
</PopOver>
</Overlay>
) : null}
{useMobileUI && isMobile() && (
{mobileUI && (
<PopUp
className={cx(`${ns}ColorPicker-popup`)}
container={popOverContainer}
isShow={isOpened}
onHide={this.handleClick}
>

View File

@ -14,9 +14,10 @@ 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 'react-datetime/css/react-datetime.css';
import {localeable, LocaleProps, TranslateFn} from '../locale';
import {isMobile, ucFirst} from '../utils/helper';
import CalendarMobile from './CalendarMobile';
const availableShortcuts: {[propName: string]: any} = {
now: {
@ -288,8 +289,9 @@ export interface DateProps extends LocaleProps, ThemeProps {
scheduleClassNames?: Array<string>;
largeMode?: boolean;
onScheduleClick?: (scheduleData: any) => void;
useMobileUI?: boolean;
// 在移动端日期展示有多种形式一种是picker 滑动选择一种是日历展开选择mobileCalendarMode为calendar表示日历展开选择
mobileCalendarMode?: 'picker' | 'calendar';
// 下面那个千万不要写,写了就会导致 keyof DateProps 得到的结果是 string | number;
// [propName: string]: any;
@ -571,13 +573,45 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
schedules,
largeMode,
scheduleClassNames,
onScheduleClick
onScheduleClick,
mobileCalendarMode
} = this.props;
const __ = this.props.translate;
const isOpened = this.state.isOpened;
let date: moment.Moment | undefined = this.state.value;
const calendarMobile = (
<CalendarMobile
isDatePicker={true}
timeFormat={timeFormat}
inputFormat={inputFormat}
startDate={date}
defaultDate={date}
minDate={minDate}
maxDate={maxDate}
dateFormat={dateFormat}
embed={embed}
viewMode={viewMode}
close={this.close}
confirm={this.handleChange}
footerExtra={this.renderShortCuts(shortcuts)}
showViewMode={
viewMode === 'quarters' || viewMode === 'months' ? 'years' : 'months'
}
timeConstraints={timeConstraints}
/>
);
const CalendarMobileTitle = (
<div className={`${ns}CalendarMobile-title`}>
{__('Calendar.datepicker')}
</div>
);
const useCalendarMobile =
useMobileUI &&
isMobile() &&
['days', 'months', 'quarters'].indexOf(viewMode) > -1;
if (embed) {
let schedulesData: DateProps['schedules'] = undefined;
if (schedules && Array.isArray(schedules)) {
@ -628,6 +662,8 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
schedules={schedulesData}
largeMode={largeMode}
onScheduleClick={onScheduleClick}
embed={embed}
useMobileUI={useMobileUI}
/>
</div>
);
@ -644,7 +680,8 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
{
'is-disabled': disabled,
'is-focused': this.state.isFocused,
[`DatePicker--border${ucFirst(borderMode)}`]: borderMode
[`DatePicker--border${ucFirst(borderMode)}`]: borderMode,
'is-mobile': useMobileUI && isMobile()
},
className
)}
@ -703,36 +740,52 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
locale={locale}
minDate={minDate}
maxDate={maxDate}
useMobileUI={useMobileUI}
// utc={utc}
/>
</PopOver>
</Overlay>
) : null}
{useMobileUI && isMobile() ? (
<PopUp
className={cx(`${ns}DatePicker-popup`)}
isShow={isOpened}
onHide={this.handleClick}
>
{this.renderShortCuts(shortcuts)}
mobileCalendarMode === 'calendar' && useCalendarMobile ? (
<PopUp
isShow={isOpened}
className={cx(`${ns}CalendarMobile-pop`)}
onHide={this.close}
header={CalendarMobileTitle}
>
{calendarMobile}
</PopUp>
) : (
<PopUp
className={cx(`${ns}DatePicker-popup DatePicker-mobile`)}
container={popOverContainer}
isShow={isOpened}
showClose={false}
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>
<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}
maxDate={maxDate}
useMobileUI={useMobileUI}
// utc={utc}
/>
</PopUp>
)
) : null}
</div>
);

View File

@ -785,7 +785,10 @@ export class DateRangePicker extends React.Component<
viewMode = 'days',
ranges
} = this.props;
const useCalendarMobile = useMobileUI && isMobile() && ['days', 'months', 'quarters'].indexOf(viewMode) > -1;
const useCalendarMobile =
useMobileUI &&
isMobile() &&
['days', 'months', 'quarters'].indexOf(viewMode) > -1;
const {isOpened, isFocused, startDate, endDate} = this.state;
@ -806,24 +809,28 @@ 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'}
/>;
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 (
@ -836,14 +843,16 @@ export class DateRangePicker extends React.Component<
className
)}
>
{useCalendarMobile
? calendarMobile
: this.renderCalendar()}
{useCalendarMobile ? calendarMobile : this.renderCalendar()}
</div>
);
}
const CalendarMobileTitle = <div className={`${ns}CalendarMobile-title`}>{__('Calendar.datepicker')}</div>;
const CalendarMobileTitle = (
<div className={`${ns}CalendarMobile-title`}>
{__('Calendar.datepicker')}
</div>
);
return (
<div
@ -856,7 +865,8 @@ export class DateRangePicker extends React.Component<
{
'is-disabled': disabled,
'is-focused': isFocused,
[`${ns}DateRangePicker--border${ucFirst(borderMode)}`]: borderMode
[`${ns}DateRangePicker--border${ucFirst(borderMode)}`]: borderMode,
'is-mobile': useMobileUI && isMobile()
},
className
)}
@ -887,33 +897,33 @@ export class DateRangePicker extends React.Component<
useMobileUI && isMobile() ? (
<PopUp
isShow={isOpened}
container={popOverContainer}
className={cx(`${ns}CalendarMobile-pop`)}
onHide={this.close}
header={CalendarMobileTitle}
>
{useCalendarMobile
? calendarMobile
: this.renderCalendar()}
{useCalendarMobile ? calendarMobile : this.renderCalendar()}
</PopUp>
)
: <Overlay
target={() => this.dom.current}
onHide={this.close}
container={popOverContainer || (() => findDOMNode(this))}
rootClose={false}
placement={overlayPlacement}
show
>
<PopOver
classPrefix={ns}
className={cx(`${ns}DateRangePicker-popover`, popoverClassName)}
) : (
<Overlay
target={() => this.dom.current}
onHide={this.close}
onClick={this.handlePopOverClick}
overlay
container={popOverContainer || (() => findDOMNode(this))}
rootClose={false}
placement={overlayPlacement}
show
>
{this.renderCalendar()}
</PopOver>
</Overlay>
<PopOver
classPrefix={ns}
className={cx(`${ns}DateRangePicker-popover`, popoverClassName)}
onHide={this.close}
onClick={this.handlePopOverClick}
overlay
>
{this.renderCalendar()}
</PopOver>
</Overlay>
)
) : null}
</div>
);

View File

@ -572,23 +572,25 @@ 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"
/>;
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 (
@ -601,14 +603,16 @@ export class MonthRangePicker extends React.Component<
className
)}
>
{mobileUI
? calendarMobile
: this.renderCalendar()}
{mobileUI ? calendarMobile : this.renderCalendar()}
</div>
);
}
const CalendarMobileTitle = <div className={`${ns}CalendarMobile-title`}>{__('Calendar.datepicker')}</div>;
const CalendarMobileTitle = (
<div className={`${ns}CalendarMobile-title`}>
{__('Calendar.datepicker')}
</div>
);
return (
<div
@ -620,7 +624,8 @@ export class MonthRangePicker extends React.Component<
`${ns}DateRangePicker`,
{
'is-disabled': disabled,
'is-focused': isFocused
'is-focused': isFocused,
'is-mobile': useMobileUI && isMobile()
},
className
)}
@ -651,31 +656,33 @@ export class MonthRangePicker extends React.Component<
mobileUI ? (
<PopUp
isShow={isOpened}
container={popOverContainer}
className={cx(`${ns}CalendarMobile-pop`)}
onHide={this.close}
header={CalendarMobileTitle}
>
{calendarMobile}
</PopUp>
)
: <Overlay
target={() => this.dom.current}
onHide={this.close}
container={popOverContainer || (() => findDOMNode(this))}
rootClose={false}
placement={overlayPlacement}
show
>
<PopOver
classPrefix={ns}
className={cx(`${ns}DateRangePicker-popover`, popoverClassName)}
) : (
<Overlay
target={() => this.dom.current}
onHide={this.close}
onClick={this.handlePopOverClick}
overlay
container={popOverContainer || (() => findDOMNode(this))}
rootClose={false}
placement={overlayPlacement}
show
>
{this.renderCalendar()}
</PopOver>
</Overlay>
<PopOver
classPrefix={ns}
className={cx(`${ns}DateRangePicker-popover`, popoverClassName)}
onHide={this.close}
onClick={this.handlePopOverClick}
overlay
>
{this.renderCalendar()}
</PopOver>
</Overlay>
)
) : null}
</div>
);

View File

@ -2,7 +2,12 @@
* @file Picker
* @description
*/
import React, {memo, ReactNode, useState, useEffect} from 'react';
import React, {
memo,
ReactNode,
useState,
useEffect
} from 'react';
import {uncontrollable} from 'uncontrollable';
import {themeable, ThemeProps} from '../theme';
@ -16,6 +21,7 @@ export type PickerValue = string | number;
export interface PickerProps extends ThemeProps, LocaleProps {
title?: String | ReactNode;
labelField?: string;
valueField?: string;
className?: string;
showToolbar?: boolean;
defaultValue?: PickerValue[];
@ -38,12 +44,14 @@ function fixToArray(data: any) {
const Picker = memo<PickerProps>(props => {
const {
title,
labelField,
valueField,
visibleItemCount = 5,
value = [],
swipeDuration = 1000,
columns = [],
itemHeight = 30,
itemHeight = 48,
showToolbar = true,
className = '',
classnames: cx,
@ -55,9 +63,11 @@ const Picker = memo<PickerProps>(props => {
const [innerValue, setInnerValue] = useState<PickerValue[]>(
fixToArray(props.value === undefined ? props.defaultValue || [] : value)
);
useEffect(() => {
setInnerValue(value);
}, [value]);
if (value === innerValue) return
setInnerValue(fixToArray(value));
}, [value])
const close = () => {
if (props.onClose) {
@ -90,7 +100,8 @@ const Picker = memo<PickerProps>(props => {
{...item}
classnames={cx}
classPrefix={ns}
labelField={labelField}
labelField={labelField || item.labelField}
valueField={valueField || item.valueField}
itemHeight={itemHeight}
swipeDuration={swipeDuration}
visibleItemCount={visibleItemCount}
@ -109,25 +120,29 @@ const Picker = memo<PickerProps>(props => {
const maskStyle = {
backgroundSize: `100% ${(wrapHeight - itemHeight) / 2}px`
};
const hasHeader = showToolbar || title;
return (
<div className={cx(className, 'PickerColumns', 'PickerColumns-popOver')}>
{showToolbar && (
<div className={cx('PickerColumns-toolbar')}>
<Button
{hasHeader && (<div className={cx('PickerColumns-header')}>
{showToolbar && (<Button
className="PickerColumns-cancel"
level="default"
onClick={close}
>
{__('cancel')}
</Button>
<Button
</Button>)}
{title && (
<div className={cx('PickerColumns-title')}>
{title}
</div>
)}
{showToolbar && (<Button
className="PickerColumns-confirm"
level="primary"
onClick={confirm}
>
{__('confirm')}
</Button>
</Button>)}
</div>
)}
<div className={cx('PickerColumns-columns')} style={columnsStyle}>

View File

@ -21,6 +21,7 @@ import useTouch from '../hooks/use-touch';
export interface PickerColumnItem {
labelField?: string;
valueField?: string;
readonly?: boolean;
value?: PickerOption;
swipeDuration?: number;
@ -68,8 +69,9 @@ function isOptionDisabled(option: PickerOption) {
const PickerColumn = forwardRef<{}, PickerColumnProps>((props, ref) => {
const {
visibleItemCount = 5,
itemHeight = 30,
itemHeight = 48,
value,
valueField = 'value',
swipeDuration = 1000,
labelField = 'text',
options = [],
@ -88,7 +90,24 @@ const PickerColumn = forwardRef<{}, PickerColumnProps>((props, ref) => {
const touch = useTouch();
const count = options.length;
const defaultIndex = options.findIndex(item => item === value);
const getOptionText = (option: [] | PickerOption) => {
if (isObject(option) && labelField in option) {
//@ts-ignore
return option[labelField];
}
return option;
};
const getOptionValue = (option: [] | PickerOption) => {
if (isObject(option) && valueField in option) {
//@ts-ignore
return option[valueField];
}
return option;
};
const defaultIndex = options.findIndex(item => getOptionValue(item) === value);
const baseOffset = useMemo(() => {
// 默认转入第一个选项的位置
@ -132,12 +151,11 @@ const PickerColumn = forwardRef<{}, PickerColumnProps>((props, ref) => {
updateState({index});
if (emitChange && props.onChange) {
requestAnimationFrame(() => {
props.onChange?.(options[index], index, confirm);
});
// setTimeout(() => {
// props.onChange?.(options[index], index, confirm);
// }, 0);
requestAnimationFrame(
() => {
props.onChange?.(getOptionValue(options[index]), index, confirm);
}
);
}
};
@ -154,7 +172,7 @@ const PickerColumn = forwardRef<{}, PickerColumnProps>((props, ref) => {
const setOptions = (options: Array<PickerOption>) => {
if (JSON.stringify(options) !== JSON.stringify(state.options)) {
updateState({options});
const index = options.findIndex(item => item === value) || 0;
const index = options.findIndex(item => getOptionValue(item) === value) || 0;
setIndex(index, true, true);
}
};
@ -168,14 +186,6 @@ const PickerColumn = forwardRef<{}, PickerColumnProps>((props, ref) => {
setIndex(index, true, true);
};
const getOptionText = (option: [] | PickerOption) => {
if (isObject(option) && labelField in option) {
//@ts-ignore
return option[labelField];
}
return option;
};
const getIndexByOffset = (offset: number) =>
range(Math.round(-offset / itemHeight), 0, count - 1);
@ -379,7 +389,7 @@ PickerColumn.defaultProps = {
options: [],
visibleItemCount: 5,
swipeDuration: 1000,
itemHeight: 30
itemHeight: 48
};
export default themeable(

View File

@ -11,6 +11,7 @@ import Button from './Button';
export interface PickerContainerProps extends ThemeProps, LocaleProps {
title?: string;
showTitle?: boolean;
headerClassName?: string;
children: (props: {
onClick: (e: React.MouseEvent) => void;
isOpened: boolean;
@ -100,6 +101,7 @@ export class PickerContainer extends React.Component<
bodyRender: popOverRender,
title,
showTitle,
headerClassName,
translate: __,
size
} = this.props;
@ -117,7 +119,7 @@ export class PickerContainer extends React.Component<
onHide={this.close}
>
{showTitle !== false ? (
<Modal.Header onClose={this.close}>
<Modal.Header onClose={this.close} className={headerClassName}>
{__(title || 'Select.placeholder')}
</Modal.Header>
) : null}

View File

@ -79,6 +79,7 @@ export class PopOverContainer extends React.Component<
{mobileUI ? (
<PopUp
isShow={this.state.isOpened}
container={popOverContainer}
className={popOverClassName}
onHide={this.close}
>

View File

@ -56,19 +56,15 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
if (this.props.isShow) {
this.scrollTop =
document.body.scrollTop || document.documentElement.scrollTop;
document.body.style.overflow =
'hidden';
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow =
'auto';
document.body.scrollTop =
this.scrollTop;
document.body.style.overflow = 'auto';
document.body.scrollTop = this.scrollTop;
}
}
componentWillUnmount() {
document.body.style.overflow = 'auto';
document.body.scrollTop =
this.scrollTop;
document.body.scrollTop = this.scrollTop;
}
handleClick(e: React.MouseEvent) {
e.stopPropagation();
@ -128,7 +124,7 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
<div className={cx(`${ns}PopUp-toolbar`)}>
<Button
className={cx(`${ns}PopUp-cancel`)}
level="default"
level="text"
onClick={onHide}
>
{__('cancel')}
@ -138,7 +134,7 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
)}
<Button
className={cx(`${ns}PopUp-confirm`)}
level="primary"
level="text"
onClick={onConfirm}
>
{__('confirm')}

View File

@ -42,8 +42,8 @@ import 'froala-editor/js/plugins/word_paste.min';
import 'froala-editor/js/languages/zh_cn.js';
// Require Editor CSS files.
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/css/froala_editor.pkgd.min.css';
// import 'froala-editor/css/froala_style.min.css';
// import 'froala-editor/css/froala_editor.pkgd.min.css';
export interface FroalaEditorComponentProps {
config: any;

View File

@ -184,7 +184,8 @@ export function normalizeOptions(
} = {
values: [],
options: []
}
},
valueField = 'value'
): Options {
if (typeof options === 'string') {
return options.split(',').map(item => {
@ -225,7 +226,7 @@ export function normalizeOptions(
});
} else if (Array.isArray(options as Options)) {
return (options as Options).map(item => {
const value = item && item.value;
const value = item && item[valueField];
const idx =
value !== undefined && !item.children
@ -242,7 +243,7 @@ export function normalizeOptions(
};
if (typeof option.children !== 'undefined') {
option.children = normalizeOptions(option.children, share);
option.children = normalizeOptions(option.children, share, valueField);
} else if (value !== undefined) {
share.values.push(value);
share.options.push(option);
@ -1015,6 +1016,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
return mobileUI ? (
<PopUp
className={cx(`Select-popup`)}
container={popOverContainer}
isShow={this.state.isOpen}
onHide={this.close}
>

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