feat(account): IA 重排+邀请入口+删偏好卡+设置/联系 formFactor 门控
账户页重排为:账户信息 → 购买套餐/我的订单/兑换码(三行合一) → 邀请好友(新增, gift 图标+t.inviteEntry/inviteEntrySub,接 NavView.invite) → 设备(常显)/ 设置/联系(仅 mobile,桌面侧栏已有同入口,formFactor 门控避免重复) → 退出。 删语言/主题/协议卡及仅供其用的 _LangSwitch/_PangolinSwitch(l10n getter 保留, Settings 页仍用)。_NavRow 加可选 subtitle 用于邀请卡副标题。 新增 InviteScreen(client/lib/screens/invite_page.dart)占位屏:邀请码/链接卡 (可复制,值写死 PANGOLIN-DEMO)+ 奖励规则三条 + 我的奖励占位卡,顶部 TODO(spec-2) 标注接口点(邀请码/链接由后端签发,奖励进度来自 /v1/invite)。 顺带修复 desktop_shell.dart 非穷尽 switch 编译错误(NavView 加 invite 后未接线, 整个 client 目前无法编译);notifications 分支占位回落账户页,留给 Task 6 接线。 test/widget/account_ia_test.dart 新增桌面态 IA 结构断言(邀请入口存在/语言不在/ 兑换在);account_desktop_nav_test.dart 同步去掉桌面态设置/联系两个失效断言 (改走桌面侧栏,非账户卡),加邀请入口断言。desktop/tablet 账户页 golden 重录。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
// invite_page.dart — 邀请好友(占位屏)
|
||||
//
|
||||
// TODO(spec-2): 邀请码/链接由后端签发,奖励进度来自 /v1/invite。当前码/链接/奖励进度
|
||||
// 均为静态占位,仅供 IA 走查——接后端前不接任何真实业务逻辑。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../l10n/app_text.dart';
|
||||
import '../pangolin_theme.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
|
||||
const String _kDemoInviteCode = 'PANGOLIN-DEMO';
|
||||
const String _kDemoInviteLink = 'https://pangolin.app/i/PANGOLIN-DEMO';
|
||||
|
||||
class InviteScreen extends ConsumerWidget {
|
||||
const InviteScreen({super.key, required this.t, this.onBack, this.embedded = false});
|
||||
final AppText t;
|
||||
final VoidCallback? onBack;
|
||||
final bool embedded;
|
||||
|
||||
Future<void> _copy(BuildContext context, String text, String toastLabel) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(toastLabel),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final c = context.pangolin;
|
||||
|
||||
Widget body = ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 20, 24),
|
||||
children: [
|
||||
_CopyCard(
|
||||
icon: PangolinIcons.gift,
|
||||
label: t.inviteCodeLabel,
|
||||
value: _kDemoInviteCode,
|
||||
mono: true,
|
||||
copyLabel: t.inviteCopyCode,
|
||||
onCopy: () => _copy(context, _kDemoInviteCode, t.copied),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_CopyCard(
|
||||
icon: PangolinIcons.link,
|
||||
label: t.inviteLinkLabel,
|
||||
value: _kDemoInviteLink,
|
||||
mono: false,
|
||||
copyLabel: t.inviteCopyLink,
|
||||
onCopy: () => _copy(context, _kDemoInviteLink, t.copied),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 0, 2, 10),
|
||||
child: Text(t.inviteRewardsTitle, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 13)),
|
||||
),
|
||||
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: [
|
||||
_RewardRow(icon: PangolinIcons.users, text: t.inviteRewardRegister),
|
||||
Divider(height: 1, color: c.border),
|
||||
_RewardRow(icon: PangolinIcons.crown, text: t.inviteRewardFirstBuy),
|
||||
Divider(height: 1, color: c.border),
|
||||
_RewardRow(icon: PangolinIcons.messageCircle, text: t.inviteRewardJoinTg),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 0, 2, 10),
|
||||
child: Text(t.inviteProgressTitle, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 13)),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: c.surface,
|
||||
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
||||
border: Border.all(color: c.border),
|
||||
boxShadow: PangolinShadow.sm,
|
||||
),
|
||||
child: Column(children: [
|
||||
Icon(PangolinIcons.gift, size: 26, color: c.fg3),
|
||||
const SizedBox(height: 10),
|
||||
Text('—', style: PangolinText.body.copyWith(color: c.fg3)),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (embedded) return body;
|
||||
return Scaffold(
|
||||
backgroundColor: c.bg,
|
||||
body: SafeArea(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 16, 10),
|
||||
child: Row(children: [
|
||||
IconButton(
|
||||
onPressed: onBack ?? () => Navigator.of(context).maybePop(),
|
||||
icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1),
|
||||
),
|
||||
Text(t.inviteTitle, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)),
|
||||
]),
|
||||
),
|
||||
Expanded(child: body),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CopyCard extends StatelessWidget {
|
||||
const _CopyCard({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.mono,
|
||||
required this.copyLabel,
|
||||
required this.onCopy,
|
||||
});
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final bool mono;
|
||||
final String copyLabel;
|
||||
final VoidCallback onCopy;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
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: 32, height: 32,
|
||||
decoration: BoxDecoration(color: c.accentSubtle, borderRadius: BorderRadius.circular(PangolinRadius.sm)),
|
||||
child: Icon(icon, size: 17, color: c.accent),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(label, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w700, fontSize: 13.5)),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Text(value,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: mono
|
||||
? PangolinText.mono.copyWith(color: c.fg1, fontWeight: FontWeight.w700, letterSpacing: 1.2, fontSize: 15)
|
||||
: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onCopy,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: c.accent,
|
||||
side: BorderSide(color: c.accentBorder),
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
),
|
||||
icon: Icon(PangolinIcons.copy, size: 15),
|
||||
label: Text(copyLabel, style: PangolinText.caption.copyWith(fontWeight: FontWeight.w700, fontSize: 12.5)),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardRow extends StatelessWidget {
|
||||
const _RewardRow({required this.icon, required this.text});
|
||||
final IconData icon;
|
||||
final String text;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 32, height: 32,
|
||||
decoration: BoxDecoration(color: c.accentSubtle, borderRadius: BorderRadius.circular(PangolinRadius.sm)),
|
||||
child: Icon(icon, size: 17, color: c.accent),
|
||||
),
|
||||
const SizedBox(width: 13),
|
||||
Expanded(child: Text(text, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w500, fontSize: 14.5))),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user