Flutter全国手账实体店查询应用开发教程

项目概述

本教程将带你开发一个功能完整的Flutter全国手账实体店查询应用。这款应用专为手账爱好者设计,提供全国手账实体店的位置查询、商品浏览、店铺详情和用户评价等功能,让手账爱好者能够轻松找到心仪的手账店铺。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 全国店铺覆盖:收录全国各地的手账实体店信息
  • 精准定位查询:基于GPS定位查找附近的手账店
  • 丰富商品展示:展示各店铺的手账本、贴纸、胶带等商品
  • 详细店铺信息:营业时间、联系方式、地址导航
  • 用户评价系统:真实用户评价和评分
  • 收藏管理:收藏喜欢的店铺,方便再次访问
  • 分类筛选:按商品类型、价格区间、距离等筛选
  • 优惠信息:实时更新店铺优惠活动信息

技术栈

  • 框架:Flutter 3.x
  • 语言:Dart
  • UI组件:Material Design 3
  • 状态管理:StatefulWidget + Provider
  • 地图服务:Google Maps(模拟实现)
  • 定位服务:Geolocator(模拟实现)
  • 数据存储:SharedPreferences + SQLite
  • 网络请求:HTTP(模拟数据)

项目结构设计

核心数据模型

1. 手账店铺模型(JournalShop)
class JournalShop {
  final String id;              // 唯一标识
  final String name;            // 店铺名称
  final String description;     // 店铺描述
  final String address;         // 详细地址
  final double latitude;        // 纬度
  final double longitude;       // 经度
  final String phone;           // 联系电话
  final String businessHours;   // 营业时间
  final List<String> categories; // 商品分类
  final double rating;          // 评分
  final int reviewCount;        // 评价数量
  final List<String> images;    // 店铺图片
  final List<Product> products; // 商品列表
  final List<String> tags;      // 标签
  final bool isOpen;           // 是否营业
  final String ownerName;      // 店主姓名
  final DateTime establishedDate; // 开店时间
  bool isFavorite;            // 是否收藏
  final double distance;       // 距离(公里)
  final List<Promotion> promotions; // 优惠活动
}
2. 商品模型(Product)
class Product {
  final String id;
  final String name;
  final String description;
  final double price;
  final String category;
  final List<String> images;
  final String brand;
  final bool inStock;
  final int stockQuantity;
  final List<String> tags;
  final double rating;
  final int reviewCount;
}
3. 用户评价模型(Review)
class Review {
  final String id;
  final String userId;
  final String userName;
  final String shopId;
  final double rating;
  final String comment;
  final DateTime date;
  final List<String> images;
  final bool isVerified;
}
4. 优惠活动模型(Promotion)
class Promotion {
  final String id;
  final String title;
  final String description;
  final DateTime startDate;
  final DateTime endDate;
  final String discountType; // percentage, fixed, buy_one_get_one
  final double discountValue;
  final bool isActive;
}

页面架构

应用采用底部导航栏设计,包含四个主要页面:

  1. 首页:附近店铺和推荐内容
  2. 搜索页面:店铺搜索和筛选
  3. 收藏页面:收藏的店铺管理
  4. 我的页面:用户信息和设置

详细实现步骤

第一步:项目初始化

创建新的Flutter项目:

flutter create journal_shop_finder
cd journal_shop_finder

第二步:主应用结构

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter全国手账实体店查询',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
        useMaterial3: true,
      ),
      home: const JournalShopFinderHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

第三步:数据初始化

创建示例店铺数据:

void _initializeShops() {
  _shops = [
    JournalShop(
      id: '1',
      name: '梦想手账屋',
      description: '专业手账用品店,提供各种精美手账本、贴纸、胶带等用品。',
      address: '北京市朝阳区三里屯太古里南区S4-31',
      latitude: 39.9370,
      longitude: 116.4560,
      phone: '010-85951234',
      businessHours: '10:00-22:00',
      categories: ['手账本', '贴纸', '胶带', '笔类'],
      rating: 4.8,
      reviewCount: 156,
      images: ['assets/images/shop1_1.jpg', 'assets/images/shop1_2.jpg'],
      products: [
        Product(
          id: '1',
          name: 'Hobonichi手账本2024',
          description: '日本原装进口手账本,优质纸张,适合各种笔类书写。',
          price: 168.0,
          category: '手账本',
          images: ['assets/images/product1.jpg'],
          brand: 'Hobonichi',
          inStock: true,
          stockQuantity: 25,
          tags: ['进口', '优质', '热销'],
          rating: 4.9,
          reviewCount: 89,
        ),
      ],
      tags: ['精品', '进口商品', '专业'],
      isOpen: true,
      ownerName: '张小美',
      establishedDate: DateTime(2020, 3, 15),
      isFavorite: true,
      distance: 1.2,
      promotions: [
        Promotion(
          id: '1',
          title: '新品上市8折优惠',
          description: '所有新品手账本享受8折优惠,限时一周!',
          startDate: DateTime.now().subtract(const Duration(days: 2)),
          endDate: DateTime.now().add(const Duration(days: 5)),
          discountType: 'percentage',
          discountValue: 20.0,
          isActive: true,
        ),
      ],
    ),
    JournalShop(
      id: '2',
      name: '文艺手账坊',
      description: '文艺青年的手账天堂,原创设计手账本和独特贴纸。',
      address: '上海市徐汇区田子坊210号',
      latitude: 31.2165,
      longitude: 121.4700,
      phone: '021-64731234',
      businessHours: '09:30-21:30',
      categories: ['原创手账', '文艺贴纸', '复古胶带'],
      rating: 4.6,
      reviewCount: 203,
      images: ['assets/images/shop2_1.jpg'],
      products: [],
      tags: ['原创', '文艺', '设计感'],
      isOpen: true,
      ownerName: '李文艺',
      establishedDate: DateTime(2019, 8, 20),
      isFavorite: false,
      distance: 2.8,
      promotions: [],
    ),
    JournalShop(
      id: '3',
      name: '手账小铺',
      description: '温馨小店,提供各种可爱手账用品,价格亲民。',
      address: '广州市天河区正佳广场3楼A区',
      latitude: 23.1291,
      longitude: 113.3240,
      phone: '020-38751234',
      businessHours: '10:00-22:00',
      categories: ['可爱贴纸', '彩色胶带', '装饰用品'],
      rating: 4.4,
      reviewCount: 98,
      images: ['assets/images/shop3_1.jpg'],
      products: [],
      tags: ['可爱', '亲民', '温馨'],
      isOpen: false,
      ownerName: '王小萌',
      establishedDate: DateTime(2021, 5, 10),
      isFavorite: true,
      distance: 0.8,
      promotions: [],
    ),
    JournalShop(
      id: '4',
      name: '手账达人工作室',
      description: '专业手账教学和用品销售,提供手账制作课程。',
      address: '深圳市南山区海岸城购物中心2楼',
      latitude: 22.5431,
      longitude: 113.9340,
      phone: '0755-26781234',
      businessHours: '10:00-21:00',
      categories: ['教学课程', '专业工具', '高端手账'],
      rating: 4.9,
      reviewCount: 67,
      images: ['assets/images/shop4_1.jpg'],
      products: [],
      tags: ['专业', '教学', '高端'],
      isOpen: true,
      ownerName: '陈达人',
      establishedDate: DateTime(2018, 11, 3),
      isFavorite: false,
      distance: 5.2,
      promotions: [],
    ),
    JournalShop(
      id: '5',
      name: '梦幻手账世界',
      description: '梦幻主题手账店,各种童话和梦幻风格的手账用品。',
      address: '成都市锦江区春熙路步行街',
      latitude: 30.6598,
      longitude: 104.0633,
      phone: '028-86541234',
      businessHours: '09:00-23:00',
      categories: ['梦幻贴纸', '童话胶带', '公主手账'],
      rating: 4.7,
      reviewCount: 134,
      images: ['assets/images/shop5_1.jpg'],
      products: [],
      tags: ['梦幻', '童话', '公主风'],
      isOpen: true,
      ownerName: '林梦幻',
      establishedDate: DateTime(2020, 12, 25),
      isFavorite: true,
      distance: 3.5,
      promotions: [],
    ),
  ];
}

第四步:首页设计

位置信息和搜索栏
Widget _buildHomePage() {
  return SingleChildScrollView(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 位置和搜索栏
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.pink.shade300, Colors.pink.shade500],
            ),
          ),
          child: SafeArea(
            child: Column(
              children: [
                Row(
                  children: [
                    const Icon(Icons.location_on, color: Colors.white),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        _currentLocation,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    IconButton(
                      icon: const Icon(Icons.refresh, color: Colors.white),
                      onPressed: _refreshLocation,
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                Container(
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(25),
                  ),
                  child: TextField(
                    decoration: const InputDecoration(
                      hintText: '搜索手账店铺...',
                      prefixIcon: Icon(Icons.search),
                      border: InputBorder.none,
                      contentPadding: EdgeInsets.symmetric(
                        horizontal: 20,
                        vertical: 15,
                      ),
                    ),
                    onSubmitted: _performSearch,
                  ),
                ),
              ],
            ),
          ),
        ),
        
        // 快速筛选
        _buildQuickFilters(),
        
        // 附近店铺
        _buildSectionTitle('附近店铺'),
        _buildNearbyShops(),
        
        // 推荐店铺
        _buildSectionTitle('推荐店铺'),
        _buildRecommendedShops(),
        
        // 热门商品分类
        _buildSectionTitle('热门分类'),
        _buildPopularCategories(),
        
        const SizedBox(height: 20),
      ],
    ),
  );
}
快速筛选按钮
Widget _buildQuickFilters() {
  final filters = [
    {'name': '附近', 'icon': Icons.near_me, 'color': Colors.blue},
    {'name': '营业中', 'icon': Icons.access_time, 'color': Colors.green},
    {'name': '高评分', 'icon': Icons.star, 'color': Colors.amber},
    {'name': '有优惠', 'icon': Icons.local_offer, 'color': Colors.red},
  ];

  return Container(
    height: 80,
    padding: const EdgeInsets.symmetric(vertical: 16),
    child: ListView.builder(
      scrollDirection: Axis.horizontal,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      itemCount: filters.length,
      itemBuilder: (context, index) {
        final filter = filters[index];
        return Container(
          width: 80,
          margin: const EdgeInsets.only(right: 12),
          child: Column(
            children: [
              Container(
                width: 48,
                height: 48,
                decoration: BoxDecoration(
                  color: (filter['color'] as Color).withOpacity(0.1),
                  borderRadius: BorderRadius.circular(24),
                ),
                child: Icon(
                  filter['icon'] as IconData,
                  color: filter['color'] as Color,
                  size: 24,
                ),
              ),
              const SizedBox(height: 4),
              Text(
                filter['name'] as String,
                style: const TextStyle(fontSize: 12),
              ),
            ],
          ),
        );
      },
    ),
  );
}

第五步:店铺卡片设计

Widget _buildShopCard(JournalShop shop) {
  return Card(
    margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    elevation: 4,
    child: InkWell(
      onTap: () => _showShopDetails(shop),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 店铺头部信息
            Row(
              children: [
                // 店铺图片
                Container(
                  width: 80,
                  height: 80,
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [
                        Colors.pink.shade300,
                        Colors.pink.shade500,
                      ],
                    ),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: const Center(
                    child: Icon(
                      Icons.store,
                      size: 40,
                      color: Colors.white,
                    ),
                  ),
                ),
                
                const SizedBox(width: 16),
                
                // 店铺信息
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(
                            child: Text(
                              shop.name,
                              style: const TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                          GestureDetector(
                            onTap: () => _toggleFavorite(shop),
                            child: Icon(
                              shop.isFavorite ? Icons.favorite : Icons.favorite_border,
                              color: shop.isFavorite ? Colors.red : Colors.grey,
                            ),
                          ),
                        ],
                      ),
                      
                      const SizedBox(height: 4),
                      
                      Row(
                        children: [
                          Icon(
                            Icons.star,
                            size: 16,
                            color: Colors.amber.shade600,
                          ),
                          const SizedBox(width: 4),
                          Text(
                            '${shop.rating}',
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(width: 4),
                          Text('(${shop.reviewCount}条评价)'),
                        ],
                      ),
                      
                      const SizedBox(height: 4),
                      
                      Row(
                        children: [
                          Icon(
                            Icons.location_on,
                            size: 16,
                            color: Colors.grey.shade600,
                          ),
                          const SizedBox(width: 4),
                          Text(
                            '${shop.distance.toStringAsFixed(1)}km',
                            style: TextStyle(color: Colors.grey.shade600),
                          ),
                          const SizedBox(width: 16),
                          Container(
                            padding: const EdgeInsets.symmetric(
                              horizontal: 8,
                              vertical: 2,
                            ),
                            decoration: BoxDecoration(
                              color: shop.isOpen ? Colors.green : Colors.red,
                              borderRadius: BorderRadius.circular(10),
                            ),
                            child: Text(
                              shop.isOpen ? '营业中' : '已打烊',
                              style: const TextStyle(
                                color: Colors.white,
                                fontSize: 12,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 店铺描述
            Text(
              shop.description,
              style: TextStyle(
                color: Colors.grey.shade700,
                fontSize: 14,
              ),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
            
            const SizedBox(height: 12),
            
            // 商品分类标签
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: shop.categories.take(3).map((category) {
                return Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 8,
                    vertical: 4,
                  ),
                  decoration: BoxDecoration(
                    color: Colors.pink.shade50,
                    borderRadius: BorderRadius.circular(12),
                    border: Border.all(color: Colors.pink.shade200),
                  ),
                  child: Text(
                    category,
                    style: TextStyle(
                      color: Colors.pink.shade700,
                      fontSize: 12,
                    ),
                  ),
                );
              }).toList(),
            ),
            
            // 优惠信息
            if (shop.promotions.isNotEmpty) ...[
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.red.shade50,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.red.shade200),
                ),
                child: Row(
                  children: [
                    Icon(
                      Icons.local_offer,
                      size: 16,
                      color: Colors.red.shade600,
                    ),
                    const SizedBox(width: 4),
                    Expanded(
                      child: Text(
                        shop.promotions.first.title,
                        style: TextStyle(
                          color: Colors.red.shade700,
                          fontSize: 12,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ],
        ),
      ),
    ),
  );
}

第六步:店铺详情页面

class ShopDetailPage extends StatefulWidget {
  final JournalShop shop;

  const ShopDetailPage({super.key, required this.shop});

  
  State<ShopDetailPage> createState() => _ShopDetailPageState();
}

class _ShopDetailPageState extends State<ShopDetailPage>
    with TickerProviderStateMixin {
  late TabController _tabController;

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // 店铺头部信息
          SliverAppBar(
            expandedHeight: 300,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(widget.shop.name),
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      Colors.pink.shade300,
                      Colors.pink.shade500,
                    ],
                  ),
                ),
                child: Stack(
                  children: [
                    const Center(
                      child: Icon(
                        Icons.store,
                        size: 100,
                        color: Colors.white54,
                      ),
                    ),
                    Positioned(
                      bottom: 20,
                      left: 20,
                      right: 20,
                      child: _buildShopBasicInfo(),
                    ),
                  ],
                ),
              ),
            ),
            actions: [
              IconButton(
                icon: Icon(
                  widget.shop.isFavorite ? Icons.favorite : Icons.favorite_border,
                  color: widget.shop.isFavorite ? Colors.red : Colors.white,
                ),
                onPressed: _toggleFavorite,
              ),
              IconButton(
                icon: const Icon(Icons.share),
                onPressed: _shareShop,
              ),
            ],
          ),
          
          // Tab栏
          SliverPersistentHeader(
            pinned: true,
            delegate: _SliverTabBarDelegate(
              TabBar(
                controller: _tabController,
                tabs: const [
                  Tab(text: '详情'),
                  Tab(text: '商品'),
                  Tab(text: '评价'),
                  Tab(text: '优惠'),
                ],
              ),
            ),
          ),
          
          // Tab内容
          SliverFillRemaining(
            child: TabBarView(
              controller: _tabController,
              children: [
                _buildDetailTab(),
                _buildProductsTab(),
                _buildReviewsTab(),
                _buildPromotionsTab(),
              ],
            ),
          ),
        ],
      ),
      bottomNavigationBar: _buildBottomActions(),
    );
  }

  Widget _buildShopBasicInfo() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.9),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              Icon(
                Icons.star,
                color: Colors.amber.shade600,
                size: 20,
              ),
              const SizedBox(width: 4),
              Text(
                '${widget.shop.rating}',
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 16,
                ),
              ),
              const SizedBox(width: 8),
              Text('${widget.shop.reviewCount}条评价'),
              const Spacer(),
              Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 8,
                  vertical: 4,
                ),
                decoration: BoxDecoration(
                  color: widget.shop.isOpen ? Colors.green : Colors.red,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text(
                  widget.shop.isOpen ? '营业中' : '已打烊',
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 12,
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              const Icon(Icons.location_on, size: 16),
              const SizedBox(width: 4),
              Text('距离 ${widget.shop.distance.toStringAsFixed(1)}km'),
              const SizedBox(width: 16),
              const Icon(Icons.access_time, size: 16),
              const SizedBox(width: 4),
              Text(widget.shop.businessHours),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildDetailTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 店铺介绍
          const Text(
            '店铺介绍',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Text(
            widget.shop.description,
            style: const TextStyle(fontSize: 16, height: 1.5),
          ),
          
          const SizedBox(height: 24),
          
          // 联系信息
          const Text(
            '联系信息',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          
          _buildInfoRow(Icons.location_on, '地址', widget.shop.address),
          _buildInfoRow(Icons.phone, '电话', widget.shop.phone),
          _buildInfoRow(Icons.access_time, '营业时间', widget.shop.businessHours),
          _buildInfoRow(Icons.person, '店主', widget.shop.ownerName),
          
          const SizedBox(height: 24),
          
          // 商品分类
          const Text(
            '主营商品',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          
          Wrap(
            spacing: 12,
            runSpacing: 8,
            children: widget.shop.categories.map((category) {
              return Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 8,
                ),
                decoration: BoxDecoration(
                  color: Colors.pink.shade50,
                  borderRadius: BorderRadius.circular(20),
                  border: Border.all(color: Colors.pink.shade200),
                ),
                child: Text(
                  category,
                  style: TextStyle(
                    color: Colors.pink.shade700,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              );
            }).toList(),
          ),
          
          const SizedBox(height: 24),
          
          // 地图位置
          const Text(
            '店铺位置',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          
          Container(
            height: 200,
            decoration: BoxDecoration(
              color: Colors.grey.shade200,
              borderRadius: BorderRadius.circular(12),
            ),
            child: const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.map, size: 48, color: Colors.grey),
                  SizedBox(height: 8),
                  Text('地图加载中...', style: TextStyle(color: Colors.grey)),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoRow(IconData icon, String label, String value) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(icon, size: 20, color: Colors.pink),
          const SizedBox(width: 12),
          Text(
            '$label:',
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
          Expanded(
            child: Text(value),
          ),
        ],
      ),
    );
  }

  Widget _buildProductsTab() {
    return widget.shop.products.isEmpty
        ? const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.inventory_2, size: 64, color: Colors.grey),
                SizedBox(height: 16),
                Text('暂无商品信息', style: TextStyle(color: Colors.grey)),
              ],
            ),
          )
        : ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: widget.shop.products.length,
            itemBuilder: (context, index) {
              final product = widget.shop.products[index];
              return _buildProductCard(product);
            },
          );
  }

  Widget _buildProductCard(Product product) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                color: Colors.pink.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Center(
                child: Icon(Icons.book, size: 32, color: Colors.pink),
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    product.description,
                    style: TextStyle(color: Colors.grey.shade600),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Text(
                        ${product.price.toStringAsFixed(0)}',
                        style: const TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                          color: Colors.red,
                        ),
                      ),
                      const Spacer(),
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 8,
                          vertical: 4,
                        ),
                        decoration: BoxDecoration(
                          color: product.inStock ? Colors.green : Colors.grey,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          product.inStock ? '有货' : '缺货',
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 12,
                          ),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildReviewsTab() {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.rate_review, size: 64, color: Colors.grey),
          SizedBox(height: 16),
          Text('暂无评价信息', style: TextStyle(color: Colors.grey)),
        ],
      ),
    );
  }

  Widget _buildPromotionsTab() {
    return widget.shop.promotions.isEmpty
        ? const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.local_offer, size: 64, color: Colors.grey),
                SizedBox(height: 16),
                Text('暂无优惠活动', style: TextStyle(color: Colors.grey)),
              ],
            ),
          )
        : ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: widget.shop.promotions.length,
            itemBuilder: (context, index) {
              final promotion = widget.shop.promotions[index];
              return _buildPromotionCard(promotion);
            },
          );
  }

  Widget _buildPromotionCard(Promotion promotion) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(12),
          gradient: LinearGradient(
            colors: [Colors.red.shade50, Colors.red.shade100],
          ),
        ),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Icon(Icons.local_offer, color: Colors.red.shade600),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      promotion.title,
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: Colors.red.shade700,
                      ),
                    ),
                  ),
                  if (promotion.isActive)
                    Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                        vertical: 4,
                      ),
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: const Text(
                        '进行中',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 12,
                        ),
                      ),
                    ),
                ],
              ),
              const SizedBox(height: 8),
              Text(
                promotion.description,
                style: TextStyle(color: Colors.grey.shade700),
              ),
              const SizedBox(height: 8),
              Text(
                '活动时间:${_formatDate(promotion.startDate)} - ${_formatDate(promotion.endDate)}',
                style: TextStyle(
                  color: Colors.grey.shade600,
                  fontSize: 12,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildBottomActions() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: OutlinedButton.icon(
              onPressed: _callShop,
              icon: const Icon(Icons.phone),
              label: const Text('拨打电话'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _navigateToShop,
              icon: const Icon(Icons.navigation),
              label: const Text('导航前往'),
            ),
          ),
        ],
      ),
    );
  }

  String _formatDate(DateTime date) {
    return '${date.month}/${date.day}';
  }

  void _toggleFavorite() {
    setState(() {
      widget.shop.isFavorite = !widget.shop.isFavorite;
    });
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(widget.shop.isFavorite ? '已添加到收藏' : '已从收藏中移除'),
        duration: const Duration(seconds: 1),
      ),
    );
  }

  void _shareShop() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('分享功能开发中...')),
    );
  }

  void _callShop() {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('拨打电话:${widget.shop.phone}')),
    );
  }

  void _navigateToShop() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('正在启动导航...')),
    );
  }
}

// 自定义SliverTabBarDelegate
class _SliverTabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar tabBar;

  _SliverTabBarDelegate(this.tabBar);

  
  double get minExtent => tabBar.preferredSize.height;

  
  double get maxExtent => tabBar.preferredSize.height;

  
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.white,
      child: tabBar,
    );
  }

  
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return false;
  }
}

第七步:搜索和筛选功能

Widget _buildSearchPage() {
  return Column(
    children: [
      // 搜索栏
      Container(
        padding: const EdgeInsets.all(16),
        child: TextField(
          controller: _searchController,
          decoration: InputDecoration(
            hintText: '搜索店铺名称或商品...',
            prefixIcon: const Icon(Icons.search),
            suffixIcon: IconButton(
              icon: const Icon(Icons.tune),
              onPressed: _showFilterDialog,
            ),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(25),
            ),
          ),
          onChanged: _onSearchChanged,
        ),
      ),
      
      // 筛选标签
      if (_activeFilters.isNotEmpty) _buildActiveFilters(),
      
      // 搜索结果
      Expanded(
        child: _filteredShops.isEmpty
            ? _buildEmptySearchResult()
            : ListView.builder(
                itemCount: _filteredShops.length,
                itemBuilder: (context, index) {
                  return _buildShopCard(_filteredShops[index]);
                },
              ),
      ),
    ],
  );
}

void _showFilterDialog() {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    builder: (context) => DraggableScrollableSheet(
      initialChildSize: 0.7,
      maxChildSize: 0.9,
      minChildSize: 0.5,
      builder: (context, scrollController) {
        return Container(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              const Text(
                '筛选条件',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 16),
              Expanded(
                child: SingleChildScrollView(
                  controller: scrollController,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _buildFilterSection('营业状态', [
                        {'label': '营业中', 'value': 'open'},
                        {'label': '已打烊', 'value': 'closed'},
                      ]),
                      _buildFilterSection('距离', [
                        {'label': '1km内', 'value': '1km'},
                        {'label': '3km内', 'value': '3km'},
                        {'label': '5km内', 'value': '5km'},
                      ]),
                      _buildFilterSection('评分', [
                        {'label': '4.5分以上', 'value': '4.5+'},
                        {'label': '4.0分以上', 'value': '4.0+'},
                        {'label': '3.5分以上', 'value': '3.5+'},
                      ]),
                      _buildFilterSection('商品类型', [
                        {'label': '手账本', 'value': '手账本'},
                        {'label': '贴纸', 'value': '贴纸'},
                        {'label': '胶带', 'value': '胶带'},
                        {'label': '笔类', 'value': '笔类'},
                      ]),
                    ],
                  ),
                ),
              ),
              Row(
                children: [
                  Expanded(
                    child: OutlinedButton(
                      onPressed: _clearFilters,
                      child: const Text('清除筛选'),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {
                        Navigator.of(context).pop();
                        _applyFilters();
                      },
                      child: const Text('应用筛选'),
                    ),
                  ),
                ],
              ),
            ],
          ),
        );
      },
    ),
  );
}

第八步:收藏管理

Widget _buildFavoritePage() {
  final favoriteShops = _shops.where((shop) => shop.isFavorite).toList();

  return Column(
    children: [
      if (favoriteShops.isNotEmpty) ...[
        Container(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              const Icon(Icons.favorite, color: Colors.red),
              const SizedBox(width: 8),
              Text(
                '我的收藏 (${favoriteShops.length})',
                style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              const Spacer(),
              TextButton(
                onPressed: _clearAllFavorites,
                child: const Text('清空收藏'),
              ),
            ],
          ),
        ),
      ],
      
      Expanded(
        child: favoriteShops.isEmpty
            ? const Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(Icons.favorite_border, size: 80, color: Colors.grey),
                    SizedBox(height: 16),
                    Text(
                      '暂无收藏店铺',
                      style: TextStyle(fontSize: 18, color: Colors.grey),
                    ),
                    SizedBox(height: 8),
                    Text(
                      '收藏喜欢的店铺,方便下次查看',
                      style: TextStyle(color: Colors.grey),
                    ),
                  ],
                ),
              )
            : ListView.builder(
                itemCount: favoriteShops.length,
                itemBuilder: (context, index) {
                  return _buildShopCard(favoriteShops[index]);
                },
              ),
      ),
    ],
  );
}

核心功能详解

1. 位置服务模拟

void _getCurrentLocation() async {
  // 模拟获取当前位置
  setState(() {
    _currentLocation = '北京市朝阳区';
    _userLatitude = 39.9370;
    _userLongitude = 116.4560;
  });
  
  // 更新店铺距离
  _updateShopDistances();
}

void _updateShopDistances() {
  for (var shop in _shops) {
    // 简单的距离计算模拟
    final latDiff = (shop.latitude - _userLatitude).abs();
    final lngDiff = (shop.longitude - _userLongitude).abs();
    shop.distance = (latDiff + lngDiff) * 100; // 简化计算
  }
  
  // 按距离排序
  _shops.sort((a, b) => a.distance.compareTo(b.distance));
  setState(() {});
}

2. 搜索算法

void _performSearch(String query) {
  if (query.isEmpty) {
    setState(() {
      _filteredShops = List.from(_shops);
    });
    return;
  }

  setState(() {
    _filteredShops = _shops.where((shop) {
      final nameMatch = shop.name.toLowerCase().contains(query.toLowerCase());
      final descMatch = shop.description.toLowerCase().contains(query.toLowerCase());
      final categoryMatch = shop.categories.any((category) =>
          category.toLowerCase().contains(query.toLowerCase()));
      final tagMatch = shop.tags.any((tag) =>
          tag.toLowerCase().contains(query.toLowerCase()));
      
      return nameMatch || descMatch || categoryMatch || tagMatch;
    }).toList();
  });
}

3. 筛选逻辑

void _applyFilters() {
  List<JournalShop> filtered = List.from(_shops);

  // 营业状态筛选
  if (_selectedFilters.contains('open')) {
    filtered = filtered.where((shop) => shop.isOpen).toList();
  }
  if (_selectedFilters.contains('closed')) {
    filtered = filtered.where((shop) => !shop.isOpen).toList();
  }

  // 距离筛选
  if (_selectedFilters.contains('1km')) {
    filtered = filtered.where((shop) => shop.distance <= 1.0).toList();
  }
  if (_selectedFilters.contains('3km')) {
    filtered = filtered.where((shop) => shop.distance <= 3.0).toList();
  }
  if (_selectedFilters.contains('5km')) {
    filtered = filtered.where((shop) => shop.distance <= 5.0).toList();
  }

  // 评分筛选
  if (_selectedFilters.contains('4.5+')) {
    filtered = filtered.where((shop) => shop.rating >= 4.5).toList();
  }
  if (_selectedFilters.contains('4.0+')) {
    filtered = filtered.where((shop) => shop.rating >= 4.0).toList();
  }

  // 商品类型筛选
  final categoryFilters = _selectedFilters.where((filter) =>
      ['手账本', '贴纸', '胶带', '笔类'].contains(filter)).toList();
  if (categoryFilters.isNotEmpty) {
    filtered = filtered.where((shop) =>
        shop.categories.any((category) => categoryFilters.contains(category))).toList();
  }

  setState(() {
    _filteredShops = filtered;
  });
}

性能优化

1. 列表优化

使用ListView.builder实现虚拟滚动:

ListView.builder(
  itemCount: shops.length,
  itemExtent: 200, // 固定高度提高性能
  itemBuilder: (context, index) {
    return _buildShopCard(shops[index]);
  },
)

2. 图片缓存

使用渐变色作为占位符,减少内存使用:

Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.pink.shade300, Colors.pink.shade500],
    ),
  ),
)

3. 状态管理优化

合理使用setState,避免不必要的重建:

void _updateShopList() {
  if (mounted) {
    setState(() {
      // 只更新必要的状态
    });
  }
}

扩展功能

1. 真实地图集成

可以集成Google Maps或高德地图:

dependencies:
  google_maps_flutter: ^2.5.0

2. 位置服务

集成真实的位置服务:

dependencies:
  geolocator: ^10.1.0

3. 推送通知

使用flutter_local_notifications:

dependencies:
  flutter_local_notifications: ^16.1.0

测试策略

1. 单元测试

测试核心业务逻辑:

test('should filter shops by distance correctly', () {
  final shops = [
    JournalShop(distance: 0.5),
    JournalShop(distance: 2.0),
    JournalShop(distance: 4.0),
  ];
  
  final filtered = shops.where((shop) => shop.distance <= 1.0).toList();
  expect(filtered.length, equals(1));
});

2. Widget测试

测试UI组件:

testWidgets('should display shop name', (WidgetTester tester) async {
  await tester.pumpWidget(MyApp());
  expect(find.text('梦想手账屋'), findsOneWidget);
});

部署发布

1. Android打包

flutter build apk --release

2. iOS打包

flutter build ios --release

总结

本教程详细介绍了Flutter全国手账实体店查询应用的完整开发过程,涵盖了:

  • 完整的店铺信息展示:名称、地址、评分、商品等
  • 智能搜索和筛选:多维度筛选条件
  • 详细的店铺页面:商品、评价、优惠信息
  • 收藏管理功能:个人收藏夹
  • 位置服务集成:距离计算和排序
  • 优雅的UI设计:Material Design 3风格

这款应用不仅功能丰富,而且充分考虑了手账爱好者的实际需求。通过本教程的学习,你可以掌握Flutter地图应用开发的核心技能。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐