Files
airz-uniapp/packages/finance/next-step/next-step.uvue
T
hujun 2c9d3feab5 feat: 完善登录流程、UI适配及协议文档更新
本次提交包含以下修改:
1. 新增微信登录相关服务及页面逻辑,实现首次微信绑定手机号流程
2. 优化产品匹配卡片 UI,移除不必要状态显示
3. 调整首页 scroll-view 属性及 banner 背景样式
4. 完善积分规则校验逻辑,增加时区及策略格式验证
5. 更新协议草案文档,补充资料清单及功能说明
6. 修复分析结果路由路径,适配新的匹配结果页面
7. 调整品牌 Logo 安卓端字体对齐样式
2026-10-22 11:24:15 +08:00

182 lines
18 KiB
Plaintext
Raw 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="true" @back="back" />
<scroll-view scroll-y :scroll-into-view="scrollTarget" :style="contentStyle(96, false)">
<view class="content">
<text v-if="loading" class="body">正在读取本次报告与产品状态…</text>
<view v-if="error.length > 0" class="panel"><text class="heading">暂时无法查看行动建议</text><text class="body">{{ error }}</text><button v-if="routeError.length == 0" class="secondary" @click="load">重新加载</button><text class="link" @click="history">前往融资档案 ›</text></view>
<template v-if="report != null && !loading">
<text class="eyebrow">评估记录 #{{ assessmentId }} · {{ assessmentType == 'enterprise' ? '企业' : '个人' }}评估 · 第 {{ report!.revisionNo }} 版报告</text>
<text v-if="report!.isPreviousResult" class="revision-notice">当前查看第 {{ report!.revisionNo }} 版结果。最新提交{{ report!.currentStatus == 'processing' ? '正在评估,成功后更新结果。' : (report!.currentStatus == 'failed' ? '评估失败,已保留上次成功结果。' : '已有新版本,可从档案查看。') }}</text>
<text class="intro">根据本次报告,安排您的下一步</text>
<view class="actions">
<view class="action-row" @click="prepare"><image class="action-icon" src="/packages/finance/static/action-prepare.svg" /><view class="action-copy"><text class="action-title">{{ report!.matching.status != 'completed' ? '查看准备说明' : (productId.length > 0 && !canPrepare ? '核对产品条件' : '准备申请') }}</text><text class="action-note">{{ report!.matching.status != 'completed' ? '本报告没有产品匹配结果,可查看准备说明' : (productId.length == 0 ? '选择本次匹配产品,查看材料准备说明' : '核对产品条件,了解所需材料') }}</text></view><text class="chevron">›</text></view>
<view class="action-row" @click="openReport"><image class="action-icon" src="/packages/finance/static/action-report.svg" /><view class="action-copy"><text class="action-title">查看已保存报告</text><text class="action-note">报告已保存至融资档案,可随时查看</text></view><text class="chevron">›</text></view>
<view class="action-row" @click="optimize"><image class="action-icon" src="/packages/finance/static/action-improve.svg" /><view class="action-copy"><text class="action-title">优化资料</text><text class="action-note">核对待补充与待核实项,修改后重新评估</text></view><text class="chevron">›</text></view>
<view class="action-row last" @click="consult"><image class="action-icon" src="/packages/finance/static/action-chat.svg" /><view class="action-copy"><text class="action-title">咨询AI</text><text class="action-note">结合本次报告,为您解释准备方向</text></view><text class="chevron">›</text></view>
</view>
<view v-if="section == 'prepare'" id="prepare-section" class="panel">
<text class="heading">材料准备</text>
<text v-if="productId.length == 0" class="body">{{ report!.matching.status == 'completed' ? '请先选择本次匹配中的产品,再查看对应材料。本页不会自动替您选择产品。' : '本报告没有可读取的产品匹配结果,暂不提供产品材料。您仍可查看报告、优化资料或咨询AI。' }}</text>
<template v-else-if="matchError.length > 0"><text class="body">{{ matchError }}</text><button class="secondary" @click="load">重新核对产品</button></template>
<template v-else-if="match != null">
<text class="product-title">{{ match!.product.name }}</text><text class="note">{{ match!.product.bank }} · 评估时版本 v{{ match!.product.version }}</text>
<text class="status">{{ match!.availabilityLabel }}</text>
<template v-if="canPrepare">
<text class="label">所需材料与办理说明</text><text class="body">{{ match!.product.materials }}</text>
<text class="label">申请条件(评估时)</text><text v-for="item in match!.product.conditions" :key="item" class="body">{{ item }}</text><text v-if="match!.product.conditions.length == 0" class="body">未提供</text>
<text class="label">资料来源</text><text class="body">{{ match!.product.source.trim() == '·' ? '未提供' : match!.product.source }}</text><text class="note">{{ match!.product.sourceRef.length > 0 ? match!.product.sourceRef : '来源引用未提供' }}</text><text class="note">核对日期:{{ match!.product.checkedOn.length > 0 ? match!.product.checkedOn : '未提供' }}</text>
<text class="label">机构办理渠道</text><text class="body">未提供。请自行通过机构官方渠道核实产品、材料和办理方式。</text>
</template>
<text v-else class="body">{{ match!.state != 'eligible' ? '本产品存在不符条件或待核实项,暂不能进入申请准备。您仍可查看报告、优化资料或咨询AI。' : '产品当前不可用或状态未知,暂不能进入申请准备。历史报告与评估时产品信息仍保留。' }}</text>
</template>
<button v-if="report!.matching.status == 'completed'" class="secondary" @click="chooseProduct">{{ productId.length == 0 ? '选择本次匹配产品' : '查看本次其他产品' }}</button>
<text v-else class="note">{{ report!.version == 1 ? '旧版报告未执行产品匹配,可重新评估。' : report!.matchingMessage }}</text>
</view>
<view v-if="improveOpen" id="improve-section" class="panel">
<text class="heading">优化资料后,重新评估</text><text class="note">以下内容来自本次已保存报告。请按实际情况核对资料。</text>
<view v-for="item in missingItems" :key="item.label" class="field-row"><text class="field-label">{{ item.label }}</text><text class="field-state">{{ item.value }}</text></view>
<text v-if="missingItems.length == 0" class="body">{{ report!.version == 1 ? '旧版报告未记录逐项完整度,请参考以下报告提示。' : '本次报告没有列出待补充或格式需核实字段,自填资料仍需核验。' }}</text>
<text v-for="item in report!.dataGaps" :key="item" class="body">{{ item }}</text><text v-for="item in report!.suggestions" :key="item" class="body">{{ item }}</text>
<text class="note">将回填本评估最新提交的资料。修改后保存为新的提交版本,历史资料和报告仍可查看。</text><button class="primary" @click="restart">修改资料并重新评估</button>
</view>
<view class="notice"><text class="notice-title">重要提示</text><text class="notice-text">本页仅提供材料准备与办理引导,不向银行或其他机构提交申请。匹配结果基于自填资料,不代表授信或获批承诺;实际产品条件、材料及办理方式以机构核实为准。</text></view>
<ReportExport :assessmentId="assessmentId" :revisionId="revisionId" @authrequired="login" @sessionchanged="load" />
</template>
<button class="home-button" @click="home">返回首页</button>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import AppHeader from '@/components/AppHeader.uvue'
import ReportExport from '@/packages/finance/components/ReportExport.uvue'
import { usePageLayout } from '@/services/layout.uts'
import { getSession } from '@/services/api.uts'
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
import { nextStepRoute } from '@/services/next-step-route.uts'
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
import { AnalysisReport, AssessmentField, fetchAnalysisReport, fetchAssessmentStatus } from '@/packages/finance/services/records.uts'
import { MatchView, fetchMatchDetailRecord, validRecordId } from '@/packages/finance/services/matches.uts'
const { pageStyle, contentStyle } = usePageLayout()
const revisionId = ref(''); const assessmentId = ref(''); const productId = ref(''); const section = ref('actions'); const assessmentType = ref('')
const report = ref<AnalysisReport | null>(null); const match = ref<MatchView | null>(null)
const loading = ref(false); const error = ref(''); const matchError = ref(''); const routeError = ref(''); const improveOpen = ref(false)
const scrollTarget = ref('')
let visible = false; let generation = 0; let loadedToken = ''
const canPrepare = computed((): boolean => match.value?.state == 'eligible' && match.value?.availability == 'available')
const missingItems = computed((): AssessmentField[] => report.value == null ? [] : report.value!.completenessItems.filter((item: AssessmentField): boolean => ['待补充', '需核实'].includes(item.value)))
const reset = () => { report.value = null; match.value = null; assessmentType.value = ''; matchError.value = ''; loadedToken = ''; improveOpen.value = false; scrollTarget.value = '' }
const home = () => { uni.reLaunch({ url: '/pages/home/home' }) }
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeNextStep=1&assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productId.value + '&section=' + section.value }) }
// 离页或账户变更后,丢弃旧请求;重新进入必须重查归属和当前可用性。
const stale = (request: number, token: string): boolean => {
if (!visible || request != generation) return true
if (getSession()?.getString('token') == token) return false
generation++; reset(); loading.value = false
if (getSession() == null) login(); else error.value = '账户已切换,请重新加载本人报告。'
return true
}
const load = async () => {
const request = ++generation; reset(); error.value = ''; loading.value = false
if (routeError.value.length > 0) { error.value = routeError.value; return }
const token = getSession()?.getString('token') ?? ''
if (token.length == 0) { login(); return }
loading.value = true
try {
const status = await fetchAssessmentStatus(assessmentId.value)
if (stale(request, token)) return
if (status.id != assessmentId.value || !status.hasReport || !['personal', 'enterprise'].includes(status.type)) throw new Error('请从本人已成功生成的报告进入下一步行动。')
const result = await fetchAnalysisReport(assessmentId.value, revisionId.value)
if (stale(request, token)) return
report.value = result; revisionId.value = result.revisionId; assessmentType.value = status.type; loadedToken = token
if (productId.value.length > 0) {
try {
if (result.matching.status != 'completed') throw new Error('本报告没有可读取的产品匹配结果。')
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
if (stale(request, token)) return
// 材料必须来自同次评估冻结产品,不允许用公共或其他报告产品补齐。
if (detail.assessmentId != assessmentId.value || detail.revisionId != result.revisionId || detail.resultVersion != result.version || detail.snapshotId != result.matching.snapshotId || detail.ruleVersion != result.matching.ruleVersion || detail.orderingVersion != result.matching.orderingVersion) throw new Error('产品与本报告的冻结版本不一致,请重新选择产品。')
match.value = detail.item
} catch (err) { if (!stale(request, token)) matchError.value = err instanceof Error ? err.message : '产品读取失败,请重试' }
}
} catch (err) { if (!stale(request, token)) { reset(); error.value = err instanceof Error ? err.message : '报告读取失败,请重试' } }
finally {
if (request == generation) {
loading.value = false
if (report.value != null && section.value == 'prepare') nextTick(() => { if (request == generation) scrollTarget.value = 'prepare-section' })
}
}
}
const ready = (): boolean => !stale(generation, loadedToken) && report.value != null && !loading.value
const openReport = () => { if (ready()) uni.navigateTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') }) }
const chooseProduct = () => { if (ready() && report.value!.matching.status == 'completed') uni.navigateTo({ url: '/packages/finance/matches/matches?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&group=eligible' }) }
const prepare = () => {
if (!ready()) return
if (section.value == 'prepare') { load(); return }
uni.redirectTo({ url: nextStepRoute(assessmentId.value, productId.value, 'prepare', revisionId.value) })
}
const optimize = () => {
if (!ready()) return
improveOpen.value = !improveOpen.value; scrollTarget.value = ''
const request = generation
if (improveOpen.value) nextTick(() => { if (request == generation) scrollTarget.value = 'improve-section' })
}
const consult = () => {
if (!ready()) return
if (productId.value.length > 0 && match.value == null) { uni.showToast({ title: '请先重新核对产品或从报告咨询AI', icon: 'none' }); return }
uni.navigateTo({ url: assistantRoute(assessmentId.value, productId.value.length > 0 ? [productId.value] : [], 'next-step', '', revisionId.value) })
}
const restart = () => {
if (!ready() || !improveOpen.value) return
clearAssessmentFlow()
uni.navigateTo({ url: '/packages/finance/assessment/assessment?editId=' + assessmentId.value })
}
const back = () => {
if (getCurrentPages().length > 1) uni.navigateBack()
else if (validRecordId(assessmentId.value)) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') })
else history()
}
onLoad((options) => {
revisionId.value = options?.['revisionId'] ?? '';
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError.value = '版本编号无效,请返回重新选择。'
assessmentId.value = options?.['assessmentId'] ?? ''; productId.value = options?.['productId'] ?? ''; section.value = options?.['section'] ?? 'actions'
if (nextStepRoute(assessmentId.value, productId.value, section.value, revisionId.value).length == 0) routeError.value = '页面参数无效,请从本次报告或匹配产品重新进入。'
})
onShow(() => { visible = true; load() })
onHide(() => { visible = false; generation++; reset(); loading.value = false; error.value = '' })
onUnload(() => { visible = false; generation++; reset() })
</script>
<style>
.page { background-color: #fff; }
.content { padding: 30rpx 36rpx 64rpx; }
.eyebrow { font-size: 22rpx; color: #91a0b2; }
.revision-notice { font-size: 24rpx; color: #9a753d; line-height: 38rpx; margin-top: 16rpx; }
.intro { font-size: 27rpx; color: #536882; margin-top: 16rpx; line-height: 42rpx; }
.actions { margin-top: 26rpx; }
.action-row { flex-direction: row; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid #eef2f7; }
.last { border-bottom-width: 0; }
.action-icon { width: 88rpx; height: 88rpx; flex-shrink: 0; }
.action-copy { flex: 1; margin-left: 24rpx; }
.action-title { font-size: 31rpx; font-weight: 700; color: #263d58; }
.action-note { font-size: 23rpx; color: #8795a8; line-height: 36rpx; margin-top: 10rpx; }
.chevron { color: #b2c0d2; font-size: 40rpx; margin-left: 16rpx; }
.panel { padding: 28rpx; margin-top: 24rpx; border: 1rpx solid #e3ebf5; border-radius: 18rpx; background-color: #f9fbff; }
.heading { font-size: 30rpx; color: #243e5c; font-weight: 700; line-height: 44rpx; }
.body { font-size: 25rpx; color: #536981; line-height: 42rpx; margin-top: 16rpx; }
.note { font-size: 22rpx; color: #8292a6; line-height: 36rpx; margin-top: 14rpx; }
.product-title { font-size: 29rpx; color: #214c83; font-weight: 700; margin-top: 24rpx; }
.demo, .status { color: #9a753d; font-size: 23rpx; line-height: 38rpx; margin-top: 16rpx; }
.label { color: #304b69; font-size: 26rpx; font-weight: 700; margin-top: 28rpx; }
.field-row { flex-direction: row; justify-content: space-between; padding: 22rpx 0; border-bottom: 1rpx solid #e8eef6; }
.field-label { flex: 1; font-size: 24rpx; color: #526880; }
.field-state { font-size: 24rpx; color: #aa7b37; margin-left: 16rpx; }
.notice { background-color: #eef6ff; border-radius: 16rpx; padding: 28rpx; margin-top: 32rpx; }
.notice-title { color: #2869b6; font-size: 27rpx; font-weight: 700; }
.notice-text { font-size: 23rpx; line-height: 39rpx; color: #6e87a4; margin-top: 14rpx; }
.primary, .home-button { margin-top: 32rpx; background-color: #0868f7; color: #fff; font-size: 27rpx; border-radius: 12rpx; }
.secondary { margin-top: 24rpx; background-color: #e7f0ff; color: #0868f7; font-size: 25rpx; border-radius: 12rpx; }
.link { color: #0868f7; font-size: 25rpx; margin-top: 24rpx; }
</style>