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