Skip to content

Material 3 弹出菜单样式规范

最后更新:2026-01-22 版本:v1.0 状态:✅ 强制执行

💡 提示:本文档是PopupMenu的详细实战指南。如需查看完整的 Material 3 设计系统规范(包括文本、图标、颜色等基础规范),请参考 Material 3 设计系统


📋 目录

  1. 设计规范
  2. 使用场景
  3. 样式规范
  4. 代码示例
  5. 常见错误
  6. 图标规范
  7. 间距规范
  8. 实际案例
  9. 最佳实践

设计规范

主题配置(已在 theme_config.dart 中配置)

dart
// 位置:lib/config/theme_config.dart
popupMenuTheme: PopupMenuThemeData(
  color: colorScheme.surface,  // 不透明,纯色背景
  elevation: 1,  // 柔和阴影
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),  // 12dp圆角
  ),
  textStyle: TextStyle(
    color: colorScheme.onSurface,
    fontSize: 14,
  ),
),

使用场景

何时使用 PopupMenu

场景推荐说明
AppBar 右侧操作✅ 推荐放置次要操作(设置、更多选项)
卡片项操作✅ 推荐编辑、删除、分享等操作
列表项操作✅ 推荐长按或点击触发的上下文菜单
底部导航操作❌ 不推荐使用 BottomSheet 更合适
主要操作❌ 不推荐应使用显式按钮

菜单项数量建议

  • 推荐:3-5 个菜单项
  • ⚠️ 可接受:6-7 个菜单项
  • 避免:超过 8 个菜单项(考虑分组或使用对话框)

样式规范

属性说明
圆角12dp与Card、Button保持一致
阴影elevation: 1柔和阴影,不突兀
背景色surface(不透明)暖白色(浅色模式)/暖灰色(深色模式)
文字大小14spbodyMedium
文字颜色onSurface自动适配深浅色模式
菜单项高度48dp (默认)Material 3标准
水平内边距12dp菜单项左右padding
最小宽度112dpMaterial 3最小宽度
最大宽度280dpMaterial 3最大宽度

✅ 正确用法

1. 基础菜单(图标 + 文字)

dart
PopupMenuButton<String>(
  icon: const Icon(Icons.more_vert),
  tooltip: 'More actions',
  offset: const Offset(0, 40),  // 向下偏移,避免遮挡内容
  itemBuilder: (context) => [
    PopupMenuItem<String>(
      value: 'action1',
      child: Row(
        children: [
          Icon(
            Icons.edit_outlined,
            color: Theme.of(context).colorScheme.onSurface,
            size: 20,
          ),
          const SizedBox(width: 12),
          const Text('编辑'),
        ],
      ),
    ),
    PopupMenuItem<String>(
      value: 'action2',
      child: Row(
        children: [
          Icon(
            Icons.delete_outline,
            color: Colors.red.shade600,
            size: 20,
          ),
          const SizedBox(width: 12),
          const Text('删除'),
        ],
      ),
    ),
  ],
  onSelected: (value) {
    // 处理菜单选择
  },
)

2. 纯图标菜单(国际化设计)

dart
PopupMenuButton<String>(
  icon: const Icon(Icons.more_vert),
  tooltip: 'Actions',
  offset: const Offset(0, 40),
  itemBuilder: (context) => [
    // 只用图标,不用文字(适合国际化)
    PopupMenuItem<String>(
      value: 'undo',
      enabled: currentIndex > 0,  // 条件禁用
      child: Icon(
        Icons.undo,
        color: currentIndex > 0
            ? Theme.of(context).colorScheme.onSurface
            : Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
        size: 24,
      ),
    ),
    PopupMenuItem<String>(
      value: 'star',
      child: Icon(
        Icons.star_outline,
        color: Colors.amber.shade600,
        size: 24,
      ),
    ),
  ],
  onSelected: (value) {
    // 处理菜单选择
  },
)

3. 禁用状态

dart
PopupMenuItem<String>(
  value: 'disabled_action',
  enabled: false,  // 禁用
  child: Row(
    children: [
      Icon(
        Icons.lock_outline,
        color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),  // 38%透明度
        size: 20,
      ),
      const SizedBox(width: 12),
      Text(
        '已锁定',
        style: TextStyle(
          color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
        ),
      ),
    ],
  ),
)

4. 分组菜单(带分隔线)

dart
PopupMenuButton<String>(
  icon: const Icon(Icons.more_vert),
  itemBuilder: (context) => [
    // 第一组
    PopupMenuItem(value: 'edit', child: Text('编辑')),
    PopupMenuItem(value: 'copy', child: Text('复制')),

    // 分隔线
    const PopupMenuDivider(),

    // 第二组(危险操作)
    PopupMenuItem(
      value: 'delete',
      child: Row(
        children: [
          Icon(Icons.delete_outline, color: Colors.red.shade600, size: 20),
          const SizedBox(width: 12),
          Text('删除', style: TextStyle(color: Colors.red.shade600)),
        ],
      ),
    ),
  ],
)

❌ 常见错误

1. 硬编码背景色

dart
// ❌ 错误:覆盖主题配置
PopupMenuButton(
  color: Colors.white,  // 不要硬编码!
  itemBuilder: ...
)

// ✅ 正确:使用主题配置
PopupMenuButton(
  // 自动使用 popupMenuTheme
  itemBuilder: ...
)

2. 圆角不一致

dart
// ❌ 错误:自定义圆角
PopupMenuButton(
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(8),  // 与主题不一致!
  ),
  itemBuilder: ...
)

// ✅ 正确:使用主题配置
PopupMenuButton(
  // 自动使用 popupMenuTheme 的圆角
  itemBuilder: ...
)

3. 阴影过深

dart
// ❌ 错误:阴影太深
PopupMenuButton(
  elevation: 8,  // 太突兀!
  itemBuilder: ...
)

// ✅ 正确:使用主题配置
PopupMenuButton(
  // 自动使用 popupMenuTheme 的 elevation: 1
  itemBuilder: ...
)

4. 菜单遮挡内容

dart
// ❌ 错误:没有偏移,遮挡锚点
PopupMenuButton(
  icon: const Icon(Icons.more_vert),
  itemBuilder: ...
)

// ✅ 正确:向下偏移
PopupMenuButton(
  icon: const Icon(Icons.more_vert),
  offset: const Offset(0, 40),  // 向下偏移40px
  itemBuilder: ...
)

5. 图标大小不一致

dart
// ❌ 错误:图标大小不一致
PopupMenuItem(
  child: Row(
    children: [
      Icon(Icons.edit, size: 16),    // 16px
      Icon(Icons.delete, size: 24),  // 24px - 不一致!
    ],
  ),
)

// ✅ 正确:统一大小
PopupMenuItem(
  child: Row(
    children: [
      Icon(Icons.edit_outlined, size: 20),    // 统一20px
      const SizedBox(width: 12),
      Icon(Icons.delete_outline, size: 20),  // 统一20px
    ],
  ),
)

🎨 图标规范

图标大小

用途大小使用场景
20dp推荐菜单项图标(带文字)
24dp推荐菜单项图标(纯图标菜单)
18dp可选次要图标

图标样式

dart
// ✅ 推荐:使用 outlined 样式
Icons.edit_outlined
Icons.delete_outline
Icons.star_outline

// ❌ 避免:使用 filled 样式(过于沉重)
Icons.edit  // 太重
Icons.delete
Icons.star

图标颜色

dart
// 常规图标
Icon(
  Icons.edit_outlined,
  color: Theme.of(context).colorScheme.onSurface,  // 使用主题色
  size: 20,
)

// 强调图标(如删除)
Icon(
  Icons.delete_outline,
  color: Colors.red.shade600,  // 可以使用语义化颜色
  size: 20,
)

// 成功图标
Icon(
  Icons.check_circle_outline,
  color: Colors.green.shade600,
  size: 20,
)

// 禁用图标
Icon(
  Icons.lock_outline,
  color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),  // 38%透明度
  size: 20,
)

📏 间距规范

菜单项布局

dart
PopupMenuItem(
  child: Row(
    children: [
      Icon(..., size: 20),
      const SizedBox(width: 12),  // 图标与文字间距:12dp
      const Text('操作'),
    ],
  ),
)

菜单偏移

dart
// AppBar右侧菜单
PopupMenuButton(
  offset: const Offset(0, 40),  // 向下偏移40dp
  ...
)

// 页面内容菜单
PopupMenuButton(
  offset: const Offset(0, 8),  // 向下偏移8dp(轻微偏移)
  ...
)

🔍 实际案例

案例1:Review Session 页面菜单

位置lib/features/vocabulary_notebook/presentation/pages/review_session_page.dart

dart
PopupMenuButton<String>(
  icon: const Icon(Icons.more_vert),
  tooltip: 'More actions',
  offset: const Offset(0, 40),  // 避免遮挡进度分数
  itemBuilder: (context) => [
    // 撤销(纯图标)
    PopupMenuItem<String>(
      value: 'undo',
      enabled: currentIndex > 0,
      child: Icon(
        Icons.undo,
        color: currentIndex > 0
            ? Theme.of(context).colorScheme.onSurface
            : Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
        size: 24,
      ),
    ),
    // 标记已掌握(图标+文字)
    PopupMenuItem<String>(
      value: 'mark_mastered',
      child: Row(
        children: [
          Icon(
            Icons.verified_outlined,
            color: Colors.green.shade600,
            size: 20,
          ),
          const SizedBox(width: 12),
          const Text('标记已掌握'),
        ],
      ),
    ),
  ],
  onSelected: (value) {
    if (value == 'undo') {
      notifier.undoLastReview();
    } else if (value == 'mark_mastered') {
      _cardKey.currentState?.playSlideOutAnimation(() {
        notifier.markAsEasy();
      });
    }
  },
)

设计亮点

  • ✅ 使用 offset 避免遮挡标题栏进度分数
  • ✅ 撤销使用纯图标(国际化)
  • ✅ 禁用状态用38%透明度
  • ✅ 成功操作用绿色图标

📊 主题配置验证

验证清单

  • [x] 圆角:12dp
  • [x] 阴影:elevation 1
  • [x] 背景:surface(不透明)
  • [x] 文字:14sp, onSurface
  • [x] 浅色模式:暖白色背景 (#FAF6F0)
  • [x] 深色模式:暖灰色背景 (#2A2520)

测试方法

dart
// 创建测试菜单
PopupMenuButton(
  itemBuilder: (context) => [
    PopupMenuItem(child: Text('测试项')),
  ],
)

检查项

  1. 菜单背景是否透明(能看到下方内容)
  2. 圆角是否为12dp
  3. 阴影是否柔和(不突兀)
  4. 文字颜色是否清晰可读

🚀 最佳实践

1. 使用主题配置

原则:除非有特殊需求,否则不要覆盖 popupMenuTheme

dart
// ✅ 好:使用主题
PopupMenuButton(
  itemBuilder: ...
)

// ❌ 坏:覆盖主题
PopupMenuButton(
  color: Colors.white,
  elevation: 3,
  shape: ...,
  itemBuilder: ...
)

2. 语义化颜色

原则:使用语义化颜色表达操作意图。

dart
// 删除/危险操作 → 红色
Icon(Icons.delete_outline, color: Colors.red.shade600)

// 成功/确认操作 → 绿色
Icon(Icons.check_circle_outline, color: Colors.green.shade600)

// 警告操作 → 橙色
Icon(Icons.warning_outline, color: Colors.orange.shade600)

// 常规操作 → 主题色
Icon(Icons.edit_outlined, color: colorScheme.onSurface)

3. 国际化设计

原则:纯图标菜单适合国际化应用。

dart
// ✅ 国际化友好(纯图标)
PopupMenuItem(
  value: 'undo',
  child: Icon(Icons.undo, size: 24),
)

// ⚠️ 需要翻译(文字)
PopupMenuItem(
  value: 'undo',
  child: Row(
    children: [
      Icon(Icons.undo, size: 20),
      const SizedBox(width: 12),
      const Text('撤销'),  // 需要翻译
    ],
  ),
)

4. 菜单项数量

原则:控制菜单项数量,避免滚动。

  • 推荐:3-5个菜单项
  • ⚠️ 可接受:6-7个菜单项
  • 避免:超过8个菜单项

🔗 相关规范


📝 变更日志

日期版本变更
2026-01-22v1.0初始版本,定义PopupMenu基础规范

维护者:Reading Vocab Helper Team 问题反馈:请在项目中提 Issue