669 lines
21 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|