import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:miaoji_zhang/features/chat/chat_page.dart'; import 'package:miaoji_zhang/shared/api/api_client.dart'; import 'package:miaoji_zhang/shared/api/auth_api.dart'; import 'package:miaoji_zhang/shared/api/business_api.dart'; import 'package:miaoji_zhang/shared/api/config_api.dart'; import 'package:miaoji_zhang/shared/services/current_ledger_store.dart'; import 'package:miaoji_zhang/shared/services/transaction_events.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/widgets/app_icons.dart'; import 'package:miaoji_zhang/shared/services/shanghai_time.dart'; class AiModePage extends StatefulWidget { const AiModePage({super.key}); @override State createState() => _AiModePageState(); } class _AiModePageState extends State { MonthSummary? _summary; BudgetsData? _budgets; String? _summaryError; String? _budgetError; bool _summaryLoading = true; bool _budgetLoading = true; int _loadRevision = 0; final _chatController = ChatPageController(); @override void initState() { super.initState(); CurrentLedgerStore.instance.addListener(_load); TransactionEvents.revision.addListener(_load); _load(); } @override void dispose() { CurrentLedgerStore.instance.removeListener(_load); TransactionEvents.revision.removeListener(_load); super.dispose(); } Future _load() async { final revision = ++_loadRevision; if (mounted) { setState(() { _summaryLoading = true; _budgetLoading = true; _summaryError = null; _budgetError = null; }); } final now = ShanghaiTime.now; await Future.wait([ () async { try { final summary = await TxApi.month(now.year, now.month); if (!mounted || revision != _loadRevision) return; setState(() => _summary = summary); } catch (error) { if (!mounted || revision != _loadRevision) return; setState(() => _summaryError = apiErrorMessage(error)); } finally { if (mounted && revision == _loadRevision) { setState(() => _summaryLoading = false); } } }(), () async { try { final budgets = await BudgetApi.get(now.year, now.month); if (!mounted || revision != _loadRevision) return; setState(() => _budgets = budgets); } catch (error) { if (!mounted || revision != _loadRevision) return; setState(() => _budgetError = apiErrorMessage(error)); } finally { if (mounted && revision == _loadRevision) { setState(() => _budgetLoading = false); } } }(), ]); } Future _openBudget() async { await context.push('/budget'); await _load(); } Future _switchToNormal() async { try { await AuthApi.switchMode('normal'); if (mounted) context.go('/home'); } catch (error) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(error)))); } } Widget _summarySection(MonthSummary? summary, double todayExpense) { if (_summaryLoading && summary == null) { return const _AiLoadCard(message: '正在加载本月收支', loading: true); } if (_summaryError != null && summary == null) { return _AiLoadCard(message: _summaryError!, onRetry: _load); } return Column( children: [ if (_summaryError != null) _AiRefreshNotice(message: _summaryError!, onRetry: _load), Row( children: [ _MiniCard( label: '今日支出', value: '¥${todayExpense.toStringAsFixed(2)}', color: AppTheme.red, ), SizedBox(width: 8), _MiniCard( label: '本月结余', value: '¥${summary!.balance.toStringAsFixed(0)}', color: AppTheme.primary, ), SizedBox(width: 8), _MiniCard( label: '本月支出', value: '¥${summary.expense.toStringAsFixed(0)}', color: context.jz.text, ), ], ), ], ); } Widget _budgetSection() { if (_budgetLoading && _budgets == null) { return const _AiLoadCard(message: '正在加载本月预算', loading: true); } if (_budgetError != null && _budgets == null) { return _AiLoadCard(message: _budgetError!, onRetry: _load); } return Column( children: [ if (_budgetError != null) _AiRefreshNotice(message: _budgetError!, onRetry: _load), AiBudgetStrip(data: _budgets!, onTap: _openBudget), ], ); } @override Widget build(BuildContext context) { final s = _summary; final today = ShanghaiTime.now.day; final todayExpense = s?.days .where((d) => d.date.day == today) .fold(0, (sum, d) => sum + d.expense) ?? 0; return Scaffold( body: Container( decoration: BoxDecoration(color: context.jz.background), child: SafeArea( bottom: false, child: Column( children: [ ValueListenableBuilder( valueListenable: PublicConfigApi.companionNotifier, builder: (_, companion, __) { return Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Row( children: [ CircleAvatar( radius: 22, backgroundColor: AppTheme.ai, child: AppIcons.icon( AppIcons.keyMap[companion.avatarKey] ?? AppIcons.cat, size: 24, color: Colors.white, ), ), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( companion.name, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, ), ), Text( '全 AI 模式 · 说句话就能记账', style: TextStyle( fontSize: 10, color: context.jz.text3, ), ), ], ), ), IconButton( tooltip: '清理上下文', onPressed: _chatController.clearContext, icon: Icon( Icons.restart_alt_rounded, size: 21, color: context.jz.text2, ), ), GestureDetector( onTap: _switchToNormal, child: Container( padding: const EdgeInsets.symmetric( horizontal: 11, vertical: 5, ), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(14), border: Border.all( color: AppTheme.ai.withValues(alpha: 0.35), ), ), child: Text( '切换模式', style: TextStyle( fontSize: 10.5, color: AppTheme.ai, ), ), ), ), ], ), ); }, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _summarySection(s, todayExpense), ), SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _budgetSection(), ), SizedBox(height: 8), Expanded( child: ChatPage(aiMode: true, controller: _chatController), ), ], ), ), ), ); } } class _AiLoadCard extends StatelessWidget { final String message; final bool loading; final Future Function()? onRetry; const _AiLoadCard({ required this.message, this.loading = false, this.onRetry, }); @override Widget build(BuildContext context) { return Container( constraints: const BoxConstraints(minHeight: 58), padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 10), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(14), border: Border.all( color: loading ? context.jz.line : AppTheme.red.withValues(alpha: 0.22), ), ), child: Row( children: [ if (loading) SizedBox( width: 17, height: 17, child: CircularProgressIndicator( strokeWidth: 2, color: AppTheme.primary, ), ) else Icon(Icons.cloud_off_rounded, size: 18, color: AppTheme.red), SizedBox(width: 10), Expanded( child: Text( message, style: TextStyle(fontSize: 11.5, color: context.jz.text2), ), ), if (!loading && onRetry != null) TextButton(onPressed: onRetry, child: Text('重试')), ], ), ); } } class _AiRefreshNotice extends StatelessWidget { final String message; final Future Function() onRetry; const _AiRefreshNotice({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 7), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), decoration: BoxDecoration( color: context.jz.warningBackground, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Icon(Icons.info_outline_rounded, size: 15, color: AppTheme.orange), SizedBox(width: 7), Expanded( child: Text( '刷新失败,当前显示上次数据:' + message, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 10.5, color: context.jz.text2), ), ), TextButton( onPressed: onRetry, style: TextButton.styleFrom( minimumSize: const Size(0, 30), padding: const EdgeInsets.symmetric(horizontal: 7), ), child: Text('重试', style: TextStyle(fontSize: 10.5)), ), ], ), ); } } class AiBudgetStrip extends StatelessWidget { final BudgetsData? data; final VoidCallback onTap; const AiBudgetStrip({super.key, required this.data, required this.onTap}); @override Widget build(BuildContext context) { final total = data?.total; final categories = data?.categories ?? const []; final amount = total?.amount ?? categories.fold(0, (sum, item) => sum + item.amount); final spent = total?.spent ?? categories.fold(0, (sum, item) => sum + item.spent); final hasBudget = amount > 0; final ratio = hasBudget ? (spent / amount).clamp(0.0, 1.0) : 0.0; final over = hasBudget && spent > amount; return Semantics( button: true, label: hasBudget ? '查看本月预算' : '设置本月预算', child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: Container( padding: const EdgeInsets.fromLTRB(13, 10, 13, 9), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(14), border: Border.all(color: context.jz.line), ), child: hasBudget ? Column( children: [ Row( children: [ AppIcons.icon( AppIcons.target, size: 16, color: over ? AppTheme.red : AppTheme.primary, ), SizedBox(width: 7), Text( '本月预算', style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.w700, ), ), Spacer(), Text( over ? '已超 ¥${(spent - amount).toStringAsFixed(0)}' : '剩余 ¥${(amount - spent).toStringAsFixed(0)}', style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.w700, color: over ? AppTheme.red : AppTheme.primaryDeep, ), ), SizedBox(width: 3), Icon( Icons.chevron_right_rounded, size: 17, color: context.jz.text3, ), ], ), SizedBox(height: 7), ClipRRect( borderRadius: BorderRadius.circular(3), child: LinearProgressIndicator( value: ratio, minHeight: 5, backgroundColor: context.jz.line, color: over ? AppTheme.red : ratio > 0.8 ? AppTheme.orange : AppTheme.primary, ), ), SizedBox(height: 5), Row( children: [ Text( '已用 ¥${spent.toStringAsFixed(0)}', style: TextStyle( fontSize: 9.5, color: context.jz.text3, ), ), Spacer(), Text( '总额 ¥${amount.toStringAsFixed(0)}', style: TextStyle( fontSize: 9.5, color: context.jz.text3, ), ), ], ), ], ) : Row( children: [ Container( width: 30, height: 30, decoration: BoxDecoration( color: context.jz.primaryBackground, borderRadius: BorderRadius.circular(9), ), child: Center( child: AppIcons.icon( AppIcons.target, size: 16, color: AppTheme.primary, ), ), ), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '本月还没设置预算', style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.w700, ), ), SizedBox(height: 2), Text( '设个上限,让 AI 帮你一起盯住', style: TextStyle( fontSize: 9.5, color: context.jz.text3, ), ), ], ), ), Text( '去设置', style: TextStyle( fontSize: 11, color: AppTheme.primaryDeep, fontWeight: FontWeight.w700, ), ), Icon( Icons.chevron_right_rounded, size: 17, color: AppTheme.primary, ), ], ), ), ), ); } } class _MiniCard extends StatelessWidget { final String label, value; final Color color; const _MiniCard({ required this.label, required this.value, required this.color, }); @override Widget build(BuildContext context) { return Expanded( child: Container( padding: const EdgeInsets.all(9), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(12), border: Border.all(color: context.jz.line), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 9, color: context.jz.text3)), SizedBox(height: 2), Text( value, style: TextStyle( fontSize: 13.5, fontWeight: FontWeight.w700, color: color, ), ), ], ), ), ); } }