import 'package:flutter/material.dart'; import 'package:go_router/go_router.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/config_api.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/widgets/app_icons.dart'; /// 首次引导:选模式 → 选 AI 伙伴(P9 + P10) /// 形象和性格从后台 API 动态拉取,管理员新增/修改即时生效 class OnboardingPage extends StatefulWidget { const OnboardingPage({super.key}); @override State createState() => _OnboardingPageState(); } class _OnboardingPageState extends State { int _step = 0; String _mode = 'normal'; String _avatar = 'cat'; String _persona = 'sassy_cat'; bool _saving = false; List _avatars = []; List _personas = []; bool _loaded = false; @override void initState() { super.initState(); _loadConfig(); } Future _loadConfig() async { if (mounted) setState(() => _loaded = false); final results = await Future.wait([_loadAvatars(), _loadPersonas()]); if (!mounted) return; final avatars = results[0] as List; final personas = results[1] as List; setState(() { _avatars = avatars; _personas = personas; if (!avatars.any((item) => item.key == _avatar) && avatars.isNotEmpty) { _avatar = avatars.first.key; } if (!personas.any((item) => item.key == _persona) && personas.isNotEmpty) { _persona = personas.first.key; } _loaded = true; }); } Future> _loadAvatars() async { try { return await PublicConfigApi.avatars(); } catch (_) { return const []; } } Future> _loadPersonas() async { try { return await PublicConfigApi.personas(); } catch (_) { return const []; } } Future _finish() async { if (!_hasValidCatalogSelection) return; setState(() => _saving = true); try { await AuthApi.completeOnboarding( appMode: _mode, avatarKey: _avatar, personaKey: _persona, ); if (!mounted) return; context.go(_mode == 'ai' ? '/ai-mode' : '/home'); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(e)))); } finally { if (mounted) setState(() => _saving = false); } } bool get _hasValidCatalogSelection => _avatars.any((item) => item.key == _avatar) && _personas.any((item) => item.key == _persona); @override Widget build(BuildContext context) { if (!_loaded) return const Scaffold( body: Center( child: CircularProgressIndicator( color: AppTheme.primary, strokeWidth: 2, ), ), ); return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 26), child: _step == 0 ? _ModeStep() : _CompanionStep(), ), ), ); } Widget _ModeStep() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 8), Row( children: [ Container( width: 28, height: 5, decoration: BoxDecoration( color: AppTheme.primary, borderRadius: BorderRadius.circular(3), ), ), SizedBox(width: 6), Container( width: 18, height: 5, decoration: BoxDecoration( color: context.jz.line, borderRadius: BorderRadius.circular(3), ), ), ], ), SizedBox(height: 12), Text( '你想怎么用它?', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800), ), SizedBox(height: 6), Text( '两种完全不同的体验,之后随时可以切换', style: TextStyle(fontSize: 12, color: context.jz.text2), ), SizedBox(height: 18), _ModeCard( selected: _mode == 'normal', iconAsset: AppIcons.wallet, iconColor: AppTheme.primary, title: '普通记账模式', desc: '经典账本界面,手动记账、报表统计一应俱全,AI 助手随叫随到', onTap: () => setState(() => _mode = 'normal'), ), SizedBox(height: 11), _ModeCard( selected: _mode == 'ai', iconAsset: AppIcons.sparkle, iconColor: AppTheme.ai, title: '全 AI 模式', desc: '没有复杂界面,打开就是和 AI 聊天,说句话就完成记账、查账、看报告', onTap: () => setState(() => _mode = 'ai'), ), Spacer(), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => setState(() => _step = 1), child: Text('继续'), ), ), SizedBox(height: 16), ], ); } Widget _CompanionStep() { final avatars = _avatars .map( (a) => ( a.key, a.defaultName, a.key == 'dog' ? AppIcons.dog : a.key == 'robot' ? AppIcons.robot : AppIcons.cat, ), ) .toList(); final personas = _personas .map((p) => (p.key, p.name, p.description)) .toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 8), Row( children: [ Container( width: 18, height: 5, decoration: BoxDecoration( color: context.jz.line, borderRadius: BorderRadius.circular(3), ), ), SizedBox(width: 6), Container( width: 28, height: 5, decoration: BoxDecoration( color: AppTheme.primary, borderRadius: BorderRadius.circular(3), ), ), ], ), SizedBox(height: 12), Text( '认识你的 AI 伙伴', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800), ), SizedBox(height: 6), Text( '选形象、挑性格 —— 它就是你的专属记账搭子', style: TextStyle(fontSize: 12, color: context.jz.text2), ), SizedBox(height: 14), if (avatars.isEmpty || personas.isEmpty) Expanded( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ AppIcons.icon(AppIcons.cloud, size: 28, color: context.jz.text3), const SizedBox(height: 12), Text( 'AI 伙伴配置暂不可用', style: TextStyle( color: context.jz.text, fontSize: 14, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text('请检查网络后重试', style: TextStyle(color: context.jz.text2, fontSize: 12)), const SizedBox(height: 12), TextButton.icon( onPressed: _loadConfig, icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('重新加载'), ), ], ), ), ) else ...[ SizedBox( height: 96, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: avatars.map((a) { final on = _avatar == a.$1; return Padding( padding: const EdgeInsets.symmetric(horizontal: 11), child: GestureDetector( onTap: () => setState(() => _avatar = a.$1), child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( duration: const Duration(milliseconds: 220), curve: Curves.elasticOut, width: 56, height: 56, child: Transform.scale( scale: on ? 1.18 : 1.0, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: on ? AppTheme.ai : context.jz.aiBackground, boxShadow: on ? [ BoxShadow( color: AppTheme.ai.withValues( alpha: 0.3, ), blurRadius: 14, ), ] : null, ), child: Center( child: AppIcons.icon( a.$3, size: on ? 28 : 24, color: on ? Colors.white : context.jz.text3, ), ), ), ), ), SizedBox(height: 6), Text( a.$2, style: TextStyle( fontSize: 10, color: on ? AppTheme.ai : context.jz.text3, fontWeight: on ? FontWeight.w600 : null, ), ), ], ), ), ); }).toList(), ), ), SizedBox(height: 16), Expanded( child: GridView.count( crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.5, children: personas.map((p) { final on = _persona == p.$1; return GestureDetector( onTap: () => setState(() => _persona = p.$1), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: on ? context.jz.aiBackground : context.jz.card, borderRadius: BorderRadius.circular(14), border: Border.all( color: on ? AppTheme.ai : context.jz.line, width: 1.5, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AppIcons.icon( AppIcons.chat, size: 18, color: on ? AppTheme.ai : context.jz.text3, ), SizedBox(height: 8), Text( p.$2, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, ), ), SizedBox(height: 3), Text( p.$3, style: TextStyle(fontSize: 10, color: context.jz.text2), ), ], ), ), ); }).toList(), ), ), ], SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.ai), onPressed: _saving || !_hasValidCatalogSelection ? null : _finish, child: _saving ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text('开始记账之旅'), ), ), SizedBox(height: 16), ], ); } } class _ModeCard extends StatelessWidget { final bool selected; final String iconAsset; final Color iconColor; final String title, desc; final VoidCallback onTap; const _ModeCard({ required this.selected, required this.iconAsset, required this.iconColor, required this.title, required this.desc, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(18), border: Border.all( color: selected ? AppTheme.primary : context.jz.line, width: selected ? 2 : 1.5, ), ), child: Row( children: [ AppIcons.icon(iconAsset, size: 22, color: iconColor), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700), ), SizedBox(height: 4), Text( desc, style: TextStyle(fontSize: 10.5, color: context.jz.text2), ), ], ), ), SizedBox(width: 8), Container( width: 20, height: 20, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: selected ? AppTheme.primary : context.jz.line, width: 1.5, ), color: selected ? AppTheme.primary : null, ), child: selected ? AppIcons.icon(AppIcons.check, size: 13, color: Colors.white) : null, ), ], ), ), ); } }