Skip to content

Commit

Permalink
feat: 新增Dialog 对话框组件
Browse files Browse the repository at this point in the history
  • Loading branch information
79E committed Jan 12, 2023
1 parent d62f7bd commit 4736364
Show file tree
Hide file tree
Showing 16 changed files with 1,002 additions and 0 deletions.
1 change: 1 addition & 0 deletions .umirc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export default defineConfig({
'components/back-top',
'components/notify',
'components/toast',
'components/dialog',
],
},
{
Expand Down
169 changes: 169 additions & 0 deletions src/components/dialog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Dialog 对话框
<code hidden="hidden" src="./demos/demo.tsx"></code>

## 介绍
用于重要信息的告知或操作的反馈,并附带少量的选项供用户进行操作。

## 使用

```tsx
import { Dialog } from 'aunt';
```

### 基本用法
<code src="./demos/demo-base.tsx"></code>

### 操作按钮
<code src="./demos/demo-action.tsx"></code>

### 自定义样式
<code src="./demos/demo-style.tsx"></code>

### 获取结果
<code src="./demos/demo-result.tsx"></code>

### 声明式
<code src="./demos/demo-dialog.tsx"></code>


## 参数

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| visible | 显示隐藏开关 | `boolean` | `-` |
| header | 自定义头部 | `React.ReactNode` | `-` |
| headerImage | 显示头部图片(优先级低于 header) | `string` | `-` |
| title | 操作按钮列表 | `string \| React.ReactNode;` | `-` |
| content | 操作按钮列表 |`string \| React.ReactNode;` | `-` |
| contentClass | 操作按钮列表 | `string` | `-` |
| contentStyle | 操作按钮列表 | `React.CSSProperties` | `-` |
| actions | 操作按钮列表 | ` DialogAction[]` | `[]` |
| renderAction | 操作按钮列表 | `React.ReactNode` | `-` |
| direction | 操作按钮列表 | `'horizontal' \| 'vertical'` | `horizontal` |
| closeOnAction | 操作按钮列表 | `boolean` | `false` |
| closeOnOverlay | 操作按钮列表 | `boolean` | `false` |
| overlayClass | 操作按钮列表 | `string` | `-` |
| overlayStyle | 操作按钮列表 | `React.CSSProperties` | `-` |
| onAction | 操作按钮列表 | `(action: DialogAction, index: number) => void;` | `-` |
| onClose | 操作按钮列表 | ` () => void;` | `-` |
| afterClose | 操作按钮列表 | `() => void;` | `-` |
| afterShow | 操作按钮列表 | `() => void;` | `-` |

### DialogAction

| 属性 | 说明 | 类型 | 默认值 |
| --------- | ----- | -------- | ------- |
| bold | 是否文字加粗 | `boolean` | `false` |
| className | `Action` 类名 | `string` | `-` |
| danger | 是否为危险状态 | `boolean` | `false` |
| disabled | 是否为禁用状态 | `boolean` | `false` |
| key | 唯一标记 | `string \| number` | `-` |
| onClick | 点击时触发 | `(event: React.MouseEvent) => void \| Promise<void>` | `-` |
| style | `Action` 样式 | `React.CSSProperties` | `-` |
| text | 标题 | `React.ReactNode` | `-` |

## 指令式

可以通过指令式的方式使用 `Dialog`

### Dialog.show

```ts | pure
const handler = Dialog.show(props)
```

可以通过调用 `Dialog` 上的 `show` 方法直接打开对话框,其中 `props` 参数的类型同上表,但不支持传入 `visible` 属性。

当对话框被关闭后,组件实例会自动销毁。

`show` 方法的返回值为一个组件控制器,包含以下属性:

| 属性 | 说明 | 类型 | 默认值 |
| ----- | ---------- | ------------ | ------ |
| close | 关闭对话框 | `() => void` | `-` |
| config | 修改对话框配置 | `(DialogProps) => void` | `-` |

`show` 只是一个很基础的方法,在实际业务中,更为常用的是下面的 `alert``confirm` 方法:

### Dialog.alert

`alert` 接受的参数同 `show`,但不支持 `closeOnAction` `actions` 属性,它的返回值不是一个控制器对象,而是 `Promise<void>`

此外,它还额外支持以下属性:

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| confirmText | 确认按钮的内容 | `ReactNode` | `'我知道了'` |
| onConfirm | 点击确认按钮时触发 | `() => void \| Promise<void>` | - |

### Dialog.confirm

`confirm` 接受的参数同 `show`,但不支持 `closeOnAction` `actions` 属性,它的返回值不是一个控制器对象,而是 `Promise<boolean>`

此外,它还额外支持以下属性:

| 属性 | 说明 | 类型 | 默认值 |
| ----------- | ------------------ | ----------------------------- | -------- |
| cancelText | 取消按钮的内容 | `ReactNode` | `'取消'` |
| confirmText | 确认按钮的内容 | `ReactNode` | `'确认'` |
| onCancel | 点击取消按钮时触发 | `() => void \| Promise<void>` | - |
| onConfirm | 点击确认按钮时触发 | `() => void \| Promise<void>` | - |

需要注意的是,对于**指令式**创建出来的 Dialog,**并不会感知父组件的重渲染和其中 state 的更新**,因此下面这种写法是完全错误的:

```tsx
export default function App() {
const [captcha, setCaptcha] = useState<string>("");
const showCaptcha = () => {
return Dialog.confirm({
title: "短信验证",
content: (
<div>
<Input
placeholder="请输入验证码"
value={captcha} // App 中 captcha 的更新是不会传递到 Dialog 中的
onChange={(v) => {
setCaptcha(v)
}}
/>
</div>
)
});
};
return (
<div>
<Button onClick={showCaptcha}>Show</Button>
</div>
);
}
```

如果你需要在 Dialog 中包含很多复杂的状态和逻辑,那么可以使用声明式的语法,或者考虑自己将内部状态和逻辑单独封装一个组件出来。

### Dialog.clear

可以通过调用 `Dialog` 上的 `clear` 方法关闭所有打开的对话框,通常用于路由监听中,处理路由前进、后退不能关闭对话框的问题。


## 样式变量

| 属性名 | 说明 | 默认值 |
| -------------- | ---------- | ------------- |
| --aunt-dialog-z-index | zindex层级 | `var(--aunt-z-index-full-screen);` |
| --aunt-dialog-width | 对话框宽度 | `calc(280 * var(--aunt-hd));` |
| --aunt-dialog-background-color | 对话框背景颜色 | `var(--aunt-white-color);` |
| --aunt-dialog-border-radius | 对话框圆角大小 | `var(--aunt-border-radius-lg);` |
| --aunt-dialog-padding | 对话框内边距 | `var(--aunt-padding-l);` |
| --aunt-dialog-header-font-size | 对话框头部文字大小 | `var(--aunt-font-size-lg);` |
| --aunt-dialog-header-color | 头部文字颜色 | `var(--aunt-text-color);` |
| --aunt-dialog-font-weight | 重要文字加粗 | `var(--aunt-font-weight-bold);` |
| --aunt-dialog-horizontal-margin | 横向边距 | `var(--aunt-padding-l);` |
| --aunt-dialog-vertical-margin | 竖向边距 | `var(--aunt-padding-sm);` |
| --aunt-dialog-content-max-height | 内容最大高度 | `70vh` |
| --aunt-dialog-content-font-size | 内容文字大小 | `var(--aunt-font-size-md);` |
| --aunt-dialog-content-line-height | 内容文字行高 | `var(--aunt-line-height-sm);` |
| --aunt-dialog-action-border | 操作边框 | `var(--aunt-border-width-base) solid var(--aunt-gray-2);` |
| --aunt-dialog-action-font-size | 操作文字大小 | `var(--aunt-font-size-lg);` |
| --aunt-dialog-action-color | 操作文字颜色 | `var(--aunt-primary-color);` |
| --aunt-dialog-action-danger-color | 危险操作文字颜色 | `var(--aunt-danger-color);` |
| --aunt-dialog-action-disabled-color | 禁用操作文字颜色 | `var(--aunt-active-color);` |
151 changes: 151 additions & 0 deletions src/components/dialog/controller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Dialog } from './dialog';
import type { DialogOptions, DialogUpdate, DialogAlertOptions, DialogConfirmOptions } from './types';
import { render, unmount } from '../../utils/dom/render';
import { resolveContainer } from '../../utils/dom/getContainer';

// 存储相关对话框的关闭函数
const dialogArray: (() => void)[] = [];
// 同步的销毁
function syncClear() {
let fn = dialogArray.pop();
while (fn) {
fn();
fn = dialogArray.pop();
}
}
function nextTickClear() {
setTimeout(syncClear);
}

const defaultOptions: DialogOptions = {}

const controller = (p: DialogOptions): DialogUpdate => {
const props = { ...defaultOptions, ...p };

// 创建父亲节点
const userContainer = resolveContainer(props.teleport);
const container = document.createElement('div');
userContainer.appendChild(container);

const update: DialogUpdate = {
config: () => {},
clear: () => {}
};

const TempNotify = () => {

const [state, setState] = useState({ ...props });
const [visible, setVisible] = useState(false);

const internalOnClosed = useCallback(() => {
const unmountResult = unmount(container);
if (unmountResult && container.parentNode) {
container.parentNode.removeChild(container);
}
}, [container]);

const destroy = useCallback(() => {
setVisible(false);
if (state.onClose) state.onClose();
setTimeout(internalOnClosed, 1000)
}, []);

update.clear = destroy;

update.config = useCallback(
nextState => {
setState(prev =>
typeof nextState === 'function'
? { ...prev, ...nextState(prev) }
: { ...prev, ...nextState }
);
},
[setState]
);

useEffect(() => {
setVisible(true);
dialogArray.push(destroy);
return () => {};
}, []);

return <Dialog
visible={visible}
{...state}
onClose={()=>{
destroy();
}}
/>;
};

render(<TempNotify />, container);

return update;
};

const show = (props: DialogOptions) => {
return controller(props);
}

const alert = (props: DialogAlertOptions ) => {
const text = props.confirmText || '我知道了'
return new Promise<void>(resolve => {
controller({
...props,
actions:[{
key:'confirm',
text,
onClick: props.onConfirm
}],
closeOnAction: true,
onClose: () => {
props.onClose?.()
resolve()
},
});
})
}

const confirm = (props: DialogConfirmOptions) => {
const cancelText = props.cancelText || '取消';
const confirmText = props.confirmText || '确定';
return new Promise<boolean>(resolve =>{
controller({
...props,
actions:[
{
key:'cancel',
text: cancelText,
onClick: async (e)=>{
props.onCancel?.(e);
resolve(false);
}
},
{
key:'confirm',
text: confirmText,
bold: true,
onClick: async (e)=>{
props.onConfirm?.(e);
resolve(true);
}
}
],
closeOnAction: true,
onClose: () => {
props.onClose?.();
resolve(false);
},
});
});
}

const DialogController = Object.assign(Dialog, {
clear: nextTickClear,
show,
alert,
confirm,
});

export default DialogController;
60 changes: 60 additions & 0 deletions src/components/dialog/demos/demo-action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { Dialog, Button, Space } from 'aunt';

export default ()=>(
<Space direction='vertical'>
<Button
block
onClick={()=>{
Dialog.show({
closeOnAction: true,
title:'《登鹳雀楼》',
content: '白日依山尽,黄河入海流。欲穷千里目,更上一层楼。',
actions:[
{
key:'show',
text:'再看看'
},
{
key:'no',
text:'还不会'
},
{
key:'ok',
text:'我会了'
}
]
})
}}
>
多个按钮
</Button>
<Button
block
onClick={()=>{
Dialog.show({
direction:'vertical',
closeOnAction: true,
title:'《赤壁》',
content: '折戟沉沙铁未销,自将磨洗认前朝。东风不与周郎便,铜雀春深锁二乔。',
actions:[
{
key:'show',
text:'再看看'
},
{
key:'no',
text:'还不会'
},
{
key:'ok',
text:'我会了'
}
]
})
}}
>
多个按钮竖向排列
</Button>
</Space>
);
Loading

0 comments on commit 4736364

Please sign in to comment.