feat(apps/home): 新增 HomeScreen 录音交互与导航组件

This commit is contained in:
zl-q
2026-03-19 00:51:52 +08:00
parent 14ccf2cb28
commit 039e8b73d6
13 changed files with 1840 additions and 847 deletions
@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/message_composer.dart';
import 'home_attachment_strip.dart';
class HomeComposerStack extends StatelessWidget {
const HomeComposerStack({
super.key,
required this.selectedImages,
required this.onRemoveImage,
required this.isHoldToSpeakMode,
required this.isRecording,
required this.isCancelGestureActive,
required this.isTranscribing,
required this.isWaitingAgent,
required this.messageController,
required this.messageFocusNode,
required this.onTapPlus,
required this.onTapRightAction,
required this.onHoldToSpeakStart,
required this.onHoldToSpeakEnd,
required this.onHoldToSpeakMoveUpdate,
required this.onHoldToSpeakCancel,
required this.onTextFieldTap,
required this.onSubmit,
});
final List<XFile> selectedImages;
final ValueChanged<int> onRemoveImage;
final bool isHoldToSpeakMode;
final bool isRecording;
final bool isCancelGestureActive;
final bool isTranscribing;
final bool isWaitingAgent;
final TextEditingController messageController;
final FocusNode messageFocusNode;
final VoidCallback onTapPlus;
final VoidCallback onTapRightAction;
final VoidCallback onHoldToSpeakStart;
final VoidCallback onHoldToSpeakEnd;
final ValueChanged<LongPressMoveUpdateDetails> onHoldToSpeakMoveUpdate;
final VoidCallback onHoldToSpeakCancel;
final VoidCallback onTextFieldTap;
final VoidCallback onSubmit;
@override
Widget build(BuildContext context) {
final process = isRecording
? MessageComposerProcess.recording
: isTranscribing
? MessageComposerProcess.transcribing
: MessageComposerProcess.idle;
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: KeyedSubtree(
key: const ValueKey('home_bottom_input_stack'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
HomeAttachmentStrip(
images: selectedImages,
onRemove: onRemoveImage,
),
if (selectedImages.isNotEmpty)
const SizedBox(height: AppSpacing.sm),
ValueListenableBuilder<TextEditingValue>(
valueListenable: messageController,
builder: (context, value, child) {
final hasMessage = value.text.trim().isNotEmpty;
return MessageComposer(
mode: isHoldToSpeakMode
? MessageComposerMode.holdToSpeak
: MessageComposerMode.text,
process: process,
hasMessage: hasMessage,
isWaitingAgent: isWaitingAgent,
iconSize: 24,
composerMinHeight: AppSpacing.xxl + AppSpacing.lg,
onTapPlus: onTapPlus,
onTapRightAction: onTapRightAction,
onHoldToSpeakStart: onHoldToSpeakStart,
onHoldToSpeakEnd: onHoldToSpeakEnd,
onHoldToSpeakMoveUpdate: onHoldToSpeakMoveUpdate,
onHoldToSpeakCancel: onHoldToSpeakCancel,
textInputChild: _buildTextInputContent(),
recordingAnimation: const SizedBox.shrink(),
recordingText: isCancelGestureActive ? '松手取消' : '松手发送',
recordingHintText: isCancelGestureActive
? '松开取消'
: '松开发送,上滑取消',
showRecordingInlineFeedback: false,
);
},
),
],
),
),
),
);
}
Widget _buildTextInputContent() {
if (isTranscribing) {
return _buildTranscribingIndicator();
}
return SizedBox.expand(
child: Align(
alignment: Alignment.centerLeft,
child: TextField(
controller: messageController,
focusNode: messageFocusNode,
minLines: 1,
maxLines: 1,
style: const TextStyle(
fontSize: AppSpacing.lg,
height: 1,
color: AppColors.slate900,
),
textAlignVertical: TextAlignVertical.center,
decoration: const InputDecoration(
hintText: '输入消息...',
hintStyle: TextStyle(
fontSize: AppSpacing.lg,
height: 1,
color: AppColors.slate400,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
isCollapsed: true,
contentPadding: EdgeInsets.zero,
filled: false,
),
onTap: onTextFieldTap,
onSubmitted: (_) => onSubmit(),
),
),
);
}
Widget _buildTranscribingIndicator() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 18,
height: 18,
child: const AppLoadingIndicator(
variant: AppLoadingVariant.inline,
size: 18,
strokeWidth: 2,
color: AppColors.blue600,
trackColor: AppColors.blue100,
),
),
const SizedBox(width: AppSpacing.sm),
_buildWaveDots(),
const SizedBox(width: AppSpacing.sm),
const Expanded(
child: Text(
'语音识别中...',
style: TextStyle(
fontSize: 14,
color: AppColors.blue600,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildWaveDots() {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(3, (index) {
return Container(
margin: const EdgeInsets.only(right: 3),
width: 3,
height: 6 + index * 2,
decoration: BoxDecoration(
color: AppColors.blue500,
borderRadius: BorderRadius.circular(2),
),
);
}),
);
}
}