马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,773 @@
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;
}
}