You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter+go_router实现带侧边栏与AppBar的局部导航求助

Complete Flutter Web Dashboard Implementation

Here's a fully runnable Flutter Web solution that meets all your requirements:

Core Components:

  • Fixed Sidebar: 20 menu items with active page highlighting
  • Top AppBar: Displays switch/close buttons for all opened pages
  • State-Preserving Pages: Uses AutomaticKeepAliveClientMixin to retain state (e.g., counter values) when switching between pages
  • Data Table with Detail Retention: Tracks which detail view was open, so switching back shows the detail instead of the list

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Web Dashboard',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MainLayout(),
    );
  }
}

class PageInfo {
  final String key;
  final String title;
  final Widget widget;

  PageInfo({required this.key, required this.title, required this.widget});
}

class MainLayout extends StatefulWidget {
  const MainLayout({super.key});

  @override
  State<MainLayout> createState() => _MainLayoutState();
}

class _MainLayoutState extends State<MainLayout> {
  final List<PageInfo> _openedPages = [];
  String? _currentPageKey;
  final Map<String, int?> _dataTableSelectedItems = {};

  @override
  void initState() {
    super.initState();
    _openPage('page_1', 'Page 1', const Page1());
  }

  void _openPage(String key, String title, Widget widget) {
    final existingPage = _openedPages.firstWhere(
      (p) => p.key == key,
      orElse: () => PageInfo(key: '', title: '', widget: const SizedBox()),
    );

    if (existingPage.key.isNotEmpty) {
      setState(() => _currentPageKey = key);
      return;
    }

    setState(() {
      _openedPages.add(PageInfo(key: key, title: title, widget: widget));
      _currentPageKey = key;
    });
  }

  void _switchPage(String key) => setState(() => _currentPageKey = key);

  void _closePage(String key) {
    if (_openedPages.length == 1) return;

    final currentIndex = _openedPages.indexWhere((p) => p.key == key);
    setState(() {
      _openedPages.removeWhere((p) => p.key == key);
      _currentPageKey = _openedPages[currentIndex > 0 ? currentIndex - 1 : 0].key;
    });
  }

  void _updateDataTableSelectedItem(String pageKey, int? itemId) {
    setState(() => _dataTableSelectedItems[pageKey] = itemId);
  }

  @override
  Widget build(BuildContext context) {
    final currentPage = _openedPages.firstWhere(
      (p) => p.key == _currentPageKey,
      orElse: () => _openedPages.first,
    );

    return Scaffold(
      body: Row(
        children: [
          // Fixed Sidebar
          Container(
            width: 250,
            color: Colors.grey[800],
            child: ListView.builder(
              itemCount: 20,
              itemBuilder: (context, index) {
                final pageNum = index + 1;
                final pageKey = 'page_$pageNum';
                final title = 'Menu Item $pageNum';
                Widget pageWidget;

                // Use DataTablePage for 5th menu item as example
                if (pageNum == 5) {
                  pageWidget = DataTablePage(
                    pageKey: pageKey,
                    initialSelectedItemId: _dataTableSelectedItems[pageKey],
                    onSelectedItemChanged: (id) => _updateDataTableSelectedItem(pageKey, id),
                  );
                } else {
                  pageWidget = GenericPage(title: title);
                }

                return ListTile(
                  leading: const Icon(Icons.list, color: Colors.white),
                  title: Text(title, style: const TextStyle(color: Colors.white)),
                  selected: _currentPageKey == pageKey,
                  selectedTileColor: Colors.grey[700],
                  onTap: () => _openPage(pageKey, title, pageWidget),
                );
              },
            ),
          ),

          // Main Content Area
          Expanded(
            child: Column(
              children: [
                // Top AppBar with Page Controls
                AppBar(
                  title: Text(currentPage.title),
                  backgroundColor: Colors.blue,
                  actions: _openedPages.map((page) {
                    return Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 4),
                      child: ElevatedButton(
                        style: ElevatedButton.styleFrom(
                          backgroundColor: _currentPageKey == page.key ? Colors.grey[700] : Colors.blue[800],
                        ),
                        onPressed: () => _switchPage(page.key),
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Text(page.title),
                            if (_openedPages.length > 1)
                              IconButton(
                                icon: const Icon(Icons.close, size: 16, color: Colors.white),
                                onPressed: () => _closePage(page.key),
                              ),
                          ],
                        ),
                      ),
                    );
                  }).toList(),
                ),

                // Page Content
                Expanded(
                  child: PageStorage(
                    bucket: PageStorageBucket(),
                    child: currentPage.widget,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// Generic State-Preserving Page
class GenericPage extends StatefulWidget {
  final String title;

  const GenericPage({super.key, required this.title});

  @override
  State<GenericPage> createState() => _GenericPageState();
}

class _GenericPageState extends State<GenericPage> with AutomaticKeepAliveClientMixin {
  int _counter = 0;

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(widget.title, style: const TextStyle(fontSize: 24)),
          const SizedBox(height: 20),
          Text('Counter: $_counter', style: const TextStyle(fontSize: 20)),
          ElevatedButton(
            onPressed: () => setState(() => _counter++),
            child: const Text('Increment Counter'),
          ),
        ],
      ),
    );
  }
}

// Data Table Page with Detail View Retention
class DataTablePage extends StatefulWidget {
  final String pageKey;
  final int? initialSelectedItemId;
  final Function(int?) onSelectedItemChanged;

  const DataTablePage({
    super.key,
    required this.pageKey,
    required this.initialSelectedItemId,
    required this.onSelectedItemChanged,
  });

  @override
  State<DataTablePage> createState() => _DataTablePageState();
}

class _DataTablePageState extends State<DataTablePage> with AutomaticKeepAliveClientMixin {
  int? _selectedItemId;
  final List<Map<String, dynamic>> _data = List.generate(
    50,
    (index) => {'id': index + 1, 'name': 'Item ${index + 1}', 'value': (index + 1) * 10},
  );

  @override
  bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    _selectedItemId = widget.initialSelectedItemId;
  }

  void _onRowSelected(int itemId) {
    setState(() => _selectedItemId = itemId);
    widget.onSelectedItemChanged(itemId);
  }

  void _goBackToList() {
    setState(() => _selectedItemId = null);
    widget.onSelectedItemChanged(null);
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    if (_selectedItemId != null) {
      final item = _data.firstWhere((d) => d['id'] == _selectedItemId);
      return Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            ElevatedButton(
              onPressed: _goBackToList,
              child: const Text('Back to List'),
            ),
            const SizedBox(height: 20),
            Text('Detail for ${item['name']}', style: const TextStyle(fontSize: 24)),
            const SizedBox(height: 10),
            Text('ID: ${item['id']}'),
            Text('Value: ${item['value']}'),
            const SizedBox(height: 20),
            const Text('This detail state persists when switching pages!', style: TextStyle(color: Colors.blue)),
          ],
        ),
      );
    } else {
      return SingleChildScrollView(
        child: PaginatedDataTable(
          header: const Text('Data Table Page'),
          columns: const [
            DataColumn(label: Text('ID')),
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Value')),
          ],
          source: _DataTableSource(data: _data, onRowSelected: _onRowSelected),
          rowsPerPage: 10,
        ),
      );
    }
  }
}

class _DataTableSource extends DataTableSource {
  final List<Map<String, dynamic>> data;
  final Function(int) onRowSelected;

  _DataTableSource({required this.data, required this.onRowSelected});

  @override
  DataRow? getRow(int index) {
    if (index >= data.length) return null;
    final item = data[index];
    return DataRow(
      onSelectChanged: (_) => onRowSelected(item['id']),
      cells: [
        DataCell(Text(item['id'].toString())),
        DataCell(Text(item['name'])),
        DataCell(Text(item['value'].toString())),
      ],
    );
  }

  @override
  bool get isRowCountApproximate => false;

  @override
  int get rowCount => data.length;

  @override
  int get selectedRowCount => 0;
}

// Dummy Page1 (same as GenericPage, just for initial load)
class Page1 extends GenericPage {
  const Page1({super.key}) : super(title: 'Page 1');
}

How to Use:

  1. Create a new Flutter Web project
  2. Replace the contents of lib/main.dart with this code
  3. Run with flutter run -d chrome

Key Notes:

  • State Retention: All pages use AutomaticKeepAliveClientMixin to preserve their state (try incrementing the counter on a page, switch to another, then come back—counter stays the same)
  • Data Table Detail: Click any row in the 5th menu item's table to open the detail view. Switch to another page and come back—it will still show the detail view
  • Page Management: The MainLayout tracks all opened pages, allowing switching and closing (can't close the last open page)
  • Sidebar: Fixed width, highlights the active page

内容的提问来源于stack exchange,提问作者Biagio63

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 13:45:56