feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑

- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作
- 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转
- 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录
- 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交
- 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
This commit is contained in:
hujun
2026-10-07 10:01:05 +08:00
parent 1723363b7e
commit 71b3c25695
37 changed files with 1389 additions and 323 deletions
+27 -11
View File
@@ -5,10 +5,10 @@
<text class="form-description">{{ mode == 'password' ? '为保障账户安全,请先验证当前密码。' : '验证码将发送至绑定手机号 ' + maskPhone(phone) }}</text>
<view class="form-card">
<template v-if="mode == 'password'"><text class="form-label">当前密码</text><input v-model="oldPassword" class="account-input old-password" :disabled="submitting" :password="!visible" maxlength="64" placeholder="请输入当前密码" /></template>
<template v-else><text class="form-label">短信验证码</text><SecurityCode v-model="code" :disabled="submitting" purpose="password" @error="codeError" @sent="errorMessage = ''" /></template>
<template v-else><text class="form-label">短信验证码</text><SecurityCode v-model="code" :disabled="submitting" :active="pageVisible" :ownerToken="ownerToken" purpose="password" @error="codeError" @sent="errorMessage = ''" /></template>
<text class="form-label">新密码</text><input v-model="newPassword" class="account-input new-password" :disabled="submitting" :password="!visible" maxlength="32" placeholder="8–32 位,包含字母和数字" />
<text class="form-label">确认新密码</text><input v-model="confirmation" class="account-input confirm-password" :disabled="submitting" :password="!visible" maxlength="32" placeholder="请再次输入新密码" confirm-type="done" @confirm="submit" />
<text class="visibility-control" @click="visible = !visible">{{ visible ? '隐藏密码' : '显示密码' }}</text>
<text class="visibility-control" @click="toggleVisibility">{{ visible ? '隐藏密码' : '显示密码' }}</text>
</view>
<text class="form-hint">密码可包含英文符号,不支持空格。修改成功后请使用新密码重新登录。</text>
<text v-if="errorMessage.length > 0" class="form-error">{{ errorMessage }}</text>
@@ -25,27 +25,43 @@
import { getAccountProfile, maskPhone, validNewPassword, validCode, changeAccountPassword } from '@/packages/account/services/account.uts'
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
const mode = ref('password'); const phone = ref(''); const oldPassword = ref(''); const newPassword = ref(''); const confirmation = ref(''); const code = ref(''); const visible = ref(false); const submitting = ref(false); const errorMessage = ref('')
onLoad((options) => { if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }; phone.value = getAccountProfile().phone; if (options != null && options['mode'] == 'sms') mode.value = 'sms' })
const ownerToken = ref(''); const pageVisible = ref(false)
let ownerId = ''; let generation = 0; let accountTimer = 0
onLoad((options) => { const session = getSession(); ownerToken.value = session?.getString('token') ?? ''; ownerId = session?.getString('userId') ?? ''; phone.value = getAccountProfile().phone; if (options != null && options['mode'] == 'sms') mode.value = 'sms' })
const clearSecrets = () => { oldPassword.value = ''; newPassword.value = ''; confirmation.value = ''; code.value = ''; visible.value = false }
const switchMode = () => { if (submitting.value) return; clearSecrets(); errorMessage.value = ''; mode.value = mode.value == 'password' ? 'sms' : 'password' }
const codeError = (message: string) => { errorMessage.value = message }
const stopAccountTimer = () => { clearInterval(accountTimer); accountTimer = 0 }
const sameOwner = (): boolean => {
if (!pageVisible.value) return false
const session = getSession()
if (ownerToken.value.length > 0 && session?.getString('token') == ownerToken.value && session?.getString('userId') == ownerId) return true
generation++; clearSecrets(); phone.value = ''; errorMessage.value = ''; stopAccountTimer()
uni.redirectTo({ url: session == null ? '/packages/account/login/login' : '/packages/account/security/security' })
return false
}
const switchMode = () => { if (!sameOwner() || submitting.value) return; clearSecrets(); errorMessage.value = ''; mode.value = mode.value == 'password' ? 'sms' : 'password' }
const toggleVisibility = () => { if (sameOwner()) visible.value = !visible.value }
const codeError = (message: string) => { if (sameOwner()) errorMessage.value = message }
onShow(() => { pageVisible.value = true; generation++; stopAccountTimer(); if (sameOwner()) accountTimer = setInterval(() => { sameOwner() }, 1000) })
const submit = async () => {
if (submitting.value) return
if (!sameOwner() || submitting.value) return
errorMessage.value = ''
if (mode.value == 'password' && oldPassword.value.length == 0) { errorMessage.value = '请输入当前密码'; return }
if (mode.value == 'sms' && !validCode(code.value)) { errorMessage.value = '请输入 6 位数字验证码'; return }
if (!validNewPassword(newPassword.value)) { errorMessage.value = '新密码需为 8–32 位,包含字母和数字,可使用符号'; return }
if (newPassword.value != confirmation.value) { errorMessage.value = '两次输入的新密码不一致'; return }
submitting.value = true
const view = generation; const token = ownerToken.value; const userId = ownerId
try {
if (getAccountProfile().phone != phone.value) throw new Error('登录状态已变化,请重新进入页面')
await changeAccountPassword(mode.value, oldPassword.value, code.value, newPassword.value)
clearSecrets(); clearSession(); uni.reLaunch({ url: '/packages/account/login/login' }); uni.showToast({ title: '密码已修改,请重新登录', icon: 'none' })
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '修改失败,请稍后重试' }
// 服务端已撤销原账户会话;离页仍清理该会话,但不能影响新账户或跳转隐藏页面。
if (getSession()?.getString('token') != token || getSession()?.getString('userId') != userId) return
clearSecrets(); clearSession()
if (view == generation && pageVisible.value) { uni.reLaunch({ url: '/packages/account/login/login' }); uni.showToast({ title: '密码已修改,请重新登录', icon: 'none' }) }
} catch (error) { if (view == generation && sameOwner()) errorMessage.value = error instanceof Error ? error.message : '修改失败,请稍后重试' }
finally { submitting.value = false }
}
onHide(() => clearSecrets())
onUnload(() => clearSecrets())
onHide(() => { pageVisible.value = false; generation++; stopAccountTimer(); clearSecrets(); errorMessage.value = '' })
onUnload(() => { pageVisible.value = false; generation++; stopAccountTimer(); clearSecrets(); phone.value = '' })
</script>
<style>
@import '../styles/forms.css';