feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑
- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作 - 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转 - 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录 - 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交 - 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
<view class="page" :style="pageStyle()"><AppHeader title="更换手机号" />
|
||||
<scroll-view scroll-y :style="contentStyle(96, false)"><view class="account-form-content" :style="bottomStyle(40)">
|
||||
<text class="form-title">验证并绑定新手机号</text><text class="form-description">请分别验证当前号码和新号码,确保账户由您本人操作。</text>
|
||||
<view class="form-card"><text class="form-label">当前手机号 {{ maskPhone(phone) }}</text><SecurityCode v-model="currentCode" :disabled="submitting" purpose="phone-current" @error="codeError" @sent="errorMessage = ''" /></view>
|
||||
<view class="form-card"><text class="form-label">新手机号</text><input v-model="newPhone" class="account-input new-phone" :disabled="submitting" type="number" maxlength="11" placeholder="请输入新的 11 位手机号" /><SecurityCode v-model="newCode" :disabled="submitting || !validPhone(newPhone) || newPhone == phone" purpose="phone-new" :phone="newPhone" @error="codeError" @sent="errorMessage = ''" /></view>
|
||||
<view class="form-card"><text class="form-label">当前手机号 {{ maskPhone(phone) }}</text><SecurityCode v-model="currentCode" :disabled="submitting" :active="pageVisible" :ownerToken="ownerToken" purpose="phone-current" @error="codeError" @sent="errorMessage = ''" /></view>
|
||||
<view class="form-card"><text class="form-label">新手机号</text><input v-model="newPhone" class="account-input new-phone" :disabled="submitting" type="number" maxlength="11" placeholder="请输入新的 11 位手机号" /><SecurityCode v-model="newCode" :disabled="submitting || !validPhone(newPhone) || newPhone == phone" :active="pageVisible" :ownerToken="ownerToken" purpose="phone-new" :phone="newPhone" @error="codeError" @sent="errorMessage = ''" /></view>
|
||||
<text class="form-hint">换绑成功后需要使用新手机号登录。若原号码已无法接收短信,请通过官方客服核验身份后处理。</text>
|
||||
<text v-if="errorMessage.length > 0" class="form-error">{{ errorMessage }}</text>
|
||||
<button class="primary-button form-submit" :disabled="submitting" @click="submit">{{ submitting ? '提交中…' : '确认更换' }}</button>
|
||||
@@ -18,20 +18,35 @@
|
||||
import { getAccountProfile, maskPhone, validPhone, changeAccountPhone } from '@/packages/account/services/account.uts'
|
||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||
const phone = ref(''); const newPhone = ref(''); const currentCode = ref(''); const newCode = ref(''); const submitting = ref(false); const errorMessage = ref('')
|
||||
onLoad(() => { if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }; phone.value = getAccountProfile().phone })
|
||||
const codeError = (message: string) => { errorMessage.value = message }
|
||||
const ownerToken = ref(''); const pageVisible = ref(false)
|
||||
let ownerId = ''; let generation = 0; let accountTimer = 0
|
||||
const clearSecrets = () => { currentCode.value = ''; newCode.value = ''; newPhone.value = '' }
|
||||
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
|
||||
}
|
||||
onLoad(() => { const session = getSession(); ownerToken.value = session?.getString('token') ?? ''; ownerId = session?.getString('userId') ?? ''; phone.value = getAccountProfile().phone })
|
||||
onShow(() => { pageVisible.value = true; generation++; stopAccountTimer(); if (sameOwner()) accountTimer = setInterval(() => { sameOwner() }, 1000) })
|
||||
const codeError = (message: string) => { if (sameOwner()) errorMessage.value = message }
|
||||
const submit = async () => {
|
||||
if (submitting.value) return
|
||||
if (!sameOwner() || submitting.value) return
|
||||
submitting.value = true; errorMessage.value = ''
|
||||
const view = generation; const token = ownerToken.value; const userId = ownerId
|
||||
try {
|
||||
if (getAccountProfile().phone != phone.value) throw new Error('登录状态已变化,请重新进入页面')
|
||||
await changeAccountPhone(newPhone.value, currentCode.value, newCode.value)
|
||||
currentCode.value = ''; newCode.value = ''; 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(() => { currentCode.value = ''; newCode.value = '' })
|
||||
onUnload(() => { currentCode.value = ''; newCode.value = '' })
|
||||
onHide(() => { pageVisible.value = false; generation++; stopAccountTimer(); clearSecrets(); errorMessage.value = '' })
|
||||
onUnload(() => { pageVisible.value = false; generation++; stopAccountTimer(); clearSecrets(); phone.value = '' })
|
||||
</script>
|
||||
<style>
|
||||
@import '../styles/forms.css';
|
||||
|
||||
@@ -3,23 +3,29 @@
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { requestSecurityCode } from '@/packages/account/services/account.uts'
|
||||
const props = defineProps({ modelValue: { type: String, default: '' }, purpose: { type: String, default: 'password' }, phone: { type: String, default: '' }, disabled: { type: Boolean, default: false } })
|
||||
import { getSession } from '@/services/api.uts'
|
||||
const props = defineProps({ modelValue: { type: String, default: '' }, purpose: { type: String, default: 'password' }, phone: { type: String, default: '' }, disabled: { type: Boolean, default: false }, active: { type: Boolean, default: true }, ownerToken: { type: String, default: '' } })
|
||||
const emit = defineEmits(['update:modelValue', 'error', 'sent'])
|
||||
const sending = ref(false); const countdown = ref(0)
|
||||
let timer = 0; let version = 0; let expiresAt = 0
|
||||
const reset = () => { version++; clearInterval(timer); countdown.value = 0; sending.value = false }
|
||||
const targetPhone = computed((): string => props.phone)
|
||||
watch(targetPhone, () => { reset(); emit('update:modelValue', '') })
|
||||
watch((): boolean => props.active, () => reset())
|
||||
watch((): string => props.ownerToken, () => { reset(); emit('update:modelValue', '') })
|
||||
const send = async () => {
|
||||
if (props.disabled || sending.value || countdown.value > 0) return
|
||||
if (!props.active || props.disabled || sending.value || countdown.value > 0) return
|
||||
const token = props.ownerToken
|
||||
if (token.length == 0 || getSession()?.getString('token') != token) { emit('error', '登录状态已变化,请重新进入页面'); return }
|
||||
const current = ++version; sending.value = true
|
||||
const valid = (): boolean => current == version && props.active && props.ownerToken == token && getSession()?.getString('token') == token
|
||||
try {
|
||||
await requestSecurityCode(props.purpose, props.phone)
|
||||
if (current != version) return
|
||||
if (!valid()) return
|
||||
expiresAt = Date.now() + 60000; countdown.value = 60
|
||||
timer = setInterval(() => { countdown.value = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)); if (countdown.value == 0) clearInterval(timer) }, 1000)
|
||||
emit('sent'); uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||
} catch (error) { if (current == version) emit('error', error instanceof Error ? error.message : '发送失败,请稍后重试') }
|
||||
} catch (error) { if (valid()) emit('error', error instanceof Error ? error.message : '发送失败,请稍后重试') }
|
||||
finally { if (current == version) sending.value = false }
|
||||
}
|
||||
onUnmounted(() => reset())
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import { assistantReturnPath } from '@/services/assistant-return.uts'
|
||||
import { nextStepRoute } from '@/services/next-step-route.uts'
|
||||
import { reportRoute } from '@/services/report-route.uts'
|
||||
import { matchReturnPath } from '@/packages/finance/services/compare.uts'
|
||||
let afterLoginUrl = '/pages/home/home'
|
||||
onLoad((options) => {
|
||||
consumeInitialLaunch()
|
||||
@@ -50,6 +51,9 @@
|
||||
} else if (options?.['resumeReport'] == '1') {
|
||||
const target = reportRoute(options?.['assessmentId'] ?? '', options?.['tab'] ?? 'overview', options?.['revisionId'] ?? '')
|
||||
if (target.length > 0) afterLoginUrl = target
|
||||
} else if (options?.['resumeMatch'] == '1') {
|
||||
const target = matchReturnPath(options?.['matchPage'] ?? '', options?.['assessmentId'] ?? '', options?.['productIds'] ?? '', options?.['revisionId'] ?? '', options?.['productId'] ?? '', options?.['group'] ?? 'eligible', options?.['tab'] ?? 'explanation', options?.['from'] ?? '')
|
||||
if (target.length > 0) afterLoginUrl = target
|
||||
} else if (options?.['resumePoints'] == '1') {
|
||||
afterLoginUrl = '/packages/account/points/points'
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<view class="profile-card" @click="openAccount"><view class="avatar"><image class="profile-avatar" :src="avatar.length > 0 ? avatar : DEFAULT_AVATAR" mode="aspectFill" @error="avatar = ''" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text v-if="!loggedIn" class="account-state">未登录</text><AppIcon v-if="loggedIn" name="chevron-right" :size="28" /></view>
|
||||
<view class="stat-row"><view class="stat-item" @click="startAssessment"><AppIcon name="edit-file" :size="42" /><text class="stat-label">开始评估</text></view><view class="stat-item" @click="openReport"><AppIcon name="file-text" :size="42" /><text class="stat-label">{{ reportLoading ? '正在查找…' : '最近报告' }}</text></view><view class="stat-item" @click="go('/packages/account/points/points')"><AppIcon name="points" :size="42" /><text class="stat-label">积分中心</text></view></view>
|
||||
<view class="menu-list"><view class="menu-row" v-for="item in menus" :key="item.name" @click="open(item.key)"><AppIcon :name="item.icon" :size="46" /><text class="menu-name">{{ item.name }}</text><text v-if="item.note.length > 0" class="menu-note">{{ item.note }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
|
||||
<button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
|
||||
<button class="logout" :disabled="loggingOut" @click="logout">{{ loggingOut ? '退出中…' : (loggedIn ? '退出登录' : '登录 / 注册') }}</button>
|
||||
<text class="version-note">AI融资 · 让融资更简单</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -34,29 +34,41 @@
|
||||
const maskedPhone = ref('')
|
||||
const avatar = ref('')
|
||||
const reportLoading = ref(false)
|
||||
let profileVisible = false; let reportRequest = 0
|
||||
onShow(() => {
|
||||
profileVisible = true
|
||||
const loggingOut = ref(false)
|
||||
let profileVisible = false; let reportRequest = 0; let generation = 0; let accountTimer = 0; let ownerToken = ''; let ownerId = ''
|
||||
const stopAccountTimer = () => { clearInterval(accountTimer); accountTimer = 0 }
|
||||
const syncProfile = () => {
|
||||
const request = ++generation; reportRequest++; reportLoading.value = false; loggingOut.value = false
|
||||
const session = getSession()
|
||||
ownerToken = session?.getString('token') ?? ''; ownerId = session?.getString('userId') ?? ''
|
||||
loggedIn.value = session != null
|
||||
const profile = getAccountProfile()
|
||||
maskedPhone.value = maskPhone(profile.phone); avatar.value = loggedIn.value ? profile.avatar : ''
|
||||
displayName.value = loggedIn.value ? profile.nickname : '登录 / 注册'
|
||||
if (loggedIn.value) fetchAccountProfile().then((fresh) => {
|
||||
if (!profileVisible || request != generation || getSession()?.getString('token') != ownerToken || getSession()?.getString('userId') != ownerId) return
|
||||
displayName.value = fresh.nickname; avatar.value = fresh.avatar; maskedPhone.value = maskPhone(fresh.phone)
|
||||
}).catch(() => { /* 读取失败保留已归属当前账户的缓存,不显示其他账户资料。 */ })
|
||||
})
|
||||
}
|
||||
const sameOwner = (): boolean => {
|
||||
if (!profileVisible) return false
|
||||
const session = getSession()
|
||||
if ((session?.getString('token') ?? '') == ownerToken && (session?.getString('userId') ?? '') == ownerId) return true
|
||||
syncProfile(); return false
|
||||
}
|
||||
onShow(() => { profileVisible = true; syncProfile(); stopAccountTimer(); accountTimer = setInterval(() => { sameOwner() }, 1000) })
|
||||
const go = (url: string) => { uni.navigateTo({ url }) }
|
||||
const startAssessment = () => {
|
||||
if (!sameOwner()) return
|
||||
const session = getSession()
|
||||
if (session == null) { go('/packages/account/login/login'); return }
|
||||
const token = session.getString('token') ?? ''
|
||||
const view = generation
|
||||
uni.showActionSheet({ itemList: ['个人评估', '企业评估'], success: (result) => {
|
||||
if (profileVisible && getSession()?.getString('token') == token) go('/packages/finance/entry/entry?type=' + (result.tapIndex == 0 ? 'personal' : 'enterprise'))
|
||||
if (view == generation && sameOwner()) go('/packages/finance/entry/entry?type=' + (result.tapIndex == 0 ? 'personal' : 'enterprise'))
|
||||
} })
|
||||
}
|
||||
const openReport = async () => {
|
||||
if (reportLoading.value) return
|
||||
if (!sameOwner() || reportLoading.value) return
|
||||
const session = getSession()
|
||||
if (session == null) { go('/packages/account/login/login'); return }
|
||||
const token = session.getString('token') ?? ''; const request = ++reportRequest
|
||||
@@ -84,22 +96,29 @@
|
||||
} finally { if (request == reportRequest) reportLoading.value = false }
|
||||
}
|
||||
// 离页或退出后,旧查找结果及弹窗回调不得把用户带到前一个账户的评估。
|
||||
onHide(() => { profileVisible = false; reportRequest++; reportLoading.value = false })
|
||||
onUnload(() => { profileVisible = false; reportRequest++; reportLoading.value = false })
|
||||
const hideProfile = () => { profileVisible = false; generation++; reportRequest++; reportLoading.value = false; stopAccountTimer(); displayName.value = '登录 / 注册'; avatar.value = ''; maskedPhone.value = ''; loggedIn.value = false }
|
||||
onHide(() => hideProfile())
|
||||
onUnload(() => hideProfile())
|
||||
const open = (key: string) => {
|
||||
if (!sameOwner()) return
|
||||
if (key == 'archive') go('/packages/finance/archive/archive')
|
||||
else if (key == 'help') go('/packages/content/help/help')
|
||||
else if (key == 'security') go(loggedIn.value ? '/packages/account/security/security' : '/packages/account/login/login')
|
||||
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { go('/packages/finance/archive/archive') } } })
|
||||
else if (key == 'privacy') { const view = generation; uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (view != generation || !sameOwner()) return; if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { go('/packages/finance/archive/archive') } } }) }
|
||||
else if (key == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识与融资方案。评估结果仅供参考,不构成授信承诺。', showCancel: false, confirmText: '我知道了' })
|
||||
}
|
||||
const openAccount = () => { go(loggedIn.value ? '/packages/account/edit-profile/edit-profile' : '/packages/account/login/login') }
|
||||
const openAccount = () => { if (sameOwner()) go(loggedIn.value ? '/packages/account/edit-profile/edit-profile' : '/packages/account/login/login') }
|
||||
const logout = () => {
|
||||
if (!sameOwner() || loggingOut.value) return
|
||||
if (!loggedIn.value) { go('/packages/account/login/login'); return }
|
||||
uni.showModal({ title: '退出登录', content: '将清除本机账户缓存和未提交的填写内容,已提交的评估记录不受影响。', confirmText: '退出登录', success: (result) => { if (result.confirm) {
|
||||
logoutAccount().then(() => { uni.reLaunch({ url: '/packages/account/login/login' }) })
|
||||
.catch(() => { uni.showToast({ title: '退出失败,请检查网络后重试', icon: 'none' }) })
|
||||
} }, fail: (error) => { console.error('logout confirmation failed', error); uni.showToast({ title: '暂时无法打开确认框,请重试', icon: 'none' }) } })
|
||||
const view = generation; const token = ownerToken; const userId = ownerId
|
||||
uni.showModal({ title: '退出登录', content: '将清除本机账户缓存和未提交的填写内容,已提交的评估记录不受影响。', confirmText: '退出登录', success: (result) => {
|
||||
if (!result.confirm || view != generation || !sameOwner() || loggingOut.value) return
|
||||
loggingOut.value = true
|
||||
logoutAccount().then(() => { if (profileVisible && view == generation && getSession() == null) uni.reLaunch({ url: '/packages/account/login/login' }) })
|
||||
.catch(() => { if (profileVisible && view == generation && getSession()?.getString('token') == token && getSession()?.getString('userId') == userId) uni.showToast({ title: '退出失败,请检查网络后重试', icon: 'none' }) })
|
||||
.finally(() => { if (view == generation) loggingOut.value = false })
|
||||
}, fail: () => { if (view == generation && sameOwner()) uni.showToast({ title: '暂时无法打开确认框,请重试', icon: 'none' }) } })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,8 +19,20 @@
|
||||
import { getAccountProfile, maskPhone } from '@/packages/account/services/account.uts'
|
||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||
const phone = ref('')
|
||||
onShow(() => { if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }; phone.value = getAccountProfile().phone })
|
||||
const go = (path: string) => { uni.navigateTo({ url: '/packages/account/' + path }) }
|
||||
let ownerToken = ''; let ownerId = ''; let pageVisible = false; let accountTimer = 0
|
||||
const stopAccountTimer = () => { clearInterval(accountTimer); accountTimer = 0 }
|
||||
const sameOwner = (): boolean => {
|
||||
if (!pageVisible) return false
|
||||
const session = getSession()
|
||||
if (ownerToken.length > 0 && session?.getString('token') == ownerToken && session?.getString('userId') == ownerId) return true
|
||||
phone.value = ''; stopAccountTimer()
|
||||
uni.redirectTo({ url: session == null ? '/packages/account/login/login' : '/packages/account/profile/profile' })
|
||||
return false
|
||||
}
|
||||
onShow(() => { pageVisible = true; const session = getSession(); ownerToken = session?.getString('token') ?? ''; ownerId = session?.getString('userId') ?? ''; stopAccountTimer(); if (!sameOwner()) return; phone.value = getAccountProfile().phone; accountTimer = setInterval(() => { sameOwner() }, 1000) })
|
||||
onHide(() => { pageVisible = false; stopAccountTimer(); phone.value = '' })
|
||||
onUnload(() => { pageVisible = false; stopAccountTimer(); phone.value = '' })
|
||||
const go = (path: string) => { if (sameOwner()) uni.navigateTo({ url: '/packages/account/' + path }) }
|
||||
</script>
|
||||
<style>
|
||||
.security-content { padding: 30rpx; }
|
||||
|
||||
@@ -4,6 +4,7 @@ import { draftText } from '@/services/assessment.uts'
|
||||
|
||||
export type AccountProfile = { nickname: string, avatar: string, phone: string }
|
||||
export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png'
|
||||
let profileWriteVersion = 0
|
||||
const profileKey = (phone: string): string => 'finance.account.profile:' + phone
|
||||
export const maskPhone = (phone: string): string => phone.length == 11 ? phone.substring(0, 3) + '****' + phone.substring(7) : phone
|
||||
export const validPhone = (phone: string): boolean => /^1[3-9][0-9]{9}$/.test(phone)
|
||||
@@ -17,7 +18,7 @@ export function requireAccount(): UTSJSONObject {
|
||||
}
|
||||
export function sameAccount(session: UTSJSONObject): boolean {
|
||||
const current = getSession()
|
||||
return current != null && draftText(current, 'token') == draftText(session, 'token') && draftText(current, 'phone') == draftText(session, 'phone')
|
||||
return current != null && draftText(current, 'token') == draftText(session, 'token') && draftText(current, 'userId') == draftText(session, 'userId') && draftText(current, 'phone') == draftText(session, 'phone')
|
||||
}
|
||||
export function getAccountProfile(): AccountProfile {
|
||||
const session = getSession()
|
||||
@@ -37,19 +38,26 @@ async function accountRequest(path: string, method: RequestMethod, payload: UTSJ
|
||||
requireAccount()
|
||||
return await requestJson(path, method, payload)
|
||||
}
|
||||
function cacheProfile(result: UTSJSONObject, phone: string): AccountProfile {
|
||||
const profile: AccountProfile = { phone, nickname: draftText(result, 'nickname'), avatar: draftText(result, 'avatarUrl') }
|
||||
function cacheProfile(result: UTSJSONObject, session: UTSJSONObject): AccountProfile {
|
||||
const phone = draftText(session, 'phone'); const name = result.getString('nickname')
|
||||
const avatar = result['avatarUrl']
|
||||
if (draftText(result, 'userId') != draftText(session, 'userId') || name == null || name.trim().length < 2 || name.length > 20
|
||||
|| (avatar != null && typeof avatar != 'string') || draftText(result, 'phoneMasked') != maskPhone(phone)) throw new Error('账户资料响应不完整或归属不符,请重试')
|
||||
const profile: AccountProfile = { phone, nickname: name, avatar: draftText(result, 'avatarUrl') }
|
||||
setUserStorage(profileKey(phone), { nickname: profile.nickname, avatar: profile.avatar })
|
||||
return profile
|
||||
}
|
||||
export async function fetchAccountProfile(): Promise<AccountProfile> {
|
||||
const session = requireAccount()
|
||||
const version = profileWriteVersion
|
||||
const result = await accountRequest('/account/profile', 'GET', {})
|
||||
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
||||
return cacheProfile(result, draftText(session, 'phone'))
|
||||
// 保存完成前发出的旧读取不能回写昵称或头像缓存。
|
||||
if (version != profileWriteVersion) throw new Error('账户资料已更新,请重新读取')
|
||||
return cacheProfile(result, session)
|
||||
}
|
||||
export async function saveAccountProfile(nickname: string, avatarId: string): Promise<void> {
|
||||
const session = requireAccount(); const phone = draftText(session, 'phone')
|
||||
const session = requireAccount()
|
||||
const name = nickname.trim()
|
||||
if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符')
|
||||
const payload: UTSJSONObject = { nickname: name }
|
||||
@@ -57,12 +65,16 @@ export async function saveAccountProfile(nickname: string, avatarId: string): Pr
|
||||
else if (avatarId.length > 0) payload['avatarId'] = avatarId
|
||||
const result = await accountRequest('/account/profile', 'PUT', payload)
|
||||
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
||||
cacheProfile(result, phone)
|
||||
profileWriteVersion++
|
||||
cacheProfile(result, session)
|
||||
}
|
||||
|
||||
export async function uploadAccountAvatar(path: string): Promise<UTSJSONObject> {
|
||||
requireAccount()
|
||||
return await uploadJson('/account/avatar', path)
|
||||
const session = requireAccount()
|
||||
const result = await uploadJson('/account/avatar', path)
|
||||
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
||||
if (!/^[1-9][0-9]{0,9}$/.test(draftText(result, 'attachmentId')) || draftText(result, 'avatarUrl').length == 0) throw new Error('头像上传响应不完整,请重试')
|
||||
return result
|
||||
}
|
||||
export async function requestSecurityCode(purpose: string, newPhone: string): Promise<void> {
|
||||
if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效')
|
||||
@@ -75,12 +87,14 @@ export async function changeAccountPassword(mode: string, oldPassword: string, c
|
||||
if (oldPassword.length == 0) throw new Error('请输入当前密码')
|
||||
if (oldPassword == newPassword) throw new Error('新密码不能与当前密码相同')
|
||||
} else if (mode != 'sms' || !validCode(code)) throw new Error('请输入 6 位数字验证码')
|
||||
await accountRequest('/account/password', 'PUT', { verification: mode, oldPassword: mode == 'password' ? oldPassword : '', code: mode == 'sms' ? code : '', newPassword })
|
||||
const result = await accountRequest('/account/password', 'PUT', { verification: mode, oldPassword: mode == 'password' ? oldPassword : '', code: mode == 'sms' ? code : '', newPassword })
|
||||
if (result['reLoginRequired'] != true) throw new Error('密码修改响应不完整,请重试')
|
||||
}
|
||||
export async function changeAccountPhone(newPhone: string, currentCode: string, newCode: string): Promise<void> {
|
||||
const session = requireAccount()
|
||||
if (!validPhone(newPhone)) throw new Error('请输入正确的新手机号')
|
||||
if (newPhone == draftText(session, 'phone')) throw new Error('新手机号不能与当前手机号相同')
|
||||
if (!validCode(currentCode) || !validCode(newCode)) throw new Error('请输入两组 6 位数字验证码')
|
||||
await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
|
||||
const result = await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
|
||||
if (result['reLoginRequired'] != true) throw new Error('手机号修改响应不完整,请重试')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user