1. add default avatar and multiple notification icons 2. update finance archive note and add form styles 3. add new pages: edit-profile, security, password, change-phone, search, help 4. refactor navigation action layout to support multiple actions 5. add search button to home page 6. add security code component and account service APIs 7. add help and search content services 8. update message notice to use svg icons 9. update asset section in finance assessment 10. update manifest appid and pack options
211 lines
21 KiB
Plaintext
211 lines
21 KiB
Plaintext
<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">
|
||
<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="['商贸零售', '制造业', '科技服务', '餐饮住宿', '专业服务', '其他行业']" />
|
||
<AssessmentSelect label="经营年限" v-model="form.operatingYears" :options="['不足 1 年', '1–3 年', '3–5 年', '5 年以上']" />
|
||
<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="请输入月收入" /></view>
|
||
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||
</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">支持照片,仅在本机预览,尚未上传。</text>
|
||
<view class="upload-grid"><view class="upload-card" v-for="(item, index) in uploadNames" :key="item" :class="index == 2 ? '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="documentPath(item).length > 0 ? 'upload-state selected-state' : 'upload-state'">{{ documentPath(item).length > 0 ? '已选择 · 管理' : '点击选择' }}</text></view></view>
|
||
<view class="consent"><view class="check-hit" @click="documentConsent = !documentConsent"><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" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { usePageLayout } from '@/services/layout.uts'
|
||
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, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice, assetValidationMessage } from '@/services/assessment.uts'
|
||
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||
const showAgreement = (title: string) => { showAgreementNotice(title) }
|
||
const step = ref(0)
|
||
const assessmentType = ref('personal')
|
||
const form = ref<AssessmentForm>(emptyAssessmentForm())
|
||
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 uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '银行卡', '收入证明'])
|
||
let initialized = false
|
||
const saveDraft = () => {
|
||
if (!initialized) return
|
||
const source = form.value
|
||
const draftForm: 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 }
|
||
if (draftForm.city == '其他城市' && customCity.value.trim().length > 0) draftForm.city = customCity.value.trim()
|
||
saveAssessmentDraft(assessmentType.value, step.value, draftForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value)
|
||
}
|
||
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 = loadAssessmentDraft(type)
|
||
if (saved != null) {
|
||
form.value = formFromDraft(saved); documents.value = documentsFromDraft(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
|
||
saveDraft()
|
||
}
|
||
const switchType = (type: string) => { if (assessmentType.value == type) return; saveDraft(); restore(type); step.value = 0; saveDraft() }
|
||
onLoad((options) => {
|
||
let type = activeAssessmentType()
|
||
if (options != null && (options['type'] == 'enterprise' || options['type'] == 'personal')) type = options['type'] as string
|
||
restore(type)
|
||
})
|
||
onShow(() => {
|
||
const requestedStep = uni.getStorageSync('assessmentEditStep')
|
||
if (typeof requestedStep == 'number') { step.value = Math.max(0, Math.min(3, requestedStep as number)); uni.removeStorageSync('assessmentEditStep'); saveDraft() }
|
||
})
|
||
onHide(() => saveDraft())
|
||
onUnload(() => saveDraft())
|
||
// 系统返回与顶部返回均先保存;页内返回优先回到上一步。
|
||
onBackPress((): boolean => { saveDraft(); if (step.value > 0) { step.value--; saveDraft(); return true }; return false })
|
||
const previous = () => { saveDraft(); if (step.value > 0) step.value--; saveDraft() }
|
||
const back = () => { if (step.value > 0) { previous(); return }; saveDraft(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/entry/entry?type=' + assessmentType.value }) }
|
||
const documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path }
|
||
const documentExpired = (name: string) => {
|
||
documents.value = documents.value.filter((file) => file.name != name); saveDraft()
|
||
uni.showToast({ title: name + '已失效,请重新选择', icon: 'none' })
|
||
}
|
||
const chooseDocument = (name: string) => {
|
||
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (result) => {
|
||
if (result.tempFilePaths.length == 0) return
|
||
documents.value = documents.value.filter((file) => file.name != name)
|
||
const document: SelectedDocument = { name, path: result.tempFilePaths[0] }
|
||
documents.value.push(document); saveDraft()
|
||
}, fail: (error) => { if (!error.errMsg.includes('cancel')) uni.showToast({ title: '选择失败,请重试或检查相册权限', icon: 'none' }) } })
|
||
}
|
||
const documentAction = (name: string) => {
|
||
const path = documentPath(name)
|
||
if (path.length == 0) { chooseDocument(name); return }
|
||
uni.showActionSheet({ itemList: ['预览图片', '重新选择', '移除资料'], success: (result) => {
|
||
if (result.tapIndex == 0) uni.previewImage({ urls: [path], fail: () => { uni.showToast({ title: '本地图片已失效,请重新选择', icon: 'none' }) } })
|
||
else if (result.tapIndex == 1) chooseDocument(name)
|
||
else if (result.tapIndex == 2) { documents.value = documents.value.filter((file) => file.name != name); saveDraft() }
|
||
} })
|
||
}
|
||
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 (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效的年营业收入' : '请输入有效的月收入'
|
||
}
|
||
if (step.value == 2) message = assetValidationMessage(form.value)
|
||
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
|
||
if (step.value == 3 && authorizations.value.some((item) => item.selected) && !dataConsent.value) message = '请阅读并同意数据授权协议,或取消授权选择'
|
||
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++; saveDraft() } else { saveDraft(); uni.navigateTo({ url: '/packages/finance/confirm/confirm' }) } }
|
||
</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: 74rpx; height: 62rpx; border-radius: 8rpx; }
|
||
.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; }
|
||
</style>
|