feat: 优化前端 UI 组件与交互体验

- 优化日历、待办、消息等页面交互
- 更新 ChatBloc 与 UI Schema 渲染
- 优化联系人、首页、设置页面体验
This commit is contained in:
qzl
2026-03-16 16:11:28 +08:00
parent a75c868bca
commit 4b92772535
18 changed files with 1591 additions and 1780 deletions
@@ -11,10 +11,10 @@ import '../../../../core/di/injection.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../chat/data/models/chat_list_item.dart';
import '../../../chat/presentation/bloc/chat_bloc.dart';
import '../../../chat/data/tools/route_navigation_tool.dart';
import '../../../messages/data/inbox_api.dart';
import '../../data/voice_recorder.dart';
import '../../../chat/ui/widgets/ui_schema_renderer.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/message_composer.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
@@ -85,15 +85,13 @@ class _HomeScreenState extends State<HomeScreen>
bool _isHoldToSpeakMode = false;
bool _isTranscribing = false;
bool _isCancelGestureActive = false;
bool _isSendingMessage = false;
int _unreadCount = 0;
final List<XFile> _selectedImages = [];
bool get _hasMessage => _messageController.text.trim().isNotEmpty;
@override
void initState() {
super.initState();
_messageController.addListener(_onMessageChanged);
_chatBloc = widget.chatBloc ?? ChatBloc(apiClient: sl());
_voiceRecorder = widget.voiceRecorder ?? RecordVoiceRecorder();
_inboxApi = sl<InboxApi>();
@@ -124,7 +122,6 @@ class _HomeScreenState extends State<HomeScreen>
@override
void dispose() {
_messageController.removeListener(_onMessageChanged);
_messageController.dispose();
_scrollController.dispose();
_listeningAnimationController.dispose();
@@ -132,27 +129,11 @@ class _HomeScreenState extends State<HomeScreen>
if (widget.chatBloc == null) {
_chatBloc.close();
}
RouteNavigationTool.instance.clearNavigator();
super.dispose();
}
void _onMessageChanged() {
setState(() {});
}
@override
Widget build(BuildContext context) {
RouteNavigationTool.instance.bindNavigator((target, {replace = false}) {
if (!mounted) {
return;
}
if (replace) {
context.go(target);
} else {
context.push(target);
}
});
return BlocProvider.value(
value: _chatBloc,
child: BlocConsumer<ChatBloc, ChatState>(
@@ -200,7 +181,9 @@ class _HomeScreenState extends State<HomeScreen>
state.isWaitingFirstToken || state.isStreaming || state.isCancelling;
if (state.isLoadingHistory && state.items.isEmpty) {
return const Center(child: CircularProgressIndicator());
return const Center(
child: AppLoadingIndicator(variant: AppLoadingVariant.surface),
);
}
return Padding(
@@ -294,9 +277,12 @@ class _HomeScreenState extends State<HomeScreen>
SizedBox(
width: _transcribingSpinnerSize,
height: _transcribingSpinnerSize,
child: CircularProgressIndicator(
child: const AppLoadingIndicator(
variant: AppLoadingVariant.inline,
size: _transcribingSpinnerSize,
strokeWidth: _transcribingStrokeWidth,
color: AppColors.blue600,
trackColor: AppColors.blue100,
),
),
SizedBox(width: AppSpacing.sm),
@@ -341,13 +327,12 @@ class _HomeScreenState extends State<HomeScreen>
padding: const EdgeInsets.symmetric(vertical: 8),
alignment: Alignment.center,
child: isLoading
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: AppColors.slate400,
),
? const AppLoadingIndicator(
variant: AppLoadingVariant.inline,
size: 14,
strokeWidth: 1.5,
color: AppColors.slate400,
trackColor: AppColors.slate200,
)
: const Text(
'查看历史',
@@ -481,12 +466,10 @@ class _HomeScreenState extends State<HomeScreen>
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: _transcribingSpinnerSize,
height: _transcribingSpinnerSize,
child: CircularProgressIndicator(
strokeWidth: _transcribingStrokeWidth,
),
child: AppLoadingIndicator(
variant: AppLoadingVariant.inline,
size: _transcribingSpinnerSize,
strokeWidth: _transcribingStrokeWidth,
),
);
},
@@ -550,31 +533,13 @@ class _HomeScreenState extends State<HomeScreen>
),
const SizedBox(width: AppSpacing.sm),
Text(statusText, style: TextStyle(fontSize: 12, color: statusColor)),
if (item.toolName == 'front.navigate_to_route' &&
item.status == ToolCallStatus.pending) ...[
const SizedBox(width: AppSpacing.sm),
GestureDetector(
onTap: () => _chatBloc.approveToolCall(item.callId),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.blue600,
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'同意',
style: TextStyle(fontSize: 11, color: AppColors.white),
),
),
),
],
],
),
);
}
Widget _buildToolResultItem(ToolResultItem item) {
return UiSchemaRenderer.render(item.uiCard);
return UiSchemaRenderer.renderSchema(item.uiSchema);
}
Widget _buildBottomInputStack(BuildContext context, ChatState state) {
@@ -611,31 +576,37 @@ class _HomeScreenState extends State<HomeScreen>
Widget _buildInputContainer(BuildContext context, ChatState state) {
final isWaitingAgent =
state.isWaitingFirstToken || state.isStreaming || state.isCancelling;
return Container(
padding: EdgeInsets.zero,
child: MessageComposer(
mode: _isHoldToSpeakMode
? MessageComposerMode.holdToSpeak
: MessageComposerMode.text,
process: _composerProcess,
hasMessage: _hasMessage,
isWaitingAgent: isWaitingAgent,
iconSize: _iconSize,
composerMinHeight: _inputMinHeight,
onTapPlus: _isRecording
? () => _stopRecording(autoSendAfterTranscribe: false)
: () => _showBottomSheet(context),
onTapRightAction: () => _onRightActionTap(context, state),
onHoldToSpeakStart: _onHoldToSpeakStart,
onHoldToSpeakEnd: _onHoldToSpeakEnd,
onHoldToSpeakMoveUpdate: _onHoldToSpeakMoveUpdate,
onHoldToSpeakCancel: _onHoldToSpeakCancel,
textInputChild: _buildTextInputContent(context),
recordingAnimation: const SizedBox.shrink(),
recordingText: _isCancelGestureActive ? '松手取消' : '松手发送',
recordingHintText: _isCancelGestureActive ? '松开取消' : '松开发送,上滑取消',
showRecordingInlineFeedback: false,
),
return ValueListenableBuilder<TextEditingValue>(
valueListenable: _messageController,
builder: (context, value, child) {
final hasMessage = value.text.trim().isNotEmpty;
return Container(
padding: EdgeInsets.zero,
child: MessageComposer(
mode: _isHoldToSpeakMode
? MessageComposerMode.holdToSpeak
: MessageComposerMode.text,
process: _composerProcess,
hasMessage: hasMessage,
isWaitingAgent: isWaitingAgent,
iconSize: _iconSize,
composerMinHeight: _inputMinHeight,
onTapPlus: _isRecording
? () => _stopRecording(autoSendAfterTranscribe: false)
: () => _showBottomSheet(context),
onTapRightAction: () => _onRightActionTap(context, state),
onHoldToSpeakStart: _onHoldToSpeakStart,
onHoldToSpeakEnd: _onHoldToSpeakEnd,
onHoldToSpeakMoveUpdate: _onHoldToSpeakMoveUpdate,
onHoldToSpeakCancel: _onHoldToSpeakCancel,
textInputChild: _buildTextInputContent(context),
recordingAnimation: const SizedBox.shrink(),
recordingText: _isCancelGestureActive ? '松手取消' : '松手发送',
recordingHintText: _isCancelGestureActive ? '松开取消' : '松开发送,上滑取消',
showRecordingInlineFeedback: false,
),
);
},
);
}
@@ -690,7 +661,7 @@ class _HomeScreenState extends State<HomeScreen>
}
void _onRightActionTap(BuildContext context, ChatState state) {
if (_isTranscribing || _isRecording) {
if (_isTranscribing || _isRecording || _isSendingMessage) {
return;
}
final isWaitingAgent =
@@ -699,7 +670,7 @@ class _HomeScreenState extends State<HomeScreen>
_onStopGenerating();
return;
}
if (_hasMessage) {
if (_messageController.text.trim().isNotEmpty) {
_sendMessage(context);
return;
}
@@ -764,6 +735,10 @@ class _HomeScreenState extends State<HomeScreen>
}
Future<void> _sendMessage(BuildContext context) async {
if (_isSendingMessage) {
return;
}
final content = _messageController.text.trim();
if (content.isEmpty && _selectedImages.isEmpty) return;
@@ -772,10 +747,19 @@ class _HomeScreenState extends State<HomeScreen>
FocusScope.of(context).unfocus();
_messageController.clear();
setState(() {
_isSendingMessage = true;
_selectedImages.clear();
});
await context.read<ChatBloc>().sendMessage(content, images: images);
try {
await context.read<ChatBloc>().sendMessage(content, images: images);
} finally {
if (mounted) {
setState(() {
_isSendingMessage = false;
});
}
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {