feat: Implement character avatar upload and refactor character form fields and API calls.

This commit is contained in:
liqupan
2026-02-02 22:48:11 +08:00
parent 6c32d845a7
commit dec5748cca
15 changed files with 369 additions and 197 deletions

View File

@@ -71,8 +71,9 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
// 使用 reverse: true 后0.0 就是列表底部(最新消息处)
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
@@ -158,11 +159,20 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
final avatarUrl = CharacterRepository.getAvatarUrl(_character!.avatarPath);
return Stack(
children: [
Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
return Container(
decoration: const BoxDecoration(
color: Color(0xFF2E1065),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF2E1065), Color(0xFF0F172A)],
),
),
child: Stack(
children: [
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
flexibleSpace: ClipRRect(
@@ -229,30 +239,27 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
),
],
),
body: Container(
decoration: const BoxDecoration(
color: Color(0xFF2E1065),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF2E1065), Color(0xFF0F172A)],
),
),
child: Column(
children: [
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Column(
children: [
Expanded(
child: ListView.separated(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
reverse: true,
controller: _scrollController,
padding: const EdgeInsets.only(top: 120, bottom: 20, left: 16, right: 16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
itemCount: _messages.length + (_isTyping ? 1 : 0),
separatorBuilder: (_, __) => const SizedBox(height: 16),
itemBuilder: (context, index) {
// 如果是正在输入的消息
if (_isTyping && index == _messages.length) {
return _buildTypingBubble();
// reverse: true 模式下,索引 0 是列表的最底部
if (_isTyping) {
if (index == 0) return _buildTypingBubble();
final msg = _messages[_messages.length - index];
return _buildMessageBubble(msg, avatarUrl);
}
final msg = _messages[index];
final msg = _messages[_messages.length - 1 - index];
return _buildMessageBubble(msg, avatarUrl);
},
),
@@ -263,15 +270,16 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
],
),
),
),
if (_isVoiceMode && _character != null)
VoiceModeOverlay(
character: _character!,
onClose: () => setState(() => _isVoiceMode = false),
),
],
if (_isVoiceMode && _character != null)
VoiceModeOverlay(
character: _character!,
onClose: () => setState(() => _isVoiceMode = false),
),
],
),
);
}
@@ -397,7 +405,10 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
child: Row(
children: [
GestureDetector(
onTap: () => setState(() => _isVoiceMode = true),
onTap: () {
FocusScope.of(context).unfocus();
setState(() => _isVoiceMode = true);
},
child: Container(
width: 44,
height: 44,