第5章:容器类组件 —— 5.7 页面骨架(Scaffold)

67 阅读8分钟

5.7 页面骨架(Scaffold)

本节介绍《Flutter实战·第二版》第5章的 5.7 节内容,讲解 Scaffold 组件的使用,这是 Material Design 中最重要的页面骨架组件。

📚 学习内容

  1. Scaffold 基础 - Material Design 页面骨架
  2. AppBar - 顶部导航栏
  3. Drawer - 左右抽屉菜单
  4. FloatingActionButton - 悬浮操作按钮
  5. BottomNavigationBar - 底部导航栏
  6. BottomAppBar - 打洞效果的底部导航栏
  7. 完整示例 - 综合所有功能

🎯 核心概念

Scaffold 是什么?

Scaffold 是 Material Design 中的页面骨架,它提供了一套标准的页面结构,包括:

  • 顶部导航栏(AppBar)
  • 左右抽屉菜单(Drawer)
  • 页面主体(Body)
  • 悬浮按钮(FloatingActionButton)
  • 底部导航栏(BottomNavigationBar/BottomAppBar)

使用 Scaffold 可以快速构建符合 Material Design 规范的页面。

📖 基础用法

Scaffold 完整示例

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 1. 顶部导航栏
      appBar: AppBar(
        title: Text('我的页面'),
        actions: [
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () {},
          ),
          IconButton(
            icon: Icon(Icons.share),
            onPressed: () {},
          ),
        ],
      ),
      
      // 2. 左侧抽屉
      drawer: Drawer(
        child: ListView(...),
      ),
      
      // 3. 右侧抽屉
      endDrawer: Drawer(
        child: ListView(...),
      ),
      
      // 4. 页面主体
      body: Center(
        child: Text('页面内容'),
      ),
      
      // 5. 悬浮按钮
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
      
      // 6. 悬浮按钮位置
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
      
      // 7. 底部导航栏
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
    );
  }
}

🎨 主要组件详解

1. AppBar(导航栏)

AppBar 是页面顶部的导航栏,提供标题、导航按钮和操作按钮。

基本属性
AppBar(
  // 标题
  title: Text('标题'),
  
  // 是否居中
  centerTitle: true,
  
  // 左侧按钮(默认是返回按钮)
  leading: IconButton(
    icon: Icon(Icons.menu),
    onPressed: () {},
  ),
  
  // 右侧操作按钮
  actions: [
    IconButton(icon: Icon(Icons.search), onPressed: () {}),
    IconButton(icon: Icon(Icons.share), onPressed: () {}),
  ],
  
  // 背景色
  backgroundColor: Colors.blue,
  
  // 阴影高度
  elevation: 4.0,
  
  // 底部组件(如 TabBar)
  bottom: TabBar(
    tabs: [
      Tab(text: 'Tab 1'),
      Tab(text: 'Tab 2'),
    ],
  ),
)
AppBar 示例
class MyAppBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('我的应用'),
        centerTitle: true,
        leading: IconButton(
          icon: Icon(Icons.menu),
          onPressed: () {
            // 打开抽屉
            Scaffold.of(context).openDrawer();
          },
        ),
        actions: [
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () {},
          ),
          IconButton(
            icon: Icon(Icons.more_vert),
            onPressed: () {},
          ),
        ],
      ),
      body: Center(child: Text('内容')),
    );
  }
}

2. Drawer(抽屉菜单)

Drawer 是从屏幕左侧或右侧滑出的侧边菜单。

基本用法
Scaffold(
  // 左侧抽屉
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Text('头部'),
        ),
        ListTile(
          leading: Icon(Icons.home),
          title: Text('首页'),
          onTap: () {
            Navigator.pop(context);
          },
        ),
        ListTile(
          leading: Icon(Icons.settings),
          title: Text('设置'),
          onTap: () {
            Navigator.pop(context);
          },
        ),
      ],
    ),
  ),
  
  // 右侧抽屉
  endDrawer: Drawer(
    child: ListView(...),
  ),
)
打开抽屉的方式
// 方式1:手势滑动(自动支持)
// 从屏幕左边缘向右滑动 → 打开 drawer
// 从屏幕右边缘向左滑动 → 打开 endDrawer

// 方式2:通过 ScaffoldState
Scaffold.of(context).openDrawer();      // 打开左侧抽屉
Scaffold.of(context).openEndDrawer();   // 打开右侧抽屉

// 方式3:使用 Builder 确保 context 正确
Builder(
  builder: (context) => IconButton(
    icon: Icon(Icons.menu),
    onPressed: () {
      Scaffold.of(context).openDrawer();
    },
  ),
)
自定义抽屉菜单
class MyDrawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        removeTop: true, // 移除顶部默认留白
        child: Column(
          children: [
            // 头部
            Container(
              height: 150,
              color: Colors.blue,
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CircleAvatar(
                      radius: 40,
                      child: Icon(Icons.person, size: 50),
                    ),
                    SizedBox(height: 8),
                    Text(
                      '用户名',
                      style: TextStyle(color: Colors.white, fontSize: 18),
                    ),
                  ],
                ),
              ),
            ),
            // 菜单列表
            Expanded(
              child: ListView(
                padding: EdgeInsets.zero,
                children: [
                  ListTile(
                    leading: Icon(Icons.home),
                    title: Text('首页'),
                    onTap: () => Navigator.pop(context),
                  ),
                  ListTile(
                    leading: Icon(Icons.settings),
                    title: Text('设置'),
                    onTap: () => Navigator.pop(context),
                  ),
                  Divider(),
                  ListTile(
                    leading: Icon(Icons.info),
                    title: Text('关于'),
                    onTap: () => Navigator.pop(context),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

3. FloatingActionButton(悬浮按钮)

FloatingActionButton(简称 FAB)是 Material Design 中的悬浮操作按钮,通常用于页面的主要操作。

基本用法
Scaffold(
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
    tooltip: '添加',
  ),
)
FAB 位置

通过 floatingActionButtonLocation 属性指定 FAB 的位置:

Scaffold(
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
  floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
)

常用位置:

位置说明
endFloat右下角(默认)
startFloat左下角
centerFloat中间底部
endDocked右下角停靠在 BottomAppBar
centerDocked中间底部停靠在 BottomAppBar
endTop右上角
startTop左上角
扩展 FAB
FloatingActionButton.extended(
  onPressed: () {},
  icon: Icon(Icons.add),
  label: Text('添加'),
)
小尺寸 FAB
FloatingActionButton.small(
  onPressed: () {},
  child: Icon(Icons.add),
)
⚠️ Hero 动画冲突

如果页面中有多个 FAB,需要指定不同的 heroTag

FloatingActionButton(
  heroTag: 'fab1', // 必须唯一
  onPressed: () {},
  child: Icon(Icons.add),
)

FloatingActionButton(
  heroTag: 'fab2', // 不同的 tag
  onPressed: () {},
  child: Icon(Icons.edit),
)

4. BottomNavigationBar(底部导航栏)

BottomNavigationBar 是标准的 Material Design 底部导航栏。

基本用法
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 0;

  final List<Widget> _pages = [
    HomePage(),
    MessagePage(),
    ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.message),
            label: '消息',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '我的',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
    );
  }
}
属性说明
BottomNavigationBar(
  // 导航项列表
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      activeIcon: Icon(Icons.home_filled), // 选中时的图标
      label: '首页',
      backgroundColor: Colors.blue, // 背景色(type 为 shifting 时有效)
    ),
  ],
  
  // 当前选中的索引
  currentIndex: 0,
  
  // 点击回调
  onTap: (index) {},
  
  // 类型
  type: BottomNavigationBarType.fixed, // fixed 或 shifting
  
  // 选中项颜色
  selectedItemColor: Colors.blue,
  
  // 未选中项颜色
  unselectedItemColor: Colors.grey,
  
  // 背景色
  backgroundColor: Colors.white,
  
  // 阴影高度
  elevation: 8.0,
  
  // 是否显示选中标签
  showSelectedLabels: true,
  
  // 是否显示未选中标签
  showUnselectedLabels: true,
)
BottomNavigationBarType
  • fixed:固定模式,所有项平分空间(默认,适用于 2-3 项)
  • shifting:切换模式,选中项会变大,有动画效果(适用于 4-5 项)

5. BottomAppBar(打洞效果)

BottomAppBar 可以配合 FAB 实现"打洞"效果。

基本用法
Scaffold(
  body: Center(child: Text('内容')),
  
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
  
  // FAB 位置必须是 centerDocked 才能实现打洞
  floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
  
  bottomNavigationBar: BottomAppBar(
    color: Colors.white,
    shape: CircularNotchedRectangle(), // 圆形打洞
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        IconButton(icon: Icon(Icons.home), onPressed: () {}),
        SizedBox(width: 40), // 中间留空给 FAB
        IconButton(icon: Icon(Icons.person), onPressed: () {}),
      ],
    ),
  ),
)
shape 属性

BottomAppBarshape 属性决定洞的外形:

  • CircularNotchedRectangle():圆形洞(最常用)
  • 也可以自定义 NotchedShape 实现其他形状(如钻石形)
打洞位置

打洞的位置取决于 FloatingActionButton 的位置:

// 中间打洞
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked

// 右侧打洞
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked

🔍 获取 ScaffoldState

方式1:Scaffold.of(context)

// 在 Scaffold 的子组件中
ElevatedButton(
  onPressed: () {
    Scaffold.of(context).openDrawer(); // 打开抽屉
  },
  child: Text('打开抽屉'),
)

方式2:使用 Builder

当在 Scaffold 的同一个 build 方法中使用时,需要用 Builder 包裹:

Scaffold(
  appBar: AppBar(
    leading: Builder(
      builder: (context) => IconButton(
        icon: Icon(Icons.menu),
        onPressed: () {
          Scaffold.of(context).openDrawer(); // 正确的 context
        },
      ),
    ),
  ),
  drawer: Drawer(...),
  body: Text('内容'),
)

方式3:使用 GlobalKey

class MyPage extends StatelessWidget {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.menu),
          onPressed: () {
            _scaffoldKey.currentState!.openDrawer();
          },
        ),
      ),
      drawer: Drawer(...),
      body: Text('内容'),
    );
  }
}

💡 实战技巧

1. 显示 SnackBar

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('这是一条消息'),
    duration: Duration(seconds: 2),
    action: SnackBarAction(
      label: '撤销',
      onPressed: () {},
    ),
  ),
);

2. 移除 Drawer 默认留白

Drawer(
  child: MediaQuery.removePadding(
    context: context,
    removeTop: true,    // 移除顶部留白
    removeBottom: true, // 移除底部留白
    child: ListView(...),
  ),
)

3. 底部导航栏切换页面

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;
  
  final List<Widget> _pages = [
    Page1(),
    Page2(),
    Page3(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages, // 使用 IndexedStack 保持页面状态
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

4. 自定义 AppBar 高度

AppBar(
  title: Text('标题'),
  toolbarHeight: 80, // 自定义高度(默认 56)
)

⚠️ 注意事项

1. Context 问题

在 Scaffold 的同一个 build 方法中调用 Scaffold.of(context) 时,context 是错误的,需要使用 Builder

// ❌ 错误
Scaffold(
  appBar: AppBar(
    leading: IconButton(
      icon: Icon(Icons.menu),
      onPressed: () {
        Scaffold.of(context).openDrawer(); // context 不对
      },
    ),
  ),
)

// ✅ 正确
Scaffold(
  appBar: AppBar(
    leading: Builder(
      builder: (context) => IconButton(
        icon: Icon(Icons.menu),
        onPressed: () {
          Scaffold.of(context).openDrawer(); // context 正确
        },
      ),
    ),
  ),
)

2. BottomNavigationBar 项数限制

  • type: fixed 模式:适用于 2-3 个项
  • type: shifting 模式:适用于 4-5 个项
  • 超过 5 个项时,考虑使用其他导航方式

3. FAB 的 heroTag 冲突

多个 FAB 时必须设置不同的 heroTag,否则会有 Hero 动画冲突。


📊 完整示例

class CompleteScaffoldDemo extends StatefulWidget {
  @override
  _CompleteScaffoldDemoState createState() => _CompleteScaffoldDemoState();
}

class _CompleteScaffoldDemoState extends State<CompleteScaffoldDemo> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 1. AppBar
      appBar: AppBar(
        title: Text('完整示例'),
        centerTitle: true,
        actions: [
          IconButton(icon: Icon(Icons.search), onPressed: () {}),
          IconButton(icon: Icon(Icons.share), onPressed: () {}),
        ],
      ),
      
      // 2. 左侧 Drawer
      drawer: Drawer(
        child: ListView(
          children: [
            DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text('左侧菜单'),
            ),
            ListTile(
              leading: Icon(Icons.home),
              title: Text('首页'),
              onTap: () => Navigator.pop(context),
            ),
          ],
        ),
      ),
      
      // 3. 右侧 Drawer
      endDrawer: Drawer(
        child: ListView(
          children: [
            DrawerHeader(
              decoration: BoxDecoration(color: Colors.green),
              child: Text('右侧菜单'),
            ),
            ListTile(
              leading: Icon(Icons.settings),
              title: Text('设置'),
              onTap: () => Navigator.pop(context),
            ),
          ],
        ),
      ),
      
      // 4. Body
      body: Center(
        child: Text('页面 $_selectedIndex'),
      ),
      
      // 5. FloatingActionButton
      floatingActionButton: FloatingActionButton(
        heroTag: 'complete_fab',
        onPressed: () {},
        child: Icon(Icons.add),
      ),
      
      // 6. BottomNavigationBar
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
    );
  }
}

🎓 练习题

练习1:实现带搜索的 AppBar

目标: 创建一个可切换搜索模式的 AppBar

要求:

  1. 默认显示标题和搜索图标
  2. 点击搜索图标后 AppBar 变成搜索框
  3. 可以取消搜索返回普通模式
💡 查看答案
class SearchAppBarPage extends StatefulWidget {
  const SearchAppBarPage({super.key});

  @override
  State<SearchAppBarPage> createState() => _SearchAppBarPageState();
}

class _SearchAppBarPageState extends State<SearchAppBarPage> {
  bool _isSearching = false;
  final TextEditingController _searchController = TextEditingController();

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: _isSearching
            ? TextField(
                controller: _searchController,
                autofocus: true,
                decoration: const InputDecoration(
                  hintText: '搜索...',
                  border: InputBorder.none,
                  hintStyle: TextStyle(color: Colors.white70),
                ),
                style: const TextStyle(color: Colors.white),
                onSubmitted: (value) {
                  print('搜索: $value');
                },
              )
            : const Text('我的应用'),
        actions: [
          if (_isSearching)
            IconButton(
              icon: const Icon(Icons.clear),
              onPressed: () {
                setState(() {
                  _isSearching = false;
                  _searchController.clear();
                });
              },
            )
          else
            IconButton(
              icon: const Icon(Icons.search),
              onPressed: () {
                setState(() {
                  _isSearching = true;
                });
              },
            ),
        ],
      ),
      body: Center(
        child: Text('搜索模式: $_isSearching'),
      ),
    );
  }
}

练习2:实现带未读消息数的 BottomNavigationBar

目标: 创建一个底部导航栏,在图标上显示未读消息数徽章

要求:

  1. 3-4 个导航项
  2. 消息页显示未读数量徽章
  3. 点击后清除未读数
💡 查看答案
class BadgeBottomNavPage extends StatefulWidget {
  const BadgeBottomNavPage({super.key});

  @override
  State<BadgeBottomNavPage> createState() => _BadgeBottomNavPageState();
}

class _BadgeBottomNavPageState extends State<BadgeBottomNavPage> {
  int _selectedIndex = 0;
  int _unreadCount = 5;

  // 创建带徽章的图标
  Widget _buildIconWithBadge(IconData icon, int count) {
    return Stack(
      clipBehavior: Clip.none,
      children: [
        Icon(icon),
        if (count > 0)
          Positioned(
            right: -8,
            top: -8,
            child: Container(
              padding: const EdgeInsets.all(4),
              decoration: const BoxDecoration(
                color: Colors.red,
                shape: BoxShape.circle,
              ),
              constraints: const BoxConstraints(
                minWidth: 16,
                minHeight: 16,
              ),
              child: Text(
                count > 99 ? '99+' : count.toString(),
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 10,
                  fontWeight: FontWeight.bold,
                ),
                textAlign: TextAlign.center,
              ),
            ),
          ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('带徽章的底部导航')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('当前页面: $_selectedIndex'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _unreadCount += 1;
                });
              },
              child: const Text('增加未读消息'),
            ),
          ],
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
            if (index == 1) {
              // 点击消息页时清除未读数
              _unreadCount = 0;
            }
          });
        },
        items: [
          const BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: _buildIconWithBadge(Icons.message, _unreadCount),
            label: '消息',
          ),
          const BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

练习3:实现电商应用首页布局

目标: 创建一个完整的电商应用首页,包含所有常用 Scaffold 组件

要求:

  1. AppBar:搜索框 + 购物车图标(带徽章)
  2. Drawer:用户信息 + 菜单列表
  3. Body:商品列表
  4. FloatingActionButton:快速发布按钮(居中停靠)
  5. BottomNavigationBar:4 个导航项
💡 查看答案
class EcommerceHomePage extends StatefulWidget {
  const EcommerceHomePage({super.key});

  @override
  State<EcommerceHomePage> createState() => _EcommerceHomePageState();
}

class _EcommerceHomePageState extends State<EcommerceHomePage> {
  int _selectedIndex = 0;
  int _cartCount = 3;

  final List<String> _pageTitles = ['首页', '分类', '购物车', '我的'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Container(
          height: 40,
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(20),
          ),
          child: TextField(
            decoration: InputDecoration(
              hintText: '搜索商品',
              prefixIcon: const Icon(Icons.search, color: Colors.grey),
              border: InputBorder.none,
              contentPadding: const EdgeInsets.symmetric(vertical: 10),
            ),
          ),
        ),
        actions: [
          Stack(
            children: [
              IconButton(
                icon: const Icon(Icons.shopping_cart),
                onPressed: () {
                  setState(() => _selectedIndex = 2);
                },
              ),
              if (_cartCount > 0)
                Positioned(
                  right: 8,
                  top: 8,
                  child: Container(
                    padding: const EdgeInsets.all(2),
                    decoration: const BoxDecoration(
                      color: Colors.red,
                      shape: BoxShape.circle,
                    ),
                    constraints: const BoxConstraints(
                      minWidth: 16,
                      minHeight: 16,
                    ),
                    child: Text(
                      _cartCount.toString(),
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 10,
                      ),
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
            ],
          ),
        ],
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            UserAccountsDrawerHeader(
              accountName: const Text('张三'),
              accountEmail: const Text('zhangsan@example.com'),
              currentAccountPicture: const CircleAvatar(
                backgroundColor: Colors.white,
                child: Icon(Icons.person, size: 40, color: Colors.blue),
              ),
              decoration: BoxDecoration(
                color: Colors.blue[700],
              ),
            ),
            ListTile(
              leading: const Icon(Icons.favorite),
              title: const Text('我的收藏'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: const Icon(Icons.history),
              title: const Text('浏览历史'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: const Icon(Icons.local_shipping),
              title: const Text('我的订单'),
              onTap: () => Navigator.pop(context),
            ),
            const Divider(),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('设置'),
              onTap: () => Navigator.pop(context),
            ),
          ],
        ),
      ),
      body: IndexedStack(
        index: _selectedIndex,
        children: [
          // 首页 - 商品列表
          GridView.builder(
            padding: const EdgeInsets.all(12),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 0.75,
              crossAxisSpacing: 12,
              mainAxisSpacing: 12,
            ),
            itemCount: 20,
            itemBuilder: (context, index) {
              return Card(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Expanded(
                      child: Container(
                        color: Colors.grey[300],
                        child: const Center(child: Icon(Icons.image, size: 50)),
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(8),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('商品 ${index + 1}'),
                          Text(
                            ${(index + 1) * 99}.00',
                            style: const TextStyle(
                              color: Colors.red,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              );
            },
          ),
          // 分类
          Center(child: Text(_pageTitles[1], style: TextStyle(fontSize: 24))),
          // 购物车
          Center(child: Text(_pageTitles[2], style: TextStyle(fontSize: 24))),
          // 我的
          Center(child: Text(_pageTitles[3], style: TextStyle(fontSize: 24))),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        heroTag: 'ecommerce_fab',
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('快速发布商品')),
          );
        },
        child: const Icon(Icons.add),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      bottomNavigationBar: BottomAppBar(
        shape: const CircularNotchedRectangle(),
        notchMargin: 6,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _buildNavButton(Icons.home, '首页', 0),
            _buildNavButton(Icons.category, '分类', 1),
            const SizedBox(width: 48), // FAB 占位
            _buildNavButton(Icons.shopping_cart, '购物车', 2),
            _buildNavButton(Icons.person, '我的', 3),
          ],
        ),
      ),
    );
  }

  Widget _buildNavButton(IconData icon, String label, int index) {
    final isSelected = _selectedIndex == index;
    return InkWell(
      onTap: () => setState(() => _selectedIndex = index),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              icon,
              color: isSelected ? Colors.blue : Colors.grey,
            ),
            Text(
              label,
              style: TextStyle(
                fontSize: 12,
                color: isSelected ? Colors.blue : Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

📖 参考资源