// account_page.dart — 账户页(套餐横幅 + 账户信息 + 设备/兑换/联系 + 语言/主题) import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/responsive/form_factor.dart'; import '../l10n/app_text.dart'; import '../pangolin_theme.dart'; import '../state/account_providers.dart'; import '../state/app_providers.dart'; import '../state/auth_provider.dart'; import '../state/navigation_provider.dart'; import '../widgets/account_screens.dart'; import '../widgets/app_top_bar.dart'; import '../widgets/pangolin_icons.dart'; /// 格式化到期日为 YYYY-MM-DD(本地时区)。 String _fmtDate(DateTime d) { final l = d.toLocal(); String two(int v) => v.toString().padLeft(2, '0'); return '${l.year}-${two(l.month)}-${two(l.day)}'; } class AccountPage extends ConsumerWidget { const AccountPage({super.key, required this.isWide}); final bool isWide; @override Widget build(BuildContext context, WidgetRef ref) { final c = context.pangolin; final t = ref.watch(appTextProvider); final isFree = ref.watch(isFreePlanProvider); final me = ref.watch(meProvider).valueOrNull; final email = (me?.email.isNotEmpty ?? false) ? me!.email : '—'; final expiresLabel = me?.expiresAt != null ? _fmtDate(me!.expiresAt!) : ''; final lang = ref.watch(localeProvider); final isDark = Theme.of(context).brightness == Brightness.dark; final pad = isWide ? 28.0 : 20.0; // desktop 在内容区下钻(切 navView);mobile/tablet 全屏 push。 final isDesktop = context.formFactor == FormFactor.desktop; void open(NavView v, Widget mobilePage) { if (isDesktop) { ref.read(navViewProvider.notifier).state = v; } else { Navigator.of(context).push(MaterialPageRoute(builder: (_) => mobilePage)); } } final body = ListView( padding: EdgeInsets.fromLTRB(pad, isWide ? 6 : 0, pad, 24), children: [ _PlanBanner(t: t, isFree: isFree, email: email, expiresLabel: expiresLabel, onUpgrade: () => open(NavView.plans, PlansScreen(t: t))), const SizedBox(height: 18), // 账户信息 _SectionLabel(text: t.accInfoTitle), _Card(children: [ _InfoRow(icon: PangolinIcons.mail, title: t.accEmail, value: email, trailing: _changeHint(c, t)), Divider(height: 1, color: c.border), _InfoRow(icon: PangolinIcons.lock, title: t.accPassword, value: '••••••••••', trailing: _changeHint(c, t)), ]), const SizedBox(height: 16), // 设备 / 兑换 / 联系 _Card(children: [ _NavRow(icon: PangolinIcons.monitorSmartphone, title: t.myDevices, onTap: () => open(NavView.devices, DevicesScreen(t: t))), Divider(height: 1, color: c.border), _NavRow(icon: PangolinIcons.settings, title: t.settingsTitle, onTap: () => open(NavView.settings, SettingsScreen(t: t))), Divider(height: 1, color: c.border), _NavRow(icon: PangolinIcons.shoppingBag, title: t.redeemEntry, onTap: () => open(NavView.redeem, RedeemScreen(t: t))), Divider(height: 1, color: c.border), _NavRow(icon: PangolinIcons.messageCircle, title: t.contactEntry, onTap: () => open(NavView.contact, ContactScreen(t: t))), ]), const SizedBox(height: 16), // 语言 / 主题 / 协议 _Card(children: [ _CustomRow( icon: PangolinIcons.globe, title: t.language, trailing: _LangSwitch(lang: lang, onChange: (l) => ref.read(localeProvider.notifier).state = l), ), Divider(height: 1, color: c.border), _CustomRow( icon: isDark ? PangolinIcons.moon : PangolinIcons.sun, title: t.darkAppearance, subtitle: isDark ? t.stateOn : t.followLight, trailing: _PangolinSwitch( value: isDark, onChanged: (v) => ref.read(themeModeProvider.notifier).state = v ? ThemeMode.dark : ThemeMode.light, ), ), Divider(height: 1, color: c.border), _CustomRow( icon: PangolinIcons.shield, title: t.protocol, trailing: Text('REALITY / Hysteria2', style: PangolinText.mono.copyWith(color: c.fg3, fontSize: 13)), ), ]), const SizedBox(height: 16), _Card(children: [ _NavRow( icon: PangolinIcons.logOut, title: t.signOut, danger: true, // 清除本地令牌 → authProvider 置未登录 → 根据登录态回登录页。 onTap: () => ref.read(authProvider.notifier).logout(), ), ]), ], ); if (isWide) return body; return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ AppTopBar(brand: t.brand), PageTitle(title: t.meTitle), Expanded(child: body), ]); } Widget _changeHint(PangolinScheme c, AppText t) => Text(t.accChange, style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600, fontSize: 13)); } class _PlanBanner extends StatelessWidget { const _PlanBanner({ required this.t, required this.isFree, required this.email, required this.expiresLabel, required this.onUpgrade, }); final AppText t; final bool isFree; final String email; final String expiresLabel; final VoidCallback onUpgrade; @override Widget build(BuildContext context) { final c = context.pangolin; if (isFree) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.xl), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm, ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration(color: c.bgSubtle, shape: BoxShape.circle), child: Icon(PangolinIcons.user, size: 22, color: c.fg2), ), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(email, style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 2), decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), child: Text(t.freePlanName, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600, fontSize: 11)), ), ])), _UpgradeButton(label: t.upgradeBtn, onTap: onUpgrade), ]), ]), ); } // PRO 渐变横幅 return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [PangolinColors.clay600, PangolinColors.clay800], ), borderRadius: BorderRadius.circular(PangolinRadius.xl), boxShadow: PangolinShadow.md, ), child: Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration(color: PangolinColors.white.withValues(alpha: 0.18), shape: BoxShape.circle), child: const Icon(PangolinIcons.crown, size: 22, color: PangolinColors.white), ), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(email, style: const TextStyle(color: PangolinColors.white, fontWeight: FontWeight.w700, fontSize: 16)), const SizedBox(height: 4), Text(expiresLabel.isEmpty ? t.proMember : '${t.proMember} · $expiresLabel', style: TextStyle(color: PangolinColors.white.withValues(alpha: 0.85), fontSize: 12)), ])), FilledButton( onPressed: onUpgrade, style: FilledButton.styleFrom( backgroundColor: PangolinColors.white, foregroundColor: PangolinColors.clay700, shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), child: Text(t.upgradeBtn, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), ), ]), ); } } class _UpgradeButton extends StatelessWidget { const _UpgradeButton({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { final c = context.pangolin; return FilledButton( onPressed: onTap, style: FilledButton.styleFrom( backgroundColor: c.accent, foregroundColor: c.fgOnAccent, shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), ), child: Text(label, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), ); } } class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.text}); final String text; @override Widget build(BuildContext context) { final c = context.pangolin; return Padding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 10), child: Text(text, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 13)), ); } } class _Card extends StatelessWidget { const _Card({required this.children}); final List children; @override Widget build(BuildContext context) { final c = context.pangolin; return Container( decoration: BoxDecoration( color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm, ), clipBehavior: Clip.antiAlias, child: Column(children: children), ); } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.icon, required this.title, required this.value, this.trailing}); final IconData icon; final String title, value; final Widget? trailing; @override Widget build(BuildContext context) { final c = context.pangolin; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row(children: [ _IconBox(icon: icon), const SizedBox(width: 13), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w500, fontSize: 15)), Text(value, style: PangolinText.caption.copyWith(color: c.fg3)), ])), if (trailing != null) trailing!, ]), ); } } class _NavRow extends StatelessWidget { const _NavRow({required this.icon, required this.title, required this.onTap, this.danger = false}); final IconData icon; final String title; final VoidCallback onTap; final bool danger; @override Widget build(BuildContext context) { final c = context.pangolin; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row(children: [ Icon(icon, size: 20, color: danger ? c.danger : c.accent), const SizedBox(width: 13), Expanded(child: Text(title, style: PangolinText.body.copyWith(color: danger ? c.danger : c.fg1, fontWeight: FontWeight.w500))), if (!danger) Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), ]), ), ); } } class _CustomRow extends StatelessWidget { const _CustomRow({required this.icon, required this.title, this.subtitle, required this.trailing}); final IconData icon; final String title; final String? subtitle; final Widget trailing; @override Widget build(BuildContext context) { final c = context.pangolin; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row(children: [ _IconBox(icon: icon), const SizedBox(width: 13), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w500, fontSize: 15)), if (subtitle != null) Text(subtitle!, style: PangolinText.caption.copyWith(color: c.fg3)), ])), trailing, ]), ); } } class _IconBox extends StatelessWidget { const _IconBox({required this.icon}); final IconData icon; @override Widget build(BuildContext context) { final c = context.pangolin; return Container( width: 32, height: 32, decoration: BoxDecoration(color: c.accentSubtle, borderRadius: BorderRadius.circular(PangolinRadius.sm)), child: Icon(icon, size: 17, color: c.accent), ); } } /// 段控:中文 / EN(单显切换)。 class _LangSwitch extends StatelessWidget { const _LangSwitch({required this.lang, required this.onChange}); final AppLang lang; final ValueChanged onChange; @override Widget build(BuildContext context) { final c = context.pangolin; Widget seg(AppLang v, String label) { final active = lang == v; return GestureDetector( onTap: () => onChange(v), child: Container( padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 5), decoration: BoxDecoration( color: active ? c.accent : Colors.transparent, borderRadius: BorderRadius.circular(PangolinRadius.full), ), child: Text(label, style: PangolinText.caption.copyWith( color: active ? c.fgOnAccent : c.fg3, fontWeight: FontWeight.w700, fontSize: 12.5)), ), ); } return Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), child: Row(mainAxisSize: MainAxisSize.min, children: [ seg(AppLang.zh, '中文'), const SizedBox(width: 2), seg(AppLang.en, 'EN'), ]), ); } } class _PangolinSwitch extends StatelessWidget { const _PangolinSwitch({required this.value, required this.onChanged}); final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { final c = context.pangolin; return GestureDetector( onTap: () => onChanged(!value), child: AnimatedContainer( duration: PangolinMotion.base, curve: PangolinMotion.easeOut, width: 46, height: 28, decoration: BoxDecoration(color: value ? c.accent : c.borderStrong, borderRadius: BorderRadius.circular(PangolinRadius.full)), child: AnimatedAlign( duration: PangolinMotion.base, curve: PangolinMotion.easeOut, alignment: value ? Alignment.centerRight : Alignment.centerLeft, child: Padding( padding: const EdgeInsets.all(3), child: Container(width: 22, height: 22, decoration: const BoxDecoration(color: PangolinColors.white, shape: BoxShape.circle)), ), ), ), ); } }