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
+44 -21
View File
@@ -20,59 +20,82 @@
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
const nickname = ref(''); const avatar = ref(''); const phone = ref(''); const saving = ref(false); const choosing = ref(false); const errorMessage = ref('')
const avatarId = ref('')
let originalName = ''; let originalAvatar = ''; let alive = true
const dirty = (): boolean => nickname.value.trim() != originalName || avatar.value != originalAvatar
let originalName = ''; let originalAvatar = ''; let ownerToken = ''; let ownerId = ''; let pageVisible = false; let generation = 0; let profileRequest = 0; let accountTimer = 0
const stopAccountTimer = () => { clearInterval(accountTimer); accountTimer = 0 }
const clearView = () => { nickname.value = ''; avatar.value = ''; phone.value = ''; avatarId.value = ''; originalName = ''; originalAvatar = ''; errorMessage.value = ''; saving.value = false; choosing.value = false }
const sameOwner = (): boolean => {
if (!pageVisible) return false
const session = getSession()
if (ownerToken.length > 0 && session?.getString('token') == ownerToken && session?.getString('userId') == ownerId) return true
generation++; profileRequest++; clearView(); stopAccountTimer()
uni.redirectTo({ url: session == null ? '/packages/account/login/login' : '/packages/account/profile/profile' })
return false
}
const dirty = (): boolean => nickname.value.trim() != originalName || avatar.value != originalAvatar || avatarId.value.length > 0
onLoad(() => {
if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
const session = getSession(); ownerToken = session?.getString('token') ?? ''; ownerId = session?.getString('userId') ?? ''
})
onShow(() => {
pageVisible = true; generation++; stopAccountTimer()
if (!sameOwner()) return
accountTimer = setInterval(() => { sameOwner() }, 1000)
if (dirty() || saving.value) return
const profile = getAccountProfile(); nickname.value = profile.nickname; avatar.value = profile.avatar; phone.value = profile.phone
originalName = profile.nickname; originalAvatar = profile.avatar
const request = ++profileRequest; const view = generation
// 服务端资料优先;请求返回时已开始编辑则不覆盖用户输入。
fetchAccountProfile().then((fresh) => {
if (!alive || dirty()) return
if (request != profileRequest || view != generation || !sameOwner() || dirty()) return
nickname.value = fresh.nickname; avatar.value = fresh.avatar; phone.value = fresh.phone
originalName = fresh.nickname; originalAvatar = fresh.avatar
}).catch((error) => { if (alive) errorMessage.value = error instanceof Error ? error.message : '资料加载失败,请重试' })
}).catch((error) => { if (request == profileRequest && view == generation && sameOwner()) errorMessage.value = error instanceof Error ? error.message : '资料加载失败,请重试' })
})
const leave = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/account/profile/profile' }) }
const back = () => {
if (saving.value) return
if (!sameOwner() || saving.value || choosing.value) return
if (!dirty()) { leave(); return }
uni.showModal({ title: '放弃修改?', content: '当前修改尚未保存。', confirmText: '放弃修改', success: (result) => { if (result.confirm) { originalName = nickname.value.trim(); originalAvatar = avatar.value; leave() } } })
const view = generation
uni.showModal({ title: '放弃修改?', content: '当前修改尚未保存。', confirmText: '放弃修改', success: (result) => { if (result.confirm && view == generation && sameOwner()) leave() } })
}
onBackPress((event): boolean => { if (event.from == 'navigateBack') return false; if (saving.value || dirty()) { back(); return true }; return false })
const loadAvatar = async (path: string) => {
const loadAvatar = async (path: string, view: number) => {
if (view != generation || !sameOwner()) return
choosing.value = true; errorMessage.value = ''
try {
const result = await uploadAccountAvatar(path)
if (alive) { avatar.value = result['avatarUrl'] as string; avatarId.value = result['attachmentId'] as string }
if (view == generation && sameOwner()) { avatar.value = result['avatarUrl'] as string; avatarId.value = result['attachmentId'] as string }
}
catch (error) { if (alive) errorMessage.value = error instanceof Error ? error.message : '图片读取失败' }
finally { if (alive) choosing.value = false }
catch (error) { if (view == generation && sameOwner()) errorMessage.value = error instanceof Error ? error.message : '图片读取失败' }
finally { choosing.value = false }
}
const chooseAvatar = () => {
if (saving.value || choosing.value) return
if (!sameOwner() || saving.value || choosing.value) return
const view = generation
uni.showActionSheet({ itemList: ['从相册选择', '恢复默认头像'], success: (action) => {
if (view != generation || !sameOwner()) return
if (action.tapIndex == 1) { avatar.value = ''; avatarId.value = 'default'; return }
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album'], success: (result) => {
if (result.tempFilePaths.length == 0) return
if (view != generation || !sameOwner() || result.tempFilePaths.length == 0) return
if (result.tempFiles[0].size > 512000) { errorMessage.value = '请选择 500KB 以内的图片'; return }
loadAvatar(result.tempFilePaths[0])
}, fail: (error) => { if (!error.errMsg.includes('cancel')) errorMessage.value = '无法打开相册,请检查权限后重试' } })
loadAvatar(result.tempFilePaths[0], view)
}, fail: (error) => { if (view == generation && sameOwner() && !error.errMsg.includes('cancel')) errorMessage.value = '无法打开相册,请检查权限后重试' } })
} })
}
const openPhone = () => { if (!saving.value && !choosing.value) uni.navigateTo({ url: '/packages/account/change-phone/change-phone' }) }
const openPhone = () => { if (sameOwner() && !saving.value && !choosing.value) uni.navigateTo({ url: '/packages/account/change-phone/change-phone' }) }
const save = async () => {
if (saving.value || choosing.value) return
if (!sameOwner() || saving.value || choosing.value) return
const view = generation; profileRequest++
errorMessage.value = ''; saving.value = true
try {
if (getAccountProfile().phone != phone.value) throw new Error('登录状态已变化,请重新进入页面')
await saveAccountProfile(nickname.value, avatarId.value)
originalName = nickname.value.trim(); originalAvatar = avatar.value
if (view != generation || !sameOwner()) return
originalName = nickname.value.trim(); originalAvatar = avatar.value; avatarId.value = ''
uni.showToast({ title: '资料已保存', icon: 'success' }); leave()
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '保存失败,请稍后重试' }
} catch (error) { if (view == generation && sameOwner()) errorMessage.value = error instanceof Error ? error.message : '保存失败,请稍后重试' }
finally { saving.value = false }
}
onUnload(() => { alive = false })
onHide(() => { pageVisible = false; generation++; profileRequest++; stopAccountTimer() })
onUnload(() => { pageVisible = false; generation++; profileRequest++; stopAccountTimer(); clearView() })
</script>
<style>
.edit-content { padding: 28rpx 30rpx 40rpx; }