Compare commits
2 Commits
1d1f50ec10
...
ef8195390f
| Author | SHA1 | Date |
|---|---|---|
|
|
ef8195390f | |
|
|
5e56f765d0 |
|
|
@ -0,0 +1,61 @@
|
|||
# 文档迁移说明
|
||||
|
||||
## 📦 按钮扩展组件文档已迁移
|
||||
|
||||
原 `docs/` 目录下的按钮扩展相关文档已整合并迁移至:
|
||||
|
||||
**新位置:** `src/components/docs/ButtonExtensions.md`
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 迁移的文档
|
||||
|
||||
以下文档已整合到新文档中:
|
||||
|
||||
| 原文档 | 状态 |
|
||||
|--------|------|
|
||||
| ~~ButtonHelpDesign.md~~ | ✅ 已整合 |
|
||||
| ~~ButtonDesignFixes.md~~ | ✅ 已整合 |
|
||||
| ~~ButtonDesignUpdate.md~~ | ✅ 已整合 |
|
||||
| ~~ButtonDesignLatestFixes.md~~ | ✅ 已整合 |
|
||||
| ~~ActionHelpPanelFix.md~~ | ✅ 已整合 |
|
||||
|
||||
---
|
||||
|
||||
## 📖 新文档包含
|
||||
|
||||
- ✅ 5种设计方案完整说明
|
||||
- ✅ 所有组件的API文档
|
||||
- ✅ 详细的使用指南和示例
|
||||
- ✅ 最佳实践和性能优化建议
|
||||
- ✅ 完整的更新日志和变更记录
|
||||
|
||||
---
|
||||
|
||||
## 🔗 快速链接
|
||||
|
||||
- **文档路径:** `/src/components/docs/ButtonExtensions.md`
|
||||
- **在线演示:** http://localhost:5173/design/button-designs
|
||||
- **菜单路径:** 组件设计 → 扩展按钮
|
||||
|
||||
---
|
||||
|
||||
## 📅 迁移时间
|
||||
|
||||
**2025-11-17** - 所有文档已完成整合
|
||||
|
||||
---
|
||||
|
||||
## 💡 文档组织原则
|
||||
|
||||
今后组件文档将遵循以下原则:
|
||||
|
||||
1. **统一位置** - 所有组件文档放在 `src/components/docs/`
|
||||
2. **就近原则** - 文档与组件代码保持近距离
|
||||
3. **单一文档** - 同一功能的文档整合到一个文件
|
||||
4. **版本控制** - 使用版本号和更新日志记录变更
|
||||
|
||||
这样可以:
|
||||
- ✅ 更容易找到和维护文档
|
||||
- ✅ 避免文档分散和重复
|
||||
- ✅ 与代码保持同步更新
|
||||
|
|
@ -0,0 +1,751 @@
|
|||
# 按钮扩展组件设计文档
|
||||
|
||||
> **版本:** v1.2.0
|
||||
> **更新时间:** 2025-11-17
|
||||
> **作者:** Nex Design Team
|
||||
> **状态:** ✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 📖 目录
|
||||
|
||||
1. [概述](#概述)
|
||||
2. [设计方案](#设计方案)
|
||||
3. [组件API](#组件api)
|
||||
4. [使用指南](#使用指南)
|
||||
5. [最佳实践](#最佳实践)
|
||||
6. [更新日志](#更新日志)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
### 设计目标
|
||||
|
||||
为终端列表页面的按钮提供清晰的操作介绍和帮助信息,解决以下问题:
|
||||
- 用户不了解按钮功能
|
||||
- 复杂操作缺少引导
|
||||
- 需要快速的上下文帮助
|
||||
|
||||
### 设计原则
|
||||
|
||||
1. **简洁至上** - 去除不必要的动画,使用扁平化设计
|
||||
2. **功能独立** - 帮助功能不影响主操作流程
|
||||
3. **易于理解** - 直观的图标和交互模式
|
||||
4. **现代美学** - 符合Material Design和Fluent Design趋势
|
||||
|
||||
---
|
||||
|
||||
## 设计方案
|
||||
|
||||
我们提供了5种不同的设计方案,可根据实际场景选择使用:
|
||||
|
||||
### 方案1:增强型工具提示 (Enhanced Tooltip)
|
||||
|
||||
**特点:**
|
||||
- 渐变色彩背景,视觉效果出众
|
||||
- 支持标题、描述、快捷键、注意事项
|
||||
- 带有脉冲动画的提示图标
|
||||
- 响应式设计,移动端友好
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 简单操作,需要快速了解功能
|
||||
- ✅ 不希望占用额外页面空间
|
||||
- ✅ 信息量较少的提示
|
||||
|
||||
**效果预览:**
|
||||
```
|
||||
[按钮] (i) ← 脉冲动画图标
|
||||
↓ 悬停
|
||||
┌────────────────────────┐
|
||||
│ 新增主机 │
|
||||
│ 向系统中添加新的主机... │
|
||||
│ 快捷键: Ctrl+N │
|
||||
│ 注意: 请确保IP不冲突 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案2:智能帮助面板 (Smart Help Panel) ⭐ 推荐
|
||||
|
||||
**特点:**
|
||||
- 侧边抽屉式设计,信息完整
|
||||
- 实时显示当前悬停按钮的详细信息
|
||||
- 包含使用场景、操作步骤、注意事项等
|
||||
- 支持查看所有可用操作的快速索引
|
||||
- 点击"?"图标直接打开帮助面板
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 复杂业务操作,需要详细说明
|
||||
- ✅ 新用户培训和引导
|
||||
- ✅ 功能较多,需要分步骤引导
|
||||
|
||||
**交互流程:**
|
||||
```
|
||||
1. 悬停按钮 → 出现"?"图标
|
||||
2. 点击"?" → 打开右侧帮助面板
|
||||
3. 显示详细内容:
|
||||
- 功能说明
|
||||
- 使用场景
|
||||
- 操作步骤
|
||||
- 注意事项
|
||||
- 快捷键
|
||||
- 权限要求
|
||||
```
|
||||
|
||||
**特殊优化:**
|
||||
- 面板打开时,鼠标离开按钮不会清空内容
|
||||
- 点击"所有可用操作"卡片可切换内容
|
||||
- 自动展开"当前操作"区域
|
||||
|
||||
---
|
||||
|
||||
### 方案3:悬浮展开卡片 (Hover Expand Card)
|
||||
|
||||
**特点:**
|
||||
- 精美的悬浮卡片设计
|
||||
- 滑入动画,视觉流畅
|
||||
- 分层信息展示
|
||||
- 使用React Portal渲染,永不被遮挡
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 需要展示较多信息
|
||||
- ✅ 不想打断操作流程
|
||||
- ✅ 希望信息就近显示
|
||||
|
||||
**技术实现:**
|
||||
```jsx
|
||||
// 使用Portal避免z-index遮挡问题
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
{createPortal(renderCard(), document.body)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案4:智能引导 (Smart Guide)
|
||||
|
||||
**特点:**
|
||||
- 简洁扁平设计,独立帮助图标
|
||||
- 点击查看弹窗式详细引导
|
||||
- 包含步骤式操作说明
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 需要详细引导,但不希望干扰主流程
|
||||
- ✅ 功能上线初期的用户引导
|
||||
- ✅ 复杂操作的分步说明
|
||||
|
||||
**设计对比:**
|
||||
```
|
||||
旧版(已废弃):脉冲动画徽章,过于复杂
|
||||
新版:简洁帮助图标,点击查看详情
|
||||
|
||||
┌────────────┐ ┌─┐
|
||||
│ 新增主机 │ │?│ ← 简洁优雅
|
||||
└────────────┘ └─┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案5:底部固定提示栏 (Bottom Hint Bar)
|
||||
|
||||
**特点:**
|
||||
- 固定底部位置,不遮挡操作区域
|
||||
- 实时更新,流畅切换
|
||||
- 三种主题可选(渐变、浅色、深色)
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 需要始终可见的提示信息
|
||||
- ✅ 不希望被tooltip遮挡操作区域
|
||||
- ✅ 简单的实时信息展示
|
||||
|
||||
---
|
||||
|
||||
## 组件API
|
||||
|
||||
### ButtonWithTip
|
||||
|
||||
增强型工具提示按钮组件。
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface ButtonWithTipProps {
|
||||
label: string // 按钮文本
|
||||
icon?: ReactNode // 按钮图标
|
||||
type?: 'primary' | 'default' // 按钮类型
|
||||
danger?: boolean // 是否为危险按钮
|
||||
disabled?: boolean // 是否禁用
|
||||
onClick?: () => void // 点击回调
|
||||
size?: 'small' | 'middle' | 'large'
|
||||
showTipIcon?: boolean // 是否显示提示图标
|
||||
tip?: {
|
||||
title?: string // 提示标题
|
||||
description?: string // 详细描述
|
||||
shortcut?: string // 快捷键
|
||||
notes?: string[] // 注意事项
|
||||
placement?: 'top' | 'bottom' | 'left' | 'right'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```jsx
|
||||
import ButtonWithTip from '@/components/ButtonWithTip/ButtonWithTip'
|
||||
|
||||
<ButtonWithTip
|
||||
label="新增主机"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
tip={{
|
||||
title: '新增主机',
|
||||
description: '向系统中添加新的主机终端设备',
|
||||
shortcut: 'Ctrl+N',
|
||||
notes: ['请确保IP地址不与现有主机冲突', 'MAC地址必须唯一']
|
||||
}}
|
||||
onClick={handleAdd}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ActionHelpPanel
|
||||
|
||||
智能帮助面板组件(方案2)。
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface ActionHelpPanelProps {
|
||||
visible: boolean // 是否显示面板
|
||||
onClose: () => void // 关闭回调
|
||||
currentAction?: { // 当前操作信息
|
||||
title: string
|
||||
icon: ReactNode
|
||||
description: string
|
||||
scenarios?: string[] // 使用场景
|
||||
steps?: string[] // 操作步骤
|
||||
warnings?: string[] // 注意事项
|
||||
shortcut?: string // 快捷键
|
||||
permission?: string // 权限要求
|
||||
badge?: {
|
||||
text: string
|
||||
color: string
|
||||
}
|
||||
}
|
||||
allActions?: Array<Action> // 所有可用操作
|
||||
placement?: 'left' | 'right' // 面板位置
|
||||
onActionSelect?: (action: Action) => void // 选择操作回调
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```jsx
|
||||
import ActionHelpPanel from '@/components/ActionHelpPanel/ActionHelpPanel'
|
||||
|
||||
const [showPanel, setShowPanel] = useState(false)
|
||||
const [currentAction, setCurrentAction] = useState(null)
|
||||
|
||||
// 在按钮外层添加hover和点击事件
|
||||
<div
|
||||
onMouseEnter={() => setCurrentAction(actionsConfig.add)}
|
||||
onMouseLeave={() => !showPanel && setCurrentAction(null)}
|
||||
>
|
||||
<Button>新增主机</Button>
|
||||
<button onClick={() => setShowPanel(true)}>?</button>
|
||||
</div>
|
||||
|
||||
<ActionHelpPanel
|
||||
visible={showPanel}
|
||||
onClose={() => setShowPanel(false)}
|
||||
currentAction={currentAction}
|
||||
allActions={Object.values(actionsConfig)}
|
||||
onActionSelect={(action) => setCurrentAction(action)}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ButtonWithHoverCard
|
||||
|
||||
悬浮展开卡片按钮组件(方案3)。
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface ButtonWithHoverCardProps {
|
||||
label: string
|
||||
icon?: ReactNode
|
||||
type?: 'primary' | 'default'
|
||||
danger?: boolean
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
size?: 'small' | 'middle' | 'large'
|
||||
cardInfo?: {
|
||||
title: string
|
||||
icon: ReactNode
|
||||
description: string
|
||||
scenarios?: string[]
|
||||
quickTips?: string[]
|
||||
warnings?: string[]
|
||||
shortcut?: string
|
||||
badge?: { text: string; color: string }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**技术要点:**
|
||||
- 使用 `createPortal` 渲染卡片到 `document.body`
|
||||
- 使用 `useRef` 获取按钮位置
|
||||
- 避免z-index遮挡问题
|
||||
|
||||
---
|
||||
|
||||
### ButtonWithGuide
|
||||
|
||||
智能引导按钮组件(方案4)。
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface ButtonWithGuideProps {
|
||||
label: string
|
||||
icon?: ReactNode
|
||||
type?: 'primary' | 'default'
|
||||
danger?: boolean
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
size?: 'small' | 'middle' | 'large'
|
||||
guide?: {
|
||||
title: string
|
||||
icon: ReactNode
|
||||
description: string
|
||||
steps?: string[]
|
||||
scenarios?: string[]
|
||||
warnings?: string[]
|
||||
shortcut?: string
|
||||
permission?: string
|
||||
badge?: { text: string; color: string }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**特点:**
|
||||
- 简洁的帮助图标(不影响按钮文字)
|
||||
- 点击打开弹窗式详细引导
|
||||
- 包含步骤式说明(使用Ant Design Steps组件)
|
||||
|
||||
---
|
||||
|
||||
### BottomHintBar
|
||||
|
||||
底部固定提示栏组件(方案5)。
|
||||
|
||||
**Props:**
|
||||
|
||||
```typescript
|
||||
interface BottomHintBarProps {
|
||||
visible: boolean
|
||||
hintInfo?: {
|
||||
title: string
|
||||
icon: ReactNode
|
||||
description: string
|
||||
quickTip?: string
|
||||
warning?: string
|
||||
shortcut?: string
|
||||
badge?: { text: string; color: string }
|
||||
}
|
||||
onClose?: () => void
|
||||
theme?: 'light' | 'dark' | 'gradient'
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```jsx
|
||||
import BottomHintBar from '@/components/BottomHintBar/BottomHintBar'
|
||||
|
||||
const [showHint, setShowHint] = useState(false)
|
||||
const [hintInfo, setHintInfo] = useState(null)
|
||||
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setHintInfo(actionConfig)
|
||||
setShowHint(true)
|
||||
}}
|
||||
onMouseLeave={() => setShowHint(false)}
|
||||
>
|
||||
<Button>操作按钮</Button>
|
||||
</div>
|
||||
|
||||
<BottomHintBar
|
||||
visible={showHint}
|
||||
hintInfo={hintInfo}
|
||||
theme="gradient"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. **选择合适的方案**
|
||||
|
||||
根据使用场景选择组件:
|
||||
|
||||
| 场景 | 推荐方案 |
|
||||
|------|---------|
|
||||
| 简单列表页面,快速提示 | 方案1 增强型工具提示 |
|
||||
| 复杂后台系统,详细引导 | 方案2 智能帮助面板 ⭐ |
|
||||
| 数据密集页面,信息丰富 | 方案3 悬浮展开卡片 |
|
||||
| 新功能上线,用户引导 | 方案4 智能引导 |
|
||||
| 操作频繁,始终可见 | 方案5 底部固定提示栏 |
|
||||
|
||||
2. **导入组件**
|
||||
|
||||
```jsx
|
||||
// 方案1
|
||||
import ButtonWithTip from '@/components/ButtonWithTip/ButtonWithTip'
|
||||
|
||||
// 方案2
|
||||
import ActionHelpPanel from '@/components/ActionHelpPanel/ActionHelpPanel'
|
||||
|
||||
// 方案3
|
||||
import ButtonWithHoverCard from '@/components/ButtonWithHoverCard/ButtonWithHoverCard'
|
||||
|
||||
// 方案4
|
||||
import ButtonWithGuide from '@/components/ButtonWithGuide/ButtonWithGuide'
|
||||
|
||||
// 方案5
|
||||
import BottomHintBar from '@/components/BottomHintBar/BottomHintBar'
|
||||
```
|
||||
|
||||
3. **配置操作信息**
|
||||
|
||||
建议将操作配置统一管理:
|
||||
|
||||
```jsx
|
||||
const actionsConfig = {
|
||||
add: {
|
||||
title: '新增主机',
|
||||
icon: <PlusOutlined />,
|
||||
description: '向系统中添加新的主机终端设备',
|
||||
scenarios: [
|
||||
'当有新设备需要接入系统管理时',
|
||||
'需要扩展终端设备数量时'
|
||||
],
|
||||
steps: [
|
||||
'点击"新增主机"按钮',
|
||||
'填写主机基本信息',
|
||||
'选择主机所属分组',
|
||||
'保存并等待主机上线'
|
||||
],
|
||||
warnings: [
|
||||
'请确保IP地址不与现有主机冲突',
|
||||
'MAC地址必须唯一且格式正确'
|
||||
],
|
||||
shortcut: 'Ctrl+N',
|
||||
permission: '管理员权限',
|
||||
badge: { text: '常用', color: 'blue' }
|
||||
},
|
||||
// ... 其他操作
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案2完整实现示例
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { Button } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import ActionHelpPanel from '@/components/ActionHelpPanel/ActionHelpPanel'
|
||||
|
||||
function MyPage() {
|
||||
const [showHelpPanel, setShowHelpPanel] = useState(false)
|
||||
const [currentAction, setCurrentAction] = useState(null)
|
||||
|
||||
// 处理悬停
|
||||
const handleHover = (actionKey) => {
|
||||
if (!showHelpPanel) {
|
||||
setCurrentAction(actionsConfig[actionKey])
|
||||
}
|
||||
}
|
||||
|
||||
// 处理离开
|
||||
const handleLeave = () => {
|
||||
if (!showHelpPanel) {
|
||||
setCurrentAction(null)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理点击
|
||||
const handleHelpClick = (e, actionKey) => {
|
||||
e.stopPropagation()
|
||||
setCurrentAction(actionsConfig[actionKey])
|
||||
setShowHelpPanel(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 按钮区域 */}
|
||||
<div
|
||||
className="button-wrapper"
|
||||
onMouseEnter={() => handleHover('add')}
|
||||
onMouseLeave={handleLeave}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
新增主机
|
||||
</Button>
|
||||
<button
|
||||
className="help-icon"
|
||||
onClick={(e) => handleHelpClick(e, 'add')}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 帮助面板 */}
|
||||
<ActionHelpPanel
|
||||
visible={showHelpPanel}
|
||||
onClose={() => setShowHelpPanel(false)}
|
||||
currentAction={currentAction}
|
||||
allActions={Object.values(actionsConfig)}
|
||||
onActionSelect={(action) => setCurrentAction(action)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 内容编写规范
|
||||
|
||||
**功能描述:**
|
||||
- 用简洁的语言描述功能(1-2句话)
|
||||
- 突出重点和目的
|
||||
- 避免技术术语
|
||||
|
||||
**使用场景:**
|
||||
- 列举2-4个实际使用场景
|
||||
- 使用"当...时"的句式
|
||||
- 帮助用户理解何时使用
|
||||
|
||||
**操作步骤:**
|
||||
- 按实际操作顺序编写
|
||||
- 每步一句话,清晰明确
|
||||
- 使用动词开头
|
||||
|
||||
**注意事项:**
|
||||
- 危险操作必须有明确警告
|
||||
- 使用醒目的颜色标记
|
||||
- 提供实际的使用建议
|
||||
|
||||
### 2. 性能优化
|
||||
|
||||
```jsx
|
||||
// ✅ 使用useMemo缓存配置
|
||||
const actionsConfig = useMemo(() => ({
|
||||
add: { ... },
|
||||
delete: { ... }
|
||||
}), [])
|
||||
|
||||
// ✅ 防止不必要的重渲染
|
||||
const handleHover = useCallback((key) => {
|
||||
if (!showPanel) {
|
||||
setCurrentAction(actionsConfig[key])
|
||||
}
|
||||
}, [showPanel, actionsConfig])
|
||||
|
||||
// ✅ 面板打开时避免hover更新
|
||||
if (!showPanel) {
|
||||
setCurrentAction(null)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 可访问性
|
||||
|
||||
```jsx
|
||||
// 添加title属性
|
||||
<button className="help-icon" title="查看帮助">
|
||||
?
|
||||
</button>
|
||||
|
||||
// 添加aria标签
|
||||
<Button aria-label="新增主机">
|
||||
新增主机
|
||||
</Button>
|
||||
|
||||
// 快捷键支持
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.ctrlKey && e.key === 'n') {
|
||||
handleAdd()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyPress)
|
||||
return () => window.removeEventListener('keydown', handleKeyPress)
|
||||
}, [])
|
||||
```
|
||||
|
||||
### 4. 响应式适配
|
||||
|
||||
```css
|
||||
/* 移动端隐藏某些提示 */
|
||||
@media (max-width: 768px) {
|
||||
.help-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hover-info-card {
|
||||
width: 100%;
|
||||
left: 0 !important;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2.0 (2025-11-17)
|
||||
|
||||
**新增:**
|
||||
- ✨ 新增 `ButtonWithGuide` 组件,替代复杂的徽章设计
|
||||
- ✨ 方案2添加 `onActionSelect` 回调,支持点击卡片切换内容
|
||||
|
||||
**修复:**
|
||||
- 🐛 修复方案2点击"?"后内容消失的问题
|
||||
- 🐛 修复方案2"所有可用操作"卡片无法点击的问题
|
||||
- 🐛 修复方案3悬浮卡片被Card遮挡(使用Portal)
|
||||
- 🐛 修复菜单图标,从 `GlobalOutlined` 改为 `BlockOutlined`
|
||||
|
||||
**优化:**
|
||||
- 💄 简化方案4设计,去掉复杂的脉冲动画
|
||||
- 💄 方案2面板打开时不响应鼠标hover事件
|
||||
- 💄 提升整体视觉一致性
|
||||
|
||||
### v1.1.0 (2025-11-17)
|
||||
|
||||
**新增:**
|
||||
- ✨ 新增 `ActionHelpPanel` 智能帮助面板组件
|
||||
- ✨ 新增 `BottomHintBar` 底部提示栏组件
|
||||
- ✨ 方案2添加点击"?"图标直接打开面板功能
|
||||
|
||||
**修复:**
|
||||
- 🐛 修复 `ButtonWithTip` 的 Tooltip overlayClassName 警告
|
||||
- 🐛 修复 `AppSider` 的 findDOMNode 警告(使用 items 配置)
|
||||
- 🐛 修复方案5底部提示栏鼠标移动时闪烁的问题
|
||||
|
||||
**优化:**
|
||||
- 💄 为方案2按钮添加悬停时的"?"图标提示
|
||||
- 💄 提升方案3悬浮卡片的 z-index 和阴影效果
|
||||
|
||||
### v1.0.0 (2025-11-17)
|
||||
|
||||
**初始版本:**
|
||||
- 🎉 发布5种按钮扩展设计方案
|
||||
- 📦 提供完整的组件API和使用文档
|
||||
- 📝 提供详细的设计指南和最佳实践
|
||||
|
||||
---
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 依赖项
|
||||
|
||||
```json
|
||||
{
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"antd": "^5.x",
|
||||
"@ant-design/icons": "^5.x"
|
||||
}
|
||||
```
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── ButtonWithTip/
|
||||
│ ├── ButtonWithTip.jsx
|
||||
│ └── ButtonWithTip.css
|
||||
├── ActionHelpPanel/
|
||||
│ ├── ActionHelpPanel.jsx
|
||||
│ └── ActionHelpPanel.css
|
||||
├── ButtonWithHoverCard/
|
||||
│ ├── ButtonWithHoverCard.jsx
|
||||
│ └── ButtonWithHoverCard.css
|
||||
├── ButtonWithGuide/
|
||||
│ ├── ButtonWithGuide.jsx
|
||||
│ └── ButtonWithGuide.css
|
||||
└── BottomHintBar/
|
||||
├── BottomHintBar.jsx
|
||||
└── BottomHintBar.css
|
||||
|
||||
src/pages/
|
||||
└── AllButtonDesigns.jsx # 演示页面
|
||||
|
||||
docs/
|
||||
└── components/
|
||||
└── ButtonExtensions.md # 本文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 如何选择合适的方案?
|
||||
|
||||
**A:** 根据以下因素选择:
|
||||
- **信息量**:少 → 方案1,多 → 方案2/3
|
||||
- **复杂度**:简单 → 方案1/5,复杂 → 方案2/4
|
||||
- **使用频率**:频繁 → 方案5,偶尔 → 方案1/3
|
||||
- **用户类型**:新手 → 方案2/4,熟练 → 方案1/5
|
||||
|
||||
### Q2: 可以混合使用多种方案吗?
|
||||
|
||||
**A:** 可以。建议:
|
||||
- 同一页面使用统一的方案
|
||||
- 不同页面可以使用不同方案
|
||||
- 核心操作使用方案2,次要操作使用方案1
|
||||
|
||||
### Q3: 如何自定义样式?
|
||||
|
||||
**A:** 所有组件都支持自定义样式:
|
||||
|
||||
```jsx
|
||||
// 通过className
|
||||
<ButtonWithTip className="my-custom-button" />
|
||||
|
||||
// 通过style
|
||||
<ButtonWithTip style={{ marginRight: 16 }} />
|
||||
|
||||
// 修改CSS变量
|
||||
:root {
|
||||
--tip-primary-gradient: linear-gradient(...);
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: 移动端如何适配?
|
||||
|
||||
**A:** 所有组件都内置了响应式支持:
|
||||
- 方案1:移动端隐藏提示图标
|
||||
- 方案2:面板宽度自适应
|
||||
- 方案3:卡片居中显示
|
||||
- 方案5:提示栏自适应布局
|
||||
|
||||
---
|
||||
|
||||
**最后更新:** 2025-11-17
|
||||
**文档版本:** v1.2.0
|
||||
|
|
@ -6,6 +6,7 @@ import UserListPage from './pages/UserListPage'
|
|||
import ImageListPage from './pages/ImageListPage'
|
||||
import VirtualMachineImagePage from './pages/VirtualMachineImagePage'
|
||||
import DocsPage from './pages/DocsPage'
|
||||
import AllButtonDesigns from './pages/AllButtonDesigns'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
|
@ -18,6 +19,7 @@ function App() {
|
|||
<Route path="/image/system" element={<ImageListPage />} />
|
||||
<Route path="/image/vm" element={<VirtualMachineImagePage />} />
|
||||
<Route path="/design" element={<DocsPage />} />
|
||||
<Route path="/design/button-designs" element={<AllButtonDesigns />} />
|
||||
{/* 其他路由将在后续添加 */}
|
||||
</Routes>
|
||||
</MainLayout>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
/* 帮助面板样式 */
|
||||
.action-help-panel .ant-drawer-header {
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.help-panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.help-panel-header-text {
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
/* 操作详情样式 */
|
||||
.help-action-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.help-action-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.help-action-icon {
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.help-action-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.help-action-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.help-action-badge {
|
||||
align-self: flex-start;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 帮助区块样式 */
|
||||
.help-section {
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #1677ff;
|
||||
}
|
||||
|
||||
.help-section-warning {
|
||||
background: #fff7e6;
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.help-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.help-section-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.help-section-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.help-section-list li {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.help-section-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.help-section-steps {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
counter-reset: step-counter;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.help-section-steps li {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
counter-increment: step-counter;
|
||||
}
|
||||
|
||||
.help-section-steps li:before {
|
||||
content: counter(step-counter);
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #1677ff;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-section-steps li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.help-shortcut {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.help-shortcut kbd {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05);
|
||||
font-size: 12px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 操作列表样式 */
|
||||
.help-actions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.help-action-item {
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-action-item:hover {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.help-action-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.help-action-item-icon {
|
||||
font-size: 16px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.help-action-item-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.help-action-item-shortcut {
|
||||
padding: 2px 6px;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.help-action-item-desc {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
/* 折叠面板自定义样式 */
|
||||
.action-help-panel .ant-collapse-ghost > .ant-collapse-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-help-panel .ant-collapse-ghost > .ant-collapse-item > .ant-collapse-header {
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-help-panel .ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.action-help-panel .ant-drawer-content-wrapper {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.help-action-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Drawer, Collapse, Badge, Tag, Empty } from 'antd'
|
||||
import {
|
||||
QuestionCircleOutlined,
|
||||
BulbOutlined,
|
||||
WarningOutlined,
|
||||
InfoCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import './ActionHelpPanel.css'
|
||||
|
||||
const { Panel } = Collapse
|
||||
|
||||
/**
|
||||
* 操作帮助面板组件
|
||||
* 在页面侧边显示当前操作的详细说明和帮助信息
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.visible - 是否显示面板
|
||||
* @param {Function} props.onClose - 关闭回调
|
||||
* @param {Object} props.currentAction - 当前操作信息
|
||||
* @param {Array} props.allActions - 所有可用操作列表
|
||||
* @param {string} props.placement - 面板位置
|
||||
* @param {Function} props.onActionSelect - 选择操作的回调
|
||||
*/
|
||||
function ActionHelpPanel({
|
||||
visible = false,
|
||||
onClose,
|
||||
currentAction = null,
|
||||
allActions = [],
|
||||
placement = 'right',
|
||||
onActionSelect,
|
||||
}) {
|
||||
const [activeKey, setActiveKey] = useState(['current'])
|
||||
|
||||
// 当 currentAction 变化时,自动展开"当前操作"面板
|
||||
useEffect(() => {
|
||||
if (currentAction && visible) {
|
||||
setActiveKey(['current'])
|
||||
}
|
||||
}, [currentAction, visible])
|
||||
|
||||
// 渲染当前操作详情
|
||||
const renderCurrentAction = () => {
|
||||
if (!currentAction) {
|
||||
return (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="将鼠标悬停在按钮上查看帮助"
|
||||
style={{ padding: '40px 0' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="help-action-detail">
|
||||
{/* 操作标题 */}
|
||||
<div className="help-action-header">
|
||||
<div className="help-action-icon">{currentAction.icon}</div>
|
||||
<div className="help-action-info">
|
||||
<h3 className="help-action-title">{currentAction.title}</h3>
|
||||
{currentAction.badge && (
|
||||
<Tag color={currentAction.badge.color} className="help-action-badge">
|
||||
{currentAction.badge.text}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作描述 */}
|
||||
{currentAction.description && (
|
||||
<div className="help-section">
|
||||
<div className="help-section-title">
|
||||
<InfoCircleOutlined /> 功能说明
|
||||
</div>
|
||||
<div className="help-section-content">{currentAction.description}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用场景 */}
|
||||
{currentAction.scenarios && currentAction.scenarios.length > 0 && (
|
||||
<div className="help-section">
|
||||
<div className="help-section-title">
|
||||
<BulbOutlined /> 使用场景
|
||||
</div>
|
||||
<ul className="help-section-list">
|
||||
{currentAction.scenarios.map((scenario, index) => (
|
||||
<li key={index}>{scenario}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作步骤 */}
|
||||
{currentAction.steps && currentAction.steps.length > 0 && (
|
||||
<div className="help-section">
|
||||
<div className="help-section-title">
|
||||
<ThunderboltOutlined /> 操作步骤
|
||||
</div>
|
||||
<ol className="help-section-steps">
|
||||
{currentAction.steps.map((step, index) => (
|
||||
<li key={index}>{step}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 注意事项 */}
|
||||
{currentAction.warnings && currentAction.warnings.length > 0 && (
|
||||
<div className="help-section help-section-warning">
|
||||
<div className="help-section-title">
|
||||
<WarningOutlined /> 注意事项
|
||||
</div>
|
||||
<ul className="help-section-list">
|
||||
{currentAction.warnings.map((warning, index) => (
|
||||
<li key={index}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快捷键 */}
|
||||
{currentAction.shortcut && (
|
||||
<div className="help-section">
|
||||
<div className="help-section-title">⌨️ 快捷键</div>
|
||||
<div className="help-shortcut">
|
||||
<kbd>{currentAction.shortcut}</kbd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 权限要求 */}
|
||||
{currentAction.permission && (
|
||||
<div className="help-section">
|
||||
<div className="help-section-title">🔐 权限要求</div>
|
||||
<div className="help-section-content">
|
||||
<Tag color="blue">{currentAction.permission}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 渲染所有操作列表
|
||||
const renderAllActions = () => {
|
||||
if (allActions.length === 0) {
|
||||
return <Empty description="暂无操作" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="help-actions-list">
|
||||
{allActions.map((action, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="help-action-item"
|
||||
onClick={() => {
|
||||
if (onActionSelect) {
|
||||
onActionSelect(action)
|
||||
setActiveKey(['current'])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="help-action-item-header">
|
||||
<span className="help-action-item-icon">{action.icon}</span>
|
||||
<span className="help-action-item-title">{action.title}</span>
|
||||
{action.shortcut && (
|
||||
<kbd className="help-action-item-shortcut">{action.shortcut}</kbd>
|
||||
)}
|
||||
</div>
|
||||
<div className="help-action-item-desc">{action.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<div className="help-panel-title">
|
||||
<QuestionCircleOutlined style={{ marginRight: 8 }} />
|
||||
操作帮助
|
||||
{currentAction && <Badge status="processing" text="实时帮助" />}
|
||||
</div>
|
||||
}
|
||||
placement={placement}
|
||||
width={420}
|
||||
open={visible}
|
||||
onClose={onClose}
|
||||
className="action-help-panel"
|
||||
>
|
||||
<Collapse
|
||||
activeKey={activeKey}
|
||||
onChange={setActiveKey}
|
||||
ghost
|
||||
expandIconPosition="end"
|
||||
>
|
||||
<Panel
|
||||
header={
|
||||
<div className="help-panel-header">
|
||||
<span className="help-panel-header-text">当前操作</span>
|
||||
{currentAction && (
|
||||
<Badge
|
||||
count="实时"
|
||||
style={{
|
||||
backgroundColor: '#52c41a',
|
||||
fontSize: 10,
|
||||
height: 18,
|
||||
lineHeight: '18px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key="current"
|
||||
>
|
||||
{renderCurrentAction()}
|
||||
</Panel>
|
||||
|
||||
<Panel header="所有可用操作" key="all">
|
||||
{renderAllActions()}
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionHelpPanel
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/* 底部提示栏基础样式 */
|
||||
.bottom-hint-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
padding: 12px 24px;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 主题样式 */
|
||||
.bottom-hint-bar-light {
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-dark {
|
||||
background: #001529;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 容器布局 */
|
||||
.hint-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 左侧区域 */
|
||||
.hint-bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hint-bar-icon {
|
||||
font-size: 24px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-icon {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.hint-bar-title-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hint-bar-title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-title {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.hint-bar-badge {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 中间区域 */
|
||||
.hint-bar-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hint-bar-description,
|
||||
.hint-bar-quick-tip,
|
||||
.hint-bar-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-description,
|
||||
.bottom-hint-bar-light .hint-bar-quick-tip {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.hint-info-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-info-icon {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.hint-tip-icon {
|
||||
font-size: 14px;
|
||||
color: #fadb14;
|
||||
}
|
||||
|
||||
.hint-warning-icon {
|
||||
font-size: 14px;
|
||||
color: #ff7a45;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-warning {
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
.hint-bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hint-bar-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shortcut-label {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.shortcut-kbd {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .shortcut-kbd {
|
||||
background: #f0f0f0;
|
||||
border-color: #d9d9d9;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.hint-bar-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hint-bar-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-close {
|
||||
background: #f0f0f0;
|
||||
border-color: #d9d9d9;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-close:hover {
|
||||
background: #e0e0e0;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
/* 进度指示条 */
|
||||
.hint-bar-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hint-bar-progress::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
animation: progressWave 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-progress {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.bottom-hint-bar-light .hint-bar-progress::after {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
@keyframes progressWave {
|
||||
0%, 100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1024px) {
|
||||
.hint-bar-container {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hint-bar-center {
|
||||
flex-basis: 100%;
|
||||
order: 3;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hint-bar-description,
|
||||
.hint-bar-quick-tip,
|
||||
.hint-bar-warning {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bottom-hint-bar {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.hint-bar-left {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hint-bar-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.hint-bar-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hint-bar-right {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shortcut-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hint-bar-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hint-bar-quick-tip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hint-bar-warning {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { Tag } from 'antd'
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
BulbOutlined,
|
||||
WarningOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import './BottomHintBar.css'
|
||||
|
||||
/**
|
||||
* 底部固定提示栏组件
|
||||
* 在页面底部显示当前悬停按钮的实时说明
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.visible - 是否显示提示栏
|
||||
* @param {Object} props.hintInfo - 当前提示信息
|
||||
* @param {Function} props.onClose - 关闭回调
|
||||
* @param {string} props.theme - 主题:light, dark, gradient
|
||||
*/
|
||||
function BottomHintBar({ visible = false, hintInfo = null, onClose, theme = 'gradient' }) {
|
||||
if (!visible || !hintInfo) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bottom-hint-bar bottom-hint-bar-${theme}`}
|
||||
onMouseEnter={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="hint-bar-container">
|
||||
{/* 左侧:图标和标题 */}
|
||||
<div className="hint-bar-left">
|
||||
<div className="hint-bar-icon">{hintInfo.icon}</div>
|
||||
<div className="hint-bar-title-section">
|
||||
<h4 className="hint-bar-title">{hintInfo.title}</h4>
|
||||
{hintInfo.badge && (
|
||||
<Tag color={hintInfo.badge.color} className="hint-bar-badge">
|
||||
{hintInfo.badge.text}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间:主要信息 */}
|
||||
<div className="hint-bar-center">
|
||||
{/* 描述 */}
|
||||
{hintInfo.description && (
|
||||
<div className="hint-bar-description">
|
||||
<InfoCircleOutlined className="hint-info-icon" />
|
||||
<span>{hintInfo.description}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快速提示 */}
|
||||
{hintInfo.quickTip && (
|
||||
<div className="hint-bar-quick-tip">
|
||||
<BulbOutlined className="hint-tip-icon" />
|
||||
<span>{hintInfo.quickTip}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 警告 */}
|
||||
{hintInfo.warning && (
|
||||
<div className="hint-bar-warning">
|
||||
<WarningOutlined className="hint-warning-icon" />
|
||||
<span>{hintInfo.warning}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:快捷键和关闭 */}
|
||||
<div className="hint-bar-right">
|
||||
{hintInfo.shortcut && (
|
||||
<div className="hint-bar-shortcut">
|
||||
<span className="shortcut-label">快捷键</span>
|
||||
<kbd className="shortcut-kbd">{hintInfo.shortcut}</kbd>
|
||||
</div>
|
||||
)}
|
||||
{onClose && (
|
||||
<button className="hint-bar-close" onClick={onClose}>
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度指示条 */}
|
||||
<div className="hint-bar-progress" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomHintBar
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/* 按钮带引导 - 简洁现代设计 */
|
||||
.button-with-guide {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 帮助图标按钮 - 简洁扁平设计 */
|
||||
.guide-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.guide-icon-btn:hover {
|
||||
background: rgba(22, 119, 255, 0.06);
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.guide-icon-btn:active {
|
||||
background: rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
/* 引导弹窗样式 */
|
||||
.button-guide-modal .ant-modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.button-guide-modal .ant-modal-body {
|
||||
padding: 24px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.guide-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guide-modal-icon {
|
||||
font-size: 24px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.guide-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.guide-modal-badge {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* 引导区块样式 */
|
||||
.guide-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #1677ff;
|
||||
}
|
||||
|
||||
.guide-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.guide-section-warning {
|
||||
background: #fff7e6;
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.guide-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guide-section-icon {
|
||||
font-size: 16px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.guide-section-warning .guide-section-icon {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.guide-section-content {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.guide-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.guide-list li {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.guide-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 步骤样式 */
|
||||
.guide-steps {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.guide-steps .ant-steps-item-title {
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.guide-steps .ant-steps-item-description {
|
||||
font-size: 13px !important;
|
||||
line-height: 1.6 !important;
|
||||
color: rgba(0, 0, 0, 0.65) !important;
|
||||
}
|
||||
|
||||
/* 引导底部 */
|
||||
.guide-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.guide-footer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guide-footer-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.guide-footer-kbd {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05);
|
||||
font-size: 11px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.button-guide-modal {
|
||||
max-width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.button-guide-modal .ant-modal-body {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.guide-footer {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { useState } from 'react'
|
||||
import { Button, Modal, Steps, Tag } from 'antd'
|
||||
import {
|
||||
QuestionCircleOutlined,
|
||||
BulbOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import './ButtonWithGuide.css'
|
||||
|
||||
/**
|
||||
* 带引导的按钮组件 - 简洁现代设计
|
||||
* 在按钮旁边显示一个简洁的帮助图标,点击后显示详细引导
|
||||
*/
|
||||
function ButtonWithGuide({
|
||||
label,
|
||||
icon,
|
||||
type = 'default',
|
||||
danger = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
guide,
|
||||
size = 'middle',
|
||||
...restProps
|
||||
}) {
|
||||
const [showGuideModal, setShowGuideModal] = useState(false)
|
||||
|
||||
const handleGuideClick = (e) => {
|
||||
e.stopPropagation()
|
||||
if (guide) {
|
||||
setShowGuideModal(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="button-with-guide">
|
||||
<Button
|
||||
type={type}
|
||||
icon={icon}
|
||||
danger={danger}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{guide && !disabled && (
|
||||
<button className="guide-icon-btn" onClick={handleGuideClick} title="查看帮助">
|
||||
<QuestionCircleOutlined />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 引导弹窗 */}
|
||||
{guide && (
|
||||
<Modal
|
||||
title={
|
||||
<div className="guide-modal-header">
|
||||
<span className="guide-modal-icon">{guide.icon || icon}</span>
|
||||
<span className="guide-modal-title">{guide.title}</span>
|
||||
{guide.badge && (
|
||||
<Tag color={guide.badge.color} className="guide-modal-badge">
|
||||
{guide.badge.text}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
open={showGuideModal}
|
||||
onCancel={() => setShowGuideModal(false)}
|
||||
footer={[
|
||||
<Button key="close" type="primary" onClick={() => setShowGuideModal(false)}>
|
||||
知道了
|
||||
</Button>,
|
||||
]}
|
||||
width={600}
|
||||
className="button-guide-modal"
|
||||
>
|
||||
{/* 功能描述 */}
|
||||
{guide.description && (
|
||||
<div className="guide-section">
|
||||
<div className="guide-section-title">
|
||||
<InfoCircleOutlined className="guide-section-icon" />
|
||||
功能说明
|
||||
</div>
|
||||
<p className="guide-section-content">{guide.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用步骤 */}
|
||||
{guide.steps && guide.steps.length > 0 && (
|
||||
<div className="guide-section">
|
||||
<div className="guide-section-title">
|
||||
<CheckCircleOutlined className="guide-section-icon" />
|
||||
操作步骤
|
||||
</div>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={guide.steps.map((step, index) => ({
|
||||
title: `步骤 ${index + 1}`,
|
||||
description: step,
|
||||
status: 'wait',
|
||||
}))}
|
||||
className="guide-steps"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用场景 */}
|
||||
{guide.scenarios && guide.scenarios.length > 0 && (
|
||||
<div className="guide-section">
|
||||
<div className="guide-section-title">
|
||||
<BulbOutlined className="guide-section-icon" />
|
||||
适用场景
|
||||
</div>
|
||||
<ul className="guide-list">
|
||||
{guide.scenarios.map((scenario, index) => (
|
||||
<li key={index}>{scenario}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 注意事项 */}
|
||||
{guide.warnings && guide.warnings.length > 0 && (
|
||||
<div className="guide-section guide-section-warning">
|
||||
<div className="guide-section-title">
|
||||
<WarningOutlined className="guide-section-icon" />
|
||||
注意事项
|
||||
</div>
|
||||
<ul className="guide-list">
|
||||
{guide.warnings.map((warning, index) => (
|
||||
<li key={index}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快捷键和权限 */}
|
||||
{(guide.shortcut || guide.permission) && (
|
||||
<div className="guide-footer">
|
||||
{guide.shortcut && (
|
||||
<div className="guide-footer-item">
|
||||
<span className="guide-footer-label">快捷键:</span>
|
||||
<kbd className="guide-footer-kbd">{guide.shortcut}</kbd>
|
||||
</div>
|
||||
)}
|
||||
{guide.permission && (
|
||||
<div className="guide-footer-item">
|
||||
<span className="guide-footer-label">权限要求:</span>
|
||||
<Tag color="blue">{guide.permission}</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonWithGuide
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
.button-guide-badge-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 引导徽章样式 - 改为放在右上角外部 */
|
||||
.button-guide-badge-wrapper .ant-badge {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.button-guide-badge-wrapper .ant-badge-count {
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 引导徽章样式 */
|
||||
.guide-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background: #1677ff;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
animation: pulseBadge 2s ease-in-out infinite;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.4);
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.guide-badge:hover {
|
||||
animation: none;
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.6);
|
||||
}
|
||||
|
||||
.guide-badge-new {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.4);
|
||||
}
|
||||
|
||||
.guide-badge-new:hover {
|
||||
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.6);
|
||||
}
|
||||
|
||||
.guide-badge-help {
|
||||
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.4);
|
||||
}
|
||||
|
||||
.guide-badge-help:hover {
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.6);
|
||||
}
|
||||
|
||||
.guide-badge-warn {
|
||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
|
||||
box-shadow: 0 2px 8px rgba(250, 173, 20, 0.4);
|
||||
}
|
||||
|
||||
.guide-badge-warn:hover {
|
||||
box-shadow: 0 4px 12px rgba(250, 173, 20, 0.6);
|
||||
}
|
||||
|
||||
@keyframes pulseBadge {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 引导弹窗样式 */
|
||||
.button-guide-modal .ant-modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.button-guide-modal .ant-modal-body {
|
||||
padding: 24px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.guide-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guide-modal-icon {
|
||||
font-size: 24px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.guide-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.guide-modal-badge {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* 引导区块样式 */
|
||||
.guide-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #1677ff;
|
||||
}
|
||||
|
||||
.guide-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.guide-section-warning {
|
||||
background: #fff7e6;
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.guide-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guide-section-icon {
|
||||
font-size: 16px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.guide-section-warning .guide-section-icon {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.guide-section-content {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.guide-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.guide-list li {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.guide-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 步骤样式 */
|
||||
.guide-steps {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.guide-steps .ant-steps-item-title {
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.guide-steps .ant-steps-item-description {
|
||||
font-size: 13px !important;
|
||||
line-height: 1.6 !important;
|
||||
color: rgba(0, 0, 0, 0.65) !important;
|
||||
}
|
||||
|
||||
/* 引导底部 */
|
||||
.guide-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.guide-footer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guide-footer-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.guide-footer-kbd {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05);
|
||||
font-size: 11px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.button-guide-modal {
|
||||
max-width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.button-guide-modal .ant-modal-body {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.guide-footer {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import { useState } from 'react'
|
||||
import { Button, Badge, Modal, Steps, Tag, Divider } from 'antd'
|
||||
import {
|
||||
QuestionCircleOutlined,
|
||||
BulbOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import './ButtonWithGuideBadge.css'
|
||||
|
||||
/**
|
||||
* 智能引导徽章按钮组件
|
||||
* 为新功能或复杂按钮添加脉冲动画的徽章,点击后显示详细引导
|
||||
* @param {Object} props
|
||||
* @param {string} props.label - 按钮文本
|
||||
* @param {ReactNode} props.icon - 按钮图标
|
||||
* @param {string} props.type - 按钮类型
|
||||
* @param {boolean} props.danger - 危险按钮
|
||||
* @param {boolean} props.disabled - 禁用状态
|
||||
* @param {Function} props.onClick - 点击回调
|
||||
* @param {Object} props.guide - 引导配置
|
||||
* @param {boolean} props.showBadge - 是否显示徽章
|
||||
* @param {string} props.badgeType - 徽章类型:new, help, warn
|
||||
* @param {string} props.size - 按钮大小
|
||||
*/
|
||||
function ButtonWithGuideBadge({
|
||||
label,
|
||||
icon,
|
||||
type = 'default',
|
||||
danger = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
guide,
|
||||
showBadge = true,
|
||||
badgeType = 'help',
|
||||
size = 'middle',
|
||||
...restProps
|
||||
}) {
|
||||
const [showGuideModal, setShowGuideModal] = useState(false)
|
||||
|
||||
const handleBadgeClick = (e) => {
|
||||
e.stopPropagation()
|
||||
if (guide) {
|
||||
setShowGuideModal(true)
|
||||
}
|
||||
}
|
||||
|
||||
const getBadgeConfig = () => {
|
||||
const configs = {
|
||||
new: {
|
||||
text: 'NEW',
|
||||
color: '#52c41a',
|
||||
icon: <InfoCircleOutlined />,
|
||||
},
|
||||
help: {
|
||||
text: '?',
|
||||
color: '#1677ff',
|
||||
icon: <QuestionCircleOutlined />,
|
||||
},
|
||||
warn: {
|
||||
text: '!',
|
||||
color: '#faad14',
|
||||
icon: <WarningOutlined />,
|
||||
},
|
||||
}
|
||||
return configs[badgeType] || configs.help
|
||||
}
|
||||
|
||||
const badgeConfig = getBadgeConfig()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="button-guide-badge-wrapper">
|
||||
{showBadge && guide && !disabled ? (
|
||||
<Badge
|
||||
count={
|
||||
<div
|
||||
className={`guide-badge guide-badge-${badgeType}`}
|
||||
onClick={handleBadgeClick}
|
||||
>
|
||||
{badgeConfig.icon}
|
||||
</div>
|
||||
}
|
||||
offset={[-5, 5]}
|
||||
>
|
||||
<Button
|
||||
type={type}
|
||||
icon={icon}
|
||||
danger={danger}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</Badge>
|
||||
) : (
|
||||
<Button
|
||||
type={type}
|
||||
icon={icon}
|
||||
danger={danger}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 引导弹窗 */}
|
||||
{guide && (
|
||||
<Modal
|
||||
title={
|
||||
<div className="guide-modal-header">
|
||||
<span className="guide-modal-icon">{guide.icon || icon}</span>
|
||||
<span className="guide-modal-title">{guide.title}</span>
|
||||
{guide.badge && (
|
||||
<Tag color={guide.badge.color} className="guide-modal-badge">
|
||||
{guide.badge.text}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
open={showGuideModal}
|
||||
onCancel={() => setShowGuideModal(false)}
|
||||
footer={[
|
||||
<Button key="close" type="primary" onClick={() => setShowGuideModal(false)}>
|
||||
知道了
|
||||
</Button>,
|
||||
]}
|
||||
width={600}
|
||||
className="button-guide-modal"
|
||||
>
|
||||
{/* 功能描述 */}
|
||||
{guide.description && (
|
||||
<div className="guide-section">
|
||||
<div className="guide-section-title">
|
||||
<InfoCircleOutlined className="guide-section-icon" />
|
||||
功能说明
|
||||
</div>
|
||||
<p className="guide-section-content">{guide.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用步骤 */}
|
||||
{guide.steps && guide.steps.length > 0 && (
|
||||
<div className="guide-section">
|
||||
<div className="guide-section-title">
|
||||
<CheckCircleOutlined className="guide-section-icon" />
|
||||
操作步骤
|
||||
</div>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={guide.steps.map((step, index) => ({
|
||||
title: `步骤 ${index + 1}`,
|
||||
description: step,
|
||||
status: 'wait',
|
||||
}))}
|
||||
className="guide-steps"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用场景 */}
|
||||
{guide.scenarios && guide.scenarios.length > 0 && (
|
||||
<div className="guide-section">
|
||||
<div className="guide-section-title">
|
||||
<BulbOutlined className="guide-section-icon" />
|
||||
适用场景
|
||||
</div>
|
||||
<ul className="guide-list">
|
||||
{guide.scenarios.map((scenario, index) => (
|
||||
<li key={index}>{scenario}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 注意事项 */}
|
||||
{guide.warnings && guide.warnings.length > 0 && (
|
||||
<div className="guide-section guide-section-warning">
|
||||
<div className="guide-section-title">
|
||||
<WarningOutlined className="guide-section-icon" />
|
||||
注意事项
|
||||
</div>
|
||||
<ul className="guide-list">
|
||||
{guide.warnings.map((warning, index) => (
|
||||
<li key={index}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快捷键和权限 */}
|
||||
{(guide.shortcut || guide.permission) && (
|
||||
<div className="guide-footer">
|
||||
{guide.shortcut && (
|
||||
<div className="guide-footer-item">
|
||||
<span className="guide-footer-label">快捷键:</span>
|
||||
<kbd className="guide-footer-kbd">{guide.shortcut}</kbd>
|
||||
</div>
|
||||
)}
|
||||
{guide.permission && (
|
||||
<div className="guide-footer-item">
|
||||
<span className="guide-footer-label">权限要求:</span>
|
||||
<Tag color="blue">{guide.permission}</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonWithGuideBadge
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
.button-hover-card-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 悬浮卡片 */
|
||||
.hover-info-card {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
animation: slideInRight 0.3s ease forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hover-info-card-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hover-info-card-content {
|
||||
width: 340px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 12px 28px rgba(0, 0, 0, 0.12),
|
||||
0 6px 12px rgba(0, 0, 0, 0.08),
|
||||
0 0 2px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hover-info-card-content .ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.hover-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.hover-card-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hover-card-icon {
|
||||
font-size: 20px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.hover-card-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.hover-card-badge {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 卡片描述 */
|
||||
.hover-card-description {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
/* 卡片区块 */
|
||||
.hover-card-section {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #1677ff;
|
||||
}
|
||||
|
||||
.hover-card-warning {
|
||||
background: #fff7e6;
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.hover-card-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 12px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.hover-card-warning .section-icon {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.hover-card-list {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.hover-card-list li {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.hover-card-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
.hover-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.footer-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.footer-kbd {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05);
|
||||
font-size: 11px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.hover-info-card-content {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.hover-info-card {
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-50%) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
import { useState, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Button, Card, Tag } from 'antd'
|
||||
import {
|
||||
BulbOutlined,
|
||||
WarningOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import './ButtonWithHoverCard.css'
|
||||
|
||||
/**
|
||||
* 悬浮展开卡片按钮组件
|
||||
* 鼠标悬停时,在按钮旁边展开一个精美的信息卡片
|
||||
* @param {Object} props
|
||||
* @param {string} props.label - 按钮文本
|
||||
* @param {ReactNode} props.icon - 按钮图标
|
||||
* @param {string} props.type - 按钮类型
|
||||
* @param {boolean} props.danger - 危险按钮
|
||||
* @param {boolean} props.disabled - 禁用状态
|
||||
* @param {Function} props.onClick - 点击回调
|
||||
* @param {Object} props.cardInfo - 卡片信息配置
|
||||
* @param {string} props.size - 按钮大小
|
||||
*/
|
||||
function ButtonWithHoverCard({
|
||||
label,
|
||||
icon,
|
||||
type = 'default',
|
||||
danger = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
cardInfo,
|
||||
size = 'middle',
|
||||
...restProps
|
||||
}) {
|
||||
const [showCard, setShowCard] = useState(false)
|
||||
const [cardPosition, setCardPosition] = useState({ top: 0, left: 0 })
|
||||
const wrapperRef = useRef(null)
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!cardInfo || disabled) return
|
||||
|
||||
if (wrapperRef.current) {
|
||||
const rect = wrapperRef.current.getBoundingClientRect()
|
||||
setCardPosition({
|
||||
top: rect.top + rect.height / 2,
|
||||
left: rect.right + 12,
|
||||
})
|
||||
}
|
||||
setShowCard(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setShowCard(false)
|
||||
}
|
||||
|
||||
// 渲染悬浮卡片
|
||||
const renderCard = () => {
|
||||
if (!showCard || !cardInfo) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`hover-info-card ${showCard ? 'hover-info-card-visible' : ''}`}
|
||||
style={{
|
||||
top: cardPosition.top,
|
||||
left: cardPosition.left,
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
size="small"
|
||||
bordered={false}
|
||||
className="hover-info-card-content"
|
||||
>
|
||||
{/* 标题区 */}
|
||||
<div className="hover-card-header">
|
||||
<div className="hover-card-title-wrapper">
|
||||
{cardInfo.icon && (
|
||||
<span className="hover-card-icon">{cardInfo.icon}</span>
|
||||
)}
|
||||
<h4 className="hover-card-title">{cardInfo.title}</h4>
|
||||
</div>
|
||||
{cardInfo.badge && (
|
||||
<Tag color={cardInfo.badge.color} className="hover-card-badge">
|
||||
{cardInfo.badge.text}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
{cardInfo.description && (
|
||||
<div className="hover-card-section">
|
||||
<p className="hover-card-description">{cardInfo.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用场景 */}
|
||||
{cardInfo.scenarios && cardInfo.scenarios.length > 0 && (
|
||||
<div className="hover-card-section">
|
||||
<div className="hover-card-section-title">
|
||||
<BulbOutlined className="section-icon" />
|
||||
使用场景
|
||||
</div>
|
||||
<ul className="hover-card-list">
|
||||
{cardInfo.scenarios.slice(0, 2).map((scenario, index) => (
|
||||
<li key={index}>{scenario}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快速提示 */}
|
||||
{cardInfo.quickTips && cardInfo.quickTips.length > 0 && (
|
||||
<div className="hover-card-section">
|
||||
<div className="hover-card-section-title">
|
||||
<ThunderboltOutlined className="section-icon" />
|
||||
快速提示
|
||||
</div>
|
||||
<ul className="hover-card-list">
|
||||
{cardInfo.quickTips.map((tip, index) => (
|
||||
<li key={index}>{tip}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 注意事项 */}
|
||||
{cardInfo.warnings && cardInfo.warnings.length > 0 && (
|
||||
<div className="hover-card-section hover-card-warning">
|
||||
<div className="hover-card-section-title">
|
||||
<WarningOutlined className="section-icon" />
|
||||
注意
|
||||
</div>
|
||||
<ul className="hover-card-list">
|
||||
{cardInfo.warnings.slice(0, 2).map((warning, index) => (
|
||||
<li key={index}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快捷键 */}
|
||||
{cardInfo.shortcut && (
|
||||
<div className="hover-card-footer">
|
||||
<span className="footer-label">快捷键</span>
|
||||
<kbd className="footer-kbd">{cardInfo.shortcut}</kbd>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className="button-hover-card-wrapper"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Button
|
||||
type={type}
|
||||
icon={icon}
|
||||
danger={danger}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 使用 Portal 渲染悬浮卡片到 body */}
|
||||
{typeof document !== 'undefined' && createPortal(renderCard(), document.body)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonWithHoverCard
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/* 按钮包裹容器 */
|
||||
.button-with-tip-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.button-with-tip {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 提示指示器 */
|
||||
.button-tip-indicator {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: help;
|
||||
transition: all 0.3s ease;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.button-with-tip-wrapper:hover .button-tip-indicator {
|
||||
color: #1677ff;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* 脉冲动画 */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 提示框样式 */
|
||||
.button-tip-overlay {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.button-tip-overlay .ant-tooltip-inner {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.button-tip-overlay .ant-tooltip-arrow {
|
||||
--antd-arrow-background-color: #667eea;
|
||||
}
|
||||
|
||||
.button-tip-overlay .ant-tooltip-arrow-content {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
/* 提示内容布局 */
|
||||
.button-tip-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.button-tip-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.button-tip-description {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button-tip-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.tip-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.tip-kbd {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button-tip-notes {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.tip-notes-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tip-notes-list {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.tip-notes-list li {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tip-notes-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 不同主题的提示框 */
|
||||
.tip-theme-success.button-tip-overlay .ant-tooltip-inner {
|
||||
background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
|
||||
}
|
||||
|
||||
.tip-theme-warning.button-tip-overlay .ant-tooltip-inner {
|
||||
background: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
}
|
||||
|
||||
.tip-theme-danger.button-tip-overlay .ant-tooltip-inner {
|
||||
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
||||
}
|
||||
|
||||
.tip-theme-info.button-tip-overlay .ant-tooltip-inner {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.button-tip-overlay {
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.button-tip-indicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import { Button, Tooltip } from 'antd'
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import './ButtonWithTip.css'
|
||||
|
||||
/**
|
||||
* 带有增强提示的按钮组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.label - 按钮文本
|
||||
* @param {ReactNode} props.icon - 按钮图标
|
||||
* @param {string} props.type - 按钮类型
|
||||
* @param {boolean} props.danger - 危险按钮
|
||||
* @param {boolean} props.disabled - 禁用状态
|
||||
* @param {Function} props.onClick - 点击回调
|
||||
* @param {Object} props.tip - 提示配置
|
||||
* @param {string} props.tip.title - 提示标题
|
||||
* @param {string} props.tip.description - 详细描述
|
||||
* @param {string} props.tip.shortcut - 快捷键提示
|
||||
* @param {Array} props.tip.notes - 注意事项列表
|
||||
* @param {string} props.tip.placement - 提示位置
|
||||
* @param {boolean} props.showTipIcon - 是否显示提示图标
|
||||
* @param {string} props.size - 按钮大小
|
||||
*/
|
||||
function ButtonWithTip({
|
||||
label,
|
||||
icon,
|
||||
type = 'default',
|
||||
danger = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
tip,
|
||||
showTipIcon = true,
|
||||
size = 'middle',
|
||||
...restProps
|
||||
}) {
|
||||
// 如果没有提示配置,直接返回普通按钮
|
||||
if (!tip) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
icon={icon}
|
||||
danger={danger}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 构建提示内容
|
||||
const tooltipContent = (
|
||||
<div className="button-tip-content">
|
||||
{tip.title && <div className="button-tip-title">{tip.title}</div>}
|
||||
{tip.description && <div className="button-tip-description">{tip.description}</div>}
|
||||
{tip.shortcut && (
|
||||
<div className="button-tip-shortcut">
|
||||
<span className="tip-label">快捷键:</span>
|
||||
<kbd className="tip-kbd">{tip.shortcut}</kbd>
|
||||
</div>
|
||||
)}
|
||||
{tip.notes && tip.notes.length > 0 && (
|
||||
<div className="button-tip-notes">
|
||||
<div className="tip-notes-title">注意事项:</div>
|
||||
<ul className="tip-notes-list">
|
||||
{tip.notes.map((note, index) => (
|
||||
<li key={index}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltipContent}
|
||||
placement={tip.placement || 'top'}
|
||||
classNames={{ root: 'button-tip-overlay' }}
|
||||
mouseEnterDelay={0.3}
|
||||
arrow={{ pointAtCenter: true }}
|
||||
>
|
||||
<div className="button-with-tip-wrapper">
|
||||
<Button
|
||||
type={type}
|
||||
icon={icon}
|
||||
danger={danger}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
className="button-with-tip"
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{showTipIcon && !disabled && (
|
||||
<QuestionCircleOutlined className="button-tip-indicator" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonWithTip
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Layout, Menu, Badge, Tooltip } from 'antd'
|
||||
import { Layout, Menu, Badge } from 'antd'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
|
|
@ -8,12 +8,12 @@ import {
|
|||
UserOutlined,
|
||||
AppstoreOutlined,
|
||||
SettingOutlined,
|
||||
BlockOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import menuData from '../../data/menuData.json'
|
||||
import './AppSider.css'
|
||||
|
||||
const { Sider } = Layout
|
||||
const { SubMenu } = Menu
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
|
|
@ -23,6 +23,7 @@ const iconMap = {
|
|||
UserOutlined: UserOutlined,
|
||||
AppstoreOutlined: AppstoreOutlined,
|
||||
SettingOutlined: SettingOutlined,
|
||||
BlockOutlined: BlockOutlined,
|
||||
}
|
||||
|
||||
function AppSider({ collapsed, onToggle }) {
|
||||
|
|
@ -86,63 +87,42 @@ function AppSider({ collapsed, onToggle }) {
|
|||
return 'overview'
|
||||
}
|
||||
|
||||
// 渲染菜单项
|
||||
const renderMenuItems = () => {
|
||||
// 生成菜单项配置
|
||||
const getMenuItems = () => {
|
||||
return menuData.map((item) => {
|
||||
const IconComponent = iconMap[item.icon]
|
||||
const icon = IconComponent ? <IconComponent /> : null
|
||||
|
||||
// 如果有子菜单
|
||||
if (item.children) {
|
||||
// 在收拢状态下,用 Tooltip 包装 SubMenu 的图标
|
||||
const subMenuIcon = collapsed ? (
|
||||
<Tooltip title={item.label} placement="right">
|
||||
{icon}
|
||||
</Tooltip>
|
||||
) : (
|
||||
icon
|
||||
)
|
||||
|
||||
return (
|
||||
<SubMenu
|
||||
key={item.key}
|
||||
icon={subMenuIcon}
|
||||
title={item.label}
|
||||
popupClassName="sider-submenu-popup"
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<Menu.Item key={child.key}>
|
||||
{child.badge ? (
|
||||
<span className="menu-item-with-badge">
|
||||
{child.label}
|
||||
<Badge
|
||||
count={child.badge}
|
||||
className={`menu-badge ${child.badge === 'HOT' ? 'badge-hot' : 'badge-new'}`}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
child.label
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</SubMenu>
|
||||
)
|
||||
return {
|
||||
key: item.key,
|
||||
icon: icon,
|
||||
label: item.label,
|
||||
popupClassName: 'sider-submenu-popup',
|
||||
children: item.children.map((child) => ({
|
||||
key: child.key,
|
||||
label: child.badge ? (
|
||||
<span className="menu-item-with-badge">
|
||||
{child.label}
|
||||
<Badge
|
||||
count={child.badge}
|
||||
className={`menu-badge ${child.badge === 'HOT' ? 'badge-hot' : 'badge-new'}`}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
child.label
|
||||
),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
// 普通菜单项 - 也用 Tooltip 包装
|
||||
const menuIcon = collapsed ? (
|
||||
<Tooltip title={item.label} placement="right">
|
||||
{icon}
|
||||
</Tooltip>
|
||||
) : (
|
||||
icon
|
||||
)
|
||||
|
||||
return (
|
||||
<Menu.Item key={item.key} icon={menuIcon} title="">
|
||||
{item.label}
|
||||
</Menu.Item>
|
||||
)
|
||||
// 普通菜单项
|
||||
return {
|
||||
key: item.key,
|
||||
icon: icon,
|
||||
label: item.label,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -162,9 +142,8 @@ function AppSider({ collapsed, onToggle }) {
|
|||
onOpenChange={handleOpenChange}
|
||||
onClick={handleMenuClick}
|
||||
className="sider-menu"
|
||||
>
|
||||
{renderMenuItems()}
|
||||
</Menu>
|
||||
items={getMenuItems()}
|
||||
/>
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@
|
|||
"key": "chart-panel",
|
||||
"label": "ChartPanel",
|
||||
"path": "/docs/components/ChartPanel.md"
|
||||
},
|
||||
{
|
||||
"key": "button-extension",
|
||||
"label": "ButtonExtension",
|
||||
"path": "/docs/components/ButtonExtension.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@
|
|||
"icon": "DashboardOutlined",
|
||||
"path": "/overview"
|
||||
},
|
||||
{
|
||||
"key": "design",
|
||||
"label": "组件设计",
|
||||
"icon": "BlockOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "button-designs",
|
||||
"label": "扩展按钮",
|
||||
"path": "/design/button-designs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "network",
|
||||
"label": "网络管理",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
.button-help-example {
|
||||
padding: 32px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
margin-bottom: 48px;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.example-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin: 0 0 24px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.example-buttons {
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.button-help-example {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.example-buttons {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
import { useState } from 'react'
|
||||
import { Space, FloatButton } from 'antd'
|
||||
import {
|
||||
PlusOutlined,
|
||||
PoweroffOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import ButtonWithTip from '../components/ButtonWithTip/ButtonWithTip'
|
||||
import ActionHelpPanel from '../components/ActionHelpPanel/ActionHelpPanel'
|
||||
import './ButtonHelpExample.css'
|
||||
|
||||
/**
|
||||
* 按钮帮助功能使用示例
|
||||
* 展示如何在终端列表页面中使用增强型按钮提示和帮助面板
|
||||
*/
|
||||
function ButtonHelpExample() {
|
||||
const [showHelpPanel, setShowHelpPanel] = useState(false)
|
||||
const [currentAction, setCurrentAction] = useState(null)
|
||||
|
||||
// 定义所有操作的详细信息
|
||||
const actionsConfig = {
|
||||
add: {
|
||||
title: '新增主机',
|
||||
icon: <PlusOutlined />,
|
||||
description: '向系统中添加新的主机终端设备,可以是服务器、台式机或笔记本电脑。',
|
||||
scenarios: [
|
||||
'当有新设备需要接入系统管理时',
|
||||
'需要扩展终端设备数量时',
|
||||
'替换旧设备,添加新设备时',
|
||||
],
|
||||
steps: [
|
||||
'点击"新增主机"按钮',
|
||||
'填写主机基本信息(名称、IP、MAC地址等)',
|
||||
'选择主机所属分组',
|
||||
'配置系统和数据盘容量',
|
||||
'保存并等待主机上线',
|
||||
],
|
||||
warnings: [
|
||||
'请确保IP地址不与现有主机冲突',
|
||||
'MAC地址必须唯一且格式正确',
|
||||
'建议先进行网络连通性测试',
|
||||
],
|
||||
shortcut: 'Ctrl+N',
|
||||
permission: '管理员权限',
|
||||
badge: { text: '常用', color: 'blue' },
|
||||
},
|
||||
batchPowerOn: {
|
||||
title: '批量开机',
|
||||
icon: <PoweroffOutlined />,
|
||||
description: '同时对多台选中的主机执行开机操作,适用于需要批量启动设备的场景。',
|
||||
scenarios: [
|
||||
'每日上班时批量启动办公设备',
|
||||
'定时任务触发的批量开机',
|
||||
'维护后批量恢复设备运行',
|
||||
],
|
||||
steps: [
|
||||
'在列表中勾选需要开机的主机',
|
||||
'点击"批量开机"按钮',
|
||||
'确认操作(系统会显示选中的主机数量)',
|
||||
'等待开机命令发送并监控状态变化',
|
||||
],
|
||||
warnings: [
|
||||
'仅对离线状态的主机有效',
|
||||
'请确保网络环境支持远程开机(Wake-on-LAN)',
|
||||
'大量主机同时开机可能导致网络负载较高',
|
||||
],
|
||||
shortcut: 'Ctrl+Shift+O',
|
||||
permission: '操作员权限',
|
||||
badge: { text: '批量操作', color: 'green' },
|
||||
},
|
||||
batchPowerOff: {
|
||||
title: '批量关机',
|
||||
icon: <PoweroffOutlined />,
|
||||
description: '同时对多台选中的主机执行关机操作,支持优雅关机和强制关机。',
|
||||
scenarios: [
|
||||
'每日下班时批量关闭办公设备节能',
|
||||
'系统维护前批量关闭设备',
|
||||
'紧急情况下批量关闭设备',
|
||||
],
|
||||
steps: [
|
||||
'在列表中勾选需要关机的主机',
|
||||
'点击"批量关机"按钮',
|
||||
'选择关机方式(优雅关机/强制关机)',
|
||||
'确认操作并等待关机完成',
|
||||
],
|
||||
warnings: [
|
||||
'仅对在线状态的主机有效',
|
||||
'强制关机可能导致数据丢失,请谨慎使用',
|
||||
'关机前请确保用户已保存工作',
|
||||
'建议优先使用优雅关机方式',
|
||||
],
|
||||
shortcut: 'Ctrl+Shift+P',
|
||||
permission: '管理员权限',
|
||||
badge: { text: '危险操作', color: 'orange' },
|
||||
},
|
||||
batchDelete: {
|
||||
title: '批量删除',
|
||||
icon: <DeleteOutlined />,
|
||||
description: '从系统中永久删除选中的主机记录,此操作不可恢复。',
|
||||
scenarios: [
|
||||
'设备报废需要从系统中移除时',
|
||||
'清理长期离线且不再使用的设备',
|
||||
'误添加的主机记录需要删除时',
|
||||
],
|
||||
steps: [
|
||||
'在列表中勾选需要删除的主机',
|
||||
'点击"批量删除"按钮',
|
||||
'仔细确认删除列表中的主机信息',
|
||||
'输入确认信息或密码(如需要)',
|
||||
'确认删除并等待操作完成',
|
||||
],
|
||||
warnings: [
|
||||
'此操作不可恢复,请谨慎操作!',
|
||||
'删除主机会同时删除相关的用户绑定和镜像授权',
|
||||
'建议删除前先导出主机配置备份',
|
||||
'无法删除正在运行中的主机,请先关机',
|
||||
],
|
||||
shortcut: 'Delete',
|
||||
permission: '超级管理员权限',
|
||||
badge: { text: '危险', color: 'red' },
|
||||
},
|
||||
refresh: {
|
||||
title: '刷新列表',
|
||||
icon: <ReloadOutlined />,
|
||||
description: '重新从服务器获取最新的主机列表数据,更新当前页面显示。',
|
||||
scenarios: [
|
||||
'需要查看最新的主机状态时',
|
||||
'执行操作后确认结果时',
|
||||
'怀疑数据显示不准确时',
|
||||
],
|
||||
steps: ['点击"刷新"按钮', '等待数据加载完成', '查看更新后的列表'],
|
||||
warnings: ['刷新会清除当前的选择状态', '正在编辑的数据可能会丢失'],
|
||||
shortcut: 'F5',
|
||||
permission: '所有用户',
|
||||
badge: { text: '安全', color: 'green' },
|
||||
},
|
||||
}
|
||||
|
||||
// 处理鼠标悬停
|
||||
const handleMouseEnter = (actionKey) => {
|
||||
setCurrentAction(actionsConfig[actionKey])
|
||||
}
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
// 延迟清空,避免快速移动时闪烁
|
||||
setTimeout(() => {
|
||||
setCurrentAction(null)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="button-help-example">
|
||||
<div className="example-section">
|
||||
<h2 className="example-title">方案一:增强型工具提示</h2>
|
||||
<p className="example-desc">鼠标悬停在按钮上即可查看详细的功能说明</p>
|
||||
|
||||
<div className="example-buttons">
|
||||
<Space wrap>
|
||||
<ButtonWithTip
|
||||
label="新增主机"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
tip={{
|
||||
title: '新增主机',
|
||||
description: '向系统中添加新的主机终端设备',
|
||||
shortcut: 'Ctrl+N',
|
||||
notes: [
|
||||
'请确保IP地址不与现有主机冲突',
|
||||
'MAC地址必须唯一且格式正确',
|
||||
],
|
||||
}}
|
||||
onClick={() => console.log('新增主机')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="批量开机"
|
||||
icon={<PoweroffOutlined />}
|
||||
tip={{
|
||||
title: '批量开机',
|
||||
description: '同时对多台选中的主机执行开机操作',
|
||||
shortcut: 'Ctrl+Shift+O',
|
||||
notes: ['仅对离线状态的主机有效', '请确保网络环境支持远程开机'],
|
||||
}}
|
||||
onClick={() => console.log('批量开机')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="批量关机"
|
||||
icon={<PoweroffOutlined />}
|
||||
tip={{
|
||||
title: '批量关机',
|
||||
description: '同时对多台选中的主机执行关机操作',
|
||||
shortcut: 'Ctrl+Shift+P',
|
||||
notes: [
|
||||
'仅对在线状态的主机有效',
|
||||
'强制关机可能导致数据丢失,请谨慎使用',
|
||||
],
|
||||
}}
|
||||
onClick={() => console.log('批量关机')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="批量删除"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
tip={{
|
||||
title: '批量删除',
|
||||
description: '从系统中永久删除选中的主机记录',
|
||||
shortcut: 'Delete',
|
||||
notes: [
|
||||
'此操作不可恢复,请谨慎操作!',
|
||||
'删除主机会同时删除相关的用户绑定和镜像授权',
|
||||
],
|
||||
}}
|
||||
onClick={() => console.log('批量删除')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="刷新"
|
||||
icon={<ReloadOutlined />}
|
||||
tip={{
|
||||
title: '刷新列表',
|
||||
description: '重新获取最新的主机列表数据',
|
||||
shortcut: 'F5',
|
||||
}}
|
||||
onClick={() => console.log('刷新')}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="example-section">
|
||||
<h2 className="example-title">方案二:智能帮助面板(推荐)</h2>
|
||||
<p className="example-desc">
|
||||
悬停按钮时,右侧帮助面板会实时显示详细的操作说明、使用场景和注意事项
|
||||
</p>
|
||||
|
||||
<div className="example-buttons">
|
||||
<Space wrap>
|
||||
<div onMouseEnter={() => handleMouseEnter('add')} onMouseLeave={handleMouseLeave}>
|
||||
<ButtonWithTip
|
||||
label="新增主机"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
showTipIcon={false}
|
||||
onClick={() => console.log('新增主机')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => handleMouseEnter('batchPowerOn')}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ButtonWithTip
|
||||
label="批量开机"
|
||||
icon={<PoweroffOutlined />}
|
||||
showTipIcon={false}
|
||||
onClick={() => console.log('批量开机')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => handleMouseEnter('batchPowerOff')}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ButtonWithTip
|
||||
label="批量关机"
|
||||
icon={<PoweroffOutlined />}
|
||||
showTipIcon={false}
|
||||
onClick={() => console.log('批量关机')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => handleMouseEnter('batchDelete')}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ButtonWithTip
|
||||
label="批量删除"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
showTipIcon={false}
|
||||
onClick={() => console.log('批量删除')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div onMouseEnter={() => handleMouseEnter('refresh')} onMouseLeave={handleMouseLeave}>
|
||||
<ButtonWithTip
|
||||
label="刷新"
|
||||
icon={<ReloadOutlined />}
|
||||
showTipIcon={false}
|
||||
onClick={() => console.log('刷新')}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 帮助面板 */}
|
||||
<ActionHelpPanel
|
||||
visible={showHelpPanel}
|
||||
onClose={() => setShowHelpPanel(false)}
|
||||
currentAction={currentAction}
|
||||
allActions={Object.values(actionsConfig)}
|
||||
/>
|
||||
|
||||
{/* 浮动帮助按钮 */}
|
||||
<FloatButton
|
||||
icon={<QuestionCircleOutlined />}
|
||||
type="primary"
|
||||
tooltip="打开帮助面板"
|
||||
onClick={() => setShowHelpPanel(!showHelpPanel)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonHelpExample
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
.all-button-designs {
|
||||
padding: 32px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
padding-bottom: 120px; /* 为底部提示栏留出空间 */
|
||||
}
|
||||
|
||||
.designs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
padding: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
color: white;
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.designs-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.designs-title {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.designs-subtitle {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.design-card {
|
||||
margin-bottom: 32px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.design-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.design-card .ant-card-head {
|
||||
background: linear-gradient(to right, #f8f9fa, #ffffff);
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.design-card .ant-card-head-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.design-description {
|
||||
padding: 16px 0;
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-left: 4px solid #1677ff;
|
||||
}
|
||||
|
||||
.design-description p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.design-description strong {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.design-demo {
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.design-hint {
|
||||
margin: 16px 0 0 0;
|
||||
padding: 12px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #1677ff;
|
||||
font-size: 13px;
|
||||
color: #0958d9;
|
||||
}
|
||||
|
||||
/* 方案2:帮助面板触发器样式 */
|
||||
.help-panel-button-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.help-panel-trigger {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.help-panel-icon {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.help-panel-button-wrapper:hover .help-panel-icon {
|
||||
display: flex;
|
||||
animation: fadeInScale 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.help-panel-icon:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.6);
|
||||
}
|
||||
|
||||
.help-panel-icon:active {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-theme-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.bottom-theme-selector span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1024px) {
|
||||
.all-button-designs {
|
||||
padding: 24px 16px;
|
||||
padding-bottom: 150px;
|
||||
}
|
||||
|
||||
.designs-header {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.designs-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.designs-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.all-button-designs {
|
||||
padding: 16px;
|
||||
padding-bottom: 180px;
|
||||
}
|
||||
|
||||
.designs-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.designs-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.designs-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.designs-subtitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.design-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.design-demo {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.design-description {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.design-description p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.designs-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.designs-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.designs-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.design-card .ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.bottom-theme-selector {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,535 @@
|
|||
import { useState } from 'react'
|
||||
import { Space, Divider, Card, Radio, FloatButton, Button } from 'antd'
|
||||
import {
|
||||
PlusOutlined,
|
||||
PoweroffOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
QuestionCircleOutlined,
|
||||
DesktopOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import ButtonWithTip from '../components/ButtonWithTip/ButtonWithTip'
|
||||
import ButtonWithHoverCard from '../components/ButtonWithHoverCard/ButtonWithHoverCard'
|
||||
import ButtonWithGuide from '../components/ButtonWithGuide/ButtonWithGuide'
|
||||
import BottomHintBar from '../components/BottomHintBar/BottomHintBar'
|
||||
import ActionHelpPanel from '../components/ActionHelpPanel/ActionHelpPanel'
|
||||
import './AllButtonDesigns.css'
|
||||
|
||||
/**
|
||||
* 所有按钮设计方案综合展示页面
|
||||
*/
|
||||
function AllButtonDesigns() {
|
||||
const [showHelpPanel, setShowHelpPanel] = useState(false)
|
||||
const [currentAction, setCurrentAction] = useState(null)
|
||||
const [showBottomHint, setShowBottomHint] = useState(false)
|
||||
const [currentHint, setCurrentHint] = useState(null)
|
||||
const [bottomTheme, setBottomTheme] = useState('gradient')
|
||||
|
||||
// 定义所有操作的配置
|
||||
const actionsConfig = {
|
||||
add: {
|
||||
title: '新增主机',
|
||||
icon: <PlusOutlined />,
|
||||
description: '向系统中添加新的主机终端设备,可以是服务器、台式机或笔记本电脑。',
|
||||
scenarios: [
|
||||
'当有新设备需要接入系统管理时',
|
||||
'需要扩展终端设备数量时',
|
||||
'替换旧设备,添加新设备时',
|
||||
],
|
||||
quickTips: ['确保设备网络连通', 'MAC地址必须唯一'],
|
||||
steps: [
|
||||
'点击"新增主机"按钮',
|
||||
'填写主机基本信息(名称、IP、MAC地址等)',
|
||||
'选择主机所属分组',
|
||||
'配置系统和数据盘容量',
|
||||
'保存并等待主机上线',
|
||||
],
|
||||
warnings: [
|
||||
'请确保IP地址不与现有主机冲突',
|
||||
'MAC地址必须唯一且格式正确',
|
||||
'建议先进行网络连通性测试',
|
||||
],
|
||||
quickTip: '填写设备信息前请先确认网络连通性',
|
||||
warning: '请确保IP地址不与现有主机冲突',
|
||||
shortcut: 'Ctrl+N',
|
||||
permission: '管理员权限',
|
||||
badge: { text: '常用', color: 'blue' },
|
||||
},
|
||||
batchPowerOn: {
|
||||
title: '批量开机',
|
||||
icon: <PoweroffOutlined />,
|
||||
description: '同时对多台选中的主机执行开机操作,适用于需要批量启动设备的场景。',
|
||||
scenarios: [
|
||||
'每日上班时批量启动办公设备',
|
||||
'定时任务触发的批量开机',
|
||||
'维护后批量恢复设备运行',
|
||||
],
|
||||
quickTips: ['仅对离线主机有效', '需要Wake-on-LAN支持'],
|
||||
steps: [
|
||||
'在列表中勾选需要开机的主机',
|
||||
'点击"批量开机"按钮',
|
||||
'确认操作',
|
||||
'等待开机命令发送并监控状态变化',
|
||||
],
|
||||
warnings: [
|
||||
'仅对离线状态的主机有效',
|
||||
'请确保网络环境支持远程开机(Wake-on-LAN)',
|
||||
'大量主机同时开机可能导致网络负载较高',
|
||||
],
|
||||
quickTip: '请先选中需要开机的离线主机',
|
||||
warning: '仅对离线状态的主机有效',
|
||||
shortcut: 'Ctrl+Shift+O',
|
||||
permission: '操作员权限',
|
||||
badge: { text: '批量', color: 'green' },
|
||||
},
|
||||
batchDelete: {
|
||||
title: '批量删除',
|
||||
icon: <DeleteOutlined />,
|
||||
description: '从系统中永久删除选中的主机记录,此操作不可恢复。',
|
||||
scenarios: [
|
||||
'设备报废需要从系统中移除时',
|
||||
'清理长期离线且不再使用的设备',
|
||||
'误添加的主机记录需要删除时',
|
||||
],
|
||||
quickTips: ['此操作不可恢复', '会删除所有关联数据'],
|
||||
steps: [
|
||||
'在列表中勾选需要删除的主机',
|
||||
'点击"批量删除"按钮',
|
||||
'仔细确认删除列表',
|
||||
'输入确认信息',
|
||||
'确认删除',
|
||||
],
|
||||
warnings: [
|
||||
'此操作不可恢复,请谨慎操作!',
|
||||
'删除主机会同时删除相关的用户绑定和镜像授权',
|
||||
'建议删除前先导出主机配置备份',
|
||||
'无法删除正在运行中的主机,请先关机',
|
||||
],
|
||||
quickTip: '此操作不可恢复,请谨慎确认',
|
||||
warning: '此操作不可恢复!会删除所有关联数据',
|
||||
shortcut: 'Delete',
|
||||
permission: '超级管理员权限',
|
||||
badge: { text: '危险', color: 'red' },
|
||||
},
|
||||
refresh: {
|
||||
title: '刷新列表',
|
||||
icon: <ReloadOutlined />,
|
||||
description: '重新从服务器获取最新的主机列表数据,更新当前页面显示。',
|
||||
scenarios: [
|
||||
'需要查看最新的主机状态时',
|
||||
'执行操作后确认结果时',
|
||||
'怀疑数据显示不准确时',
|
||||
],
|
||||
quickTips: ['会清除当前选择', '获取最新数据'],
|
||||
steps: ['点击"刷新"按钮', '等待数据加载完成', '查看更新后的列表'],
|
||||
warnings: ['刷新会清除当前的选择状态', '正在编辑的数据可能会丢失'],
|
||||
quickTip: '获取最新的主机状态信息',
|
||||
warning: '刷新会清除当前的选择状态',
|
||||
shortcut: 'F5',
|
||||
permission: '所有用户',
|
||||
badge: { text: '安全', color: 'green' },
|
||||
},
|
||||
}
|
||||
|
||||
// 处理底部提示栏的hover事件
|
||||
const handleHintHover = (actionKey) => {
|
||||
const action = actionsConfig[actionKey]
|
||||
if (action) {
|
||||
setCurrentHint(action)
|
||||
setShowBottomHint(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleHintLeave = () => {
|
||||
// 移除延迟,立即隐藏
|
||||
setShowBottomHint(false)
|
||||
}
|
||||
|
||||
// 处理帮助面板的hover事件
|
||||
const handlePanelHover = (actionKey) => {
|
||||
// 只在面板未打开时才通过hover更新内容
|
||||
if (!showHelpPanel) {
|
||||
const action = actionsConfig[actionKey]
|
||||
if (action) {
|
||||
setCurrentAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePanelLeave = () => {
|
||||
// 只在面板未打开时才清空内容
|
||||
if (!showHelpPanel) {
|
||||
setTimeout(() => {
|
||||
setCurrentAction(null)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理帮助图标点击
|
||||
const handleHelpIconClick = (e, actionKey) => {
|
||||
e.stopPropagation()
|
||||
const action = actionsConfig[actionKey]
|
||||
if (action) {
|
||||
setCurrentAction(action)
|
||||
setShowHelpPanel(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="all-button-designs">
|
||||
<div className="designs-header">
|
||||
<DesktopOutlined className="designs-icon" />
|
||||
<div>
|
||||
<h1 className="designs-title">按钮介绍设计方案对比</h1>
|
||||
<p className="designs-subtitle">5种创新设计,为用户提供清晰的操作指引</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 方案1:增强型工具提示 */}
|
||||
<Card className="design-card" title="方案 1:增强型工具提示 (Enhanced Tooltip)">
|
||||
<div className="design-description">
|
||||
<p>
|
||||
<strong>特点:</strong>
|
||||
渐变色彩背景 + 脉冲动画指示器 + 多维度信息展示
|
||||
</p>
|
||||
<p>
|
||||
<strong>适用场景:</strong>
|
||||
简单操作,需要快速了解功能,不希望占用额外页面空间
|
||||
</p>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="design-demo">
|
||||
<Space wrap size="middle">
|
||||
<ButtonWithTip
|
||||
label="新增主机"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
tip={{
|
||||
title: '新增主机',
|
||||
description: '向系统中添加新的主机终端设备',
|
||||
shortcut: 'Ctrl+N',
|
||||
notes: ['请确保IP地址不与现有主机冲突', 'MAC地址必须唯一且格式正确'],
|
||||
}}
|
||||
onClick={() => console.log('新增主机')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="批量开机"
|
||||
icon={<PoweroffOutlined />}
|
||||
tip={{
|
||||
title: '批量开机',
|
||||
description: '同时对多台选中的主机执行开机操作',
|
||||
shortcut: 'Ctrl+Shift+O',
|
||||
notes: ['仅对离线状态的主机有效', '请确保网络环境支持远程开机'],
|
||||
}}
|
||||
onClick={() => console.log('批量开机')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="批量删除"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
tip={{
|
||||
title: '批量删除',
|
||||
description: '从系统中永久删除选中的主机记录',
|
||||
shortcut: 'Delete',
|
||||
notes: ['此操作不可恢复,请谨慎操作!', '删除主机会同时删除相关的用户绑定'],
|
||||
}}
|
||||
onClick={() => console.log('批量删除')}
|
||||
/>
|
||||
|
||||
<ButtonWithTip
|
||||
label="刷新"
|
||||
icon={<ReloadOutlined />}
|
||||
tip={{
|
||||
title: '刷新列表',
|
||||
description: '重新获取最新的主机列表数据',
|
||||
shortcut: 'F5',
|
||||
}}
|
||||
onClick={() => console.log('刷新')}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 方案2:智能帮助面板 */}
|
||||
<Card className="design-card" title="方案 2:智能帮助面板 (Smart Help Panel) ⭐ 推荐">
|
||||
<div className="design-description">
|
||||
<p>
|
||||
<strong>特点:</strong>
|
||||
侧边抽屉式帮助 + 实时显示详细信息 + 完整的操作指引
|
||||
</p>
|
||||
<p>
|
||||
<strong>适用场景:</strong>
|
||||
复杂业务操作,需要详细指引,新用户培训和引导
|
||||
</p>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="design-demo">
|
||||
<Space wrap size="middle">
|
||||
<div
|
||||
className="help-panel-button-wrapper"
|
||||
onMouseEnter={() => handlePanelHover('add')}
|
||||
onMouseLeave={handlePanelLeave}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />} className="help-panel-trigger">
|
||||
新增主机
|
||||
</Button>
|
||||
<button
|
||||
className="help-panel-icon"
|
||||
onClick={(e) => handleHelpIconClick(e, 'add')}
|
||||
title="查看帮助"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="help-panel-button-wrapper"
|
||||
onMouseEnter={() => handlePanelHover('batchPowerOn')}
|
||||
onMouseLeave={handlePanelLeave}
|
||||
>
|
||||
<Button icon={<PoweroffOutlined />} className="help-panel-trigger">
|
||||
批量开机
|
||||
</Button>
|
||||
<button
|
||||
className="help-panel-icon"
|
||||
onClick={(e) => handleHelpIconClick(e, 'batchPowerOn')}
|
||||
title="查看帮助"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="help-panel-button-wrapper"
|
||||
onMouseEnter={() => handlePanelHover('batchDelete')}
|
||||
onMouseLeave={handlePanelLeave}
|
||||
>
|
||||
<Button icon={<DeleteOutlined />} danger className="help-panel-trigger">
|
||||
批量删除
|
||||
</Button>
|
||||
<button
|
||||
className="help-panel-icon"
|
||||
onClick={(e) => handleHelpIconClick(e, 'batchDelete')}
|
||||
title="查看帮助"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="help-panel-button-wrapper"
|
||||
onMouseEnter={() => handlePanelHover('refresh')}
|
||||
onMouseLeave={handlePanelLeave}
|
||||
>
|
||||
<Button icon={<ReloadOutlined />} className="help-panel-trigger">
|
||||
刷新
|
||||
</Button>
|
||||
<button
|
||||
className="help-panel-icon"
|
||||
onClick={(e) => handleHelpIconClick(e, 'refresh')}
|
||||
title="查看帮助"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
</Space>
|
||||
<p className="design-hint">💡 悬停按钮查看提示,点击"?"图标打开详细帮助面板</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 方案3:悬浮展开卡片 */}
|
||||
<Card className="design-card" title="方案 3:悬浮展开卡片 (Hover Expand Card)">
|
||||
<div className="design-description">
|
||||
<p>
|
||||
<strong>特点:</strong>
|
||||
精美的悬浮卡片 + 滑入动画 + 分层信息展示
|
||||
</p>
|
||||
<p>
|
||||
<strong>适用场景:</strong>
|
||||
需要展示较多信息,但不想使用弹窗打断操作流程
|
||||
</p>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="design-demo">
|
||||
<Space wrap size="middle">
|
||||
<ButtonWithHoverCard
|
||||
label="新增主机"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
cardInfo={actionsConfig.add}
|
||||
onClick={() => console.log('新增主机')}
|
||||
/>
|
||||
|
||||
<ButtonWithHoverCard
|
||||
label="批量开机"
|
||||
icon={<PoweroffOutlined />}
|
||||
cardInfo={actionsConfig.batchPowerOn}
|
||||
onClick={() => console.log('批量开机')}
|
||||
/>
|
||||
|
||||
<ButtonWithHoverCard
|
||||
label="批量删除"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
cardInfo={actionsConfig.batchDelete}
|
||||
onClick={() => console.log('批量删除')}
|
||||
/>
|
||||
|
||||
<ButtonWithHoverCard
|
||||
label="刷新"
|
||||
icon={<ReloadOutlined />}
|
||||
cardInfo={actionsConfig.refresh}
|
||||
onClick={() => console.log('刷新')}
|
||||
/>
|
||||
</Space>
|
||||
<p className="design-hint">💡 鼠标悬停在按钮上,右侧会展开详细信息卡片</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 方案4:智能引导 */}
|
||||
<Card className="design-card" title="方案 4:智能引导 (Smart Guide)">
|
||||
<div className="design-description">
|
||||
<p>
|
||||
<strong>特点:</strong>
|
||||
简洁扁平设计 + 独立帮助图标 + 点击查看详情
|
||||
</p>
|
||||
<p>
|
||||
<strong>适用场景:</strong>
|
||||
需要详细引导,但不希望干扰主操作流程
|
||||
</p>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="design-demo">
|
||||
<Space wrap size="middle">
|
||||
<ButtonWithGuide
|
||||
label="新增主机"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
guide={actionsConfig.add}
|
||||
onClick={() => console.log('新增主机')}
|
||||
/>
|
||||
|
||||
<ButtonWithGuide
|
||||
label="批量开机"
|
||||
icon={<PoweroffOutlined />}
|
||||
guide={actionsConfig.batchPowerOn}
|
||||
onClick={() => console.log('批量开机')}
|
||||
/>
|
||||
|
||||
<ButtonWithGuide
|
||||
label="批量删除"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
guide={actionsConfig.batchDelete}
|
||||
onClick={() => console.log('批量删除')}
|
||||
/>
|
||||
|
||||
<ButtonWithGuide
|
||||
label="刷新"
|
||||
icon={<ReloadOutlined />}
|
||||
guide={actionsConfig.refresh}
|
||||
onClick={() => console.log('刷新')}
|
||||
/>
|
||||
</Space>
|
||||
<p className="design-hint">💡 点击按钮旁边的帮助图标查看详细引导</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 方案5:底部固定提示栏 */}
|
||||
<Card className="design-card" title="方案 5:底部固定提示栏 (Bottom Hint Bar)">
|
||||
<div className="design-description">
|
||||
<p>
|
||||
<strong>特点:</strong>
|
||||
固定底部位置 + 实时更新 + 不遮挡内容 + 多主题可选
|
||||
</p>
|
||||
<p>
|
||||
<strong>适用场景:</strong>
|
||||
需要始终可见的提示信息,不希望被tooltip遮挡操作区域
|
||||
</p>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="design-demo">
|
||||
<div className="bottom-theme-selector">
|
||||
<span>主题:</span>
|
||||
<Radio.Group
|
||||
value={bottomTheme}
|
||||
onChange={(e) => setBottomTheme(e.target.value)}
|
||||
size="small"
|
||||
>
|
||||
<Radio.Button value="gradient">渐变</Radio.Button>
|
||||
<Radio.Button value="light">浅色</Radio.Button>
|
||||
<Radio.Button value="dark">深色</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<Space wrap size="middle">
|
||||
<div
|
||||
onMouseEnter={() => handleHintHover('add')}
|
||||
onMouseLeave={handleHintLeave}
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
新增主机
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => handleHintHover('batchPowerOn')}
|
||||
onMouseLeave={handleHintLeave}
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<Button icon={<PoweroffOutlined />}>批量开机</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => handleHintHover('batchDelete')}
|
||||
onMouseLeave={handleHintLeave}
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<Button icon={<DeleteOutlined />} danger>
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => handleHintHover('refresh')}
|
||||
onMouseLeave={handleHintLeave}
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<Button icon={<ReloadOutlined />}>刷新</Button>
|
||||
</div>
|
||||
</Space>
|
||||
<p className="design-hint">💡 鼠标悬停在按钮上,底部会显示实时提示信息</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 帮助面板 */}
|
||||
<ActionHelpPanel
|
||||
visible={showHelpPanel}
|
||||
onClose={() => setShowHelpPanel(false)}
|
||||
currentAction={currentAction}
|
||||
allActions={Object.values(actionsConfig)}
|
||||
onActionSelect={(action) => setCurrentAction(action)}
|
||||
/>
|
||||
|
||||
{/* 底部提示栏 */}
|
||||
<BottomHintBar
|
||||
visible={showBottomHint}
|
||||
hintInfo={currentHint}
|
||||
onClose={() => setShowBottomHint(false)}
|
||||
theme={bottomTheme}
|
||||
/>
|
||||
|
||||
{/* 浮动帮助按钮 */}
|
||||
<FloatButton
|
||||
icon={<QuestionCircleOutlined />}
|
||||
type="primary"
|
||||
tooltip="打开帮助面板"
|
||||
onClick={() => setShowHelpPanel(!showHelpPanel)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AllButtonDesigns
|
||||
|
|
@ -17,6 +17,7 @@ function DocsPage() {
|
|||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentDocPath, setCurrentDocPath] = useState('')
|
||||
const [openKeys, setOpenKeys] = useState(['design']) // 默认展开设计规范
|
||||
const contentRef = useRef(null)
|
||||
|
||||
// 构建文档路径到 key 的映射
|
||||
|
|
@ -52,6 +53,15 @@ function DocsPage() {
|
|||
return null
|
||||
}
|
||||
|
||||
// 根据文档 key 查找所属的父级分组
|
||||
const findParentGroup = (docKey) => {
|
||||
for (const group of docsMenuData) {
|
||||
const found = group.children.find((item) => item.key === docKey)
|
||||
if (found) return group.key
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 加载 markdown 文件
|
||||
const loadMarkdown = async (key) => {
|
||||
const path = findDocPath(key)
|
||||
|
|
@ -84,6 +94,12 @@ function DocsPage() {
|
|||
const handleMenuClick = ({ key }) => {
|
||||
setSelectedKey(key)
|
||||
loadMarkdown(key)
|
||||
|
||||
// 自动展开当前文档所属的父级分组,收起其他分组
|
||||
const parentGroup = findParentGroup(key)
|
||||
if (parentGroup) {
|
||||
setOpenKeys([parentGroup])
|
||||
}
|
||||
}
|
||||
|
||||
// 解析相对路径,返回绝对路径
|
||||
|
|
@ -122,6 +138,12 @@ function DocsPage() {
|
|||
if (targetKey) {
|
||||
setSelectedKey(targetKey)
|
||||
loadMarkdown(targetKey)
|
||||
|
||||
// 自动展开当前文档所属的父级分组,收起其他分组
|
||||
const parentGroup = findParentGroup(targetKey)
|
||||
if (parentGroup) {
|
||||
setOpenKeys([parentGroup])
|
||||
}
|
||||
} else {
|
||||
console.warn('未找到文档:', absolutePath)
|
||||
}
|
||||
|
|
@ -186,7 +208,8 @@ function DocsPage() {
|
|||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
defaultOpenKeys={['design', 'components', 'pages']}
|
||||
openKeys={openKeys}
|
||||
onOpenChange={setOpenKeys}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
className="docs-menu"
|
||||
|
|
|
|||
Loading…
Reference in New Issue