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/services/session_store.dart'; import 'package:miaoji_zhang/shared/services/sync_service.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/update/update_coordinator.dart'; import 'package:miaoji_zhang/shared/widgets/app_icons.dart'; import 'package:miaoji_zhang/shared/widgets/app_controls.dart'; import 'package:miaoji_zhang/shared/version.dart'; /// 我的页(P6):模式切换 + 设置入口 + 退出登录 class MePage extends StatefulWidget { const MePage({super.key}); @override State createState() => _MePageState(); } class _MePageState extends State { UserProfile? _profile; @override void initState() { super.initState(); PublicConfigApi.companionNotifier.addListener(_refreshCompanion); SessionStore.instance.addListener(_refreshSession); SyncService.instance.refreshLocalStatus(); _load(); } void _refreshCompanion() { if (mounted) setState(() {}); } void _refreshSession() { if (mounted) setState(() {}); } @override void dispose() { PublicConfigApi.companionNotifier.removeListener(_refreshCompanion); SessionStore.instance.removeListener(_refreshSession); super.dispose(); } Future _load() async { try { final p = await AuthApi.me(); if (mounted) setState(() => _profile = p); } catch (_) {} } Future _checkUpdate() => UpdateCoordinator.instance.checkManually(context); Future _switchMode(String mode) async { if (_profile?.appMode == mode) return; try { final p = await AuthApi.switchMode(mode); if (!mounted) return; setState(() => _profile = p); if (mode == 'ai') context.go('/ai-mode'); } catch (e) { if (mounted) ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(e)))); } } Future _logout() async { final guest = SessionStore.instance.isGuest; final ok = await showJzConfirmSheet( context, title: guest ? '退出游客模式' : '退出登录', message: guest ? '游客数据会继续加密保存在本机,下次进入游客模式仍可使用。' : '账号本地数据会继续加密保存在本机,切换账号不会混用。', confirmLabel: '退出', destructive: true, ); if (!ok) return; await AuthApi.logout(); if (mounted) context.go('/login'); } @override Widget build(BuildContext context) { final p = _profile; final session = SessionStore.instance; return Scaffold( body: SafeArea( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16), children: [ // 用户头 Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ CircleAvatar( radius: 27, backgroundColor: context.jz.card, child: AppIcons.icon( AppIcons.user, size: 26, color: context.jz.text2, ), ), SizedBox(width: 13), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( session.nickname ?? p?.nickname ?? p?.username ?? (session.isGuest ? '游客' : '…'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, ), ), SizedBox(height: 3), Text( '@${p?.username ?? ''}', style: TextStyle(fontSize: 11, color: context.jz.text3), ), ], ), ], ), ), Card( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 8, ), child: session.isGuest ? const GuestLocalStatusCard() : Column( children: [ AnimatedBuilder( animation: SyncService.instance, builder: (context, _) { final sync = SyncService.instance; return JzSwitchTile( value: session.cloudSyncEnabled, title: '云同步', subtitle: session.cloudSyncEnabled ? sync.statusLabel : '本地模式:数据不会发送到服务端', onChanged: (value) async { await session.setCloudSyncEnabled(value); if (value) { await sync.run(); } else { sync.refreshLocalStatus(); } if (mounted) setState(() {}); }, ); }, ), if (session.needsReauth) InkWell( onTap: () => context.go('/login'), borderRadius: BorderRadius.circular(10), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), decoration: BoxDecoration( color: context.jz.warningBackground, borderRadius: BorderRadius.circular(10), ), child: Text( '云同步登录已过期,点击重新登录;本地记账不受影响', style: TextStyle( fontSize: 10.5, color: context.jz.text2, ), ), ), ), ], ), ), ), SizedBox(height: 12), // 模式切换 if (!session.isGuest && session.aiEnabled) ...[ Card( child: Padding( padding: const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'App 模式', style: TextStyle( fontSize: 13.5, fontWeight: FontWeight.w700, ), ), SizedBox(height: 3), Text( '选择你喜欢的使用方式,可随时切换', style: TextStyle( fontSize: 10.5, color: context.jz.text3, ), ), SizedBox(height: 11), Row( children: [ _ModeOpt( title: '普通记账模式', desc: '经典账本界面', iconAsset: AppIcons.wallet, color: AppTheme.primary, selected: session.appMode != 'ai', onTap: () => _switchMode('normal'), ), SizedBox(width: 10), _ModeOpt( title: '全 AI 模式', desc: '对话作为主界面', iconAsset: AppIcons.sparkle, color: AppTheme.ai, selected: session.appMode == 'ai', onTap: () => _switchMode('ai'), ), ], ), ], ), ), ), SizedBox(height: 12), ], // 设置入口 Card( child: Column( children: [ if (!session.isGuest && session.aiEnabled) _row( AppIcons.avatarAsset(PublicConfigApi.companionAvatarKey), context.jz.aiBackground, AppTheme.ai, '${PublicConfigApi.companionName} 性格设置', trailing: p?.aiCompanion?.personaKey, onTap: () => context.push('/companion'), ), _row( AppIcons.target, context.jz.primaryBackground, AppTheme.primary, '预算管理', onTap: () => context.push('/budget'), ), _row( AppIcons.tag, context.jz.primaryBackground, AppTheme.primary, '分类管理', onTap: () => context.push('/categories'), ), if (!session.isGuest && PublicConfigApi.screenshotEnabled) _row( AppIcons.camera, context.jz.primaryBackground, AppTheme.red, '智能识别', trailing: '设置', onTap: () => context.push('/screenshot-settings'), ), _row( AppIcons.user, context.jz.background, context.jz.text2, session.isGuest ? '本地数据' : '账号与数据', onTap: () async { await context.push('/account-data'); _load(); }, ), _row( AppIcons.gear, context.jz.background, context.jz.text2, '外观设置', onTap: () => context.push('/appearance'), ), if (session.isAccount) _row( AppIcons.bell, context.jz.primaryBackground, AppTheme.primary, '通知设置', onTap: () => context.push('/notification-settings'), ), if (session.isAccount && SyncService.instance.conflictCount > 0) _row( AppIcons.offline, context.jz.warningBackground, AppTheme.orange, '同步冲突', trailing: '${SyncService.instance.conflictCount} 项待处理', onTap: () async { await context.push('/sync-conflicts'); if (mounted) setState(() {}); }, ), AnimatedBuilder( animation: UpdateCoordinator.instance, builder: (context, _) => _row( AppIcons.cloud, context.jz.primaryBackground, AppTheme.primary, '检查更新', trailing: UpdateCoordinator.instance.checking ? '正在检查…' : AppVersion.display, onTap: UpdateCoordinator.instance.checking ? () {} : _checkUpdate, ), ), _row( AppIcons.trash, context.jz.expenseBackground, AppTheme.red, '回收站', trailing: '保留 30 天', onTap: () => context.push('/recycle-bin'), ), ], ), ), SizedBox(height: 12), Card( child: _row( AppIcons.exportIcon, context.jz.expenseBackground, AppTheme.red, session.isGuest ? '退出游客模式' : '退出登录', onTap: _logout, ), ), SizedBox(height: 16), Center( child: Text( AppVersion.display, style: TextStyle(fontSize: 10, color: context.jz.text3), ), ), SizedBox(height: 12), ], ), ), ); } Widget _row( String iconAsset, Color bg, Color fg, String label, { String? trailing, required VoidCallback onTap, }) { return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 13), child: Row( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(8), ), child: Center( child: AppIcons.icon(iconAsset, size: 15, color: fg), ), ), SizedBox(width: 12), Text(label, style: TextStyle(fontSize: 13.5)), Spacer(), if (trailing != null) Text( trailing, style: TextStyle(fontSize: 11.5, color: context.jz.text3), ), AppIcons.icon( AppIcons.chevronRight, size: 16, color: context.jz.text3, ), ], ), ), ); } } class GuestLocalStatusCard extends StatelessWidget { const GuestLocalStatusCard({super.key}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: context.jz.primaryBackground, borderRadius: BorderRadius.circular(12), ), child: Center( child: AppIcons.icon( AppIcons.check, size: 19, color: AppTheme.primaryDeep, ), ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '本机保存中', style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w700), ), SizedBox(height: 3), Text( '账单、分类、预算和统计仅保存在本机,不会自动上传', style: TextStyle( fontSize: 10.5, height: 1.4, color: context.jz.text2, ), ), ], ), ), SizedBox(width: 8), DecoratedBox( decoration: BoxDecoration( color: context.jz.primaryBackground, borderRadius: BorderRadius.all(Radius.circular(12)), ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 9, vertical: 5), child: Text( '游客模式', style: TextStyle( color: AppTheme.primaryDeep, fontSize: 10, fontWeight: FontWeight.w700, ), ), ), ), ], ), ); } } class _ModeOpt extends StatelessWidget { final String title, desc; final String iconAsset; final Color color; final bool selected; final VoidCallback onTap; const _ModeOpt({ required this.title, required this.desc, required this.iconAsset, required this.color, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return Expanded( child: GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(11), decoration: BoxDecoration( color: selected ? context.jz.primaryBackground : context.jz.card, borderRadius: BorderRadius.circular(12), border: Border.all( color: selected ? AppTheme.primary : context.jz.line, width: 1.5, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ AppIcons.icon(iconAsset, size: 20, color: color), Spacer(), Container( width: 15, height: 15, 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: 10, color: Colors.white, ) : null, ), ], ), SizedBox(height: 7), Text( title, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700), ), SizedBox(height: 3), Text( desc, style: TextStyle(fontSize: 9, color: context.jz.text2), ), ], ), ), ), ); } }