import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:sales_chat/providers/chat_provider.dart'; import 'package:sales_chat/providers/stats_provider.dart'; import 'package:sales_chat/models/user.dart'; import 'package:sales_chat/models/group.dart'; import 'package:sales_chat/widgets/user_avatar.dart'; import 'package:sales_chat/theme/app_theme.dart'; import 'package:sales_chat/services/api_service.dart'; /// 管理页面 —— 微信风格后台管理 /// /// 包含三个标签页:用户管理、群组管理、数据报表 /// 设计风格:白底列表、无边框无阴影、圆角头像、色彩标签 class AdminPage extends StatefulWidget { const AdminPage({super.key}); @override State createState() => _AdminPageState(); } class _AdminPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('管理'), bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: '用户'), Tab(text: '群组'), Tab(text: '报表'), ], ), ), body: TabBarView( controller: _tabController, children: [ _SalesManagementTab(), _GroupManagementTab(), _ReportTab(), ], ), ); } } // ============================================================ // 用户管理标签页 // ============================================================ class _SalesManagementTab extends StatefulWidget { @override State<_SalesManagementTab> createState() => _SalesManagementTabState(); } class _SalesManagementTabState extends State<_SalesManagementTab> { List _users = []; bool _isLoading = true; @override void initState() { super.initState(); _loadUsers(); } Future _loadUsers() async { try { final apiService = context.read(); final userList = await apiService.getUsers(); setState(() { _users = userList.map((data) => User.fromJson(data)).toList(); _isLoading = false; }); } catch (e) { setState(() { _isLoading = false; _users = []; }); } } @override Widget build(BuildContext context) { if (_isLoading) { return const Center( child: CircularProgressIndicator(color: AppTheme.primaryColor), ); } return Scaffold( backgroundColor: AppTheme.scaffoldBackground, body: _users.isEmpty ? _buildEmptyState() : RefreshIndicator( color: AppTheme.primaryColor, onRefresh: _loadUsers, child: ListView.separated( padding: const EdgeInsets.only(top: 8, bottom: 16), itemCount: _users.length, separatorBuilder: (_, __) => const Divider( height: 0.5, indent: 76, endIndent: 16, ), itemBuilder: (context, index) { final user = _users[index]; return _UserListTile( user: user, onRefresh: _loadUsers, ); }, ), ), ); } /// 空状态提示 Widget _buildEmptyState() { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people_outline, size: 64, color: AppTheme.textHint), SizedBox(height: 16), Text( '暂无用户', style: TextStyle( color: AppTheme.textSecondary, fontSize: 15, ), ), ], ), ); } } /// 用户列表项 —— 微信风格白色行 /// /// 布局:首字母头像 | 显示名+角色标签 / 邮箱子标题 | "…"菜单 class _UserListTile extends StatelessWidget { final User user; final VoidCallback onRefresh; const _UserListTile({required this.user, required this.onRefresh}); @override Widget build(BuildContext context) { return Container( color: AppTheme.cardBackground, child: InkWell( onTap: () {}, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // 首字母头像 _buildAvatar(), const SizedBox(width: 12), // 显示名、角色标签、子标题 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // 显示名 Flexible( child: Text( user.displayName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: AppTheme.textPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), // 角色标签 _buildRoleBadge(), ], ), const SizedBox(height: 4), // 邮箱 / ID 子标题 Text( user.email ?? user.id, style: const TextStyle( fontSize: 13, color: AppTheme.textHint, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), // "…" 操作菜单 PopupMenuButton( icon: const Icon( Icons.more_horiz, color: AppTheme.textHint, size: 22, ), padding: const EdgeInsets.all(4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), onSelected: (value) => _handleAction(context, value), itemBuilder: (context) => [ const PopupMenuItem( value: 'change_role', height: 44, child: Row( children: [ Icon(Icons.edit_outlined, size: 18, color: AppTheme.textSecondary,), SizedBox(width: 10), Text('修改角色', style: TextStyle(fontSize: 15),), ], ), ), const PopupMenuItem( value: 'delete', height: 44, child: Row( children: [ Icon(Icons.delete_outline, size: 18, color: AppTheme.errorColor,), SizedBox(width: 10), Text('删除账号', style: TextStyle( fontSize: 15, color: AppTheme.errorColor,),), ], ), ), ], ), ], ), ), ), ); } /// 构建首字母圆形头像 Widget _buildAvatar() { final initial = _getInitial(); final bgColor = _getRoleColor(); return Container( width: 44, height: 44, decoration: BoxDecoration( color: bgColor.withValues(alpha: 0.12), shape: BoxShape.circle, ), alignment: Alignment.center, child: Text( initial, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: bgColor, ), ), ); } /// 获取首字母 String _getInitial() { final name = user.displayName; if (name.isEmpty) return '?'; return name.substring(0, 1).toUpperCase(); } /// 角色标签颜色 Color _getRoleColor() { if (user.isSuperAdmin) return AppTheme.errorColor; if (user.isAdmin) return Colors.orange; return AppTheme.primaryColor; } /// 角色标签名 String _getRoleName() { if (user.isSuperAdmin) return '超级管理员'; if (user.isAdmin) return '管理员'; if (user.isGuest) return '访客'; return '销售'; } /// 构建角色标签(小色块) Widget _buildRoleBadge() { final color = _getRoleColor(); final name = _getRoleName(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( name, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: color, ), ), ); } /// 处理菜单操作 void _handleAction(BuildContext context, String action) { switch (action) { case 'delete': _showDeleteConfirmDialog(context); break; case 'change_role': _showChangeRoleDialog(context); break; } } /// 删除确认弹窗 —— 微信风格 void _showDeleteConfirmDialog(BuildContext outerContext) { showDialog( context: outerContext, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: const Text( '确认删除', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, ), ), content: Text( '确定要删除用户「${user.displayName}」吗?此操作不可撤销。', style: const TextStyle( fontSize: 15, color: AppTheme.textSecondary, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text( '取消', style: TextStyle(color: AppTheme.textSecondary), ), ), TextButton( onPressed: () async { Navigator.pop(dialogContext); try { final apiService = outerContext.read(); await apiService.deleteUser(user.id); onRefresh(); if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( const SnackBar(content: Text('用户已删除')), ); } } catch (e) { if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( SnackBar(content: Text('失败:$e')), ); } } }, child: const Text( '删除', style: TextStyle(color: AppTheme.errorColor), ), ), ], ), ); } /// 修改角色弹窗 —— 微信风格 void _showChangeRoleDialog(BuildContext outerContext) { String role = user.extra?['role'] ?? 'sales'; showDialog( context: outerContext, builder: (dialogContext) => StatefulBuilder( builder: (dialogContext, setState) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: const Text( '修改角色', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, ), ), content: DropdownButtonFormField( initialValue: role, decoration: const InputDecoration( labelText: '选择角色', ), items: const [ DropdownMenuItem(value: 'sales', child: Text('销售')), DropdownMenuItem(value: 'admin', child: Text('管理员')), DropdownMenuItem( value: 'super_admin', child: Text('超级管理员'),), ], onChanged: (v) { setState(() { role = v!; }); }, ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text( '取消', style: TextStyle(color: AppTheme.textSecondary), ), ), ElevatedButton( onPressed: () async { Navigator.pop(dialogContext); try { final apiService = outerContext.read(); await apiService.updateUserRole(user.id, role); onRefresh(); if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( const SnackBar(content: Text('角色已更新')), ); } } catch (e) { if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( SnackBar(content: Text('失败:$e')), ); } } }, child: const Text('更新'), ), ], ); }, ), ); } } // ============================================================ // 群组管理标签页 // ============================================================ class _GroupManagementTab extends StatefulWidget { @override State<_GroupManagementTab> createState() => _GroupManagementTabState(); } class _GroupManagementTabState extends State<_GroupManagementTab> { @override Widget build(BuildContext context) { return Consumer( builder: (context, chatProvider, _) { final groups = chatProvider.groups; if (chatProvider.isLoading) { return const Center( child: CircularProgressIndicator(color: AppTheme.primaryColor), ); } return Scaffold( backgroundColor: AppTheme.scaffoldBackground, body: groups.isEmpty ? _buildEmptyState() : ListView.separated( padding: const EdgeInsets.only(top: 8, bottom: 80), itemCount: groups.length, separatorBuilder: (_, __) => const Divider( height: 0.5, indent: 76, endIndent: 16, ), itemBuilder: (context, index) { final group = groups[index]; return _GroupListTile(group: group); }, ), floatingActionButton: FloatingActionButton( heroTag: 'createGroup', onPressed: () => _showCreateGroupDialog(), backgroundColor: AppTheme.primaryColor, child: const Icon(Icons.add, color: Colors.white), ), ); }, ); } /// 空状态提示 Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.group_outlined, size: 64, color: AppTheme.textHint), const SizedBox(height: 16), const Text( '暂无群组', style: TextStyle( color: AppTheme.textSecondary, fontSize: 15, ), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () => _showCreateGroupDialog(), icon: const Icon(Icons.add, size: 18), label: const Text('创建群组'), ), ], ), ); } /// 创建群组弹窗 —— 微信风格 Future _showCreateGroupDialog() async { final nameController = TextEditingController(); final result = await showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: const Text( '创建群组', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, ), ), content: TextField( controller: nameController, decoration: const InputDecoration( labelText: '群组名称', hintText: '请输入群组名称', ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text( '取消', style: TextStyle(color: AppTheme.textSecondary), ), ), ElevatedButton( onPressed: () { if (nameController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请输入群组名称')), ); return; } Navigator.pop(context, true); }, child: const Text('创建'), ), ], ), ); if (result == true && nameController.text.isNotEmpty) { try { final apiService = context.read(); await apiService.createGroup(nameController.text); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('群组已创建')), ); context.read().loadGroups(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('创建失败:$e')), ); } } } } } /// 群组列表项 —— 微信风格白色行 class _GroupListTile extends StatelessWidget { final Group group; const _GroupListTile({required this.group}); @override Widget build(BuildContext context) { return Container( color: AppTheme.cardBackground, child: InkWell( onTap: () {}, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // 群组头像 GroupAvatar(groupName: group.name, radius: 22), const SizedBox(width: 12), // 群名 + 成员数 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( group.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: AppTheme.textPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( '${group.memberCount} 位成员', style: const TextStyle( fontSize: 13, color: AppTheme.textHint, ), ), ], ), ), // "…" 操作菜单 PopupMenuButton( icon: const Icon( Icons.more_horiz, color: AppTheme.textHint, size: 22, ), padding: const EdgeInsets.all(4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), onSelected: (value) => _handleAction(context, value), itemBuilder: (context) => [ const PopupMenuItem( value: 'kick', height: 44, child: Row( children: [ Icon(Icons.person_remove_outlined, size: 18, color: AppTheme.textSecondary,), SizedBox(width: 10), Text('踢出用户', style: TextStyle(fontSize: 15),), ], ), ), const PopupMenuItem( value: 'stats', height: 44, child: Row( children: [ Icon(Icons.bar_chart_outlined, size: 18, color: AppTheme.textSecondary,), SizedBox(width: 10), Text('群组统计', style: TextStyle(fontSize: 15),), ], ), ), ], ), ], ), ), ), ); } /// 处理菜单操作 void _handleAction(BuildContext context, String action) { switch (action) { case 'kick': _showKickDialog(context); break; case 'stats': _showGroupStats(context); break; } } /// 踢出用户弹窗 —— 微信风格 void _showKickDialog(BuildContext outerContext) { final userIdController = TextEditingController(); final reasonController = TextEditingController(); showDialog( context: outerContext, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: const Text( '踢出用户', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, ), ), content: SizedBox( width: 400, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: userIdController, decoration: const InputDecoration( labelText: '用户 ID', hintText: '请输入用户 ID', ), ), const SizedBox(height: 16), TextField( controller: reasonController, decoration: const InputDecoration( labelText: '原因', hintText: '选填', ), maxLines: 2, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text( '取消', style: TextStyle(color: AppTheme.textSecondary), ), ), TextButton( onPressed: () async { if (userIdController.text.isEmpty) { ScaffoldMessenger.of(dialogContext).showSnackBar( const SnackBar(content: Text('请输入用户 ID')), ); return; } Navigator.pop(dialogContext); try { final apiService = outerContext.read(); await apiService.kickGroupMember( group.id, userIdController.text.trim(), reason: reasonController.text.trim().isNotEmpty ? reasonController.text.trim() : null, ); if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( const SnackBar(content: Text('用户已踢出')), ); } } catch (e) { if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( SnackBar(content: Text('失败:$e')), ); } } }, child: const Text( '踢出', style: TextStyle(color: AppTheme.errorColor), ), ), ], ), ); } /// 群组统计弹窗 —— 微信风格 void _showGroupStats(BuildContext outerContext) async { try { final apiService = outerContext.read(); final stats = await apiService.getGroupStats(group.id); if (outerContext.mounted) { showDialog( context: outerContext, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: Text( group.name, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, ), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ _buildStatRow('邀请数', '${stats['stats']?['totalInvites'] ?? 0}',), const Divider(height: 24), _buildStatRow('点击数', '${stats['stats']?['totalClicks'] ?? 0}',), const Divider(height: 24), _buildStatRow('加入数', '${stats['stats']?['totalJoins'] ?? 0}',), ], ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('关闭'), ), ), ], ), ); } } catch (e) { if (outerContext.mounted) { ScaffoldMessenger.of(outerContext).showSnackBar( SnackBar(content: Text('加载统计失败:$e')), ); } } } /// 统计行 Widget _buildStatRow(String label, String value) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( fontSize: 15, color: AppTheme.textSecondary, ), ), Text( value, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ); } } // ============================================================ // 数据报表标签页 // ============================================================ class _ReportTab extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( builder: (context, statsProvider, _) { if (statsProvider.isLoading) { return const Center( child: CircularProgressIndicator(color: AppTheme.primaryColor), ); } final stats = statsProvider.myStats; if (stats == null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.analytics_outlined, size: 64, color: AppTheme.textHint,), const SizedBox(height: 16), const Text( '暂无统计数据', style: TextStyle( color: AppTheme.textSecondary, fontSize: 15, ), ), const SizedBox(height: 16), ElevatedButton( onPressed: () => statsProvider.refresh(), child: const Text('加载统计'), ), ], ), ); } return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 总览区块 _buildSection( context, title: '总览', children: [ _buildStatRow('总邀请数', '${stats.total.totalInvites}'), _buildStatRow('总加入数', '${stats.total.totalJoins}'), _buildStatRow('总转化数', '${stats.total.totalConversions}'), _buildStatRow('总收入', '${stats.total.totalRevenue}'), ], ), const SizedBox(height: 16), // 今天区块 _buildSection( context, title: '今天', children: [ _buildStatRow('创建邀请', '${stats.today.invitesCreated}'), _buildStatRow('加入数', '${stats.today.joins}'), _buildStatRow('转化数', '${stats.today.conversions}'), _buildStatRow('收入', '${stats.today.revenue}'), ], ), ], ), ); }, ); } /// 构建白色区块(带标题和数据行) Widget _buildSection( BuildContext context, { required String title, required List children, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 区块标题 Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), child: Text( title, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ), // 白色卡片容器 Container( decoration: BoxDecoration( color: AppTheme.cardBackground, borderRadius: BorderRadius.circular(12), ), child: Column( children: _insertDividers(children), ), ), ], ); } /// 在数据行之间插入细分隔线 List _insertDividers(List children) { final result = []; for (int i = 0; i < children.length; i++) { result.add(children[i]); if (i < children.length - 1) { result.add(const Divider( height: 0.5, thickness: 0.5, indent: 16, endIndent: 16, color: AppTheme.dividerColor, ),); } } return result; } /// 数据行 —— 标签在左,数值在右 Widget _buildStatRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( fontSize: 15, color: AppTheme.textSecondary, ), ), Text( value, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), ); } }