feat: 优化前端 UI 组件与交互体验
- 优化日历、待办、消息等页面交互 - 更新 ChatBloc 与 UI Schema 渲染 - 优化联系人、首页、设置页面体验
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user