feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑
- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作 - 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转 - 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录 - 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交 - 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
This commit is contained in:
@@ -84,8 +84,8 @@
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { MatchView, MatchReason, ProductView, fetchMatchDetailRecord, fetchPublicProduct, validRecordId, requestProductExplanation } from '@/packages/finance/services/matches.uts'
|
||||
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery } from '@/packages/finance/services/compare.uts'
|
||||
import { MatchView, MatchReason, MatchDetail, ProductView, fetchMatchDetailRecord, fetchPublicProduct, validRecordId, requestProductExplanation } from '@/packages/finance/services/matches.uts'
|
||||
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery, sameMatchSnapshot, matchReturnPath } from '@/packages/finance/services/compare.uts'
|
||||
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
|
||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||
const revisionId = ref(''); const assessmentId = ref(''); const productId = ref(''); const tab = ref('details')
|
||||
@@ -96,6 +96,7 @@
|
||||
const aiBusy = ref(false); const aiError = ref(''); const pollStopped = ref(false)
|
||||
let timer: number = 0; let pollCount = 0; let requestKey = ''
|
||||
let routeError = ''; let visible = false; let generation = 0
|
||||
let owner = ''; let accountTimer = 0; let frozenDetail: MatchDetail | null = null
|
||||
const positiveVisible = computed((): MatchReason[] => match.value == null ? [] : (expanded.value ? match.value!.positive : match.value!.positive.slice(0, 3)))
|
||||
const attention = computed((): MatchReason[] => match.value == null ? [] : match.value!.blocking.concat(match.value!.cautions))
|
||||
const attentionVisible = computed((): MatchReason[] => attentionExpanded.value ? attention.value : attention.value.slice(0, 3))
|
||||
@@ -104,10 +105,23 @@
|
||||
// 缩短重复的规则模板标题;核验限制仍保留在逐条依据和页尾,不改判定含义。
|
||||
const reasonTitle = (reason: MatchReason): string => reason.text.replace('(资料尚需核验)。', '').replace('本产品已配置条件', '产品条件')
|
||||
function stopPoll(): void { if (timer != 0) clearTimeout(timer); timer = 0 }
|
||||
const current = (request: number, token: string): boolean => visible && generation == request && getSession()?.getString('token') == token
|
||||
const stopAccountTimer = () => { if (accountTimer != 0) clearInterval(accountTimer); accountTimer = 0 }
|
||||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeMatch=1&matchPage=product&assessmentId=' + assessmentId.value + '&revisionId=' + revisionId.value + '&productIds=' + selectedIds.value.join(',') + '&productId=' + productId.value + '&tab=' + tab.value + '&from=' + from }) }
|
||||
const current = (request: number, token: string): boolean => {
|
||||
if (!visible || generation != request) return false
|
||||
if (getSession()?.getString('token') == token) return true
|
||||
generation++; stopPoll(); stopAccountTimer(); product.value = null; match.value = null; frozenDetail = null; openReason.value = ''; requestKey = ''; loading.value = false; aiBusy.value = false; aiError.value = ''
|
||||
if (getSession() == null) login(); else error.value = '账户已切换,请重新加载本人产品资料。'
|
||||
selectedIds.value = []
|
||||
return false
|
||||
}
|
||||
const ready = (): boolean => current(generation, owner) && product.value != null && !loading.value
|
||||
const updateUrl = () => {
|
||||
// #ifdef WEB
|
||||
window.history.replaceState(window.history.state, '', location.pathname + location.search + '#' + matchReturnPath('product', assessmentId.value, selectedIds.value.join(','), revisionId.value, productId.value, 'eligible', tab.value, from))
|
||||
// #endif
|
||||
}
|
||||
const explanationError = (request: number, token: string, message: string) => {
|
||||
if (!visible || generation != request) return
|
||||
if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||
if (current(request, token)) aiError.value = message
|
||||
}
|
||||
let pollAction: () => void = () => {}
|
||||
@@ -118,21 +132,22 @@
|
||||
timer = setTimeout(() => { timer = 0; pollAction() }, 3000)
|
||||
}
|
||||
async function refreshExplanation(): Promise<void> {
|
||||
if (aiBusy.value || match.value == null) return
|
||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||
if (!ready() || aiBusy.value || match.value == null) return
|
||||
const request = generation; const token = owner
|
||||
aiError.value = ''; aiBusy.value = true
|
||||
try {
|
||||
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||||
const result = detail.item
|
||||
if (current(request, token)) { revisionId.value = detail.revisionId; match.value = result; schedulePoll() }
|
||||
if (!current(request, token)) return
|
||||
if (frozenDetail == null || !sameMatchSnapshot(frozenDetail!, detail)) throw new Error('解释状态与本次冻结结果不一致,请重新加载。')
|
||||
match.value = detail.item; schedulePoll()
|
||||
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : '解释状态读取失败,请重试')
|
||||
} finally { if (current(request, token)) aiBusy.value = false }
|
||||
}
|
||||
pollAction = () => { refreshExplanation() }
|
||||
const checkExplanation = () => { pollCount = 0; pollStopped.value = false; refreshExplanation() }
|
||||
const generateExplanation = async () => {
|
||||
if (aiBusy.value || match.value == null || aiPending.value || match.value!.aiStatus == 'completed') return
|
||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||
if (!ready() || aiBusy.value || match.value == null || !match.value!.aiCanRequest || (match.value!.aiStatus == 'failed' && !match.value!.aiCanRetry) || aiPending.value || match.value!.aiStatus == 'completed') return
|
||||
const request = generation; const token = owner
|
||||
// 网络结果不确定时保留同一个幂等键,只有收到明确失败状态后才开始新的尝试。
|
||||
if (requestKey.length == 0) requestKey = 'explain_' + Date.now().toString() + '_' + Math.random().toString(36).slice(2, 12)
|
||||
aiBusy.value = true; aiError.value = ''; pollCount = 0; pollStopped.value = false
|
||||
@@ -143,40 +158,41 @@
|
||||
} finally { if (current(request, token)) aiBusy.value = false }
|
||||
}
|
||||
const load = async () => {
|
||||
stopPoll(); pollCount = 0; pollStopped.value = false; aiError.value = ''
|
||||
const request = ++generation; product.value = null; match.value = null; error.value = ''; expanded.value = false
|
||||
stopPoll(); stopAccountTimer(); pollCount = 0; pollStopped.value = false; aiError.value = ''; aiBusy.value = false
|
||||
const request = ++generation; product.value = null; match.value = null; frozenDetail = null; error.value = ''; expanded.value = false; loading.value = false
|
||||
if (routeError.length > 0) { error.value = routeError; return }
|
||||
const session = getSession()
|
||||
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||
const token = session.getString('token') ?? ''; loading.value = true
|
||||
if (session == null) { login(); return }
|
||||
const token = session.getString('token') ?? ''; if (owner.length > 0 && owner != token) { selectedIds.value = []; requestKey = '' }; owner = token; loading.value = true
|
||||
accountTimer = setInterval(() => { current(generation, owner) }, 1000)
|
||||
try {
|
||||
// 带评估编号始终读取冻结结果;404/越权/失败不能降级为公共资料。
|
||||
if (assessmentId.value.length > 0) {
|
||||
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||||
const result = detail.item
|
||||
if (current(request, token)) { revisionId.value = detail.revisionId; match.value = result; product.value = result.product; schedulePoll() }
|
||||
if (current(request, token)) { revisionId.value = detail.revisionId; frozenDetail = detail; match.value = result; product.value = result.product; saveCompareSelection(assessmentId.value, selectedIds.value, revisionId.value); updateUrl(); schedulePoll() }
|
||||
} else {
|
||||
const result = await fetchPublicProduct(productId.value)
|
||||
if (current(request, token)) product.value = result
|
||||
}
|
||||
} catch (err) { if (visible && generation == request) error.value = err instanceof Error ? err.message : '产品读取失败' }
|
||||
} catch (err) { if (current(request, token)) error.value = err instanceof Error ? err.message : '产品读取失败' }
|
||||
finally { if (generation == request) loading.value = false }
|
||||
}
|
||||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||
// 仅携带选择ID;助手会重新校验归属并读取冻结事实,进入页面不会调用模型。
|
||||
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, [productId.value], 'product', '', revisionId.value) }) }
|
||||
function toMatches(): void { if (assessmentId.value.length > 0) uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else history() }
|
||||
const consult = () => { if (ready() && match.value != null) uni.navigateTo({ url: assistantRoute(assessmentId.value, [productId.value], 'product', '', revisionId.value) }) }
|
||||
function toMatches(): void { if (owner.length > 0 && !current(generation, owner)) return; if (assessmentId.value.length > 0) uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else history() }
|
||||
// 替换当前路由保留栏目、来源与对比选择,刷新后仍停留在同一栏。
|
||||
const switchTab = (value: string) => {
|
||||
if (value == tab.value) return
|
||||
if (!ready() || value == tab.value) return
|
||||
const query = assessmentId.value.length > 0 ? compareQuery(assessmentId.value, selectedIds.value, revisionId.value) + '&' : '?'
|
||||
uni.redirectTo({ url: '/packages/finance/product/product' + query + 'productId=' + productId.value + '&tab=' + value + '&from=' + from })
|
||||
}
|
||||
// 下一步页重新核对当前可用性,避免沿用产品详情进入时的旧状态。
|
||||
const prepare = () => { if (canPrepare.value) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productId.value + '§ion=prepare' }) }
|
||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else if (from == 'compare') uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else toMatches() }
|
||||
const prepare = () => { if (ready() && canPrepare.value) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productId.value + '§ion=prepare' }) }
|
||||
const back = () => { if (owner.length > 0 && !current(generation, owner)) return; if (getCurrentPages().length > 1) uni.navigateBack(); else if (from == 'compare') uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else toMatches() }
|
||||
const openCompare = () => {
|
||||
if (match.value == null) return
|
||||
if (!ready() || match.value == null) return
|
||||
if (!selectedIds.value.includes(productId.value)) {
|
||||
if (selectedIds.value.length >= 3) { uni.showToast({ title: '已选满3项,请在对比页调整选择', icon: 'none' }); return }
|
||||
selectedIds.value.push(productId.value)
|
||||
@@ -199,10 +215,11 @@
|
||||
try { saveCompareSelection(assessmentId.value, parseCompareIds(options!['productIds']!), revisionId.value) }
|
||||
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
||||
}
|
||||
if (matchReturnPath('product', assessmentId.value, options?.['productIds'] ?? '', revisionId.value, productId.value, 'eligible', tab.value, from).length == 0) routeError = '产品页面参数无效,请返回重新选择。'
|
||||
})
|
||||
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value, revisionId.value); load() })
|
||||
onHide(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; error.value = ''; aiError.value = ''; aiBusy.value = false; loading.value = false; openReason.value = '' })
|
||||
onUnload(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; requestKey = '' })
|
||||
onHide(() => { pageActive.value = false; visible = false; stopPoll(); stopAccountTimer(); generation++; product.value = null; match.value = null; frozenDetail = null; error.value = ''; aiError.value = ''; aiBusy.value = false; loading.value = false; openReason.value = '' })
|
||||
onUnload(() => { pageActive.value = false; visible = false; stopPoll(); stopAccountTimer(); generation++; product.value = null; match.value = null; frozenDetail = null; requestKey = '' })
|
||||
</script>
|
||||
<style>
|
||||
.product-page { background-color: #fff; }
|
||||
|
||||
Reference in New Issue
Block a user