896 lines
28 KiB
Dart
896 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:sales_chat/providers/chat_provider.dart';
|
||
import 'package:sales_chat/providers/auth_provider.dart';
|
||
import 'package:sales_chat/models/group.dart';
|
||
import 'package:sales_chat/models/message.dart';
|
||
import 'package:sales_chat/widgets/user_avatar.dart' as custom;
|
||
import 'package:sales_chat/theme/app_theme.dart';
|
||
|
||
/// 聊天页面 —— 微信风格
|
||
class ChatPage extends StatefulWidget {
|
||
const ChatPage({super.key});
|
||
|
||
@override
|
||
State<ChatPage> createState() => _ChatPageState();
|
||
}
|
||
|
||
class _ChatPageState extends State<ChatPage> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
AppBar(
|
||
title: const Text('聊天'),
|
||
),
|
||
Expanded(
|
||
child: Consumer<ChatProvider>(
|
||
builder: (context, chatProvider, _) {
|
||
if (chatProvider.isLoading && chatProvider.groups.isEmpty) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
if (chatProvider.groups.isEmpty) {
|
||
return _buildEmptyState();
|
||
}
|
||
|
||
if (chatProvider.currentGroup != null) {
|
||
return _buildChatView(chatProvider);
|
||
}
|
||
|
||
return _buildGroupList(chatProvider);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 空状态占位
|
||
Widget _buildEmptyState() {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.chat_bubble_outline,
|
||
size: 64,
|
||
color: AppTheme.textHint,
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'暂无群聊',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
color: AppTheme.textSecondary,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'请联系管理员将您加入群聊',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: AppTheme.textHint,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 群组列表 —— 微信会话列表风格
|
||
/// 白底行 + 细分割线,无卡片阴影
|
||
Widget _buildGroupList(ChatProvider chatProvider) {
|
||
return Container(
|
||
color: AppTheme.cardBackground,
|
||
child: ListView.separated(
|
||
// 去掉顶部和底部多余间距
|
||
padding: EdgeInsets.zero,
|
||
itemCount: chatProvider.groups.length,
|
||
// 每行之间用细分割线
|
||
separatorBuilder: (_, __) => const Divider(
|
||
height: 0.5,
|
||
thickness: 0.5,
|
||
indent: 76, // 头像右侧开始
|
||
color: AppTheme.dividerColor,
|
||
),
|
||
itemBuilder: (context, index) {
|
||
final group = chatProvider.groups[index];
|
||
return _WeChatGroupRow(
|
||
group: group,
|
||
onTap: () => chatProvider.selectGroup(group),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 聊天视图 —— 聊天界面
|
||
Widget _buildChatView(ChatProvider chatProvider) {
|
||
final group = chatProvider.currentGroup!;
|
||
final authProvider = context.read<AuthProvider>();
|
||
|
||
return Column(
|
||
children: [
|
||
// 聊天顶栏
|
||
Container(
|
||
color: AppTheme.cardBackground,
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(Icons.arrow_back_ios, size: 20),
|
||
onPressed: () => chatProvider.deselectGroup(),
|
||
),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => _showGroupInfo(context, group),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
group.name,
|
||
style: const TextStyle(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
Text(
|
||
'${group.memberCount}位成员',
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
color: AppTheme.textHint,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.more_horiz, size: 22),
|
||
onPressed: () => _showGroupInfo(context, group),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 0.5, thickness: 0.5),
|
||
Expanded(
|
||
child: _ChatMessagesList(
|
||
messages: chatProvider.currentMessages,
|
||
currentUserId: authProvider.user?.id ?? '',
|
||
onLoadMore: () {
|
||
// 加载更多历史消息
|
||
},
|
||
),
|
||
),
|
||
_ChatInput(
|
||
onSend: (text) {
|
||
chatProvider.sendMessage(text);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 显示群信息底部弹窗
|
||
void _showGroupInfo(BuildContext context, Group group) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
backgroundColor: AppTheme.cardBackground,
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||
),
|
||
builder: (context) => _GroupInfoSheet(group: group),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 群组列表行 —— 微信会话列表风格
|
||
// ================================================================
|
||
class _WeChatGroupRow extends StatelessWidget {
|
||
final Group group;
|
||
final VoidCallback onTap;
|
||
|
||
const _WeChatGroupRow({
|
||
required this.group,
|
||
required this.onTap,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return InkWell(
|
||
onTap: onTap,
|
||
// 点击水波纹颜色
|
||
splashColor: AppTheme.dividerColor,
|
||
highlightColor: AppTheme.timestampBg,
|
||
child: Container(
|
||
color: AppTheme.cardBackground,
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
child: Row(
|
||
children: [
|
||
// 左侧群头像
|
||
custom.GroupAvatar(
|
||
groupName: group.name,
|
||
radius: 24,
|
||
),
|
||
const SizedBox(width: 12),
|
||
// 右侧群名 + 成员数
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
group.name,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w500,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'${group.memberCount}位成员',
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: AppTheme.textHint,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 右侧箭头图标
|
||
Icon(
|
||
Icons.chevron_right,
|
||
color: AppTheme.textHint,
|
||
size: 20,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 消息列表
|
||
// ================================================================
|
||
class _ChatMessagesList extends StatelessWidget {
|
||
final List<Message> messages;
|
||
final String currentUserId;
|
||
final VoidCallback? onLoadMore;
|
||
|
||
const _ChatMessagesList({
|
||
required this.messages,
|
||
required this.currentUserId,
|
||
this.onLoadMore,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (messages.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.chat_bubble_outline,
|
||
size: 48,
|
||
color: AppTheme.textHint,
|
||
),
|
||
const SizedBox(height: 12),
|
||
Text(
|
||
'暂无消息',
|
||
style: TextStyle(color: AppTheme.textSecondary),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'发送第一条消息开始聊天吧',
|
||
style: TextStyle(color: AppTheme.textHint, fontSize: 12),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
color: AppTheme.scaffoldBackground,
|
||
child: ListView.builder(
|
||
reverse: true,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
itemCount: messages.length,
|
||
itemBuilder: (context, index) {
|
||
final message = messages[index];
|
||
final isMe = message.senderId == currentUserId;
|
||
|
||
// 滚动到顶部时加载更多
|
||
if (index == messages.length - 1 && onLoadMore != null) {
|
||
onLoadMore!();
|
||
}
|
||
|
||
// 判断是否需要显示时间戳(与下一条消息间隔超过5分钟)
|
||
final bool showTimestamp;
|
||
if (index == messages.length - 1) {
|
||
// 最旧的消息,始终显示时间
|
||
showTimestamp = true;
|
||
} else {
|
||
final nextMessage = messages[index + 1];
|
||
if (message.createdAt != null && nextMessage.createdAt != null) {
|
||
final diff = message.createdAt!
|
||
.difference(nextMessage.createdAt!)
|
||
.inMinutes;
|
||
showTimestamp = diff.abs() >= 5;
|
||
} else {
|
||
showTimestamp = false;
|
||
}
|
||
}
|
||
|
||
return _MessageBubble(
|
||
message: message,
|
||
isMe: isMe,
|
||
showTimestamp: showTimestamp,
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 消息气泡 —— 微信风格
|
||
// ================================================================
|
||
class _MessageBubble extends StatelessWidget {
|
||
final Message message;
|
||
final bool isMe;
|
||
final bool showTimestamp;
|
||
|
||
const _MessageBubble({
|
||
required this.message,
|
||
required this.isMe,
|
||
this.showTimestamp = false,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 撤回消息:居中灰色斜体
|
||
if (message.hasRecall) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Column(
|
||
children: [
|
||
if (showTimestamp && message.createdAt != null)
|
||
_buildTimestamp(),
|
||
const SizedBox(height: 4),
|
||
Center(
|
||
child: Text(
|
||
'消息已撤回',
|
||
style: TextStyle(
|
||
color: AppTheme.textHint,
|
||
fontSize: 12,
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Column(
|
||
children: [
|
||
// 时间戳居中显示在消息上方
|
||
if (showTimestamp && message.createdAt != null)
|
||
_buildTimestamp(),
|
||
const SizedBox(height: 4),
|
||
// 消息行
|
||
GestureDetector(
|
||
onLongPress: () => _showMessageOptions(context),
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 对方消息:左侧头像
|
||
if (!isMe) ...[
|
||
custom.UserAvatar(
|
||
displayName:
|
||
message.senderName.isNotEmpty ? message.senderName : 'U',
|
||
radius: 16,
|
||
),
|
||
const SizedBox(width: 8),
|
||
],
|
||
// 气泡内容
|
||
Flexible(
|
||
child: Column(
|
||
crossAxisAlignment:
|
||
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||
children: [
|
||
// 对方消息:发送者名称(群聊显示)
|
||
if (!isMe && message.senderName.isNotEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 2, left: 2),
|
||
child: Text(
|
||
message.senderName,
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
color: AppTheme.textHint,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
),
|
||
),
|
||
// 气泡
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12, vertical: 9),
|
||
decoration: BoxDecoration(
|
||
// 自己:微信绿气泡;对方:白色气泡
|
||
color: isMe
|
||
? AppTheme.bubbleSelf
|
||
: AppTheme.bubbleOther,
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: const Radius.circular(12),
|
||
topRight: const Radius.circular(12),
|
||
// 微信风格:外角小圆角,内角大圆角
|
||
bottomLeft:
|
||
Radius.circular(isMe ? 12 : 4),
|
||
bottomRight:
|
||
Radius.circular(isMe ? 4 : 12),
|
||
),
|
||
// 无阴影,干净清爽
|
||
),
|
||
child: Text(
|
||
message.content,
|
||
style: TextStyle(
|
||
// 己方蓝色气泡用白字,对方灰色气泡用深色字
|
||
color: isMe ? Colors.white : AppTheme.textPrimary,
|
||
fontSize: 15,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 自己消息:右侧留出间距(对称对方头像)
|
||
if (isMe) const SizedBox(width: 8),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 居中时间戳
|
||
Widget _buildTimestamp() {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 4, bottom: 2),
|
||
child: Center(
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.timestampBg,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
_formatTime(message.createdAt!),
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
color: AppTheme.timestampText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 长按消息菜单
|
||
void _showMessageOptions(BuildContext context) {
|
||
final authProvider = context.read<AuthProvider>();
|
||
final chatProvider = context.read<ChatProvider>();
|
||
final isAdmin = authProvider.user?.isAdmin ?? false;
|
||
final canRecall = isMe && _isRecallable(message.createdAt);
|
||
|
||
showModalBottomSheet(
|
||
context: context,
|
||
backgroundColor: AppTheme.cardBackground,
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||
),
|
||
builder: (context) => SafeArea(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 顶部拖拽指示条
|
||
Container(
|
||
margin: const EdgeInsets.only(top: 8, bottom: 4),
|
||
width: 36,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.dragHandleColor,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
if (canRecall)
|
||
ListTile(
|
||
leading: Icon(Icons.undo, color: AppTheme.primaryColor),
|
||
title: const Text('撤回消息'),
|
||
subtitle: const Text('其他人将不再看到此消息'),
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
_handleRecall(context, chatProvider);
|
||
},
|
||
),
|
||
if (isAdmin && !canRecall)
|
||
ListTile(
|
||
leading: const Icon(Icons.delete, color: Colors.red),
|
||
title:
|
||
const Text('删除消息', style: TextStyle(color: Colors.red)),
|
||
subtitle: const Text('管理员删除消息'),
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
_handleDelete(context, chatProvider);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: Icon(Icons.copy, color: AppTheme.textSecondary),
|
||
title: const Text('复制'),
|
||
onTap: () {
|
||
Clipboard.setData(ClipboardData(text: message.content));
|
||
Navigator.pop(context);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('已复制到剪贴板')),
|
||
);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading:
|
||
Icon(Icons.cancel_outlined, color: AppTheme.textHint),
|
||
title: const Text('取消'),
|
||
onTap: () => Navigator.pop(context),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 判断消息是否可撤回(2分钟内)
|
||
bool _isRecallable(DateTime? createdAt) {
|
||
if (createdAt == null) return false;
|
||
final now = DateTime.now();
|
||
final diff = now.difference(createdAt);
|
||
return diff.inMinutes < 2;
|
||
}
|
||
|
||
/// 撤回消息
|
||
Future<void> _handleRecall(
|
||
BuildContext context, ChatProvider chatProvider) 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),
|
||
child: const Text('撤回'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed == true) {
|
||
final success = await chatProvider.recallMessage(message.id);
|
||
if (context.mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(success ? '消息已撤回' : (chatProvider.error ?? '撤回失败')),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 删除消息(管理员)
|
||
Future<void> _handleDelete(
|
||
BuildContext context, ChatProvider chatProvider) 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: Colors.red),
|
||
child: const Text('删除'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed == true) {
|
||
final success = await chatProvider.deleteMessage(message.id);
|
||
if (context.mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(success ? '消息已删除' : (chatProvider.error ?? '删除失败')),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 格式化时间
|
||
String _formatTime(DateTime time) {
|
||
final now = DateTime.now();
|
||
final today = DateTime(now.year, now.month, now.day);
|
||
final yesterday = today.subtract(const Duration(days: 1));
|
||
final messageDate = DateTime(time.year, time.month, time.day);
|
||
|
||
final hour = time.hour.toString().padLeft(2, '0');
|
||
final minute = time.minute.toString().padLeft(2, '0');
|
||
|
||
if (messageDate == today) {
|
||
return '$hour:$minute';
|
||
} else if (messageDate == yesterday) {
|
||
return '昨天 $hour:$minute';
|
||
} else if (now.year == time.year) {
|
||
return '${time.month}月${time.day}日 $hour:$minute';
|
||
} else {
|
||
return '${time.year}年${time.month}月${time.day}日 $hour:$minute';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 聊天输入栏 —— 微信风格
|
||
// ================================================================
|
||
class _ChatInput extends StatefulWidget {
|
||
final Function(String) onSend;
|
||
|
||
const _ChatInput({required this.onSend});
|
||
|
||
@override
|
||
State<_ChatInput> createState() => _ChatInputState();
|
||
}
|
||
|
||
class _ChatInputState extends State<_ChatInput> {
|
||
final _controller = TextEditingController();
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _handleSend() {
|
||
final text = _controller.text.trim();
|
||
if (text.isNotEmpty) {
|
||
widget.onSend(text);
|
||
_controller.clear();
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: const BoxDecoration(
|
||
color: AppTheme.chatInputBg,
|
||
border: Border(
|
||
top: BorderSide(
|
||
color: AppTheme.dividerColor,
|
||
width: 0.5,
|
||
),
|
||
),
|
||
),
|
||
padding: EdgeInsets.only(
|
||
left: 12,
|
||
right: 12,
|
||
top: 8,
|
||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
// 输入框
|
||
Expanded(
|
||
child: Container(
|
||
constraints: const BoxConstraints(
|
||
minHeight: 36,
|
||
maxHeight: 120,
|
||
),
|
||
child: TextField(
|
||
controller: _controller,
|
||
maxLines: null,
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
decoration: InputDecoration(
|
||
hintText: '输入消息...',
|
||
hintStyle: const TextStyle(
|
||
color: AppTheme.textHint,
|
||
fontSize: 15,
|
||
),
|
||
filled: true,
|
||
fillColor: AppTheme.cardBackground,
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide.none,
|
||
),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 8,
|
||
),
|
||
isDense: true,
|
||
),
|
||
onSubmitted: (_) => _handleSend(),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
// 发送按钮:微信绿圆形
|
||
GestureDetector(
|
||
onTap: _handleSend,
|
||
child: Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: const BoxDecoration(
|
||
color: AppTheme.primaryColor,
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: const Icon(
|
||
Icons.send_rounded,
|
||
color: Colors.white,
|
||
size: 18,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 群信息底部弹窗 —— 微信风格,干净白底
|
||
// ================================================================
|
||
class _GroupInfoSheet extends StatelessWidget {
|
||
final Group group;
|
||
|
||
const _GroupInfoSheet({required this.group});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||
decoration: const BoxDecoration(
|
||
color: AppTheme.cardBackground,
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 顶部拖拽指示条
|
||
Container(
|
||
width: 36,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 20),
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.dragHandleColor,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
// 群头像
|
||
custom.GroupAvatar(
|
||
groupName: group.name,
|
||
radius: 40,
|
||
),
|
||
const SizedBox(height: 12),
|
||
// 群名称
|
||
Text(
|
||
group.name,
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
// 成员数
|
||
Text(
|
||
'${group.memberCount}位成员',
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
color: AppTheme.textHint,
|
||
),
|
||
),
|
||
// 群描述
|
||
if (group.description != null &&
|
||
group.description!.isNotEmpty) ...[
|
||
const SizedBox(height: 12),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.chatInputBg,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Text(
|
||
group.description!,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
color: AppTheme.textSecondary,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
// 频道列表
|
||
if (group.panels.isNotEmpty) ...[
|
||
const SizedBox(height: 16),
|
||
const Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
'频道',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: group.panels
|
||
.where((p) => p.type == 0)
|
||
.map((p) => Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.chatInputBg,
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.tag,
|
||
size: 14, color: AppTheme.textHint),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
p.name,
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: AppTheme.textPrimary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
))
|
||
.toList(),
|
||
),
|
||
],
|
||
const SizedBox(height: 24),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|