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 { try { final avatars = await PublicConfigApi.avatars(); final personas = await PublicConfigApi.personas(); if (mounted) setState(() { _avatars = avatars; _personas = personas; if (avatars.isNotEmpty) _avatar = avatars.first.key; if (personas.isNotEmpty) _persona = personas.first.key; _loaded = true; }); } catch (_) { // API 失败用硬编码兜底 if (mounted) setState(() => _loaded = true); } } // 兜底数据(API 不可用时) static const _fallbackAvatars = [ ('cat', '小账喵', AppIcons.cat), ('dog', '阿福汪', AppIcons.dog), ('robot', '账小智', AppIcons.robot), ]; static const _fallbackPersonas = [ ('sassy_cat', '毒舌猫娘', '乱花钱会被无情吐槽'), ('gentle', '温柔小暖', '永远鼓励,温柔提醒'), ('strict', '严格管家', '理性专业,数据说话'), ('meme', '沙雕损友', '玩梗高手,快乐记账'), ]; Future _finish() async { 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); } } @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() { // 优先 API 数据,回退硬编码 final avatars = _avatars.isNotEmpty ? _avatars .map( (a) => ( a.key, a.defaultName, a.key == 'dog' ? AppIcons.dog : a.key == 'robot' ? AppIcons.robot : AppIcons.cat, ), ) .toList() : _fallbackAvatars; final personas = _personas.isNotEmpty ? _personas.map((p) => (p.key, p.name, p.description)).toList() : _fallbackPersonas; 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), 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 : Colors.white, 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 ? 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, ), ], ), ), ); } }