Files
hujun 71b3c25695 feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑
- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作
- 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转
- 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录
- 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交
- 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
2026-10-07 10:01:05 +08:00

117 lines
8.4 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="修改资料" customBack @back="back" />
<scroll-view scroll-y :style="contentStyle(96, false)"><view class="edit-content" :style="bottomStyle(40)">
<view class="avatar-panel" @click="chooseAvatar"><image class="edit-avatar" :src="avatar.length > 0 ? avatar : DEFAULT_AVATAR" mode="aspectFill" @error="avatar = ''" /><text class="avatar-action">{{ choosing ? '读取中…' : '更换头像' }}</text></view>
<view class="edit-card"><text class="field-title">昵称</text><input v-model="nickname" class="nickname-input" :disabled="saving" maxlength="20" placeholder="请输入昵称" /><text class="field-help">2–20 个字符,用于个人中心展示</text></view>
<view class="phone-row" @click="openPhone"><view class="phone-copy"><text class="field-title">绑定手机号</text><text class="phone-value">{{ maskPhone(phone) }}</text></view><text class="change-label">更换</text><AppIcon name="chevron-right" :size="28" /></view>
<text class="profile-note">昵称与融资档案中的实名信息独立,修改后不影响已填写的融资资料。</text>
<text v-if="errorMessage.length > 0" class="error-text">{{ errorMessage }}</text>
<button class="primary-button save-button" :disabled="saving || choosing" @click="save">{{ saving ? '保存中…' : '保存资料' }}</button>
</view></scroll-view>
</view>
</template>
<script setup lang="uts">
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
import { usePageLayout } from '@/services/layout.uts'
import { getSession } from '@/services/api.uts'
import { DEFAULT_AVATAR, maskPhone, getAccountProfile, fetchAccountProfile, saveAccountProfile, uploadAccountAvatar } from '@/packages/account/services/account.uts'
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 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(() => {
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 (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 (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 (!sameOwner() || saving.value || choosing.value) return
if (!dirty()) { leave(); return }
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, view: number) => {
if (view != generation || !sameOwner()) return
choosing.value = true; errorMessage.value = ''
try {
const result = await uploadAccountAvatar(path)
if (view == generation && sameOwner()) { avatar.value = result['avatarUrl'] as string; avatarId.value = result['attachmentId'] as string }
}
catch (error) { if (view == generation && sameOwner()) errorMessage.value = error instanceof Error ? error.message : '图片读取失败' }
finally { choosing.value = false }
}
const chooseAvatar = () => {
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 (view != generation || !sameOwner() || result.tempFilePaths.length == 0) return
if (result.tempFiles[0].size > 512000) { errorMessage.value = '请选择 500KB 以内的图片'; return }
loadAvatar(result.tempFilePaths[0], view)
}, fail: (error) => { if (view == generation && sameOwner() && !error.errMsg.includes('cancel')) errorMessage.value = '无法打开相册,请检查权限后重试' } })
} })
}
const openPhone = () => { if (sameOwner() && !saving.value && !choosing.value) uni.navigateTo({ url: '/packages/account/change-phone/change-phone' }) }
const save = async () => {
if (!sameOwner() || saving.value || choosing.value) return
const view = generation; profileRequest++
errorMessage.value = ''; saving.value = true
try {
await saveAccountProfile(nickname.value, avatarId.value)
if (view != generation || !sameOwner()) return
originalName = nickname.value.trim(); originalAvatar = avatar.value; avatarId.value = ''
uni.showToast({ title: '资料已保存', icon: 'success' }); leave()
} catch (error) { if (view == generation && sameOwner()) errorMessage.value = error instanceof Error ? error.message : '保存失败,请稍后重试' }
finally { saving.value = false }
}
onHide(() => { pageVisible = false; generation++; profileRequest++; stopAccountTimer() })
onUnload(() => { pageVisible = false; generation++; profileRequest++; stopAccountTimer(); clearView() })
</script>
<style>
.edit-content { padding: 28rpx 30rpx 40rpx; }
.avatar-panel { align-items: center; padding: 20rpx 0 36rpx; }
.edit-avatar { width: 140rpx; height: 140rpx; border-radius: 70rpx; background-color: #eaf2ff; }
.avatar-action { color: #0868f7; font-size: 27rpx; margin-top: 20rpx; }
.edit-card { padding: 26rpx; border-radius: 18rpx; background-color: #ffffff; }
.field-title { color: #29394f; font-size: 28rpx; font-weight: 600; }
.nickname-input { height: 86rpx; color: #23344e; font-size: 30rpx; border-bottom: 1rpx solid #edf1f6; }
.field-help { color: #8793a6; font-size: 23rpx; margin-top: 16rpx; }
.phone-row { flex-direction: row; align-items: center; padding: 26rpx; margin-top: 20rpx; border-radius: 18rpx; background-color: #ffffff; }
.phone-copy { flex: 1; }
.phone-value { color: #66758a; font-size: 26rpx; margin-top: 12rpx; }
.change-label { color: #0868f7; font-size: 25rpx; margin-right: 8rpx; }
.profile-note { color: #738198; font-size: 24rpx; line-height: 38rpx; margin: 24rpx 4rpx; }
.error-text { color: #d64040; font-size: 25rpx; line-height: 38rpx; margin-bottom: 20rpx; }
.save-button { margin: 12rpx 0 0; }
</style>