604 lines
22 KiB
Dart
604 lines
22 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import '../models/ai_tutor.dart';
|
||
|
|
import '../models/conversation.dart';
|
||
|
|
|
||
|
|
/// AI对话页面
|
||
|
|
class AIConversationScreen extends StatefulWidget {
|
||
|
|
final AITutor tutor;
|
||
|
|
|
||
|
|
const AIConversationScreen({
|
||
|
|
super.key,
|
||
|
|
required this.tutor,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<AIConversationScreen> createState() => _AIConversationScreenState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _AIConversationScreenState extends State<AIConversationScreen> {
|
||
|
|
final TextEditingController _messageController = TextEditingController();
|
||
|
|
final ScrollController _scrollController = ScrollController();
|
||
|
|
final List<ConversationMessage> _messages = [];
|
||
|
|
bool _isTyping = false;
|
||
|
|
bool _isRecording = false;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_initializeConversation();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _initializeConversation() {
|
||
|
|
// 添加导师的欢迎消息
|
||
|
|
final welcomeMessage = ConversationMessage(
|
||
|
|
id: 'welcome_${DateTime.now().millisecondsSinceEpoch}',
|
||
|
|
content: _getWelcomeMessage(),
|
||
|
|
type: MessageType.ai,
|
||
|
|
timestamp: DateTime.now(),
|
||
|
|
);
|
||
|
|
|
||
|
|
setState(() {
|
||
|
|
_messages.add(welcomeMessage);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
String _getWelcomeMessage() {
|
||
|
|
switch (widget.tutor.type) {
|
||
|
|
case TutorType.business:
|
||
|
|
return '${widget.tutor.introduction}\n\n让我们开始商务英语对话练习吧!你可以告诉我你的工作背景,或者我们可以模拟一个商务场景。';
|
||
|
|
case TutorType.daily:
|
||
|
|
return '${widget.tutor.introduction}\n\n今天想聊什么呢?我们可以谈论天气、兴趣爱好,或者你今天做了什么有趣的事情!';
|
||
|
|
case TutorType.travel:
|
||
|
|
return '${widget.tutor.introduction}\n\n准备好开始我们的旅行英语之旅了吗?告诉我你想去哪里旅行,或者我们可以模拟在机场、酒店的场景!';
|
||
|
|
case TutorType.academic:
|
||
|
|
return '${widget.tutor.introduction}\n\n欢迎来到学术英语课堂!我们可以讨论你的研究领域,或者练习学术演讲技巧。';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Scaffold(
|
||
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
||
|
|
appBar: AppBar(
|
||
|
|
backgroundColor: widget.tutor.type.color,
|
||
|
|
elevation: 0,
|
||
|
|
leading: IconButton(
|
||
|
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||
|
|
onPressed: () => Navigator.of(context).pop(),
|
||
|
|
),
|
||
|
|
title: Row(
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.tutor.avatar,
|
||
|
|
style: const TextStyle(fontSize: 24),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
Expanded(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.tutor.name,
|
||
|
|
style: const TextStyle(
|
||
|
|
color: Colors.white,
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
widget.tutor.type.displayName,
|
||
|
|
style: const TextStyle(
|
||
|
|
color: Colors.white70,
|
||
|
|
fontSize: 12,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
actions: [
|
||
|
|
IconButton(
|
||
|
|
icon: const Icon(Icons.info_outline, color: Colors.white),
|
||
|
|
onPressed: _showTutorInfo,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
body: Column(
|
||
|
|
children: [
|
||
|
|
Expanded(
|
||
|
|
child: ListView.builder(
|
||
|
|
controller: _scrollController,
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
itemCount: _messages.length + (_isTyping ? 1 : 0),
|
||
|
|
itemBuilder: (context, index) {
|
||
|
|
if (index == _messages.length && _isTyping) {
|
||
|
|
return _buildTypingIndicator();
|
||
|
|
}
|
||
|
|
return _buildMessageBubble(_messages[index]);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
_buildInputArea(),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildMessageBubble(ConversationMessage message) {
|
||
|
|
final isUser = message.type == MessageType.user;
|
||
|
|
|
||
|
|
return Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
if (!isUser) ...[
|
||
|
|
CircleAvatar(
|
||
|
|
radius: 16,
|
||
|
|
backgroundColor: widget.tutor.type.color.withOpacity(0.1),
|
||
|
|
child: Text(
|
||
|
|
widget.tutor.avatar,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
],
|
||
|
|
Flexible(
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: isUser ? widget.tutor.type.color : Colors.white,
|
||
|
|
borderRadius: BorderRadius.circular(20),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.05),
|
||
|
|
blurRadius: 5,
|
||
|
|
offset: const Offset(0, 2),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
message.content,
|
||
|
|
style: TextStyle(
|
||
|
|
color: isUser ? Colors.white : Colors.black87,
|
||
|
|
fontSize: 14,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
_formatTime(message.timestamp),
|
||
|
|
style: TextStyle(
|
||
|
|
color: isUser ? Colors.white70 : Colors.grey,
|
||
|
|
fontSize: 10,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (isUser) ...[
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
CircleAvatar(
|
||
|
|
radius: 16,
|
||
|
|
backgroundColor: Colors.blue.withOpacity(0.1),
|
||
|
|
child: const Icon(
|
||
|
|
Icons.person,
|
||
|
|
size: 16,
|
||
|
|
color: Colors.blue,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildTypingIndicator() {
|
||
|
|
return Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
CircleAvatar(
|
||
|
|
radius: 16,
|
||
|
|
backgroundColor: widget.tutor.type.color.withOpacity(0.1),
|
||
|
|
child: Text(
|
||
|
|
widget.tutor.avatar,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
borderRadius: BorderRadius.circular(20),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.05),
|
||
|
|
blurRadius: 5,
|
||
|
|
offset: const Offset(0, 2),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
_buildDot(0),
|
||
|
|
const SizedBox(width: 4),
|
||
|
|
_buildDot(1),
|
||
|
|
const SizedBox(width: 4),
|
||
|
|
_buildDot(2),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildDot(int index) {
|
||
|
|
return AnimatedContainer(
|
||
|
|
duration: Duration(milliseconds: 600 + (index * 200)),
|
||
|
|
width: 6,
|
||
|
|
height: 6,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: widget.tutor.type.color.withOpacity(0.6),
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildInputArea() {
|
||
|
|
return Container(
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
decoration: const BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black12,
|
||
|
|
blurRadius: 5,
|
||
|
|
offset: Offset(0, -2),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
GestureDetector(
|
||
|
|
onTapDown: (_) => _startRecording(),
|
||
|
|
onTapUp: (_) => _stopRecording(),
|
||
|
|
onTapCancel: () => _stopRecording(),
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.all(12),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: _isRecording ? Colors.red : widget.tutor.type.color,
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
),
|
||
|
|
child: Icon(
|
||
|
|
_isRecording ? Icons.stop : Icons.mic,
|
||
|
|
color: Colors.white,
|
||
|
|
size: 20,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
Expanded(
|
||
|
|
child: TextField(
|
||
|
|
controller: _messageController,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
hintText: '输入消息...',
|
||
|
|
border: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(25),
|
||
|
|
borderSide: BorderSide.none,
|
||
|
|
),
|
||
|
|
filled: true,
|
||
|
|
fillColor: Colors.grey[100],
|
||
|
|
contentPadding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 16,
|
||
|
|
vertical: 12,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
onSubmitted: (_) => _sendMessage(),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: _sendMessage,
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.all(12),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: widget.tutor.type.color,
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
),
|
||
|
|
child: const Icon(
|
||
|
|
Icons.send,
|
||
|
|
color: Colors.white,
|
||
|
|
size: 20,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _sendMessage() {
|
||
|
|
final text = _messageController.text.trim();
|
||
|
|
if (text.isEmpty) return;
|
||
|
|
|
||
|
|
// 添加用户消息
|
||
|
|
final userMessage = ConversationMessage(
|
||
|
|
id: 'user_${DateTime.now().millisecondsSinceEpoch}',
|
||
|
|
content: text,
|
||
|
|
type: MessageType.user,
|
||
|
|
timestamp: DateTime.now(),
|
||
|
|
);
|
||
|
|
|
||
|
|
setState(() {
|
||
|
|
_messages.add(userMessage);
|
||
|
|
_isTyping = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
_messageController.clear();
|
||
|
|
_scrollToBottom();
|
||
|
|
|
||
|
|
// 生成AI回复
|
||
|
|
_generateAIResponse(text);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _generateAIResponse(String userMessage) {
|
||
|
|
// 模拟AI思考时间
|
||
|
|
Future.delayed(const Duration(milliseconds: 1500), () {
|
||
|
|
final aiResponse = _getAIResponse(userMessage);
|
||
|
|
|
||
|
|
final aiMessage = ConversationMessage(
|
||
|
|
id: 'ai_${DateTime.now().millisecondsSinceEpoch}',
|
||
|
|
content: aiResponse,
|
||
|
|
type: MessageType.ai,
|
||
|
|
timestamp: DateTime.now(),
|
||
|
|
);
|
||
|
|
|
||
|
|
setState(() {
|
||
|
|
_isTyping = false;
|
||
|
|
_messages.add(aiMessage);
|
||
|
|
});
|
||
|
|
|
||
|
|
_scrollToBottom();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
String _getAIResponse(String userMessage) {
|
||
|
|
final message = userMessage.toLowerCase();
|
||
|
|
|
||
|
|
// 根据导师类型和用户消息生成相应回复
|
||
|
|
switch (widget.tutor.type) {
|
||
|
|
case TutorType.business:
|
||
|
|
return _getBusinessResponse(message);
|
||
|
|
case TutorType.daily:
|
||
|
|
return _getDailyResponse(message);
|
||
|
|
case TutorType.travel:
|
||
|
|
return _getTravelResponse(message);
|
||
|
|
case TutorType.academic:
|
||
|
|
return _getAcademicResponse(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
String _getBusinessResponse(String message) {
|
||
|
|
if (message.contains('meeting') || message.contains('会议')) {
|
||
|
|
return "Great! Let's discuss meeting preparation. What type of meeting are you attending? Is it a client presentation, team meeting, or board meeting?";
|
||
|
|
} else if (message.contains('presentation') || message.contains('演讲')) {
|
||
|
|
return "Presentations are crucial in business. What's your presentation topic? I can help you structure your content and practice key phrases.";
|
||
|
|
} else if (message.contains('email') || message.contains('邮件')) {
|
||
|
|
return "Business emails require professional tone. Are you writing to a client, colleague, or supervisor? What's the main purpose of your email?";
|
||
|
|
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
|
||
|
|
return "Hello! I'm your business English tutor. I can help you with presentations, meetings, negotiations, and professional communication. What would you like to practice today?";
|
||
|
|
} else {
|
||
|
|
return "That's an interesting point. In business context, we should consider the professional implications. Could you elaborate on your specific business scenario?";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
String _getDailyResponse(String message) {
|
||
|
|
if (message.contains('weather') || message.contains('天气')) {
|
||
|
|
return "The weather is a great conversation starter! How's the weather where you are? You can say 'It's sunny/rainy/cloudy today' or 'What a beautiful day!'";
|
||
|
|
} else if (message.contains('food') || message.contains('吃') || message.contains('饭')) {
|
||
|
|
return "Food is always a fun topic! What's your favorite cuisine? You can practice ordering food or describing flavors. Try saying 'I'd like to order...' or 'This tastes delicious!'";
|
||
|
|
} else if (message.contains('hobby') || message.contains('爱好')) {
|
||
|
|
return "Hobbies are personal and interesting! What do you enjoy doing in your free time? You can say 'I enjoy...' or 'My hobby is...' or 'In my spare time, I like to...'";
|
||
|
|
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
|
||
|
|
return "Hi there! I'm here to help you with everyday English conversations. We can talk about weather, food, hobbies, shopping, or any daily activities. What interests you?";
|
||
|
|
} else {
|
||
|
|
return "That's interesting! In daily conversations, we often share personal experiences. Can you tell me more about that? It's great practice for natural English!";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
String _getTravelResponse(String message) {
|
||
|
|
if (message.contains('airport') || message.contains('flight') || message.contains('机场')) {
|
||
|
|
return "Airport conversations are essential for travelers! Are you checking in, going through security, or asking for directions? Try phrases like 'Where is gate B12?' or 'Is this flight delayed?'";
|
||
|
|
} else if (message.contains('hotel') || message.contains('酒店')) {
|
||
|
|
return "Hotel interactions are important! Are you checking in, asking about amenities, or reporting an issue? Practice saying 'I have a reservation under...' or 'Could you help me with...'";
|
||
|
|
} else if (message.contains('restaurant') || message.contains('餐厅')) {
|
||
|
|
return "Dining out while traveling is fun! Are you making a reservation, ordering food, or asking about local specialties? Try 'Table for two, please' or 'What do you recommend?'";
|
||
|
|
} else if (message.contains('direction') || message.contains('路')) {
|
||
|
|
return "Getting directions is crucial when traveling! Practice asking 'How do I get to...?' or 'Is it walking distance?' You can also say 'Could you show me on the map?'";
|
||
|
|
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
|
||
|
|
return "Welcome, fellow traveler! I'm your travel English companion. I can help you with airport conversations, hotel bookings, restaurant orders, and asking for directions. Where shall we start?";
|
||
|
|
} else {
|
||
|
|
return "That sounds like a great travel experience! When traveling, it's important to communicate clearly. Can you describe the situation in more detail? I'll help you with the right phrases!";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
String _getAcademicResponse(String message) {
|
||
|
|
if (message.contains('research') || message.contains('研究')) {
|
||
|
|
return "Research is fundamental in academics! What's your research area? We can practice presenting findings, discussing methodology, or explaining complex concepts clearly.";
|
||
|
|
} else if (message.contains('presentation') || message.contains('论文')) {
|
||
|
|
return "Academic presentations require clear structure and precise language. Are you presenting research results, defending a thesis, or giving a conference talk? Let's work on your key points!";
|
||
|
|
} else if (message.contains('discussion') || message.contains('讨论')) {
|
||
|
|
return "Academic discussions involve critical thinking and evidence-based arguments. What topic are you discussing? Practice phrases like 'According to the research...' or 'The evidence suggests...'";
|
||
|
|
} else if (message.contains('writing') || message.contains('写作')) {
|
||
|
|
return "Academic writing has specific conventions. Are you working on an essay, research paper, or thesis? I can help with structure, citations, and formal language.";
|
||
|
|
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
|
||
|
|
return "Greetings! I'm your academic English tutor. I specialize in research discussions, academic presentations, scholarly writing, and conference communications. What academic skill would you like to develop?";
|
||
|
|
} else {
|
||
|
|
return "That's a thoughtful academic point. In scholarly discourse, we need to support our arguments with evidence. Could you provide more context or examples to strengthen your position?";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _startRecording() {
|
||
|
|
setState(() {
|
||
|
|
_isRecording = true;
|
||
|
|
});
|
||
|
|
// TODO: 实现语音录制功能
|
||
|
|
}
|
||
|
|
|
||
|
|
void _stopRecording() {
|
||
|
|
setState(() {
|
||
|
|
_isRecording = false;
|
||
|
|
});
|
||
|
|
// TODO: 处理录制的语音
|
||
|
|
}
|
||
|
|
|
||
|
|
void _scrollToBottom() {
|
||
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
|
|
if (_scrollController.hasClients) {
|
||
|
|
_scrollController.animateTo(
|
||
|
|
_scrollController.position.maxScrollExtent,
|
||
|
|
duration: const Duration(milliseconds: 300),
|
||
|
|
curve: Curves.easeOut,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void _showTutorInfo() {
|
||
|
|
showModalBottomSheet(
|
||
|
|
context: context,
|
||
|
|
isScrollControlled: true,
|
||
|
|
backgroundColor: Colors.transparent,
|
||
|
|
builder: (context) => Container(
|
||
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
||
|
|
decoration: const BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
width: 40,
|
||
|
|
height: 4,
|
||
|
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.grey[300],
|
||
|
|
borderRadius: BorderRadius.circular(2),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Expanded(
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(20),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.tutor.avatar,
|
||
|
|
style: const TextStyle(fontSize: 40),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 16),
|
||
|
|
Expanded(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.tutor.name,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 20,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
widget.tutor.type.displayName,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
color: widget.tutor.type.color,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
Text(
|
||
|
|
'个性特点',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
color: widget.tutor.type.color,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Text(
|
||
|
|
widget.tutor.personality,
|
||
|
|
style: const TextStyle(fontSize: 14),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
Text(
|
||
|
|
'专业领域',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
color: widget.tutor.type.color,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Wrap(
|
||
|
|
spacing: 8,
|
||
|
|
runSpacing: 8,
|
||
|
|
children: widget.tutor.specialties.map((specialty) {
|
||
|
|
return Container(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 12,
|
||
|
|
vertical: 6,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: widget.tutor.type.color.withOpacity(0.1),
|
||
|
|
borderRadius: BorderRadius.circular(16),
|
||
|
|
border: Border.all(
|
||
|
|
color: widget.tutor.type.color.withOpacity(0.3),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Text(
|
||
|
|
specialty,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 12,
|
||
|
|
color: widget.tutor.type.color,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}).toList(),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
String _formatTime(DateTime time) {
|
||
|
|
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_messageController.dispose();
|
||
|
|
_scrollController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
}
|