diff --git a/lib/screens/activity_list_screen.dart b/lib/screens/activity_list_screen.dart index 13c114a..0712784 100644 --- a/lib/screens/activity_list_screen.dart +++ b/lib/screens/activity_list_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'publish_screen.dart'; import 'activity_detail_screen.dart'; +import 'friends_screen.dart'; import 'dart:math'; class ActivityListScreen extends StatefulWidget { @@ -73,6 +74,13 @@ class _ActivityListScreenState extends State { ); return; } + if (index == 1) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const FriendsScreen()), + ); + return; + } setState(() { _selectedIndex = index; }); diff --git a/lib/screens/friends_screen.dart b/lib/screens/friends_screen.dart new file mode 100644 index 0000000..bff2f34 --- /dev/null +++ b/lib/screens/friends_screen.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; + +class FriendsScreen extends StatefulWidget { + const FriendsScreen({super.key}); + + @override + State createState() => _FriendsScreenState(); +} + +class _FriendsScreenState extends State { + final List> _followingUsers = [ + {'name': '林阿姨', 'avatar': 3, 'isFollowing': true}, + {'name': '黄叔叔', 'avatar': 7, 'isFollowing': true}, + {'name': '周老师', 'avatar': 12, 'isFollowing': true}, + {'name': '何姐', 'avatar': 18, 'isFollowing': true}, + {'name': '苏大哥', 'avatar': 24, 'isFollowing': false}, + {'name': '陶阿姨', 'avatar': 31, 'isFollowing': true}, + ]; + + final List> _fansUsers = [ + {'name': '白阿姨', 'avatar': 5, 'isFollowing': false}, + {'name': '邓叔', 'avatar': 14, 'isFollowing': true}, + {'name': '贺老师', 'avatar': 19, 'isFollowing': false}, + {'name': '任姐', 'avatar': 26, 'isFollowing': true}, + {'name': '罗阿姨', 'avatar': 38, 'isFollowing': false}, + {'name': '章叔叔', 'avatar': 46, 'isFollowing': true}, + ]; + + void _toggleFollow(List> users, int index) { + setState(() { + users[index]['isFollowing'] = !(users[index]['isFollowing'] as bool); + }); + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + backgroundColor: const Color(0xFFFFF8F0), + appBar: AppBar( + title: const Text('好友'), + bottom: const TabBar( + labelColor: Color(0xFFFF8A3D), + unselectedLabelColor: Color(0xFF999999), + indicatorColor: Color(0xFFFF8A3D), + tabs: [ + Tab(text: '关注'), + Tab(text: '粉丝'), + ], + ), + ), + body: TabBarView( + children: [ + _buildUserList(_followingUsers), + _buildUserList(_fansUsers), + ], + ), + ), + ); + } + + Widget _buildUserList(List> users) { + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: users.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final user = users[index]; + final isFollowing = user['isFollowing'] as bool; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + ), + child: Row( + children: [ + CircleAvatar( + radius: 22, + backgroundColor: const Color(0xFFFFE7CF), + backgroundImage: NetworkImage('https://i.pravatar.cc/50?img=${user['avatar']}'), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + user['name'] as String, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + ), + ), + SizedBox( + height: 34, + child: TextButton( + onPressed: () => _toggleFollow(users, index), + style: TextButton.styleFrom( + backgroundColor: isFollowing ? Colors.white : const Color(0xFFFF8A3D), + foregroundColor: isFollowing ? const Color(0xFFFF8A3D) : Colors.white, + side: const BorderSide(color: Color(0xFFFF8A3D)), + padding: const EdgeInsets.symmetric(horizontal: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + ), + child: Text( + isFollowing ? '已关注' : '关注', + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/screens/messages_screen.dart b/lib/screens/messages_screen.dart index 8aaa3db..769963c 100644 --- a/lib/screens/messages_screen.dart +++ b/lib/screens/messages_screen.dart @@ -3,32 +3,111 @@ import 'package:flutter/material.dart'; class MessagesScreen extends StatelessWidget { const MessagesScreen({super.key}); + static final List> _mockMessages = [ + {'name': '王阿姨', 'preview': '今天的太极活动您要来吗?', 'time': '09:40', 'avatar': 8, 'unread': 2}, + {'name': '李老师', 'preview': '书法课作业我已经发群里啦。', 'time': '昨天', 'avatar': 15, 'unread': 0}, + {'name': '陈叔', 'preview': '周末一起去人民公园拍照吧。', 'time': '昨天', 'avatar': 21, 'unread': 1}, + {'name': '健康服务助手', 'preview': '您的问诊报告已更新,请及时查看。', 'time': '周日', 'avatar': 29, 'unread': 0}, + {'name': '张姐', 'preview': '广场舞队今晚7点老地方集合。', 'time': '周六', 'avatar': 35, 'unread': 5}, + {'name': '社区活动中心', 'preview': '您报名的茶话会还有2个名额。', 'time': '02/12', 'avatar': 42, 'unread': 0}, + {'name': '赵大哥', 'preview': '下棋那局改天再战,哈哈。', 'time': '02/09', 'avatar': 47, 'unread': 0}, + ]; + @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFFFFF8F0), appBar: AppBar(title: const Text('消息')), - body: ListView( + body: ListView.separated( padding: const EdgeInsets.all(16), - children: [ - _notificationTile(Icons.event, '活动提醒', '您报名的「太极晨练」明天7:00开始', '10分钟前', const Color(0xFF1976D2)), - _notificationTile(Icons.check_circle, '报名成功', '您已成功报名「周末茶话会」', '1小时前', const Color(0xFF4CAF50)), - _notificationTile(Icons.payment, '支付通知', '挂号费¥50已支付成功', '昨天', const Color(0xFFFF9800)), - _notificationTile(Icons.chat, '问诊回复', '王医生已回复您的问诊', '昨天', const Color(0xFF9C27B0)), - _notificationTile(Icons.campaign, '系统通知', '欢迎使用伴享!完善资料赢取积分', '2天前', const Color(0xFF607D8B)), - ], + itemCount: _mockMessages.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) => _buildMessageTile(_mockMessages[index]), ), ); } - Widget _notificationTile(IconData icon, String title, String content, String time, Color color) { - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: CircleAvatar(backgroundColor: color.withOpacity(0.1), child: Icon(icon, color: color)), - title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), - subtitle: Text(content, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), - trailing: Text(time, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + Widget _buildMessageTile(Map message) { + final unread = message['unread'] as int; + return InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () { + // TODO: 跳转会话详情页 + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + ), + child: Row( + children: [ + CircleAvatar( + radius: 24, + backgroundColor: const Color(0xFFFFE7CF), + backgroundImage: NetworkImage('https://i.pravatar.cc/50?img=${message['avatar']}'), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message['name'] as String, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 4), + Text( + message['preview'] as String, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF8A8A8A), + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + message['time'] as String, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + const SizedBox(height: 8), + unread > 0 + ? Container( + width: 18, + height: 18, + decoration: const BoxDecoration( + color: Color(0xFFFF4D4F), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + '$unread', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ) + : const SizedBox(height: 18), + ], + ), + ], + ), ), ); } diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 56a8728..8d8acb2 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -1,18 +1,22 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'profile_setup_screen.dart'; + class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + @override State createState() => _ProfileScreenState(); } class _ProfileScreenState extends State { - final _nicknameController = TextEditingController(); - String? _selectedCity; - List _selectedInterests = []; - - final _cities = ['北京', '上海', '广州', '深圳', '成都', '重庆']; - final _interests = ['太极', '晨练', '书法', '摄影', '舞蹈', '旅游', '茶艺', '手工', '唱歌', '棋牌']; + String _nickname = '伴享用户'; + List _interests = []; + String? _avatarPath; + bool _loading = true; @override void initState() { @@ -20,83 +24,129 @@ class _ProfileScreenState extends State { _loadProfile(); } - void _loadProfile() async { + Future _loadProfile() async { final prefs = await SharedPreferences.getInstance(); + if (!mounted) return; setState(() { - _nicknameController.text = prefs.getString('nickname') ?? ''; - _selectedCity = prefs.getString('city'); - _selectedInterests = prefs.getStringList('interests') ?? []; + _nickname = prefs.getString('user_nickname') ?? prefs.getString('nickname') ?? '伴享用户'; + _interests = prefs.getStringList('user_interests') ?? prefs.getStringList('interests') ?? []; + _avatarPath = prefs.getString('user_avatar_path'); + _loading = false; }); } - void _save() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('nickname', _nicknameController.text); - if (_selectedCity != null) await prefs.setString('city', _selectedCity!); - await prefs.setStringList('interests', _selectedInterests); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('保存成功')), + bool get _hasAvatar { + final path = _avatarPath; + return path != null && path.isNotEmpty && File(path).existsSync(); + } + + Future _openProfileSetup() async { + await Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProfileSetupScreen()), ); + _loadProfile(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('个人资料')), - body: ListView( - padding: EdgeInsets.all(16), - children: [ - TextField( - controller: _nicknameController, - decoration: InputDecoration( - labelText: '昵称', - border: OutlineInputBorder(), + backgroundColor: const Color(0xFFFFF8F0), + appBar: AppBar(title: const Text('我的资料')), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 28, 24, 12), + child: Column( + children: [ + SizedBox( + width: 80, + height: 80, + child: ClipOval( + child: _hasAvatar + ? Image.file(File(_avatarPath!), fit: BoxFit.cover) + : Container( + color: const Color(0xFFFFE7CF), + child: const Icon( + Icons.person, + size: 42, + color: Color(0xFFCC7A2F), + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + _nickname.isEmpty ? '伴享用户' : _nickname, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 28), + if (_interests.isNotEmpty) + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, + children: _interests + .map( + (interest) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFFFB84D), + borderRadius: BorderRadius.circular(18), + ), + child: Text( + interest, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ) + .toList(), + ) + else + const Text( + '还没有设置兴趣标签', + style: TextStyle( + fontSize: 14, + color: Color(0xFF999999), + ), + ), + ], + ), + ), ), - ), - SizedBox(height: 20), - DropdownButtonFormField( - value: _selectedCity, - decoration: InputDecoration( - labelText: '居住城市', - border: OutlineInputBorder(), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: _openProfileSetup, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF8A3D), + foregroundColor: Colors.white, + ), + child: const Text( + '编辑资料', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ), ), - items: _cities.map((city) => DropdownMenuItem(value: city, child: Text(city))).toList(), - onChanged: (val) => setState(() => _selectedCity = val), - ), - SizedBox(height: 20), - Text('兴趣爱好(最多5个)', style: TextStyle(fontSize: 16)), - SizedBox(height: 10), - Wrap( - spacing: 8, - children: _interests.map((interest) { - final selected = _selectedInterests.contains(interest); - return FilterChip( - label: Text(interest), - selected: selected, - onSelected: (val) { - setState(() { - if (val && _selectedInterests.length < 5) { - _selectedInterests.add(interest); - } else { - _selectedInterests.remove(interest); - } - }); - }, - ); - }).toList(), - ), - SizedBox(height: 40), - ElevatedButton( - onPressed: _save, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF333333), - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 16), - ), - child: Text('保存', style: TextStyle(fontSize: 16)), - ), - ], + ], + ), ), ); } diff --git a/lib/screens/profile_setup_screen.dart b/lib/screens/profile_setup_screen.dart index 377e411..0f3ae37 100644 --- a/lib/screens/profile_setup_screen.dart +++ b/lib/screens/profile_setup_screen.dart @@ -42,6 +42,7 @@ class _ProfileSetupScreenState extends State { void _finishSetup() async { await AuthService.updateProfile( nickname: _nicknameController.text.trim().isEmpty ? null : _nicknameController.text.trim(), + avatar: _avatar?.path, birthYear: _birthYear, gender: _gender, interests: _selectedInterests.toList(), diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index bd06be8..4eda458 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -5,6 +5,9 @@ class AuthService { static const _tokenKey = 'auth_token'; static const _phoneKey = 'user_phone'; static const _nicknameKey = 'user_nickname'; + static const _avatarKey = 'user_avatar_path'; + static const _cityKey = 'user_city'; + static const _interestsKey = 'user_interests'; static User? _currentUser; static User? get currentUser => _currentUser; @@ -18,7 +21,9 @@ class AuthService { id: '1', phone: prefs.getString(_phoneKey) ?? '', nickname: prefs.getString(_nicknameKey), - city: '成都', + avatar: prefs.getString(_avatarKey), + city: prefs.getString(_cityKey) ?? '成都', + interests: prefs.getStringList(_interestsKey) ?? [], ); return true; } @@ -52,6 +57,7 @@ class AuthService { static Future updateProfile({ String? nickname, + String? avatar, int? birthYear, String? gender, String? city, @@ -61,10 +67,20 @@ class AuthService { if (nickname != null) { await prefs.setString(_nicknameKey, nickname); } + if (avatar != null) { + await prefs.setString(_avatarKey, avatar); + } + if (city != null) { + await prefs.setString(_cityKey, city); + } + if (interests != null) { + await prefs.setStringList(_interestsKey, interests); + } _currentUser = User( id: _currentUser?.id ?? '1', phone: _currentUser?.phone ?? '', nickname: nickname ?? _currentUser?.nickname, + avatar: avatar ?? _currentUser?.avatar, birthYear: birthYear ?? _currentUser?.birthYear, gender: gender ?? _currentUser?.gender, city: city ?? _currentUser?.city ?? '成都',