Compare commits

...

2 Commits

Author SHA1 Message Date
mula.liu ef8195390f 添加了按钮扩展 2025-11-18 15:41:28 +08:00
mula.liu 5e56f765d0 添加了按钮扩展 2025-11-18 15:38:15 +08:00
23 changed files with 4396 additions and 55 deletions

61
docs/MIGRATION.md 100644
View File

@ -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. **版本控制** - 使用版本号和更新日志记录变更
这样可以:
- ✅ 更容易找到和维护文档
- ✅ 避免文档分散和重复
- ✅ 与代码保持同步更新

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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%;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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>
)
}

View File

@ -94,6 +94,11 @@
"key": "chart-panel",
"label": "ChartPanel",
"path": "/docs/components/ChartPanel.md"
},
{
"key": "button-extension",
"label": "ButtonExtension",
"path": "/docs/components/ButtonExtension.md"
}
]
}

View File

@ -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": "网络管理",

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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"