refactor: 重构项目分包结构与资源管理
1. 调整页面路由,将业务页面迁移到对应分包,主包仅保留首页 2. 拆分公共服务模块,将账户、融资相关逻辑移入对应分包 3. 统一资源路径,将专属静态资源放入各分包static目录,更新导航栏图标资源 4. 修复启动引导逻辑,实现仅首次冷启动显示引导页 5. 更新README文档,补充分包配置与开发规范说明
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<view class="analyzing-page" :style="pageStyle()">
|
||||
<view class="safe" :style="statusStyle(80)"></view>
|
||||
<text class="analyzing-title">AI分析中</text>
|
||||
<text class="analyzing-sub">正在整理融资评估信息…</text>
|
||||
<view class="orbital"><view class="orbit orbit-one"></view><view class="orbit orbit-two"></view><view class="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
|
||||
<view class="analysis-list"><view class="analysis-item" v-for="(item,index) in items" :key="item"><view :class="progress > index ? 'analysis-check done' : 'analysis-check'"><text class="analysis-mark">{{ progress > index ? '✓' : '·' }}</text></view><text class="analysis-label">{{ item }}</text></view></view>
|
||||
<text class="estimate">正在生成示例报告,请稍候…</text>
|
||||
<text class="view-now" @click="finish">查看演示结果</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
const { statusStyle, pageStyle } = usePageLayout()
|
||||
const progress = ref(0)
|
||||
const items = ['数据解析中', '信用评估中', '匹配方案中', '风险检测中']
|
||||
let timer = 0
|
||||
let finishTimer = 0
|
||||
let finished = false
|
||||
const finish = () => {
|
||||
if (finished) return
|
||||
finished = true
|
||||
clearInterval(timer)
|
||||
clearTimeout(finishTimer)
|
||||
uni.redirectTo({ url: '/packages/finance/report/report' })
|
||||
}
|
||||
onShow(() => {
|
||||
if (finished) return
|
||||
timer = setInterval(() => { if (progress.value < 4) progress.value++; else clearInterval(timer) }, 550)
|
||||
finishTimer = setTimeout(() => finish(), 3200)
|
||||
})
|
||||
onHide(() => { clearInterval(timer); clearTimeout(finishTimer) })
|
||||
onUnmounted(() => { clearInterval(timer); clearTimeout(finishTimer) })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.analyzing-page { background: linear-gradient(160deg, #061c49, #002c75); align-items: center; color: #fff; }
|
||||
.safe { }
|
||||
.analyzing-title { font-size: 45rpx; font-weight: 800; }
|
||||
.analyzing-sub { margin-top: 20rpx; color: #c6d9ff; font-size: 25rpx; }
|
||||
.orbital { width: 400rpx; height: 400rpx; margin-top: 55rpx; align-items: center; justify-content: center; position: relative; }
|
||||
.orbit { position: absolute; border-radius: 50%; border: 2rpx solid rgba(79, 176, 255, .46); }
|
||||
.orbit-one { width: 350rpx; height: 350rpx; }
|
||||
.orbit-two { width: 280rpx; height: 280rpx; border-color: rgba(57, 235, 255, .5); }
|
||||
.ai-core { width: 220rpx; height: 220rpx; border-radius: 50%; background-color: rgba(10, 102, 247, .32); box-shadow: 0 0 70rpx rgba(0, 165, 255, .48); align-items: center; justify-content: center; }
|
||||
.ai-core-image { width: 185rpx; height: 185rpx; border-radius: 50%; }
|
||||
.analysis-list { margin-top: 30rpx; width: 390rpx; }
|
||||
.analysis-item { flex-direction: row; align-items: center; margin-top: 23rpx; font-size: 26rpx; color: #d8e6ff; }
|
||||
.analysis-label { font-size: 27rpx; color: #d8e6ff; }
|
||||
.analysis-mark { font-size: 24rpx; color: #ffffff; }
|
||||
.analysis-check { width: 38rpx; height: 38rpx; border-radius: 50%; border: 2rpx solid #4c73a7; align-items: center; justify-content: center; margin-right: 17rpx; }
|
||||
.analysis-check.done { background-color: #158aff; border-color: #41d7ff; color: white; }
|
||||
.estimate { margin-top: 64rpx; color: #8faeda; font-size: 22rpx; }
|
||||
.view-now { margin-top: 28rpx; color: #62c8ff; font-size: 23rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader title="我的融资档案" />
|
||||
<scroll-view scroll-y :style="contentStyle(96, false)"><view class="archive-content">
|
||||
<view class="archive-switch"><text :class="type == 'personal' ? 'switch-item switch-active' : 'switch-item'" @click="switchType('personal')">个人档案</text><text :class="type == 'enterprise' ? 'switch-item switch-active' : 'switch-item'" @click="switchType('enterprise')">企业档案</text></view>
|
||||
<view class="completion"><view class="completion-head"><text class="completion-title">档案完整度</text><text class="completion-score">{{ progress.percent }}<text class="score-unit">%</text></text></view><view class="progress-track"><view class="progress-bar" :style="'width:' + progress.percent + '%;'"></view></view><text class="completion-desc">{{ progress.percent == 100 ? '基础信息已完善,可继续查看评估方案。' : '完善基础信息,让融资评估更贴近您的情况。' }}</text></view>
|
||||
<view class="archive-list"><view class="archive-row" v-for="item in archiveItems" :key="item.step" @click="edit(item.step)"><AppIcon :name="item.icon" :size="48" /><view class="archive-labels"><text class="archive-name">{{ item.name }}</text><text class="archive-note">{{ item.note }}</text></view><text :class="item.done ? 'state state-done' : 'state'">{{ item.done ? (item.step == 3 ? '已选择' : '已完善') : (item.step == 3 ? '选填' : '待完善') }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
|
||||
<button class="primary-button complete-button" @click="complete">{{ progress.percent == 100 ? '查看并确认信息' : '继续完善档案' }}</button>
|
||||
<text class="local-note">草稿保存在当前设备,资料与授权可自主选择。</text>
|
||||
</view></scroll-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 { AssessmentProgress, activeAssessmentType, assessmentProgress } from '@/services/assessment.uts'
|
||||
const { pageStyle, contentStyle } = usePageLayout()
|
||||
type ArchiveItem = { icon: string, name: string, note: string, done: boolean, step: number }
|
||||
const type = ref('personal')
|
||||
const progress = ref<AssessmentProgress>({ percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false })
|
||||
const archiveItems = computed((): ArchiveItem[] => [
|
||||
{ icon: 'file-text', name: '基本信息', note: type.value == 'enterprise' ? '企业与联系人信息' : '身份与联系方式', done: progress.value.basicDone, step: 0 },
|
||||
{ icon: 'income', name: type.value == 'enterprise' ? '经营信息' : '职业与收入', note: type.value == 'enterprise' ? '经营行业、年限与营收' : '职业、收入与缴纳情况', done: progress.value.incomeDone, step: 1 },
|
||||
{ icon: 'asset', name: '资产与负债', note: '资产情况与信用卡使用', done: progress.value.assetsDone, step: 2 },
|
||||
{ icon: 'authorization', name: '资料与授权', note: '本地资料与数据授权意向', done: progress.value.authorizationSelected, step: 3 }
|
||||
])
|
||||
const refresh = () => { progress.value = assessmentProgress(type.value) }
|
||||
onShow(() => { type.value = activeAssessmentType(); refresh() })
|
||||
const switchType = (next: string) => { type.value = next; uni.setStorageSync('assessmentActiveType', next); refresh() }
|
||||
const edit = (step: number) => { uni.setStorageSync('assessmentEditStep', step); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
||||
const complete = () => {
|
||||
if (progress.value.percent == 100) uni.navigateTo({ url: '/packages/finance/confirm/confirm' })
|
||||
else if (!progress.value.basicDone) edit(0)
|
||||
else if (!progress.value.incomeDone) edit(1)
|
||||
else edit(2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.archive-content { padding: 26rpx 30rpx 64rpx; }
|
||||
.archive-switch { flex-direction: row; background-color: #eaf0f8; border-radius: 16rpx; padding: 6rpx; margin-bottom: 24rpx; }
|
||||
.switch-item { flex: 1; text-align: center; font-size: 27rpx; color: #708099; padding: 18rpx 0; border-radius: 12rpx; }
|
||||
.switch-active { background-color: #fff; color: #0868f7; font-weight: 600; }
|
||||
.completion { padding: 30rpx; border-radius: 20rpx; background-color: #edf5ff; }
|
||||
.completion-head { flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.completion-title { font-size: 30rpx; font-weight: 600; color: #233d64; }
|
||||
.completion-score { color: #0868f7; font-size: 52rpx; line-height: 64rpx; font-weight: 800; }
|
||||
.score-unit { color: #0868f7; font-size: 28rpx; font-weight: 600; }
|
||||
.completion-desc { color: #6c819d; font-size: 25rpx; line-height: 38rpx; margin-top: 20rpx; }
|
||||
.progress-track { height: 12rpx; border-radius: 6rpx; background-color: #d9e6f8; margin-top: 20rpx; overflow: hidden; }
|
||||
.progress-bar { height: 12rpx; border-radius: 6rpx; background-color: #0868f7; }
|
||||
.archive-list { margin-top: 24rpx; background-color: #fff; border: 1rpx solid #e5ecf5; border-radius: 18rpx; padding: 0 22rpx; }
|
||||
.archive-row { min-height: 126rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||
.archive-labels { flex: 1; min-width: 0; margin: 0 18rpx; }
|
||||
.archive-name { font-size: 28rpx; color: #26374e; font-weight: 500; }
|
||||
.archive-note { font-size: 23rpx; color: #8190a4; margin-top: 8rpx; line-height: 34rpx; }
|
||||
.state { font-size: 23rpx; color: #8c99ab; margin-right: 10rpx; flex-shrink: 0; }
|
||||
.state-done { color: #16a478; }
|
||||
.complete-button { margin-top: 32rpx; }
|
||||
.local-note { font-size: 23rpx; color: #8795a8; line-height: 36rpx; margin-top: 22rpx; text-align: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<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">
|
||||
<AssessmentSelect label="房产情况" v-model="form.house" :options="['无房产', '全款房产', '按揭房产']" />
|
||||
<AssessmentSelect label="车辆情况" v-model="form.car" :options="['无车辆', '全款车辆', '按揭车辆']" />
|
||||
<AssessmentSelect label="存款情况" v-model="form.savings" :options="['5 万元以下', '5–20 万元', '20–50 万元', '50 万元以上']" />
|
||||
<AssessmentSelect label="信用卡使用" v-model="form.creditCard" :options="['无信用卡', '正常使用,无逾期', '当前存在逾期', '历史有逾期,已结清']" />
|
||||
<view class="form-row"><text class="field-label">其他资产</text><input v-model="form.otherAssets" class="field-input" placeholder="选填" /></view>
|
||||
</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 } 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, 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 == 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; }
|
||||
.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>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<view class="assistant-page page" :style="pageStyle()">
|
||||
<AppHeader title="AI融资助手" />
|
||||
<scroll-view scroll-y class="chat-scroll" :scroll-into-view="lastMessageId" :style="contentStyle(210, false)">
|
||||
<view class="chat-content">
|
||||
<view class="assistant-intro"><image src="/packages/finance/static/robot.png" mode="aspectFit" class="assistant-intro-image"></image><view class="assistant-bubble"><text class="bubble-text">您好!我是AI融资助手,可以帮您了解融资条件、准备材料和比较方案。</text></view></view>
|
||||
<view class="quick-questions">
|
||||
<view class="question" v-for="item in questions" :key="item" @click="ask(item)"><text class="question-text">{{ item }}</text><AppIcon name="chevron-right" :size="28" muted /></view>
|
||||
</view>
|
||||
<view v-for="(item,index) in messages" :key="index" :id="'message-' + index" :class="item.role == 'user' ? 'message-row user-row' : 'message-row'">
|
||||
<image v-if="item.role == 'assistant'" class="message-avatar" src="/packages/finance/static/robot.png" mode="aspectFit"></image>
|
||||
<view :class="item.role == 'user' ? 'message-bubble user-bubble' : 'message-bubble'"><text :class="item.role == 'user' ? 'bubble-text user-text' : 'bubble-text'">{{ item.text }}</text></view>
|
||||
</view>
|
||||
<view v-if="sending" class="message-row"><image class="message-avatar" src="/packages/finance/static/robot.png" mode="aspectFit"></image><view class="message-bubble"><text class="sending-text">正在整理建议…</text></view></view>
|
||||
<view v-if="errorMessage.length > 0" class="assistant-error"><text class="error-text">{{ errorMessage }}</text><text @click="retry" class="retry-text">重试</text></view>
|
||||
<view id="chat-end" class="chat-end"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="chat-input" :style="bottomStyle(17)"><input v-model="draft" confirm-type="send" @confirm="send" placeholder="请输入您的问题..." class="chat-input-input" /><button @click="send" :disabled="sending || draft.trim().length == 0" :class="sending || draft.trim().length == 0 ? 'chat-input-button send-disabled' : 'chat-input-button'"><AppIcon name="send" :size="40" /></button></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
import { sendAssistantMessage } from '@/packages/finance/services/api.uts'
|
||||
const questions = ['我的融资额度受哪些因素影响?', '如何提高贷款通过率?', '企业融资需要哪些材料?', '有哪些适合我的方案?']
|
||||
type ChatMessage = { role: string, text: string }
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const draft = ref('')
|
||||
const lastMessageId = ref('')
|
||||
const lastQuestion = ref('')
|
||||
const sending = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const scrollToEnd = async () => {
|
||||
lastMessageId.value = ''
|
||||
await nextTick()
|
||||
lastMessageId.value = 'chat-end'
|
||||
}
|
||||
const requestAnswer = async (text: string) => {
|
||||
sending.value = true
|
||||
errorMessage.value = ''
|
||||
scrollToEnd()
|
||||
try { const answer = await sendAssistantMessage(text); const response: ChatMessage = { role: 'assistant', text: answer }; messages.value.push(response) } catch (error) { errorMessage.value = '暂未收到回复,请重试' }
|
||||
sending.value = false
|
||||
scrollToEnd()
|
||||
}
|
||||
const send = () => {
|
||||
const text = draft.value.trim()
|
||||
if (text.length == 0 || sending.value) return
|
||||
lastQuestion.value = text
|
||||
const message: ChatMessage = { role: 'user', text: text }
|
||||
messages.value.push(message)
|
||||
draft.value = ''
|
||||
requestAnswer(text)
|
||||
}
|
||||
const ask = (text: string) => { if (sending.value) return; draft.value = text; send() }
|
||||
const retry = () => { if (!sending.value && lastQuestion.value.length > 0) requestAnswer(lastQuestion.value) }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.assistant-page { background-color: #f4f7fb; }
|
||||
.chat-scroll { }
|
||||
.chat-content { padding: 28rpx 26rpx 50rpx; }
|
||||
.assistant-intro { flex-direction: row; align-items: flex-start; }
|
||||
.assistant-intro-image, .message-avatar { width: 78rpx; height: 78rpx; border-radius: 24rpx; background-color: #e7f2ff; margin-right: 16rpx; }
|
||||
.assistant-bubble, .message-bubble { max-width: 530rpx; padding: 24rpx; border-radius: 8rpx 22rpx 22rpx 22rpx; background-color: #fff; color: #344259; font-size: 25rpx; line-height: 42rpx; border: 1rpx solid #e4ebf5; }
|
||||
.quick-questions { margin: 25rpx 0 36rpx 94rpx; }
|
||||
.question { min-height: 72rpx; margin-top: 13rpx; padding: 14rpx 18rpx; border-radius: 16rpx; border: 2rpx solid #bcd9ff; background-color: #eef6ff; color: #0868f7; font-size: 23rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||
.question-arrow { font-size: 34rpx; }
|
||||
.message-row { margin-top: 22rpx; flex-direction: row; align-items: flex-start; }
|
||||
.user-row { justify-content: flex-end; }
|
||||
.user-bubble { color: #fff; background-color: #0868f7; border-color: #0868f7; border-radius: 22rpx 8rpx 22rpx 22rpx; }
|
||||
.chat-input { position: fixed; left: 0; right: 0; bottom: 0; min-height: 114rpx; padding: 17rpx 24rpx; background-color: #fff; border-top: 1rpx solid #e3eaf4; flex-direction: row; align-items: center; }
|
||||
.chat-input-input { flex: 1; height: 78rpx; border-radius: 39rpx; background-color: #f2f5f9; padding: 0 28rpx; font-size: 25rpx; }
|
||||
.chat-input-button { width: 76rpx; height: 76rpx; line-height: 76rpx; border-radius: 50%; margin-left: 15rpx; padding: 0; background-color: #0868f7; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.send-disabled { opacity: .55; }
|
||||
/* #ifndef APP */
|
||||
.chat-input-button { display: flex; }
|
||||
/* #endif */
|
||||
.bubble-text { font-size: 27rpx; line-height: 43rpx; color: #344259; }
|
||||
.user-text { color: #ffffff; }
|
||||
.question-text { flex: 1; margin-right: 12rpx; color: #0868f7; font-size: 24rpx; line-height: 38rpx; }
|
||||
.sending-text { font-size: 25rpx; line-height: 40rpx; color: #75869d; }
|
||||
.error-text { flex: 1; color: #bb4857; font-size: 23rpx; line-height: 36rpx; }
|
||||
.retry-text { color: #0868f7; font-size: 25rpx; padding: 0 6rpx; }
|
||||
.chat-end { height: 12rpx; }
|
||||
.assistant-error { margin: 18rpx 0 0 94rpx; color: #e05a5a; font-size: 21rpx; flex-direction: row; justify-content: space-between; }
|
||||
.assistant-error-text-last-child { color: #0868f7; margin-left: 18rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<view class="select-row" @click="choose"><text class="select-label">{{ label }}</text><text :class="modelValue.length > 0 ? 'select-value' : 'select-value select-placeholder'">{{ modelValue.length > 0 ? modelValue : '请选择' }}</text><view class="select-arrow"><AppIcon name="chevron-right" :size="28" /></view></view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
const props = defineProps({ label: { type: String, default: '' }, modelValue: { type: String, default: '' }, options: { type: Array as PropType<string[]>, default: () => [] as string[] } })
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const choose = () => { uni.showActionSheet({ itemList: props.options, success: (result) => { if (result.tapIndex >= 0 && result.tapIndex < props.options.length) emit('update:modelValue', props.options[result.tapIndex]) } }) }
|
||||
</script>
|
||||
<style>
|
||||
.select-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||
.select-label { font-size: 28rpx; color: #29374e; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.select-value { flex: 1; min-width: 0; text-align: right; color: #22324a; font-size: 27rpx; line-height: 38rpx; }
|
||||
.select-placeholder { color: #8995a7; }
|
||||
.select-arrow { width: 32rpx; height: 32rpx; margin-left: 12rpx; align-items: center; justify-content: center; transform: rotate(90deg); }
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<view class="page page-white" :style="pageStyle()">
|
||||
<AppHeader title="确认信息" />
|
||||
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||
<view class="confirm-content">
|
||||
<view class="confirm-progress"><view class="progress-line"><view class="progress-segment" v-for="i in 5" :key="i"></view></view><text class="progress-count">5/5</text></view>
|
||||
<view class="confirm-tip"><view class="tip-icon"><text class="tip-mark">✓</text></view><view class="tip-copy"><text class="tip-title">{{ assessmentType == 'enterprise' ? '企业融资信息' : '个人融资信息' }}</text><text class="tip-desc">请确认后再开始评估,可随时返回修改。</text></view></view>
|
||||
<text v-if="!hasDraft" class="empty-hint">还没有评估信息,请先完成信息填写。</text>
|
||||
<view class="info-section" v-for="section in sections" :key="section.title"><view class="section-bar"><text class="section-name">{{ section.title }}</text><text class="edit" @click="edit(section.step)">编辑</text></view><template v-for="(row, index) in section.rows" :key="row.label"><view v-if="index < 3 || section.expanded" class="info-row"><text class="info-label">{{ row.label }}</text><text class="info-value">{{ row.value }}</text></view></template><text v-if="section.rows.length > 3" class="expand-link" @click="section.expanded = !section.expanded">{{ section.expanded ? '收起详情' : '查看其余信息' }}</text></view>
|
||||
<text v-if="isDemo" class="demo-note">本次为演示评估,不会发起真实融资申请。</text>
|
||||
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="submit-bar" :style="bottomStyle(20)"><button class="primary-button submit-button" :disabled="submitting" @click="submit">{{ !hasDraft ? '去填写信息' : (submitting ? '提交中…' : '确认并开始评估') }}</button></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { API_BASE_URL, submitAssessment } from '@/packages/finance/services/api.uts'
|
||||
import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft } 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 }
|
||||
const submitting = ref(false)
|
||||
const hasDraft = ref(false)
|
||||
const isDemo = API_BASE_URL.length == 0
|
||||
const assessmentType = ref('personal')
|
||||
const sections = ref<InfoSection[]>([])
|
||||
const payload = ref<UTSJSONObject>({ source: 'mobile', confirmed: false })
|
||||
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 loadDraft = () => {
|
||||
assessmentType.value = activeAssessmentType()
|
||||
const saved = loadAssessmentDraft(assessmentType.value)
|
||||
if (saved == null) { hasDraft.value = false; sections.value = []; return }
|
||||
hasDraft.value = true
|
||||
const enterprise = assessmentType.value == 'enterprise'
|
||||
const basicRows: InfoRow[] = []
|
||||
if (enterprise) {
|
||||
appendRow(basicRows, '企业名称', displayValue(saved, 'enterpriseName'))
|
||||
appendRow(basicRows, '信用代码', mask(displayValue(saved, 'businessCode'), 4, 4))
|
||||
}
|
||||
appendRow(basicRows, enterprise ? '联系人' : '姓名', displayValue(saved, 'name'))
|
||||
appendRow(basicRows, '手机号', mask(displayValue(saved, 'phone'), 3, 4))
|
||||
appendRow(basicRows, '所在城市', displayValue(saved, 'city'))
|
||||
if (!enterprise) {
|
||||
appendRow(basicRows, '身份证号', mask(displayValue(saved, 'idCard'), 4, 4))
|
||||
appendRow(basicRows, '婚姻状况', displayValue(saved, 'maritalStatus'))
|
||||
}
|
||||
const incomeRows: InfoRow[] = enterprise ? [
|
||||
{ label: '经营行业', value: displayValue(saved, 'occupation') }, { label: '经营年限', value: displayValue(saved, 'operatingYears') }, { label: '年营业收入', value: draftText(saved, 'annualRevenue').length > 0 ? draftText(saved, 'annualRevenue') + ' 万元' : '未填写' }
|
||||
] : [
|
||||
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
||||
]
|
||||
const files = documentsFromDraft(saved)
|
||||
const fileNames = files.map((file) => file.name).join('、')
|
||||
const grants = saved['authorizations'] as string[] | null
|
||||
const grantNames: string[] = []
|
||||
if (grants != null) { if (grants.includes('credit')) grantNames.push('征信'); if (grants.includes('business')) grantNames.push('经营'); if (grants.includes('fund')) grantNames.push('公积金') }
|
||||
sections.value = [
|
||||
{ title: '基本信息', step: 0, rows: basicRows, expanded: false },
|
||||
{ title: enterprise ? '经营信息' : '职业与收入', step: 1, rows: incomeRows, expanded: false },
|
||||
{ title: '资产与负债', step: 2, rows: [{ label: '房产情况', value: displayValue(saved, 'house') }, { label: '车辆情况', value: displayValue(saved, 'car') }, { label: '存款情况', value: displayValue(saved, 'savings') }, { label: '信用卡使用', value: displayValue(saved, 'creditCard') }, { label: '其他资产', value: displayValue(saved, 'otherAssets') }], expanded: false },
|
||||
{ title: '资料与授权', step: 3, rows: [{ label: '本地资料', value: fileNames.length > 0 ? fileNames : '未选择' }, { label: '授权意向', value: grantNames.length > 0 ? grantNames.join('、') : '未选择' }, { label: '当前状态', value: '资料未上传 · 数据未查询' }], expanded: false }
|
||||
]
|
||||
payload.value = { ...saved, source: 'mobile', confirmed: true }
|
||||
}
|
||||
onShow(() => loadDraft())
|
||||
const edit = (step: number) => {
|
||||
uni.setStorageSync('assessmentEditStep', step)
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/assessment/assessment') uni.navigateBack({ delta: 1 })
|
||||
else uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + assessmentType.value })
|
||||
}
|
||||
const submit = async () => {
|
||||
if (!hasDraft.value) { edit(0); return }
|
||||
if (submitting.value) return
|
||||
const saved = payload.value
|
||||
const amount = draftText(saved, assessmentType.value == 'enterprise' ? 'annualRevenue' : 'monthlyIncome')
|
||||
if (draftText(saved, 'name').length < 2 || !/^1[3-9][0-9]{9}$/.test(draftText(saved, 'phone')) || draftText(saved, 'city').length == 0) { uni.showToast({ title: '请先完善基本信息', icon: 'none' }); edit(0); return }
|
||||
if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(draftText(saved, 'idCard'))) { uni.showToast({ title: '请补全身份证信息', icon: 'none' }); edit(0); return }
|
||||
if (assessmentType.value == 'enterprise' && (draftText(saved, 'enterpriseName').length < 2 || !/^[0-9A-Z]{18}$/.test(draftText(saved, 'businessCode').toUpperCase()))) { uni.showToast({ title: '请补全企业信息', icon: 'none' }); edit(0); return }
|
||||
if (amount.length == 0 || parseFloat(amount) <= 0 || isNaN(parseFloat(amount))) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
||||
const grants = saved['authorizations'] as string[] | null
|
||||
if ((documentsFromDraft(saved).length > 0 && saved['documentConsent'] != true) || (grants != null && grants.length > 0 && saved['dataConsent'] != true)) { uni.showToast({ title: '请确认资料与授权选择', icon: 'none' }); edit(3); return }
|
||||
submitting.value = true
|
||||
try { await submitAssessment(payload.value); uni.redirectTo({ url: '/packages/finance/analyzing/analyzing' }) }
|
||||
catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.confirm-content { padding: 26rpx 30rpx 0; }
|
||||
.confirm-progress { flex-direction: row; align-items: center; margin-bottom: 24rpx; }
|
||||
.progress-line { flex: 1; flex-direction: row; }
|
||||
.progress-segment { flex: 1; height: 8rpx; margin-right: 8rpx; border-radius: 8rpx; background-color: #0868f7; }
|
||||
.progress-count { font-size: 24rpx; color: #758297; margin-left: 20rpx; }
|
||||
.confirm-tip { padding: 24rpx; border-radius: 18rpx; background-color: #eff6ff; flex-direction: row; align-items: center; }
|
||||
.tip-icon { width: 56rpx; height: 56rpx; border-radius: 28rpx; background-color: #0868f7; align-items: center; justify-content: center; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.tip-mark { color: #fff; font-size: 28rpx; }
|
||||
.tip-copy { flex: 1; min-width: 0; }
|
||||
.tip-title { color: #22324b; font-size: 29rpx; font-weight: 600; }
|
||||
.tip-desc { color: #738198; font-size: 24rpx; line-height: 36rpx; margin-top: 7rpx; }
|
||||
.empty-hint { font-size: 28rpx; line-height: 44rpx; color: #778499; margin-top: 40rpx; }
|
||||
.info-section { margin-top: 18rpx; border-bottom: 1rpx solid #e9eef5; padding-bottom: 14rpx; }
|
||||
.section-bar { height: 74rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||
.section-name { font-size: 29rpx; font-weight: 600; color: #22324b; }
|
||||
.edit { padding: 16rpx 0 16rpx 24rpx; color: #0868f7; font-size: 25rpx; }
|
||||
.info-row { min-height: 62rpx; flex-direction: row; align-items: flex-start; padding: 9rpx 0; }
|
||||
.info-label { color: #758297; font-size: 25rpx; line-height: 38rpx; width: 176rpx; flex-shrink: 0; margin-right: 16rpx; }
|
||||
.info-value { flex: 1; min-width: 0; text-align: right; color: #26374f; font-size: 26rpx; line-height: 38rpx; }
|
||||
.expand-link { color: #7387a3; font-size: 24rpx; padding: 15rpx 0 10rpx; }
|
||||
.demo-note { font-size: 23rpx; line-height: 36rpx; color: #8490a3; margin-top: 24rpx; }
|
||||
.scroll-spacer { min-height: 152rpx; }
|
||||
.submit-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx; border-top: 1rpx solid #edf1f6; background-color: #fff; }
|
||||
.submit-button { margin: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<view :class="type == 'enterprise' ? 'page entry-page enterprise-page' : 'page entry-page'" :style="pageStyle()">
|
||||
<AppHeader :title="type == 'enterprise' ? '企业信用融资' : '个人信用融资'" />
|
||||
<view class="entry-content">
|
||||
<view class="intro-panel">
|
||||
<view class="intro-copy">
|
||||
<text class="intro-title">{{ type == 'enterprise' ? '企业信用融资' : '个人信用融资' }}</text>
|
||||
<text class="intro-sub">{{ type == 'enterprise' ? '适合企业 / 个体工商户' : '适合有稳定收入的个人用户' }}</text>
|
||||
<view class="benefit" v-for="item in benefits" :key="item"><view class="benefit-dot"></view><text class="benefit-text">{{ item }}</text></view>
|
||||
</view>
|
||||
<image class="entry-image" :src="type == 'enterprise' ? '/static/mobile/images/enterprise-building-cutout.png' : '/static/mobile/images/personal-building-cutout.png'" mode="aspectFit"></image>
|
||||
</view>
|
||||
<button :class="type == 'enterprise' ? 'orange-button start-button' : 'primary-button start-button'" @click="start">开始评估</button>
|
||||
<view class="process-block">
|
||||
<text class="section-title">评估流程预览</text>
|
||||
<view class="process-row">
|
||||
<view class="process-item"><view class="process-icon"><AppIcon name="file-text" :size="50" /></view><text class="process-label">填写信息</text></view><view class="arrow"><AppIcon name="arrow-right" :size="32" /></view>
|
||||
<view class="process-item"><view class="process-icon"><AppIcon name="sparkles" :size="50" /></view><text class="process-label">AI分析</text></view><view class="arrow"><AppIcon name="arrow-right" :size="32" /></view>
|
||||
<view class="process-item"><view class="process-icon"><AppIcon name="building" :size="50" /></view><text class="process-label">匹配方案</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="privacy-tip"><text class="privacy-text">自主选择授权范围,随时补充评估资料</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
const { pageStyle } = usePageLayout()
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
const type = ref('personal')
|
||||
const benefits = ref(['了解可选融资额度', '在线评估,便捷省时', '多家机构方案对比'])
|
||||
onLoad((options) => {
|
||||
if (options != null && options['type'] == 'enterprise') {
|
||||
type.value = 'enterprise'
|
||||
benefits.value = ['匹配企业资金需求', '多维度经营资质评估', '多种融资方案对比']
|
||||
}
|
||||
})
|
||||
const start = () => { uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.entry-content { padding: 28rpx; }
|
||||
.intro-panel { min-height: 430rpx; background: linear-gradient(145deg, #eef6ff, #ffffff); border-radius: 20rpx; border: 1rpx solid #dce9fb; padding: 38rpx 28rpx; flex-direction: row; position: relative; overflow: hidden; }
|
||||
.enterprise-page .intro-panel { background: linear-gradient(145deg, #fff5ec, #ffffff); border-color: #ffe2cd; }
|
||||
.intro-copy { width: 63%; z-index: 2; }
|
||||
.intro-title { font-size: 42rpx; line-height: 56rpx; font-weight: 900; color: #0868f7; }
|
||||
.enterprise-page .intro-title { color: #f36b12; }
|
||||
.intro-sub { font-size: 23rpx; color: #5f6d82; margin-top: 8rpx; margin-bottom: 30rpx; }
|
||||
.benefit { flex-direction: row; align-items: center; margin-top: 18rpx; color: #23324a; font-size: 25rpx; font-weight: 600; }
|
||||
.benefit .app-icon { margin-right: 12rpx; }
|
||||
.benefit-text { color: #34445c; font-size: 25rpx; line-height: 38rpx; }
|
||||
.benefit-dot { width: 10rpx; height: 10rpx; border-radius: 5rpx; background-color: #0868f7; margin-right: 14rpx; flex-shrink: 0; }
|
||||
.enterprise-page .benefit-dot { background-color: #f47a1a; }
|
||||
|
||||
.entry-image { position: absolute; width: 285rpx; height: 300rpx; right: 16rpx; bottom: -22rpx; }
|
||||
.start-button { margin-top: 30rpx; }
|
||||
.process-block { margin-top: 48rpx; }
|
||||
.process-row { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 34rpx; padding: 0 20rpx; }
|
||||
.process-item { align-items: center; color: #455369; font-size: 22rpx; }
|
||||
.process-icon { width: 88rpx; height: 88rpx; border-radius: 26rpx; background-color: #e9f2ff; align-items: center; justify-content: center; margin-bottom: 14rpx; }
|
||||
.enterprise-page .process-icon { background-color: #fff0e5; }
|
||||
.process-label { font-size: 24rpx; color: #455369; }
|
||||
.privacy-text { font-size: 22rpx; color: #748398; text-align: center; line-height: 34rpx; }
|
||||
|
||||
.arrow { margin-top: -28rpx; }
|
||||
.privacy-tip { margin-top: 55rpx; align-items: center; color: #97a3b4; font-size: 21rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader title="匹配融资方案" />
|
||||
<view class="filter-bar"><view v-for="item in filters" :key="item.id" :class="filter == item.id ? 'filter active' : 'filter'" @click="filter = item.id"><text :class="filter == item.id ? 'filter-label selected' : 'filter-label'">{{ item.label }}</text></view></view>
|
||||
<scroll-view scroll-y :style="contentStyle(314, false)">
|
||||
<view class="match-content">
|
||||
<view v-if="loading" class="state-panel"><text class="state-text">正在匹配融资方案…</text></view>
|
||||
<view v-else-if="errorMessage.length > 0" class="state-panel"><text class="state-text">{{ errorMessage }}</text><button class="retry-button" @click="loadMatches">重新加载</button></view>
|
||||
<view v-else-if="matches.length == 0" class="state-panel"><text class="state-text">暂无可展示方案,请完善评估资料后重试</text></view>
|
||||
<view v-else>
|
||||
<text class="result-summary">共 {{ matches.length }} 个方案 · {{ isDemo ? '示例数据' : '按您的评估信息匹配' }}</text>
|
||||
<view class="match-card" v-for="item in sortedMatches" :key="item.id" @click="openProduct(item.id)">
|
||||
<view class="bank-row"><view class="bank-logo" :style="'background-color:' + item.color"><text class="bank-initial">{{ item.initials }}</text></view><view class="bank-copy"><text class="bank-name">{{ item.bank }}</text><text class="match-tag">个人信用融资</text></view><text class="match-score">{{ item.score }}%匹配</text></view>
|
||||
<view class="metrics"><view class="metric"><text class="metric-value">{{ item.amount }}<text class="metric-unit"> 万</text></text><text class="metric-label">最高额度</text></view><view class="metric"><text class="metric-value rate-value">{{ item.rate.toFixed(2) }}%</text><text class="metric-label">年化利率(起)</text></view><view class="metric term-metric"><text class="term-value">{{ item.term }}</text><text class="metric-label">贷款期限</text></view></view>
|
||||
<view class="card-footer"><text class="footer-hint">{{ item.advantages.join(' · ') }}</text><view class="detail-link"><text class="detail-label">查看详情</text><AppIcon name="chevron-right" :size="26" /></view></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="list-tip">展示参数仅供参考,实际额度、利率及条件以机构审批为准。银行以文字简称标识。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<BottomNav active="" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { FinanceProduct } from '@/packages/finance/services/finance.uts'
|
||||
import { fetchMatches, API_BASE_URL } from '@/packages/finance/services/api.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import BottomNav from '@/components/BottomNav.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
const { contentStyle, pageStyle } = usePageLayout()
|
||||
type MatchFilter = { id: string, label: string }
|
||||
const filters: MatchFilter[] = [{ id: 'all', label: '综合排序' }, { id: 'amount', label: '额度优先' }, { id: 'rate', label: '低息优先' }]
|
||||
const filter = ref('all')
|
||||
const matches = ref<FinanceProduct[]>([])
|
||||
const loading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const isDemo = API_BASE_URL.length == 0
|
||||
const sortedMatches = computed((): FinanceProduct[] => {
|
||||
const items = matches.value.slice()
|
||||
items.sort((a: FinanceProduct, b: FinanceProduct): number => {
|
||||
if (filter.value == 'amount') return b.amount - a.amount
|
||||
if (filter.value == 'rate') return a.rate - b.rate
|
||||
return b.score - a.score
|
||||
})
|
||||
return items
|
||||
})
|
||||
const loadMatches = async () => {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try { matches.value = await fetchMatches() } catch (error) { errorMessage.value = '方案加载失败,请重试' }
|
||||
loading.value = false
|
||||
}
|
||||
onMounted(() => { loadMatches() })
|
||||
const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product?id=' + id }) }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter-bar { height: 88rpx; background-color: #fff; flex-direction: row; padding: 0 28rpx; border-bottom: 1rpx solid #e9eef6; }
|
||||
.filter { flex: 1; align-items: center; justify-content: center; }
|
||||
.active { border-bottom: 4rpx solid #0868f7; }
|
||||
.filter-label { color: #718098; font-size: 25rpx; }
|
||||
.selected { color: #0868f7; font-weight: 700; }
|
||||
.match-content { padding: 24rpx 28rpx 90rpx; }
|
||||
.result-summary { color: #6f7f96; font-size: 22rpx; margin-bottom: 20rpx; }
|
||||
.match-card { border-radius: 18rpx; margin-bottom: 20rpx; background-color: #fff; border: 1rpx solid #e4ebf5; padding: 24rpx; }
|
||||
.bank-row { flex-direction: row; align-items: center; }
|
||||
.bank-logo { width: 64rpx; height: 64rpx; border-radius: 20rpx; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.bank-initial { color: #fff; font-size: 30rpx; font-weight: 700; }
|
||||
.bank-copy { flex: 1; }
|
||||
.bank-name { font-size: 27rpx; font-weight: 700; color: #20324d; }
|
||||
.match-tag { font-size: 21rpx; color: #7a899f; margin-top: 6rpx; }
|
||||
.match-score { color: #178461; font-size: 20rpx; }
|
||||
.metrics { flex-direction: row; margin-top: 26rpx; margin-bottom: 22rpx; }
|
||||
.metric { flex: 1; }
|
||||
.term-metric { align-items: flex-end; }
|
||||
.metric-value { color: #20324d; font-size: 35rpx; font-weight: 800; line-height: 44rpx; }
|
||||
.metric-unit { font-size: 23rpx; }
|
||||
.rate-value { color: #0868f7; }
|
||||
.term-value { color: #20324d; font-size: 25rpx; font-weight: 600; line-height: 44rpx; }
|
||||
.metric-label { margin-top: 7rpx; font-size: 21rpx; color: #7c8aa0; }
|
||||
.card-footer { border-top: 1rpx solid #edf1f6; padding-top: 18rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||
.footer-hint { flex: 1; margin-right: 16rpx; font-size: 20rpx; line-height: 32rpx; color: #78889f; }
|
||||
.detail-link { flex-direction: row; align-items: center; flex-shrink: 0; }
|
||||
.detail-label { font-size: 24rpx; color: #0868f7; font-weight: 600; margin-right: 6rpx; }
|
||||
.list-tip { color: #7a899e; font-size: 21rpx; line-height: 34rpx; margin: 8rpx 8rpx 0; }
|
||||
.state-panel { padding: 70rpx 20rpx; align-items: center; }
|
||||
.state-text { font-size: 26rpx; line-height: 42rpx; color: #728197; text-align: center; }
|
||||
.retry-button { margin-top: 20rpx; color: #0868f7; background-color: #e8f2ff; font-size: 24rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader title="产品详情"><text v-if="available" class="share" @click="shareProduct">分享</text></AppHeader>
|
||||
<scroll-view scroll-y :style="contentStyle(224, false)">
|
||||
<view class="product-content">
|
||||
<view v-if="loading" class="state"><text class="state-text">正在加载产品…</text></view>
|
||||
<view v-else-if="!available" class="state"><text class="state-title">{{ loadFailed ? '产品加载失败' : '暂未找到该产品' }}</text><text class="state-text">请重试或返回方案列表选择其他产品</text><button class="retry-button" @click="loadProduct">重新加载</button></view>
|
||||
<view v-else>
|
||||
<view class="bank-head"><view class="bank-mark" :style="'background-color:' + product.color"><text class="bank-initial">{{ product.initials }}</text></view><view class="bank-copy"><text class="product-name">{{ product.bank }}</text><text class="product-tags">个人信用融资 · {{ isDemo ? '示例产品' : '产品信息' }}</text></view></view>
|
||||
<view class="product-metrics"><view class="metric"><text class="metric-label">最高额度</text><text class="amount">{{ product.amount }}万</text></view><view class="metric"><text class="metric-label">年化利率(起)</text><text class="rate">{{ product.rate.toFixed(2) }}%</text></view><view class="metric last-metric"><text class="metric-label">贷款期限</text><text class="term">{{ product.term }}</text></view></view>
|
||||
<view class="detail-section"><text class="section-title">产品优势</text><view class="advantage-grid"><view class="advantage" v-for="item in product.advantages" :key="item"><AppIcon name="check-circle" :size="30" /><text class="advantage-label">{{ item }}</text></view></view></view>
|
||||
<view class="detail-section"><text class="section-title">申请条件</text><view class="condition"><text class="condition-label">年龄要求</text><text class="condition-value">{{ product.age }}</text></view><view class="condition"><text class="condition-label">工作要求</text><text class="condition-value">{{ product.work }}</text></view><view class="condition"><text class="condition-label">信用要求</text><text class="condition-value">{{ product.credit }}</text></view></view>
|
||||
<view class="detail-section"><text class="section-title">所需材料</text><text class="material">{{ product.materials }}</text></view>
|
||||
<text class="product-note">额度、利率及申请条件仅供参考,以机构实际审批为准。{{ isDemo ? '当前可体验申请准备流程,尚未向机构提交资料。' : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-if="available" class="apply-bar" :style="bottomStyle(18)"><button class="primary-button apply-button" @click="apply">准备申请</button></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { FinanceProduct, demoProducts } from '@/packages/finance/services/finance.uts'
|
||||
import { fetchProduct, API_BASE_URL } from '@/packages/finance/services/api.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||||
const product = ref<FinanceProduct>(demoProducts[0])
|
||||
const available = ref(false)
|
||||
const productId = ref('')
|
||||
const loading = ref(true)
|
||||
const loadFailed = ref(false)
|
||||
const isDemo = API_BASE_URL.length == 0
|
||||
const loadProduct = async () => {
|
||||
loading.value = true
|
||||
loadFailed.value = false
|
||||
available.value = false
|
||||
try {
|
||||
const selected = await fetchProduct(productId.value)
|
||||
if (selected != null) { product.value = selected; available.value = true }
|
||||
} catch (error) { loadFailed.value = true }
|
||||
loading.value = false
|
||||
}
|
||||
onLoad((options) => {
|
||||
productId.value = options?.['id'] ?? ''
|
||||
loadProduct()
|
||||
})
|
||||
const shareProduct = () => {
|
||||
const item = product.value
|
||||
if (!available.value) return
|
||||
uni.setClipboardData({ data: item.bank + '|最高' + item.amount + '万元|年化利率' + item.rate.toFixed(2) + '%起|' + item.term + '。展示信息仅供参考,以机构审批为准。' })
|
||||
}
|
||||
const apply = () => {
|
||||
uni.showModal({
|
||||
title: '准备申请资料',
|
||||
content: '先完善融资档案,核对申请所需材料。当前尚未开通机构申请,资料不会提交至银行。',
|
||||
confirmText: '完善档案',
|
||||
success: (result) => { if (result.confirm) uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.share { color: #0868f7; font-size: 25rpx; padding: 16rpx 0 16rpx 16rpx; }
|
||||
.product-content { padding: 24rpx 28rpx 60rpx; }
|
||||
.bank-head { padding: 26rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e6edf6; flex-direction: row; align-items: center; }
|
||||
.bank-mark { width: 80rpx; height: 80rpx; border-radius: 24rpx; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||
.bank-initial { color: #fff; font-size: 36rpx; font-weight: 700; }
|
||||
.bank-copy { flex: 1; }
|
||||
.product-name { font-size: 30rpx; font-weight: 700; color: #20324d; }
|
||||
.product-tags { color: #75859b; font-size: 22rpx; margin-top: 10rpx; }
|
||||
.product-metrics { margin-top: 20rpx; padding: 30rpx 12rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e6edf6; flex-direction: row; }
|
||||
.metric { flex: 1; align-items: center; border-right: 1rpx solid #edf1f6; }
|
||||
.last-metric { border-right: 0; }
|
||||
.metric-label { color: #75859b; font-size: 21rpx; }
|
||||
.amount, .rate, .term { margin-top: 14rpx; font-size: 36rpx; line-height: 46rpx; font-weight: 800; color: #20324d; }
|
||||
.rate { color: #0868f7; }
|
||||
.term { font-size: 27rpx; }
|
||||
.detail-section { margin-top: 20rpx; padding: 26rpx; background-color: #fff; border-radius: 18rpx; border: 1rpx solid #e6edf6; }
|
||||
.advantage-grid { margin-top: 24rpx; flex-direction: row; flex-wrap: wrap; }
|
||||
.advantage { width: 50%; flex-direction: row; align-items: center; margin-bottom: 16rpx; }
|
||||
.advantage-label { margin-left: 12rpx; font-size: 25rpx; color: #435168; }
|
||||
.condition { min-height: 78rpx; padding: 14rpx 0; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||
.condition-label { color: #748399; font-size: 24rpx; flex-shrink: 0; margin-right: 20rpx; }
|
||||
.condition-value { flex: 1; text-align: right; color: #273b54; font-size: 24rpx; line-height: 36rpx; }
|
||||
.material { margin-top: 18rpx; font-size: 24rpx; line-height: 38rpx; color: #53667f; }
|
||||
.product-note { margin: 22rpx 6rpx 0; color: #7a899e; font-size: 21rpx; line-height: 34rpx; }
|
||||
.apply-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 18rpx 28rpx; background-color: #fff; border-top: 1rpx solid #e7edf6; }
|
||||
.apply-button { margin: 0; }
|
||||
.state { padding: 80rpx 24rpx; align-items: center; }
|
||||
.state-title { font-size: 30rpx; color: #20324d; font-weight: 600; margin-bottom: 14rpx; }
|
||||
.state-text { font-size: 25rpx; line-height: 40rpx; text-align: center; color: #75859b; }
|
||||
.retry-button { margin-top: 24rpx; color: #0868f7; background-color: #e8f2ff; font-size: 25rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader title="融资需求报告" />
|
||||
<scroll-view scroll-y class="report-scroll" :style="contentStyle(96, false)">
|
||||
<view class="report-content">
|
||||
<view class="score-panel"><text class="score-label">综合评分 · 示例报告</text><view class="score-row"><text class="big-score">85</text><text class="score-unit">分</text><text class="score-badge">良好</text></view><text class="score-caption">从五个维度了解融资资质</text></view>
|
||||
<view class="radar-panel"><image class="radar-image" src="/packages/finance/static/finance-radar.png" mode="aspectFit"></image><text class="radar-label top">还款能力</text><text class="radar-label left">收入稳定</text><text class="radar-label right">信用状况</text><text class="radar-label bottom-left">资产条件</text><text class="radar-label bottom-right">负债情况</text></view>
|
||||
<view class="conclusion"><text class="section-title">评估结论</text><text class="conclusion-text">示例资质整体良好,可进一步比较信用融资产品的额度、期限与还款安排。</text><view class="suggest-row"><text class="suggest-label">建议期限</text><text class="suggest-value">6–36个月</text></view><text class="report-note">本报告为体验示例,正式评估以授权资料和机构审核为准。</text></view>
|
||||
<button class="primary-button" @click="viewMatches">查看匹配方案</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
const { contentStyle, pageStyle } = usePageLayout()
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
const viewMatches = () => { uni.navigateTo({ url: '/packages/finance/matches/matches' }) }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.report-scroll { }
|
||||
.report-content { padding: 28rpx 30rpx 80rpx; }
|
||||
.score-panel { background-color: #fff; border-radius: 18rpx; padding: 30rpx; border: 1rpx solid #e7edf6; }
|
||||
.score-label { font-size: 24rpx; color: #6f7d91; }
|
||||
.score-row { flex-direction: row; align-items: flex-end; margin-top: 8rpx; }
|
||||
.big-score { color: #102557; font-size: 82rpx; line-height: 90rpx; font-weight: 900; }
|
||||
.score-unit { font-size: 28rpx; margin: 0 16rpx 14rpx 5rpx; }
|
||||
.score-badge { padding: 8rpx 17rpx; border-radius: 10rpx; background-color: #e5f8ee; color: #0ba868; font-size: 22rpx; margin-bottom: 15rpx; }
|
||||
.score-caption { color: #8793a6; font-size: 22rpx; margin-top: 6rpx; }
|
||||
.radar-panel { height: 370rpx; margin-top: 20rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
||||
.radar-image { width: 340rpx; height: 310rpx; }
|
||||
.radar-core { position: absolute; width: 92rpx; height: 92rpx; left: 50%; top: 50%; margin-left: -46rpx; margin-top: -46rpx; border-radius: 50%; background-color: #0868f7; align-items: center; justify-content: center; color: white; font-size: 34rpx; font-weight: 800; box-shadow: 0 0 32rpx rgba(8,104,247,.28); }
|
||||
.radar-label { position: absolute; color: #58667b; font-size: 21rpx; }
|
||||
.top { top: 28rpx; }
|
||||
.left { left: 27rpx; top: 162rpx; }
|
||||
.right { right: 27rpx; top: 162rpx; }
|
||||
.bottom-left { left: 95rpx; bottom: 32rpx; }
|
||||
.bottom-right { right: 86rpx; bottom: 32rpx; }
|
||||
.conclusion { margin: 24rpx 0 32rpx; background-color: #fff; border-radius: 18rpx; padding: 28rpx; border: 1rpx solid #e7edf6; }
|
||||
.conclusion-text { margin-top: 18rpx; font-size: 24rpx; color: #59677b; line-height: 42rpx; }
|
||||
.suggest-row { margin-top: 24rpx; padding-top: 22rpx; border-top: 1rpx solid #edf1f6; flex-direction: row; justify-content: space-between; font-size: 25rpx; }
|
||||
.suggest-value { color: #0868f7; font-size: 27rpx; font-weight: 700; }
|
||||
.suggest-label { color: #53667f; font-size: 25rpx; }
|
||||
.report-note { font-size: 21rpx; line-height: 34rpx; color: #78889e; margin-top: 20rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
||||
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
|
||||
export { API_BASE_URL } from '@/services/api.uts'
|
||||
|
||||
export async function submitAssessment(payload: UTSJSONObject): Promise<string> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(650)
|
||||
uni.setStorageSync('latestAssessment', payload)
|
||||
return 'ASSESSMENT-DEMO-001'
|
||||
}
|
||||
const result = await requestJson('/assessments', 'POST', payload)
|
||||
const id = result['id'] as string | null
|
||||
if (id == null || id.length == 0) throw new Error('评估提交成功但未返回编号')
|
||||
return id
|
||||
}
|
||||
|
||||
export async function fetchMatches(): Promise<FinanceProduct[]> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(320)
|
||||
return demoProducts.slice()
|
||||
}
|
||||
const result = await requestJson('/matches', 'GET', new UTSJSONObject())
|
||||
const items = result['items'] as UTSJSONObject[] | null
|
||||
return items == null ? [] : items.map((item: UTSJSONObject): FinanceProduct => parseProduct(item))
|
||||
}
|
||||
|
||||
export async function fetchProduct(id: string): Promise<FinanceProduct | null> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(180)
|
||||
const products = demoProducts.filter((item: FinanceProduct): boolean => item.id == id)
|
||||
return products.length > 0 ? products[0] : null
|
||||
}
|
||||
const result = await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject())
|
||||
return parseProduct(result)
|
||||
}
|
||||
|
||||
export async function sendAssistantMessage(message: string): Promise<string> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(600)
|
||||
return '结合您的评估结果,建议优先选择年化利率较低、可随借随还的产品,并重点确认提前还款规则。'
|
||||
}
|
||||
const result = await requestJson('/assistant/messages', 'POST', { message })
|
||||
const reply = result['reply'] as string | null
|
||||
if (reply == null || reply.length == 0) throw new Error('助手暂时没有返回内容')
|
||||
return reply
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export type FinanceProduct = {
|
||||
id: string, bank: string, initials: string, color: string,
|
||||
amount: number, rate: number, term: string, score: number,
|
||||
advantages: string[], age: string, work: string, credit: string, materials: string
|
||||
}
|
||||
|
||||
// 列表和详情共用产品标识;正式数据由 API 适配层提供。
|
||||
export const demoProducts: FinanceProduct[] = [
|
||||
{ id: '1', bank: '招商银行·闪电贷', initials: '招', color: '#c93547', amount: 50, rate: 3.85, term: '12–36个月', score: 96, advantages: ['线上申请', '无需抵押', '额度循环', '还款灵活'], age: '22–55周岁', work: '现单位工作满6个月', credit: '近两年无严重逾期', materials: '身份证 · 银行卡 · 收入证明' },
|
||||
{ id: '2', bank: '平安银行·新一贷', initials: '平', color: '#ed7924', amount: 30, rate: 4.20, term: '12–60个月', score: 91, advantages: ['信用融资', '期限可选', '在线预审', '无需担保'], age: '23–55周岁', work: '有稳定工作或经营收入', credit: '征信记录良好', materials: '身份证 · 收入证明 · 居住证明' },
|
||||
{ id: '3', bank: '建设银行·快e贷', initials: '建', color: '#2869b7', amount: 20, rate: 4.35, term: '12–36个月', score: 88, advantages: ['线上办理', '便捷用款', '信用授信', '支持提前还款'], age: '22–60周岁', work: '有稳定收入来源', credit: '符合机构信用要求', materials: '身份证 · 银行卡 · 资产或收入证明' }
|
||||
]
|
||||
|
||||
export function parseProduct(item: UTSJSONObject): FinanceProduct {
|
||||
return {
|
||||
id: item['id'] as string ?? '', bank: item['bank'] as string ?? '融资产品',
|
||||
initials: item['initials'] as string ?? '融', color: item['color'] as string ?? '#2869b7',
|
||||
amount: item['amount'] as number ?? 0, rate: item['rate'] as number ?? 0,
|
||||
term: item['term'] as string ?? '以审批为准', score: item['score'] as number ?? 0,
|
||||
advantages: item['advantages'] as string[] ?? [], age: item['age'] as string ?? '以机构要求为准',
|
||||
work: item['work'] as string ?? '以机构要求为准', credit: item['credit'] as string ?? '以机构要求为准',
|
||||
materials: item['materials'] as string ?? '以机构要求为准'
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Reference in New Issue
Block a user