feat: v1.1.1 batch2 - profile page, messages, friends screen

This commit is contained in:
Ubuntu 2026-02-17 17:10:36 +08:00
parent c20335bd7b
commit 6ff5757110
6 changed files with 360 additions and 88 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'publish_screen.dart'; import 'publish_screen.dart';
import 'activity_detail_screen.dart'; import 'activity_detail_screen.dart';
import 'friends_screen.dart';
import 'dart:math'; import 'dart:math';
class ActivityListScreen extends StatefulWidget { class ActivityListScreen extends StatefulWidget {
@ -73,6 +74,13 @@ class _ActivityListScreenState extends State<ActivityListScreen> {
); );
return; return;
} }
if (index == 1) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const FriendsScreen()),
);
return;
}
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
}); });

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
class FriendsScreen extends StatefulWidget {
const FriendsScreen({super.key});
@override
State<FriendsScreen> createState() => _FriendsScreenState();
}
class _FriendsScreenState extends State<FriendsScreen> {
final List<Map<String, dynamic>> _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<Map<String, dynamic>> _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<Map<String, dynamic>> 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<Map<String, dynamic>> 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),
),
),
),
],
),
);
},
);
}
}

View File

@ -3,32 +3,111 @@ import 'package:flutter/material.dart';
class MessagesScreen extends StatelessWidget { class MessagesScreen extends StatelessWidget {
const MessagesScreen({super.key}); const MessagesScreen({super.key});
static final List<Map<String, dynamic>> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFFFF8F0),
appBar: AppBar(title: const Text('消息')), appBar: AppBar(title: const Text('消息')),
body: ListView( body: ListView.separated(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ itemCount: _mockMessages.length,
_notificationTile(Icons.event, '活动提醒', '您报名的「太极晨练」明天7:00开始', '10分钟前', const Color(0xFF1976D2)), separatorBuilder: (_, __) => const SizedBox(height: 10),
_notificationTile(Icons.check_circle, '报名成功', '您已成功报名「周末茶话会」', '1小时前', const Color(0xFF4CAF50)), itemBuilder: (context, index) => _buildMessageTile(_mockMessages[index]),
_notificationTile(Icons.payment, '支付通知', '挂号费¥50已支付成功', '昨天', const Color(0xFFFF9800)),
_notificationTile(Icons.chat, '问诊回复', '王医生已回复您的问诊', '昨天', const Color(0xFF9C27B0)),
_notificationTile(Icons.campaign, '系统通知', '欢迎使用伴享!完善资料赢取积分', '2天前', const Color(0xFF607D8B)),
],
), ),
); );
} }
Widget _notificationTile(IconData icon, String title, String content, String time, Color color) { Widget _buildMessageTile(Map<String, dynamic> message) {
return Card( final unread = message['unread'] as int;
margin: const EdgeInsets.only(bottom: 8), return InkWell(
child: ListTile( borderRadius: BorderRadius.circular(14),
leading: CircleAvatar(backgroundColor: color.withOpacity(0.1), child: Icon(icon, color: color)), onTap: () {
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), // TODO:
subtitle: Text(content, style: const TextStyle(fontSize: 14, color: Color(0xFF666666))), },
trailing: Text(time, style: const TextStyle(fontSize: 12, color: Color(0xFF999999))), child: Container(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 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),
],
),
],
),
), ),
); );
} }

View File

@ -1,18 +1,22 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'profile_setup_screen.dart';
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override @override
State<ProfileScreen> createState() => _ProfileScreenState(); State<ProfileScreen> createState() => _ProfileScreenState();
} }
class _ProfileScreenState extends State<ProfileScreen> { class _ProfileScreenState extends State<ProfileScreen> {
final _nicknameController = TextEditingController(); String _nickname = '伴享用户';
String? _selectedCity; List<String> _interests = [];
List<String> _selectedInterests = []; String? _avatarPath;
bool _loading = true;
final _cities = ['北京', '上海', '广州', '深圳', '成都', '重庆'];
final _interests = ['太极', '晨练', '书法', '摄影', '舞蹈', '旅游', '茶艺', '手工', '唱歌', '棋牌'];
@override @override
void initState() { void initState() {
@ -20,83 +24,129 @@ class _ProfileScreenState extends State<ProfileScreen> {
_loadProfile(); _loadProfile();
} }
void _loadProfile() async { Future<void> _loadProfile() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (!mounted) return;
setState(() { setState(() {
_nicknameController.text = prefs.getString('nickname') ?? ''; _nickname = prefs.getString('user_nickname') ?? prefs.getString('nickname') ?? '伴享用户';
_selectedCity = prefs.getString('city'); _interests = prefs.getStringList('user_interests') ?? prefs.getStringList('interests') ?? [];
_selectedInterests = prefs.getStringList('interests') ?? []; _avatarPath = prefs.getString('user_avatar_path');
_loading = false;
}); });
} }
void _save() async { bool get _hasAvatar {
final prefs = await SharedPreferences.getInstance(); final path = _avatarPath;
await prefs.setString('nickname', _nicknameController.text); return path != null && path.isNotEmpty && File(path).existsSync();
if (_selectedCity != null) await prefs.setString('city', _selectedCity!); }
await prefs.setStringList('interests', _selectedInterests);
ScaffoldMessenger.of(context).showSnackBar( Future<void> _openProfileSetup() async {
SnackBar(content: Text('保存成功')), await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProfileSetupScreen()),
); );
_loadProfile();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('个人资料')), backgroundColor: const Color(0xFFFFF8F0),
body: ListView( appBar: AppBar(title: const Text('我的资料')),
padding: EdgeInsets.all(16), body: SafeArea(
children: [ child: Column(
TextField( children: [
controller: _nicknameController, Expanded(
decoration: InputDecoration( child: _loading
labelText: '昵称', ? const Center(child: CircularProgressIndicator())
border: OutlineInputBorder(), : 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),
),
),
],
),
),
), ),
), Padding(
SizedBox(height: 20), padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
DropdownButtonFormField<String>( child: SizedBox(
value: _selectedCity, width: double.infinity,
decoration: InputDecoration( height: 52,
labelText: '居住城市', child: ElevatedButton(
border: OutlineInputBorder(), 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)),
),
],
), ),
); );
} }

View File

@ -42,6 +42,7 @@ class _ProfileSetupScreenState extends State<ProfileSetupScreen> {
void _finishSetup() async { void _finishSetup() async {
await AuthService.updateProfile( await AuthService.updateProfile(
nickname: _nicknameController.text.trim().isEmpty ? null : _nicknameController.text.trim(), nickname: _nicknameController.text.trim().isEmpty ? null : _nicknameController.text.trim(),
avatar: _avatar?.path,
birthYear: _birthYear, birthYear: _birthYear,
gender: _gender, gender: _gender,
interests: _selectedInterests.toList(), interests: _selectedInterests.toList(),

View File

@ -5,6 +5,9 @@ class AuthService {
static const _tokenKey = 'auth_token'; static const _tokenKey = 'auth_token';
static const _phoneKey = 'user_phone'; static const _phoneKey = 'user_phone';
static const _nicknameKey = 'user_nickname'; 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? _currentUser;
static User? get currentUser => _currentUser; static User? get currentUser => _currentUser;
@ -18,7 +21,9 @@ class AuthService {
id: '1', id: '1',
phone: prefs.getString(_phoneKey) ?? '', phone: prefs.getString(_phoneKey) ?? '',
nickname: prefs.getString(_nicknameKey), nickname: prefs.getString(_nicknameKey),
city: '成都', avatar: prefs.getString(_avatarKey),
city: prefs.getString(_cityKey) ?? '成都',
interests: prefs.getStringList(_interestsKey) ?? [],
); );
return true; return true;
} }
@ -52,6 +57,7 @@ class AuthService {
static Future<void> updateProfile({ static Future<void> updateProfile({
String? nickname, String? nickname,
String? avatar,
int? birthYear, int? birthYear,
String? gender, String? gender,
String? city, String? city,
@ -61,10 +67,20 @@ class AuthService {
if (nickname != null) { if (nickname != null) {
await prefs.setString(_nicknameKey, nickname); 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( _currentUser = User(
id: _currentUser?.id ?? '1', id: _currentUser?.id ?? '1',
phone: _currentUser?.phone ?? '', phone: _currentUser?.phone ?? '',
nickname: nickname ?? _currentUser?.nickname, nickname: nickname ?? _currentUser?.nickname,
avatar: avatar ?? _currentUser?.avatar,
birthYear: birthYear ?? _currentUser?.birthYear, birthYear: birthYear ?? _currentUser?.birthYear,
gender: gender ?? _currentUser?.gender, gender: gender ?? _currentUser?.gender,
city: city ?? _currentUser?.city ?? '成都', city: city ?? _currentUser?.city ?? '成都',