Files
MaliangAINovalWriter/AINoval/lib/screens/settings/widgets/account_management_panel.dart
2025-09-10 00:07:52 +08:00

774 lines
25 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:ainoval/config/app_config.dart';
import 'package:ainoval/utils/web_theme.dart';
import 'package:ainoval/widgets/forms/change_password_form.dart';
import 'package:ainoval/widgets/common/top_toast.dart';
import 'package:ainoval/services/auth_service.dart';
/// 账户管理面板
/// 集成在设置面板中的账户相关功能
class AccountManagementPanel extends StatefulWidget {
const AccountManagementPanel({Key? key}) : super(key: key);
@override
State<AccountManagementPanel> createState() => _AccountManagementPanelState();
}
class _AccountManagementPanelState extends State<AccountManagementPanel> {
int _selectedTabIndex = 0;
Map<String, dynamic>? _userInfo;
bool _isLoadingUserInfo = false;
bool _isEditingPersonalInfo = false;
bool _isSavingPersonalInfo = false;
final GlobalKey<FormState> _personalInfoFormKey = GlobalKey<FormState>();
final TextEditingController _displayNameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
final List<String> _tabs = ['个人信息', '修改密码', '安全设置'];
@override
void initState() {
super.initState();
_loadUserInfo();
}
@override
void dispose() {
_displayNameController.dispose();
_emailController.dispose();
_phoneController.dispose();
super.dispose();
}
/// 加载用户信息
Future<void> _loadUserInfo() async {
setState(() {
_isLoadingUserInfo = true;
});
try {
final authService = AuthService();
// 确保初始化以加载本地存储中的登录状态
await authService.init();
final userInfo = await authService.getCurrentUser();
if (!mounted) return;
setState(() {
_userInfo = userInfo;
_isLoadingUserInfo = false;
});
_populateControllersFromUserInfo(userInfo);
} catch (e) {
if (mounted) {
setState(() {
_isLoadingUserInfo = false;
});
TopToast.error(context, '加载用户信息失败:${e.toString().replaceAll('AuthException: ', '')}');
}
}
}
void _populateControllersFromUserInfo(Map<String, dynamic> info) {
try {
_displayNameController.text = (info['displayName'] ?? '').toString();
_emailController.text = (info['email'] ?? '').toString();
_phoneController.text = (info['phone'] ?? '').toString();
} catch (_) {}
}
void _toggleEditing() {
setState(() {
_isEditingPersonalInfo = !_isEditingPersonalInfo;
if (_isEditingPersonalInfo && _userInfo != null) {
_populateControllersFromUserInfo(_userInfo!);
}
});
}
Future<void> _savePersonalInfo() async {
if (!_isEditingPersonalInfo) return;
final form = _personalInfoFormKey.currentState;
if (form == null || !form.validate()) {
return;
}
setState(() {
_isSavingPersonalInfo = true;
});
try {
final authService = AuthService();
await authService.init();
final updated = await authService.updateUserProfile({
'displayName': _displayNameController.text.trim(),
'email': _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
'phone': _phoneController.text.trim().isEmpty ? null : _phoneController.text.trim(),
});
if (!mounted) return;
setState(() {
_userInfo = updated;
_isEditingPersonalInfo = false;
_isSavingPersonalInfo = false;
});
TopToast.success(context, '个人信息已保存');
} catch (e) {
if (!mounted) return;
setState(() {
_isSavingPersonalInfo = false;
});
TopToast.error(context, e.toString().replaceAll('AuthException: ', '保存失败:'));
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
'账户管理',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: WebTheme.getTextColor(context),
),
),
const SizedBox(height: 16),
// 用户信息概览卡片
_buildUserOverviewCard(),
const SizedBox(height: 24),
// Tab导航
_buildTabNavigation(),
const SizedBox(height: 16),
// Tab内容
Expanded(
child: _buildTabContent(),
),
],
);
}
/// 构建用户概览卡片
Widget _buildUserOverviewCard() {
final username = AppConfig.username ?? '游客';
final userId = AppConfig.userId ?? '未知';
return Card(
elevation: 2,
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: WebTheme.getSurfaceColor(context),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 头像
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: WebTheme.getPrimaryColor(context).withOpacity(0.1),
),
child: Icon(
Icons.person,
size: 25,
color: WebTheme.getPrimaryColor(context),
),
),
const SizedBox(width: 16),
// 用户信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
username,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: WebTheme.getTextColor(context),
),
),
const SizedBox(height: 4),
Text(
'ID: $userId',
style: TextStyle(
fontSize: 14,
color: WebTheme.getSecondaryTextColor(context),
),
),
if (_userInfo != null) ...[
const SizedBox(height: 4),
Text(
'积分: ${_userInfo!['credits'] ?? 0}',
style: TextStyle(
fontSize: 14,
color: WebTheme.getPrimaryColor(context),
fontWeight: FontWeight.w500,
),
),
],
],
),
),
// 刷新按钮
IconButton(
onPressed: _isLoadingUserInfo ? null : _loadUserInfo,
icon: _isLoadingUserInfo
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
WebTheme.getSecondaryTextColor(context),
),
),
)
: Icon(
Icons.refresh,
color: WebTheme.getSecondaryTextColor(context),
),
tooltip: '刷新用户信息',
),
],
),
),
);
}
/// 构建Tab导航
Widget _buildTabNavigation() {
return Container(
height: 48,
decoration: BoxDecoration(
color: WebTheme.getSurfaceColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: WebTheme.getBorderColor(context),
width: 1,
),
),
child: Row(
children: _tabs.asMap().entries.map((entry) {
final index = entry.key;
final title = entry.value;
final isSelected = _selectedTabIndex == index;
return Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_selectedTabIndex = index;
});
},
child: Container(
height: double.infinity,
decoration: BoxDecoration(
color: isSelected
? WebTheme.getPrimaryColor(context).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Center(
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? WebTheme.getPrimaryColor(context)
: WebTheme.getSecondaryTextColor(context),
),
),
),
),
),
);
}).toList(),
),
);
}
/// 构建Tab内容
Widget _buildTabContent() {
switch (_selectedTabIndex) {
case 0:
return _buildPersonalInfoTab();
case 1:
return _buildChangePasswordTab();
case 2:
return _buildSecuritySettingsTab();
default:
return Container();
}
}
/// 个人信息Tab
Widget _buildPersonalInfoTab() {
return SingleChildScrollView(
child: Card(
elevation: 2,
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: WebTheme.getSurfaceColor(context),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.person_outline,
color: WebTheme.getPrimaryColor(context),
size: 24,
),
const SizedBox(width: 8),
Text(
'个人信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
),
),
const Spacer(),
if (_userInfo != null && !_isLoadingUserInfo && !_isEditingPersonalInfo)
OutlinedButton.icon(
onPressed: _toggleEditing,
icon: const Icon(Icons.edit, size: 16),
label: const Text('编辑'),
),
if (_isEditingPersonalInfo) ...[
TextButton(
onPressed: _isSavingPersonalInfo ? null : _toggleEditing,
child: const Text('取消'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _isSavingPersonalInfo ? null : _savePersonalInfo,
icon: _isSavingPersonalInfo
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save, size: 16),
label: const Text('保存'),
),
],
],
),
const SizedBox(height: 20),
if (_isLoadingUserInfo)
const Center(child: CircularProgressIndicator())
else if (_userInfo != null && !_isEditingPersonalInfo) ...[
_buildInfoField('用户名', AppConfig.username ?? '未知'),
const SizedBox(height: 16),
_buildInfoField('显示名称', (_userInfo!['displayName'] ?? '未设置').toString()),
const SizedBox(height: 16),
_buildInfoField('邮箱', (_userInfo!['email'] ?? '未设置').toString()),
const SizedBox(height: 16),
_buildInfoField('手机号', (_userInfo!['phone'] ?? '未设置').toString()),
const SizedBox(height: 16),
_buildInfoField('注册时间', _formatDateTime(_userInfo!['createdAt'])),
const SizedBox(height: 16),
_buildInfoField('最后登录', _formatDateTime(_userInfo!['lastLoginAt'])),
] else if (_userInfo != null && _isEditingPersonalInfo) ...[
Form(
key: _personalInfoFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildEditableTextField(
label: '显示名称',
controller: _displayNameController,
hintText: '请输入显示名称',
validator: (v) {
if (v == null || v.trim().isEmpty) {
return '显示名称不能为空';
}
if (v.trim().length > 32) {
return '显示名称过长最多32个字符';
}
return null;
},
),
const SizedBox(height: 16),
_buildEditableTextField(
label: '邮箱',
controller: _emailController,
hintText: '请输入邮箱(可留空)',
keyboardType: TextInputType.emailAddress,
validator: (v) {
final value = (v ?? '').trim();
if (value.isEmpty) return null; // 允许空
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
if (!emailRegex.hasMatch(value)) {
return '邮箱格式不正确';
}
return null;
},
),
const SizedBox(height: 16),
_buildEditableTextField(
label: '手机号',
controller: _phoneController,
hintText: '请输入手机号(可留空)',
keyboardType: TextInputType.phone,
validator: (v) {
final value = (v ?? '').trim();
if (value.isEmpty) return null; // 允许空
final phoneRegex = RegExp(r'^[0-9+\-\s]{6,20}$');
if (!phoneRegex.hasMatch(value)) {
return '手机号格式不正确';
}
return null;
},
),
],
),
),
] else ...[
Center(
child: Column(
children: [
Icon(
Icons.error_outline,
size: 48,
color: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(height: 16),
Text(
'无法加载用户信息',
style: TextStyle(
fontSize: 16,
color: WebTheme.getSecondaryTextColor(context),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _loadUserInfo,
child: const Text('重试'),
),
],
),
),
],
],
),
),
),
);
}
/// 修改密码Tab
Widget _buildChangePasswordTab() {
return Card(
elevation: 2,
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: WebTheme.getSurfaceColor(context),
child: ChangePasswordForm(
showTitle: false,
onSuccess: () {
TopToast.success(context, '密码修改成功');
},
),
);
}
/// 安全设置Tab
Widget _buildSecuritySettingsTab() {
return SingleChildScrollView(
child: Card(
elevation: 2,
shadowColor: WebTheme.getShadowColor(context, opacity: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: WebTheme.getSurfaceColor(context),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.security,
color: WebTheme.getPrimaryColor(context),
size: 24,
),
const SizedBox(width: 8),
Text(
'安全设置',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
),
),
],
),
const SizedBox(height: 20),
_buildSecurityItem(
icon: Icons.device_unknown,
title: '登录设备管理',
subtitle: '查看和管理登录设备',
onTap: () {
TopToast.info(context, '登录设备管理功能开发中');
},
),
const Divider(height: 32),
_buildSecurityItem(
icon: Icons.history,
title: '登录历史',
subtitle: '查看最近的登录记录',
onTap: () {
TopToast.info(context, '登录历史功能开发中');
},
),
const Divider(height: 32),
_buildSecurityItem(
icon: Icons.key,
title: 'API密钥管理',
subtitle: '管理第三方API访问密钥',
onTap: () {
TopToast.info(context, 'API密钥管理功能开发中');
},
),
const Divider(height: 32),
_buildSecurityItem(
icon: Icons.privacy_tip,
title: '隐私设置',
subtitle: '管理数据使用和隐私偏好',
onTap: () {
TopToast.info(context, '隐私设置功能开发中');
},
),
],
),
),
),
);
}
/// 构建信息字段
Widget _buildInfoField(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: WebTheme.getSecondaryTextColor(context),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: WebTheme.getBackgroundColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: WebTheme.getBorderColor(context),
width: 1,
),
),
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: WebTheme.getTextColor(context),
),
),
),
],
);
}
/// 构建可编辑文本字段
Widget _buildEditableTextField({
required String label,
required TextEditingController controller,
String? hintText,
TextInputType? keyboardType,
String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: WebTheme.getSecondaryTextColor(context),
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
validator: validator,
decoration: InputDecoration(
hintText: hintText,
filled: true,
fillColor: WebTheme.getBackgroundColor(context),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: WebTheme.getBorderColor(context)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: WebTheme.getPrimaryColor(context), width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
),
],
);
}
/// 构建安全设置项
Widget _buildSecurityItem({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: WebTheme.getPrimaryColor(context).withOpacity(0.1),
),
child: Icon(
icon,
size: 20,
color: WebTheme.getPrimaryColor(context),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: WebTheme.getTextColor(context),
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: WebTheme.getSecondaryTextColor(context),
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: WebTheme.getSecondaryTextColor(context),
),
],
),
),
);
}
/// 格式化日期时间(兼容多种后端返回格式)
String _formatDateTime(dynamic value) {
if (value == null) return '未知';
try {
DateTime dateTime;
if (value is String) {
dateTime = DateTime.parse(value);
} else if (value is int) {
// 兼容时间戳(秒/毫秒)
if (value > 1000000000000) {
dateTime = DateTime.fromMillisecondsSinceEpoch(value);
} else if (value > 1000000000) {
dateTime = DateTime.fromMillisecondsSinceEpoch(value * 1000);
} else {
return '未知';
}
} else if (value is List) {
// 兼容 [year, month, day, hour?, minute?, second?]
final year = _toInt(value, 0);
final month = _toInt(value, 1);
final day = _toInt(value, 2);
final hour = _toInt(value, 3) ?? 0;
final minute = _toInt(value, 4) ?? 0;
final second = _toInt(value, 5) ?? 0;
if (year != null && month != null && day != null) {
dateTime = DateTime(year, month, day, hour, minute, second);
} else {
return '未知';
}
} else if (value is Map && value.containsKey('\$date')) {
final d = value['\$date'];
if (d is String) {
dateTime = DateTime.parse(d);
} else if (d is int) {
dateTime = DateTime.fromMillisecondsSinceEpoch(d);
} else {
return '未知';
}
} else {
return '未知';
}
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} catch (_) {
return '未知';
}
}
int? _toInt(List<dynamic> list, int index) {
if (index >= list.length) return null;
final v = list[index];
if (v is int) return v;
if (v is String) return int.tryParse(v);
return null;
}
}