merge: WU1(后端+前端) 并入内测阶段1 integration
This commit is contained in:
commit
7ecf016914
@ -39,6 +39,35 @@ export interface InviteRegisterReq {
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名密码注册请求体(贴 PasswordRegisterReqVO;内测种子用户建号即登录,WU1 §3.2)。
|
||||
* 前端校验仅提示(信任边界在后端):username 须匹配 ^[a-zA-Z0-9]{4,30}$、password 6-32 位;
|
||||
* 用户名已占用后端返回 1-002-090-004,由 request 拦截器统一 Toast。
|
||||
*/
|
||||
export interface PasswordRegisterReq {
|
||||
/** 用户名(登录主键之一;^[a-zA-Z0-9]{4,30}$) */
|
||||
username: string
|
||||
/** 密码(明文,6-32 位;后端立即 BCrypt 编码,前端不落日志、不回显) */
|
||||
password: string
|
||||
/** 昵称(可选;不传则后端默认取用户名) */
|
||||
nickname?: string
|
||||
/** 客户端 anonId(可选;仅注册落 first_anon_id 一次,匿名↔登录归因) */
|
||||
anonId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名密码登录请求体(贴 PasswordLoginReqVO;WU1 §3.2)。
|
||||
* 防枚举:后端对「查无用户名」与「密码错误」统一返 1-002-090-005(文案一致,不可区分)。
|
||||
*/
|
||||
export interface PasswordLoginReq {
|
||||
/** 用户名(登录主键之一;^[a-zA-Z0-9]{4,30}$) */
|
||||
username: string
|
||||
/** 密码(明文;登录不重复长度校验,仅非空) */
|
||||
password: string
|
||||
/** 客户端 anonId(可选;登录不落库,仅遥测归因透传) */
|
||||
anonId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录/注册成功响应(贴 LoginRespVO;sms-login 与 invite-register 共用)。
|
||||
* token 不落日志、手机号不在此返回(安全基线)。
|
||||
@ -65,8 +94,10 @@ export interface LoginResp {
|
||||
export interface PlayerMeResp {
|
||||
/** 玩家编号 */
|
||||
userId: number
|
||||
/** 脱敏手机号(如 138****0000;后端禁回明文) */
|
||||
/** 脱敏手机号(如 138****0000;后端禁回明文;用户名用户 mobile 为 null 时回空串) */
|
||||
mobile: string
|
||||
/** 用户名(用户名密码路身份展示,非敏感明文;手机号用户为空串,WU1 §3.2) */
|
||||
username: string
|
||||
/** 昵称 */
|
||||
nickname: string
|
||||
/** 头像 URL */
|
||||
@ -117,6 +148,34 @@ export function inviteRegister(dto: InviteRegisterReq): Promise<LoginResp> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名密码注册(建号即登录,@PermitAll 免登;受总开关 password-auth-enabled 管控,WU1 §3.2)。
|
||||
* POST /app-api/passport/password-register
|
||||
* @param dto 注册请求体(username + password + 可选 nickname/anonId)
|
||||
* @returns 真 OAuth2 token 与玩家身份摘要(与短信路 token 一字不差)
|
||||
*/
|
||||
export function passwordRegister(dto: PasswordRegisterReq): Promise<LoginResp> {
|
||||
return request<LoginResp>({
|
||||
url: '/app-api/passport/password-register',
|
||||
method: 'post',
|
||||
data: dto,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名密码登录(@PermitAll 免登;防枚举:查无用户名与密码错误统一 1-002-090-005,WU1 §3.2)。
|
||||
* POST /app-api/passport/password-login
|
||||
* @param dto 登录请求体(username + password + 可选 anonId)
|
||||
* @returns 真 OAuth2 token 与玩家身份摘要(同 sms-login 响应)
|
||||
*/
|
||||
export function passwordLogin(dto: PasswordLoginReq): Promise<LoginResp> {
|
||||
return request<LoginResp>({
|
||||
url: '/app-api/passport/password-login',
|
||||
method: 'post',
|
||||
data: dto,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录玩家信息(登录态必需;未登录返回 401,由 request.ts 401 拦截兜底)。
|
||||
* GET /app-api/passport/me
|
||||
|
||||
@ -9,18 +9,28 @@ export default {
|
||||
subtitle: 'Sign in to create and interact with your mini-games',
|
||||
tabSms: 'Code login',
|
||||
tabInvite: 'Invite register',
|
||||
tabPassword: 'Username',
|
||||
mobileLabel: 'Phone',
|
||||
mobilePlaceholder: 'Enter phone number',
|
||||
codeLabel: 'Code',
|
||||
codePlaceholder: 'Enter SMS code',
|
||||
inviteLabel: 'Invite code',
|
||||
invitePlaceholder: 'Enter beta invite code',
|
||||
modeLogin: 'Log in',
|
||||
modeRegister: 'Register',
|
||||
usernameLabel: 'Username',
|
||||
usernamePlaceholder: '4-30 letters or digits',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: '6-32 characters',
|
||||
passwordHint: 'Seed users: register and sign in with a username and password, no phone needed',
|
||||
sendCode: 'Get code',
|
||||
sending: 'Sending…',
|
||||
resendIn: 'Resend in {n}s',
|
||||
smsHint: 'Unregistered numbers will be registered and signed in automatically',
|
||||
inviteHint: 'Beta invite: currently in restricted activation, an invite code is required',
|
||||
invalidMobile: 'Please enter a valid phone number',
|
||||
invalidUsername: 'Username must be 4-30 letters or digits',
|
||||
invalidPassword: 'Password must be 6-32 characters',
|
||||
codeSent: 'Code sent (seed-phase Debug channel, check the backend for the code)',
|
||||
needAgree: 'Please read and accept the User Agreement and Privacy Policy first',
|
||||
loginSuccess: 'Signed in',
|
||||
@ -31,6 +41,8 @@ export default {
|
||||
agreePlaceholder: '(placeholder, final copy pending legal)',
|
||||
submitSms: 'Log in / register',
|
||||
submitInvite: 'Register & log in',
|
||||
submitPasswordLogin: 'Log in',
|
||||
submitPasswordRegister: 'Register & log in',
|
||||
},
|
||||
|
||||
// —— Share landing ——
|
||||
|
||||
@ -10,9 +10,10 @@ export default {
|
||||
login: {
|
||||
// 品牌副标题(标题本身用 common.brand)
|
||||
subtitle: '登录后即可创作、互动你的小游戏',
|
||||
// 两 Tab 名
|
||||
// 三 Tab 名
|
||||
tabSms: '验证码登录',
|
||||
tabInvite: '邀请码注册',
|
||||
tabPassword: '用户名密码',
|
||||
// 手机号 / 验证码 / 邀请码占位符 + 标签
|
||||
mobileLabel: '手机号',
|
||||
mobilePlaceholder: '请输入手机号',
|
||||
@ -20,6 +21,14 @@ export default {
|
||||
codePlaceholder: '请输入短信验证码',
|
||||
inviteLabel: '邀请码',
|
||||
invitePlaceholder: '请输入内测邀请码',
|
||||
// 用户名密码(第三 Tab,内测种子用户;WU1 §3.5):子模式切换 + 输入占位 + 提示
|
||||
modeLogin: '登录',
|
||||
modeRegister: '注册',
|
||||
usernameLabel: '用户名',
|
||||
usernamePlaceholder: '4-30 位数字或字母',
|
||||
passwordLabel: '密码',
|
||||
passwordPlaceholder: '6-32 位密码',
|
||||
passwordHint: '内测种子用户:用户名 + 密码即可注册登录,无需手机号',
|
||||
// 发码按钮三态:默认 / 发送中 / 倒计时重发({n} 为剩余秒数,模板插值)
|
||||
sendCode: '获取验证码',
|
||||
sending: '发送中…',
|
||||
@ -29,6 +38,8 @@ export default {
|
||||
inviteHint: '内测邀请:当前为受限激活期,需邀请码注册',
|
||||
// 表单校验 / 流程 Toast
|
||||
invalidMobile: '请输入正确的手机号',
|
||||
invalidUsername: '用户名为 4-30 位数字或字母',
|
||||
invalidPassword: '密码长度为 6-32 位',
|
||||
codeSent: '验证码已发送(种子期 Debug 渠道,后台查码)',
|
||||
needAgree: '请先阅读并勾选用户协议与隐私政策',
|
||||
loginSuccess: '登录成功',
|
||||
@ -38,9 +49,11 @@ export default {
|
||||
agreeAnd: '与',
|
||||
agreePrivacy: '《隐私政策》',
|
||||
agreePlaceholder: '(占位,正式文案待法务)',
|
||||
// 提交按钮两态(按当前 Tab)
|
||||
// 提交按钮多态(按当前 Tab;用户名密码 Tab 再按登录/注册子模式)
|
||||
submitSms: '登录 / 注册',
|
||||
submitInvite: '注册并登录',
|
||||
submitPasswordLogin: '登录',
|
||||
submitPasswordRegister: '注册并登录',
|
||||
},
|
||||
|
||||
// —— 分享落地页(Share.vue,/share/:gameId)——
|
||||
|
||||
@ -3,10 +3,13 @@
|
||||
* 登录页 | Login.vue(路由 /login,真实鉴权件 spec §9.1)
|
||||
* ----------------------------------------------------------------------------
|
||||
* 职责:
|
||||
* 1. 两 Tab 登录/注册:
|
||||
* 1. 三 Tab 登录/注册:
|
||||
* ① 手机号 + 验证码(主路径,拍板1C):send-sms-code 发码 → sms-login 登录/自动注册一体;
|
||||
* ② 邀请码注册(受限激活期陌生玩家入口,拍板2/3):mobile + inviteCode → invite-register,
|
||||
* 页内文案注明「内测邀请」。
|
||||
* ③ 用户名密码(内测种子用户,WU1 §3.5):username + password,子模式登录/注册二选一
|
||||
* → password-login / password-register(建号即登录);前端做与后端一致的用户名正则+
|
||||
* 密码长度预校验(仅提示,信任边界在后端)。
|
||||
* 2. 隐私政策/用户协议占位勾选(P-ACC-02 占位提前;未勾选不可提交)。
|
||||
* 3. 携带 useUserStore().anonId 入参(匿名↔登录身份衔接,拍板7=B)。
|
||||
* 4. 支持 ?redirect= 回跳(守卫/互动跳转登录后回到原页)。
|
||||
@ -19,7 +22,14 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from 'vant'
|
||||
import { useUserStore } from '../../store/user'
|
||||
import { sendSmsCode, smsLogin, inviteRegister, type LoginResp } from '../../api/passport'
|
||||
import {
|
||||
sendSmsCode,
|
||||
smsLogin,
|
||||
inviteRegister,
|
||||
passwordRegister,
|
||||
passwordLogin,
|
||||
type LoginResp,
|
||||
} from '../../api/passport'
|
||||
import { track } from '../../telemetry'
|
||||
import { AppButton } from '../../components'
|
||||
import entryZh from '../../locales/entry.zh'
|
||||
@ -36,15 +46,21 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
/** 当前 Tab:'sms'=验证码登录(默认主路径) / 'invite'=邀请码注册 */
|
||||
const activeTab = ref<'sms' | 'invite'>('sms')
|
||||
/** 当前 Tab:'sms'=验证码登录(默认主路径) / 'invite'=邀请码注册 / 'password'=用户名密码 */
|
||||
const activeTab = ref<'sms' | 'invite' | 'password'>('sms')
|
||||
|
||||
/** 手机号(两 Tab 共用) */
|
||||
/** 手机号(sms/invite 两 Tab 共用;password Tab 不用) */
|
||||
const mobile = ref<string>('')
|
||||
/** 短信验证码(sms Tab) */
|
||||
const code = ref<string>('')
|
||||
/** 邀请码(invite Tab) */
|
||||
const inviteCode = ref<string>('')
|
||||
/** 用户名(password Tab) */
|
||||
const username = ref<string>('')
|
||||
/** 密码(password Tab;不 trim,允许含空格,绝不落日志/遥测) */
|
||||
const password = ref<string>('')
|
||||
/** 用户名密码 Tab 子模式:'login'=登录(默认)/ 'register'=注册(建号即登录) */
|
||||
const passwordMode = ref<'login' | 'register'>('login')
|
||||
/** 隐私政策/用户协议勾选(占位;未勾选不可提交) */
|
||||
const agreed = ref<boolean>(false)
|
||||
|
||||
@ -60,6 +76,21 @@ const MOBILE_RE = /^1[3-9]\d{9}$/
|
||||
/** 手机号是否合法 */
|
||||
const mobileValid = computed(() => MOBILE_RE.test(mobile.value.trim()))
|
||||
|
||||
/** 用户名校验:与后端 PasswordReqVO 一致 ^[a-zA-Z0-9]{4,30}$(前端仅提示,信任边界在后端) */
|
||||
const USERNAME_RE = /^[a-zA-Z0-9]{4,30}$/
|
||||
/** 用户名是否合法 */
|
||||
const usernameValid = computed(() => USERNAME_RE.test(username.value.trim()))
|
||||
/**
|
||||
* 密码是否合法(与后端一致的长度预校验,仅提示):
|
||||
* - 注册:6-32 位(后端 @Length(6,32));
|
||||
* - 登录:仅非空(后端登录不重复长度校验,避免给出策略线索)。
|
||||
* 密码不 trim(前导/尾随空格可能是密码一部分)。
|
||||
*/
|
||||
const passwordValid = computed(() => {
|
||||
const len = password.value.length
|
||||
return passwordMode.value === 'register' ? len >= 6 && len <= 32 : len > 0
|
||||
})
|
||||
|
||||
/**
|
||||
* 解析回跳地址(?redirect=)。
|
||||
* 仅接受站内相对路径(以 / 开头且非 //),防开放重定向;缺省回 /feed。
|
||||
@ -78,11 +109,26 @@ const canSend = computed(() => mobileValid.value && !sending.value && countdown.
|
||||
|
||||
/** 提交按钮是否可点(按当前 Tab 校验必填项 + 已勾选协议 + 不在提交中) */
|
||||
const canSubmit = computed(() => {
|
||||
if (!agreed.value || submitting.value || !mobileValid.value) return false
|
||||
if (!agreed.value || submitting.value) return false
|
||||
if (activeTab.value === 'password') {
|
||||
// 用户名密码 Tab:不依赖手机号,按 username 正则 + 密码长度(子模式相关)预校验
|
||||
return usernameValid.value && passwordValid.value
|
||||
}
|
||||
// sms / invite 两 Tab 共用手机号校验
|
||||
if (!mobileValid.value) return false
|
||||
if (activeTab.value === 'sms') return code.value.trim().length > 0
|
||||
return inviteCode.value.trim().length > 0
|
||||
})
|
||||
|
||||
/** 提交按钮文案(按当前 Tab;用户名密码 Tab 再按登录/注册子模式二态) */
|
||||
const submitLabel = computed(() => {
|
||||
if (activeTab.value === 'sms') return t('login.submitSms')
|
||||
if (activeTab.value === 'invite') return t('login.submitInvite')
|
||||
return passwordMode.value === 'register'
|
||||
? t('login.submitPasswordRegister')
|
||||
: t('login.submitPasswordLogin')
|
||||
})
|
||||
|
||||
/** 启动 60s 发码倒计时 */
|
||||
function startCountdown(): void {
|
||||
countdown.value = 60
|
||||
@ -138,12 +184,21 @@ function afterLoginSuccess(resp: LoginResp): void {
|
||||
/**
|
||||
* 提交(按当前 Tab 分派):
|
||||
* - sms:smsLogin(已注册登录 / 未注册自动注册一体);
|
||||
* - invite:inviteRegister(手机号+邀请码受限激活)。
|
||||
* 两路均携带 anonId(身份衔接,可选);失败由拦截器 Toast 错误码,仅解锁。
|
||||
* - invite:inviteRegister(手机号+邀请码受限激活);
|
||||
* - password:登录子模式 passwordLogin / 注册子模式 passwordRegister(建号即登录)。
|
||||
* 三路均携带 anonId(身份衔接,可选);失败由拦截器 Toast 错误码(1-002-090/091),仅解锁。
|
||||
*/
|
||||
async function onSubmit(): Promise<void> {
|
||||
if (!canSubmit.value) {
|
||||
if (!agreed.value) showToast({ message: t('login.needAgree'), type: 'fail' })
|
||||
if (!agreed.value) {
|
||||
showToast({ message: t('login.needAgree'), type: 'fail' })
|
||||
return
|
||||
}
|
||||
// 用户名密码 Tab 的预校验提示(仅本地提示,后端仍会二次校验)
|
||||
if (activeTab.value === 'password') {
|
||||
if (!usernameValid.value) showToast({ message: t('login.invalidUsername'), type: 'fail' })
|
||||
else if (!passwordValid.value) showToast({ message: t('login.invalidPassword'), type: 'fail' })
|
||||
}
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
@ -152,16 +207,23 @@ async function onSubmit(): Promise<void> {
|
||||
let resp: LoginResp
|
||||
if (activeTab.value === 'sms') {
|
||||
resp = await smsLogin({ mobile: mobile.value.trim(), code: code.value.trim(), anonId })
|
||||
} else {
|
||||
} else if (activeTab.value === 'invite') {
|
||||
resp = await inviteRegister({
|
||||
mobile: mobile.value.trim(),
|
||||
inviteCode: inviteCode.value.trim(),
|
||||
anonId,
|
||||
})
|
||||
} else {
|
||||
// 用户名密码路:username 去空白后提交;password 原样(不 trim);子模式决定注册/登录端点
|
||||
const uname = username.value.trim()
|
||||
resp =
|
||||
passwordMode.value === 'register'
|
||||
? await passwordRegister({ username: uname, password: password.value, anonId })
|
||||
: await passwordLogin({ username: uname, password: password.value, anonId })
|
||||
}
|
||||
afterLoginSuccess(resp)
|
||||
} catch {
|
||||
// 错误码(验证码错误 / 邀请码无效·耗尽·过期 / 已注册等 1-002-090/091)已由拦截器 Toast;解锁停留本页
|
||||
// 错误码(验证码错误 / 邀请码无效·耗尽·过期 / 用户名已占用·用户名或密码错误 1-002-090/091)已由拦截器 Toast;解锁停留本页
|
||||
submitting.value = false
|
||||
}
|
||||
// 成功后 router.replace 跳转,本组件卸载,无需复位 submitting
|
||||
@ -176,7 +238,7 @@ async function onSubmit(): Promise<void> {
|
||||
<p class="login-sub">{{ t('login.subtitle') }}</p>
|
||||
</header>
|
||||
|
||||
<!-- Tab 切换:验证码登录 / 邀请码注册 -->
|
||||
<!-- Tab 切换:验证码登录 / 邀请码注册 / 用户名密码 -->
|
||||
<div class="login-tabs">
|
||||
<button
|
||||
class="login-tab"
|
||||
@ -194,13 +256,21 @@ async function onSubmit(): Promise<void> {
|
||||
>
|
||||
{{ t('login.tabInvite') }}
|
||||
</button>
|
||||
<button
|
||||
class="login-tab"
|
||||
:class="{ 'is-active': activeTab === 'password' }"
|
||||
type="button"
|
||||
@click="activeTab = 'password'"
|
||||
>
|
||||
{{ t('login.tabPassword') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 表单区 -->
|
||||
<section class="login-form">
|
||||
<!-- 手机号(两 Tab 共用)。保留原生 input:maxlength/inputmode/type 属输入校验 UX,
|
||||
与倒计时/校验逻辑相关,不替成 Field(Field 不透传 maxlength/inputmode),仅令牌化样式 + i18n 占位。 -->
|
||||
<div class="field">
|
||||
<!-- 手机号(sms/invite 两 Tab 共用;password Tab 无手机号,不渲染)。保留原生 input:
|
||||
maxlength/inputmode/type 属输入校验 UX,与倒计时/校验逻辑相关,不替成 Field,仅令牌化样式 + i18n 占位。 -->
|
||||
<div v-if="activeTab !== 'password'" class="field">
|
||||
<input
|
||||
v-model="mobile"
|
||||
class="field-input"
|
||||
@ -230,7 +300,7 @@ async function onSubmit(): Promise<void> {
|
||||
</template>
|
||||
|
||||
<!-- 邀请码注册:邀请码(受限激活期陌生玩家入口) -->
|
||||
<template v-else>
|
||||
<template v-else-if="activeTab === 'invite'">
|
||||
<div class="field">
|
||||
<input
|
||||
v-model="inviteCode"
|
||||
@ -243,6 +313,51 @@ async function onSubmit(): Promise<void> {
|
||||
<p class="login-hint">{{ t('login.inviteHint') }}</p>
|
||||
</template>
|
||||
|
||||
<!-- 用户名密码:子模式(登录/注册)切换 + 用户名 + 密码(内测种子用户,WU1 §3.5) -->
|
||||
<template v-else>
|
||||
<!-- 登录/注册子模式切换(注册走 password-register 建号即登录,登录走 password-login) -->
|
||||
<div class="pwd-mode">
|
||||
<button
|
||||
class="pwd-mode-btn"
|
||||
:class="{ 'is-active': passwordMode === 'login' }"
|
||||
type="button"
|
||||
@click="passwordMode = 'login'"
|
||||
>
|
||||
{{ t('login.modeLogin') }}
|
||||
</button>
|
||||
<button
|
||||
class="pwd-mode-btn"
|
||||
:class="{ 'is-active': passwordMode === 'register' }"
|
||||
type="button"
|
||||
@click="passwordMode = 'register'"
|
||||
>
|
||||
{{ t('login.modeRegister') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<input
|
||||
v-model="username"
|
||||
class="field-input"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="username"
|
||||
maxlength="30"
|
||||
:placeholder="t('login.usernamePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<input
|
||||
v-model="password"
|
||||
class="field-input"
|
||||
type="password"
|
||||
:autocomplete="passwordMode === 'register' ? 'new-password' : 'current-password'"
|
||||
maxlength="32"
|
||||
:placeholder="t('login.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<p class="login-hint">{{ t('login.passwordHint') }}</p>
|
||||
</template>
|
||||
|
||||
<!-- 隐私政策/用户协议占位勾选(未勾选不可提交)。协议/隐私链接名 + 占位说明均抽 key。 -->
|
||||
<label class="agree">
|
||||
<input v-model="agreed" type="checkbox" class="agree-box" />
|
||||
@ -255,9 +370,9 @@ async function onSubmit(): Promise<void> {
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- 提交(按当前 Tab 文案二态) -->
|
||||
<!-- 提交(按当前 Tab / password 子模式文案多态) -->
|
||||
<AppButton block size="large" :loading="submitting" :disabled="!canSubmit" @click="onSubmit">
|
||||
{{ activeTab === 'sms' ? t('login.submitSms') : t('login.submitInvite') }}
|
||||
{{ submitLabel }}
|
||||
</AppButton>
|
||||
</section>
|
||||
</div>
|
||||
@ -365,6 +480,31 @@ async function onSubmit(): Promise<void> {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* —— 用户名密码:登录/注册子模式切换(比主 Tab 更轻,作段控) —— */
|
||||
.pwd-mode {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 3px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.pwd-mode-btn {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
border: 0;
|
||||
border-radius: calc(var(--radius) - 3px);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.pwd-mode-btn.is-active {
|
||||
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* —— 提示文案 —— */
|
||||
.login-hint {
|
||||
margin: -6px 2px 0;
|
||||
|
||||
@ -54,12 +54,13 @@ const meLoading = ref(true)
|
||||
|
||||
/**
|
||||
* 用 userStore 现有字段构造兜底账号信息(getMe 失败/mock 模式时用)。
|
||||
* 手机号 store 无持久化,兜底留空(passport.me 才回脱敏手机号)。
|
||||
* 手机号/用户名 store 均无持久化,兜底留空(passport.me 才回脱敏手机号与用户名)。
|
||||
*/
|
||||
function fallbackMe(): PlayerMeResp {
|
||||
return {
|
||||
userId: Number(userStore.userId) || 0,
|
||||
mobile: '',
|
||||
username: '',
|
||||
nickname: userStore.nickname,
|
||||
avatar: userStore.avatar,
|
||||
creatorFlag: userStore.creatorFlag,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user