refactor(login): move match-route logic to dedicated service and fix import
- 提取登录回跳、产品对比的路由匹配逻辑至独立的 match-route.uts 服务 - 修复登录页面导入对比服务的错误,改为导入新的 match-route 模块 - 保持原有的路由参数校验和跳转逻辑不变,仅重构代码结构
This commit is contained in:
@@ -30,7 +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'
|
||||
import { matchReturnPath } from '@/services/match-route.uts'
|
||||
let afterLoginUrl = '/pages/home/home'
|
||||
onLoad((options) => {
|
||||
consumeInitialLaunch()
|
||||
|
||||
@@ -26,9 +26,12 @@
|
||||
<template v-else>
|
||||
<AssessmentSelect label="职业类型" v-model="form.occupation" :options="['企业职员', '事业单位', '公务员', '个体经营', '自由职业', '其他']" />
|
||||
<view class="form-row"><text class="field-label">工作单位</text><input v-model="form.company" class="field-input" placeholder="请输入工作单位" /></view>
|
||||
<view class="form-row"><text class="field-label">月收入(万元)</text><input v-model="form.monthlyIncome" class="field-input" type="digit" placeholder="请输入月收入" /></view>
|
||||
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||
<view class="form-row"><text class="field-label">月收入</text><input v-model="form.monthlyIncome" class="field-input" type="digit" placeholder="请输入月收入" /><text class="field-unit">万元</text></view>
|
||||
<AssessmentSelect label="社保连续缴纳" :modelValue="form.socialSecurity" @update:modelValue="socialSecurityChanged" :options="contributionOptions(form.socialSecurity)" />
|
||||
<view v-if="contributionBaseVisible(form.socialSecurity)" class="form-row"><text class="field-label">社保月基数</text><input v-model="form.socialSecurityBase" class="field-input" type="digit" maxlength="15" placeholder="选填" /><text class="field-unit">万元</text></view>
|
||||
<AssessmentSelect label="公积金连续缴纳" :modelValue="form.housingFund" @update:modelValue="housingFundChanged" :options="contributionOptions(form.housingFund)" />
|
||||
<view v-if="contributionBaseVisible(form.housingFund)" class="form-row"><text class="field-label">公积金月基数</text><input v-model="form.housingFundBase" class="field-input" type="digit" maxlength="15" placeholder="选填" /><text class="field-unit">万元</text></view>
|
||||
<text class="local-note">按当前连续缴纳时长选择。基数是计算缴费的月工资基数,不是个人缴费金额;不清楚可留空。</text>
|
||||
</template>
|
||||
|
||||
</view>
|
||||
@@ -69,8 +72,11 @@
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
|
||||
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, contributionValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||
import { contributionOptions as contributionChoices, contributionBaseVisible as baseVisible } from '@/services/contribution.uts'
|
||||
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||||
const contributionOptions = (current: string): string[] => contributionChoices(current)
|
||||
const contributionBaseVisible = (duration: string): boolean => baseVisible(duration)
|
||||
const showAgreement = (title: string) => { showAgreementNotice(title) }
|
||||
const editId = ref(''); const editError = ref(''); const editLoading = ref(false)
|
||||
let baseRevisionId = ''; let editGeneration = 0
|
||||
@@ -101,6 +107,8 @@
|
||||
if (session == null) uni.redirectTo({ url: '/packages/account/login/login' })
|
||||
return false
|
||||
}
|
||||
const socialSecurityChanged = (value: string) => { if (!ensureOwner()) return; form.value.socialSecurity = value; if (!contributionBaseVisible(value)) form.value.socialSecurityBase = '' }
|
||||
const housingFundChanged = (value: string) => { if (!ensureOwner()) return; form.value.housingFund = value; if (!contributionBaseVisible(value)) form.value.housingFundBase = '' }
|
||||
const startAccountTimer = () => {
|
||||
stopAccountTimer()
|
||||
if (ownerToken.length == 0) return
|
||||
@@ -109,7 +117,7 @@
|
||||
const rememberInput = () => {
|
||||
if (!initialized || !sameOwner()) return
|
||||
const source = form.value
|
||||
const currentForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears, operatingMonths: source.operatingMonths }
|
||||
const currentForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, socialSecurityBase: source.socialSecurityBase, housingFundBase: source.housingFundBase, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears, operatingMonths: source.operatingMonths }
|
||||
if (currentForm.city == '其他城市' && customCity.value.trim().length > 0) currentForm.city = customCity.value.trim()
|
||||
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value, editId.value, baseRevisionId)
|
||||
}
|
||||
@@ -216,6 +224,7 @@
|
||||
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
|
||||
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效年营收(万元,最多六位小数)' : '请输入有效月收入(万元,最多六位小数)'
|
||||
}
|
||||
if (step.value == 1 && message.length == 0 && assessmentType.value == 'personal') message = contributionValidationMessage(form.value)
|
||||
if (step.value == 2) message = assetValidationMessage(form.value)
|
||||
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
|
||||
if (step.value == 3 && authorizations.value.some((item) => item.selected) && !dataConsent.value) message = '请阅读并同意数据授权协议,或取消授权选择'
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { submitAssessment } from '@/packages/finance/services/api.uts'
|
||||
import { activeAssessmentType, getAssessmentInput, draftText, documentsFromInput, ownershipFromInput, formFromInput, assetValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||
import { activeAssessmentType, getAssessmentInput, draftText, documentsFromInput, ownershipFromInput, formFromInput, assetValidationMessage, contributionValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||
type InfoRow = { label: string, value: string }
|
||||
type InfoSection = { title: string, step: number, rows: InfoRow[], expanded: boolean }
|
||||
@@ -67,7 +67,7 @@
|
||||
const incomeRows: InfoRow[] = enterprise ? [
|
||||
{ label: '经营行业', value: displayValue(saved, 'occupation') }, { label: '经营月数', value: valueWithUnit(saved, 'operatingMonths', '个月') }, { label: '年营业收入', value: draftText(saved, 'annualRevenue').length > 0 ? draftText(saved, 'annualRevenue') + ' 万元' : '未填写' }
|
||||
] : [
|
||||
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 万元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
||||
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 万元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '社保月缴存基数', value: valueWithUnit(saved, 'socialSecurityBase', '万元') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }, { label: '公积金月缴存基数', value: valueWithUnit(saved, 'housingFundBase', '万元') }
|
||||
]
|
||||
const files = documentsFromInput(saved)
|
||||
const fileNames = files.map((file) => file.name).join('、')
|
||||
@@ -122,6 +122,8 @@
|
||||
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
||||
const grants = saved['authorizations'] as string[] | null
|
||||
if ((documentsFromInput(saved).length > 0 && saved['documentConsent'] != true) || (grants != null && grants.length > 0 && saved['dataConsent'] != true)) { uni.showToast({ title: '请确认资料与授权选择', icon: 'none' }); edit(3); return }
|
||||
const contributionError = assessmentType.value == 'personal' ? contributionValidationMessage(formFromInput(payload.value)) : ''
|
||||
if (contributionError.length > 0) { uni.showToast({ title: contributionError, icon: 'none' }); edit(1); return }
|
||||
const assetError = assetValidationMessage(formFromInput(saved))
|
||||
if (assetError.length > 0) { uni.showToast({ title: assetError, icon: 'none' }); edit(2); return }
|
||||
submitting.value = true
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
<template>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader title="本次融资匹配" :customBack="true" @back="back" />
|
||||
<AppHeader title="AI匹配结果" :customBack="true" @back="back" />
|
||||
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||
<view v-if="pageActive" class="content">
|
||||
<text class="eyebrow">评估记录 {{ assessmentId.length > 0 ? '#' + assessmentId : '待选择' }}</text>
|
||||
<text class="headline">找到适合本次需求的方向</text>
|
||||
<text class="intro">每项结论都有依据。自填资料尚未核验,匹配结果不代表审批。</text>
|
||||
<view class="summary-card"><text class="summary-title">{{ summaryTitle }}</text><text class="summary-note">依据本次资料筛选 · 查看产品了解具体条件</text></view>
|
||||
<view v-if="assessmentId.length == 0 && error.length == 0" class="state-panel"><text class="state-title">先选择一份评估</text><text class="muted">从融资档案查看已完成的评估,再获取对应匹配结果。</text><button class="primary" @click="history">选择历史评估</button><button class="secondary" @click="start">开始新评估</button></view>
|
||||
<template v-else>
|
||||
<view class="tabs">
|
||||
<view v-for="item in groups" :key="item.id" :class="group == item.id ? 'tab chosen' : 'tab'" @click="selectGroup(item.id)"><text :class="group == item.id ? 'tab-label selected' : 'tab-label'">{{ item.label }}</text><text class="count">{{ count(item.id) }}</text></view>
|
||||
</view>
|
||||
<text v-if="message.length > 0" class="muted notice">{{ message }}</text>
|
||||
<view v-if="error.length == 0 && assessmentId.length > 0" class="compare-bar"><text class="compare-caption">已选 {{ selectedIds.length }} / 3 项,可跨分组选择</text><button class="compare-button" :disabled="selectedIds.length < 2" @click="openCompare">对比所选产品</button></view>
|
||||
<text v-if="loading && matches.length == 0" class="state-copy">正在读取本次匹配结果…</text>
|
||||
<view v-if="error.length > 0" class="state-panel"><text class="state-title">暂时无法读取</text><text class="muted">{{ error }}</text><button class="secondary" @click="retry">重新加载</button><text class="link" @click="history">返回融资档案</text></view>
|
||||
<view v-if="pageError.length > 0" class="state-panel"><text class="muted">{{ pageError }}</text><button class="secondary" :disabled="loading" @click="loadMore">重试加载更多</button></view>
|
||||
<view v-if="!loading && error.length == 0 && matches.length == 0" class="state-panel"><text class="state-title">{{ status == 'unavailable' ? '本次未执行匹配' : (group == 'eligible' ? '暂无符合已知条件的产品' : (group == 'ineligible' ? '暂无条件不符的产品' : '暂无待核实的产品')) }}</text><text class="muted">{{ status == 'unavailable' ? '历史结果不会自动补算。可从融资档案发起新的评估。' : '可切换分组查看其他结果,或返回报告了解资料准备情况。' }}</text><text class="link" @click="report">查看本次报告</text></view>
|
||||
<view v-for="item in matches" :key="item.id" class="card" @click="openProduct(item.id)">
|
||||
<view class="card-head"><view class="mark"><text class="mark-text">融</text></view><view class="identity"><text class="name">{{ item.product.name }}</text><text class="bank">{{ item.product.bank }} · {{ item.product.objects }}</text></view></view>
|
||||
<view class="badge-row"><text :class="item.state == 'eligible' ? 'badge good' : (item.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ item.stateLabel }}</text></view>
|
||||
<view class="amount-row"><text class="amount">{{ item.product.amount }}</text><text class="caption">产品额度范围</text></view>
|
||||
<view class="metrics"><view class="metric"><text class="value">{{ item.product.rate }}</text><text class="caption">年化利率范围</text></view><view class="metric"><text class="value">{{ item.product.term }}</text><text class="caption">期限范围</text></view></view>
|
||||
<view class="reasons"><text class="reason" v-for="reason in preview(item)" :key="reason.id">{{ reason.text }}</text></view>
|
||||
<view class="card-head"><view :class="bankMarkClass(item.product.bank)"><text class="mark-text">{{ item.product.bank.slice(0, 1) }}</text></view><view class="identity"><text class="bank-name">{{ item.product.bank }}</text><text class="product-name">{{ item.product.name }}</text></view><text :class="item.state == 'eligible' ? 'match-state good' : (item.state == 'ineligible' ? 'match-state blocked' : 'match-state pending')">{{ item.stateLabel }}</text><text class="card-arrow">›</text></view>
|
||||
<view class="tags"><text class="tag">年化 {{ item.product.rate }}</text><text class="tag">{{ item.product.term }}</text><text class="tag">{{ item.product.objects }}</text></view>
|
||||
<text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }};历史结果仍可查看。</text>
|
||||
<view class="card-foot"><text class="version">评估时版本 v{{ item.product.version }}</text><text class="detail-link">查看依据与详情 ›</text></view>
|
||||
<button class="choose-button" @click.stop="toggleCompare(item.id)">{{ selectedIds.includes(item.id) ? '已加入对比 · 点击移除' : '加入对比' }}</button>
|
||||
<view class="card-foot"><text class="detail-link">查看匹配依据</text><view class="choose-control" @click.stop="toggleCompare(item.id)"><text :class="selectedIds.includes(item.id) ? 'selection-box selected-box' : 'selection-box'">{{ selectedIds.includes(item.id) ? '✓' : '+' }}</text><text class="choose-label">{{ selectedIds.includes(item.id) ? '已选对比' : '加入对比' }}</text></view></view>
|
||||
</view>
|
||||
<button v-if="hasMore && pageError.length == 0" class="secondary" :disabled="loading" @click="loadMore">{{ loading ? '正在加载…' : '查看更多' }}</button>
|
||||
<button v-if="hasMore && pageError.length == 0" class="more-button" :disabled="loading" @click="loadMore">{{ loading ? '正在加载…' : '查看更多产品(共' + count(group) + '项)' }}</button>
|
||||
<view v-if="error.length == 0 && matches.length > 0" class="compare-bar"><text class="compare-caption">已选 {{ selectedIds.length }} / 3 项</text><button class="compare-button" :disabled="selectedIds.length < 2" @click="openCompare">对比所选产品</button></view>
|
||||
<text v-if="matches.length > 0 && !hasMore" class="end-note">已显示本组全部结果 · 顺序不代表产品优劣</text>
|
||||
<text v-if="message.length > 0" class="muted notice">{{ message }}</text>
|
||||
<text class="link" @click="report">返回本次报告</text>
|
||||
</template>
|
||||
</view>
|
||||
@@ -39,7 +33,7 @@
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { MatchView, MatchReason, MatchPage, fetchMatchPage, validRecordId } from '@/packages/finance/services/matches.uts'
|
||||
import { MatchView, MatchPage, fetchMatchPage, validRecordId } from '@/packages/finance/services/matches.uts'
|
||||
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery, matchReturnPath } from '@/packages/finance/services/compare.uts'
|
||||
const { pageStyle, contentStyle } = usePageLayout()
|
||||
type MatchGroup = { id: string, label: string }
|
||||
@@ -52,7 +46,14 @@
|
||||
let cursor = ''; let generation = 0; let visible = false; let routeError = ''; let owner = ''
|
||||
let firstPage: MatchPage | null = null; let accountTimer = 0
|
||||
const count = (id: string): string => id == 'eligible' ? eligible.value : (id == 'ineligible' ? ineligible.value : unknown.value)
|
||||
const preview = (item: MatchView): MatchReason[] => item.state == 'ineligible' ? item.blocking.slice(0, 3) : (item.state == 'unknown' ? item.cautions.slice(0, 3) : item.positive.slice(0, 3))
|
||||
const summaryTitle = computed((): string => {
|
||||
if (assessmentId.value.length == 0) return '选择评估,查看融资匹配'
|
||||
if (error.value.length > 0) return '匹配结果暂不可读取'
|
||||
if (status.value == 'unavailable') return '这份报告尚未执行产品匹配'
|
||||
if (eligible.value == '—') return '正在为你读取匹配结果'
|
||||
return eligible.value == '0' ? '暂未筛选出符合已知条件的产品' : '为你筛选出' + eligible.value + '项符合已知条件的产品'
|
||||
})
|
||||
const bankMarkClass = (bank: string): string => bank.includes('工商') || bank.includes('招商') || bank.includes('中国银行') ? 'mark mark-red' : (bank.includes('农业') || bank.includes('邮储') ? 'mark mark-green' : 'mark mark-blue')
|
||||
const clear = () => { matches.value = []; message.value = ''; status.value = ''; pageError.value = ''; hasMore.value = false; cursor = ''; firstPage = null; eligible.value = '—'; ineligible.value = '—'; unknown.value = '—' }
|
||||
const stopAccountTimer = () => { if (accountTimer != 0) clearInterval(accountTimer); accountTimer = 0 }
|
||||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeMatch=1&matchPage=matches' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value).replace('?', '&') + '&group=' + group.value }) }
|
||||
@@ -130,34 +131,46 @@
|
||||
onUnload(() => { visible = false; pageActive.value = false; stopAccountTimer(); generation++; clear() })
|
||||
</script>
|
||||
<style>
|
||||
.compare-bar { background-color: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 22rpx; }
|
||||
.page { background-color: #fff; }
|
||||
.summary-card { background-color: #edf5ff; padding: 28rpx 26rpx; border-radius: 10rpx; }
|
||||
.summary-title { color: #1a5aa8; font-size: 31rpx; line-height: 47rpx; font-weight: 600; }
|
||||
.summary-note { color: #7188a4; font-size: 22rpx; line-height: 35rpx; margin-top: 9rpx; }
|
||||
.compare-bar { flex-direction: row; align-items: center; justify-content: space-between; border-top: 1rpx solid #edf1f6; padding: 20rpx 0; margin-top: 20rpx; }
|
||||
.compare-caption { color: #506c8b; font-size: 24rpx; }
|
||||
.compare-button { background-color: #0868f7; color: #fff; font-size: 25rpx; margin-top: 14rpx; border-radius: 12rpx; }
|
||||
.choose-button { color: #0868f7; background-color: #eef5ff; font-size: 24rpx; margin-top: 22rpx; border-radius: 12rpx; }
|
||||
.compare-button { background-color: #0868f7; color: #fff; font-size: 24rpx; border-radius: 10rpx; margin: 0; padding: 0 22rpx; }
|
||||
.content { padding: 30rpx 28rpx 60rpx; }
|
||||
.eyebrow { font-size: 22rpx; color: #61758f; }
|
||||
.headline { font-size: 37rpx; font-weight: 700; color: #142d4e; margin-top: 14rpx; }
|
||||
.intro { font-size: 24rpx; line-height: 38rpx; color: #718198; margin-top: 14rpx; }
|
||||
.tabs { flex-direction: row; background-color: #fff; border-radius: 16rpx; padding: 8rpx; margin-top: 28rpx; }
|
||||
.tab { flex: 1; align-items: center; padding: 17rpx 2rpx; border-radius: 12rpx; }
|
||||
.tabs { flex-direction: row; background-color: #f7f9fc; border-radius: 10rpx; padding: 6rpx; margin-top: 24rpx; margin-bottom: 24rpx; }
|
||||
.tab { flex: 1; flex-direction: row; align-items: center; justify-content: center; padding: 15rpx 2rpx; border-radius: 8rpx; }
|
||||
.chosen { background-color: #eaf3ff; }
|
||||
.tab-label { font-size: 24rpx; color: #6e7e93; }
|
||||
.selected { color: #0868f7; font-weight: 700; }
|
||||
.count { font-size: 31rpx; color: #223d60; margin-top: 8rpx; font-weight: 700; }
|
||||
.count { font-size: 24rpx; color: #61768e; margin-left: 8rpx; }
|
||||
.muted { font-size: 24rpx; line-height: 39rpx; color: #758399; }
|
||||
.notice { margin: 20rpx 4rpx; }
|
||||
.card { padding: 28rpx; background-color: #fff; border: 1rpx solid #e5edf7; border-radius: 20rpx; margin-bottom: 22rpx; }
|
||||
.card { padding: 24rpx 22rpx 18rpx; background-color: #fff; border: 1rpx solid #e2e8ef; border-radius: 10rpx; margin-bottom: 20rpx; }
|
||||
.card-head { flex-direction: row; align-items: center; }
|
||||
.mark { width: 70rpx; height: 70rpx; background-color: #eaf3ff; border-radius: 16rpx; align-items: center; justify-content: center; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.mark-text { font-size: 32rpx; color: #0868f7; font-weight: 700; }
|
||||
.mark { width: 66rpx; height: 66rpx; border-radius: 33rpx; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.mark-blue { background-color: #edf4fd; }
|
||||
.mark-red { background-color: #fcedef; }
|
||||
.mark-green { background-color: #eaf6f1; }
|
||||
.mark-text { font-size: 30rpx; color: #2765ad; font-weight: 700; }
|
||||
.mark-red .mark-text { color: #b84554; }
|
||||
.mark-green .mark-text { color: #28856b; }
|
||||
.identity { flex: 1; }
|
||||
.name { font-size: 29rpx; line-height: 42rpx; color: #203652; font-weight: 700; }
|
||||
.bank { font-size: 22rpx; color: #7a899d; line-height: 34rpx; margin-top: 6rpx; }
|
||||
.bank-name { font-size: 28rpx; line-height: 40rpx; color: #24354b; font-weight: 600; }
|
||||
.product-name { font-size: 22rpx; color: #7a899d; line-height: 34rpx; margin-top: 3rpx; }
|
||||
.match-state { font-size: 20rpx; line-height: 30rpx; max-width: 140rpx; flex-shrink: 0; text-align: right; margin-left: 10rpx; }
|
||||
.card-arrow { font-size: 38rpx; color: #a3aebe; margin-left: 12rpx; }
|
||||
.tags { flex-direction: row; flex-wrap: wrap; margin-top: 18rpx; }
|
||||
.tag { background-color: #f2f5f9; color: #718197; font-size: 20rpx; line-height: 30rpx; padding: 4rpx 10rpx; border-radius: 4rpx; margin-right: 8rpx; margin-bottom: 6rpx; }
|
||||
.badge-row { flex-direction: row; align-items: center; margin-top: 20rpx; }
|
||||
.badge { padding: 8rpx 16rpx; border-radius: 8rpx; font-size: 22rpx; }
|
||||
.good { color: #187857; background-color: #eaf7f1; }
|
||||
.blocked { color: #ae593d; background-color: #fff0e9; }
|
||||
.pending { color: #9a7320; background-color: #fff8e5; }
|
||||
.good { color: #187857; }
|
||||
.blocked { color: #ae593d; }
|
||||
.pending { color: #9a7320; }
|
||||
.demo { font-size: 21rpx; margin-left: 14rpx; color: #78879b; }
|
||||
.amount-row { margin-top: 26rpx; }
|
||||
.amount { font-size: 38rpx; font-weight: 700; color: #123a6c; line-height: 54rpx; }
|
||||
@@ -168,9 +181,14 @@
|
||||
.reasons { margin-top: 22rpx; border-top: 1rpx solid #edf2f8; padding-top: 12rpx; }
|
||||
.reason { font-size: 23rpx; line-height: 37rpx; color: #60758e; margin-top: 8rpx; }
|
||||
.availability { font-size: 23rpx; line-height: 37rpx; color: #a87534; margin-top: 12rpx; }
|
||||
.card-foot { flex-direction: row; justify-content: space-between; margin-top: 24rpx; }
|
||||
.card-foot { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 10rpx; }
|
||||
.version { font-size: 21rpx; color: #98a3b2; }
|
||||
.detail-link { font-size: 23rpx; color: #0868f7; }
|
||||
.detail-link { font-size: 20rpx; color: #8c99aa; }
|
||||
.choose-control { flex-direction: row; align-items: center; padding: 5rpx 0 5rpx 12rpx; }
|
||||
.selection-box { width: 28rpx; height: 28rpx; text-align: center; line-height: 26rpx; border: 1rpx solid #c3d4e9; border-radius: 6rpx; color: #527ba9; font-size: 22rpx; }
|
||||
.selected-box { background-color: #0868f7; color: #fff; border-color: #0868f7; }
|
||||
.choose-label { font-size: 21rpx; color: #527ba9; margin-left: 8rpx; }
|
||||
.more-button { background-color: #0868f7; color: #fff; font-size: 24rpx; border-radius: 10rpx; margin-top: 6rpx; }
|
||||
.state-panel { padding: 42rpx 28rpx; background-color: #fff; border-radius: 18rpx; margin-top: 24rpx; }
|
||||
.state-title { font-size: 30rpx; font-weight: 700; color: #273f5f; margin-bottom: 16rpx; }
|
||||
.state-copy { padding: 40rpx 12rpx; font-size: 26rpx; color: #73839a; }
|
||||
|
||||
@@ -18,16 +18,21 @@
|
||||
<text v-show="downloadStatus.length > 0" class="download-status">{{ downloadStatus }}</text>
|
||||
<view v-if="report!.version == 1" class="legacy"><text class="report-note">这是一份旧版自填报告,当时未采集融资需求、未计算完整度与准备状态,也未执行产品匹配。历史内容保持原样。</text></view>
|
||||
<template v-if="tab == 'overview'">
|
||||
<view :class="report!.readinessLabel == '可进入资料核对' ? 'readiness-band ready' : 'readiness-band attention'"><text class="readiness-label">融资准备状态</text><text class="readiness-title">{{ report!.readinessLabel }}</text></view>
|
||||
<text class="summary">{{ report!.summary }}</text>
|
||||
<view :class="report!.financingGrade.grade.startsWith('A') || report!.financingGrade.grade.startsWith('B') ? 'readiness-band ready' : 'readiness-band attention'">
|
||||
<text class="readiness-label">融资能力综合等级</text>
|
||||
<view v-if="report!.financingGrade.status == 'evaluated'" class="grade-line"><text class="grade-code">{{ report!.financingGrade.grade }}</text><text class="grade-label">({{ report!.financingGrade.label }})</text></view>
|
||||
<text v-else class="readiness-title">{{ report!.financingGrade.label }}</text>
|
||||
<text v-if="report!.financingGrade.status != 'evaluated'" class="grade-caption">{{ report!.financingGrade.notice }}</text>
|
||||
</view>
|
||||
<view class="priority-section"><text class="section-title">{{ priorityItems.length > 0 ? '需重点完善的 ' + priorityItems.slice(0, 3).length.toString() + ' 项' : '资料核验与准备' }}</text><view v-for="(item, index) in priorityItems.slice(0, 3)" :key="item" class="priority-row"><text class="priority-number">{{ index + 1 }}</text><text class="priority-text">{{ item }}</text></view><text v-if="priorityItems.length == 0" class="report-note">本次暂无额外改善项,请按机构要求核实资料。</text><text v-if="priorityItems.length > 3" class="more-advice" @click="switchTab('improvements')">查看全部 {{ priorityItems.length }} 项改善建议 ›</text></view>
|
||||
<!-- 首屏保持原型的状态、摘要和编号改善项;完整度与所有冻结依据按需展开。 -->
|
||||
<!-- 首屏展示等级和编号改善项;完整度与所有冻结依据按需展开。 -->
|
||||
<view class="details-toggle" @click="detailsOpen = !detailsOpen"><text class="details-label">查看完整报告</text><text class="details-action">{{ detailsOpen ? '收起' : '展开' }}</text></view>
|
||||
<template v-if="detailsOpen">
|
||||
<view v-if="report!.suggestions.length > 0" class="panel"><text class="section-title">改善建议与依据</text><text v-for="item in report!.suggestions" :key="item" class="body-text">{{ item }}</text></view>
|
||||
<view class="panel"><view class="panel-heading"><text class="section-title">资料完整度</text><text class="percent">{{ report!.completenessPercent == null ? '—' : report!.completenessPercent!.toString() + '%' }}</text></view><view v-if="report!.completenessPercent != null" class="progress-track"><view class="progress-fill" :style="'width:' + report!.completenessPercent!.toString() + '%;'"></view></view><text class="report-note">{{ report!.completenessText }}</text><text class="report-note">完整度不代表信用评分、资料真实性或获批概率。</text></view>
|
||||
<view v-if="report!.demand.length > 0" class="panel"><text class="section-title">本次资金需求</text><view class="fact-row" v-for="fact in report!.demand" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view></view>
|
||||
<view v-if="report!.version == 2" class="panel"><text class="section-title">准备状态依据</text><text v-if="report!.readinessBasis.length == 0" class="report-note">本次没有可展示的准备状态依据。</text><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text><text class="report-note">资料未经核验;完整度达到100%仍需核对机构要求。</text></view>
|
||||
<view class="panel"><text class="section-title">综合评分依据</text><template v-if="report!.financingGrade.status == 'evaluated'"><text class="body-text">融资能力 {{ report!.financingGrade.capacityScore }} / 100 · 资料完整度 {{ report!.financingGrade.completenessPercent }}%</text><text class="report-note">加权原始分 {{ report!.financingGrade.weightedScore }} · 最终分 {{ report!.financingGrade.score }}</text><view class="grade-dimension" v-for="item in report!.financingGrade.dimensions" :key="item.key"><view class="panel-heading"><text class="body-text">{{ item.label }}</text><text class="dimension-score">{{ item.score }} / {{ item.maximum }}</text></view><text class="report-note">{{ item.basis }}</text></view></template><text v-for="item in report!.financingGrade.limitations" :key="item" class="body-text">{{ item }}</text><text class="report-note">{{ report!.financingGrade.notice }}</text></view>
|
||||
<view v-if="report!.version == 2" class="panel"><text class="section-title">资料准备状态:{{ report!.readinessLabel }}</text><text v-if="report!.readinessBasis.length == 0" class="report-note">本次没有可展示的准备状态依据。</text><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text><text class="report-note">资料未经核验;完整度达到100%仍需核对机构要求。</text></view>
|
||||
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '主要优势' : '资料解读' }}</text><text v-if="report!.observations.length == 0" class="report-note">暂无有充分依据的优势说明。</text><text class="body-text" v-for="(item, index) in report!.observations" :key="index">{{ item }}</text></view>
|
||||
<view class="panel"><view class="panel-heading" @click="factsOpen = !factsOpen"><text class="section-title">本次提交的事实</text><text class="toggle">{{ factsOpen ? '收起' : '展开' }}</text></view><text class="report-note">来源:用户自填,未经外部核验;未填写不等于零。</text><template v-if="factsOpen"><view class="fact-row" v-for="fact in report!.facts" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view><text v-if="report!.facts.length == 0" class="report-note">暂无可展示的事实。</text></template></view>
|
||||
<view v-if="report!.version == 2" class="panel"><view class="panel-heading" @click="completenessOpen = !completenessOpen"><text class="section-title">逐项填写情况</text><text class="toggle">{{ completenessOpen ? '收起' : '展开' }}</text></view><template v-if="completenessOpen"><view class="fact-row" v-for="item in report!.completenessItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text v-if="report!.completenessItems.length == 0" class="report-note">本次未评定逐项完整度。</text></template></view>
|
||||
@@ -94,13 +99,7 @@
|
||||
let generation = 0; let visible = false
|
||||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeReport=1&assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&tab=' + tab.value }) }
|
||||
const missingItems = computed((): AssessmentField[] => report.value == null ? [] : report.value!.completenessItems.filter((item: AssessmentField): boolean => ['待补充','需核实'].includes(item.value)))
|
||||
const priorityItems = computed((): string[] => {
|
||||
const value = report.value
|
||||
if (value == null) return []
|
||||
// 使用已保存建议及真实缺失项,不补造原型中的固定三条或融资评分。
|
||||
const items = value.suggestionSummaries.concat(missingItems.value.map((item: AssessmentField): string => (item.value == '待补充' ? '补充' : '核实') + item.label))
|
||||
return items.filter((item: string, index: number): boolean => item.trim().length > 0 && items.indexOf(item) == index)
|
||||
})
|
||||
const priorityItems = computed((): string[] => report.value == null ? [] : report.value!.priorityItems)
|
||||
const adviceReasons = (item: MatchView): MatchReason[] => item.state == 'ineligible' ? item.blocking.concat(item.cautions) : item.cautions
|
||||
const reset = () => { report.value = null; candidates.value = []; adviceGroups.value = []; candidateLoading.value = false; candidateError.value = ''; factsOpen.value = false; completenessOpen.value = false; metadataOpen.value = false; detailsOpen.value = false; downloadStatus.value = '' }
|
||||
const current = (request: number, token: string): boolean => visible && request == generation && getSession()?.getString('token') == token
|
||||
@@ -214,9 +213,16 @@
|
||||
.readiness-band { padding: 28rpx 30rpx; min-height: 154rpx; background-color: #eef9f3; }
|
||||
.attention { background-color: #fff8ed; }
|
||||
.readiness-label { color: #526272; font-size: 25rpx; line-height: 38rpx; }
|
||||
.grade-line { flex-direction: row; align-items: center; flex-wrap: wrap; margin-top: 8rpx; }
|
||||
.grade-code { font-size: 76rpx; line-height: 90rpx; font-weight: 700; color: #179363; }
|
||||
.grade-label { font-size: 36rpx; font-weight: 600; color: #179363; }
|
||||
.grade-caption { font-size: 22rpx; line-height: 34rpx; color: #687b70; margin-top: 8rpx; }
|
||||
.attention .grade-code { color: #a57925; }
|
||||
.attention .grade-label { color: #a57925; }
|
||||
.grade-dimension { padding: 16rpx 0; border-bottom: 1rpx solid #edf1f5; }
|
||||
.dimension-score { font-size: 25rpx; line-height: 38rpx; color: #2763ad; }
|
||||
.readiness-title { color: #17a46b; font-size: 46rpx; line-height: 64rpx; font-weight: 700; margin-top: 9rpx; }
|
||||
.attention .readiness-title { color: #a57925; }
|
||||
.summary { font-size: 26rpx; line-height: 44rpx; color: #4d5869; margin-top: 24rpx; }
|
||||
.priority-section { margin-top: 30rpx; }
|
||||
.priority-row { flex-direction: row; align-items: flex-start; padding: 24rpx 0; border-bottom: 1rpx solid #edf0f4; min-height: 88rpx; }
|
||||
.priority-number { width: 36rpx; height: 36rpx; border-radius: 18rpx; background-color: #f66f4e; text-align: center; color: #fff; font-size: 22rpx; line-height: 36rpx; margin-right: 18rpx; margin-top: 2rpx; flex-shrink: 0; }
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { validRecordId, MatchDetail, MatchReason } from './matches.uts'
|
||||
import { MatchDetail, MatchReason } from './matches.uts'
|
||||
import { parseCompareIds } from '@/services/match-route.uts'
|
||||
export { parseCompareIds, compareQuery, matchReturnPath } from '@/services/match-route.uts'
|
||||
|
||||
let scope = ''
|
||||
let selected: string[] = []
|
||||
|
||||
export function parseCompareIds(raw: string): string[] {
|
||||
if (raw.length == 0) return []
|
||||
const ids = raw.split(',')
|
||||
if (ids.length > 3) throw new Error('每次最多对比3个产品,请返回重新选择。')
|
||||
const seen: string[] = []
|
||||
ids.forEach((id: string) => {
|
||||
if (!validRecordId(id) || seen.includes(id)) throw new Error('对比产品编号无效或重复,请返回重新选择。')
|
||||
seen.push(id)
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
// 只缓存少量选择ID,不持久化产品或个人事实;换账号、会话或评估后立即隔离。
|
||||
export function compareSelection(assessmentId: string, revisionId: string = ''): string[] {
|
||||
const session = getSession()
|
||||
@@ -27,23 +17,6 @@ export function saveCompareSelection(assessmentId: string, ids: string[], revisi
|
||||
compareSelection(assessmentId, revisionId)
|
||||
selected = parseCompareIds(ids.join(','))
|
||||
}
|
||||
export function compareQuery(assessmentId: string, ids: string[], revisionId: string = ''): string {
|
||||
return '?assessmentId=' + encodeURIComponent(assessmentId) + '&productIds=' + ids.join(',') + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
|
||||
}
|
||||
// 登录回跳只接受已声明页面与编号,不接受任意URL。
|
||||
export function matchReturnPath(page: string, assessmentId: string, rawIds: string, revisionId: string, productId: string = '', group: string = 'eligible', tab: string = 'explanation', from: string = ''): string {
|
||||
if (!['matches','product','compare'].includes(page) || (revisionId.length > 0 && !validRecordId(revisionId))) return ''
|
||||
if (assessmentId.length > 0 ? !validRecordId(assessmentId) : page != 'product') return ''
|
||||
let ids: string[] = []
|
||||
try { ids = parseCompareIds(rawIds) } catch (err) { return '' }
|
||||
if (assessmentId.length == 0 && (revisionId.length > 0 || ids.length > 0)) return ''
|
||||
if (page == 'matches' && !['eligible','ineligible','unknown'].includes(group)) return ''
|
||||
if (page == 'product' && (!validRecordId(productId) || !['explanation','details','conditions'].includes(tab) || (assessmentId.length == 0 && tab == 'explanation') || !['','matches','product','compare','report','next-step','archive'].includes(from))) return ''
|
||||
const query = assessmentId.length > 0 ? compareQuery(assessmentId, ids, revisionId) : '?'
|
||||
if (page == 'matches') return '/packages/finance/matches/matches' + query + '&group=' + group
|
||||
if (page == 'compare') return '/packages/finance/compare/compare' + query
|
||||
return '/packages/finance/product/product' + query + (assessmentId.length > 0 ? '&' : '') + 'productId=' + productId + '&tab=' + tab + '&from=' + from
|
||||
}
|
||||
export function sameMatchSnapshot(a: MatchDetail, b: MatchDetail): boolean {
|
||||
return a.assessmentId == b.assessmentId && a.revisionId == b.revisionId && a.resultVersion == b.resultVersion && a.snapshotId == b.snapshotId && a.ruleVersion == b.ruleVersion && a.orderingVersion == b.orderingVersion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export type FinancingDimension = { key: string, label: string, score: number, maximum: number, basis: string }
|
||||
export type FinancingGradeView = { status: string, grade: string, label: string, score: string, weightedScore: string, capacityScore: number, completenessPercent: number, capacityWeight: number, completenessWeight: number, dimensions: FinancingDimension[], limitations: string[], notice: string }
|
||||
|
||||
// 只读取该版报告保存的等级;旧报告不借用当前规则在客户端补算。
|
||||
export function financingGradeFrom(raw: UTSJSONObject | null): FinancingGradeView {
|
||||
const empty: FinancingGradeView = { status: 'unavailable', grade: '', label: '本次未评级', score: '', weightedScore: '', capacityScore: 0, completenessPercent: 0, capacityWeight: 80, completenessWeight: 20, dimensions: [], limitations: [], notice: '这份报告未计算综合等级,可重新评估获取。' }
|
||||
if (raw == null) return empty
|
||||
const fail = (): void => { throw new Error('融资等级数据暂不可读取,请重新加载。') }
|
||||
const status = raw.getString('status') ?? ''
|
||||
if (!['evaluated', 'insufficient_data', 'needs_verification', 'policy_unconfirmed'].includes(status) || raw.getString('verificationStatus') != 'not_verified' || (raw.getString('policyVersion') ?? '').length == 0) fail()
|
||||
empty.status = status
|
||||
empty.label = raw.getString('label') ?? '本次未评级'
|
||||
empty.notice = raw.getString('notice') ?? ''
|
||||
const limitations = raw['limitations'] as string[] | null
|
||||
if (limitations == null || !Array.isArray(limitations) || limitations.some((item: string): boolean => typeof item != 'string')) fail()
|
||||
empty.limitations = limitations!
|
||||
if (status != 'evaluated') {
|
||||
if (raw['grade'] != null || raw['score'] != null) fail()
|
||||
return empty
|
||||
}
|
||||
const grade = raw.getString('grade') ?? ''
|
||||
const score = raw.getString('score') ?? ''
|
||||
const weighted = raw.getString('weightedScore') ?? ''
|
||||
const capacity = raw.getNumber('capacityScore'); const complete = raw.getNumber('completenessPercent')
|
||||
const capacityWeight = raw.getNumber('capacityWeight'); const completeWeight = raw.getNumber('completenessWeight')
|
||||
if (!['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-'].includes(grade)
|
||||
|| !/^(?:100|[1-9]?[0-9])\.[0-9]{2}$/.test(score) || !/^(?:100|[1-9]?[0-9])\.[0-9]{2}$/.test(weighted) || parseFloat(score) > 100 || parseFloat(weighted) > 100 || parseFloat(score) > parseFloat(weighted)
|
||||
|| capacity == null || !Number.isInteger(capacity) || capacity < 0 || capacity > 100 || complete == null || !Number.isInteger(complete) || complete < 0 || complete > 100
|
||||
|| capacityWeight == null || completeWeight == null || !Number.isInteger(capacityWeight) || !Number.isInteger(completeWeight) || capacityWeight < 1 || completeWeight < 1 || capacityWeight + completeWeight != 100) fail()
|
||||
const dimensions = raw['dimensions'] as UTSJSONObject[] | null
|
||||
if (dimensions == null || !Array.isArray(dimensions) || dimensions.length != 5) fail()
|
||||
const keys: string[] = []
|
||||
let sum = 0; let maximumSum = 0
|
||||
empty.dimensions = dimensions!.map((item: UTSJSONObject): FinancingDimension => {
|
||||
const key = item.getString('key') ?? ''; const points = item.getNumber('score'); const maximum = item.getNumber('maximum')
|
||||
if (!['repayment', 'debt', 'income', 'supplement', 'assets'].includes(key) || keys.includes(key) || points == null || maximum == null || !Number.isInteger(points) || !Number.isInteger(maximum) || points < 0 || points > maximum || maximum < 1) fail()
|
||||
keys.push(key); sum += points!; maximumSum += maximum!
|
||||
return { key, label: item.getString('label') ?? '', score: points!, maximum: maximum!, basis: item.getString('basis') ?? '' }
|
||||
})
|
||||
if (sum != capacity || maximumSum != 100) fail()
|
||||
empty.grade = grade; empty.score = score; empty.weightedScore = weighted; empty.capacityScore = capacity!; empty.completenessPercent = complete!; empty.capacityWeight = capacityWeight!; empty.completenessWeight = completeWeight!
|
||||
return empty
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getSession, requestJson } from '@/services/api.uts'
|
||||
import { assessmentApiPayload, draftText } from '@/services/assessment.uts'
|
||||
import { assessmentRequestKey } from '@/services/assessment-flow.uts'
|
||||
import { contributionLabels, contributionCodes, contributionLabel } from '@/services/contribution.uts'
|
||||
import { yuanToWan } from '@/services/money.uts'
|
||||
import { FinancingGradeView, financingGradeFrom } from '@/packages/finance/services/financing-grade.uts'
|
||||
|
||||
export type AssessmentRecord = { id: string, type: string, status: string, statusLabel: string, message: string, city: string, submittedAt: string, taskId: string, stage: string, canRetry: boolean, revisionId: string, revisionNo: number, hasReport: boolean, publishedRevisionId: string }
|
||||
export type AssessmentPage = { items: AssessmentRecord[], hasMore: boolean, nextCursor: string }
|
||||
@@ -44,10 +46,48 @@ export async function retryAssessment(id: string, requestId: string): Promise<vo
|
||||
}
|
||||
|
||||
export type ReportMatching = { status: string, snapshotId: string, ruleVersion: string, orderingVersion: string, eligible: string, ineligible: string, unknown: string }
|
||||
export type AnalysisReport = { revisionId: string, revisionNo: number, isPreviousResult: boolean, currentStatus: string, version: number, generatedAt: string, completenessPercent: number | null, matching: ReportMatching, summary: string, facts: AssessmentField[], observations: string[], suggestions: string[], suggestionSummaries: string[], dataGaps: string[], disclaimer: string, matchingMessage: string, modelCode: string, demand: AssessmentField[], completenessText: string, completenessItems: AssessmentField[], readinessLabel: string, readinessBasis: string[], warnings: string[] }
|
||||
export type AnalysisReport = { revisionId: string, revisionNo: number, isPreviousResult: boolean, currentStatus: string, version: number, generatedAt: string, completenessPercent: number | null, matching: ReportMatching, summary: string, facts: AssessmentField[], observations: string[], suggestions: string[], priorityItems: string[], dataGaps: string[], disclaimer: string, matchingMessage: string, modelCode: string, demand: AssessmentField[], completenessText: string, completenessItems: AssessmentField[], readinessLabel: string, readinessBasis: string[], warnings: string[], financingGrade: FinancingGradeView }
|
||||
|
||||
function reportPriorityItems(improvements: UTSJSONObject[], fields: UTSJSONObject[], basis: UTSJSONObject[]): string[] {
|
||||
const items: string[] = []; const covered: string[] = []
|
||||
// 仅用冻结证据中的明确问题标识合并;普通事实引用不表示同一个问题。
|
||||
improvements.forEach((item: UTSJSONObject) => {
|
||||
const text = draftText(item, 'text').trim()
|
||||
if (text.length == 0) return
|
||||
const keys: string[] = []
|
||||
const refs = item['evidenceRefs'] as string[]
|
||||
refs.forEach((ref: string) => {
|
||||
let related: string[] = []
|
||||
if (ref.startsWith('fact:')) {
|
||||
const field = fields.find((entry: UTSJSONObject): boolean => draftText(entry, 'fieldKey') == ref.slice(5))
|
||||
if (field != null && ['missing', 'invalid'].includes(draftText(field, 'state'))) related = [draftText(field, 'state') + ':' + ref.slice(5)]
|
||||
} else if (ref.startsWith('readiness:')) {
|
||||
const entry = basis.find((value: UTSJSONObject): boolean => draftText(value, 'basisId') == ref.slice(10))
|
||||
if (entry != null && ['MISSING_FIELD', 'INVALID_FIELD'].includes(draftText(entry, 'code'))) {
|
||||
const kind = draftText(entry, 'code') == 'MISSING_FIELD' ? 'missing' : 'invalid'
|
||||
related = (entry['fieldRefs'] as string[]).map((key: string): string => kind + ':' + key)
|
||||
} else if (entry != null && (entry['issueRefs'] as string[]).length > 0) related = [ref]
|
||||
}
|
||||
related.forEach((key: string) => { if (!keys.includes(key)) keys.push(key) })
|
||||
})
|
||||
if (!items.includes(text) && (keys.length == 0 || keys.some((key: string): boolean => !covered.includes(key)))) items.push(text)
|
||||
keys.forEach((key: string) => { if (!covered.includes(key)) covered.push(key) })
|
||||
})
|
||||
fields.forEach((field: UTSJSONObject) => {
|
||||
const state = draftText(field, 'state'); const key = state + ':' + draftText(field, 'fieldKey')
|
||||
const label = draftText(field, 'label').trim()
|
||||
if (!['missing', 'invalid'].includes(state) || covered.includes(key) || label.length == 0) return
|
||||
const text = (state == 'missing' ? '补充' : '核实') + label
|
||||
if (!items.includes(text)) items.push(text)
|
||||
covered.push(key)
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
export async function fetchAnalysisReport(id: string, revisionId: string = ''): Promise<AnalysisReport> {
|
||||
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
|
||||
const version = result['resultVersion'] as number
|
||||
const financingGrade = financingGradeFrom(version == 2 ? result['financingGrade'] as UTSJSONObject | null : null)
|
||||
if (revisionId.length > 0 && result.getString('revisionId') != revisionId) throw new Error('报告版本不一致,请重新选择。')
|
||||
if (result['assessmentId'] != id || (version != 1 && version != 2) || (version == 1 && result['reportType'] != 'self_reported_analysis') || (version == 2 && result['reportType'] != 'financing_needs_analysis')) throw new Error('报告格式不支持')
|
||||
const date = new Date(draftText(result, 'generatedAt'))
|
||||
@@ -74,6 +114,7 @@ export async function fetchAnalysisReport(id: string, revisionId: string = ''):
|
||||
if (fact['value'] != null && fact['unit'] == 'month') value += ' 个月'
|
||||
if (fact['state'] == 'invalid') value = '格式需核实'
|
||||
const enumLabels: UTSJSONObject = { six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '无' }
|
||||
if (['socialSecurity', 'housingFund'].includes(draftText(fact, 'fieldKey'))) value = contributionLabel(value)
|
||||
if (enumLabels[value] != null) value = enumLabels[value] as string
|
||||
// v2的需求在独立区块展示,避免用途代码和同一金额重复出现。
|
||||
if (version == 2 && ['requestedAmountYuan', 'requestedTermMonths', 'purposeCode'].includes(draftText(fact, 'fieldKey'))) return
|
||||
@@ -108,17 +149,18 @@ export async function fetchAnalysisReport(id: string, revisionId: string = ''):
|
||||
}).filter((label: string): boolean => label.length > 0)
|
||||
return draftText(item, 'text') + (labels.length > 0 ? '(依据:' + labels.join(';') + ')' : '')
|
||||
})
|
||||
return { revisionId: draftText(result, 'revisionId'), revisionNo: result.getNumber('revisionNo') ?? 1, isPreviousResult: result.getBoolean('isPreviousResult') ?? false, currentStatus: draftText(result, 'currentStatus'), version, generatedAt, completenessPercent, matching, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), suggestionSummaries: (result['improvements'] as UTSJSONObject[]).map((item: UTSJSONObject): string => draftText(item, 'text')), dataGaps: explain('riskWarnings'),
|
||||
return { revisionId: draftText(result, 'revisionId'), revisionNo: result.getNumber('revisionNo') ?? 1, isPreviousResult: result.getBoolean('isPreviousResult') ?? false, currentStatus: draftText(result, 'currentStatus'), version, generatedAt, completenessPercent, matching, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), priorityItems: reportPriorityItems(result['improvements'] as UTSJSONObject[], items, basis), dataGaps: explain('riskWarnings'),
|
||||
// 新报告展示服务端冻结结果的统计;旧报告无此字段时保留原提示,不重算历史。
|
||||
disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage').length > 0 ? draftText(result, 'matchingMessage') : '产品资料及匹配规则尚未就绪,本报告未执行产品匹配。', modelCode: draftText(result, 'modelCode'),
|
||||
demand, completenessText, completenessItems, readinessLabel: readiness['status'] == 'evaluated' ? draftText(readiness, 'label') : '未评定',
|
||||
demand, completenessText, completenessItems, financingGrade, readinessLabel: readiness['status'] == 'evaluated' ? draftText(readiness, 'label') : '未评定',
|
||||
readinessBasis: basis.map((item: UTSJSONObject): string => draftText(item, 'message')), warnings: result['systemWarnings'] as string[] }
|
||||
}
|
||||
const observations = (result['observations'] as UTSJSONObject[]).map((item: UTSJSONObject): string => {
|
||||
const fact = rawFacts.find((entry: UTSJSONObject): boolean => entry['id'] == item['factId'])
|
||||
return (fact == null ? '' : draftText(fact, 'label') + ':') + draftText(item, 'text')
|
||||
})
|
||||
return { revisionId: draftText(result, 'revisionId'), revisionNo: result.getNumber('revisionNo') ?? 1, isPreviousResult: result.getBoolean('isPreviousResult') ?? false, currentStatus: draftText(result, 'currentStatus'), version, generatedAt, completenessPercent: null, matching, summary: draftText(result, 'summary'), facts, observations, suggestions: result['suggestions'] as string[], suggestionSummaries: result['suggestions'] as string[], dataGaps: result['dataGaps'] as string[], disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage'), modelCode: draftText(result, 'modelCode'), demand: [], completenessText: '旧版报告未计算资料完整度', completenessItems: [], readinessLabel: '未评定', readinessBasis: [], warnings: [] }
|
||||
const legacySuggestions = (result['suggestions'] as string[]).map((text: string): string => text.trim())
|
||||
return { revisionId: draftText(result, 'revisionId'), revisionNo: result.getNumber('revisionNo') ?? 1, isPreviousResult: result.getBoolean('isPreviousResult') ?? false, currentStatus: draftText(result, 'currentStatus'), version, generatedAt, completenessPercent: null, matching, summary: draftText(result, 'summary'), facts, observations, suggestions: result['suggestions'] as string[], priorityItems: legacySuggestions.filter((text: string, index: number): boolean => text.length > 0 && legacySuggestions.indexOf(text) == index), dataGaps: result['dataGaps'] as string[], disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage'), modelCode: draftText(result, 'modelCode'), demand: [], completenessText: '旧版报告未计算资料完整度', completenessItems: [], readinessLabel: '未评定', readinessBasis: [], warnings: [], financingGrade }
|
||||
}
|
||||
|
||||
function enumValue(value: string, labels: string[], codes: string[]): string | null {
|
||||
@@ -140,7 +182,7 @@ export async function createAssessment(payload: UTSJSONObject): Promise<string>
|
||||
const values = assessmentApiPayload(payload)
|
||||
const form = new UTSJSONObject()
|
||||
const textKeys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode']
|
||||
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
|
||||
textKeys.concat(moneyKeys).forEach((key: string) => { const value = draftText(values, key).trim(); form[key] = value.length == 0 ? null : value })
|
||||
if (form['idCard'] != null) form['idCard'] = (form['idCard'] as string).toUpperCase()
|
||||
if (form['businessCode'] != null) form['businessCode'] = (form['businessCode'] as string).toUpperCase()
|
||||
@@ -152,7 +194,7 @@ export async function createAssessment(payload: UTSJSONObject): Promise<string>
|
||||
})
|
||||
form['maritalStatus'] = enumValue(draftText(values, 'maritalStatus'), ['已婚', '未婚', '离异', '丧偶'], ['married', 'single', 'divorced', 'widowed'])
|
||||
const socialKeys = ['socialSecurity', 'housingFund']
|
||||
socialKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳'], ['six_months_plus', 'under_six_months', 'none']) })
|
||||
socialKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), contributionLabels, contributionCodes) })
|
||||
const ownershipKeys = ['house', 'car']
|
||||
ownershipKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['有', '无'], ['yes', 'no']) })
|
||||
const cityNames = ['深圳市', '广州市', '北京市', '上海市', '杭州市']
|
||||
@@ -221,20 +263,21 @@ export async function fetchAssessmentDetail(id: string, revisionId: string = '')
|
||||
fields.push(field); sections[section].fields.push(field)
|
||||
if (provided) sections[section].provided++
|
||||
}
|
||||
const keys = ['name', 'phone', 'city', 'idCard', 'enterpriseName', 'businessCode', 'occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car']
|
||||
const labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车']
|
||||
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||
const keys = ['name', 'phone', 'city', 'idCard', 'enterpriseName', 'businessCode', 'occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car', 'socialSecurityBase', 'housingFundBase']
|
||||
const labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车', '社保月缴存基数', '公积金月缴存基数']
|
||||
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
|
||||
const enumLabels: UTSJSONObject = { married: '已婚', single: '未婚', divorced: '离异', widowed: '丧偶', yes: '有', no: '无', six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '未缴纳' }
|
||||
keys.forEach((key: string, index: number) => {
|
||||
const raw = form[key]
|
||||
let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : raw as string)
|
||||
if (raw != null && moneyKeys.includes(key)) value = yuanToWan(value) + ' 万元'
|
||||
if (raw != null && key == 'operatingMonths') value += ' 个月'
|
||||
if (['socialSecurity', 'housingFund'].includes(key)) value = contributionLabel(value)
|
||||
if (['maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car'].includes(key) && enumLabels[value] != null) value = enumLabels[value] as string
|
||||
const personalOnly = ['idCard', 'company', 'monthlyIncome', 'maritalStatus', 'socialSecurity', 'housingFund']
|
||||
const personalOnly = ['idCard', 'company', 'monthlyIncome', 'maritalStatus', 'socialSecurity', 'housingFund', 'socialSecurityBase', 'housingFundBase']
|
||||
const enterpriseOnly = ['enterpriseName', 'businessCode', 'operatingMonths', 'annualRevenue']
|
||||
if ((result['type'] == 'enterprise' && personalOnly.includes(key)) || (result['type'] == 'personal' && enterpriseOnly.includes(key))) return
|
||||
const incomeKeys = ['occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'socialSecurity', 'housingFund']
|
||||
const incomeKeys = ['occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'socialSecurity', 'housingFund', 'socialSecurityBase', 'housingFundBase']
|
||||
const assetKeys = ['creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'house', 'car']
|
||||
// “无”及数值0都是已填写,不能用真值判断遗漏零负债、零经营月数。
|
||||
addField(incomeKeys.includes(key) ? 1 : (assetKeys.includes(key) ? 2 : 0), labels[index], value, raw != null && value.length > 0)
|
||||
@@ -255,14 +298,15 @@ export async function fetchAssessmentEdit(id: string): Promise<UTSJSONObject> {
|
||||
const form = result['form'] as UTSJSONObject | null
|
||||
if (form == null || Array.isArray(form) || typeof form != 'object' || result.getString('moneyUnit') != 'yuan') throw new Error('修改资料不完整,请返回档案重新读取。')
|
||||
const payload: UTSJSONObject = { type: result['type'], editAssessmentId: id, baseRevisionId: result['revisionId'], step: 0 }
|
||||
const money = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||
const money = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
|
||||
const labels: UTSJSONObject = { married: '已婚', single: '未婚', divorced: '离异', widowed: '丧偶', yes: '有', no: '无', six_months_plus: '连续缴纳 6 个月以上', under_six_months: '缴纳不足 6 个月', none: '未缴纳' }
|
||||
const enums = ['maritalStatus', 'house', 'car', 'socialSecurity', 'housingFund']
|
||||
const keys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode', 'monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'creditLoanCount', 'creditCardCount', 'operatingMonths', 'maritalStatus', 'house', 'car', 'socialSecurity', 'housingFund']
|
||||
const keys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode', 'monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'creditLoanCount', 'creditCardCount', 'operatingMonths', 'maritalStatus', 'house', 'car', 'socialSecurity', 'housingFund', 'socialSecurityBase', 'housingFundBase']
|
||||
keys.forEach((key: string) => {
|
||||
const raw = form[key]
|
||||
let value = raw == null ? '' : (typeof raw == 'number' ? (raw as number).toString() : raw as string)
|
||||
if (value.length > 0 && money.includes(key)) value = yuanToWan(value)
|
||||
if (['socialSecurity', 'housingFund'].includes(key)) value = contributionLabel(value)
|
||||
if (enums.includes(key) && labels[value] != null) value = labels[value] as string
|
||||
payload[key] = value
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user