From 5e56f765d0976bea64f8de2f0b5b1641e020bec0 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Tue, 18 Nov 2025 15:38:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/MIGRATION.md | 61 ++ docs/components/ButtonExtension.md | 789 ++++++++++++++++++ src/App.jsx | 2 + .../ActionHelpPanel/ActionHelpPanel.css | 259 ++++++ .../ActionHelpPanel/ActionHelpPanel.jsx | 228 +++++ .../BottomHintBar/BottomHintBar.css | 304 +++++++ .../BottomHintBar/BottomHintBar.jsx | 90 ++ .../ButtonWithGuide/ButtonWithGuide.css | 196 +++++ .../ButtonWithGuide/ButtonWithGuide.jsx | 165 ++++ .../ButtonWithGuideBadge.css | 243 ++++++ .../ButtonWithGuideBadge.jsx | 222 +++++ .../ButtonWithHoverCard.css | 189 +++++ .../ButtonWithHoverCard.jsx | 179 ++++ .../ButtonWithTip/ButtonWithTip.css | 163 ++++ .../ButtonWithTip/ButtonWithTip.jsx | 105 +++ src/components/MainLayout/AppSider.jsx | 87 +- src/data/docsMenuData.json | 5 + src/data/menuData.json | 12 + src/examples/ButtonHelpExample.css | 49 ++ src/examples/ButtonHelpExample.jsx | 321 +++++++ src/pages/AllButtonDesigns.css | 260 ++++++ src/pages/AllButtonDesigns.jsx | 535 ++++++++++++ src/pages/DocsPage.jsx | 25 +- 23 files changed, 4434 insertions(+), 55 deletions(-) create mode 100644 docs/MIGRATION.md create mode 100644 docs/components/ButtonExtension.md create mode 100644 src/components/ActionHelpPanel/ActionHelpPanel.css create mode 100644 src/components/ActionHelpPanel/ActionHelpPanel.jsx create mode 100644 src/components/BottomHintBar/BottomHintBar.css create mode 100644 src/components/BottomHintBar/BottomHintBar.jsx create mode 100644 src/components/ButtonWithGuide/ButtonWithGuide.css create mode 100644 src/components/ButtonWithGuide/ButtonWithGuide.jsx create mode 100644 src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.css create mode 100644 src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx create mode 100644 src/components/ButtonWithHoverCard/ButtonWithHoverCard.css create mode 100644 src/components/ButtonWithHoverCard/ButtonWithHoverCard.jsx create mode 100644 src/components/ButtonWithTip/ButtonWithTip.css create mode 100644 src/components/ButtonWithTip/ButtonWithTip.jsx create mode 100644 src/examples/ButtonHelpExample.css create mode 100644 src/examples/ButtonHelpExample.jsx create mode 100644 src/pages/AllButtonDesigns.css create mode 100644 src/pages/AllButtonDesigns.jsx diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md new file mode 100644 index 0000000..5300384 --- /dev/null +++ b/docs/MIGRATION.md @@ -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. **版本控制** - 使用版本号和更新日志记录变更 + +这样可以: +- ✅ 更容易找到和维护文档 +- ✅ 避免文档分散和重复 +- ✅ 与代码保持同步更新 diff --git a/docs/components/ButtonExtension.md b/docs/components/ButtonExtension.md new file mode 100644 index 0000000..fcc1d44 --- /dev/null +++ b/docs/components/ButtonExtension.md @@ -0,0 +1,789 @@ +# 按钮扩展组件设计文档 + +> **版本:** 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' + +} + 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 // 所有可用操作 + 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和点击事件 +
setCurrentAction(actionsConfig.add)} + onMouseLeave={() => !showPanel && setCurrentAction(null)} +> + + +
+ + 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) + +
{ + setHintInfo(actionConfig) + setShowHint(true) + }} + onMouseLeave={() => setShowHint(false)} +> + +
+ + +``` + +--- + +## 使用指南 + +### 快速开始 + +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: , + 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 ( +
+ {/* 按钮区域 */} +
handleHover('add')} + onMouseLeave={handleLeave} + > + + +
+ + {/* 帮助面板 */} + setShowHelpPanel(false)} + currentAction={currentAction} + allActions={Object.values(actionsConfig)} + onActionSelect={(action) => setCurrentAction(action)} + /> +
+ ) +} +``` + +--- + +## 最佳实践 + +### 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属性 + + +// 添加aria标签 + + +// 快捷键支持 +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 # 本文档 +``` + +--- + +## 在线演示 + +访问地址:**http://localhost:5173/design/button-designs** + +菜单路径:**组件设计 → 扩展按钮** + +--- + +## 常见问题 + +### Q1: 如何选择合适的方案? + +**A:** 根据以下因素选择: +- **信息量**:少 → 方案1,多 → 方案2/3 +- **复杂度**:简单 → 方案1/5,复杂 → 方案2/4 +- **使用频率**:频繁 → 方案5,偶尔 → 方案1/3 +- **用户类型**:新手 → 方案2/4,熟练 → 方案1/5 + +### Q2: 可以混合使用多种方案吗? + +**A:** 可以。建议: +- 同一页面使用统一的方案 +- 不同页面可以使用不同方案 +- 核心操作使用方案2,次要操作使用方案1 + +### Q3: 如何自定义样式? + +**A:** 所有组件都支持自定义样式: + +```jsx +// 通过className + + +// 通过style + + +// 修改CSS变量 +:root { + --tip-primary-gradient: linear-gradient(...); +} +``` + +### Q4: 移动端如何适配? + +**A:** 所有组件都内置了响应式支持: +- 方案1:移动端隐藏提示图标 +- 方案2:面板宽度自适应 +- 方案3:卡片居中显示 +- 方案5:提示栏自适应布局 + +--- + +## 贡献指南 + +欢迎提出改进建议! + +**提交Issues:** +- 使用清晰的标题 +- 提供复现步骤 +- 附上截图或录屏 + +**提交Pull Requests:** +- 遵循现有代码风格 +- 添加必要的注释 +- 更新相关文档 + +--- + +## 许可证 + +MIT License + +--- + +## 联系方式 + +- 项目地址:`/Users/jiliu/工作/projects/Nex Design` +- 文档路径:`src/components/docs/ButtonExtensions.md` +- 在线演示:`http://localhost:5173/design/button-designs` + +--- + +**最后更新:** 2025-11-17 +**文档版本:** v1.2.0 diff --git a/src/App.jsx b/src/App.jsx index ef4dc3f..b4364c0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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() { } /> } /> } /> + } /> {/* 其他路由将在后续添加 */} diff --git a/src/components/ActionHelpPanel/ActionHelpPanel.css b/src/components/ActionHelpPanel/ActionHelpPanel.css new file mode 100644 index 0000000..bc039f7 --- /dev/null +++ b/src/components/ActionHelpPanel/ActionHelpPanel.css @@ -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; + } +} diff --git a/src/components/ActionHelpPanel/ActionHelpPanel.jsx b/src/components/ActionHelpPanel/ActionHelpPanel.jsx new file mode 100644 index 0000000..a3cde11 --- /dev/null +++ b/src/components/ActionHelpPanel/ActionHelpPanel.jsx @@ -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 ( + + ) + } + + return ( +
+ {/* 操作标题 */} +
+
{currentAction.icon}
+
+

{currentAction.title}

+ {currentAction.badge && ( + + {currentAction.badge.text} + + )} +
+
+ + {/* 操作描述 */} + {currentAction.description && ( +
+
+ 功能说明 +
+
{currentAction.description}
+
+ )} + + {/* 使用场景 */} + {currentAction.scenarios && currentAction.scenarios.length > 0 && ( +
+
+ 使用场景 +
+
    + {currentAction.scenarios.map((scenario, index) => ( +
  • {scenario}
  • + ))} +
+
+ )} + + {/* 操作步骤 */} + {currentAction.steps && currentAction.steps.length > 0 && ( +
+
+ 操作步骤 +
+
    + {currentAction.steps.map((step, index) => ( +
  1. {step}
  2. + ))} +
+
+ )} + + {/* 注意事项 */} + {currentAction.warnings && currentAction.warnings.length > 0 && ( +
+
+ 注意事项 +
+
    + {currentAction.warnings.map((warning, index) => ( +
  • {warning}
  • + ))} +
+
+ )} + + {/* 快捷键 */} + {currentAction.shortcut && ( +
+
⌨️ 快捷键
+
+ {currentAction.shortcut} +
+
+ )} + + {/* 权限要求 */} + {currentAction.permission && ( +
+
🔐 权限要求
+
+ {currentAction.permission} +
+
+ )} +
+ ) + } + + // 渲染所有操作列表 + const renderAllActions = () => { + if (allActions.length === 0) { + return + } + + return ( +
+ {allActions.map((action, index) => ( +
{ + if (onActionSelect) { + onActionSelect(action) + setActiveKey(['current']) + } + }} + > +
+ {action.icon} + {action.title} + {action.shortcut && ( + {action.shortcut} + )} +
+
{action.description}
+
+ ))} +
+ ) + } + + return ( + + + 操作帮助 + {currentAction && } + + } + placement={placement} + width={420} + open={visible} + onClose={onClose} + className="action-help-panel" + > + + + 当前操作 + {currentAction && ( + + )} + + } + key="current" + > + {renderCurrentAction()} + + + + {renderAllActions()} + + + + ) +} + +export default ActionHelpPanel diff --git a/src/components/BottomHintBar/BottomHintBar.css b/src/components/BottomHintBar/BottomHintBar.css new file mode 100644 index 0000000..36de3a3 --- /dev/null +++ b/src/components/BottomHintBar/BottomHintBar.css @@ -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%; + } +} diff --git a/src/components/BottomHintBar/BottomHintBar.jsx b/src/components/BottomHintBar/BottomHintBar.jsx new file mode 100644 index 0000000..9efe937 --- /dev/null +++ b/src/components/BottomHintBar/BottomHintBar.jsx @@ -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 ( +
e.stopPropagation()} + > +
+ {/* 左侧:图标和标题 */} +
+
{hintInfo.icon}
+
+

{hintInfo.title}

+ {hintInfo.badge && ( + + {hintInfo.badge.text} + + )} +
+
+ + {/* 中间:主要信息 */} +
+ {/* 描述 */} + {hintInfo.description && ( +
+ + {hintInfo.description} +
+ )} + + {/* 快速提示 */} + {hintInfo.quickTip && ( +
+ + {hintInfo.quickTip} +
+ )} + + {/* 警告 */} + {hintInfo.warning && ( +
+ + {hintInfo.warning} +
+ )} +
+ + {/* 右侧:快捷键和关闭 */} +
+ {hintInfo.shortcut && ( +
+ 快捷键 + {hintInfo.shortcut} +
+ )} + {onClose && ( + + )} +
+
+ + {/* 进度指示条 */} +
+
+ ) +} + +export default BottomHintBar diff --git a/src/components/ButtonWithGuide/ButtonWithGuide.css b/src/components/ButtonWithGuide/ButtonWithGuide.css new file mode 100644 index 0000000..0968427 --- /dev/null +++ b/src/components/ButtonWithGuide/ButtonWithGuide.css @@ -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; + } +} diff --git a/src/components/ButtonWithGuide/ButtonWithGuide.jsx b/src/components/ButtonWithGuide/ButtonWithGuide.jsx new file mode 100644 index 0000000..0f30cc3 --- /dev/null +++ b/src/components/ButtonWithGuide/ButtonWithGuide.jsx @@ -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 ( + <> +
+ + {guide && !disabled && ( + + )} +
+ + {/* 引导弹窗 */} + {guide && ( + + {guide.icon || icon} + {guide.title} + {guide.badge && ( + + {guide.badge.text} + + )} +
+ } + open={showGuideModal} + onCancel={() => setShowGuideModal(false)} + footer={[ + , + ]} + width={600} + className="button-guide-modal" + > + {/* 功能描述 */} + {guide.description && ( +
+
+ + 功能说明 +
+

{guide.description}

+
+ )} + + {/* 使用步骤 */} + {guide.steps && guide.steps.length > 0 && ( +
+
+ + 操作步骤 +
+ ({ + title: `步骤 ${index + 1}`, + description: step, + status: 'wait', + }))} + className="guide-steps" + /> +
+ )} + + {/* 使用场景 */} + {guide.scenarios && guide.scenarios.length > 0 && ( +
+
+ + 适用场景 +
+
    + {guide.scenarios.map((scenario, index) => ( +
  • {scenario}
  • + ))} +
+
+ )} + + {/* 注意事项 */} + {guide.warnings && guide.warnings.length > 0 && ( +
+
+ + 注意事项 +
+
    + {guide.warnings.map((warning, index) => ( +
  • {warning}
  • + ))} +
+
+ )} + + {/* 快捷键和权限 */} + {(guide.shortcut || guide.permission) && ( +
+ {guide.shortcut && ( +
+ 快捷键: + {guide.shortcut} +
+ )} + {guide.permission && ( +
+ 权限要求: + {guide.permission} +
+ )} +
+ )} + + )} + + ) +} + +export default ButtonWithGuide diff --git a/src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.css b/src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.css new file mode 100644 index 0000000..334f120 --- /dev/null +++ b/src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.css @@ -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; + } +} diff --git a/src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx b/src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx new file mode 100644 index 0000000..35a8224 --- /dev/null +++ b/src/components/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx @@ -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: , + }, + help: { + text: '?', + color: '#1677ff', + icon: , + }, + warn: { + text: '!', + color: '#faad14', + icon: , + }, + } + return configs[badgeType] || configs.help + } + + const badgeConfig = getBadgeConfig() + + return ( + <> +
+ {showBadge && guide && !disabled ? ( + + {badgeConfig.icon} +
+ } + offset={[-5, 5]} + > + + + ) : ( + + )} + + + {/* 引导弹窗 */} + {guide && ( + + {guide.icon || icon} + {guide.title} + {guide.badge && ( + + {guide.badge.text} + + )} + + } + open={showGuideModal} + onCancel={() => setShowGuideModal(false)} + footer={[ + , + ]} + width={600} + className="button-guide-modal" + > + {/* 功能描述 */} + {guide.description && ( +
+
+ + 功能说明 +
+

{guide.description}

+
+ )} + + {/* 使用步骤 */} + {guide.steps && guide.steps.length > 0 && ( +
+
+ + 操作步骤 +
+ ({ + title: `步骤 ${index + 1}`, + description: step, + status: 'wait', + }))} + className="guide-steps" + /> +
+ )} + + {/* 使用场景 */} + {guide.scenarios && guide.scenarios.length > 0 && ( +
+
+ + 适用场景 +
+
    + {guide.scenarios.map((scenario, index) => ( +
  • {scenario}
  • + ))} +
+
+ )} + + {/* 注意事项 */} + {guide.warnings && guide.warnings.length > 0 && ( +
+
+ + 注意事项 +
+
    + {guide.warnings.map((warning, index) => ( +
  • {warning}
  • + ))} +
+
+ )} + + {/* 快捷键和权限 */} + {(guide.shortcut || guide.permission) && ( +
+ {guide.shortcut && ( +
+ 快捷键: + {guide.shortcut} +
+ )} + {guide.permission && ( +
+ 权限要求: + {guide.permission} +
+ )} +
+ )} +
+ )} + + ) +} + +export default ButtonWithGuideBadge diff --git a/src/components/ButtonWithHoverCard/ButtonWithHoverCard.css b/src/components/ButtonWithHoverCard/ButtonWithHoverCard.css new file mode 100644 index 0000000..eb42451 --- /dev/null +++ b/src/components/ButtonWithHoverCard/ButtonWithHoverCard.css @@ -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); + } + } +} diff --git a/src/components/ButtonWithHoverCard/ButtonWithHoverCard.jsx b/src/components/ButtonWithHoverCard/ButtonWithHoverCard.jsx new file mode 100644 index 0000000..9800e64 --- /dev/null +++ b/src/components/ButtonWithHoverCard/ButtonWithHoverCard.jsx @@ -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 ( +
+ + {/* 标题区 */} +
+
+ {cardInfo.icon && ( + {cardInfo.icon} + )} +

{cardInfo.title}

+
+ {cardInfo.badge && ( + + {cardInfo.badge.text} + + )} +
+ + {/* 描述 */} + {cardInfo.description && ( +
+

{cardInfo.description}

+
+ )} + + {/* 使用场景 */} + {cardInfo.scenarios && cardInfo.scenarios.length > 0 && ( +
+
+ + 使用场景 +
+
    + {cardInfo.scenarios.slice(0, 2).map((scenario, index) => ( +
  • {scenario}
  • + ))} +
+
+ )} + + {/* 快速提示 */} + {cardInfo.quickTips && cardInfo.quickTips.length > 0 && ( +
+
+ + 快速提示 +
+
    + {cardInfo.quickTips.map((tip, index) => ( +
  • {tip}
  • + ))} +
+
+ )} + + {/* 注意事项 */} + {cardInfo.warnings && cardInfo.warnings.length > 0 && ( +
+
+ + 注意 +
+
    + {cardInfo.warnings.slice(0, 2).map((warning, index) => ( +
  • {warning}
  • + ))} +
+
+ )} + + {/* 快捷键 */} + {cardInfo.shortcut && ( +
+ 快捷键 + {cardInfo.shortcut} +
+ )} +
+
+ ) + } + + return ( + <> +
+ +
+ + {/* 使用 Portal 渲染悬浮卡片到 body */} + {typeof document !== 'undefined' && createPortal(renderCard(), document.body)} + + ) +} + +export default ButtonWithHoverCard diff --git a/src/components/ButtonWithTip/ButtonWithTip.css b/src/components/ButtonWithTip/ButtonWithTip.css new file mode 100644 index 0000000..f1d665a --- /dev/null +++ b/src/components/ButtonWithTip/ButtonWithTip.css @@ -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; + } +} diff --git a/src/components/ButtonWithTip/ButtonWithTip.jsx b/src/components/ButtonWithTip/ButtonWithTip.jsx new file mode 100644 index 0000000..f8d4130 --- /dev/null +++ b/src/components/ButtonWithTip/ButtonWithTip.jsx @@ -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 ( + + ) + } + + // 构建提示内容 + const tooltipContent = ( +
+ {tip.title &&
{tip.title}
} + {tip.description &&
{tip.description}
} + {tip.shortcut && ( +
+ 快捷键: + {tip.shortcut} +
+ )} + {tip.notes && tip.notes.length > 0 && ( +
+
注意事项:
+
    + {tip.notes.map((note, index) => ( +
  • {note}
  • + ))} +
+
+ )} +
+ ) + + return ( + +
+ + {showTipIcon && !disabled && ( + + )} +
+
+ ) +} + +export default ButtonWithTip diff --git a/src/components/MainLayout/AppSider.jsx b/src/components/MainLayout/AppSider.jsx index d2c32cb..38c0cc3 100644 --- a/src/components/MainLayout/AppSider.jsx +++ b/src/components/MainLayout/AppSider.jsx @@ -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 ? : null // 如果有子菜单 if (item.children) { - // 在收拢状态下,用 Tooltip 包装 SubMenu 的图标 - const subMenuIcon = collapsed ? ( - - {icon} - - ) : ( - icon - ) - - return ( - - {item.children.map((child) => ( - - {child.badge ? ( - - {child.label} - - - ) : ( - child.label - )} - - ))} - - ) + return { + key: item.key, + icon: icon, + label: item.label, + popupClassName: 'sider-submenu-popup', + children: item.children.map((child) => ({ + key: child.key, + label: child.badge ? ( + + {child.label} + + + ) : ( + child.label + ), + })), + } } - // 普通菜单项 - 也用 Tooltip 包装 - const menuIcon = collapsed ? ( - - {icon} - - ) : ( - icon - ) - - return ( - - {item.label} - - ) + // 普通菜单项 + 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()} - + items={getMenuItems()} + /> ) } diff --git a/src/data/docsMenuData.json b/src/data/docsMenuData.json index 99ca1f5..80a5329 100644 --- a/src/data/docsMenuData.json +++ b/src/data/docsMenuData.json @@ -94,6 +94,11 @@ "key": "chart-panel", "label": "ChartPanel", "path": "/docs/components/ChartPanel.md" + }, + { + "key": "button-extension", + "label": "ButtonExtension", + "path": "/docs/components/ButtonExtension.md" } ] } diff --git a/src/data/menuData.json b/src/data/menuData.json index 161cf4d..1bfec6c 100644 --- a/src/data/menuData.json +++ b/src/data/menuData.json @@ -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": "网络管理", diff --git a/src/examples/ButtonHelpExample.css b/src/examples/ButtonHelpExample.css new file mode 100644 index 0000000..586086b --- /dev/null +++ b/src/examples/ButtonHelpExample.css @@ -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; + } +} diff --git a/src/examples/ButtonHelpExample.jsx b/src/examples/ButtonHelpExample.jsx new file mode 100644 index 0000000..e1208c6 --- /dev/null +++ b/src/examples/ButtonHelpExample.jsx @@ -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: , + description: '向系统中添加新的主机终端设备,可以是服务器、台式机或笔记本电脑。', + scenarios: [ + '当有新设备需要接入系统管理时', + '需要扩展终端设备数量时', + '替换旧设备,添加新设备时', + ], + steps: [ + '点击"新增主机"按钮', + '填写主机基本信息(名称、IP、MAC地址等)', + '选择主机所属分组', + '配置系统和数据盘容量', + '保存并等待主机上线', + ], + warnings: [ + '请确保IP地址不与现有主机冲突', + 'MAC地址必须唯一且格式正确', + '建议先进行网络连通性测试', + ], + shortcut: 'Ctrl+N', + permission: '管理员权限', + badge: { text: '常用', color: 'blue' }, + }, + batchPowerOn: { + title: '批量开机', + icon: , + description: '同时对多台选中的主机执行开机操作,适用于需要批量启动设备的场景。', + scenarios: [ + '每日上班时批量启动办公设备', + '定时任务触发的批量开机', + '维护后批量恢复设备运行', + ], + steps: [ + '在列表中勾选需要开机的主机', + '点击"批量开机"按钮', + '确认操作(系统会显示选中的主机数量)', + '等待开机命令发送并监控状态变化', + ], + warnings: [ + '仅对离线状态的主机有效', + '请确保网络环境支持远程开机(Wake-on-LAN)', + '大量主机同时开机可能导致网络负载较高', + ], + shortcut: 'Ctrl+Shift+O', + permission: '操作员权限', + badge: { text: '批量操作', color: 'green' }, + }, + batchPowerOff: { + title: '批量关机', + icon: , + description: '同时对多台选中的主机执行关机操作,支持优雅关机和强制关机。', + scenarios: [ + '每日下班时批量关闭办公设备节能', + '系统维护前批量关闭设备', + '紧急情况下批量关闭设备', + ], + steps: [ + '在列表中勾选需要关机的主机', + '点击"批量关机"按钮', + '选择关机方式(优雅关机/强制关机)', + '确认操作并等待关机完成', + ], + warnings: [ + '仅对在线状态的主机有效', + '强制关机可能导致数据丢失,请谨慎使用', + '关机前请确保用户已保存工作', + '建议优先使用优雅关机方式', + ], + shortcut: 'Ctrl+Shift+P', + permission: '管理员权限', + badge: { text: '危险操作', color: 'orange' }, + }, + batchDelete: { + title: '批量删除', + icon: , + description: '从系统中永久删除选中的主机记录,此操作不可恢复。', + scenarios: [ + '设备报废需要从系统中移除时', + '清理长期离线且不再使用的设备', + '误添加的主机记录需要删除时', + ], + steps: [ + '在列表中勾选需要删除的主机', + '点击"批量删除"按钮', + '仔细确认删除列表中的主机信息', + '输入确认信息或密码(如需要)', + '确认删除并等待操作完成', + ], + warnings: [ + '此操作不可恢复,请谨慎操作!', + '删除主机会同时删除相关的用户绑定和镜像授权', + '建议删除前先导出主机配置备份', + '无法删除正在运行中的主机,请先关机', + ], + shortcut: 'Delete', + permission: '超级管理员权限', + badge: { text: '危险', color: 'red' }, + }, + refresh: { + title: '刷新列表', + icon: , + description: '重新从服务器获取最新的主机列表数据,更新当前页面显示。', + scenarios: [ + '需要查看最新的主机状态时', + '执行操作后确认结果时', + '怀疑数据显示不准确时', + ], + steps: ['点击"刷新"按钮', '等待数据加载完成', '查看更新后的列表'], + warnings: ['刷新会清除当前的选择状态', '正在编辑的数据可能会丢失'], + shortcut: 'F5', + permission: '所有用户', + badge: { text: '安全', color: 'green' }, + }, + } + + // 处理鼠标悬停 + const handleMouseEnter = (actionKey) => { + setCurrentAction(actionsConfig[actionKey]) + } + + // 处理鼠标离开 + const handleMouseLeave = () => { + // 延迟清空,避免快速移动时闪烁 + setTimeout(() => { + setCurrentAction(null) + }, 200) + } + + return ( +
+
+

方案一:增强型工具提示

+

鼠标悬停在按钮上即可查看详细的功能说明

+ +
+ + } + type="primary" + tip={{ + title: '新增主机', + description: '向系统中添加新的主机终端设备', + shortcut: 'Ctrl+N', + notes: [ + '请确保IP地址不与现有主机冲突', + 'MAC地址必须唯一且格式正确', + ], + }} + onClick={() => console.log('新增主机')} + /> + + } + tip={{ + title: '批量开机', + description: '同时对多台选中的主机执行开机操作', + shortcut: 'Ctrl+Shift+O', + notes: ['仅对离线状态的主机有效', '请确保网络环境支持远程开机'], + }} + onClick={() => console.log('批量开机')} + /> + + } + tip={{ + title: '批量关机', + description: '同时对多台选中的主机执行关机操作', + shortcut: 'Ctrl+Shift+P', + notes: [ + '仅对在线状态的主机有效', + '强制关机可能导致数据丢失,请谨慎使用', + ], + }} + onClick={() => console.log('批量关机')} + /> + + } + danger + tip={{ + title: '批量删除', + description: '从系统中永久删除选中的主机记录', + shortcut: 'Delete', + notes: [ + '此操作不可恢复,请谨慎操作!', + '删除主机会同时删除相关的用户绑定和镜像授权', + ], + }} + onClick={() => console.log('批量删除')} + /> + + } + tip={{ + title: '刷新列表', + description: '重新获取最新的主机列表数据', + shortcut: 'F5', + }} + onClick={() => console.log('刷新')} + /> + +
+
+ +
+

方案二:智能帮助面板(推荐)

+

+ 悬停按钮时,右侧帮助面板会实时显示详细的操作说明、使用场景和注意事项 +

+ +
+ +
handleMouseEnter('add')} onMouseLeave={handleMouseLeave}> + } + type="primary" + showTipIcon={false} + onClick={() => console.log('新增主机')} + /> +
+ +
handleMouseEnter('batchPowerOn')} + onMouseLeave={handleMouseLeave} + > + } + showTipIcon={false} + onClick={() => console.log('批量开机')} + /> +
+ +
handleMouseEnter('batchPowerOff')} + onMouseLeave={handleMouseLeave} + > + } + showTipIcon={false} + onClick={() => console.log('批量关机')} + /> +
+ +
handleMouseEnter('batchDelete')} + onMouseLeave={handleMouseLeave} + > + } + danger + showTipIcon={false} + onClick={() => console.log('批量删除')} + /> +
+ +
handleMouseEnter('refresh')} onMouseLeave={handleMouseLeave}> + } + showTipIcon={false} + onClick={() => console.log('刷新')} + /> +
+
+
+
+ + {/* 帮助面板 */} + setShowHelpPanel(false)} + currentAction={currentAction} + allActions={Object.values(actionsConfig)} + /> + + {/* 浮动帮助按钮 */} + } + type="primary" + tooltip="打开帮助面板" + onClick={() => setShowHelpPanel(!showHelpPanel)} + /> +
+ ) +} + +export default ButtonHelpExample diff --git a/src/pages/AllButtonDesigns.css b/src/pages/AllButtonDesigns.css new file mode 100644 index 0000000..86500d9 --- /dev/null +++ b/src/pages/AllButtonDesigns.css @@ -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; + } +} diff --git a/src/pages/AllButtonDesigns.jsx b/src/pages/AllButtonDesigns.jsx new file mode 100644 index 0000000..3cd1423 --- /dev/null +++ b/src/pages/AllButtonDesigns.jsx @@ -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: , + description: '向系统中添加新的主机终端设备,可以是服务器、台式机或笔记本电脑。', + scenarios: [ + '当有新设备需要接入系统管理时', + '需要扩展终端设备数量时', + '替换旧设备,添加新设备时', + ], + quickTips: ['确保设备网络连通', 'MAC地址必须唯一'], + steps: [ + '点击"新增主机"按钮', + '填写主机基本信息(名称、IP、MAC地址等)', + '选择主机所属分组', + '配置系统和数据盘容量', + '保存并等待主机上线', + ], + warnings: [ + '请确保IP地址不与现有主机冲突', + 'MAC地址必须唯一且格式正确', + '建议先进行网络连通性测试', + ], + quickTip: '填写设备信息前请先确认网络连通性', + warning: '请确保IP地址不与现有主机冲突', + shortcut: 'Ctrl+N', + permission: '管理员权限', + badge: { text: '常用', color: 'blue' }, + }, + batchPowerOn: { + title: '批量开机', + icon: , + 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: , + description: '从系统中永久删除选中的主机记录,此操作不可恢复。', + scenarios: [ + '设备报废需要从系统中移除时', + '清理长期离线且不再使用的设备', + '误添加的主机记录需要删除时', + ], + quickTips: ['此操作不可恢复', '会删除所有关联数据'], + steps: [ + '在列表中勾选需要删除的主机', + '点击"批量删除"按钮', + '仔细确认删除列表', + '输入确认信息', + '确认删除', + ], + warnings: [ + '此操作不可恢复,请谨慎操作!', + '删除主机会同时删除相关的用户绑定和镜像授权', + '建议删除前先导出主机配置备份', + '无法删除正在运行中的主机,请先关机', + ], + quickTip: '此操作不可恢复,请谨慎确认', + warning: '此操作不可恢复!会删除所有关联数据', + shortcut: 'Delete', + permission: '超级管理员权限', + badge: { text: '危险', color: 'red' }, + }, + refresh: { + title: '刷新列表', + icon: , + 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 ( +
+
+ +
+

按钮介绍设计方案对比

+

5种创新设计,为用户提供清晰的操作指引

+
+
+ + {/* 方案1:增强型工具提示 */} + +
+

+ 特点: + 渐变色彩背景 + 脉冲动画指示器 + 多维度信息展示 +

+

+ 适用场景: + 简单操作,需要快速了解功能,不希望占用额外页面空间 +

+
+ +
+ + } + type="primary" + tip={{ + title: '新增主机', + description: '向系统中添加新的主机终端设备', + shortcut: 'Ctrl+N', + notes: ['请确保IP地址不与现有主机冲突', 'MAC地址必须唯一且格式正确'], + }} + onClick={() => console.log('新增主机')} + /> + + } + tip={{ + title: '批量开机', + description: '同时对多台选中的主机执行开机操作', + shortcut: 'Ctrl+Shift+O', + notes: ['仅对离线状态的主机有效', '请确保网络环境支持远程开机'], + }} + onClick={() => console.log('批量开机')} + /> + + } + danger + tip={{ + title: '批量删除', + description: '从系统中永久删除选中的主机记录', + shortcut: 'Delete', + notes: ['此操作不可恢复,请谨慎操作!', '删除主机会同时删除相关的用户绑定'], + }} + onClick={() => console.log('批量删除')} + /> + + } + tip={{ + title: '刷新列表', + description: '重新获取最新的主机列表数据', + shortcut: 'F5', + }} + onClick={() => console.log('刷新')} + /> + +
+
+ + {/* 方案2:智能帮助面板 */} + +
+

+ 特点: + 侧边抽屉式帮助 + 实时显示详细信息 + 完整的操作指引 +

+

+ 适用场景: + 复杂业务操作,需要详细指引,新用户培训和引导 +

+
+ +
+ +
handlePanelHover('add')} + onMouseLeave={handlePanelLeave} + > + + +
+ +
handlePanelHover('batchPowerOn')} + onMouseLeave={handlePanelLeave} + > + + +
+ +
handlePanelHover('batchDelete')} + onMouseLeave={handlePanelLeave} + > + + +
+ +
handlePanelHover('refresh')} + onMouseLeave={handlePanelLeave} + > + + +
+
+

💡 悬停按钮查看提示,点击"?"图标打开详细帮助面板

+
+
+ + {/* 方案3:悬浮展开卡片 */} + +
+

+ 特点: + 精美的悬浮卡片 + 滑入动画 + 分层信息展示 +

+

+ 适用场景: + 需要展示较多信息,但不想使用弹窗打断操作流程 +

+
+ +
+ + } + type="primary" + cardInfo={actionsConfig.add} + onClick={() => console.log('新增主机')} + /> + + } + cardInfo={actionsConfig.batchPowerOn} + onClick={() => console.log('批量开机')} + /> + + } + danger + cardInfo={actionsConfig.batchDelete} + onClick={() => console.log('批量删除')} + /> + + } + cardInfo={actionsConfig.refresh} + onClick={() => console.log('刷新')} + /> + +

💡 鼠标悬停在按钮上,右侧会展开详细信息卡片

+
+
+ + {/* 方案4:智能引导 */} + +
+

+ 特点: + 简洁扁平设计 + 独立帮助图标 + 点击查看详情 +

+

+ 适用场景: + 需要详细引导,但不希望干扰主操作流程 +

+
+ +
+ + } + type="primary" + guide={actionsConfig.add} + onClick={() => console.log('新增主机')} + /> + + } + guide={actionsConfig.batchPowerOn} + onClick={() => console.log('批量开机')} + /> + + } + danger + guide={actionsConfig.batchDelete} + onClick={() => console.log('批量删除')} + /> + + } + guide={actionsConfig.refresh} + onClick={() => console.log('刷新')} + /> + +

💡 点击按钮旁边的帮助图标查看详细引导

+
+
+ + {/* 方案5:底部固定提示栏 */} + +
+

+ 特点: + 固定底部位置 + 实时更新 + 不遮挡内容 + 多主题可选 +

+

+ 适用场景: + 需要始终可见的提示信息,不希望被tooltip遮挡操作区域 +

+
+ +
+
+ 主题: + setBottomTheme(e.target.value)} + size="small" + > + 渐变 + 浅色 + 深色 + +
+ +
handleHintHover('add')} + onMouseLeave={handleHintLeave} + style={{ display: 'inline-block' }} + > + +
+ +
handleHintHover('batchPowerOn')} + onMouseLeave={handleHintLeave} + style={{ display: 'inline-block' }} + > + +
+ +
handleHintHover('batchDelete')} + onMouseLeave={handleHintLeave} + style={{ display: 'inline-block' }} + > + +
+ +
handleHintHover('refresh')} + onMouseLeave={handleHintLeave} + style={{ display: 'inline-block' }} + > + +
+
+

💡 鼠标悬停在按钮上,底部会显示实时提示信息

+
+
+ + {/* 帮助面板 */} + setShowHelpPanel(false)} + currentAction={currentAction} + allActions={Object.values(actionsConfig)} + onActionSelect={(action) => setCurrentAction(action)} + /> + + {/* 底部提示栏 */} + setShowBottomHint(false)} + theme={bottomTheme} + /> + + {/* 浮动帮助按钮 */} + } + type="primary" + tooltip="打开帮助面板" + onClick={() => setShowHelpPanel(!showHelpPanel)} + /> +
+ ) +} + +export default AllButtonDesigns diff --git a/src/pages/DocsPage.jsx b/src/pages/DocsPage.jsx index e20f754..6deb754 100644 --- a/src/pages/DocsPage.jsx +++ b/src/pages/DocsPage.jsx @@ -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() {