feat(mining-app): 资产页面优化及个人资料编辑功能
- 删除资产页面的"提现"按钮,将"划转"改为"C2C" - 删除积分值卡片上的"可提现"标签 - 简化资产页面和兑换页面的标题栏,移除左右图标 - 统一资产页面背景色与兑换页面一致 - 新增个人资料编辑页面,支持头像颜色选择和昵称修改 - 头像和昵称支持本地存储 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
854bb7a0ac
commit
3096297198
|
|
@ -11,6 +11,7 @@ import '../../presentation/pages/contribution/contribution_records_page.dart';
|
|||
import '../../presentation/pages/trading/trading_page.dart';
|
||||
import '../../presentation/pages/asset/asset_page.dart';
|
||||
import '../../presentation/pages/profile/profile_page.dart';
|
||||
import '../../presentation/pages/profile/edit_profile_page.dart';
|
||||
import '../../presentation/pages/profile/mining_records_page.dart';
|
||||
import '../../presentation/pages/profile/planting_records_page.dart';
|
||||
import '../../presentation/widgets/main_shell.dart';
|
||||
|
|
@ -112,6 +113,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
path: Routes.plantingRecords,
|
||||
builder: (context, state) => const PlantingRecordsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.editProfile,
|
||||
builder: (context, state) => const EditProfilePage(),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => MainShell(child: child),
|
||||
routes: [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class Routes {
|
|||
static const String trading = '/trading';
|
||||
static const String asset = '/asset';
|
||||
static const String profile = '/profile';
|
||||
static const String editProfile = '/edit-profile';
|
||||
static const String miningRecords = '/mining-records';
|
||||
static const String contributionRecords = '/contribution-records';
|
||||
static const String plantingRecords = '/planting-records';
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class AssetPage extends ConsumerWidget {
|
|||
final asset = assetAsync.valueOrNull;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,381 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../providers/user_providers.dart';
|
||||
|
||||
class EditProfilePage extends ConsumerStatefulWidget {
|
||||
const EditProfilePage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<EditProfilePage> createState() => _EditProfilePageState();
|
||||
}
|
||||
|
||||
class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||
static const Color _orange = Color(0xFFFF6B00);
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _darkText = Color(0xFF1F2937);
|
||||
static const Color _grayText = Color(0xFF6B7280);
|
||||
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||
|
||||
late TextEditingController _nicknameController;
|
||||
int _selectedAvatarIndex = 0;
|
||||
bool _isLoading = false;
|
||||
|
||||
// 预设头像颜色列表
|
||||
static const List<Color> _avatarColors = [
|
||||
Color(0xFFFF6B00), // 橙色
|
||||
Color(0xFF10B981), // 绿色
|
||||
Color(0xFF3B82F6), // 蓝色
|
||||
Color(0xFFEF4444), // 红色
|
||||
Color(0xFF8B5CF6), // 紫色
|
||||
Color(0xFFF59E0B), // 琥珀色
|
||||
Color(0xFFEC4899), // 粉色
|
||||
Color(0xFF06B6D4), // 青色
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final user = ref.read(userNotifierProvider);
|
||||
_nicknameController = TextEditingController(text: user.nickname ?? '');
|
||||
_selectedAvatarIndex = user.avatarIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nicknameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userNotifierProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _bgGray,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: _darkText),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'编辑资料',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : _saveProfile,
|
||||
child: Text(
|
||||
'保存',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _isLoading ? _grayText : _orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 头像选择区域
|
||||
_buildAvatarSection(user),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 昵称输入区域
|
||||
_buildNicknameSection(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 用户信息展示(只读)
|
||||
_buildInfoSection(user),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatarSection(UserState user) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'选择头像',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 当前头像预览
|
||||
Center(
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
_avatarColors[_selectedAvatarIndex].withValues(alpha: 0.8),
|
||||
_avatarColors[_selectedAvatarIndex],
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _avatarColors[_selectedAvatarIndex].withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_getAvatarText(user),
|
||||
style: const TextStyle(
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 头像颜色选择网格
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: _avatarColors.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _selectedAvatarIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedAvatarIndex = index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
_avatarColors[index].withValues(alpha: 0.8),
|
||||
_avatarColors[index],
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: isSelected
|
||||
? Border.all(color: _darkText, width: 3)
|
||||
: null,
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: _avatarColors[index].withValues(alpha: 0.4),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(Icons.check, color: Colors.white, size: 28)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNicknameSection() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'昵称',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _nicknameController,
|
||||
maxLength: 20,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入昵称',
|
||||
hintStyle: const TextStyle(color: _grayText),
|
||||
filled: true,
|
||||
fillColor: _bgGray,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _orange, width: 2),
|
||||
),
|
||||
counterStyle: const TextStyle(color: _grayText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoSection(UserState user) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'账户信息',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem('手机号', user.phone ?? '--'),
|
||||
const Divider(height: 24),
|
||||
_buildInfoItem('实名状态', user.isKycVerified ? '已实名' : '未实名',
|
||||
valueColor: user.isKycVerified ? _green : _grayText),
|
||||
if (user.realName != null && user.realName!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
_buildInfoItem('真实姓名', _maskName(user.realName!)),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(String label, String value, {Color? valueColor}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: _grayText,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: valueColor ?? _darkText,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getAvatarText(UserState user) {
|
||||
final nickname = _nicknameController.text;
|
||||
if (nickname.isNotEmpty) {
|
||||
return nickname.substring(0, 1).toUpperCase();
|
||||
}
|
||||
if (user.realName?.isNotEmpty == true) {
|
||||
return user.realName!.substring(0, 1).toUpperCase();
|
||||
}
|
||||
return 'U';
|
||||
}
|
||||
|
||||
String _maskName(String name) {
|
||||
if (name.length <= 1) return name;
|
||||
return '${name.substring(0, 1)}${'*' * (name.length - 1)}';
|
||||
}
|
||||
|
||||
Future<void> _saveProfile() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final notifier = ref.read(userNotifierProvider.notifier);
|
||||
|
||||
// 保存头像
|
||||
await notifier.updateAvatar(_selectedAvatarIndex);
|
||||
|
||||
// 保存昵称
|
||||
final nickname = _nicknameController.text.trim();
|
||||
if (nickname.isNotEmpty) {
|
||||
await notifier.updateNickname(nickname);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('保存成功'),
|
||||
backgroundColor: _green,
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,18 @@ class ProfilePage extends ConsumerWidget {
|
|||
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
||||
// 预设头像颜色列表(与编辑页面保持一致)
|
||||
static const List<Color> _avatarColors = [
|
||||
Color(0xFFFF6B00), // 橙色
|
||||
Color(0xFF10B981), // 绿色
|
||||
Color(0xFF3B82F6), // 蓝色
|
||||
Color(0xFFEF4444), // 红色
|
||||
Color(0xFF8B5CF6), // 紫色
|
||||
Color(0xFFF59E0B), // 琥珀色
|
||||
Color(0xFFEC4899), // 粉色
|
||||
Color(0xFF06B6D4), // 青色
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userNotifierProvider);
|
||||
|
|
@ -94,34 +106,39 @@ class ProfilePage extends ConsumerWidget {
|
|||
}
|
||||
|
||||
Widget _buildUserHeader(BuildContext context, UserState user) {
|
||||
final avatarColor = _avatarColors[user.avatarIndex % _avatarColors.length];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
// 头像
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [_orange.withValues(alpha: 0.8), _orange],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
// 头像(可点击)
|
||||
GestureDetector(
|
||||
onTap: () => context.push(Routes.editProfile),
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [avatarColor.withValues(alpha: 0.8), avatarColor],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
user.nickname?.isNotEmpty == true
|
||||
? user.nickname!.substring(0, 1).toUpperCase()
|
||||
: (user.realName?.isNotEmpty == true
|
||||
? user.realName!.substring(0, 1).toUpperCase()
|
||||
: 'U'),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
child: Text(
|
||||
user.nickname?.isNotEmpty == true
|
||||
? user.nickname!.substring(0, 1).toUpperCase()
|
||||
: (user.realName?.isNotEmpty == true
|
||||
? user.realName!.substring(0, 1).toUpperCase()
|
||||
: 'U'),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -137,7 +154,9 @@ class ProfilePage extends ConsumerWidget {
|
|||
Row(
|
||||
children: [
|
||||
Text(
|
||||
user.realName ?? user.nickname ?? '榴莲用户',
|
||||
user.nickname?.isNotEmpty == true
|
||||
? user.nickname!
|
||||
: (user.realName ?? '榴莲用户'),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -190,9 +209,7 @@ class ProfilePage extends ConsumerWidget {
|
|||
|
||||
// 编辑按钮
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// TODO: 编辑个人资料
|
||||
},
|
||||
onPressed: () => context.push(Routes.editProfile),
|
||||
icon: const Icon(
|
||||
Icons.edit_outlined,
|
||||
color: _grayText,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class UserState {
|
|||
final DateTime? lastLoginAt;
|
||||
final String? accessToken;
|
||||
final String? refreshToken;
|
||||
final int avatarIndex;
|
||||
final bool isLoggedIn;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
|
@ -31,6 +32,7 @@ class UserState {
|
|||
this.lastLoginAt,
|
||||
this.accessToken,
|
||||
this.refreshToken,
|
||||
this.avatarIndex = 0,
|
||||
this.isLoggedIn = false,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
|
|
@ -50,6 +52,7 @@ class UserState {
|
|||
DateTime? lastLoginAt,
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
int? avatarIndex,
|
||||
bool? isLoggedIn,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
|
|
@ -66,6 +69,7 @@ class UserState {
|
|||
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
refreshToken: refreshToken ?? this.refreshToken,
|
||||
avatarIndex: avatarIndex ?? this.avatarIndex,
|
||||
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
|
|
@ -86,6 +90,8 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
final refreshToken = prefs.getString('refresh_token');
|
||||
final accountSequence = prefs.getString('account_sequence');
|
||||
final phone = prefs.getString('phone');
|
||||
final avatarIndex = prefs.getInt('avatar_index') ?? 0;
|
||||
final nickname = prefs.getString('nickname');
|
||||
|
||||
if (accessToken != null && refreshToken != null && accountSequence != null) {
|
||||
state = state.copyWith(
|
||||
|
|
@ -93,6 +99,8 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
refreshToken: refreshToken,
|
||||
accountSequence: accountSequence,
|
||||
phone: phone,
|
||||
avatarIndex: avatarIndex,
|
||||
nickname: nickname,
|
||||
isLoggedIn: true,
|
||||
);
|
||||
// 登录后自动获取用户详情
|
||||
|
|
@ -114,6 +122,8 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
await prefs.remove('refresh_token');
|
||||
await prefs.remove('account_sequence');
|
||||
await prefs.remove('phone');
|
||||
await prefs.remove('avatar_index');
|
||||
await prefs.remove('nickname');
|
||||
}
|
||||
|
||||
Future<void> sendSmsCode(String phone, String type) async {
|
||||
|
|
@ -261,6 +271,20 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
// 静默失败,不影响用户体验
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新头像索引(本地存储)
|
||||
Future<void> updateAvatar(int avatarIndex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('avatar_index', avatarIndex);
|
||||
state = state.copyWith(avatarIndex: avatarIndex);
|
||||
}
|
||||
|
||||
/// 更新昵称(本地存储)
|
||||
Future<void> updateNickname(String nickname) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('nickname', nickname);
|
||||
state = state.copyWith(nickname: nickname);
|
||||
}
|
||||
}
|
||||
|
||||
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>(
|
||||
|
|
|
|||
Loading…
Reference in New Issue