fix: improve phone number input handling and standardize browser autofill behavior
Project CI / Frontend tests (pull_request) Successful in 1m37s
Project CI / Repository checks (pull_request) Successful in 2m49s
Project CI / Native shell tests (pull_request) Successful in 6m10s
Project CI / Backend tests (pull_request) Successful in 6m59s

- Updated `autoComplete` attributes to `tel-national` for better compatibility with domestic phone numbers.
- Added `normalizePhoneInput` in `authService` to handle browser autofills with `+86` or `86` prefixes for mainland China numbers.
- Enhanced tests to cover phone number normalization logic.
This commit is contained in:
2026-07-22 11:46:47 +08:00
parent ae9502d9ed
commit bfb4d3d3b9
6 changed files with 28 additions and 7 deletions
@@ -1790,6 +1790,14 @@
- 验证:即使 `/api/auth/login-options` 返回空、失败或只返回 `["password"]`,登录弹窗也应同时显示 `短信登录``密码登录``验证码` 输入和“获取验证码”按钮;短信发送真实可用性再通过 `POST /api/auth/phone/send-code` 验证。
- 关联:`src/components/auth/AuthGate.tsx``src/components/auth/LoginScreen.tsx``src/components/auth/AuthGate.test.tsx``scripts/dev-utils.mjs``scripts/dev.mjs`
## 浏览器自动填充手机号带 `+86`
- 现象:登录弹窗的手机号被浏览器回填为 `+86 1xxxxxxxxxx`,点击获取验证码或登录后返回“手机号格式不正确”。
- 原因:`autocomplete="tel"` 允许浏览器回填含国家码的完整电话号码,`inputMode="numeric"` 只提示软键盘布局,不会过滤自动填充;历史 `normalizePhoneInput` 还保留了 `+`,而后端大陆手机号边界只接受 11 位国内号码。
- 处理:手机号字段用 `autocomplete="tel-national"` 提示国内号码,并在 `authService.normalizePhoneInput` 统一把 `+86 1xxxxxxxxxx``86 1xxxxxxxxxx` 收口为 `1xxxxxxxxxx`;所有认证请求和最近登录号码缓存继续共用该边界,不在单个页签临时特判。
- 验证:`npm run test -- src/services/authService.test.ts`,覆盖带 `+86`、不带加号的 `86` 和普通 11 位国内号码。
- 关联:`src/services/authService.ts``src/services/authService.test.ts``src/components/auth/LoginScreen.tsx``src/components/auth/BindPhoneScreen.tsx`
## 本地短信收不到验证码先查 provider
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
@@ -55,6 +55,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
9. 账号信息面板只展示 `账号信息` 标题;绑定手机号和绑定微信以紧凑模块展示当前绑定状态,已绑定手机号展示完整手机号,已绑定微信优先展示微信平台实际返回并由后端保存的 `wechatDisplayName`。小程序 `jscode2session` 不能直接返回微信昵称或个人微信号,只能稳定拿到当前小程序维度的 `openid`,并在满足微信开放平台条件时拿到 `unionid`;小程序昵称来自快捷登录后按需展示的原生 `input type="nickname"` 提交的 `displayName`。后端下发 `wechatAccount` 作为绑定账号标识,前端在没有真实昵称时展示微信账号尾号,不展示裸“已绑定”。换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。
10. H5 登录态从未登录变为已登录,或从已登录变为未登录后,必须刷新当前页面一次,确保推荐运行态、作品架、个人缓存和私有 query 都按新身份重新初始化;普通 access token 续期、账号资料更新和同一登录态内的设置变化不得触发整页刷新。
11. 同一账号允许多端同时在线。新增登录和单设备退出只影响对应 refresh session,不得提升账号级 `tokenVersion` 让其它设备的 access token 失效;只有“退出全部设备”、修改密码、重置密码等明确安全动作才吊销全端 refresh session 并提升 `tokenVersion`
12. 主站手机号输入以中国大陆 11 位国内号码为前端提交口径;输入框使用 `tel-national` 提示浏览器回填国内号码,认证 service 边界还必须把浏览器可能回填的 `+86 1xxxxxxxxxx``86 1xxxxxxxxxx` 收口为 `1xxxxxxxxxx`,再用于验证码、密码登录、绑定、换绑、重置密码和最近登录号码缓存;不得只依赖 `inputMode="numeric"`,它只是软键盘提示,不会限制自动填充值。
## 账户与充值
+1 -1
View File
@@ -118,7 +118,7 @@ export function BindPhoneScreen({
</PlatformFieldLabel>
<PlatformTextField
autoComplete="tel"
autoComplete="tel-national"
inputMode="numeric"
value={phone}
onChange={(event) => setPhone(event.target.value)}
+3 -3
View File
@@ -261,7 +261,7 @@ export function LoginScreen({
</PlatformFieldLabel>
<PlatformTextField
autoComplete="tel"
autoComplete="tel-national"
inputMode="numeric"
value={phone}
onChange={(event) => setPhone(event.target.value)}
@@ -504,7 +504,7 @@ function PhoneCodeForm({
</PlatformFieldLabel>
<PlatformTextField
autoComplete="tel"
autoComplete="tel-national"
inputMode="numeric"
value={phone}
onChange={(event) => onPhoneChange(event.target.value)}
@@ -600,7 +600,7 @@ function PasswordResetPanel({
</PlatformFieldLabel>
<PlatformTextField
autoComplete="tel"
autoComplete="tel-national"
inputMode="numeric"
value={phone}
onChange={(event) => onPhoneChange(event.target.value)}
+9 -2
View File
@@ -49,6 +49,7 @@ import {
liftAuthRiskBlock,
loginWithPhoneCode,
logoutAllAuthSessions,
normalizePhoneInput,
redeemRegistrationInviteCode,
requestWechatMiniProgramPhoneLogin,
revokeAuthSession,
@@ -104,6 +105,12 @@ describe('authService', () => {
clearStoredAccessToken({ emit: false });
});
it('normalizes mainland China browser autofill phone numbers to national format', () => {
expect(normalizePhoneInput('+86 198 7654 3210')).toBe('19876543210');
expect(normalizePhoneInput('86-198-7654-3210')).toBe('19876543210');
expect(normalizePhoneInput('198 7654 3210')).toBe('19876543210');
});
it('auth entry posts phone password credentials and 写入 access token', async () => {
apiClientMocks.requestJson.mockResolvedValue({
token: 'jwt-entry-token',
@@ -217,7 +224,7 @@ describe('authService', () => {
providerRequestId: 'mock-request-id',
});
const result = await sendPhoneLoginCode(' 138 0013 8000 ');
const result = await sendPhoneLoginCode('+86 138 0013 8000');
expect(result.cooldownSeconds).toBe(60);
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
@@ -277,7 +284,7 @@ describe('authService', () => {
});
const response = await loginWithPhoneCode(
'13800138000',
'+86 138 0013 8000',
'123456',
'spring-2026',
);
+6 -1
View File
@@ -69,7 +69,12 @@ const PUBLIC_AUTH_REQUEST_OPTIONS = {
const LAST_LOGIN_PHONE_STORAGE_KEY = 'genarrative:last-login-phone';
export function normalizePhoneInput(phoneInput: string) {
return phoneInput.replace(/[^\d+]/gu, '').trim();
const compactPhone = phoneInput.trim().replace(/[^\d+]/gu, '');
const mainlandChinaInternationalPhone = compactPhone.match(
/^\+?86(1\d{10})$/u,
);
return mainlandChinaInternationalPhone?.[1] ?? compactPhone;
}
export function normalizeInviteCodeInput(inviteCode: string | undefined) {