feat(通用组件): 新增 Dialog 和 Pagination 组件
- Dialog: 封装可复用的弹窗组件,支持受控和非受控模式 - Pagination: 封装统一样式的分页组件,匹配系统主题色 - 优化组件API设计,提升开发体验
This commit is contained in:
parent
a44b06cc15
commit
a495a3c115
344
src/components/Dialog/README.md
Normal file
344
src/components/Dialog/README.md
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
# Dialog 弹框通用组件
|
||||||
|
|
||||||
|
基于 Arco Design Modal 封装的通用弹框组件,提供更丰富的功能和更好的用户体验。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- ✅ 完整的 TypeScript 类型支持
|
||||||
|
- ✅ 支持受控和非受控两种模式
|
||||||
|
- ✅ 提供 `beforeClose` 钩子,可以阻止关闭
|
||||||
|
- ✅ 支持异步确认操作
|
||||||
|
- ✅ 自定义头部、内容、底部渲染
|
||||||
|
- ✅ 响应式主题适配(亮色/暗色)
|
||||||
|
- ✅ 美观的 UI 设计
|
||||||
|
- ✅ 完善的 ref 方法调用
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
### 1. 非受控模式(推荐)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import Dialog, { DialogRef } from '@/components/Dialog';
|
||||||
|
|
||||||
|
function Demo() {
|
||||||
|
const dialogRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
// 执行异步操作
|
||||||
|
await api.deleteUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => dialogRef.current?.open()}>打开弹框</Button>
|
||||||
|
|
||||||
|
<Dialog ref={dialogRef} title="确认删除" onConfirm={handleConfirm}>
|
||||||
|
确定要删除这条记录吗?
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 受控模式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Dialog from '@/components/Dialog';
|
||||||
|
|
||||||
|
function Demo() {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setVisible(true)}>打开弹框</Button>
|
||||||
|
|
||||||
|
<Dialog visible={visible} onVisibleChange={setVisible} title="用户信息">
|
||||||
|
<div>用户详细信息...</div>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 1. beforeClose 钩子
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
title="编辑用户"
|
||||||
|
beforeClose={async () => {
|
||||||
|
// 检查是否有未保存的修改
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
const confirmed = await showConfirm('有未保存的修改,确定关闭吗?');
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserForm />
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 自定义底部按钮
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
title="批量操作"
|
||||||
|
footer={(close) => (
|
||||||
|
<Space>
|
||||||
|
<Button onClick={close}>取消</Button>
|
||||||
|
<Button type="primary" onClick={handleBatchDelete}>
|
||||||
|
批量删除
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" status="warning" onClick={handleBatchExport}>
|
||||||
|
批量导出
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BatchOperationForm />
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 自定义头部
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
header={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<IconUser style={{ marginRight: 8 }} />
|
||||||
|
<span>用户详情</span>
|
||||||
|
<Tag color="blue" style={{ marginLeft: 'auto' }}>
|
||||||
|
VIP
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UserDetail />
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 隐藏底部按钮
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dialog ref={dialogRef} title="查看详情" footerHidden>
|
||||||
|
<DetailView />
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 只显示确认按钮
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
title="提示信息"
|
||||||
|
showCancel={false}
|
||||||
|
confirmText="我知道了"
|
||||||
|
>
|
||||||
|
操作成功!
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 异步确认操作
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
title="提交表单"
|
||||||
|
onConfirm={async () => {
|
||||||
|
// 组件会自动显示 loading 状态
|
||||||
|
await api.submitForm(formData);
|
||||||
|
Message.success('提交成功');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form />
|
||||||
|
</Dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 |
|
||||||
|
| --------------- | ----------------------------------- | ----------------------------------- | -------- |
|
||||||
|
| title | 弹框标题 | `ReactNode` | `'提示'` |
|
||||||
|
| width | 弹框宽度 | `number \| string` | `520` |
|
||||||
|
| height | 内容区域高度 | `number \| string` | `'auto'` |
|
||||||
|
| confirmText | 确认按钮文本 | `string` | `'确定'` |
|
||||||
|
| cancelText | 取消按钮文本 | `string` | `'取消'` |
|
||||||
|
| showConfirm | 是否显示确认按钮 | `boolean` | `true` |
|
||||||
|
| showCancel | 是否显示取消按钮 | `boolean` | `true` |
|
||||||
|
| confirmLoading | 确认按钮加载状态 | `boolean` | `false` |
|
||||||
|
| confirmDisabled | 确认按钮禁用状态 | `boolean` | `false` |
|
||||||
|
| footerHidden | 隐藏底部按钮区域 | `boolean` | `false` |
|
||||||
|
| maskClosable | 点击遮罩层是否关闭 | `boolean` | `false` |
|
||||||
|
| escToClose | ESC 键是否关闭 | `boolean` | `true` |
|
||||||
|
| closable | 是否显示关闭按钮 | `boolean` | `true` |
|
||||||
|
| className | 自定义类名 | `string` | - |
|
||||||
|
| children | 弹框内容 | `ReactNode` | - |
|
||||||
|
| footer | 自定义底部渲染 | `ReactNode \| (close) => ReactNode` | - |
|
||||||
|
| header | 自定义头部渲染 | `ReactNode` | - |
|
||||||
|
| alignCenter | 是否居中显示 | `boolean` | `true` |
|
||||||
|
| mountContainer | 挂载容器 | `string \| HTMLElement` | - |
|
||||||
|
| onConfirm | 点击确认按钮的回调 | `() => void \| Promise<void>` | - |
|
||||||
|
| onCancel | 点击取消按钮的回调 | `() => void` | - |
|
||||||
|
| beforeClose | 关闭前的回调,返回 false 可阻止关闭 | `() => boolean \| Promise<boolean>` | - |
|
||||||
|
| onClose | 关闭后的回调 | `() => void` | - |
|
||||||
|
| onOpen | 打开后的回调 | `() => void` | - |
|
||||||
|
| visible | 受控模式:是否显示 | `boolean` | - |
|
||||||
|
| onVisibleChange | 受控模式:显示状态变化回调 | `(visible: boolean) => void` | - |
|
||||||
|
|
||||||
|
### Ref 方法
|
||||||
|
|
||||||
|
| 方法 | 说明 | 类型 |
|
||||||
|
| ---------- | ---------------------------------- | --------------- |
|
||||||
|
| open | 打开弹框 | `() => void` |
|
||||||
|
| close | 关闭弹框 | `() => void` |
|
||||||
|
| forceClose | 强制关闭弹框(不触发 beforeClose) | `() => void` |
|
||||||
|
| isVisible | 获取当前显示状态 | `() => boolean` |
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
### 与 Vue CoiDialog 对比改进
|
||||||
|
|
||||||
|
| 特性 | Vue CoiDialog | React Dialog |
|
||||||
|
| ---------------- | ------------- | ------------------- |
|
||||||
|
| TypeScript 支持 | ✅ | ✅ |
|
||||||
|
| 受控/非受控模式 | ❌ | ✅ |
|
||||||
|
| beforeClose 钩子 | ❌ | ✅ |
|
||||||
|
| 响应式主题 | ❌ | ✅ |
|
||||||
|
| 异步确认处理 | 手动 | 自动 |
|
||||||
|
| 方法调用 | defineExpose | useImperativeHandle |
|
||||||
|
|
||||||
|
### 样式特点
|
||||||
|
|
||||||
|
- 圆角设计(12px)
|
||||||
|
- 柔和阴影效果
|
||||||
|
- 清晰的分隔线
|
||||||
|
- 响应式主题适配
|
||||||
|
- 自定义滚动条样式
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **受控与非受控模式**
|
||||||
|
|
||||||
|
- 非受控模式:使用 `ref` 调用方法
|
||||||
|
- 受控模式:使用 `visible` 和 `onVisibleChange`
|
||||||
|
- 不要混用两种模式
|
||||||
|
|
||||||
|
2. **异步操作**
|
||||||
|
|
||||||
|
- `onConfirm` 返回 Promise 时,组件会自动显示 loading
|
||||||
|
- 操作失败不会关闭弹框
|
||||||
|
- 操作成功会自动关闭弹框
|
||||||
|
|
||||||
|
3. **beforeClose 钩子**
|
||||||
|
|
||||||
|
- 返回 `false` 可以阻止关闭
|
||||||
|
- 支持异步判断
|
||||||
|
- `forceClose()` 方法会跳过此钩子
|
||||||
|
|
||||||
|
4. **性能优化**
|
||||||
|
- 使用 `unmountOnExit` 卸载未显示的弹框
|
||||||
|
- 避免在循环中创建多个弹框实例
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { Button, Form, Input, Message } from '@arco-design/web-react';
|
||||||
|
import Dialog, { DialogRef } from '@/components/Dialog';
|
||||||
|
|
||||||
|
function UserManagement() {
|
||||||
|
const dialogRef = useRef<DialogRef>(null);
|
||||||
|
const [formData, setFormData] = useState({});
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await api.updateUser(formData);
|
||||||
|
Message.success('保存成功');
|
||||||
|
setHasChanges(false);
|
||||||
|
} catch (error) {
|
||||||
|
Message.error('保存失败');
|
||||||
|
throw error; // 抛出错误,阻止弹框关闭
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeforeClose = async () => {
|
||||||
|
if (hasChanges) {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认关闭',
|
||||||
|
content: '有未保存的修改,确定关闭吗?',
|
||||||
|
onOk: () => resolve(true),
|
||||||
|
onCancel: () => resolve(false),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={() => dialogRef.current?.open()}>
|
||||||
|
编辑用户
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
title="编辑用户信息"
|
||||||
|
width={600}
|
||||||
|
onConfirm={handleSubmit}
|
||||||
|
beforeClose={handleBeforeClose}
|
||||||
|
>
|
||||||
|
<Form onChange={() => setHasChanges(true)}>
|
||||||
|
<Form.Item label="用户名">
|
||||||
|
<Input placeholder="请输入用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="邮箱">
|
||||||
|
<Input placeholder="请输入邮箱" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **统一管理弹框状态**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 推荐:使用 ref 统一管理
|
||||||
|
const dialogRef = useRef<DialogRef>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **错误处理**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
await api.submit();
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(error.message);
|
||||||
|
throw error; // 阻止关闭
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **复用弹框实例**
|
||||||
|
```tsx
|
||||||
|
// 推荐:一个弹框实例处理多种情况
|
||||||
|
const openEditDialog = (user) => {
|
||||||
|
setCurrentUser(user);
|
||||||
|
dialogRef.current?.open();
|
||||||
|
};
|
||||||
|
```
|
||||||
224
src/components/Dialog/index.tsx
Normal file
224
src/components/Dialog/index.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import { Modal, Button, Space } from '@arco-design/web-react';
|
||||||
|
import type { DialogProps, DialogRef } from './interface';
|
||||||
|
import styles from './style/index.module.less';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog 弹框通用组件
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const dialogRef = useRef<DialogRef>(null);
|
||||||
|
*
|
||||||
|
* <Dialog
|
||||||
|
* ref={dialogRef}
|
||||||
|
* title="确认操作"
|
||||||
|
* onConfirm={handleConfirm}
|
||||||
|
* >
|
||||||
|
* 确定要执行此操作吗?
|
||||||
|
* </Dialog>
|
||||||
|
*
|
||||||
|
* // 调用方法
|
||||||
|
* dialogRef.current?.open();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const Dialog = forwardRef<DialogRef, DialogProps>((props, ref) => {
|
||||||
|
const {
|
||||||
|
title = '提示',
|
||||||
|
width = 520,
|
||||||
|
height = 'auto',
|
||||||
|
confirmText = '确定',
|
||||||
|
cancelText = '取消',
|
||||||
|
showConfirm = true,
|
||||||
|
showCancel = true,
|
||||||
|
confirmLoading = false,
|
||||||
|
confirmDisabled = false,
|
||||||
|
footerHidden = false,
|
||||||
|
maskClosable = false,
|
||||||
|
escToClose = true,
|
||||||
|
closable = true,
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
alignCenter = true,
|
||||||
|
mountContainer,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
beforeClose,
|
||||||
|
onClose,
|
||||||
|
onOpen,
|
||||||
|
visible: controlledVisible,
|
||||||
|
onVisibleChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// 内部状态管理
|
||||||
|
const [innerVisible, setInnerVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 判断是否为受控组件
|
||||||
|
const isControlled = controlledVisible !== undefined;
|
||||||
|
const visible = isControlled ? controlledVisible : innerVisible;
|
||||||
|
|
||||||
|
// 更新显示状态
|
||||||
|
const updateVisible = useCallback(
|
||||||
|
(newVisible: boolean) => {
|
||||||
|
if (!isControlled) {
|
||||||
|
setInnerVisible(newVisible);
|
||||||
|
}
|
||||||
|
onVisibleChange?.(newVisible);
|
||||||
|
},
|
||||||
|
[isControlled, onVisibleChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打开弹框
|
||||||
|
const handleOpen = useCallback(() => {
|
||||||
|
updateVisible(true);
|
||||||
|
onOpen?.();
|
||||||
|
}, [updateVisible, onOpen]);
|
||||||
|
|
||||||
|
// 关闭弹框
|
||||||
|
const handleClose = useCallback(
|
||||||
|
async (force = false) => {
|
||||||
|
// 如果不是强制关闭,执行 beforeClose 钩子
|
||||||
|
if (!force && beforeClose) {
|
||||||
|
try {
|
||||||
|
const canClose = await beforeClose();
|
||||||
|
if (!canClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('beforeClose error:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVisible(false);
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
[beforeClose, updateVisible, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确认按钮处理
|
||||||
|
const handleConfirm = useCallback(async () => {
|
||||||
|
if (!onConfirm) {
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await onConfirm();
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('onConfirm error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [onConfirm, handleClose]);
|
||||||
|
|
||||||
|
// 取消按钮处理
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
onCancel?.();
|
||||||
|
handleClose();
|
||||||
|
}, [onCancel, handleClose]);
|
||||||
|
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: handleOpen,
|
||||||
|
close: () => handleClose(false),
|
||||||
|
forceClose: () => handleClose(true),
|
||||||
|
isVisible: () => visible,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 自定义底部渲染
|
||||||
|
const renderFooter = () => {
|
||||||
|
if (footerHidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footer) {
|
||||||
|
return typeof footer === 'function' ? footer(handleClose) : footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles['dialog-footer']}>
|
||||||
|
<Space>
|
||||||
|
{showCancel && (
|
||||||
|
<Button size="default" onClick={handleCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showConfirm && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="default"
|
||||||
|
loading={loading || confirmLoading}
|
||||||
|
disabled={confirmDisabled}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义头部渲染
|
||||||
|
const renderHeader = () => {
|
||||||
|
if (header !== undefined) {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={`${styles['dialog-wrapper']} ${className}`}
|
||||||
|
visible={visible}
|
||||||
|
title={renderHeader()}
|
||||||
|
footer={renderFooter()}
|
||||||
|
onCancel={handleClose}
|
||||||
|
maskClosable={maskClosable}
|
||||||
|
escToExit={escToClose}
|
||||||
|
closable={closable}
|
||||||
|
alignCenter={alignCenter}
|
||||||
|
unmountOnExit
|
||||||
|
style={{
|
||||||
|
width: typeof width === 'number' ? `${width}px` : width,
|
||||||
|
}}
|
||||||
|
getPopupContainer={
|
||||||
|
mountContainer
|
||||||
|
? () => {
|
||||||
|
if (typeof mountContainer === 'string') {
|
||||||
|
return document.querySelector(mountContainer) || document.body;
|
||||||
|
}
|
||||||
|
return mountContainer;
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles['dialog-content']}
|
||||||
|
style={{
|
||||||
|
height: typeof height === 'number' ? `${height}px` : height,
|
||||||
|
maxHeight: height === 'auto' ? 'none' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Dialog.displayName = 'Dialog';
|
||||||
|
|
||||||
|
export default Dialog;
|
||||||
|
export type { DialogProps, DialogRef };
|
||||||
73
src/components/Dialog/interface.ts
Normal file
73
src/components/Dialog/interface.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog 组件属性接口
|
||||||
|
*/
|
||||||
|
export interface DialogProps {
|
||||||
|
/** 弹框标题 */
|
||||||
|
title?: ReactNode;
|
||||||
|
/** 弹框宽度 */
|
||||||
|
width?: number | string;
|
||||||
|
/** 内容区域高度 */
|
||||||
|
height?: number | string;
|
||||||
|
/** 确认按钮文本 */
|
||||||
|
confirmText?: string;
|
||||||
|
/** 取消按钮文本 */
|
||||||
|
cancelText?: string;
|
||||||
|
/** 是否显示确认按钮 */
|
||||||
|
showConfirm?: boolean;
|
||||||
|
/** 是否显示取消按钮 */
|
||||||
|
showCancel?: boolean;
|
||||||
|
/** 确认按钮加载状态 */
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
/** 确认按钮禁用状态 */
|
||||||
|
confirmDisabled?: boolean;
|
||||||
|
/** 隐藏底部按钮区域 */
|
||||||
|
footerHidden?: boolean;
|
||||||
|
/** 点击遮罩层是否关闭 */
|
||||||
|
maskClosable?: boolean;
|
||||||
|
/** ESC键是否关闭 */
|
||||||
|
escToClose?: boolean;
|
||||||
|
/** 是否显示关闭按钮 */
|
||||||
|
closable?: boolean;
|
||||||
|
/** 弹框类名 */
|
||||||
|
className?: string;
|
||||||
|
/** 自定义内容渲染 */
|
||||||
|
children?: ReactNode;
|
||||||
|
/** 自定义底部渲染 */
|
||||||
|
footer?: ReactNode | ((close: () => void) => ReactNode);
|
||||||
|
/** 自定义头部渲染 */
|
||||||
|
header?: ReactNode;
|
||||||
|
/** 是否居中显示 */
|
||||||
|
alignCenter?: boolean;
|
||||||
|
/** 挂载容器 */
|
||||||
|
mountContainer?: string | HTMLElement;
|
||||||
|
/** 点击确认按钮的回调 */
|
||||||
|
onConfirm?: () => void | Promise<void>;
|
||||||
|
/** 点击取消按钮的回调 */
|
||||||
|
onCancel?: () => void;
|
||||||
|
/** 关闭前的回调,返回 false 可以阻止关闭 */
|
||||||
|
beforeClose?: () => boolean | Promise<boolean>;
|
||||||
|
/** 关闭后的回调 */
|
||||||
|
onClose?: () => void;
|
||||||
|
/** 打开后的回调 */
|
||||||
|
onOpen?: () => void;
|
||||||
|
/** 受控模式:是否显示 */
|
||||||
|
visible?: boolean;
|
||||||
|
/** 受控模式:显示状态变化回调 */
|
||||||
|
onVisibleChange?: (visible: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog 组件实例方法
|
||||||
|
*/
|
||||||
|
export interface DialogRef {
|
||||||
|
/** 打开弹框 */
|
||||||
|
open: () => void;
|
||||||
|
/** 关闭弹框 */
|
||||||
|
close: () => void;
|
||||||
|
/** 快速关闭弹框(不触发 beforeClose) */
|
||||||
|
forceClose: () => void;
|
||||||
|
/** 获取当前显示状态 */
|
||||||
|
isVisible: () => boolean;
|
||||||
|
}
|
||||||
113
src/components/Dialog/style/index.module.less
Normal file
113
src/components/Dialog/style/index.module.less
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
.dialog-wrapper {
|
||||||
|
// 覆盖 Arco Design Modal 默认样式
|
||||||
|
:global {
|
||||||
|
.arco-modal {
|
||||||
|
// 弹框整体样式
|
||||||
|
.arco-modal-wrapper {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 头部样式
|
||||||
|
.arco-modal-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid var(--color-border-2, #f0f0f0);
|
||||||
|
background: var(--color-bg-2, #fff);
|
||||||
|
|
||||||
|
.arco-modal-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1, #1d2129);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭按钮样式
|
||||||
|
.arco-modal-close-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: var(--color-text-3, #86909c);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-fill-2, #f7f8fa);
|
||||||
|
color: var(--color-text-1, #1d2129);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域样式
|
||||||
|
.arco-modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--color-bg-1, #fff);
|
||||||
|
color: var(--color-text-2, #4e5969);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部样式
|
||||||
|
.arco-modal-footer {
|
||||||
|
padding: 12px 24px 16px;
|
||||||
|
border-top: 1px solid var(--color-border-2, #f0f0f0);
|
||||||
|
background: var(--color-bg-2, #fafafa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域
|
||||||
|
.dialog-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
// 自定义滚动条样式
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 20%);
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部按钮区域
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暗色主题适配
|
||||||
|
:global {
|
||||||
|
body[arco-theme='dark'] {
|
||||||
|
.dialog-wrapper {
|
||||||
|
.arco-modal {
|
||||||
|
.arco-modal-header {
|
||||||
|
background: var(--color-bg-3);
|
||||||
|
border-bottom-color: var(--color-border-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-body {
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-footer {
|
||||||
|
background: var(--color-bg-3);
|
||||||
|
border-top-color: var(--color-border-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/components/Pagination/index.tsx
Normal file
27
src/components/Pagination/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Pagination as ArcoPagination,
|
||||||
|
PaginationProps,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
|
import styles from './style/index.module.less';
|
||||||
|
|
||||||
|
interface CustomPaginationProps extends PaginationProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: React.FC<CustomPaginationProps> = (props) => {
|
||||||
|
const { className, ...restProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles['custom-pagination']} ${className || ''}`}>
|
||||||
|
<ArcoPagination
|
||||||
|
{...restProps}
|
||||||
|
showJumper={false}
|
||||||
|
sizeCanChange={true}
|
||||||
|
showTotal={(total) => `共 ${total} 条`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
||||||
154
src/components/Pagination/style/index.module.less
Normal file
154
src/components/Pagination/style/index.module.less
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
// 覆盖 Arco Design 分页组件的样式
|
||||||
|
:global {
|
||||||
|
.arco-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
// 总数显示
|
||||||
|
.arco-pagination-total-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4e5969;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码按钮样式
|
||||||
|
.arco-pagination-item {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 4px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
color: #4e5969;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e5e6eb;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前选中的页码
|
||||||
|
&.arco-pagination-item-active {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgb(var(--arcoblue-6)) 0%,
|
||||||
|
rgb(var(--arcoblue-7)) 100%
|
||||||
|
);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 8px rgba(var(--arcoblue-6), 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgb(var(--arcoblue-7)) 0%,
|
||||||
|
rgb(var(--arcoblue-8)) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 4px 12px rgba(var(--arcoblue-6), 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用状态
|
||||||
|
&.arco-pagination-item-disabled {
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
color: #c9cdd4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
color: #c9cdd4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 省略号
|
||||||
|
.arco-pagination-item-jumper {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一页/下一页按钮
|
||||||
|
.arco-pagination-item-previous,
|
||||||
|
.arco-pagination-item-next {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
.arco-icon {
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.arco-pagination-item-disabled):hover {
|
||||||
|
background-color: #e5e6eb;
|
||||||
|
|
||||||
|
.arco-icon {
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.arco-pagination-item-disabled {
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.arco-icon {
|
||||||
|
color: #c9cdd4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每页显示数量选择器
|
||||||
|
.arco-pagination-size-changer {
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
.arco-select-view {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e5e6eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转输入框
|
||||||
|
.arco-pagination-jumper {
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
.arco-pagination-jumper-input {
|
||||||
|
.arco-input-inner-wrapper {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgb(var(--arcoblue-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user