feat: 支持评估版本隔离的报告、匹配及PDF导出功能

新增revisionId参数到路由、服务及导出接口,实现不同评估版本的隔离访问。删除已废弃的需求相关服务文件,移除取消采集的融资金额、期限和用途字段,更新相关页面提示文本。同时更新文档,说明评估版本、最新资料及产品展示规则的调整,支持前台正常业务名称显示,去除环境标签。
This commit is contained in:
hujun
2026-09-30 19:11:44 +08:00
parent 542c107317
commit 3b2a0139e4
34 changed files with 1389 additions and 338 deletions
+31 -47
View File
@@ -3,7 +3,8 @@
<AppHeader :title="titles[step]" :customBack="true" @back="back" />
<scroll-view scroll-y :style="contentStyle(96, false)">
<view class="assessment-content">
<text class="local-note">未提交内容不会保存,离开或刷新后需重新填写。</text><view class="progress-row"><view class="step-line"><view v-for="i in 5" :key="i" :class="i <= step + 1 ? 'step-segment done' : 'step-segment'"></view></view><text class="step-count">{{ step + 1 }}/5</text></view>
<!-- 离页清空表单时仅切换提示可见性,避免卸载期间创建原生文本节点。 -->
<text v-if="editId.length > 0" class="section-hint">修改本次评估 · 提交后保留此前版本</text><text v-show="editLoading" class="local-note">正在读取已提交资料…</text><text v-show="editError.length > 0" class="local-note">{{ editError }}</text><text class="local-note">未提交内容不会保存,离开或刷新后需重新填写。</text><view class="progress-row"><view class="step-line"><view v-for="i in 5" :key="i" :class="i <= step + 1 ? 'step-segment done' : 'step-segment'"></view></view><text class="step-count">{{ step + 1 }}/5</text></view>
<view v-if="step == 0" class="type-row"><text class="type-label">融资类型</text><view class="type-switch"><text :class="assessmentType == 'personal' ? 'type-item type-active' : 'type-item'" @click="switchType('personal')">个人</text><text :class="assessmentType == 'enterprise' ? 'type-item type-active' : 'type-item'" @click="switchType('enterprise')">企业</text></view></view>
<text v-else class="section-hint">{{ assessmentType == 'enterprise' ? '企业信用融资' : '个人信用融资' }} · {{ step == 3 ? '资料与授权均为选填' : '请按实际情况填写' }}</text>
<view v-if="step == 0" class="form-list">
@@ -29,14 +30,7 @@
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
</template>
<text class="asset-section-title">资金需求(必填)</text>
<text class="local-note">填写您期望的金额、期限和用途,不代表机构审批结果。</text>
<view class="form-row"><text class="field-label">融资金额</text><input v-model="form.requestedAmountWan" 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.requestedTermMonths" class="field-input" type="number" maxlength="4" placeholder="请输入整数月" /><text class="field-unit">个月</text></view>
<text v-if="purposesLoading" class="local-note">正在加载资金用途…</text>
<view v-else-if="purposeError.length > 0" class="purpose-error"><text class="local-note">{{ purposeError }}</text><button class="purpose-retry" @click="loadPurposes">重新加载用途</button></view>
<AssessmentSelect v-else label="资金用途" :modelValue="form.purposeLabel" @update:modelValue="purposeChanged" :options="purposeLabels" />
<view v-if="form.purposeCode == 'other'" class="purpose-detail"><text class="field-label">用途说明</text><textarea v-model="form.purposeDetail" class="purpose-textarea" maxlength="200" placeholder="请简述资金用途(2至200字)" /></view>
</view>
<view v-else-if="step == 2" class="form-list">
<text class="asset-section-title">贷款信息</text>
@@ -61,15 +55,16 @@
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
</view>
</scroll-view>
<view class="bottom-action" :style="bottomStyle(20)"><button v-if="step > 0" class="back-button" @click="previous">上一步</button><button class="primary-button next-button" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
<view class="bottom-action" :style="bottomStyle(20)"><button v-if="step > 0" class="back-button" @click="previous">上一步</button><button class="primary-button next-button" :disabled="editLoading || editError.length > 0" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
</view>
</template>
<script setup lang="uts">
import { getSession } from '@/services/api.uts'
import { fetchAssessmentEdit } from '@/packages/finance/services/records.uts'
import { writeAssessmentFlow } from '@/services/assessment-flow.uts'
import { wanAmountValid } from '@/services/money.uts'
import { FinancingPurpose, fetchFinancingPurposes, demandValidationMessage } from '@/services/assessment-demand.uts'
import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
@@ -77,47 +72,28 @@
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
const showAgreement = (title: string) => { showAgreementNotice(title) }
const editId = ref(''); const editError = ref(''); const editLoading = ref(false)
let baseRevisionId = ''; let editGeneration = 0
const step = ref(0)
const assessmentType = ref('personal')
const form = ref<AssessmentForm>(emptyAssessmentForm())
let ownerToken = ''; let goingToConfirm = false
const sameOwner = (): boolean => { const session = getSession(); return session != null && session['token'] == ownerToken }
const purposes = ref<FinancingPurpose[]>([])
const purposesLoading = ref(false)
const purposeError = ref('')
const purposeLabels = computed((): string[] => purposes.value.filter((item) => item.type == assessmentType.value).map((item) => item.label))
const purposeChanged = (label: string) => {
const item = purposes.value.find((entry) => entry.type == assessmentType.value && entry.label == label)
if (item == null) return
form.value.purposeCode = item.code; form.value.purposeLabel = item.label
// 从其他用途切走时清空说明,避免提交隐藏的过期内容。
if (!item.requiresDetail) form.value.purposeDetail = ''
}
const loadPurposes = async () => {
if (purposesLoading.value) return
purposesLoading.value = true; purposeError.value = ''
try {
const items = await fetchFinancingPurposes()
if (sameOwner()) purposes.value = items
} catch (error) {
if (sameOwner()) purposeError.value = error instanceof Error ? error.message : '资金用途加载失败,请重试'
} finally { purposesLoading.value = false }
}
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[]>([])
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
const titles = ['基本信息', '收入与资金需求', '资产 / 负债信息', '资料上传 / 授权']
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '身份证反面', '银行卡', '收入证明'])
let initialized = false
const rememberInput = () => {
if (!initialized || !sameOwner()) return
const source = form.value
const currentForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears, operatingMonths: source.operatingMonths, requestedAmountWan: source.requestedAmountWan, requestedTermMonths: source.requestedTermMonths, purposeCode: source.purposeCode, purposeLabel: source.purposeLabel, purposeDetail: source.purposeDetail }
const currentForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears, operatingMonths: source.operatingMonths }
if (currentForm.city == '其他城市' && customCity.value.trim().length > 0) currentForm.city = customCity.value.trim()
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value)
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value, editId.value, baseRevisionId)
}
const restore = (type: string) => {
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); customCity.value = ''; documents.value = []; documentConsent.value = false; dataConsent.value = false
@@ -134,7 +110,17 @@
initialized = true
rememberInput()
}
const switchType = (type: string) => { if (assessmentType.value == type) return; rememberInput(); restore(type); step.value = 0; rememberInput() }
const switchType = (type: string) => { if (editId.value.length > 0 || assessmentType.value == type) return; rememberInput(); restore(type); step.value = 0; rememberInput() }
const loadEdit = async (type: string) => {
const ticket = ++editGeneration; editLoading.value = true
try {
const saved = await fetchAssessmentEdit(editId.value)
if (ticket != editGeneration || !sameOwner()) return
baseRevisionId = saved.getString('baseRevisionId') ?? ''
writeAssessmentFlow(ownerToken, saved); restore(saved.getString('type') ?? type)
} catch (err) { if (ticket == editGeneration && sameOwner()) editError.value = err instanceof Error ? err.message : '资料读取失败,请返回重试。' }
finally { if (ticket == editGeneration) editLoading.value = false }
}
onLoad((options) => {
const session = getSession()
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
@@ -142,8 +128,10 @@
clearAssessmentFlow()
let type = activeAssessmentType()
if (options != null && (options['type'] == 'enterprise' || options['type'] == 'personal')) type = options['type'] as string
restore(type)
loadPurposes()
editId.value = options?.['editId'] ?? ''
if (editId.value.length == 0) { restore(type); return }
if (!/^[1-9][0-9]{0,9}$/.test(editId.value)) { editError.value = '评估编号无效,请返回档案重新选择。'; return }
loadEdit(type)
})
onShow(() => {
goingToConfirm = false
@@ -154,12 +142,14 @@
// 离开填写流程即丢弃未提交内容;进入确认页时只在内存传递。
onHide(() => {
if (!goingToConfirm) {
clearAssessmentFlow(); form.value = emptyAssessmentForm(); step.value = 0; documents.value = []
editGeneration++; if (editId.value.length > 0) editError.value = '已离开修改流程,请返回档案重新进入。'
// 离页清空资料但不切换步骤,避免卸载时又挂载另一组原生输入组件。
clearAssessmentFlow(); form.value = emptyAssessmentForm(); documents.value = []
customCity.value = ''; documentConsent.value = false; dataConsent.value = false
authorizations.value.forEach((item) => { item.selected = false })
}
})
onUnload(() => { initialized = false; clearAssessmentFlow() })
onUnload(() => { editGeneration++; initialized = false; clearAssessmentFlow() })
// 页内前后步骤只维护当前内存状态,返回入口后不保留填写内容。
onBackPress((): boolean => { rememberInput(); if (step.value > 0) { step.value--; rememberInput(); return true }; return false })
const previous = () => { rememberInput(); if (step.value > 0) step.value--; rememberInput() }
@@ -199,9 +189,6 @@
if (step.value == 1) {
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效年营收(万元,最多六位小数)' : '请输入有效月收入(万元,最多六位小数)'
else if (purposesLoading.value || purposeError.value.length > 0) message = '请先加载资金用途后继续'
else if (!purposes.value.some((item) => item.type == assessmentType.value && item.code == form.value.purposeCode)) message = '请选择资金用途'
else message = demandValidationMessage(form.value.requestedAmountWan, form.value.requestedTermMonths, form.value.purposeCode, form.value.purposeDetail)
}
if (step.value == 2) message = assetValidationMessage(form.value)
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
@@ -209,7 +196,7 @@
if (message.length == 0) return true
uni.showToast({ title: message, icon: 'none' }); return false
}
const next = () => { if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
const next = () => { if (editLoading.value || editError.value.length > 0 || !sameOwner()) return; if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
</script>
<style>
@@ -231,9 +218,6 @@
.form-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
.field-label { font-size: 28rpx; color: #29374e; margin-right: 18rpx; flex-shrink: 0; }
.field-input { flex: 1; min-width: 0; text-align: right; font-size: 27rpx; color: #22324a; }
.purpose-detail { margin-top: 26rpx; }
.purpose-textarea { width: 100%; height: 150rpx; margin-top: 16rpx; padding: 18rpx; border-radius: 12rpx; background-color: #f5f7fb; font-size: 26rpx; color: #22324a; }
.purpose-retry { margin: 14rpx 0; font-size: 24rpx; color: #0868f7; background-color: #eff6ff; }
.upload-title { font-size: 30rpx; font-weight: 600; color: #20304a; margin-top: 28rpx; }
.optional { font-size: 24rpx; color: #748198; font-weight: 400; }
.upload-desc { font-size: 24rpx; line-height: 36rpx; color: #748198; margin-top: 10rpx; }