Compare commits

..

2 Commits

Author SHA1 Message Date
wangjia 0bc39ebeba docs(claude): 更新节点信息 racknerd → pangolin1(已迁移)
ci-pangolin / Lint — shellcheck (push) Successful in 7s
ci-pangolin / OpenAPI Sync Check (push) Successful in 30s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 6s
ci-pangolin / Flutter — analyze + test (push) Successful in 23s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 4s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 6s
ci-pangolin / Go — build + test (push) Successful in 9s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Successful in 4m20s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
线上节点已从 RackNerd 107.172.55.251(别名 racknerd)迁到
pangolin1 103.119.13.48(Debian12,2核/~1GB/2G swap),单机跑
server+agent+sing-box。旧别名/IP 标注作废。deploy 示例命令同步改名。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:54:11 +08:00
wangjia c550eea5d8 feat(client/auth): 登录/注册页 Enter 键导航与提交
登录:邮箱框回车 → 跳到密码框;密码框回车 → 直接登录(校验条件同主按钮)。
注册:邮箱框回车 → 发送验证码并聚焦验证码框;验证码满 6 位回车 → 进入设密码步
并聚焦密码框;密码框(≥6 位)回车 → 直接注册。

经 TextField.onSubmitted + FocusNode.requestFocus 实现;桌面端回车即触发。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:44:34 +08:00
2 changed files with 49 additions and 9 deletions
+7 -4
View File
@@ -18,18 +18,21 @@ Pangolin 是面向中国大陆用户的终端 VPN(科学上网)项目,**自研
## 部署目标:自有 VPS 节点
当前节点:**RackNerd VPS `107.172.55.251`**(Debian 12 bookworm,root,1 核 / 512MB / 2G swap)。
当前节点:**`pangolin1` = `103.119.13.48`**(Debian 12 bookworm,root,2 核 / ~1GB / 2G swap)。
单机部署:控制面 `pangolin-server` + 节点 `pangolin-agent` + 数据面 `sing-box` 三者同机,均 systemd 常驻。
- **SSH 免密**:本机 `~/.ssh/config` 有别名 `racknerd`(密钥登录,密码登录已禁)→ 直接 `ssh racknerd`
- **SSH 免密**:本机 `~/.ssh/config` 有别名 `pangolin1`(密钥登录,密码登录已禁)→ 直接 `ssh pangolin1`
- 已用 `deploy/bootstrap/init.sh` 加固(ufw + fail2ban + BBR + swap + pangolin 用户)。
- **512MB 内存是硬约束**:控制面用 **SQLite**(见下),不要在这上面跑 MySQL 8。
- **内存仍紧(~1GB)**:控制面用 **SQLite**(见下),不要在这上面跑 MySQL 8。
- 改任何机器系统(装包/改配置/重装)前**先问用户**(只读操作除外)。
> 历史:早期节点为 RackNerd VPS `107.172.55.251`(别名 `racknerd`),已迁离;该别名/IP 作废,勿再用。
## deploy/ 结构
```bash
# ① 新机初始化(幂等,root 跑一次;后续每台新机复用)
# 远程:scp -r deploy/bootstrap racknerd:/root/ && ssh racknerd 'cd /root/bootstrap && bash init.sh'
# 远程:scp -r deploy/bootstrap pangolin1:/root/ && ssh pangolin1 'cd /root/bootstrap && bash init.sh'
deploy/bootstrap/init.sh # 加固/swap/BBR/pangolin用户/ufw/fail2ban/Telegram监控
deploy/bootstrap/monitor/ # pangolin-monitor(节点自报)+ deadman-watch(常在线主机探活)
+42 -5
View File
@@ -128,6 +128,33 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
}
}
// ── Enter 键导航 / 提交 ───────────────────────────────────────────
// 登录:邮箱回车 → 跳密码;密码回车 → 直接登录(校验条件同主按钮 enabled)。
void _submitLogin() {
if (!_loading && _emailValid && _pw.text.isNotEmpty) _doLogin();
}
// 注册:邮箱回车 → 发码并跳验证码;验证码(6位)回车 → 进设密码步并聚焦密码;
// 密码(≥6位)回车 → 直接注册。
void _onRegisterEmailSubmit() {
if (_loading || !_emailValid) return;
if (!_sent) _sendCode();
_codeFocus.requestFocus();
}
void _onRegisterCodeSubmit() {
if (!_sent || _code.text.length != 6) return;
setState(() => _step = 1);
// step 1 的密码框在本帧之后才挂载,延到下一帧再聚焦。
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _pwFocus.requestFocus();
});
}
void _submitRegister() {
if (!_loading && _pw.text.length >= 6) _doRegister();
}
void _setMode(_AuthMode m) => setState(() {
_mode = m;
_step = 0;
@@ -335,12 +362,14 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
kBareInput.copyWith(contentPadding: const EdgeInsets.symmetric(vertical: 13));
// 密码输入 + 显示/隐藏切换(眼睛按钮)。
Widget _pwInput(PangolinScheme c, String hint) => Row(children: [
Widget _pwInput(PangolinScheme c, String hint, {VoidCallback? onSubmit}) => Row(children: [
Expanded(
child: TextField(
controller: _pw,
focusNode: _pwFocus,
obscureText: !_pwVisible,
textInputAction: TextInputAction.done,
onSubmitted: onSubmit == null ? null : (_) => onSubmit(),
decoration: _bare.copyWith(hintText: hint),
onChanged: (_) => setState(() {}),
),
@@ -370,9 +399,12 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
Widget _login(PangolinScheme c, AppText t) {
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_field(c, icon: PangolinIcons.mail, label: t.emailLabel, focused: _emailFocus.hasFocus,
child: TextField(controller: _email, focusNode: _emailFocus, decoration: _bare.copyWith(hintText: t.emailPh), onChanged: (_) => setState(() {}))),
child: TextField(controller: _email, focusNode: _emailFocus,
textInputAction: TextInputAction.next,
onSubmitted: (_) => _pwFocus.requestFocus(),
decoration: _bare.copyWith(hintText: t.emailPh), onChanged: (_) => setState(() {}))),
const SizedBox(height: 16),
_field(c, icon: PangolinIcons.lock, label: t.pwLabel, focused: _pwFocus.hasFocus, child: _pwInput(c, t.pwPh)),
_field(c, icon: PangolinIcons.lock, label: t.pwLabel, focused: _pwFocus.hasFocus, child: _pwInput(c, t.pwPh, onSubmit: _submitLogin)),
Align(
alignment: Alignment.centerRight,
child: Padding(
@@ -391,7 +423,10 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_field(c, icon: PangolinIcons.mail, label: t.emailLabel, focused: _emailFocus.hasFocus,
child: Row(children: [
Expanded(child: TextField(controller: _email, focusNode: _emailFocus, decoration: _bare.copyWith(hintText: t.emailPh), onChanged: (_) => setState(() {}))),
Expanded(child: TextField(controller: _email, focusNode: _emailFocus,
textInputAction: TextInputAction.next,
onSubmitted: (_) => _onRegisterEmailSubmit(),
decoration: _bare.copyWith(hintText: t.emailPh), onChanged: (_) => setState(() {}))),
GestureDetector(
onTap: (!_loading && _emailValid) ? _sendCode : null,
child: Text(_sent ? t.resend : t.sendCode,
@@ -410,6 +445,8 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
const SizedBox(height: 16),
_field(c, icon: PangolinIcons.shieldCheck, label: t.codeLabel, focused: _codeFocus.hasFocus,
child: TextField(controller: _code, focusNode: _codeFocus, keyboardType: TextInputType.number, maxLength: 6,
textInputAction: TextInputAction.next,
onSubmitted: (_) => _onRegisterCodeSubmit(),
style: PangolinText.mono.copyWith(letterSpacing: 6, color: c.fg1),
decoration: _bare.copyWith(counterText: '', hintText: ''), onChanged: (_) => setState(() {}))),
const SizedBox(height: 16),
@@ -434,7 +471,7 @@ class _AuthScreenState extends ConsumerState<AuthScreen>
]),
),
const SizedBox(height: 16),
_field(c, icon: PangolinIcons.lock, label: t.pwLabel, focused: _pwFocus.hasFocus, child: _pwInput(c, t.setPwPh)),
_field(c, icon: PangolinIcons.lock, label: t.pwLabel, focused: _pwFocus.hasFocus, child: _pwInput(c, t.setPwPh, onSubmit: _submitRegister)),
const SizedBox(height: 18),
_primaryBtn(c, label: _loading ? '...' : t.doCreate,
onPressed: (!_loading && _pw.text.length >= 6) ? _doRegister : null),