import 'package:flutter/material.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/data/demo_data.dart'; import 'package:patbond_flutter/models/models.dart'; import 'package:patbond_flutter/state/app_state.dart'; import 'package:patbond_flutter/widgets/common.dart'; enum HomeSegment { feed, services } class HomePage extends StatefulWidget { const HomePage({ required this.appState, required this.onOpenPost, required this.onOpenServices, required this.onOpenCreate, super.key, }); final AppState appState; final ValueChanged onOpenPost; final ValueChanged onOpenServices; final VoidCallback onOpenCreate; @override State createState() => _HomePageState(); } class _HomePageState extends State { HomeSegment segment = HomeSegment.feed; String query = ''; String sort = '综合'; List get filteredPosts { final keyword = query.trim().toLowerCase(); if (keyword.isEmpty) return widget.appState.posts; return widget.appState.posts.where((post) { return post.content.toLowerCase().contains(keyword) || post.authorName.toLowerCase().contains(keyword) || post.tags.any((tag) => tag.toLowerCase().contains(keyword)); }).toList(); } List get filteredProviders { final keyword = query.trim().toLowerCase(); final result = serviceProviders.where((provider) { if (keyword.isEmpty) return true; return provider.name.toLowerCase().contains(keyword) || provider.description.toLowerCase().contains(keyword) || provider.tags.any((tag) => tag.toLowerCase().contains(keyword)); }).toList(); if (sort == '距离') { result.sort( (a, b) => double.parse( a.distance.replaceAll('km', ''), ).compareTo(double.parse(b.distance.replaceAll('km', ''))), ); } else if (sort == '评分') { result.sort((a, b) => b.rating.compareTo(a.rating)); } return result; } String get greeting { final hour = DateTime.now().hour; if (hour < 6) return '夜深了'; if (hour < 11) return '早上好'; if (hour < 14) return '中午好'; if (hour < 18) return '下午好'; return '晚上好'; } Future selectArea() async { final selected = await showModalBottomSheet( context: context, useSafeArea: true, showDragHandle: true, builder: (context) { return _AreaPickerSheet(current: widget.appState.locationWeather); }, ); if (selected != null) { await widget.appState.updateLocationWeather(selected); } } Future showWeatherDetails() { final weather = widget.appState.locationWeather; return showModalBottomSheet( context: context, useSafeArea: true, showDragHandle: true, builder: (context) => _WeatherDetailsSheet(weather: weather), ); } @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () async => Future.delayed(const Duration(milliseconds: 500)), child: ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), children: [ _WeatherStatusBar( weather: widget.appState.locationWeather, onAreaTap: selectArea, onWeatherTap: showWeatherDetails, ), const SizedBox(height: 12), _PetGreetingCard( greeting: greeting, petName: widget.appState.pet.name, petAvatar: widget.appState.pet.avatarUrl, advice: widget.appState.locationWeather.petAdvice, ), const SizedBox(height: 14), TextField( onChanged: (value) => setState(() => query = value), decoration: const InputDecoration( hintText: '搜索动态、医院、美容或遛狗服务…', prefixIcon: Icon(Icons.search), ), ), const SizedBox(height: 14), SegmentedButton( segments: const [ ButtonSegment( value: HomeSegment.feed, icon: Icon(Icons.forum_outlined), label: Text('社区动态'), ), ButtonSegment( value: HomeSegment.services, icon: Icon(Icons.storefront_outlined), label: Text('本地服务'), ), ], selected: {segment}, showSelectedIcon: false, onSelectionChanged: (value) { setState(() => segment = value.first); }, ), const SizedBox(height: 18), if (segment == HomeSegment.feed) ...[ _StoryRow(onCreate: widget.onOpenCreate), const SizedBox(height: 18), _PromoCard(onTap: () => widget.onOpenServices(true)), const SizedBox(height: 18), if (filteredPosts.isEmpty) const EmptyState(message: '没有找到相关动态') else ...filteredPosts.map( (post) => Padding( padding: const EdgeInsets.only(bottom: 16), child: _PostCard( post: post, onTap: () => widget.onOpenPost(post), onLike: () => widget.appState.updatePost( post.copyWith( hasLiked: !post.hasLiked, likes: post.hasLiked ? post.likes - 1 : post.likes + 1, ), ), ), ), ), ] else ...[ _CategoryGrid(onTap: (_) => widget.onOpenServices(false)), const SizedBox(height: 18), Row( children: [ Text('附近推荐', style: Theme.of(context).textTheme.titleLarge), const Spacer(), for (final item in ['综合', '距离', '评分']) Padding( padding: const EdgeInsets.only(left: 6), child: ChoiceChip( label: Text(item), selected: sort == item, onSelected: (_) => setState(() => sort = item), ), ), ], ), const SizedBox(height: 12), if (filteredProviders.isEmpty) const EmptyState(message: '没有找到相关服务') else ...filteredProviders.map( (provider) => Padding( padding: const EdgeInsets.only(bottom: 12), child: _ProviderCompactCard( provider: provider, onTap: () => widget.onOpenServices( provider.kind == ProviderKind.personal, ), ), ), ), ], ], ), ); } } class _WeatherStatusBar extends StatelessWidget { const _WeatherStatusBar({ required this.weather, required this.onAreaTap, required this.onWeatherTap, }); final LocationWeather weather; final VoidCallback onAreaTap; final VoidCallback onWeatherTap; @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: InkWell( key: const ValueKey('area-selector'), onTap: onAreaTap, borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 3), child: Row( children: [ const Icon( Icons.location_on_outlined, size: 21, color: AppColors.ink, ), const SizedBox(width: 5), Flexible( child: Text( weather.displayArea, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w800, ), ), ), const SizedBox(width: 3), const Icon( Icons.keyboard_arrow_down, size: 18, color: AppColors.muted, ), ], ), ), ), ), InkWell( key: const ValueKey('weather-details'), onTap: onWeatherTap, borderRadius: BorderRadius.circular(99), child: Container( padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 7), decoration: BoxDecoration( color: _weatherColor(weather.condition).withAlpha(22), borderRadius: BorderRadius.circular(99), ), child: Row( children: [ Icon( _weatherIcon(weather.condition), size: 19, color: _weatherColor(weather.condition), ), const SizedBox(width: 5), Text( '${weather.temperature}°C ${weather.conditionText}', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w800, ), ), ], ), ), ), ], ); } } class _PetGreetingCard extends StatelessWidget { const _PetGreetingCard({ required this.greeting, required this.petName, required this.petAvatar, required this.advice, }); final String greeting; final String petName; final String petAvatar; final String advice; @override Widget build(BuildContext context) { return Container( height: 138, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( borderRadius: BorderRadius.circular(26), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.surfaceTint, AppColors.canvas], ), ), child: Stack( children: [ Positioned( left: 160, top: 15, child: Icon( Icons.pets, size: 28, color: AppColors.primary.withAlpha(34), ), ), Positioned( right: -5, bottom: -10, child: RemoteImage( url: petAvatar, width: 145, height: 145, borderRadius: BorderRadius.circular(72), ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 19, 130, 17), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$greeting,$petName 👋', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.ink, fontSize: 20, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 7), Text( advice, maxLines: 3, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.primaryDark, fontSize: 12, height: 1.45, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), ); } } class _AreaPickerSheet extends StatelessWidget { const _AreaPickerSheet({required this.current}); final LocationWeather current; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('选择地区', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 4), Text( '当前为演示天气,选择后会保存在本机。', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 14), Flexible( child: ListView.separated( shrinkWrap: true, itemCount: locationWeatherOptions.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final option = locationWeatherOptions[index]; final selected = option.city == current.city && option.district == current.district; return ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundColor: _weatherColor( option.condition, ).withAlpha(22), child: Icon( _weatherIcon(option.condition), color: _weatherColor(option.condition), ), ), title: Text( option.displayArea, style: const TextStyle(fontWeight: FontWeight.w800), ), subtitle: Text( '${option.conditionText} · ${option.lowTemperature}° / ${option.highTemperature}°', ), trailing: selected ? const Icon(Icons.check_circle, color: AppColors.primary) : Text('${option.temperature}°C'), onTap: () => Navigator.pop(context, option), ); }, ), ), ], ), ); } } class _WeatherDetailsSheet extends StatelessWidget { const _WeatherDetailsSheet({required this.weather}); final LocationWeather weather; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 28), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( weather.displayArea, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 18), Row( children: [ Icon( _weatherIcon(weather.condition), color: _weatherColor(weather.condition), size: 56, ), const SizedBox(width: 16), Text( '${weather.temperature}°', style: const TextStyle( fontSize: 44, fontWeight: FontWeight.w300, ), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( weather.conditionText, style: const TextStyle(fontWeight: FontWeight.w800), ), Text( '${weather.lowTemperature}° / ${weather.highTemperature}°', style: Theme.of(context).textTheme.bodySmall, ), ], ), const Spacer(), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ const Text('湿度'), Text( '${weather.humidity}%', style: const TextStyle(fontWeight: FontWeight.w800), ), ], ), ], ), const SizedBox(height: 20), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surfaceTint, borderRadius: BorderRadius.circular(18), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.pets, color: AppColors.primary), const SizedBox(width: 10), Expanded(child: Text(weather.petAdvice)), ], ), ), ], ), ); } } IconData _weatherIcon(WeatherCondition condition) { return switch (condition) { WeatherCondition.sunny => Icons.wb_sunny_rounded, WeatherCondition.cloudy => Icons.cloud_queue_rounded, WeatherCondition.rainy => Icons.water_drop_rounded, WeatherCondition.overcast => Icons.cloud_rounded, WeatherCondition.snow => Icons.ac_unit_rounded, }; } Color _weatherColor(WeatherCondition condition) { return switch (condition) { WeatherCondition.sunny => AppColors.warning, WeatherCondition.cloudy => AppColors.muted, WeatherCondition.rainy => const Color(0xFF0284C7), WeatherCondition.overcast => AppColors.ink, WeatherCondition.snow => const Color(0xFF0891B2), }; } class _StoryRow extends StatelessWidget { const _StoryRow({required this.onCreate}); final VoidCallback onCreate; @override Widget build(BuildContext context) { final stories = [ ('发布', Icons.add, petAvatar), ('柴犬圈', Icons.pets, generatedPetImage), ( '猫咪圈', Icons.pets, 'https://images.unsplash.com/photo-1574158622682-e40e69881006?auto=format&fit=crop&w=300&q=80', ), ( '救助站', Icons.volunteer_activism, 'https://images.unsplash.com/photo-1548199973-03cce0bbc87b?auto=format&fit=crop&w=300&q=80', ), ]; return SizedBox( height: 86, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: stories.length, separatorBuilder: (_, _) => const SizedBox(width: 16), itemBuilder: (context, index) { final item = stories[index]; return InkWell( onTap: index == 0 ? onCreate : null, borderRadius: BorderRadius.circular(36), child: SizedBox( width: 62, child: Column( children: [ Container( width: 58, height: 58, padding: const EdgeInsets.all(2), decoration: const BoxDecoration( shape: BoxShape.circle, gradient: AppColors.brandGradient, ), child: index == 0 ? const CircleAvatar( backgroundColor: Colors.white, child: Icon(Icons.add, color: AppColors.primary), ) : RemoteImage( url: item.$3, borderRadius: BorderRadius.circular(28), ), ), const SizedBox(height: 5), Text( item.$1, maxLines: 1, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ); }, ), ); } } class _PromoCard extends StatelessWidget { const _PromoCard({required this.onTap}); final VoidCallback onTap; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( // 深端在左承载白字(4.5:1 达标);brandGradient 不承载正文文字(FIX-1)。 gradient: const LinearGradient( colors: [AppColors.primaryStrong, AppColors.primary], ), borderRadius: BorderRadius.circular(26), ), child: Row( children: [ const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '新用户首单立减 ¥20', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w800, ), ), SizedBox(height: 5), Text( '遛狗 / 洗澡 / 寄养均可使用', style: TextStyle(color: Colors.white, fontSize: 12), ), ], ), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.white, foregroundColor: AppColors.primaryStrong, ), onPressed: onTap, child: const Text('去使用'), ), ], ), ); } } class _PostCard extends StatelessWidget { const _PostCard({ required this.post, required this.onTap, required this.onLike, }); final PostModel post; final VoidCallback onTap; final VoidCallback onLike; @override Widget build(BuildContext context) { return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(14), child: Row( children: [ RemoteImage( url: post.authorAvatar, width: 38, height: 38, borderRadius: BorderRadius.circular(19), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( post.authorName, style: const TextStyle(fontWeight: FontWeight.w700), ), Text( '${post.time} · ${post.breedTag}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), const Icon(Icons.more_horiz, color: AppColors.muted), ], ), ), AspectRatio( aspectRatio: 4 / 3, child: RemoteImage(url: post.mainImage), ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( post.content, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), Row( children: [ ActionChip( avatar: Icon( post.hasLiked ? Icons.favorite : Icons.favorite_border, size: 17, color: post.hasLiked ? Colors.red : AppColors.primary, ), label: Text('${post.likes}'), onPressed: onLike, ), const SizedBox(width: 8), Chip( avatar: const Icon(Icons.chat_bubble_outline, size: 16), label: Text('${post.comments.length}'), ), const Spacer(), const Icon(Icons.share_outlined, color: AppColors.muted), ], ), ], ), ), ], ), ), ); } } class _CategoryGrid extends StatelessWidget { const _CategoryGrid({required this.onTap}); final ValueChanged onTap; static const items = [ ('投喂', Icons.restaurant), ('遛狗', Icons.directions_walk), ('洗澡', Icons.bathtub_outlined), ('美容', Icons.content_cut), ('寄养', Icons.home_outlined), ('运输', Icons.local_shipping_outlined), ('领养', Icons.favorite_border), ('医院', Icons.local_hospital_outlined), ]; @override Widget build(BuildContext context) { return SectionCard( child: GridView.count( crossAxisCount: 4, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 18, crossAxisSpacing: 8, childAspectRatio: .9, children: items.map((item) { return InkWell( onTap: () => onTap(item.$1), borderRadius: BorderRadius.circular(18), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircleAvatar( backgroundColor: AppColors.surfaceTint, child: Icon(item.$2, color: AppColors.primary), ), const SizedBox(height: 7), Text(item.$1, style: Theme.of(context).textTheme.bodySmall), ], ), ); }).toList(), ), ); } } class _ProviderCompactCard extends StatelessWidget { const _ProviderCompactCard({required this.provider, required this.onTap}); final ServiceProviderModel provider; final VoidCallback onTap; @override Widget build(BuildContext context) { return SectionCard( padding: const EdgeInsets.all(12), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Row( children: [ RemoteImage( url: provider.image, width: 82, height: 82, borderRadius: BorderRadius.circular(17), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( provider.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w800), ), const SizedBox(height: 4), Text( '${provider.distance} · ⭐ ${provider.rating}', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 7), Text( '¥${provider.price} 起', style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.w800, ), ), ], ), ), const Icon(Icons.chevron_right, color: AppColors.muted), ], ), ), ); } }