feat: 🎸 对接了企业岗位的部分信息
This commit is contained in:
147
src/components/Toast/README.md
Normal file
147
src/components/Toast/README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Toast 组件使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
自定义的全局 Toast 组件,支持命令式调用,无需在组件树中添加 Provider。
|
||||
|
||||
## 特性
|
||||
|
||||
- 🚀 轻量级,零依赖
|
||||
- 📱 响应式设计
|
||||
- 🎨 支持多种类型(success, error, warning, info)
|
||||
- ⚡ 命令式 API,简单易用
|
||||
- 🔧 高度可定制
|
||||
- 🌙 支持暗色主题
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 基本用法
|
||||
|
||||
```jsx
|
||||
import toast from "@/components/Toast";
|
||||
|
||||
// 成功提示
|
||||
toast.success("操作成功!");
|
||||
|
||||
// 错误提示
|
||||
toast.error("操作失败!");
|
||||
|
||||
// 警告提示
|
||||
toast.warning("请注意!");
|
||||
|
||||
// 信息提示
|
||||
toast.info("这是一条信息");
|
||||
```
|
||||
|
||||
### 2. 高级用法
|
||||
|
||||
```jsx
|
||||
// 自定义持续时间
|
||||
toast.success("3秒后消失", { duration: 3000 });
|
||||
|
||||
// 永不自动消失
|
||||
toast.info("手动关闭", { duration: 0 });
|
||||
|
||||
// 禁用关闭按钮
|
||||
toast.error("无法关闭", { closable: false });
|
||||
|
||||
// 带标题的提示
|
||||
toast.success("操作完成", {
|
||||
title: "成功",
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
// 获取 Toast ID,手动关闭
|
||||
const toastId = toast.info("loading...");
|
||||
setTimeout(() => {
|
||||
toast.remove(toastId);
|
||||
}, 2000);
|
||||
```
|
||||
|
||||
### 3. 在组件中使用 Hook(可选)
|
||||
|
||||
如果需要在组件内部管理 Toast 状态,可以使用 Provider 方式:
|
||||
|
||||
```jsx
|
||||
import { ToastProvider, useToast } from "@/components/Toast";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<YourComponent />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function YourComponent() {
|
||||
const { addToast, removeAllToasts } = useToast();
|
||||
|
||||
const handleClick = () => {
|
||||
addToast({
|
||||
type: "success",
|
||||
message: "这是通过 Hook 创建的 Toast",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>显示 Toast</button>
|
||||
<button onClick={removeAllToasts}>清空所有 Toast</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 全局方法
|
||||
|
||||
- `toast.success(message, options?)` - 显示成功提示
|
||||
- `toast.error(message, options?)` - 显示错误提示
|
||||
- `toast.warning(message, options?)` - 显示警告提示
|
||||
- `toast.info(message, options?)` - 显示信息提示
|
||||
- `toast.remove(id)` - 移除指定 Toast
|
||||
- `toast.removeAll()` - 移除所有 Toast
|
||||
|
||||
### Options 参数
|
||||
|
||||
```typescript
|
||||
interface ToastOptions {
|
||||
title?: string; // 标题
|
||||
duration?: number; // 持续时间(毫秒),0 表示不自动关闭
|
||||
closable?: boolean; // 是否显示关闭按钮,默认 true
|
||||
type?: "success" | "error" | "warning" | "info"; // Toast 类型
|
||||
}
|
||||
```
|
||||
|
||||
### Hook API
|
||||
|
||||
- `useToast()` - 返回 Toast 管理方法
|
||||
- `addToast(options)` - 添加 Toast
|
||||
- `removeToast(id)` - 移除指定 Toast
|
||||
- `removeAllToasts()` - 移除所有 Toast
|
||||
- `toasts` - 当前 Toast 列表
|
||||
|
||||
## 样式定制
|
||||
|
||||
可以通过修改 CSS 变量来定制样式:
|
||||
|
||||
```css
|
||||
.toast-item {
|
||||
/* 自定义背景色 */
|
||||
background: your-color;
|
||||
|
||||
/* 自定义圆角 */
|
||||
border-radius: your-radius;
|
||||
|
||||
/* 自定义阴影 */
|
||||
box-shadow: your-shadow;
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. Toast 容器会自动创建和销毁,无需手动管理
|
||||
2. 支持同时显示多个 Toast
|
||||
3. 在移动端会自动适配屏幕宽度
|
||||
4. 支持暗色主题自动切换
|
||||
178
src/components/Toast/index.css
Normal file
178
src/components/Toast/index.css
Normal file
@@ -0,0 +1,178 @@
|
||||
/* Toast 容器 */
|
||||
.toast-container,
|
||||
.global-toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.global-toast-container .toast-container {
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* Toast 项目 */
|
||||
.toast-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.08),
|
||||
0 9px 28px 0 rgba(0, 0, 0, 0.05),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12);
|
||||
border-left: 4px solid #d9d9d9;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 显示状态 */
|
||||
.toast-item.toast-visible {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 移除状态 */
|
||||
.toast-item.toast-removing {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Toast 类型样式 */
|
||||
.toast-item.toast-success {
|
||||
border-left-color: #52c41a;
|
||||
}
|
||||
|
||||
.toast-item.toast-success .toast-icon {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.toast-item.toast-error {
|
||||
border-left-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.toast-item.toast-error .toast-icon {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.toast-item.toast-warning {
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.toast-item.toast-warning .toast-icon {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.toast-item.toast-info {
|
||||
border-left-color: #1890ff;
|
||||
}
|
||||
|
||||
.toast-item.toast-info .toast-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
|
||||
/* Toast 内容 */
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #bfbfbf;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
margin-top: 2px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: #8c8c8c;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.toast-item:hover {
|
||||
box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.toast-container,
|
||||
.global-toast-container {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.toast-item {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色主题支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.toast-item {
|
||||
background: #1f1f1f;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.32),
|
||||
0 9px 28px 0 rgba(0, 0, 0, 0.24),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: #bfbfbf;
|
||||
background: #262626;
|
||||
}
|
||||
}
|
||||
259
src/components/Toast/index.jsx
Normal file
259
src/components/Toast/index.jsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect, createContext, useContext } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
|
||||
// Toast 上下文
|
||||
const ToastContext = createContext();
|
||||
|
||||
// Toast 类型
|
||||
const TOAST_TYPES = {
|
||||
SUCCESS: "success",
|
||||
ERROR: "error",
|
||||
WARNING: "warning",
|
||||
INFO: "info",
|
||||
};
|
||||
|
||||
// // Toast 图标
|
||||
// const TOAST_ICONS = {
|
||||
// [TOAST_TYPES.SUCCESS]: "✓",
|
||||
// [TOAST_TYPES.ERROR]: "✕",
|
||||
// [TOAST_TYPES.WARNING]: "⚠",
|
||||
// [TOAST_TYPES.INFO]: "ℹ",
|
||||
// };
|
||||
|
||||
// 单个 Toast 组件
|
||||
const ToastItem = ({ toast, onRemove }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 显示动画
|
||||
const showTimer = setTimeout(() => setIsVisible(true), 10);
|
||||
|
||||
// 自动移除
|
||||
const removeTimer = setTimeout(() => {
|
||||
handleRemove();
|
||||
}, toast.duration || 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(showTimer);
|
||||
clearTimeout(removeTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleRemove = () => {
|
||||
setIsRemoving(true);
|
||||
setTimeout(() => {
|
||||
onRemove(toast.id);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`toast-item toast-${toast.type} ${
|
||||
isVisible ? "toast-visible" : ""
|
||||
} ${isRemoving ? "toast-removing" : ""}`}
|
||||
onClick={toast.closable !== false ? handleRemove : undefined}
|
||||
>
|
||||
<div className="toast-content">
|
||||
{toast.title && <div className="toast-title">{toast.title}</div>}
|
||||
<div className="toast-message">{toast.message}</div>
|
||||
</div>
|
||||
{toast.closable !== false && (
|
||||
<button className="toast-close" onClick={handleRemove}>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Toast 容器组件
|
||||
const ToastContainer = () => {
|
||||
const { toasts, removeToast } = useContext(ToastContext);
|
||||
|
||||
return (
|
||||
<div className="toast-container">
|
||||
{toasts.map((toast) => (
|
||||
<ToastItem key={toast.id} toast={toast} onRemove={removeToast} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Toast Provider 组件
|
||||
export const ToastProvider = ({ children }) => {
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
const addToast = (toast) => {
|
||||
const id = Date.now() + Math.random();
|
||||
const newToast = {
|
||||
id,
|
||||
type: TOAST_TYPES.INFO,
|
||||
duration: 3000,
|
||||
closable: true,
|
||||
...toast,
|
||||
};
|
||||
|
||||
setToasts((prev) => [...prev, newToast]);
|
||||
return id;
|
||||
};
|
||||
|
||||
const removeToast = (id) => {
|
||||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||
};
|
||||
|
||||
const removeAllToasts = () => {
|
||||
setToasts([]);
|
||||
};
|
||||
|
||||
const value = {
|
||||
toasts,
|
||||
addToast,
|
||||
removeToast,
|
||||
removeAllToasts,
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={value}>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook for using toast
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error("useToast must be used within a ToastProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// 全局 Toast 管理器
|
||||
class ToastManager {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.root = null;
|
||||
this.toasts = [];
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
// 初始化容器
|
||||
init() {
|
||||
if (this.container) return;
|
||||
|
||||
this.container = document.createElement("div");
|
||||
this.container.className = "global-toast-container";
|
||||
document.body.appendChild(this.container);
|
||||
|
||||
this.root = createRoot(this.container);
|
||||
this.render();
|
||||
}
|
||||
|
||||
// 渲染 Toast 列表
|
||||
render() {
|
||||
if (!this.root) return;
|
||||
|
||||
this.root.render(
|
||||
<div className="toast-container">
|
||||
{this.toasts.map((toast) => (
|
||||
<ToastItem
|
||||
key={toast.id}
|
||||
toast={toast}
|
||||
onRemove={this.remove.bind(this)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 添加 Toast
|
||||
add(options) {
|
||||
this.init();
|
||||
|
||||
const id = Date.now() + Math.random();
|
||||
const toast = {
|
||||
id,
|
||||
type: TOAST_TYPES.INFO,
|
||||
duration: 3000,
|
||||
closable: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.toasts.push(toast);
|
||||
this.render();
|
||||
|
||||
// 自动移除
|
||||
if (toast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.remove(id);
|
||||
}, toast.duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// 移除 Toast
|
||||
remove(id) {
|
||||
this.toasts = this.toasts.filter((toast) => toast.id !== id);
|
||||
this.render();
|
||||
|
||||
// 如果没有 Toast 了,清理容器
|
||||
if (this.toasts.length === 0) {
|
||||
setTimeout(() => {
|
||||
this.cleanup();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除所有 Toast
|
||||
removeAll() {
|
||||
this.toasts = [];
|
||||
this.render();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
// 清理容器
|
||||
cleanup() {
|
||||
if (this.container && this.toasts.length === 0) {
|
||||
this.root?.unmount();
|
||||
document.body.removeChild(this.container);
|
||||
this.container = null;
|
||||
this.root = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 便捷方法
|
||||
success(message, options = {}) {
|
||||
return this.add({ ...options, message, type: TOAST_TYPES.SUCCESS });
|
||||
}
|
||||
|
||||
error(message, options = {}) {
|
||||
return this.add({ ...options, message, type: TOAST_TYPES.ERROR });
|
||||
}
|
||||
|
||||
warning(message, options = {}) {
|
||||
return this.add({ ...options, message, type: TOAST_TYPES.WARNING });
|
||||
}
|
||||
|
||||
info(message, options = {}) {
|
||||
return this.add({ ...options, message, type: TOAST_TYPES.INFO });
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const toastManager = new ToastManager();
|
||||
|
||||
// 导出全局 Toast API
|
||||
export const toast = {
|
||||
success: (message, options) => toastManager.success(message, options),
|
||||
error: (message, options) => toastManager.error(message, options),
|
||||
warning: (message, options) => toastManager.warning(message, options),
|
||||
info: (message, options) => toastManager.info(message, options),
|
||||
remove: (id) => toastManager.remove(id),
|
||||
removeAll: () => toastManager.removeAll(),
|
||||
};
|
||||
|
||||
export default toast;
|
||||
Reference in New Issue
Block a user