1053 lines
31 KiB
Dart
1053 lines
31 KiB
Dart
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<AdminPage> createState() => _AdminPageState();
|
|
}
|
|
|
|
class _AdminPageState extends State<AdminPage>
|
|
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<User> _users = [];
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUsers();
|
|
}
|
|
|
|
Future<void> _loadUsers() async {
|
|
try {
|
|
final apiService = context.read<ApiService>();
|
|
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<String>(
|
|
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<ApiService>();
|
|
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<String>(
|
|
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<ApiService>();
|
|
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<ChatProvider>(
|
|
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<void> _showCreateGroupDialog() async {
|
|
final nameController = TextEditingController();
|
|
|
|
final result = await showDialog<bool>(
|
|
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<ApiService>();
|
|
await apiService.createGroup(nameController.text);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('群组已创建')),
|
|
);
|
|
context.read<ChatProvider>().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<String>(
|
|
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<ApiService>();
|
|
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<ApiService>();
|
|
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<StatsProvider>(
|
|
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<Widget> 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<Widget> _insertDividers(List<Widget> children) {
|
|
final result = <Widget>[];
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|