mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 20:09:08 +08:00
Merge branch 'baidu:master' into master
This commit is contained in:
commit
e3f8c3762f
@ -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) |
|
||||
|
2854
__tests__/renderers/Form/__snapshots__/text.test.tsx.snap
Normal file
2854
__tests__/renderers/Form/__snapshots__/text.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
285
__tests__/renderers/Form/text.test.tsx
Normal file
285
__tests__/renderers/Form/text.test.tsx
Normal 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');
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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(/取消/));
|
||||
|
@ -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=""
|
||||
|
@ -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>
|
||||
`;
|
||||
|
124
__tests__/renderers/__snapshots__/Timeline.test.tsx.snap
Normal file
124
__tests__/renderers/__snapshots__/Timeline.test.tsx.snap
Normal 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>
|
||||
`;
|
@ -348,6 +348,12 @@ Content-Type: application/pdf
|
||||
Content-Disposition: attachment; filename="download.pdf"
|
||||
```
|
||||
|
||||
如果接口存在跨域,除了常见的 cors header 外,还需要添加以下 header
|
||||
|
||||
```
|
||||
Access-Control-Expose-Headers: Content-Disposition
|
||||
```
|
||||
|
||||
## 倒计时
|
||||
|
||||
主要用于发验证码的场景,通过设置倒计时 `countDown`(单位是秒),让点击按钮后禁用一段时间:
|
||||
|
@ -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` 跳转,当命中当前页面时,跳转到目标页面。
|
||||
|
193
docs/zh-CN/components/calendar.md
Normal file
193
docs/zh-CN/components/calendar.md
Normal 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` | 放大模式 |
|
||||
|
@ -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` | | 保存顺序失败提示 |
|
||||
|
@ -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 | 隐藏下拉图标 |
|
||||
|
@ -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` | | 是否显示「非」按钮 |
|
||||
|
@ -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` | 放大模式 |
|
||||
|
@ -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 样式类名 |
|
||||
|
@ -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 | | `''` | 默认值 |
|
||||
|
@ -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/) 字符串 |
|
||||
|
@ -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 }` |
|
||||
|
@ -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` | | 接口 |
|
||||
|
@ -263,7 +263,6 @@ List 的内容、Card 卡片的内容配置同上
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ----------- | ----------------- | ------ | -------------------------------------------------------------------------------------- |
|
||||
| type | `string` | | 如果在 Table、Card 和 List 中,为`"color"`;在 Form 中用作静态展示,为`"static-color"` |
|
||||
| className | `string` | | 外层 CSS 类名 |
|
||||
| placeholder | `string` | | 占位文本 |
|
||||
| map | `object` | | 映射配置 |
|
||||
|
@ -70,6 +70,12 @@ order: 13
|
||||
}
|
||||
```
|
||||
|
||||
_特殊字符变量名_
|
||||
|
||||
> 1.6.1 及以上版本
|
||||
|
||||
默认变量名不支持特殊字符比如 `${ xxx.yyy }` 意思取 xxx 变量的 yyy 属性,如果变量名就是 `xxx.yyy` 怎么获取?这个时候需要用到转义语法,如:`${ xxx\.yyy }`
|
||||
|
||||
### 公式
|
||||
|
||||
除了支持简单表达式外,还集成了很多公式(函数)如:
|
||||
|
@ -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 的 disabledOn,crud 中的 itemDraggableOn 等等,可以参考相应的组件文档。
|
||||
|
@ -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 及以上版本
|
||||
|
23
docs/zh-CN/extend/debug.md
Normal file
23
docs/zh-CN/extend/debug.md
Normal 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 工具展开后,点击任意组件就能看到这个组件的数据链
|
@ -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 无法知晓页面首次打开的时间,需要自行处理。
|
||||
|
@ -99,3 +99,7 @@ amisScoped.updateProps({
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## CRUD api 分页功能失效
|
||||
|
||||
如果 api 地址中有变量,比如 `/api/mock2/sample/${id}`,amis 就不会自动加上分页参数,需要自己加上,改成 `/api/mock2/sample/${id}?page=${page}&perPage=${perPage}`
|
||||
|
@ -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)。
|
||||
|
@ -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`就是完全替换当前数据。
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
358
examples/components/Linkage/Event.jsx
Normal file
358
examples/components/Linkage/Event.jsx
Normal 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=2,如果想要拿到当前域的数据需要通过args数据映射
|
||||
// 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
34
examples/components/Linkage/FormSubmit.jsx
Normal file
34
examples/components/Linkage/FormSubmit.jsx
Normal 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: []
|
||||
}
|
||||
};
|
@ -12,7 +12,6 @@ export default {
|
||||
type: 'input-text',
|
||||
name: 'name'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Email',
|
||||
type: 'input-email',
|
||||
|
@ -103,6 +103,8 @@
|
||||
})();
|
||||
}
|
||||
|
||||
window.enableAMISDebug = true;
|
||||
|
||||
/* @require ./index.jsx 标记为同步依赖,提前加载 */
|
||||
amis.require(['./index.jsx'], function (app) {
|
||||
var initialState = {};
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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>`;
|
||||
|
@ -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
7
scss/_thirds.scss
Normal 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');
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -95,4 +95,8 @@
|
||||
&-tab {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-btnCancel {
|
||||
color: var(--PopUp-cancelAction-color);
|
||||
}
|
||||
}
|
||||
|
27
scss/components/_city-area.scss
Normal file
27
scss/components/_city-area.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,10 @@
|
||||
.#{$ns}Collapse-arrow {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.#{$ns}Collapse-icon-tranform {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
167
scss/components/_debug.scss
Normal 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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,8 @@
|
||||
color: var(--ColorPicker-color);
|
||||
border-radius: var(--borderRadius);
|
||||
|
||||
&-popup{
|
||||
height: 80vh;
|
||||
&-popup {
|
||||
height: px2rem(400px);
|
||||
}
|
||||
|
||||
&:not(.is-disabled) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
&-popup {
|
||||
height: 80vh;
|
||||
height: px2rem(400px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)};
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
138
src/actions/Action.ts
Normal 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; // 自定义JS,actionType: 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
58
src/actions/AjaxAction.ts
Normal 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());
|
27
src/actions/BreakAction.ts
Normal file
27
src/actions/BreakAction.ts
Normal 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());
|
41
src/actions/BroadcastAction.ts
Normal file
41
src/actions/BroadcastAction.ts
Normal 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
36
src/actions/CmptAction.ts
Normal 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());
|
27
src/actions/ContinueAction.ts
Normal file
27
src/actions/ContinueAction.ts
Normal 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
41
src/actions/CopyAction.ts
Normal 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());
|
46
src/actions/CustomAction.ts
Normal file
46
src/actions/CustomAction.ts
Normal 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());
|
28
src/actions/DialogAction.ts
Normal file
28
src/actions/DialogAction.ts
Normal 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());
|
28
src/actions/DrawerAction.ts
Normal file
28
src/actions/DrawerAction.ts
Normal 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());
|
38
src/actions/EmailAction.ts
Normal file
38
src/actions/EmailAction.ts
Normal 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
77
src/actions/LoopAction.ts
Normal 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());
|
39
src/actions/OpenPageAction.ts
Normal file
39
src/actions/OpenPageAction.ts
Normal 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());
|
26
src/actions/ParallelAction.ts
Normal file
26
src/actions/ParallelAction.ts
Normal 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());
|
35
src/actions/SwitchAction.ts
Normal file
35
src/actions/SwitchAction.ts
Normal 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
20
src/actions/index.ts
Normal 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';
|
@ -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>}
|
||||
|
@ -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
315
src/components/CityArea.tsx
Normal 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'
|
||||
})
|
||||
)
|
||||
);
|
@ -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';
|
||||
|
@ -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')} />
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}>
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
|
@ -79,6 +79,7 @@ export class PopOverContainer extends React.Component<
|
||||
{mobileUI ? (
|
||||
<PopUp
|
||||
isShow={this.state.isOpened}
|
||||
container={popOverContainer}
|
||||
className={popOverClassName}
|
||||
onHide={this.close}
|
||||
>
|
||||
|
@ -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')}
|
||||
|
@ -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;
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user