import 'package:flutter/material.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_controls.dart'; import 'package:miaoji_zhang/shared/widgets/app_icons.dart'; import 'package:miaoji_zhang/shared/widgets/backend_status_icon.dart'; /// AI 性格设置页(P5):形象/性格从后台 API 动态拉取 class CompanionPage extends StatefulWidget { const CompanionPage({super.key}); @override State createState() => _CompanionPageState(); } class _CompanionPageState extends State { String _avatar = 'cat', _persona = 'sassy_cat'; double _roast = 60, _sticker = 70, _proactive = 40; bool _saving = false; bool _refreshing = false; int _loadRevision = 0; List _avatars = []; List _personas = []; // 兜底 static const _fallbackAvatars = [ ('dog', '阿福汪', AppIcons.dog), ('cat', '小账喵', AppIcons.cat), ('robot', '账小智', AppIcons.robot), ]; static const _fallbackPersonas = [ ('sassy_cat', '毒舌猫娘', '乱花钱会被无情吐槽', AppTheme.ai), ('gentle', '温柔小暖', '永远鼓励,温柔提醒', AppTheme.primary), ('strict', '严格管家', '理性专业,数据说话', AppTheme.ai), ('meme', '沙雕损友', '玩梗高手,快乐记账', AppTheme.orange), ]; @override void initState() { super.initState(); _load(); } Future _load() async { final revision = ++_loadRevision; setState(() => _refreshing = true); final cached = await Future.wait([ AuthApi.cachedCompanion(), PublicConfigApi.cachedAvatars(), PublicConfigApi.cachedPersonas(), ]); if (!mounted || revision != _loadRevision) return; _applyCompanion(cached[0] as AiCompanion?); setState(() { _avatars = cached[1] as List; _personas = cached[2] as List; }); await Future.wait([ AuthApi.me(forceRemote: true).then((profile) { if (mounted && revision == _loadRevision) { _applyCompanion(profile.aiCompanion); } }, onError: (_) {}), PublicConfigApi.avatars().then((values) { if (mounted && revision == _loadRevision) { setState(() => _avatars = values); } }, onError: (_) {}), PublicConfigApi.personas().then((values) { if (mounted && revision == _loadRevision) { setState(() => _personas = values); } }, onError: (_) {}), ]); if (mounted && revision == _loadRevision) { setState(() => _refreshing = false); } } void _applyCompanion(AiCompanion? companion) { if (companion == null || !mounted) return; setState(() { _avatar = companion.avatarKey; _persona = companion.personaKey; _roast = companion.roastLevel.toDouble(); _sticker = companion.stickerFrequency.toDouble(); _proactive = companion.proactiveLevel.toDouble(); }); } Future _save() async { setState(() => _saving = true); try { await AuthApi.updateCompanion( avatarKey: _avatar, personaKey: _persona, roastLevel: _roast.round(), stickerFrequency: _sticker.round(), proactiveLevel: _proactive.round(), ); await PublicConfigApi.refreshCompanion(); if (!mounted) return; Navigator.pop(context); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已保存'))); } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(e)))); } } finally { if (mounted) { setState(() => _saving = false); } } } @override Widget build(BuildContext context) { 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, AppTheme.ai)) .toList() : _fallbackPersonas; return Scaffold( appBar: AppBar( title: Text('AI 性格设置'), actions: [BackendStatusIcon(onRetry: _load)], bottom: _refreshing ? const PreferredSize( preferredSize: Size.fromHeight(2), child: LinearProgressIndicator(minHeight: 2), ) : null, ), body: ListView( padding: const EdgeInsets.all(16), children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: avatars.map((a) { final on = a.$1 == _avatar; return GestureDetector( onTap: () => setState(() => _avatar = a.$1), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 11), child: Column( children: [ AnimatedContainer( duration: const Duration(milliseconds: 200), width: on ? 66 : 52, height: on ? 66 : 52, 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 ? 30 : 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), GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.9, children: personas.map((p) { final on = p.$1 == _persona; return GestureDetector( onTap: () => setState(() => _persona = p.$1), child: Container( 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, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( p.$2, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, ), ), SizedBox(height: 4), Text( p.$3, style: TextStyle(fontSize: 10, color: context.jz.text2), ), ], ), ), ); }).toList(), ), SizedBox(height: 14), Card( child: Padding( padding: const EdgeInsets.all(14), child: Column( children: [ _slider('吐槽力度', _roast, (v) => setState(() => _roast = v)), _slider( '表情包频率', _sticker, (v) => setState(() => _sticker = v), ), ], ), ), ), SizedBox(height: 16), ValueListenableBuilder( valueListenable: ApiClient.availability, builder: (context, availability, _) => ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.ai), onPressed: availability == BackendAvailability.online && !_saving ? _save : null, child: _saving ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text('保存设置'), ), ), ], ), ); } Widget _slider(String label, double value, ValueChanged onChanged) => Column( children: [ Row( children: [ Text( label, style: TextStyle(fontSize: 12, color: context.jz.text2), ), Spacer(), Text( '${value.round()}%', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), ], ), JzSlider( value: value, min: 0, max: 100, color: AppTheme.ai, onChanged: onChanged, ), ], ); }