feat: v1.1.1 batch2 - profile page, messages, friends screen
This commit is contained in:
parent
c20335bd7b
commit
6ff5757110
@ -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<ActivityListScreen> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (index == 1) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const FriendsScreen()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
|
||||
118
lib/screens/friends_screen.dart
Normal file
118
lib/screens/friends_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,32 +3,111 @@ import 'package:flutter/material.dart';
|
||||
class MessagesScreen extends StatelessWidget {
|
||||
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
|
||||
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<String, dynamic> 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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<ProfileScreen> createState() => _ProfileScreenState();
|
||||
}
|
||||
|
||||
class _ProfileScreenState extends State<ProfileScreen> {
|
||||
final _nicknameController = TextEditingController();
|
||||
String? _selectedCity;
|
||||
List<String> _selectedInterests = [];
|
||||
|
||||
final _cities = ['北京', '上海', '广州', '深圳', '成都', '重庆'];
|
||||
final _interests = ['太极', '晨练', '书法', '摄影', '舞蹈', '旅游', '茶艺', '手工', '唱歌', '棋牌'];
|
||||
String _nickname = '伴享用户';
|
||||
List<String> _interests = [];
|
||||
String? _avatarPath;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -20,83 +24,129 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
_loadProfile();
|
||||
}
|
||||
|
||||
void _loadProfile() async {
|
||||
Future<void> _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<void> _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<String>(
|
||||
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)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ class _ProfileSetupScreenState extends State<ProfileSetupScreen> {
|
||||
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(),
|
||||
|
||||
@ -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<void> 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 ?? '成都',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user