Files
ai-contract-platform/mobile/lib/app/shell/app_shell.dart
T
yuqei 3df85fdb04 feat: land phase15 legal docs and commercial ops
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
2026-05-21 14:42:44 +08:00

335 lines
11 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../features/auth/application/auth_controller.dart';
import '../../features/auth/domain/auth_session.dart';
import '../../features/auth/presentation/login_screen.dart';
import '../../features/contracts/presentation/generated_contract_result_screen.dart';
import '../../features/dashboard/presentation/dashboard_screen.dart';
import '../../features/profile/presentation/profile_screen.dart';
import '../../features/review/presentation/review_entry_screen.dart';
import '../../features/review_history/presentation/review_detail_screen.dart';
import '../../features/review_history/presentation/review_history_screen.dart';
import '../../features/contracts/presentation/contract_template_list_screen.dart';
import '../../features/tools/presentation/tools_hub_screen.dart';
import '../../shared/network/api_client.dart';
import '../../shared/notifications/app_notification_action.dart';
import '../../shared/notifications/notification_service.dart';
import '../../shared/storage/session_store.dart';
import '../navigation/app_router.dart';
import '../theme/brand_assets.dart';
class AiContractAppShell extends StatefulWidget {
const AiContractAppShell({super.key});
@override
State<AiContractAppShell> createState() => _AiContractAppShellState();
}
class _AiContractAppShellState extends State<AiContractAppShell>
with WidgetsBindingObserver {
AppTab _currentTab = AppTab.dashboard;
late final AuthController _authController;
final ApiClient _apiClient = const ApiClient();
String? _activeSessionUserId;
bool _isHandlingNotificationAction = false;
@override
void initState() {
super.initState();
_authController = AuthController();
WidgetsBinding.instance.addObserver(this);
SessionStore.listenable.addListener(_handleSessionChanged);
NotificationService.instance.pendingAction
.addListener(_handleNotificationSignal);
unawaited(_syncNotificationLifecycle());
}
@override
void dispose() {
NotificationService.instance.pendingAction
.removeListener(_handleNotificationSignal);
SessionStore.listenable.removeListener(_handleSessionChanged);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
unawaited(_syncNotificationLifecycle(runSyncNow: true));
}
}
void _selectTab(AppTab tab) {
setState(() {
_currentTab = tab;
});
}
void _openTools() {
setState(() {
_currentTab = AppTab.tools;
});
}
Future<void> _openContractWorkspace() async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ContractTemplateListScreen(),
),
);
}
void _handleSessionChanged() {
unawaited(_syncNotificationLifecycle());
}
void _handleNotificationSignal() {
unawaited(_tryHandlePendingNotificationAction());
}
Future<void> _syncNotificationLifecycle({bool runSyncNow = false}) async {
final session = SessionStore.current;
if (session == null || !session.isAuthenticated) {
if (_activeSessionUserId != null) {
_activeSessionUserId = null;
await _safeNotificationCall(NotificationService.instance.onSignedOut);
}
return;
}
if (_activeSessionUserId != session.userId) {
_activeSessionUserId = session.userId;
await _safeNotificationCall(NotificationService.instance.onSignedIn);
runSyncNow = true;
}
if (runSyncNow) {
await _safeNotificationCall(
NotificationService.instance.syncReviewNotifications);
}
await _tryHandlePendingNotificationAction();
}
Future<void> _tryHandlePendingNotificationAction() async {
if (!mounted || _isHandlingNotificationAction) {
return;
}
final session = SessionStore.current;
final action = NotificationService.instance.pendingAction.value;
if (session == null || !session.isAuthenticated || action == null) {
return;
}
_isHandlingNotificationAction = true;
try {
switch (action.type) {
case AppNotificationActionType.reviewStatus:
final reviewId = action.reviewId;
if (reviewId == null || reviewId.isEmpty) {
break;
}
_selectTab(AppTab.history);
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ReviewDetailScreen(
reviewId: reviewId,
initialTitle: action.reviewTitle?.isNotEmpty == true
? action.reviewTitle!
: '审查详情',
),
),
);
break;
case AppNotificationActionType.generatedContract:
final contractId = action.contractId;
if (contractId == null || contractId.isEmpty) {
break;
}
final contract = await _apiClient.getGeneratedContract(
session.accessToken,
contractId,
);
if (!mounted) {
return;
}
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => GeneratedContractResultScreen(contract: contract),
),
);
break;
}
NotificationService.instance.clearPendingAction();
} catch (_) {
// Keep the action and allow a later retry.
} finally {
_isHandlingNotificationAction = false;
}
}
Future<T?> _safeNotificationCall<T>(Future<T> Function() action) async {
try {
return await action();
} on MissingPluginException {
return null;
} catch (_) {
return null;
}
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<AuthSession?>(
valueListenable: SessionStore.listenable,
builder: (context, session, _) {
final isAuthenticated = session?.isAuthenticated ?? false;
if (!isAuthenticated) {
return Scaffold(
resizeToAvoidBottomInset: true,
body: SafeArea(
child: LoginScreen(controller: _authController),
),
);
}
return Scaffold(
appBar: AppBar(
toolbarHeight: 66,
systemOverlayStyle: SystemUiOverlayStyle.light,
titleSpacing: 16,
title: Row(
children: [
const BrandMark(size: 34),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_currentTab.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 2),
Text(
'${session!.userDisplayName} · ${session.role}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xBFE8FBDC),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(999),
border:
Border.all(color: Colors.white.withValues(alpha: 0.08)),
),
child: const Text(
'Live',
style: TextStyle(
color: BrandAssets.accentLime,
fontSize: 11,
fontWeight: FontWeight.w800,
),
),
),
],
),
),
body: SafeArea(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 240),
child: _buildCurrentScreen(),
),
),
bottomNavigationBar: Container(
decoration: const BoxDecoration(
color: BrandAssets.surface,
border: Border(top: BorderSide(color: BrandAssets.line)),
boxShadow: <BoxShadow>[
BoxShadow(
color: Color(0x140F172A),
blurRadius: 22,
offset: Offset(0, -8),
),
],
),
child: SafeArea(
top: false,
child: NavigationBar(
height: 68,
selectedIndex: AppTab.values.indexOf(_currentTab),
onDestinationSelected: (index) {
_selectTab(AppTab.values[index]);
},
destinations: AppTab.values
.map(
(item) => NavigationDestination(
icon: Icon(item.icon),
selectedIcon:
Icon(item.icon, color: BrandAssets.surfaceInk),
label: item.label,
),
)
.toList(growable: false),
),
),
),
);
},
);
}
Widget _buildCurrentScreen() {
switch (_currentTab) {
case AppTab.dashboard:
return DashboardScreen(
key: const ValueKey(AppTab.dashboard),
onStartReview: () => _selectTab(AppTab.review),
onOpenHistory: () => _selectTab(AppTab.history),
onOpenProfile: () => _selectTab(AppTab.profile),
onOpenLegalChat: _openTools,
onOpenContracts: _openContractWorkspace,
);
case AppTab.review:
return const ReviewEntryScreen(key: ValueKey(AppTab.review));
case AppTab.tools:
return const ToolsHubScreen(key: ValueKey(AppTab.tools));
case AppTab.history:
return ReviewHistoryScreen(
key: const ValueKey(AppTab.history),
onStartReview: () => _selectTab(AppTab.review),
);
case AppTab.profile:
return ProfileScreen(
key: const ValueKey(AppTab.profile),
onOpenHistory: () => _selectTab(AppTab.history),
);
}
}
}