Files
chat/client/flutter/lib/pages/invite_page.dart
2026-04-25 16:36:34 +08:00

669 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:sales_chat/providers/invite_provider.dart';
import 'package:sales_chat/providers/chat_provider.dart';
import 'package:sales_chat/models/invite.dart';
import 'package:sales_chat/theme/app_theme.dart';
import 'package:sales_chat/services/api_service.dart';
import 'package:intl/intl.dart';
/// 邀请页面 —— Twitter 风格
class InvitePage extends StatefulWidget {
const InvitePage({super.key});
@override
State<InvitePage> createState() => _InvitePageState();
}
class _InvitePageState extends State<InvitePage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
Future<void> _loadData() async {
final inviteProvider = context.read<InviteProvider>();
final chatProvider = context.read<ChatProvider>();
// 先确保群组数据已加载
await chatProvider.loadGroups();
await Future.wait([
inviteProvider.loadInvites(),
inviteProvider.loadAvailableGroups(chatProvider.groups),
]);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('邀请'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadData,
),
],
),
body: Consumer<InviteProvider>(
builder: (context, inviteProvider, _) {
if (inviteProvider.isLoading && inviteProvider.invites.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 创建邀请按钮 —— 顶部醒目主色按钮
_buildCreateInviteButton(),
const SizedBox(height: 8),
// 我的邀请列表
_buildInviteListSection(inviteProvider),
],
),
);
},
),
);
}
/// 创建邀请按钮 —— 醒目的主色按钮,非卡片样式
Widget _buildCreateInviteButton() {
return Container(
width: double.infinity,
color: AppTheme.cardBackground,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: ElevatedButton.icon(
onPressed: () => _showCreateInviteDialog(),
icon: const Icon(Icons.add, size: 20),
label: const Text('创建新邀请'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
),
);
}
/// 邀请列表区块
Widget _buildInviteListSection(InviteProvider inviteProvider) {
return Container(
color: AppTheme.cardBackground,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 区块标题
const Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text(
'我的邀请',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
if (inviteProvider.invites.isEmpty)
_buildEmptyState()
else
..._buildInviteListItems(inviteProvider.invites),
],
),
);
}
/// 构建邀请列表项(带分隔线)
List<Widget> _buildInviteListItems(List invites) {
final items = <Widget>[];
for (int i = 0; i < invites.length; i++) {
final invite = invites[i];
items.add(_InviteListTile(
invite: invite,
onTap: () => _showInviteDetail(invite),
onDeactivate: () => _deactivateInvite(invite.code),
));
if (i < invites.length - 1) {
items.add(const Padding(
padding: EdgeInsets.only(left: 16),
child: Divider(height: 0.5, thickness: 0.5),
));
}
}
return items;
}
/// 空状态
Widget _buildEmptyState() {
return Padding(
padding: const EdgeInsets.all(32),
child: Center(
child: Column(
children: [
Icon(
Icons.card_giftcard_outlined,
size: 48,
color: AppTheme.textHint,
),
const SizedBox(height: 16),
Text(
'暂无邀请',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14),
),
const SizedBox(height: 8),
Text(
'点击上方按钮创建您的第一个邀请',
style: TextStyle(color: AppTheme.textHint, fontSize: 12),
),
],
),
),
);
}
/// 创建邀请对话框
Future<void> _showCreateInviteDialog() async {
final chatProvider = context.read<ChatProvider>();
var groups = chatProvider.groups;
// 如果没有群组,先创建一个
if (groups.isEmpty) {
final created = await _showCreateGroupDialog();
if (!created) return;
groups = chatProvider.groups;
if (groups.isEmpty) return;
}
String? selectedGroupId = groups.first.id;
int expiryDays = 7;
await showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('创建邀请'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 群组选择
DropdownButtonFormField<String>(
value: selectedGroupId,
decoration: const InputDecoration(
labelText: '选择群组',
border: OutlineInputBorder(),
),
items: groups.map((g) {
return DropdownMenuItem(
value: g.id,
child: Text(g.name),
);
}).toList(),
onChanged: (value) {
setState(() {
selectedGroupId = value;
});
},
),
const SizedBox(height: 16),
// 有效期
Text('有效期(天)', style: Theme.of(context).textTheme.bodySmall),
Slider(
value: expiryDays.toDouble(),
min: 1,
max: 30,
divisions: 29,
label: '$expiryDays',
onChanged: (value) {
setState(() {
expiryDays = value.round();
});
},
),
Text(
'$expiryDays',
style: TextStyle(color: AppTheme.textSecondary),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
final inviteProvider = context.read<InviteProvider>();
Navigator.pop(context);
final invite = await inviteProvider.createInvite(
groupId: selectedGroupId!,
expiresInDays: expiryDays,
);
if (invite != null && mounted) {
_showInviteDetail(invite);
}
},
child: const Text('创建'),
),
],
);
},
),
);
}
/// 显示邀请详情底部弹窗
void _showInviteDetail(Invite invite) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppTheme.cardBackground,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) => _InviteDetailSheet(invite: invite),
);
}
/// 停用邀请确认
Future<void> _deactivateInvite(String code) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('停用邀请'),
content: const Text('确定吗?此邀请将不再可用。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: AppTheme.errorColor),
child: const Text('停用'),
),
],
),
);
if (confirmed == true && mounted) {
final inviteProvider = context.read<InviteProvider>();
await inviteProvider.deactivateInvite(code);
}
}
/// 创建群组对话框(没有群组时自动弹出)
Future<bool> _showCreateGroupDialog() async {
final name = await showDialog<String>(
context: context,
builder: (context) {
final controller = TextEditingController();
return AlertDialog(
title: const Text('创建群组'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('您还没有群组,需要先创建一个群组才能生成邀请。'),
const SizedBox(height: 16),
TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
labelText: '群组名称',
hintText: '例如:客户交流群',
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text.trim()),
child: const Text('创建'),
),
],
);
},
);
if (name == null || name.isEmpty) return false;
try {
final apiService = context.read<ApiService>();
await apiService.createGroup(name);
await context.read<ChatProvider>().loadGroups();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('群组已创建')),
);
}
return true;
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('创建失败:$e')),
);
}
return false;
}
}
}
/// 邀请列表行 —— 卡片背景行,邀请码 + 状态徽章 + 统计
class _InviteListTile extends StatelessWidget {
final Invite invite;
final VoidCallback onTap;
final VoidCallback onDeactivate;
const _InviteListTile({
required this.invite,
required this.onTap,
required this.onDeactivate,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// 左侧:邀请码 + 统计
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:邀请码 + 状态
Row(
children: [
// 邀请码(等宽字体)
Text(
invite.code,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(width: 8),
// 状态徽章
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: invite.isValid
? AppTheme.successColor.withValues(alpha: 0.1)
: AppTheme.errorColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
invite.isValid ? '有效' : '已失效',
style: TextStyle(
fontSize: 11,
color: invite.isValid ? AppTheme.successColor : AppTheme.errorColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 6),
// 第二行:点击数 + 加入数
Row(
children: [
Icon(Icons.touch_app_outlined, size: 14, color: AppTheme.textHint),
const SizedBox(width: 3),
Text(
'${invite.clickCount} 点击',
style: const TextStyle(color: AppTheme.textHint, fontSize: 12),
),
const SizedBox(width: 12),
Icon(Icons.person_add_outlined, size: 14, color: AppTheme.textHint),
const SizedBox(width: 3),
Text(
'${invite.joinCount} 加入',
style: const TextStyle(color: AppTheme.textHint, fontSize: 12),
),
if (invite.expiresAt != null) ...[
const SizedBox(width: 12),
Icon(Icons.schedule, size: 14, color: AppTheme.textHint),
const SizedBox(width: 3),
Text(
dateFormat.format(invite.expiresAt!),
style: const TextStyle(color: AppTheme.textHint, fontSize: 12),
),
],
],
),
],
),
),
// 右侧:删除按钮或箭头
if (!invite.isValid)
GestureDetector(
onTap: onDeactivate,
child: const Padding(
padding: EdgeInsets.all(4),
child: Icon(Icons.delete_outline, size: 20, color: AppTheme.textHint),
),
)
else
const Icon(Icons.chevron_right, color: AppTheme.textHint, size: 20),
],
),
),
);
}
}
/// 邀请详情底部弹窗 —— Twitter 风格
class _InviteDetailSheet extends StatelessWidget {
final Invite invite;
const _InviteDetailSheet({required this.invite});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final inviteUrl = invite.link ?? 'http://localhost:3000/join/${invite.code}';
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 顶部把手
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: AppTheme.dragHandleColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
const Text(
'邀请详情',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 24),
// 二维码
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.cardBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.dividerColor),
),
child: QrImageView(
data: inviteUrl,
size: 200,
backgroundColor: AppTheme.cardBackground,
),
),
const SizedBox(height: 20),
// 统计数据
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_SheetStatItem(label: '点击数', value: '${invite.clickCount}'),
Container(
width: 0.5,
height: 30,
color: AppTheme.dividerColor,
),
_SheetStatItem(label: '加入数', value: '${invite.joinCount}'),
Container(
width: 0.5,
height: 30,
color: AppTheme.dividerColor,
),
_SheetStatItem(
label: '创建时间',
value: invite.createdAt != null
? dateFormat.format(invite.createdAt!)
: '',
),
],
),
const SizedBox(height: 20),
// 邀请链接
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.chatInputBg,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(
child: Text(
inviteUrl,
style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
onTap: () {
// 复制到剪贴板
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'复制',
style: TextStyle(
fontSize: 12,
color: AppTheme.primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 20),
// 操作按钮
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// 分享
},
icon: const Icon(Icons.share, size: 18),
label: const Text('分享'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
// 保存二维码
},
icon: const Icon(Icons.download, size: 18),
label: const Text('保存二维码'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
),
);
}
}
/// 底部弹窗统计项
class _SheetStatItem extends StatelessWidget {
final String label;
final String value;
const _SheetStatItem({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
);
}
}