Files
airz-uniapp/packages/finance/assessment/assessment.uvue
T

425 lines
38 KiB
Plaintext
Raw Normal View History

2026-09-10 10:57:40 +08:00
<template>
<view class="page page-white" :style="pageStyle()">
<AppHeader :title="titles[step]" :customBack="true" @back="back" />
<scroll-view scroll-y :style="contentStyle(96, false)">
<view class="assessment-content">
<!-- 离页清空表单时仅切换提示可见性,避免卸载期间创建原生文本节点。 -->
<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">
<view v-if="assessmentType == 'enterprise'" class="form-row"><text class="field-label">企业名称</text><input v-model="form.enterpriseName" class="field-input" placeholder="请输入企业名称" /></view>
<view v-if="assessmentType == 'enterprise'" class="form-row"><text class="field-label">信用代码</text><input v-model="form.businessCode" class="field-input" maxlength="18" placeholder="统一社会信用代码" /></view>
<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>
<AssessmentSelect v-if="assessmentType == 'personal'" label="婚姻状况" v-model="form.maritalStatus" :options="['未婚', '已婚', '离异', '丧偶']" />
</view>
<view v-else-if="step == 1" class="form-list">
<template v-if="assessmentType == 'enterprise'">
<AssessmentSelect label="经营行业" v-model="form.occupation" :options="['商贸零售', '制造业', '科技服务', '餐饮住宿', '专业服务', '其他行业']" />
<view class="form-row"><text class="field-label">经营月数</text><input v-model="form.operatingMonths" class="field-input" type="number" maxlength="4" placeholder="请输入实际经营月数" /></view>
<view class="form-row"><text class="field-label">年营收(万元)</text><input v-model="form.annualRevenue" class="field-input" type="digit" placeholder="请输入年营业收入" /></view>
</template>
<template v-else>
<AssessmentSelect label="职业类型" v-model="form.occupation" :options="['企业职员', '事业单位', '公务员', '个体经营', '自由职业', '其他']" />
<view class="form-row"><text class="field-label">工作单位</text><input v-model="form.company" class="field-input" placeholder="请输入工作单位" /></view>
<view class="form-row"><text class="field-label">月收入</text><input v-model="form.monthlyIncome" class="field-input" type="digit" placeholder="请输入月收入" /><text class="field-unit">万元</text></view>
<AssessmentSelect label="社保连续缴纳" :modelValue="form.socialSecurity" @update:modelValue="socialSecurityChanged" :options="contributionOptions(form.socialSecurity)" />
<view v-if="contributionBaseVisible(form.socialSecurity)" class="form-row"><text class="field-label">社保月基数</text><input v-model="form.socialSecurityBase" class="field-input" type="digit" maxlength="15" placeholder="选填" /><text class="field-unit">万元</text></view>
<AssessmentSelect label="公积金连续缴纳" :modelValue="form.housingFund" @update:modelValue="housingFundChanged" :options="contributionOptions(form.housingFund)" />
<view v-if="contributionBaseVisible(form.housingFund)" class="form-row"><text class="field-label">公积金月基数</text><input v-model="form.housingFundBase" class="field-input" type="digit" maxlength="15" placeholder="选填" /><text class="field-unit">万元</text></view>
<text class="local-note">按当前连续缴纳时长选择。基数是计算缴费的月工资基数,不是个人缴费金额;不清楚可留空。</text>
</template>
</view>
<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.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>
<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>
<AssessmentSelect label="是否有房" v-model="form.house" :options="['有', '无']" />
<AssessmentSelect label="是否有车" v-model="form.car" :options="['有', '无']" />
</view>
<view v-else class="upload-area">
<text class="upload-title">资料选择 <text class="optional">(选填)</text></text>
<text class="upload-desc">JPG / PNG,单张最多5MB。仅本人可查看和删除;照片未核验,暂不参与评估。</text>
<view class="upload-grid"><view class="upload-card" v-for="(item, index) in uploadNames" :key="item" :class="index == uploadNames.length - 1 ? 'upload-last' : ''" @click="documentAction(item)"><image v-if="documentPath(item).length > 0" class="document-thumb" :src="documentPath(item)" mode="aspectFill" @error="documentExpired(item, '')" /><AppIcon v-else name="upload" :size="52" /><text class="upload-name">{{ item }}</text><text class="upload-state">{{ documentState(item) }}</text></view></view>
<text v-if="documentMessage.length > 0" class="upload-desc">{{ documentMessage }}</text>
<text v-if="documentRecoveryFailed" class="consent-link" @click="restoreDocuments">照片读取失败 · 点击重试</text>
<view class="consent"><view class="check-hit" @click="toggleDocumentConsent"><view :class="documentConsent ? 'checkbox checked' : 'checkbox'"><text v-if="documentConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('资料使用协议')">《资料使用协议》</text></text></view>
<view class="data-section"><text class="upload-title">数据授权 <text class="optional">(可选)</text></text><text class="upload-desc">自主选择授权项目,可随时取消选择。</text><view class="grant-list"><view v-for="grant in authorizations" :key="grant.key" :class="grant.selected ? 'grant-card grant-selected' : 'grant-card'" @click="grant.selected = !grant.selected"><text :class="grant.selected ? 'grant-name grant-name-active' : 'grant-name'">{{ grant.name }}</text><view :class="grant.selected ? 'checkbox checked' : 'checkbox'"><text v-if="grant.selected" class="check-mark">✓</text></view></view></view><view class="consent"><view class="check-hit" @click="dataConsent = !dataConsent"><view :class="dataConsent ? 'checkbox checked' : 'checkbox'"><text v-if="dataConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('数据授权协议')">《数据授权协议》</text></text></view><text class="local-note">当前仅保存选择意向,不查询外部数据。</text></view>
</view>
<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" :disabled="editLoading || editError.length > 0" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
2026-09-10 10:57:40 +08:00
</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 { usePageLayout } from '@/services/layout.uts'
2026-09-10 10:57:40 +08:00
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, 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'
import { Agreement, emptyAgreement, fetchAgreement, grantConsent, consentRequestId } from '@/services/agreements.uts'
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
const contributionOptions = (current: string): string[] => contributionChoices(current)
const contributionBaseVisible = (duration: string): boolean => baseVisible(duration)
const editId = ref(''); const editError = ref(''); const editLoading = ref(false)
let baseRevisionId = ''; let editGeneration = 0
2026-09-10 10:57:40 +08:00
const step = ref(0)
const assessmentType = ref('personal')
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[]>([])
const documentBusy = ref(''); const documentMessage = ref('')
const documentRecoveryFailed = ref(false); let thumbnailIds: string[] = []
let documentGeneration = 0; let documentConsentId = ''; let consentKey = ''; let pendingAgreement: Agreement = emptyAgreement()
let choosingImage = false; let previewingImage = false
const clearDocumentTemps = () => { documents.value.forEach((file) => { if (file.documentId.length > 0) releaseDocumentTemp(file.path); file.path = '' }) }
const invalidateDocuments = () => { documentGeneration++; documentBusy.value = ''; thumbnailIds = []; documentRecoveryFailed.value = false; clearDocumentTemps() }
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '身份证反面', '银行卡', '收入证明'])
let initialized = false
const stopAccountTimer = () => { if (accountTimer != 0) clearInterval(accountTimer); accountTimer = 0 }
const ensureOwner = (): boolean => {
if (!visible) return false
if (sameOwner()) return true
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
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' })
return false
}
const socialSecurityChanged = (value: string) => { if (!ensureOwner()) return; form.value.socialSecurity = value; if (!contributionBaseVisible(value)) form.value.socialSecurityBase = '' }
const housingFundChanged = (value: string) => { if (!ensureOwner()) return; form.value.housingFund = value; if (!contributionBaseVisible(value)) form.value.housingFundBase = '' }
const startAccountTimer = () => {
stopAccountTimer()
if (ownerToken.length == 0) return
accountTimer = setInterval(() => { if (visible && !sameOwner()) ensureOwner() }, 1000)
}
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()
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value, editId.value, baseRevisionId)
2026-09-10 10:57:40 +08:00
}
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
authorizations.value.forEach((item) => { item.selected = false })
const saved = getAssessmentInput(type)
if (saved != null) {
form.value = formFromInput(saved); documents.value = documentsFromInput(saved)
const savedStep = saved['step']; if (typeof savedStep == 'number') step.value = Math.max(0, Math.min(3, savedStep as number))
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 = '其他城市' }
}
initialized = true
rememberInput()
}
const hydrateDocumentCards = () => {
if (!visible || !sameOwner() || step.value != 3) return
const ticket = documentGeneration
uploadNames.value.forEach((name) => {
const item = documents.value.find((file) => file.name == name)
if (item == null || item.documentId.length == 0 || item.deletePending || item.path.length > 0 || thumbnailIds.includes(item.documentId)) return
const id = item.documentId; thumbnailIds.push(id)
downloadDocument(id).then((path) => {
const current = documents.value.find((file) => file.documentId == id && !file.deletePending)
if (ticket != documentGeneration || !visible || !sameOwner() || current == null) { releaseDocumentTemp(path); return }
if (current.path.length > 0) { releaseDocumentTemp(path); return }
current.path = path
}).catch(() => {
if (ticket == documentGeneration && visible && sameOwner()) documentMessage.value = '缩略图读取失败,点击对应卡片可重试查看'
}).finally(() => { if (ticket == documentGeneration) thumbnailIds = thumbnailIds.filter((value) => value != id) })
})
}
const restoreDocuments = async () => {
if (!ensureOwner() || !initialized || documentBusy.value.length > 0) return
const ticket = documentGeneration; documentBusy.value = 'restore'
try {
const saved = await fetchOwnedDocuments()
if (ticket != documentGeneration || !ensureOwner()) return
const pending = documents.value.filter((file) => file.documentId.length == 0)
documents.value.forEach((file) => { if (file.documentId.length > 0) releaseDocumentTemp(file.path) })
// 卡片先处理待上传项;同类已保存照片通过卡片逐份管理。
documents.value = pending.concat(saved.map((file): SelectedDocument => ({ name: file.name, path: '', documentId: file.documentId, consentId: file.consentId, deletePending: file.deletePending })))
documentRecoveryFailed.value = false; hydrateDocumentCards()
rememberInput()
} catch (error) { if (ticket == documentGeneration && ensureOwner()) { documentRecoveryFailed.value = true; documentMessage.value = error instanceof Error ? error.message : '已保存照片读取失败,请重新读取' } }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const switchType = (type: string) => { if (!ensureOwner() || editId.value.length > 0 || assessmentType.value == type) return; rememberInput(); invalidateDocuments(); restore(type); step.value = 0; rememberInput(); restoreDocuments() }
const loadEdit = async (type: string) => {
const ticket = ++editGeneration; editLoading.value = true
try {
const saved = await fetchAssessmentEdit(editId.value)
if (ticket != editGeneration || !ensureOwner()) return
baseRevisionId = saved.getString('baseRevisionId') ?? ''
writeAssessmentFlow(ownerToken, saved); restore(saved.getString('type') ?? type)
restoreDocuments()
} catch (err) { if (ticket == editGeneration && ensureOwner()) 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 }
ownerToken = session['token'] as string
clearAssessmentFlow()
let type = activeAssessmentType()
if (options != null && (options['type'] == 'enterprise' || options['type'] == 'personal')) type = options['type'] as string
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(() => {
visible = true; stopAccountTimer()
const returningFromImage = choosingImage || previewingImage
goingToConfirm = false; goingToAgreement = false; previewingImage = false
if (initialized && !sameOwner()) ensureOwner()
const input = getAssessmentInput(assessmentType.value)
if (initialized && input != null && typeof input['step'] == 'number') step.value = Math.max(0, Math.min(3, input['step'] as number))
startAccountTimer()
if (!returningFromImage) restoreDocuments()
})
// 离开填写流程即丢弃未提交内容;进入确认页时只在内存传递。
onHide(() => {
visible = false; stopAccountTimer()
if (choosingImage || previewingImage) return
invalidateDocuments()
if (!goingToConfirm && !goingToAgreement) {
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(() => { visible = false; stopAccountTimer(); editGeneration++; invalidateDocuments(); 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() }
const back = () => { if (step.value > 0) { previous(); return }; rememberInput(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/entry/entry?type=' + assessmentType.value }) }
const cardDocument = (name: string): SelectedDocument | null => documents.value.find((file) => file.name == name && file.documentId.length == 0) ?? documents.value.find((file) => file.name == name) ?? null
const documentPath = (name: string): string => { const item = cardDocument(name); return item == null ? '' : item.path }
const documentState = (name: string): string => {
if (documentBusy.value == name) return '处理中…'
const item = cardDocument(name)
if (item == null) return '点击上传'
const count = documents.value.filter((file) => file.name == name && file.documentId.length > 0).length
if (item.documentId.length > 0 && count > 1) return count.toString() + ' 张照片 · 管理'
if (item.deletePending) return '删除未完成 · 重试'
return item.documentId.length > 0 ? '已上传 · 管理' : '未上传 · 重试'
}
const documentExpired = (name: string, id: string) => {
const item = id.length == 0 ? cardDocument(name) : documents.value.find((file) => file.name == name && file.documentId == id)
if (item != null) { if (item.documentId.length > 0) releaseDocumentTemp(item.path); item.path = '' }
rememberInput(); documentMessage.value = '预览已失效,已上传资料仍可重新查看或删除'
}
const toggleDocumentConsent = async () => {
if (!ensureOwner() || documentBusy.value.length > 0) return
if (documentConsent.value) { documentConsent.value = false; rememberInput(); return }
const ticket = documentGeneration; documentBusy.value = 'consent'; documentMessage.value = ''
try {
if (pendingAgreement.version.length == 0) {
const agreement = await fetchAgreement('document-use')
if (ticket != documentGeneration || !ensureOwner()) return
pendingAgreement = agreement; consentKey = consentRequestId()
}
const record = await grantConsent(pendingAgreement, consentKey)
if (ticket != documentGeneration || !ensureOwner()) return
if (record.revokedAt.length > 0) { pendingAgreement = emptyAgreement(); consentKey = ''; throw new Error('该同意已撤销,请重新阅读并确认') }
documentConsentId = record.consentId; documentConsent.value = true; pendingAgreement = emptyAgreement(); consentKey = ''; rememberInput()
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '同意记录未确认,请再次点击重试' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const uploadSelectedDocument = async (name: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = documents.value.find((file) => file.name == name && file.documentId.length == 0)
if (item == null || item.path.length == 0 || item.documentId.length > 0) return
if (!documentConsent.value || documentConsentId.length == 0) { documentMessage.value = '请阅读并确认资料使用协议后上传'; return }
const ticket = documentGeneration; documentBusy.value = name; documentMessage.value = ''
try {
const id = await uploadDocument(item.path, documentCategory(name), documentConsentId)
if (ticket != documentGeneration || !ensureOwner()) return
item.documentId = id; item.consentId = documentConsentId; rememberInput()
documentMessage.value = '照片已单独保存,未核验、暂不参与评估'
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '保存结果未确认,请手动重试;可能另存一份照片' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const chooseDocument = (name: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
if (!documentConsent.value || documentConsentId.length == 0) { documentMessage.value = '请阅读并确认资料使用协议后上传'; return }
const ticket = documentGeneration; choosingImage = true
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (result) => {
choosingImage = false
if (ticket != documentGeneration || !sameOwner() || result.tempFilePaths.length == 0) return
documents.value = documents.value.filter((file) => file.name != name || file.documentId.length > 0)
const document: SelectedDocument = { name, path: result.tempFilePaths[0], documentId: '', consentId: '', deletePending: false }
documents.value.unshift(document); rememberInput(); uploadSelectedDocument(name)
}, fail: (error) => { choosingImage = false; if (ticket == documentGeneration && sameOwner() && !error.errMsg.includes('cancel')) documentMessage.value = '选择失败,请重试或检查相册权限' } })
}
const previewDocument = async (name: string, id: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = documents.value.find((file) => file.name == name && (id.length == 0 || file.documentId == id))
if (item == null || item.deletePending || item.documentId.length == 0) return
const ticket = documentGeneration; documentBusy.value = name
try {
const path = await downloadDocument(item.documentId)
if (ticket != documentGeneration || !ensureOwner()) { releaseDocumentTemp(path); return }
releaseDocumentTemp(item.path); item.path = path; previewingImage = true
uni.previewImage({ urls: [path], fail: () => { previewingImage = false; documentExpired(name, item.documentId) } })
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '资料读取失败,请重试' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const removeDocument = async (name: string, id: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = documents.value.find((file) => file.name == name && (id.length == 0 || file.documentId == id))
if (item == null) return
if (item.documentId.length == 0) { documents.value = documents.value.filter((file) => file.name != name || file.documentId.length > 0); rememberInput(); return }
const ticket = documentGeneration; documentBusy.value = name; item.deletePending = true; releaseDocumentTemp(item.path); item.path = ''; rememberInput()
try {
await deleteDocument(item.documentId)
if (ticket != documentGeneration || !ensureOwner()) return
documents.value = documents.value.filter((file) => file.documentId != item.documentId); rememberInput(); documentMessage.value = '照片已删除'
hydrateDocumentCards()
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '删除未完成,请对同一资料重试' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const documentItemAction = (name: string, id: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const ticket = documentGeneration; const item = documents.value.find((file) => file.name == name && file.documentId == id)
if (item == null) { chooseDocument(name); return }
const list = item.deletePending ? ['重试删除'] : (item.documentId.length > 0 ? ['查看照片', '删除照片'] : ['重试上传', '重新选择', '移除未上传照片'])
uni.showActionSheet({ itemList: list, success: (result) => {
if (ticket != documentGeneration || !ensureOwner()) return
if (item.deletePending) removeDocument(name, item.documentId)
else if (item.documentId.length > 0) { if (result.tapIndex == 0) previewDocument(name, item.documentId); else removeDocument(name, item.documentId) }
else { if (result.tapIndex == 0) uploadSelectedDocument(name); else if (result.tapIndex == 1) chooseDocument(name); else removeDocument(name, item.documentId) }
} })
}
function documentPhotoAction(name: string, offset: number): void {
const saved = documents.value.filter((file) => file.name == name && file.documentId.length > 0)
const group = saved.slice(offset, offset + 5); const ticket = documentGeneration
const list = group.map((file, index): string => '照片 ' + (offset + index + 1).toString() + (file.deletePending ? '(删除未完成)' : ''))
const more = offset + group.length < saved.length
// 微信操作菜单最多六项,每次保留五张照片和一个翻页入口。
if (more || offset > 0) list.push(more ? '更多照片' : '返回第一组')
uni.showActionSheet({ itemList: list, success: (result) => {
if (ticket != documentGeneration || !ensureOwner() || result.tapIndex < 0 || result.tapIndex >= list.length) return
if (result.tapIndex == group.length) { documentPhotoAction(name, more ? offset + group.length : 0); return }
documentItemAction(name, group[result.tapIndex].documentId)
} })
}
const documentAction = (name: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = cardDocument(name)
if (item == null) { chooseDocument(name); return }
if (item.documentId.length == 0) { documentItemAction(name, ''); return }
const saved = documents.value.filter((file) => file.name == name && file.documentId.length > 0)
if (saved.length == 1) { documentItemAction(name, item.documentId); return }
documentPhotoAction(name, 0)
}
const validateStep = (): boolean => {
let message = ''
if (step.value == 0) {
if (assessmentType.value == 'enterprise' && form.value.enterpriseName.trim().length < 2) message = '请输入企业名称'
else if (assessmentType.value == 'enterprise' && !/^[0-9A-Z]{18}$/.test(form.value.businessCode.trim().toUpperCase())) message = '请输入 18 位统一社会信用代码'
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 = '请选择或填写所在城市'
}
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' ? '请输入有效年营收(万元,最多六位小数)' : '请输入有效月收入(万元,最多六位小数)'
}
if (step.value == 1 && message.length == 0 && assessmentType.value == 'personal') message = contributionValidationMessage(form.value)
if (step.value == 2) message = assetValidationMessage(form.value)
if (step.value == 3 && (documentBusy.value.length > 0 || documents.value.some((item) => item.documentId.length == 0 || item.deletePending))) message = '请先完成上传、删除重试,或移除未上传照片'
if (step.value == 3 && authorizations.value.some((item) => item.selected) && !dataConsent.value) message = '请阅读并同意数据授权协议,或取消授权选择'
if (message.length == 0) return true
uni.showToast({ title: message, icon: 'none' }); return false
}
const next = () => { if (!ensureOwner() || editLoading.value || editError.value.length > 0) return; if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput(); hydrateDocumentCards() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
2026-09-10 10:57:40 +08:00
</script>
<style>
.assessment-content { padding: 26rpx 30rpx 0; }
.progress-row { flex-direction: row; align-items: center; }
.step-line { height: 8rpx; flex: 1; flex-direction: row; }
.step-segment { flex: 1; height: 8rpx; border-radius: 8rpx; background-color: #e8edf4; margin-right: 8rpx; }
.done { background-color: #0868f7; }
.step-count { color: #758297; font-size: 24rpx; margin-left: 20rpx; }
.type-row { margin-top: 30rpx; flex-direction: row; align-items: center; justify-content: space-between; }
.type-label { font-size: 28rpx; color: #29374e; }
.type-switch { flex-direction: row; padding: 5rpx; border-radius: 14rpx; background-color: #eef4fd; }
.type-item { width: 130rpx; padding: 13rpx 0; border-radius: 10rpx; text-align: center; color: #68809f; font-size: 26rpx; }
.type-active { color: #0868f7; font-weight: 600; background-color: #fff; }
.section-hint { color: #738198; font-size: 24rpx; line-height: 36rpx; margin-top: 30rpx; }
.form-list { margin-top: 14rpx; }
.asset-section-title { margin-top: 28rpx; font-size: 26rpx; line-height: 38rpx; color: #738198; font-weight: 600; }
.field-unit { margin-left: 12rpx; font-size: 25rpx; color: #738198; flex-shrink: 0; }
.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; }
.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; }
.upload-grid { flex-direction: row; margin-top: 22rpx; }
.upload-card { flex: 1; min-width: 0; height: 200rpx; margin-right: 14rpx; border-radius: 16rpx; background-color: #f1f6fd; align-items: center; justify-content: center; }
.upload-last { margin-right: 0; }
.document-thumb { width: 100%; height: 100rpx; border-radius: 10rpx; }
.upload-name { font-size: 25rpx; color: #304260; margin-top: 16rpx; }
.upload-state { color: #7a889e; font-size: 22rpx; margin-top: 9rpx; }
.selected-state { color: #0868f7; }
.consent { margin-top: 24rpx; flex-direction: row; align-items: flex-start; }
.check-hit { width: 44rpx; height: 42rpx; flex-shrink: 0; justify-content: center; }
.checkbox { width: 30rpx; height: 30rpx; border-radius: 8rpx; border: 2rpx solid #9aa8ba; align-items: center; justify-content: center; flex-shrink: 0; }
.checked { background-color: #0868f7; border-color: #0868f7; }
.check-mark { color: #fff; font-size: 22rpx; }
.consent-copy { flex: 1; min-width: 0; color: #687991; font-size: 24rpx; line-height: 40rpx; }
.consent-link { color: #0868f7; font-size: 24rpx; line-height: 40rpx; }
.data-section { margin-top: 24rpx; padding-top: 4rpx; border-top: 1rpx solid #e9eef6; }
.grant-list { margin-top: 18rpx; }
.grant-card { min-height: 78rpx; padding: 0 22rpx; border: 1rpx solid #e3eaf4; border-radius: 14rpx; margin-top: 12rpx; flex-direction: row; align-items: center; justify-content: space-between; }
.grant-selected { border-color: #c3d9ff; background-color: #f1f6ff; }
.grant-name { font-size: 27rpx; color: #344761; }
.grant-name-active { color: #0868f7; }
.local-note { color: #8290a4; font-size: 22rpx; line-height: 34rpx; margin-top: 14rpx; }
.scroll-spacer { min-height: 160rpx; }
.bottom-action { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx; background-color: #fff; border-top: 1rpx solid #edf1f6; flex-direction: row; }
.back-button { width: 180rpx; height: 92rpx; line-height: 92rpx; margin-right: 18rpx; border-radius: 16rpx; background-color: #eef3f9; color: #526075; font-size: 28rpx; }
.next-button { flex: 1; margin: 0; }
2026-09-10 10:57:40 +08:00
</style>