feat: 重构 Home Screen 视觉设计与消息输入组件

- 新增 Home Screen 视觉设计 token (背景、工具栏、对话区、输入框等)
- 重构首页布局为浮动式底部输入栈结构
- 新增 HomeBackgroundField、HomeFloatingHeader、HomeAttachmentStrip 组件
- 优化 MessageComposer 视觉样式为悬浮 shell 设计
- 添加相关测试用例
This commit is contained in:
qzl
2026-03-13 17:25:29 +08:00
parent 4c10929498
commit 3273d63b23
10 changed files with 1212 additions and 259 deletions
+75 -62
View File
@@ -9,6 +9,8 @@ enum MessageComposerMode { text, holdToSpeak }
enum MessageComposerProcess { idle, recording, transcribing }
const messageComposerContainerKey = ValueKey('message_composer_container');
const messageComposerShellKey = ValueKey('message_composer_shell');
const messageComposerInnerKey = ValueKey('message_composer_inner');
const messageComposerPlusButtonKey = ValueKey('message_composer_plus_button');
const messageComposerRightButtonKey = ValueKey('message_composer_right_button');
const messageComposerHoldAreaKey = ValueKey('message_composer_hold_area');
@@ -35,7 +37,7 @@ class MessageComposer extends StatelessWidget {
required this.textInputChild,
required this.recordingAnimation,
this.holdToSpeakText = '按住说话',
this.recordingText = '发送',
this.recordingText = '发送',
this.transcribingText = '语音识别中...',
this.recordingHintText = '松开发送,上滑取消',
this.showRecordingInlineFeedback = true,
@@ -69,71 +71,77 @@ class MessageComposer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
return KeyedSubtree(
key: messageComposerContainerKey,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(color: AppColors.slate200),
boxShadow: const [
BoxShadow(
color: AppColors.slate200,
blurRadius: AppRadius.lg,
offset: Offset(AppSpacing.none, AppSpacing.xs),
),
BoxShadow(
color: AppColors.white,
blurRadius: AppRadius.md,
offset: Offset(AppSpacing.none, -AppSpacing.xs),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IgnorePointer(
ignoring: _isRecording && _isHoldMode,
child: Opacity(
opacity: _isRecording && _isHoldMode ? AppSpacing.none : 1,
child: IconButton(
key: messageComposerPlusButtonKey,
visualDensity: VisualDensity.compact,
onPressed: onTapPlus,
icon: Icon(
LucideIcons.plus,
size: iconSize,
color: AppColors.slate500,
child: Container(
key: messageComposerShellKey,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: AppColors.homeComposerShell,
borderRadius: BorderRadius.circular(AppRadius.xxl),
border: Border.all(color: AppColors.homeComposerBorder),
boxShadow: const [
BoxShadow(
color: AppColors.slate200,
blurRadius: AppRadius.lg,
offset: Offset(AppSpacing.none, AppSpacing.sm),
),
BoxShadow(
color: AppColors.white,
blurRadius: AppRadius.md,
offset: Offset(AppSpacing.none, -AppSpacing.xs),
),
],
),
child: KeyedSubtree(
key: messageComposerInnerKey,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IgnorePointer(
ignoring: _isRecording && _isHoldMode,
child: Opacity(
opacity: _isRecording && _isHoldMode ? AppSpacing.none : 1,
child: IconButton(
key: messageComposerPlusButtonKey,
visualDensity: VisualDensity.compact,
onPressed: onTapPlus,
icon: Icon(
LucideIcons.plus,
size: iconSize,
color: AppColors.slate500,
),
),
),
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(child: _buildCenterArea()),
const SizedBox(width: AppSpacing.sm),
IconButton(
key: messageComposerRightButtonKey,
visualDensity: VisualDensity.compact,
onPressed: onTapRightAction,
icon: _isTranscribing
? const SizedBox(
width: AppSpacing.lg,
height: AppSpacing.lg,
child: CircularProgressIndicator(
strokeWidth: AppSpacing.xs / 2,
color: AppColors.blue600,
),
)
: Icon(
_resolveRightIcon(),
size: iconSize,
color: _resolveRightIconColor(),
),
),
],
),
const SizedBox(width: AppSpacing.sm),
Expanded(child: _buildCenterArea()),
const SizedBox(width: AppSpacing.sm),
IconButton(
key: messageComposerRightButtonKey,
visualDensity: VisualDensity.compact,
onPressed: onTapRightAction,
icon: _isTranscribing
? const SizedBox(
width: AppSpacing.lg,
height: AppSpacing.lg,
child: CircularProgressIndicator(
strokeWidth: AppSpacing.xs / 2,
color: AppColors.blue600,
),
)
: Icon(
_resolveRightIcon(),
size: iconSize,
color: _resolveRightIconColor(),
),
),
],
),
),
);
}
@@ -201,6 +209,11 @@ class MessageComposer extends StatelessWidget {
children: [
recordingAnimation,
const SizedBox(height: AppSpacing.xs),
Text(
recordingText,
style: const TextStyle(color: AppColors.slate700),
),
const SizedBox(height: AppSpacing.xs),
Text(
recordingHintText,
key: messageComposerRecordingHintKey,