f97ae8186b
对照 design/ui_kits mobile+tablet 原型逐屏补齐 Flutter 客户端: - l10n 资源层(strings_zh/en,单显)+ Riverpod 状态层(连接状态机/免费额度/ 节点选择/语言/主题),UI 与数据解耦,mock 数据接 API 不动 UI。 - 连接键严格三态(off 虚线轨道环 / connecting 旋转弧 / on 满环+计时+光晕), 状态来自 connectionProvider,点击只派发事件——禁止乐观显示。 - 节点页置顶「智能选择」推荐卡(clay 渐变 zap + 推荐胶囊,默认选中); 免费额度卡(剩余分钟+进度条 ≤3 分钟切 warning + 看广告解锁变绿)。 - Tab 左右滑动切换(手势竞技场仲裁,子页滚动不误触发,200ms 方向感知滑入)。 - iPad/宽屏 ≥900 LayoutBuilder 切侧栏分栏(导航行高 ≥48,连接页双栏/节点双列网格), 复用同一批原子组件,不 fork 页面。 - 语义 token 零硬编码;文案全部走 l10n;套餐数字对齐 §7;无支付表单/emoji 国旗。 - CI 红线词扫描扩展到 client/lib;新增 Flutter analyze+test CI 任务。 - 测试:连接状态机/额度/节点单测、连接键三态与卡片组件测试、 golden(连接键三态/推荐卡/额度卡 × 明暗)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
7.0 KiB
Dart
169 lines
7.0 KiB
Dart
// auth_screen.dart — 登录 / 注册页(文案经 AppText 单显)
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../l10n/app_text.dart';
|
|
import '../pangolin_theme.dart';
|
|
import 'pangolin_button.dart';
|
|
import 'pangolin_icons.dart';
|
|
import 'pangolin_logo.dart';
|
|
|
|
enum _AuthMode { login, register }
|
|
|
|
class AuthScreen extends StatefulWidget {
|
|
const AuthScreen({super.key, required this.onDone, required this.t});
|
|
final VoidCallback onDone;
|
|
final AppText t;
|
|
|
|
@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);
|
|
|
|
@override
|
|
void dispose() {
|
|
_email.dispose();
|
|
_code.dispose();
|
|
_pw.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = context.pangolin;
|
|
final t = widget.t;
|
|
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(t.brand, style: PangolinText.h1.copyWith(color: c.fg1)),
|
|
const SizedBox(height: 4),
|
|
Text(t.authTagline, style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600)),
|
|
]),
|
|
const SizedBox(height: 28),
|
|
Row(children: [
|
|
_tab(_AuthMode.login, t.tabLogin, c),
|
|
_tab(_AuthMode.register, t.tabRegister, c),
|
|
]),
|
|
Divider(height: 1, color: c.border),
|
|
const SizedBox(height: 22),
|
|
Expanded(child: _mode == _AuthMode.login ? _login(c, t) : _register(c, t)),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
child: Text(t.tos, 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, AppText t) {
|
|
return Column(children: [
|
|
_field(c, icon: PangolinIcons.mail, label: t.emailLabel,
|
|
child: TextField(controller: _email, decoration: _bare.copyWith(hintText: t.emailPh), onChanged: (_) => setState(() {}))),
|
|
const SizedBox(height: 16),
|
|
_field(c, icon: PangolinIcons.lock, label: t.pwLabel,
|
|
child: TextField(controller: _pw, obscureText: true, decoration: _bare.copyWith(hintText: t.pwPh), onChanged: (_) => setState(() {}))),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Text(t.forgotPw, style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600)),
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
PangolinButton(label: t.doLogin, expand: true, onPressed: (_emailValid && _pw.text.isNotEmpty) ? widget.onDone : null),
|
|
]);
|
|
}
|
|
|
|
Widget _register(PangolinScheme c, AppText t) {
|
|
if (_step == 0) {
|
|
return Column(children: [
|
|
_field(c, icon: PangolinIcons.mail, label: t.emailLabel,
|
|
child: Row(children: [
|
|
Expanded(child: TextField(controller: _email, decoration: _bare.copyWith(hintText: t.emailPh), onChanged: (_) => setState(() {}))),
|
|
GestureDetector(
|
|
onTap: _emailValid ? () => setState(() => _sent = true) : null,
|
|
child: Text(_sent ? t.resend : t.sendCode,
|
|
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),
|
|
Expanded(child: Text(t.codeSentTo(_email.text), style: PangolinText.caption.copyWith(color: c.success, fontWeight: FontWeight.w400))),
|
|
]),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_field(c, icon: PangolinIcons.shieldCheck, label: t.codeLabel,
|
|
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.doNext, 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.pwLabel,
|
|
child: TextField(controller: _pw, obscureText: true, decoration: _bare.copyWith(hintText: t.setPwPh), onChanged: (_) => setState(() {}))),
|
|
const SizedBox(height: 18),
|
|
PangolinButton(label: t.doCreate, expand: true, onPressed: _pw.text.length >= 6 ? widget.onDone : null),
|
|
]);
|
|
}
|
|
}
|