新增静态演示界面
This commit is contained in:
@@ -1,13 +1,860 @@
|
||||
// import 'package:flutter/cupertino.dart';
|
||||
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';
|
||||
|
||||
class HomePage extends StatelessWidget{
|
||||
const HomePage({super.key});
|
||||
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<PostModel> onOpenPost;
|
||||
final ValueChanged<bool> onOpenServices;
|
||||
final VoidCallback onOpenCreate;
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
HomeSegment segment = HomeSegment.feed;
|
||||
String query = '';
|
||||
String sort = '综合';
|
||||
|
||||
List<PostModel> 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<ServiceProviderModel> 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<void> selectArea() async {
|
||||
final selected = await showModalBottomSheet<LocationWeather>(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
showDragHandle: true,
|
||||
builder: (context) {
|
||||
return _AreaPickerSheet(current: widget.appState.locationWeather);
|
||||
},
|
||||
);
|
||||
if (selected != null) {
|
||||
await widget.appState.updateLocationWeather(selected);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showWeatherDetails() {
|
||||
final weather = widget.appState.locationWeather;
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
showDragHandle: true,
|
||||
builder: (context) => _WeatherDetailsSheet(weather: weather),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text("Patbond"),
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
Future<void>.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<HomeSegment>(
|
||||
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: [Color(0xFFECFDF5), Color(0xFFFFF7ED)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
const Positioned(
|
||||
left: 160,
|
||||
top: 15,
|
||||
child: Icon(Icons.pets, size: 28, color: Color(0x224F46E5)),
|
||||
),
|
||||
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: Color(0xFF475569),
|
||||
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: const Color(0xFFEEF2FF),
|
||||
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 => const Color(0xFFF59E0B),
|
||||
WeatherCondition.cloudy => const Color(0xFF64748B),
|
||||
WeatherCondition.rainy => const Color(0xFF0284C7),
|
||||
WeatherCondition.overcast => const Color(0xFF475569),
|
||||
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: LinearGradient(
|
||||
colors: [AppColors.primary, Color(0xFF8B5CF6)],
|
||||
),
|
||||
),
|
||||
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(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primary, Color(0xFF7C3AED)],
|
||||
),
|
||||
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: Color(0xFFE0E7FF), fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.primary,
|
||||
),
|
||||
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<String> 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: const Color(0xFFEEF2FF),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user