526e7f2f33
基于 design/flutter/ 起步包在 client/ 下建立 Flutter 工程:
- pubspec.yaml:flutter_svg / lucide_icons / google_fonts 依赖;
SVG 资产注册;字体由 google_fonts 运行时加载,无需本地 ttf
- lib/pangolin_theme.dart:完整设计令牌(色阶 / 间距 / 圆角 /
动效 / 文字阶),PangolinText 改用 GoogleFonts getter,
PangolinTheme.light / .dark 开箱即用
- lib/main.dart:runApp + 明暗主题 + 登录 → 引导 → 主框架 RootFlow
- lib/widgets/:全套组件雏形
pangolin_icons Lucide 图标映射
pangolin_button 胶囊按钮四态
country_code 国家码块 + 信号条
status_pill 色点胶囊
connect_button 核心连接键三态(off/connecting/on + 动画)
server_tile 服务器列表行
plan_card 套餐卡(专业版渐变高亮)
auth_screen 登录 / 注册(邮箱验证码 + 设密码)
onboarding_screen 首次引导 PageView(3 屏可滑动 + 进度点)
home_shell 底部 4 Tab(连接/节点/统计/账户 + 状态机)
account_screens 套餐选择 / 设备管理 / 兑换 / 联系我们
pangolin_logo PangolinMark / BrandLockup / AppIcon(SVG)
- assets/:logo-mark / logo-mark-white / logo-wordmark / app-icon(SVG)
- android/:Kotlin 主 Activity + Manifest + Gradle 配置
- ios/:Runner.xcodeproj + xcscheme + Podfile + AppDelegate + storyboard
运行方式(cd client/):
flutter pub get && flutter run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
7.5 KiB
Dart
156 lines
7.5 KiB
Dart
// auth_screen.dart — 登录 / 注册页
|
|
import 'package:flutter/material.dart';
|
|
import '../pangolin_theme.dart';
|
|
import 'pangolin_icons.dart';
|
|
import 'pangolin_button.dart';
|
|
import 'pangolin_logo.dart';
|
|
|
|
enum _AuthMode { login, register }
|
|
|
|
class AuthScreen extends StatefulWidget {
|
|
const AuthScreen({super.key, required this.onDone, this.zh = true});
|
|
final VoidCallback onDone;
|
|
final bool zh;
|
|
|
|
@override
|
|
State<AuthScreen> createState() => _AuthScreenState();
|
|
}
|
|
|
|
class _AuthScreenState extends State<AuthScreen> {
|
|
_AuthMode _mode = _AuthMode.login;
|
|
int _step = 0;
|
|
bool _sent = false;
|
|
final _email = TextEditingController();
|
|
final _code = TextEditingController();
|
|
final _pw = TextEditingController();
|
|
|
|
bool get _emailValid => RegExp(r'\S+@\S+\.\S+').hasMatch(_email.text);
|
|
|
|
String _t(String zh, String en) => widget.zh ? zh : en;
|
|
|
|
@override
|
|
void dispose() { _email.dispose(); _code.dispose(); _pw.dispose(); super.dispose(); }
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = context.pangolin;
|
|
return Scaffold(
|
|
backgroundColor: c.bg,
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 28),
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 18),
|
|
Column(children: [
|
|
const PangolinMark(size: 52),
|
|
const SizedBox(height: 12),
|
|
Text(widget.zh ? '穿山甲' : 'Pangolin', style: PangolinText.h1.copyWith(color: c.fg1)),
|
|
const SizedBox(height: 4),
|
|
Text(_t('极速 · 稳定 · 省心', 'Fast · Stable · Effortless'),
|
|
style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600)),
|
|
]),
|
|
const SizedBox(height: 28),
|
|
Row(children: [
|
|
_tab(_AuthMode.login, _t('登录', 'Log in'), c),
|
|
_tab(_AuthMode.register, _t('注册', 'Sign up'), c),
|
|
]),
|
|
Divider(height: 1, color: c.border),
|
|
const SizedBox(height: 22),
|
|
Expanded(child: _mode == _AuthMode.login ? _login(c) : _register(c)),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
child: Text(
|
|
_t('继续即代表同意《服务条款》与《隐私政策》', 'By continuing you agree to our Terms & Privacy Policy'),
|
|
textAlign: TextAlign.center,
|
|
style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _tab(_AuthMode m, String label, PangolinScheme c) {
|
|
final on = _mode == m;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => setState(() { _mode = m; _step = 0; _sent = false; }),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: on ? c.accent : Colors.transparent, width: 2))),
|
|
child: Center(child: Text(label, style: PangolinText.body.copyWith(color: on ? c.fg1 : c.fg3, fontWeight: on ? FontWeight.w700 : FontWeight.w500))),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _field(PangolinScheme c, {required IconData icon, required String label, required Widget child}) {
|
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(label, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 7),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
|
decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.md), border: Border.all(color: c.borderStrong, width: 1.5)),
|
|
child: Row(children: [Icon(icon, size: 18, color: c.fg3), const SizedBox(width: 10), Expanded(child: child)]),
|
|
),
|
|
]);
|
|
}
|
|
|
|
InputDecoration get _bare => const InputDecoration(border: InputBorder.none, isCollapsed: true, contentPadding: EdgeInsets.symmetric(vertical: 12));
|
|
|
|
Widget _login(PangolinScheme c) {
|
|
return Column(children: [
|
|
_field(c, icon: PangolinIcons.mail, label: _t('邮箱', 'Email'),
|
|
child: TextField(controller: _email, decoration: _bare.copyWith(hintText: 'your@email.com'), onChanged: (_) => setState(() {}))),
|
|
const SizedBox(height: 16),
|
|
_field(c, icon: PangolinIcons.lock, label: _t('密码', 'Password'),
|
|
child: TextField(controller: _pw, obscureText: true, decoration: _bare.copyWith(hintText: _t('输入密码', 'Enter password')), onChanged: (_) => setState(() {}))),
|
|
Align(alignment: Alignment.centerRight, child: Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Text(_t('忘记密码?', 'Forgot password?'), style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600)),
|
|
)),
|
|
const SizedBox(height: 18),
|
|
PangolinButton(label: _t('登录', 'Log in'), expand: true, onPressed: (_emailValid && _pw.text.isNotEmpty) ? widget.onDone : null),
|
|
]);
|
|
}
|
|
|
|
Widget _register(PangolinScheme c) {
|
|
if (_step == 0) {
|
|
return Column(children: [
|
|
_field(c, icon: PangolinIcons.mail, label: _t('邮箱', 'Email'),
|
|
child: Row(children: [
|
|
Expanded(child: TextField(controller: _email, decoration: _bare.copyWith(hintText: 'your@email.com'), onChanged: (_) => setState(() {}))),
|
|
GestureDetector(
|
|
onTap: _emailValid ? () => setState(() => _sent = true) : null,
|
|
child: Text(_sent ? _t('重新发送', 'Resend') : _t('发送验证码', 'Send code'),
|
|
style: PangolinText.caption.copyWith(color: _emailValid ? c.accent : c.fg3, fontWeight: FontWeight.w700)),
|
|
),
|
|
])),
|
|
if (_sent) Padding(padding: const EdgeInsets.only(top: 8), child: Row(children: [
|
|
Icon(PangolinIcons.checkCircle, size: 14, color: c.success), const SizedBox(width: 6),
|
|
Text('${_t('验证码已发送至', 'Code sent to')} ${_email.text}', style: PangolinText.caption.copyWith(color: c.success, fontWeight: FontWeight.w400)),
|
|
])),
|
|
const SizedBox(height: 16),
|
|
_field(c, icon: PangolinIcons.shieldCheck, label: _t('邮箱验证码', 'Verification code'),
|
|
child: TextField(controller: _code, keyboardType: TextInputType.number, maxLength: 6,
|
|
style: PangolinText.mono.copyWith(letterSpacing: 6, color: c.fg1),
|
|
decoration: _bare.copyWith(counterText: '', hintText: '······'), onChanged: (_) => setState(() {}))),
|
|
const SizedBox(height: 8),
|
|
PangolinButton(label: _t('下一步', 'Next'), expand: true, onPressed: (_sent && _code.text.length == 6) ? () => setState(() => _step = 1) : null),
|
|
]);
|
|
}
|
|
return Column(children: [
|
|
Row(children: [Icon(PangolinIcons.checkCircle, size: 15, color: c.success), const SizedBox(width: 7), Text(_email.text, style: PangolinText.sm.copyWith(color: c.fg2))]),
|
|
const SizedBox(height: 16),
|
|
_field(c, icon: PangolinIcons.lock, label: _t('密码', 'Password'),
|
|
child: TextField(controller: _pw, obscureText: true,
|
|
decoration: _bare.copyWith(hintText: _t('设置登录密码(用于多端登录)', 'Set a password (multi-device)')), onChanged: (_) => setState(() {}))),
|
|
const SizedBox(height: 18),
|
|
PangolinButton(label: _t('创建账户', 'Create account'), expand: true, onPressed: _pw.text.length >= 6 ? widget.onDone : null),
|
|
]);
|
|
}
|
|
}
|