fix: 修复匹配详情提示及公积金可选路径问题
修复匹配详情中“用户自填未经核验”等来源说明,清理未参与项的橙色注意提示,修正贷款统计口径文案;补充公积金完整可选路径提取逻辑,确保明确无额外时长要求的公积金分支可正确识别并通过。
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<view v-if="processing" class="analysis-pulse"><view class="pulse-dot" :style="pulseDotStyle()"></view><text class="stage-label">{{ stageLabel }}</text><text class="pulse-dots" :style="pulseDotsStyle()">···</text></view>
|
||||
<text v-if="error.length > 0" class="status-message">{{ error }}</text>
|
||||
<text v-if="canRetry" class="view-now" @click="retry">{{ retrying ? '正在提交…' : '重新分析' }}</text>
|
||||
<text v-if="succeeded" class="view-now" @click="report">查看分析报告</text>
|
||||
<text v-if="succeeded" class="view-now" @click="report">查看匹配结果</text>
|
||||
<text class="view-now" @click="refreshStatus">{{ retrying ? '正在提交重试…' : '刷新状态' }}</text>
|
||||
<text class="view-now" @click="archive">查看已提交资料</text>
|
||||
</view>
|
||||
@@ -18,9 +18,11 @@
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { fetchAssessmentStatus, retryAssessment } from '@/packages/finance/services/records.uts'
|
||||
import { reportRoute } from '@/services/report-route.uts'
|
||||
const { statusStyle, pageStyle } = usePageLayout()
|
||||
const title = ref('正在读取评估状态')
|
||||
const message = ref(''); const error = ref(''); const assessmentId = ref('')
|
||||
const publishedRevisionId = ref('')
|
||||
const canRetry = ref(false); const succeeded = ref(false); const retrying = ref(false); const processing = ref(false); const stageLabel = ref('正在准备分析')
|
||||
let retryKey = ''; let retryTask = ''; let retryOwner = ''
|
||||
let timer = 0; let motionTimer = 0; let visible = false; let generation = 0
|
||||
@@ -57,7 +59,14 @@
|
||||
stageLabel.value = result.stage == 'queued' ? '正在排队,分析任务即将开始' : '正在整理资料,生成分析报告'
|
||||
if (retryTask != result.taskId || retryOwner != owner) { retryKey = ''; retryTask = result.taskId; retryOwner = owner }
|
||||
title.value = result.statusLabel
|
||||
message.value = result.message.length > 0 ? result.message : (result.status == 'processing' ? (result.stage == 'queued' ? '资料已保存,等待分析。您可以稍后从档案查看。' : '正在分析自填资料,您可以稍后从档案查看。') : '分析已完成,可查看本次报告。')
|
||||
message.value = result.message.length > 0 ? result.message : (result.status == 'processing' ? (result.stage == 'queued' ? '资料已保存,等待分析。您可以稍后从档案查看。' : '正在分析自填资料,您可以稍后从档案查看。') : (result.status == 'failed' ? '分析未完成,可重新分析或刷新状态。已提交的资料无需重新填写。' : '分析已完成,可查看本次报告。'))
|
||||
if (result.status == 'succeeded') {
|
||||
// 只打开本次已发布版本;重评处理中或旧报告回包不会触发跳转。
|
||||
const target = result.hasReport && result.publishedRevisionId == result.revisionId ? reportRoute(assessmentId.value, 'matches', result.publishedRevisionId) : ''
|
||||
succeeded.value = target.length > 0
|
||||
if (target.length > 0) { publishedRevisionId.value = result.publishedRevisionId; uni.redirectTo({ url: target }); return }
|
||||
message.value = '本次报告尚未就绪,请刷新状态。'
|
||||
}
|
||||
// 只轮询服务端仍在处理的任务,不用计时器模拟完成。
|
||||
if (result.status == 'processing') timer = setTimeout(() => { loadStatus() }, 5000)
|
||||
} catch (err) {
|
||||
@@ -86,11 +95,11 @@
|
||||
} catch (err) { if (visible && request == generation && getSession()?.getString('token') == owner) error.value = err instanceof Error ? err.message : '重新分析失败,请重试' }
|
||||
finally { retrying.value = false }
|
||||
}
|
||||
const report = () => { uni.navigateTo({ url: '/packages/finance/report/report?assessmentId=' + encodeURIComponent(assessmentId.value) }) }
|
||||
const report = () => { const target = reportRoute(assessmentId.value, 'matches', publishedRevisionId.value); if (succeeded.value && target.length > 0) uni.redirectTo({ url: target }) }
|
||||
const archive = () => { uni.reLaunch({ url: '/packages/finance/archive/archive?assessmentId=' + encodeURIComponent(assessmentId.value) }) }
|
||||
onLoad((options) => { if (options != null) assessmentId.value = options['assessmentId'] ?? '' })
|
||||
onShow(() => { visible = true; loadStatus() })
|
||||
onHide(() => { visible = false; generation++; clearTimeout(timer); stopMotion(); canRetry.value = false; succeeded.value = false; processing.value = false; message.value = ''; error.value = '' })
|
||||
onHide(() => { visible = false; generation++; clearTimeout(timer); stopMotion(); canRetry.value = false; succeeded.value = false; processing.value = false; publishedRevisionId.value = ''; message.value = ''; error.value = '' })
|
||||
onUnload(() => { visible = false; generation++; clearTimeout(timer); stopMotion() })
|
||||
</script>
|
||||
|
||||
|
||||
@@ -72,16 +72,17 @@
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
|
||||
import { AssessmentRecord, AssessmentDetail, ArchiveSection, fetchAssessments, fetchAssessmentDetail, fetchArchiveCompleteness, fetchAssessmentRevisions } from '@/packages/finance/services/records.uts'
|
||||
import { AssessmentRecord, AssessmentDetail, ArchiveSection, ArchiveCompleteness, archiveSections, fetchAssessments, fetchAssessmentDetail, fetchArchiveCompleteness, fetchAssessmentRevisions } from '@/packages/finance/services/records.uts'
|
||||
const { pageStyle, contentStyle } = usePageLayout()
|
||||
const type = ref('personal'); const selectedId = ref(''); const records = ref<AssessmentRecord[]>([])
|
||||
const revisionId = ref(''); const versions = ref<AssessmentRecord[]>([]); const versionsMore = ref(false); const versionsError = ref(''); const versionsLoading = ref(false); let versionsCursor = ''
|
||||
const detail = ref<AssessmentDetail | null>(null); const expanded = ref(''); const historyOpen = ref(false)
|
||||
const loading = ref(false); const error = ref(''); const listError = ref(''); const listLoading = ref(false); const hasMore = ref(false)
|
||||
const completeness = ref<number | null>(null); const completenessFailed = ref(false)
|
||||
const archiveCompleteness = ref<ArchiveCompleteness | null>(null)
|
||||
const completenessMessage = ref('提交评估后,查看对应报告的资料完整度。')
|
||||
let cursor = ''; let generation = 0; let reportRequest = 0; let visible = false; let routeError = ''; let routeId = ''; let owner = ''; let accountTimer = 0
|
||||
const sections = computed((): ArchiveSection[] => detail.value != null ? detail.value!.sections : [
|
||||
const sections = computed((): ArchiveSection[] => detail.value != null ? archiveSections(detail.value!, archiveCompleteness.value) : [
|
||||
{ id: 'basic', icon: 'file-text', title: '基本信息', note: type.value == 'enterprise' ? '企业与联系人信息' : '身份与联系方式', fields: [], provided: 0 },
|
||||
{ id: 'income', icon: 'income', title: type.value == 'enterprise' ? '经营信息' : '职业与收入', note: type.value == 'enterprise' ? '经营行业、月数与营收' : '职业、收入与缴纳情况', fields: [], provided: 0 },
|
||||
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
|
||||
@@ -94,7 +95,7 @@
|
||||
}
|
||||
const clearView = () => {
|
||||
detail.value = null; records.value = []; versions.value = []; versionsMore.value = false; versionsError.value = ''; versionsCursor = ''; versionsLoading.value = false; cursor = ''; hasMore.value = false; expanded.value = ''
|
||||
completeness.value = null; completenessFailed.value = false; reportRequest++
|
||||
completeness.value = null; archiveCompleteness.value = null; completenessFailed.value = false; reportRequest++
|
||||
completenessMessage.value = '提交评估后,查看对应报告的资料完整度。'
|
||||
error.value = ''; listError.value = ''; listLoading.value = false
|
||||
}
|
||||
@@ -120,13 +121,17 @@
|
||||
return false
|
||||
}
|
||||
const readCompleteness = async (id: string, request: number, token: string) => {
|
||||
if (detail.value == null || detail.value!.record.id != id) return
|
||||
const expectedRevision = detail.value!.record.revisionId
|
||||
const reportSequence = ++reportRequest
|
||||
completeness.value = null; completenessFailed.value = false; completenessMessage.value = '正在读取本次报告的资料完整度…'
|
||||
completeness.value = null; archiveCompleteness.value = null; completenessFailed.value = false; completenessMessage.value = '正在读取本次报告的资料完整度…'
|
||||
try {
|
||||
const result = await fetchArchiveCompleteness(id, detail.value?.record.revisionId ?? revisionId.value)
|
||||
if (active(request, token) && reportSequence == reportRequest) { completeness.value = result.percent; completenessMessage.value = result.description }
|
||||
const result = await fetchArchiveCompleteness(id, expectedRevision)
|
||||
if (active(request, token) && reportSequence == reportRequest && detail.value?.record.id == id && detail.value?.record.revisionId == expectedRevision) {
|
||||
archiveCompleteness.value = result; completeness.value = result.percent; completenessMessage.value = result.description
|
||||
}
|
||||
} catch (err) {
|
||||
if (active(request, token) && reportSequence == reportRequest) {
|
||||
if (active(request, token) && reportSequence == reportRequest && detail.value?.record.id == id && detail.value?.record.revisionId == expectedRevision) {
|
||||
completenessFailed.value = true; completenessMessage.value = '完整度暂时读取失败,已提交资料仍可查看。'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
<view class="form-row"><text class="field-label">{{ assessmentType == 'enterprise' ? '联系人' : '姓名' }}</text><input v-model="form.name" class="field-input" placeholder="请输入姓名" /></view>
|
||||
<view v-if="assessmentType == 'personal'" class="form-row"><text class="field-label">身份证号</text><input v-model="form.idCard" class="field-input" maxlength="18" placeholder="请输入身份证号" /></view>
|
||||
<view class="form-row"><text class="field-label">手机号</text><input v-model="form.phone" class="field-input" type="number" maxlength="11" placeholder="请输入手机号" /></view>
|
||||
<AssessmentSelect label="所在城市" :modelValue="form.city" @update:modelValue="cityChanged" :options="['深圳市', '广州市', '北京市', '上海市', '杭州市', '其他城市']" />
|
||||
<view v-if="form.city == '其他城市' || customCity.length > 0" class="form-row"><text class="field-label">城市名称</text><input v-model="customCity" class="field-input" placeholder="请输入所在城市" /></view>
|
||||
<AssessmentProvinceSelect v-model="form.city" />
|
||||
<AssessmentSelect v-if="assessmentType == 'personal'" label="婚姻状况" v-model="form.maritalStatus" :options="['未婚', '已婚', '离异', '丧偶']" />
|
||||
</view>
|
||||
<view v-else-if="step == 1" class="form-list">
|
||||
@@ -37,11 +36,14 @@
|
||||
</view>
|
||||
<view v-else-if="step == 2" class="form-list">
|
||||
<text class="asset-section-title">贷款信息</text>
|
||||
<view class="form-row"><text class="field-label">信用贷款笔数</text><input v-model="form.creditLoanCount" class="field-input" type="number" maxlength="4" placeholder="无则填 0" /><text class="field-unit">笔</text></view>
|
||||
<view class="form-row"><text class="field-label">贷款笔数</text><input v-model="form.creditLoanCount" class="field-input" type="number" maxlength="4" placeholder="无则填 0" /><text class="field-unit">笔</text></view>
|
||||
<text class="local-note">统计信用贷款、房贷、车贷等全部当前未结清贷款。无贷款填 0,下面两项金额可留空;已填写的金额会保留。</text>
|
||||
<view v-if="form.loanCountScope != 'all_loans' && form.creditLoanCount.length > 0"><text class="local-note">旧资料仅统计信用贷款,请按上述全部贷款口径检查并确认笔数;原笔数和金额均已保留。</text><text class="consent-link" @click="confirmLoanCountScope">已按全部贷款口径确认笔数</text></view>
|
||||
<view class="form-row"><text class="field-label">欠款总余额</text><input v-model="form.totalDebtBalance" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<view class="form-row"><text class="field-label">贷款月还款金额</text><input v-model="form.monthlyLoanPayment" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<text class="asset-section-title">信用卡信息</text>
|
||||
<view class="form-row"><text class="field-label">银行信用卡张数</text><input v-model="form.creditCardCount" class="field-input" type="number" maxlength="4" placeholder="无则填 0" /><text class="field-unit">张</text></view>
|
||||
<text class="local-note">无信用卡填 0,总额度和已使用总额度可留空;已填写的金额会保留。</text>
|
||||
<view class="form-row"><text class="field-label">总额度</text><input v-model="form.creditCardTotalLimit" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<view class="form-row"><text class="field-label">已使用总额度</text><input v-model="form.creditCardUsedLimit" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<text class="asset-section-title">房车信息</text>
|
||||
@@ -74,6 +76,8 @@
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
|
||||
import AssessmentProvinceSelect from '@/packages/finance/components/AssessmentProvinceSelect.uvue'
|
||||
import { provinceForCity, provinceCodeForName } from '@/services/cities.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'
|
||||
import { uploadDocument, downloadDocument, deleteDocument, releaseDocumentTemp, documentCategory, fetchOwnedDocuments } from '@/services/documents.uts'
|
||||
@@ -88,8 +92,6 @@
|
||||
const form = ref<AssessmentForm>(emptyAssessmentForm())
|
||||
let ownerToken = ''; let goingToConfirm = false; let goingToAgreement = false; let visible = false; let accountTimer = 0
|
||||
const sameOwner = (): boolean => { const session = getSession(); return session != null && session['token'] == ownerToken }
|
||||
const customCity = ref('')
|
||||
const cityChanged = (city: string) => { form.value.city = city; if (city != '其他城市') customCity.value = '' }
|
||||
const documentConsent = ref(false)
|
||||
const dataConsent = ref(false)
|
||||
const documents = ref<SelectedDocument[]>([])
|
||||
@@ -110,7 +112,7 @@
|
||||
const session = getSession()
|
||||
ownerToken = session?.getString('token') ?? ''
|
||||
editGeneration++; invalidateDocuments(); documentConsentId = ''; consentKey = ''; pendingAgreement = emptyAgreement(); documentMessage.value = ''; initialized = false; stopAccountTimer(); clearAssessmentFlow()
|
||||
form.value = emptyAssessmentForm(); documents.value = []; customCity.value = ''; step.value = 0
|
||||
form.value = emptyAssessmentForm(); documents.value = []; step.value = 0
|
||||
documentConsent.value = false; dataConsent.value = false; authorizations.value.forEach((item) => { item.selected = false })
|
||||
baseRevisionId = ''; editLoading.value = false; editError.value = '账户已变化,请返回重新填写。'
|
||||
if (session == null) uni.redirectTo({ url: '/packages/account/login/login' })
|
||||
@@ -126,13 +128,13 @@
|
||||
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, 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()
|
||||
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, loanCountScope: source.loanCountScope, 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 }
|
||||
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value, editId.value, baseRevisionId)
|
||||
}
|
||||
const confirmLoanCountScope = () => { if (!ensureOwner()) return; form.value.loanCountScope = 'all_loans'; rememberInput() }
|
||||
const showAgreement = (title: string) => { if (!ensureOwner()) return; rememberInput(); goingToAgreement = true; showAgreementNotice(title) }
|
||||
const restore = (type: string) => {
|
||||
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); customCity.value = ''; documents.value = []; documentConsent.value = false; dataConsent.value = false
|
||||
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); documents.value = []; documentConsent.value = false; dataConsent.value = false
|
||||
authorizations.value.forEach((item) => { item.selected = false })
|
||||
const saved = getAssessmentInput(type)
|
||||
if (saved != null) {
|
||||
@@ -141,7 +143,7 @@
|
||||
documentConsent.value = saved['documentConsent'] == true; dataConsent.value = saved['dataConsent'] == true
|
||||
const grants = saved['authorizations'] as string[] | null
|
||||
if (grants != null) authorizations.value.forEach((item) => { item.selected = grants.includes(item.key) })
|
||||
if (form.value.city.length > 0 && !['深圳市', '广州市', '北京市', '上海市', '杭州市', '其他城市'].includes(form.value.city)) { customCity.value = form.value.city; form.value.city = '其他城市' }
|
||||
if (form.value.city.length > 0) form.value.city = provinceForCity(form.value.city)
|
||||
}
|
||||
initialized = true
|
||||
rememberInput()
|
||||
@@ -221,7 +223,7 @@
|
||||
editGeneration++; if (editId.value.length > 0) editError.value = '已离开修改流程,请返回档案重新进入。'
|
||||
// 离页清空资料但不切换步骤,避免卸载时又挂载另一组原生输入组件。
|
||||
clearAssessmentFlow(); form.value = emptyAssessmentForm(); documents.value = []
|
||||
customCity.value = ''; documentConsent.value = false; dataConsent.value = false
|
||||
documentConsent.value = false; dataConsent.value = false
|
||||
authorizations.value.forEach((item) => { item.selected = false })
|
||||
}
|
||||
})
|
||||
@@ -358,7 +360,7 @@
|
||||
else if (form.value.name.trim().length < 2) message = '请输入正确姓名'
|
||||
else if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(form.value.idCard.trim())) message = '请输入正确身份证号'
|
||||
else if (!/^1[3-9][0-9]{9}$/.test(form.value.phone)) message = '请输入正确手机号'
|
||||
else if (form.value.city.length == 0 || (form.value.city == '其他城市' && customCity.value.trim().length == 0)) message = '请选择或填写所在城市'
|
||||
else if (provinceCodeForName(form.value.city) == null) message = '请选择所在省份'
|
||||
}
|
||||
if (step.value == 1) {
|
||||
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
{ id: 'score', label: '匹配度', height: 120 },
|
||||
{ id: 'amount', label: '额度范围', height: 120 }, { id: 'term', label: '期限范围', height: 120 }, { id: 'rate', label: '年化利率', height: 120 },
|
||||
{ id: 'objects', label: '适用对象', height: 120 }, { id: 'regions', label: '适用地区', height: 140 },
|
||||
{ id: 'positive', label: '推荐与参考理由', height: 360 }, { id: 'blocking', label: '不推荐原因', height: 300 }, { id: 'cautions', label: '注意与补充资料', height: 300 },
|
||||
{ id: 'positive', label: '推荐与参考理由', height: 360 }, { id: 'blocking', label: '不推荐原因', height: 300 }, { id: 'cautions', label: '注意事项', height: 300 },
|
||||
{ id: 'conditions', label: '准入条件', height: 300 }, { id: 'repayment', label: '还款方式', height: 120 },
|
||||
{ id: 'materials', label: '准备材料', height: 260 }, { id: 'channel', label: '办理渠道', height: 120 }, { id: 'source', label: '资料来源', height: 260 }
|
||||
]
|
||||
@@ -125,8 +125,8 @@
|
||||
if (key == 'score') return matchPercentage(m.state, m.score) || '—'
|
||||
if (key == 'availability') return m.availabilityLabel + ';不改变历史结论'
|
||||
if (key == 'positive') return compareReasonText(m.positive, '暂无已确认满足的条件')
|
||||
if (key == 'blocking') return compareReasonText(m.blocking, '未发现已知不符条件;仍以核验为准')
|
||||
if (key == 'cautions') return compareReasonText(m.cautions, '暂无待核实提示;不代表已获审批')
|
||||
if (key == 'blocking') return compareReasonText(m.blocking, '未发现已知不符条件;结果仅供参考')
|
||||
if (key == 'cautions') return compareReasonText(m.cautions, '暂无需要特别注意的已知条件;不代表审批结果')
|
||||
if (key == 'conditions') return p.conditions.length > 0 ? p.conditions.join('\n\n') : '未提供'
|
||||
if (key == 'amount') return hasProductValue(p.amount) ? p.amount : '—'
|
||||
if (key == 'term') return hasProductValue(p.term) ? p.term : '—'
|
||||
@@ -142,7 +142,7 @@
|
||||
const rowHasValue = (row: CompareRow): boolean => columns.value.some((column: CompareColumn): boolean => !usable(column) || hasProductValue(cell(column, row.id)))
|
||||
const overviewRows = computed((): CompareRow[] => overviewDefinitions.filter((row: CompareRow): boolean => !['score', 'amount', 'term', 'rate'].includes(row.id) || rowHasValue(row)))
|
||||
const rows = computed((): CompareRow[] => detailRows.filter((row: CompareRow): boolean => !['score', 'amount', 'term', 'rate', 'repayment', 'channel', 'source'].includes(row.id) || rowHasValue(row)))
|
||||
const orderingNote = computed((): string => columns.value.some((column: CompareColumn): boolean => usable(column) && column.data!.orderingVersion == 'product-score-descending-v1') ? '匹配列表按匹配度排序,此处按所选顺序对比。匹配度不代表获批概率。' : '按所选顺序对比,具体条件以机构核验为准。')
|
||||
const orderingNote = computed((): string => columns.value.some((column: CompareColumn): boolean => usable(column) && column.data!.orderingVersion == 'product-score-descending-v1') ? '同组产品按匹配度排序,此处按所选顺序对比。匹配度不代表获批概率。' : '按所选顺序对比,具体条件以机构核验为准。')
|
||||
const loadColumn = async (id: string) => {
|
||||
if (!current(generation, owner)) return
|
||||
const column = columns.value.find((c: CompareColumn): boolean => c.id == id)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<view class="province-row" @click="open"><text class="province-label">所在省份</text><text :class="isSelected ? 'province-value' : 'province-value province-placeholder'">{{ isSelected ? modelValue : '请选择省份' }}</text><AppIcon name="chevron-right" :size="28" /></view>
|
||||
<view v-if="opened" class="province-mask" @click="cancel">
|
||||
<view class="province-sheet" @click.stop="noop">
|
||||
<view class="province-toolbar"><text class="province-title">选择所在省份</text><text class="province-cancel" @click="cancel">取消</text></view>
|
||||
<scroll-view scroll-y class="province-list" :scroll-top="selectedTop">
|
||||
<view v-for="item in provinceOptions" :key="item.code" :id="'province-' + item.code" :class="item.name == modelValue ? 'province-option province-active' : 'province-option'" @click="choose(item.name)"><text class="province-option-name">{{ item.name }}</text><text v-if="item.name == modelValue" class="province-check">✓</text></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
import { provinceOptions, provinceCodeForName } from '@/services/cities.uts'
|
||||
const props = defineProps({ modelValue: { type: String, default: '' } })
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const opened = ref(false)
|
||||
const selectedTop = ref(0)
|
||||
const isSelected = computed((): boolean => provinceCodeForName(props.modelValue) != null)
|
||||
const noop = () => {}
|
||||
const open = () => {
|
||||
uni.hideKeyboard(); selectedTop.value = 0; opened.value = true
|
||||
const index = provinceOptions.findIndex((item): boolean => item.name == props.modelValue)
|
||||
nextTick(() => { selectedTop.value = Math.max(0, index - 2) * 94 * uni.getWindowInfo().windowWidth / 750 })
|
||||
}
|
||||
const cancel = () => { opened.value = false }
|
||||
const choose = (name: string) => { emit('update:modelValue', name); opened.value = false }
|
||||
</script>
|
||||
<style>
|
||||
.province-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||
.province-label { font-size: 28rpx; color: #29374e; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.province-value { flex: 1; min-width: 0; margin-right: 12rpx; text-align: right; color: #22324a; font-size: 27rpx; line-height: 38rpx; }
|
||||
.province-placeholder { color: #8995a7; }
|
||||
.province-mask { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1000; background-color: rgba(20, 32, 52, 0.4); justify-content: flex-end; }
|
||||
.province-sheet { background-color: #ffffff; border-top-left-radius: 24rpx; border-top-right-radius: 24rpx; padding-bottom: 48rpx; }
|
||||
.province-toolbar { height: 112rpx; flex-direction: row; align-items: center; justify-content: space-between; padding-left: 32rpx; padding-right: 32rpx; }
|
||||
.province-title { font-size: 30rpx; color: #22324a; font-weight: 600; }
|
||||
.province-cancel { font-size: 28rpx; color: #6e7e95; padding: 16rpx; }
|
||||
.province-list { height: 650rpx; }
|
||||
.province-option { height: 94rpx; padding-left: 40rpx; padding-right: 40rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||
.province-option-name { color: #22324a; font-size: 28rpx; }
|
||||
.province-active { background-color: #eef4ff; }
|
||||
.province-check { color: #2468ec; font-size: 30rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<view class="match-card" @click="emit('open', item.id)">
|
||||
<view class="match-head">
|
||||
<view :class="markClass(item.product.bank)"><image v-if="item.product.logo.length > 0 && !logoFailed" class="institution-logo" :src="item.product.logo" mode="aspectFit" @error="logoFailed = true" /><text v-else class="institution-letter">{{ item.product.bank.length > 0 ? item.product.bank.slice(0, 1) : item.product.name.slice(0, 1) }}</text></view>
|
||||
<view class="identity"><text v-if="item.product.bank.length > 0" class="institution-name">{{ item.product.bank }}</text><text :class="item.product.bank.length > 0 ? 'product-name' : 'institution-name'">{{ item.product.name }}</text></view>
|
||||
<view class="match-status"><text v-if="matchPercentage(item.state, item.score).length > 0" :class="item.state == 'eligible' ? 'percentage' : 'percentage reference-percentage'">匹配度 {{ matchPercentage(item.state, item.score) }}</text><text :class="item.state == 'eligible' ? 'state eligible' : (item.state == 'ineligible' ? 'state ineligible' : 'state unknown')">{{ item.stateLabel }}</text></view><text class="arrow">›</text>
|
||||
</view>
|
||||
<view class="product-tags"><text v-if="hasProductValue(item.product.rate)" class="product-tag">年化 {{ item.product.rate }}</text><text v-if="hasProductValue(item.product.term)" class="product-tag">{{ item.product.term }}</text><text v-if="hasProductValue(item.product.channel)" class="product-tag">{{ item.product.channel }}</text><text v-if="!hasProductValue(item.product.rate) && !hasProductValue(item.product.term) && !hasProductValue(item.product.channel)" class="product-tag">{{ item.product.objects }}</text></view>
|
||||
<text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }}</text>
|
||||
<view v-if="selectable" class="card-actions"><text class="detail-label">查看匹配依据</text><view class="compare-control" @click.stop="emit('select', item.id)"><text :class="selected ? 'selection selected-box' : 'selection'">{{ selected ? '✓' : '+' }}</text><text class="compare-label">{{ selected ? '已选对比' : '加入对比' }}</text></view></view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { MatchView, hasProductValue, matchPercentage } from '@/packages/finance/services/matches.uts'
|
||||
const props = defineProps({ item: { type: Object as PropType<MatchView>, required: true }, selectable: { type: Boolean, default: false }, selected: { type: Boolean, default: false } })
|
||||
const emit = defineEmits(['open', 'select'])
|
||||
const logoFailed = ref(false)
|
||||
watch((): string => props.item.product.logo, () => { logoFailed.value = false })
|
||||
const markClass = (bank: string): string => bank.includes('工商') || bank.includes('招商') || bank.includes('中国银行') ? 'institution-mark red' : (bank.includes('农业') || bank.includes('邮储') ? 'institution-mark green' : 'institution-mark blue')
|
||||
</script>
|
||||
<style>
|
||||
.match-card { padding: 28rpx 6rpx 24rpx; border-bottom: 1rpx solid #edf1f6; background-color: #fff; }
|
||||
.match-head { flex-direction: row; align-items: center; }
|
||||
.institution-mark { width: 72rpx; height: 72rpx; border-radius: 36rpx; align-items: center; justify-content: center; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.blue { background-color: #edf4ff; }.red { background-color: #fff0f1; }.green { background-color: #eaf8f2; }
|
||||
.institution-logo { width: 58rpx; height: 58rpx; }.institution-letter { font-size: 31rpx; color: #1763b9; font-weight: 700; }
|
||||
.red .institution-letter { color: #bd2e3d; }.green .institution-letter { color: #138a63; }
|
||||
.identity { flex: 1; min-width: 0; }.institution-name { font-size: 28rpx; line-height: 40rpx; font-weight: 600; color: #24354b; }
|
||||
.product-name { font-size: 24rpx; line-height: 36rpx; color: #77869a; margin-top: 3rpx; }
|
||||
.match-status { align-items: flex-end; margin-left: 12rpx; flex-shrink: 0; }.percentage { color: #15946c; font-size: 23rpx; line-height: 36rpx; font-weight: 600; }
|
||||
.reference-percentage { color: #547aa8; }
|
||||
.state { font-size: 20rpx; line-height: 30rpx; padding: 2rpx 10rpx; border-radius: 5rpx; margin-top: 3rpx; }
|
||||
.eligible { color: #19815d; background-color: #eaf8f2; }.ineligible { color: #b56849; background-color: #fff3ed; }.unknown { color: #947125; background-color: #fff8e8; }
|
||||
.arrow { color: #97a4b5; font-size: 36rpx; margin-left: 14rpx; }
|
||||
.product-tags { flex-direction: row; flex-wrap: wrap; margin-left: 90rpx; margin-top: 15rpx; }
|
||||
.product-tag { background-color: #f0f5fc; color: #547aa8; font-size: 20rpx; line-height: 30rpx; padding: 3rpx 9rpx; border-radius: 4rpx; margin-right: 8rpx; margin-bottom: 5rpx; }
|
||||
.availability { font-size: 21rpx; line-height: 34rpx; color: #9c783a; margin-top: 10rpx; }
|
||||
.card-actions { flex-direction: row; justify-content: space-between; align-items: center; margin-left: 90rpx; margin-top: 10rpx; }
|
||||
.detail-label { font-size: 20rpx; color: #8b9aae; }.compare-control { flex-direction: row; align-items: center; padding: 4rpx 0 4rpx 12rpx; }
|
||||
.selection { width: 28rpx; height: 28rpx; border: 1rpx solid #c3d4e9; border-radius: 6rpx; color: #527ba9; font-size: 22rpx; text-align: center; line-height: 26rpx; }.selected-box { background-color: #0868f7; border-color: #0868f7; color: #fff; }.compare-label { color: #527ba9; font-size: 21rpx; margin-left: 8rpx; }
|
||||
</style>
|
||||
@@ -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, contributionValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||
import { activeAssessmentType, getAssessmentInput, draftText, documentsFromInput, ownershipFromInput, formFromInput, assetValidationMessage, assetNotApplicableFields, 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 }
|
||||
@@ -45,7 +45,12 @@
|
||||
const mask = (value: string, left: number, right: number): string => { if (value.length <= left + right) return value; return value.substring(0, left) + '****' + value.substring(value.length - right) }
|
||||
const displayValue = (saved: UTSJSONObject, key: string): string => { const value = draftText(saved, key); return value.length > 0 ? value : '未填写' }
|
||||
const appendRow = (rows: InfoRow[], label: string, value: string) => { const row: InfoRow = { label, value }; rows.push(row) }
|
||||
const valueWithUnit = (saved: UTSJSONObject, key: string, unit: string): string => { const value = draftText(saved, key); return value.length > 0 ? value + ' ' + unit : '未填写' }
|
||||
const valueWithUnit = (saved: UTSJSONObject, key: string, unit: string): string => {
|
||||
const value = draftText(saved, key)
|
||||
if (value.length > 0) return value + ' ' + unit
|
||||
if (assetNotApplicableFields(formFromInput(saved)).includes(key)) return ['totalDebtBalance', 'monthlyLoanPayment'].includes(key) ? '不适用(无贷款)' : '不适用(无信用卡)'
|
||||
return '未填写'
|
||||
}
|
||||
const loadInput = () => {
|
||||
assessmentType.value = activeAssessmentType()
|
||||
const saved = getAssessmentInput(assessmentType.value)
|
||||
@@ -59,7 +64,7 @@
|
||||
}
|
||||
appendRow(basicRows, enterprise ? '联系人' : '姓名', displayValue(saved, 'name'))
|
||||
appendRow(basicRows, '手机号', mask(displayValue(saved, 'phone'), 3, 4))
|
||||
appendRow(basicRows, '所在城市', displayValue(saved, 'city'))
|
||||
appendRow(basicRows, '所在省份', displayValue(saved, 'city'))
|
||||
if (!enterprise) {
|
||||
appendRow(basicRows, '身份证号', mask(displayValue(saved, 'idCard'), 4, 4))
|
||||
appendRow(basicRows, '婚姻状况', displayValue(saved, 'maritalStatus'))
|
||||
@@ -78,7 +83,8 @@
|
||||
{ title: '基本信息', step: 0, rows: basicRows, expanded: false },
|
||||
{ title: enterprise ? '经营信息' : '职业与收入', step: 1, rows: incomeRows, expanded: false },
|
||||
{ title: '资产与负债', step: 2, rows: [
|
||||
{ label: '信用贷款笔数', value: valueWithUnit(saved, 'creditLoanCount', '笔') },
|
||||
{ label: draftText(saved, 'loanCountScope') == 'all_loans' ? '贷款笔数' : '信用贷款笔数(旧口径)', value: valueWithUnit(saved, 'creditLoanCount', '笔') },
|
||||
{ label: '贷款统计口径', value: draftText(saved, 'loanCountScope') == 'all_loans' ? '信用贷款、房贷、车贷等全部当前未结清贷款' : '旧资料仅统计信用贷款,请返回资产页重新确认' },
|
||||
{ label: '欠款总余额', value: valueWithUnit(saved, 'totalDebtBalance', '万元') },
|
||||
{ label: '贷款月还款金额', value: valueWithUnit(saved, 'monthlyLoanPayment', '万元') },
|
||||
{ label: '银行信用卡张数', value: valueWithUnit(saved, 'creditCardCount', '张') },
|
||||
|
||||
@@ -12,16 +12,11 @@
|
||||
<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="bankMarkClass(item.product.bank)"><image v-if="item.product.logo.length > 0" class="mark-logo" :src="item.product.logo" mode="aspectFit" @error="item.product.logo = ''" /><text v-else class="mark-text">{{ item.product.bank.length > 0 ? item.product.bank.slice(0, 1) : item.product.name.slice(0, 1) }}</text></view><view class="identity"><text v-if="item.product.bank.length > 0" class="bank-name">{{ item.product.bank }}</text><text class="product-name">{{ item.product.name }}</text></view><view class="card-status"><text :class="item.state == 'eligible' ? 'match-state good' : (item.state == 'ineligible' ? 'match-state blocked' : 'match-state pending')">{{ item.stateLabel }}</text><text v-if="matchPercentage(item.state, item.score).length > 0" class="match-score">匹配度 {{ matchPercentage(item.state, item.score) }}</text></view><text class="card-arrow">›</text></view>
|
||||
<view class="tags"><text v-if="hasProductValue(item.product.rate)" class="tag">年化 {{ item.product.rate }}</text><text v-if="hasProductValue(item.product.term)" 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="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>
|
||||
<view v-if="!loading && error.length == 0 && matches.length == 0" class="state-panel"><text class="state-title">{{ status == 'unavailable' ? '本次未执行匹配' : (group == 'all' ? '本次暂无可展示的产品' : (group == 'eligible' ? '暂无符合已知条件的产品' : (group == 'ineligible' ? '暂无条件不符的产品' : '暂无资料不足或待确认的产品'))) }}</text><text class="muted">{{ status == 'unavailable' ? '历史结果不会自动补算。可从融资档案发起新的评估。' : '可切换分组查看其他结果,或返回报告了解资料准备情况。' }}</text><text class="link" @click="report">查看本次报告</text></view>
|
||||
<MatchProductCard v-for="item in matches" :key="item.id" :item="item" :selectable="true" :selected="selectedIds.includes(item.id)" @open="openProduct" @select="toggleCompare" />
|
||||
<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="matches.length > 0 && !hasMore" class="end-note">{{ group == 'all' ? '已显示全部匹配产品' : '已显示本组全部结果' }}</text>
|
||||
<text v-if="message.length > 0" class="muted notice">{{ message }}</text>
|
||||
<text class="link" @click="report">返回本次报告</text>
|
||||
</template>
|
||||
@@ -31,29 +26,30 @@
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import MatchProductCard from '@/packages/finance/components/MatchProductCard.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { MatchView, MatchPage, fetchMatchPage, validRecordId, hasProductValue, matchPercentage } 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 }
|
||||
const groups: MatchGroup[] = [{ id: 'eligible', label: '符合条件' }, { id: 'ineligible', label: '条件不符' }, { id: 'unknown', label: '待确认' }]
|
||||
const revisionId = ref(''); const assessmentId = ref(''); const group = ref('eligible'); const matches = ref<MatchView[]>([])
|
||||
const groups: MatchGroup[] = [{ id: 'all', label: '全部' }, { id: 'eligible', label: '符合条件' }, { id: 'unknown', label: '待核实' }, { id: 'ineligible', label: '条件不符' }]
|
||||
const revisionId = ref(''); const assessmentId = ref(''); const group = ref('all'); const matches = ref<MatchView[]>([])
|
||||
const selectedIds = ref<string[]>([])
|
||||
const pageActive = ref(false)
|
||||
const loading = ref(false); const error = ref(''); const pageError = ref(''); const message = ref(''); const status = ref('')
|
||||
const eligible = ref('—'); const ineligible = ref('—'); const unknown = ref('—'); const hasMore = ref(false)
|
||||
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 count = (id: string): string => id == 'all' ? (eligible.value == '—' ? '—' : (Number(eligible.value) + Number(unknown.value) + Number(ineligible.value)).toString()) : (id == 'eligible' ? eligible.value : (id == 'ineligible' ? ineligible.value : unknown.value))
|
||||
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 + '项符合已知条件的产品'
|
||||
if (group.value == 'all') return matches.value.length > 0 ? '为你展示' + matches.value.length.toString() + '款融资产品' : '本次产品匹配结果'
|
||||
return '本次' + count(group.value) + '款' + (group.value == 'eligible' ? '符合条件' : (group.value == 'unknown' ? '待核实' : '条件不符')) + '产品'
|
||||
})
|
||||
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 }) }
|
||||
@@ -116,8 +112,8 @@
|
||||
revisionId.value = options?.['revisionId'] ?? '';
|
||||
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError = '版本编号无效,请返回重新选择。'
|
||||
assessmentId.value = options?.['assessmentId'] ?? ''
|
||||
const selected = options?.['group'] ?? 'eligible'
|
||||
if (['eligible','ineligible','unknown'].includes(selected)) group.value = selected
|
||||
const selected = options?.['group'] ?? 'all'
|
||||
if (['all','eligible','ineligible','unknown'].includes(selected)) group.value = selected
|
||||
else routeError = '匹配分组参数无效,请从报告重新进入。'
|
||||
if (assessmentId.value.length > 0 && !validRecordId(assessmentId.value)) routeError = '评估编号无效,请从融资档案重新选择。'
|
||||
if (options?.['productIds'] != null) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<view class="bank-header">
|
||||
<view class="bank-mark"><image class="bank-icon" :src="product!.logo.length > 0 ? product!.logo : '/static/mobile/icons-generated/building.png'" @error="product!.logo = ''" mode="aspectFit" /></view>
|
||||
<view class="bank-info">
|
||||
<view class="bank-title-row"><text v-if="product!.bank.length > 0" class="bank-name">{{ product!.bank }}</text><text v-if="match != null" :class="match!.state == 'eligible' ? 'badge good' : (match!.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ match!.state == 'eligible' ? (match!.score != null ? '匹配度 ' + match!.score!.toString() + '%' : '可供参考') : (match!.state == 'ineligible' ? '暂不推荐' : match!.stateLabel) }}</text></view>
|
||||
<view class="bank-title-row"><text v-if="product!.bank.length > 0" class="bank-name">{{ product!.bank }}</text><text v-if="match != null" :class="match!.state == 'eligible' ? 'badge good' : (match!.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ matchPercentage(match!.state, match!.score).length > 0 ? '匹配度 ' + matchPercentage(match!.state, match!.score) : (match!.state == 'eligible' ? '可供参考' : (match!.state == 'ineligible' ? '暂不推荐' : match!.stateLabel)) }}</text></view>
|
||||
<text v-if="match != null" :class="match!.state == 'eligible' ? 'state good-text' : 'state attention-text'">{{ match!.stateLabel }}</text>
|
||||
<text class="product-name">{{ product!.name }}</text>
|
||||
</view>
|
||||
@@ -23,19 +23,19 @@
|
||||
<template v-if="tab == 'explanation' && match != null">
|
||||
<view class="reason-section">
|
||||
<text class="heading good-text">{{ match!.state == 'eligible' ? '推荐理由(' + match!.positive.length + '条)' : '参考理由(' + match!.positive.length + '条)' }}</text>
|
||||
<text v-if="match!.positive.length == 0" class="empty-note">暂无已确认满足的条件,不补充未经验证的推荐理由。</text>
|
||||
<text v-if="match!.positive.length == 0" class="empty-note">暂无可展示的推荐理由。</text>
|
||||
<view v-for="(reason, index) in positiveVisible" :key="reason.id" class="reason-row">
|
||||
<text class="number green-number">{{ index + 1 }}</text>
|
||||
<view class="reason-content"><text class="reason-title">{{ reasonTitle(reason) }}</text><text class="reason-detail">{{ reason.explanation.length > 0 ? reason.explanation : reason.evidence.join(';') }}</text><text class="evidence-toggle" @click="openReason = openReason == reason.id ? '' : reason.id">{{ openReason == reason.id ? '收起依据' : '查看依据' }}</text><view v-if="openReason == reason.id" class="evidence-box"><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view></view>
|
||||
<view class="reason-content"><text class="reason-title">{{ reasonTitle(reason) }}</text><text v-if="reason.explanation.length > 0" class="reason-detail">{{ reason.explanation }}</text><text class="evidence-toggle" @click="openReason = openReason == reason.id ? '' : reason.id">{{ openReason == reason.id ? '收起依据' : '查看依据' }}</text><view v-if="openReason == reason.id" class="evidence-box"><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view></view>
|
||||
</view>
|
||||
<text v-if="match!.positive.length > 3" class="expand" @click="expanded = !expanded">{{ expanded ? '收起' : '展开其余' + (match!.positive.length - 3) + '条依据' }}</text>
|
||||
</view>
|
||||
<view class="reason-section attention-section">
|
||||
<text class="heading attention-text">不推荐/需注意的点({{ attention.length }}条)</text>
|
||||
<text v-if="attention.length == 0" class="empty-note">已知规则中暂无不符或待核实项,仍需核验资料,不代表审批结果。</text>
|
||||
<text v-if="attention.length == 0" class="empty-note">当前没有需要特别注意的已知条件。结果仅供参考,不代表审批结果。</text>
|
||||
<view v-for="(reason, index) in attentionVisible" :key="reason.id" class="reason-row">
|
||||
<text class="number orange-number">{{ index + 1 }}</text>
|
||||
<view class="reason-content"><text class="reason-title">{{ reasonTitle(reason) }}</text><text class="reason-detail">{{ reason.explanation.length > 0 ? reason.explanation : reason.evidence.join(';') }}</text><text class="evidence-toggle" @click="openReason = openReason == reason.id ? '' : reason.id">{{ openReason == reason.id ? '收起依据' : '查看依据' }}</text><view v-if="openReason == reason.id" class="evidence-box"><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view></view>
|
||||
<view class="reason-content"><text class="reason-title">{{ reasonTitle(reason) }}</text><text v-if="reason.explanation.length > 0" class="reason-detail">{{ reason.explanation }}</text><text class="evidence-toggle" @click="openReason = openReason == reason.id ? '' : reason.id">{{ openReason == reason.id ? '收起依据' : '查看依据' }}</text><view v-if="openReason == reason.id" class="evidence-box"><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view></view>
|
||||
</view>
|
||||
<text v-if="attention.length > 3" class="expand" @click="attentionExpanded = !attentionExpanded">{{ attentionExpanded ? '收起' : '展开其余' + (attention.length - 3) + '条注意事项' }}</text>
|
||||
</view>
|
||||
@@ -69,7 +69,7 @@
|
||||
<view class="details-section"><text class="heading">产品准入条件</text><text class="muted">公开条件不代表个人已符合,缺少核验依据时仍需核实。</text><text v-for="(condition, index) in product!.conditions" :key="index" class="condition">{{ condition }}</text></view>
|
||||
<view class="details-section"><text class="heading">资料准备说明</text><view v-for="(material, index) in product!.materialList" :key="index" class="material-item"><text class="condition">{{ material.name }} · {{ material.required ? '必需' : '补充' }} · {{ material.objects }}</text><text v-if="material.note.length > 0" class="evidence">{{ material.note }}</text></view><text v-if="product!.materialList.length == 0 || product!.materials != '未提供'" class="body">{{ product!.materials }}</text><text class="muted">仅提供材料引导,不向机构提交申请。实际条件和材料须向机构核实。</text></view>
|
||||
</template>
|
||||
<view class="footnotes"><text class="record-note">{{ assessmentId.length > 0 ? '本次评估 #' + assessmentId : '公共产品资料' }} · 产品版本 v{{ product!.version }}</text><text v-if="match != null" class="record-note">{{ match!.availabilityLabel }},历史参数与结论保留。</text><text v-if="match != null && !canPrepare" class="record-note">{{ match!.state != 'eligible' ? '请先核对不符条件或待核实项。' : '当前产品不可用或状态未知,暂不提供准备入口。' }}</text><text v-if="match != null && match!.score != null" class="record-note">匹配度表示条件匹配程度,不代表获批概率。</text><text class="record-note">自填信息未经核验;本页面不承诺额度、放款或获批。</text><text v-if="assessmentId.length > 0" class="link" @click="openCompare">查看同类产品对比</text><text v-if="assessmentId.length > 0" class="link" @click="toMatches">返回本次匹配</text><text v-else class="link" @click="history">选择评估查看匹配结果</text></view>
|
||||
<view class="footnotes"><text class="record-note">{{ assessmentId.length > 0 ? '本次评估 #' + assessmentId : '公共产品资料' }} · 产品版本 v{{ product!.version }}</text><text v-if="match != null" class="record-note">{{ match!.availabilityLabel }},历史参数与结论保留。</text><text v-if="match != null && !canPrepare" class="record-note">{{ match!.state != 'eligible' ? '请先核对不符条件或待核实项。' : '当前产品不可用或状态未知,暂不提供准备入口。' }}</text><text v-if="match != null && match!.score != null" class="record-note">匹配度表示条件匹配程度,不代表获批概率。</text><text class="record-note">结果仅供参考,不代表审批结果;本页面不承诺额度、放款或获批。</text><text v-if="assessmentId.length > 0" class="link" @click="openCompare">查看同类产品对比</text><text v-if="assessmentId.length > 0" class="link" @click="toMatches">返回本次匹配</text><text v-else class="link" @click="history">选择评估查看匹配结果</text></view>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -84,7 +84,7 @@
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { MatchView, MatchReason, MatchDetail, ProductView, fetchMatchDetailRecord, fetchPublicProduct, validRecordId, requestProductExplanation, hasProductValue } from '@/packages/finance/services/matches.uts'
|
||||
import { MatchView, MatchReason, MatchDetail, ProductView, fetchMatchDetailRecord, fetchPublicProduct, validRecordId, requestProductExplanation, hasProductValue, matchPercentage } 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()
|
||||
@@ -102,8 +102,8 @@
|
||||
const attentionVisible = computed((): MatchReason[] => attentionExpanded.value ? attention.value : attention.value.slice(0, 3))
|
||||
const aiPending = computed((): boolean => match.value != null && ['queued','running'].includes(match.value!.aiStatus))
|
||||
const canPrepare = computed((): boolean => match.value != null && match.value!.state == 'eligible' && match.value!.availability == 'available')
|
||||
// 缩短重复的规则模板标题;核验限制仍保留在逐条依据和页尾,不改判定含义。
|
||||
const reasonTitle = (reason: MatchReason): string => reason.text.replace('(资料尚需核验)。', '').replace('本产品已配置条件', '产品条件')
|
||||
// 缩短规则模板标题,不改判定含义。
|
||||
const reasonTitle = (reason: MatchReason): string => reason.text.replace('本产品已配置条件', '产品条件')
|
||||
function stopPoll(): void { if (timer != 0) clearTimeout(timer); timer = 0 }
|
||||
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 }) }
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<view v-if="report != null" class="header-export" :style="navigationActionStyle(96, 1)"><ReportExport :assessmentId="assessmentId" :revisionId="revisionId" :toolbar="true" @authrequired="login" @sessionchanged="loadReport" @statuschange="exportStatus" /></view>
|
||||
</view>
|
||||
<view class="tabs"><view v-for="item in tabs" :key="item.id" :class="tab == item.id ? 'tab active' : 'tab'" @click="switchTab(item.id)"><text :class="tab == item.id ? 'tab-label selected' : 'tab-label'">{{ item.label }}</text></view></view>
|
||||
<scroll-view scroll-y :style="contentStyle(report == null ? 178 : 314, false)">
|
||||
<scroll-view scroll-y :style="contentStyle(report == null || tab == 'matches' ? 178 : 314, false)">
|
||||
<view class="report-content" :style="bottomStyle(24)">
|
||||
<!-- 离页清空报告时只切换可见性,避免在页面销毁过程中重新挂载文本节点。 -->
|
||||
<text v-show="report == null" class="eyebrow">评估记录 #{{ assessmentId.length > 0 ? assessmentId : '待选择' }}</text>
|
||||
@@ -32,25 +32,22 @@
|
||||
<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 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 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 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>
|
||||
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '风险与核验边界' : '待补充信息' }}</text><text class="body-text" v-for="(item, index) in report!.warnings" :key="'system-' + index">{{ item }}</text><text class="body-text" v-for="(item, index) in report!.dataGaps" :key="index">{{ item }}</text><text v-if="report!.warnings.length == 0 && report!.dataGaps.length == 0" class="report-note">本次暂无额外提示,不代表资料已核验或无风险。</text></view>
|
||||
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '风险与核验边界' : '待补充信息' }}</text><text class="body-text" v-for="(item, index) in report!.warnings" :key="'system-' + index">{{ item }}</text><text class="body-text" v-for="(item, index) in report!.dataGaps" :key="index">{{ item }}</text><text v-if="report!.warnings.length == 0 && report!.dataGaps.length == 0" class="report-note">本次暂无额外提示,仍需关注实际风险和机构要求。</text></view>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="tab == 'matches'">
|
||||
<text class="headline">本次需求的可选方向</text><text class="intro">以下为评估时的条件判断。展示顺序不代表优劣,也不代表机构审批。</text>
|
||||
<view v-if="report!.matching.status == 'completed'" class="stats"><view class="stat" @click="allMatches('eligible')"><text class="stat-number">{{ report!.matching.eligible }}</text><text class="stat-label">符合已知条件</text></view><view class="stat" @click="allMatches('ineligible')"><text class="stat-number">{{ report!.matching.ineligible }}</text><text class="stat-label">条件不符</text></view><view class="stat" @click="allMatches('unknown')"><text class="stat-number">{{ report!.matching.unknown }}</text><text class="stat-label">待核实</text></view></view>
|
||||
<text class="report-note">{{ report!.matchingMessage }}</text>
|
||||
<view class="matching-summary"><text class="matching-title">{{ candidates.length > 0 ? '为你展示' + candidates.length.toString() + '款融资产品' : '本次产品匹配结果' }}</text><text class="matching-caption">根据本次资料匹配,进入产品查看具体条件与依据。</text></view>
|
||||
<view v-if="report!.matching.status != 'completed'" class="panel"><text class="section-title">本次未执行产品匹配</text><text class="body-text">这与“已匹配但没有候选”不同。历史报告不会自动补算;需要采用新资料与当前产品条件时,请重新评估。</text></view>
|
||||
<template v-else>
|
||||
<text v-if="candidateLoading" class="body-text">正在读取历史候选…</text>
|
||||
<view v-if="candidateError.length > 0" class="panel"><text class="section-title">候选读取失败</text><text class="body-text">{{ candidateError }}</text><button class="secondary" @click="loadCandidates">重试候选</button></view>
|
||||
<view v-if="!candidateLoading && candidateError.length == 0 && candidates.length == 0" class="panel"><text class="section-title">暂无符合已知条件的产品</text><text class="body-text">本次已执行匹配,可查看条件不符或待核实原因。没有候选不代表永久无法融资。</text></view>
|
||||
<view v-for="item in candidates" :key="item.id" class="product-card" @click="openProduct(item.id)"><view class="panel-heading"><text class="product-name">{{ item.product.name }}</text><text class="version">v{{ item.product.version }}</text></view><text class="report-note">{{ item.product.bank }} · {{ item.stateLabel }}</text><text class="amount">{{ item.product.amount }}</text><view class="metrics"><text class="metric">年化 {{ item.product.rate }}</text><text class="metric">{{ item.product.term }}</text></view><text class="reason" v-for="reason in item.positive.slice(0, 2)" :key="reason.id">{{ reason.text }}</text><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }};仍保留历史参数。</text><text class="action">查看匹配依据与详情 ›</text></view>
|
||||
<text v-if="candidates.length > 0" class="report-note">摘要最多展示3项符合已知条件的产品,全部分组见完整匹配列表。</text>
|
||||
<button class="primary" @click="allMatches('eligible')">查看全部匹配结果</button><view class="links"><text class="action" @click="allMatches('ineligible')">查看不符原因</text><text class="action" @click="allMatches('unknown')">查看待核实项</text></view>
|
||||
<text v-if="candidateLoading" class="body-text">正在读取匹配产品…</text>
|
||||
<view v-if="candidateError.length > 0" class="panel"><text class="section-title">匹配产品读取失败</text><text class="body-text">{{ candidateError }}</text><button class="secondary" @click="loadCandidates">重新加载</button></view>
|
||||
<view v-if="!candidateLoading && candidateError.length == 0 && candidates.length == 0" class="panel"><text class="section-title">本次暂无可展示的产品</text><text class="body-text">可重新评估,采用最新资料与产品条件进行匹配。</text></view>
|
||||
<MatchProductCard v-for="item in candidates" :key="item.id" :item="item" @open="openProduct" />
|
||||
<button v-if="candidates.length > 0" class="matching-more" @click="allMatches('all')">查看更多产品(共{{ matchingTotal }}款)</button>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -61,19 +58,23 @@
|
||||
<view v-if="report!.matching.status != 'completed'" class="panel"><text class="section-title">产品条件提示</text><text class="report-note">本次未执行产品匹配,没有可关联的产品改善依据。</text></view>
|
||||
<template v-else><view v-for="group in adviceGroups" :key="group.id" class="panel"><text class="section-title">{{ group.label }}</text><text class="report-note">仅展示本组前3项产品的已有依据,可进入完整列表查看更多。</text><text v-if="group.loading" class="body-text">正在读取产品依据…</text><template v-if="group.error.length > 0"><text class="body-text">{{ group.error }}</text><button class="secondary" @click="loadAdvice(group.id)">重试{{ group.label }}</button></template><text v-if="!group.loading && group.error.length == 0 && group.items.length == 0" class="report-note">本组暂无产品依据。</text><view v-for="item in group.items" :key="item.id" class="advice-product"><text class="product-name" @click="openProduct(item.id)">{{ item.product.name }} ›</text><text class="source-note">{{ item.stateLabel }} · 评估时版本 v{{ item.product.version }}</text><view v-for="reason in adviceReasons(item)" :key="reason.id"><text class="body-text">{{ reason.text }}</text><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }}</text></view><text class="action" @click="allMatches(group.id)">查看全部{{ group.label }} ›</text></view></template>
|
||||
</template>
|
||||
<view v-if="tab == 'matches'" class="matching-footer"><text class="report-link" @click="editAssessment">修改资料,重新评估 ›</text><text class="matching-disclaimer">匹配度仅供参考,不代表获批概率。</text></view>
|
||||
<template v-else>
|
||||
<view class="report-links"><text class="report-link" @click="nextStep">下一步行动 ›</text><text class="report-link" @click="consult">咨询AI ›</text></view>
|
||||
<view class="details-toggle" @click="metadataOpen = !metadataOpen"><text class="metadata-toggle">报告信息</text><text class="details-action">{{ metadataOpen ? '收起' : '展开' }}</text></view>
|
||||
<view v-if="metadataOpen" class="metadata"><text class="report-note">评估 #{{ assessmentId }} · 第 {{ report!.revisionNo }} 版 · {{ report!.generatedAt }}</text><text class="report-note">分析来源:用户自填资料 · {{ report!.modelCode }}</text><text class="report-note">报告已保存至融资档案。导出不含身份信息,请妥善保管下载文件。</text><text class="disclaimer">{{ report!.disclaimer }}</text><view class="report-links"><text class="report-link" @click="start">发起新评估</text><text class="report-link" @click="history">历史评估</text></view></view>
|
||||
<text v-show="!metadataOpen" class="disclaimer">自填资料未经核验,结果仅供参考,不代表授信或获批。</text>
|
||||
<view v-if="metadataOpen" class="metadata"><text class="report-note">评估 #{{ assessmentId }} · 第 {{ report!.revisionNo }} 版 · {{ report!.generatedAt }}</text><text class="report-note">报告已保存至融资档案。导出不含身份信息,请妥善保管下载文件。</text><text class="disclaimer">{{ report!.disclaimer }}</text><view class="report-links"><text class="report-link" @click="start">发起新评估</text><text class="report-link" @click="history">历史评估</text></view></view>
|
||||
<text v-show="!metadataOpen" class="disclaimer">本报告依据本次资料生成,结果仅供参考,不代表授信或获批。</text>
|
||||
</template>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-if="report != null" class="report-footer" :style="bottomStyle(24)"><button class="reevaluate-button" @click="editAssessment">重新评估</button></view>
|
||||
<view v-if="report != null && tab != 'matches'" class="report-footer" :style="bottomStyle(24)"><button class="reevaluate-button" @click="editAssessment">重新评估</button></view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
import ReportExport from '@/packages/finance/components/ReportExport.uvue'
|
||||
import MatchProductCard from '@/packages/finance/components/MatchProductCard.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
|
||||
@@ -100,6 +101,7 @@
|
||||
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[] => report.value == null ? [] : report.value!.priorityItems)
|
||||
const matchingTotal = computed((): string => report.value == null || report.value!.matching.status != 'completed' ? '0' : (Number(report.value!.matching.eligible) + Number(report.value!.matching.unknown) + Number(report.value!.matching.ineligible)).toString())
|
||||
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
|
||||
@@ -120,7 +122,7 @@
|
||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||
candidateLoading.value = true; candidateError.value = ''; candidates.value = []
|
||||
try {
|
||||
const result = await fetchMatchPage(assessmentId.value, 'eligible', '', revisionId.value)
|
||||
const result = await fetchMatchPage(assessmentId.value, 'all', '', revisionId.value)
|
||||
if (!current(request, token)) { sessionChanged(request, token); return }
|
||||
verifyMatches(result); candidates.value = result.items
|
||||
} catch (err) { if (!sessionChanged(request, token)) candidateError.value = err instanceof Error ? err.message : '候选读取失败' }
|
||||
@@ -205,6 +207,12 @@
|
||||
.revision-notice { font-size: 24rpx; line-height: 38rpx; color: #886934; padding: 18rpx 22rpx; background-color: #fff8eb; border-radius: 12rpx; margin-bottom: 12rpx; }
|
||||
.download-status { font-size: 23rpx; line-height: 36rpx; color: #547292; padding-bottom: 18rpx; }
|
||||
.report-content { padding: 28rpx 30rpx 24rpx; }
|
||||
.matching-summary { background-color: #edf5ff; padding: 25rpx 26rpx; border-radius: 10rpx; margin-bottom: 8rpx; }
|
||||
.matching-title { color: #244f85; font-size: 30rpx; line-height: 44rpx; font-weight: 600; }
|
||||
.matching-caption { color: #7188a4; font-size: 22rpx; line-height: 35rpx; margin-top: 8rpx; }
|
||||
.matching-more { background-color: #0868f7; color: #fff; height: 76rpx; line-height: 76rpx; font-size: 25rpx; border-radius: 10rpx; margin: 26rpx 0 0; }
|
||||
.matching-footer { align-items: center; padding-top: 22rpx; }
|
||||
.matching-disclaimer { font-size: 21rpx; line-height: 34rpx; color: #8c98a8; margin-top: 20rpx; text-align: center; }
|
||||
.eyebrow { color: #778ba3; font-size: 22rpx; }
|
||||
.headline { color: #1a3859; font-size: 34rpx; font-weight: 700; margin-top: 22rpx; }
|
||||
.intro { color: #73869d; font-size: 24rpx; line-height: 40rpx; margin-top: 14rpx; }
|
||||
|
||||
@@ -2,6 +2,7 @@ import { revisionParams } from './records.uts'
|
||||
import { requestJson } from '@/services/api.uts'
|
||||
import { API_BASE_URL } from '@/services/config.uts'
|
||||
import { yuanToWan } from '@/services/money.uts'
|
||||
import { provinceOptions } from '@/services/cities.uts'
|
||||
|
||||
export type ProductMaterial = { name: string, note: string, required: boolean, objects: string }
|
||||
export type ProductView = { logo: string, materialList: ProductMaterial[], consultPhone: string, repayment: string, channel: string, approval: string, id: string, version: number, name: string, bank: string, type: string, demo: boolean, amount: string, rate: string, term: string, objects: string, regions: string, valid: string, materials: string, source: string, sourceRef: string, checkedOn: string, conditions: string[] }
|
||||
@@ -12,16 +13,23 @@ export type MatchPage = { assessmentId: string, revisionId: string, resultVersio
|
||||
|
||||
function text(data: UTSJSONObject, key: string): string { return data.getString(key) ?? '' }
|
||||
export function validRecordId(value: string): boolean { return /^[1-9][0-9]{0,9}$/.test(value) }
|
||||
export function eligibilityLabel(value: string): string { return value == 'eligible' ? '符合已知条件' : (value == 'ineligible' ? '条件不符' : '待确认') }
|
||||
export function eligibilityLabel(value: string): string { return value == 'eligible' ? '符合已知条件' : (value == 'ineligible' ? '条件不符' : '待核实') }
|
||||
export function hasProductValue(value: string): boolean { return value.trim().length > 0 && !['未提供', '待核实', '—'].includes(value.trim()) }
|
||||
export function matchPercentage(state: string, score: number | null): string { return state == 'eligible' && score != null ? score.toString() + '%' : '' }
|
||||
const fieldNames: UTSJSONObject = { requestedAmountYuan: '融资金额', requestedTermMonths: '融资期限', applicantType: '申请主体', purposeCode: '资金用途', cityCode: '所在城市', monthlyIncome: '月收入', annualRevenue: '年营收', operatingMonths: '经营月数', totalDebtBalance: '贷款余额', monthlyLoanPayment: '每月还款额', creditLoanCount: '贷款笔数', creditCardCount: '信用卡数量', creditCardTotalLimit: '信用卡总额度', creditCardUsedLimit: '信用卡已用额度', socialSecurity: '社保情况', housingFund: '公积金情况', house: '房产情况', car: '车辆情况', establishedMonths: '企业成立月数', annualInvoiceAmount: '年开票金额', taxPaymentMonths: '连续纳税月数', taxpayerGrade: '纳税等级', enterpriseCreditStatus: '企业信用状态', annualRevenueGrowthPercent: '年营收增长率', debtRatioPercent: '负债率', cashFlowStability: '经营流水稳定性', industryCategory: '行业类别' }
|
||||
export function matchPercentage(state: string, score: number | null): string { return ['eligible','unknown','ineligible'].includes(state) && score != null ? score.toString() + '%' : '' }
|
||||
const fieldNames: UTSJSONObject = { age: '年龄', occupation: '职业类型', employer: '单位名称', requestedAmountYuan: '融资金额', requestedTermMonths: '融资期限', applicantType: '申请主体', purposeCode: '资金用途', cityCode: '所在城市', provinceCode: '所在省份', monthlyIncome: '月收入', annualRevenue: '年营收', operatingMonths: '经营月数', totalDebtBalance: '贷款余额', monthlyLoanPayment: '每月还款额', creditLoanCount: '贷款笔数', creditCardCount: '信用卡数量', creditCardTotalLimit: '信用卡总额度', creditCardUsedLimit: '信用卡已用额度', socialSecurity: '社保情况', housingFund: '公积金情况', house: '房产情况', car: '车辆情况', establishedMonths: '企业成立月数', annualInvoiceAmount: '年开票金额', taxPaymentMonths: '连续纳税月数', taxpayerGrade: '纳税等级', enterpriseCreditStatus: '企业信用状态', annualRevenueGrowthPercent: '年营收增长率', debtRatioPercent: '负债率', cashFlowStability: '经营流水稳定性', industryCategory: '行业类别' }
|
||||
const labels: UTSJSONObject = { personal: '个人', enterprise: '企业', daily_consumption: '日常消费', education: '教育进修', home_improvement: '家居装修', personal_business: '个体经营周转', working_capital: '经营周转', inventory_purchase: '采购备货', equipment_purchase: '设备购置', business_expansion: '经营扩展', other: '其他用途', none: '无', under_six_months: '不足六个月', six_months_plus: '六个月及以上', yes: '有', no: '无', normal: '正常', abnormal: '异常', stable: '稳定', unstable: '不稳定', manufacturing: '制造业', trade: '贸易', services: '服务业', construction: '建筑业', agriculture: '农业', finance: '金融业', real_estate: '房地产业', '440300': '深圳市', '440100': '广州市', '110100': '北京市', '310100': '上海市', '330100': '杭州市' }
|
||||
const moneyFields = ['requestedAmountYuan','monthlyIncome','annualRevenue','totalDebtBalance','monthlyLoanPayment','creditCardTotalLimit','creditCardUsedLimit','annualInvoiceAmount']
|
||||
const advisoryReasonCodes = ['AI_CONDITION_NOT_ASSESSED', 'PROFILE_DETAIL_NOT_ASSESSED']
|
||||
|
||||
function displayReasonText(value: string): string {
|
||||
return value.replace(/((?:根据自填资料判断|资料尚需核验|用户自填[,、]未经核验|自填信息未经核验|产品快照))/g, '').replace(/自填信息未经核验;本页面不承诺额度、放款或获批。/g, '结果仅供参考,不代表审批结果。').trim()
|
||||
}
|
||||
|
||||
function valueText(value: any | null, field: string): string {
|
||||
if (value == null) return '未提供'
|
||||
const raw = value.toString()
|
||||
if (field == 'provinceCode') return provinceOptions.find((item): boolean => item.code == raw)?.name ?? '待核实'
|
||||
if (field == 'age') return raw + ' 岁'
|
||||
if (moneyFields.includes(field)) return yuanToWan(raw) + ' 万元'
|
||||
if (field == 'requestedTermMonths' || field == 'operatingMonths' || field == 'establishedMonths' || field == 'taxPaymentMonths') return raw + ' 个月'
|
||||
if (field == 'debtRatioPercent' || field == 'annualRevenueGrowthPercent') return raw + '%'
|
||||
@@ -81,24 +89,37 @@ function matchView(data: UTSJSONObject): MatchView {
|
||||
const evidence = data['evidence'] as UTSJSONObject[]
|
||||
const ai = data['aiExplanation'] as UTSJSONObject
|
||||
const explanations = (ai['reasonExplanations'] as UTSJSONObject[] | null) ?? []
|
||||
const reasons = (key: string): MatchReason[] => (data[key] as UTSJSONObject[]).map((r: UTSJSONObject): MatchReason => {
|
||||
const hiddenAdvisory = (reason: UTSJSONObject): boolean => {
|
||||
const code = text(reason, 'code')
|
||||
if (!advisoryReasonCodes.includes(code)) return false
|
||||
const refs = (reason['evidenceRefs'] as string[] | null) ?? []
|
||||
return refs.length > 0 && refs.every((ref: string): boolean => {
|
||||
const item = evidence.find((entry: UTSJSONObject): boolean => text(entry, 'evidenceId') == ref)
|
||||
if (item == null || text(item, 'reasonCode') != code || text(item, 'outcome') != 'unknown') return false
|
||||
const source = text(item, 'source')
|
||||
return code == 'AI_CONDITION_NOT_ASSESSED' ? source == 'ai_self_reported' : ['self_reported', 'product_snapshot'].includes(source)
|
||||
})
|
||||
}
|
||||
// 仅隐藏证据一致的未参与判断提示;异常记录与原始快照保持可追溯。
|
||||
const reasons = (key: string): MatchReason[] => (data[key] as UTSJSONObject[]).filter((r: UTSJSONObject): boolean => key != 'cautions' || !hiddenAdvisory(r)).map((r: UTSJSONObject): MatchReason => {
|
||||
const reasonFields: string[] = []
|
||||
const reasonText = ['AI_CONDITION_PASS', 'AI_CONDITION_FAIL'].includes(text(r, 'code')) ? text(r, 'text').replace('(根据自填资料判断)', '') : text(r, 'text')
|
||||
const lines = (r['evidenceRefs'] as string[]).map((ref: string): string => {
|
||||
const reasonText = displayReasonText(text(r, 'text'))
|
||||
const lines = ((r['evidenceRefs'] as string[] | null) ?? []).map((ref: string): string => {
|
||||
const e = evidence.find((item: UTSJSONObject): boolean => text(item, 'evidenceId') == ref)
|
||||
if (e == null) return '证据暂不可读取'
|
||||
const fields = e['fieldRefs'] as string[]; const field = fields.length > 0 ? fields[0] : ''
|
||||
fields.forEach((name: string) => { if (!reasonFields.includes(name)) reasonFields.push(name) })
|
||||
if (text(e, 'source') == 'ai_self_reported') {
|
||||
if (e['actual'] != null && fieldNames.getString(field) != null) return fieldNames.getString(field)! + ':' + valueText(e['actual'], field)
|
||||
const names = fields.map((name: string): string => fieldNames.getString(name) ?? '').filter((name: string): boolean => name.length > 0)
|
||||
if (e['actual'] != null && (field == 'age' || fieldNames.getString(field) != null)) return (field == 'age' ? '年龄' : fieldNames.getString(field)!) + ':' + valueText(e['actual'], field)
|
||||
const names = fields.map((name: string): string => name == 'age' ? '年龄' : (fieldNames.getString(name) ?? '')).filter((name: string): boolean => name.length > 0)
|
||||
return names.length > 0 ? '关联资料:' + names.join('、') : reasonText
|
||||
}
|
||||
const source = text(e, 'source') == 'verified' ? '外部核验' : (text(e, 'source') == 'product_snapshot' ? '产品快照' : (text(e, 'source') == 'self_reported' ? '用户自填,未经核验' : '来源未知'))
|
||||
return (fieldNames.getString(field) ?? field) + ':' + valueText(e['actual'], field) + ' · ' + source
|
||||
const source = text(e, 'source')
|
||||
const sourceNote = ['self_reported', 'product_snapshot'].includes(source) ? '' : ' · ' + (source == 'verified' ? '外部核验' : '来源未知')
|
||||
return (fieldNames.getString(field) ?? field) + ':' + valueText(e['actual'], field) + sourceNote
|
||||
})
|
||||
const explanation = explanations.find((item: UTSJSONObject): boolean => text(item, 'reasonId') == text(r, 'reasonId'))
|
||||
return { id: text(r, 'reasonId'), text: reasonText, evidence: lines, fields: reasonFields, explanation: text(ai, 'status') == 'completed' && explanation != null ? text(explanation, 'text') : '' }
|
||||
return { id: text(r, 'reasonId'), text: reasonText, evidence: lines, fields: reasonFields, explanation: text(ai, 'status') == 'completed' && explanation != null ? displayReasonText(text(explanation, 'text')) : '' }
|
||||
})
|
||||
const availability = text(data, 'currentAvailability')
|
||||
const score = data.getNumber('score')
|
||||
@@ -108,7 +129,7 @@ function matchView(data: UTSJSONObject): MatchView {
|
||||
return { score, id: text(data, 'productId'), state: text(data, 'eligibility'), stateLabel: missingData ? '资料不足' : eligibilityLabel(text(data, 'eligibility')), availability,
|
||||
availabilityLabel: availability == 'available' ? '当前产品可查看' : (availability == 'unavailable' ? '当前产品已下架' : '当前产品状态暂无法确认'),
|
||||
product: productView(data['productSnapshot'] as UTSJSONObject, false), positive: reasons('positiveReasons'), blocking: reasons('blockingReasons'), cautions: reasons('cautions'),
|
||||
aiText: text(ai, 'status') == 'completed' ? text(ai, 'text') : (text(ai, 'status') == 'failed' ? 'AI解释暂不可用,已保留全部规则依据。' : '当前展示规则依据,尚未生成单产品AI解释。'),
|
||||
aiText: text(ai, 'status') == 'completed' ? displayReasonText(text(ai, 'text')) : (text(ai, 'status') == 'failed' ? 'AI解释暂不可用,已保留全部规则依据。' : '当前展示规则依据,尚未生成单产品AI解释。'),
|
||||
aiStatus: text(ai, 'status'), aiCanRetry: ai.getBoolean('canRetry') ?? false, aiCanRequest: data.getBoolean('canRequestExplanation') ?? false, aiGeneratedAt: text(ai, 'generatedAt') }
|
||||
}
|
||||
export async function requestProductExplanation(id: string, productId: string, requestId: string, revisionId: string = ''): Promise<MatchView> {
|
||||
@@ -116,6 +137,7 @@ export async function requestProductExplanation(id: string, productId: string, r
|
||||
return detailRecord(data, id, productId, revisionId).item
|
||||
}
|
||||
export async function fetchMatchPage(id: string, group: string, cursor: string, revisionId: string = ''): Promise<MatchPage> {
|
||||
if (!['all','eligible','unknown','ineligible'].includes(group)) throw new Error('匹配分组无效,请重新选择。')
|
||||
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches', 'GET', { ...revisionParams(revisionId), eligibility: group, limit: '3', cursor })
|
||||
const matching = data['matching'] as UTSJSONObject; const status = text(matching, 'status')
|
||||
const resolvedRevision = text(data, 'revisionId'); const version = data.getNumber('resultVersion') ?? 0
|
||||
@@ -123,7 +145,7 @@ export async function fetchMatchPage(id: string, group: string, cursor: string,
|
||||
const items = (data['items'] as UTSJSONObject[]).map((v: UTSJSONObject): MatchView => matchView(v))
|
||||
const hasMore = data.getBoolean('hasMore') ?? false; const nextCursor = text(data, 'nextCursor')
|
||||
const ids: string[] = []
|
||||
items.forEach((item: MatchView) => { if (item.state != group || ids.includes(item.id)) throw new Error('匹配分组或产品编号不一致,请重新加载。'); ids.push(item.id) })
|
||||
items.forEach((item: MatchView) => { if ((group != 'all' && item.state != group) || ids.includes(item.id)) throw new Error('匹配分组或产品编号不一致,请重新加载。'); ids.push(item.id) })
|
||||
if (!['completed','unavailable'].includes(status) || items.length > 3 || (hasMore && (items.length == 0 || nextCursor.length == 0)) || (!hasMore && nextCursor.length > 0) || (status == 'unavailable' && (items.length > 0 || hasMore))) throw new Error('匹配分页状态不完整,请重新加载。')
|
||||
if (status == 'completed') {
|
||||
if (text(matching, 'catalogSnapshotId').length == 0 || text(matching, 'ruleVersion').length == 0 || text(matching, 'orderingPolicyVersion').length == 0) throw new Error('匹配快照版本不完整,请重新加载。')
|
||||
@@ -132,7 +154,7 @@ export async function fetchMatchPage(id: string, group: string, cursor: string,
|
||||
}
|
||||
const reason = text(matching, 'reasonCode')
|
||||
const count = (key: string): string => { const v = matching.getNumber(key); return v == null ? '—' : v.toString() }
|
||||
const message = status == 'completed' ? (text(matching, 'orderingPolicyVersion') == 'product-score-descending-v1' ? '按本次条件匹配度排序,匹配度不代表获批概率。' : '按本次评估时的产品条件判断,展示顺序不代表优劣。') : (reason == 'LEGACY_INPUT' ? '旧版报告未执行产品匹配,可重新评估获取新的结果。' : (reason == 'MATCH_RULES_UNPUBLISHED' ? '本次评估尚未启用产品匹配规则。' : '本次评估没有可用产品目录,未执行产品匹配。'))
|
||||
const message = status == 'completed' ? (group == 'all' ? '优先展示符合条件的产品,其次为待核实、条件不符的产品。' : (text(matching, 'orderingPolicyVersion') == 'product-score-descending-v1' ? '按本次条件匹配度排序,匹配度不代表获批概率。' : '按本次评估时的产品条件判断,展示顺序不代表优劣。')) : (reason == 'LEGACY_INPUT' ? '旧版报告未执行产品匹配,可重新评估获取新的结果。' : (reason == 'MATCH_RULES_UNPUBLISHED' ? '本次评估尚未启用产品匹配规则。' : '本次评估没有可用产品目录,未执行产品匹配。'))
|
||||
return { assessmentId: id, revisionId: resolvedRevision, resultVersion: version, snapshotId: text(matching, 'catalogSnapshotId'), ruleVersion: text(matching, 'ruleVersion'), orderingVersion: text(matching, 'orderingPolicyVersion'), status, message, eligible: count('candidateCount'), ineligible: count('ineligibleCount'), unknown: count('unknownCount'), items, hasMore, cursor: nextCursor }
|
||||
}
|
||||
export async function fetchMatchDetail(id: string, productId: string, revisionId: string = ''): Promise<MatchView> {
|
||||
|
||||
@@ -3,27 +3,51 @@ 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 { provinceCodeForName } from '@/services/cities.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 }
|
||||
export type AssessmentField = { label: string, value: string }
|
||||
export type ArchiveSection = { id: string, icon: string, title: string, note: string, fields: AssessmentField[], provided: number }
|
||||
export type AssessmentDetail = { record: AssessmentRecord, fields: AssessmentField[], sections: ArchiveSection[] }
|
||||
export type ArchiveCompleteness = { percent: number | null, description: string }
|
||||
export type ArchiveField = { fieldKey: string, label: string, value: string, isMissing: boolean }
|
||||
export type ArchiveSection = { id: string, icon: string, title: string, note: string, fields: ArchiveField[], provided: number }
|
||||
export type AssessmentDetail = { record: AssessmentRecord, fields: ArchiveField[], sections: ArchiveSection[] }
|
||||
export type ArchiveCompleteness = { assessmentId: string, revisionId: string, percent: number | null, description: string, notApplicableFields: string[] }
|
||||
|
||||
const archiveAmountKeys = ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||
|
||||
// 档案只消费报告中已保存的完整度,不用当前表单规则重算旧评估,也不把缺失当作0%。
|
||||
export async function fetchArchiveCompleteness(id: string, revisionId: string = ''): Promise<ArchiveCompleteness> {
|
||||
const report = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
|
||||
if (report.getString('assessmentId') != id) throw new Error('报告与当前档案不一致')
|
||||
if (revisionId.length > 0 && report.getString('revisionId') != revisionId) throw new Error('报告版本不一致,请重新选择。')
|
||||
if (report.getNumber('resultVersion') == 1) return { percent: null, description: '这份旧版报告未计算完整度,已提交资料仍可查看。' }
|
||||
const result: ArchiveCompleteness = { assessmentId: id, revisionId: draftText(report, 'revisionId'), percent: null, description: '这份报告尚未评定资料完整度。', notApplicableFields: [] }
|
||||
if (report.getNumber('resultVersion') == 1) { result.description = '这份旧版报告未计算完整度,已提交资料仍可查看。'; return result }
|
||||
if (report.getNumber('resultVersion') != 2) throw new Error('报告格式暂不支持')
|
||||
const completeness = report['completeness'] as UTSJSONObject | null
|
||||
if (completeness == null || completeness.getString('status') != 'evaluated') return { percent: null, description: '这份报告尚未评定资料完整度。' }
|
||||
if (completeness == null || completeness.getString('status') != 'evaluated') return result
|
||||
const percent = completeness.getNumber('percent')
|
||||
if (percent == null || !Number.isInteger(percent) || percent < 0 || percent > 100) throw new Error('完整度数据暂不可读取')
|
||||
return { percent, description: '反映本次评估时的资料填写情况,不代表信用评分或审批结果。' }
|
||||
result.percent = percent; result.description = '反映本次评估时的资料填写情况,不代表信用评分或审批结果。'
|
||||
const items = completeness['items'] as UTSJSONObject[] | null
|
||||
if (items != null && Array.isArray(items)) archiveAmountKeys.forEach((key: string) => {
|
||||
const matching = items.filter((item: UTSJSONObject): boolean => item != null && item['fieldKey'] == key)
|
||||
if (matching.length == 1 && matching[0]['state'] == 'not_applicable') result.notApplicableFields.push(key)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// 只投影同版报告已经保存的不适用项,原档案与编辑回填继续保留原值。
|
||||
export function archiveSections(detail: AssessmentDetail, completeness: ArchiveCompleteness | null): ArchiveSection[] {
|
||||
if (completeness == null || completeness.assessmentId != detail.record.id
|
||||
|| completeness.revisionId.length == 0 || completeness.revisionId != detail.record.revisionId) return detail.sections
|
||||
return detail.sections.map((section: ArchiveSection): ArchiveSection => ({
|
||||
id: section.id, icon: section.icon, title: section.title, note: section.note, provided: section.provided,
|
||||
fields: section.fields.map((field: ArchiveField): ArchiveField => {
|
||||
if (!field.isMissing || !archiveAmountKeys.includes(field.fieldKey) || !completeness!.notApplicableFields.includes(field.fieldKey)) return field
|
||||
return { fieldKey: field.fieldKey, label: field.label, value: '不适用', isMissing: field.isMissing }
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function validAssessmentId(value: string): boolean { return /^[1-9][0-9]{0,9}$/.test(value) }
|
||||
@@ -112,6 +136,7 @@ export async function fetchAnalysisReport(id: string, revisionId: string = ''):
|
||||
let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : draftText(fact, 'value'))
|
||||
if (fact['value'] != null && fact['unit'] == 'yuan') value = yuanToWan(value) + ' 万元'
|
||||
if (fact['value'] != null && fact['unit'] == 'month') value += ' 个月'
|
||||
if (fact['value'] != null && fact['unit'] == 'age') 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)
|
||||
@@ -194,15 +219,18 @@ export async function createAssessment(payload: UTSJSONObject): Promise<string>
|
||||
if (value.length > 0 && !/^[0-9]{1,4}$/.test(value)) throw new Error('数量与经营月数须为0至9999的整数')
|
||||
form[key] = value.length == 0 ? null : parseInt(value)
|
||||
})
|
||||
if (draftText(values, 'loanCountScope') == 'all_loans') form['loanCountScope'] = 'all_loans'
|
||||
form['maritalStatus'] = enumValue(draftText(values, 'maritalStatus'), ['已婚', '未婚', '离异', '丧偶'], ['married', 'single', 'divorced', 'widowed'])
|
||||
const socialKeys = ['socialSecurity', 'housingFund']
|
||||
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 provinceCode = provinceCodeForName(draftText(values, 'city').trim())
|
||||
const cityNames = ['深圳市', '广州市', '北京市', '上海市', '杭州市']
|
||||
const cityCodes = ['440300', '440100', '110100', '310100', '330100']
|
||||
const cityIndex = cityNames.indexOf(draftText(values, 'city'))
|
||||
form['cityCode'] = cityIndex < 0 ? null : cityCodes[cityIndex]
|
||||
form['cityCode'] = provinceCode != null || cityIndex < 0 ? null : cityCodes[cityIndex]
|
||||
if (provinceCode != null) form['provinceCode'] = provinceCode
|
||||
// 卡片资料单独保存,确认页明示暂不绑定评估;只发送自填信息,绝不把本机路径或附件送入分析。
|
||||
const body: UTSJSONObject = { inputVersion: 3, type: values['type'], moneyUnit: 'yuan', form, documents: [] as UTSJSONObject[], consentIds: [] as string[] }
|
||||
if (editId.length > 0) body['baseRevisionId'] = baseRevision
|
||||
@@ -252,7 +280,7 @@ export async function fetchAssessmentDetail(id: string, revisionId: string = '')
|
||||
const record = recordFrom(result, id, revisionId)
|
||||
const form = result['form'] as UTSJSONObject | null
|
||||
if (form == null) throw new Error('评估详情不完整')
|
||||
const fields: AssessmentField[] = []
|
||||
const fields: ArchiveField[] = []
|
||||
const enterprise = result['type'] == 'enterprise'
|
||||
const sections: ArchiveSection[] = [
|
||||
{ id: 'basic', icon: 'file-text', title: '基本信息', note: enterprise ? '企业与联系人信息' : '身份与联系方式', fields: [], provided: 0 },
|
||||
@@ -260,14 +288,16 @@ export async function fetchAssessmentDetail(id: string, revisionId: string = '')
|
||||
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
|
||||
{ id: 'authorization', icon: 'authorization', title: '资料与授权', note: '私有资料与外部查询授权', fields: [], provided: 0 }
|
||||
]
|
||||
const addField = (section: number, label: string, value: string, provided: boolean) => {
|
||||
const field: AssessmentField = { label, value }
|
||||
const addField = (section: number, fieldKey: string, label: string, value: string, provided: boolean, isMissing: boolean) => {
|
||||
const field: ArchiveField = { fieldKey, label, value, isMissing }
|
||||
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', 'socialSecurityBase', 'housingFundBase']
|
||||
const labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车', '社保月缴存基数', '公积金月缴存基数']
|
||||
labels[11] = draftText(form, 'loanCountScope') == 'all_loans' ? '贷款笔数' : '信用贷款笔数(旧口径)'
|
||||
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
|
||||
if (form.getString('provinceCode') != null) labels[2] = '所在省份'
|
||||
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]
|
||||
@@ -282,7 +312,7 @@ export async function fetchAssessmentDetail(id: string, revisionId: string = '')
|
||||
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)
|
||||
addField(incomeKeys.includes(key) ? 1 : (assetKeys.includes(key) ? 2 : 0), key, labels[index], value, raw != null && value.length > 0, raw == null)
|
||||
})
|
||||
return { record, fields, sections }
|
||||
}
|
||||
@@ -300,6 +330,7 @@ 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 }
|
||||
payload['loanCountScope'] = draftText(form, 'loanCountScope') == 'all_loans' ? 'all_loans' : ''
|
||||
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']
|
||||
|
||||
Reference in New Issue
Block a user