feat(client): P3 账号/套餐视角/配额接真(#6 6C)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled

- account_providers:新增 meProvider(GET /v1/me,登录态变化自动重取)、
  plansProvider、usageProvider(family by days)。
- isFreePlanProvider 改为派生自 meProvider.plan(去掉可写演示开关)。
- quotaProvider:总额取 plans free.daily_minutes,今日剩余取 me.quota_today_min
  (后端已算好);adUnlocked 保留本地态(ad SDK 未接)。
- account_page:邮箱/套餐/到期接 me;删「演示:免费版视角」开关 + kDemoEmail;
  协议标签 WireGuard→REALITY/Hysteria2。
- 重写 quota_controller_test 为 provider 驱动;account/quota golden 重生成。

flutter analyze 0 error;113 tests passed。统计页(weekly/月流量/延迟)随 P4
一并接(其均延迟依赖 P4 实测探针)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-18 23:51:28 +08:00
parent 3d87bffbe9
commit bb39b36d84
9 changed files with 151 additions and 63 deletions
+25 -21
View File
@@ -5,13 +5,19 @@ 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/navigation_provider.dart';
import '../widgets/account_screens.dart';
import '../widgets/app_top_bar.dart';
import '../widgets/pangolin_icons.dart';
const String kDemoEmail = 'me@pangolin.vpn';
/// 格式化到期日为 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});
@@ -22,6 +28,9 @@ class AccountPage extends ConsumerWidget {
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;
@@ -39,12 +48,12 @@ class AccountPage extends ConsumerWidget {
final body = ListView(
padding: EdgeInsets.fromLTRB(pad, isWide ? 6 : 0, pad, 24),
children: [
_PlanBanner(t: t, isFree: isFree, onUpgrade: () => open(NavView.plans, PlansScreen(t: t))),
_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: kDemoEmail, trailing: _changeHint(c, t)),
_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)),
]),
@@ -79,20 +88,7 @@ class AccountPage extends ConsumerWidget {
_CustomRow(
icon: PangolinIcons.shield,
title: t.protocol,
trailing: Text('WireGuard', style: PangolinText.mono.copyWith(color: c.fg3, fontSize: 13)),
),
]),
const SizedBox(height: 16),
// 演示:免费版视角开关
_Card(children: [
_CustomRow(
icon: PangolinIcons.clock,
title: lang == AppLang.zh ? '演示:免费版视角' : 'Demo: free-tier view',
subtitle: lang == AppLang.zh ? '切换额度卡与套餐横幅' : 'Toggles quota UI & plan banner',
trailing: _PangolinSwitch(
value: isFree,
onChanged: (v) => ref.read(isFreePlanProvider.notifier).state = v,
),
trailing: Text('REALITY / Hysteria2', style: PangolinText.mono.copyWith(color: c.fg3, fontSize: 13)),
),
]),
const SizedBox(height: 16),
@@ -115,9 +111,17 @@ class AccountPage extends ConsumerWidget {
}
class _PlanBanner extends StatelessWidget {
const _PlanBanner({required this.t, required this.isFree, required this.onUpgrade});
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
@@ -141,7 +145,7 @@ class _PlanBanner extends StatelessWidget {
),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(kDemoEmail, style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w700)),
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),
@@ -173,9 +177,9 @@ class _PlanBanner extends StatelessWidget {
),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(kDemoEmail, style: const TextStyle(color: PangolinColors.white, fontWeight: FontWeight.w700, fontSize: 16)),
Text(email, style: const TextStyle(color: PangolinColors.white, fontWeight: FontWeight.w700, fontSize: 16)),
const SizedBox(height: 4),
Text('${t.proMember} · 2026-12-31',
Text(expiresLabel.isEmpty ? t.proMember : '${t.proMember} · $expiresLabel',
style: TextStyle(color: PangolinColors.white.withOpacity(0.85), fontSize: 12)),
])),
FilledButton(