优化
This commit is contained in:
895
client/flutter/lib/pages/chat_page.dart
Normal file
895
client/flutter/lib/pages/chat_page.dart
Normal file
@@ -0,0 +1,895 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user