feat: 重构评估流程为内存状态管理并接入真实提交与报告接口
本次重构将评估草稿改为内存状态管理,移除本地存储草稿,新增评估流程内存状态模块;接入真实的账户资料获取、头像上传接口,新增账户缓存清理逻辑;重构评估提交逻辑,接入真实提交接口并返回评估ID;新增分析页和报告页的真实状态查询、报告获取功能;更新README文档,记录开发进度和接口状态。
This commit is contained in:
@@ -1,65 +1,117 @@
|
||||
<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>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader :title="selectedId.length > 0 ? '评估记录详情' : '我的融资档案'" :customBack="selectedId.length > 0" @back="backToList" />
|
||||
<scroll-view scroll-y :style="contentStyle(0, false)">
|
||||
<view class="archive-content">
|
||||
<text class="archive-title">{{ selectedId.length > 0 ? '已提交的评估资料' : '已提交的评估记录' }}</text>
|
||||
<text class="archive-note">提交后的资料不可修改;再次评估请新建记录。未提交的填写内容不会保存。</text>
|
||||
<view v-if="selectedId.length == 0" class="start-actions">
|
||||
<button class="primary-button start-button" @click="start('personal')">个人评估</button>
|
||||
<button class="primary-button start-button" @click="start('enterprise')">企业评估</button>
|
||||
</view>
|
||||
<view v-if="error.length > 0" class="notice"><text>{{ error }}</text><text class="action" @click="refresh">重试加载</text></view>
|
||||
<text v-if="loading" class="archive-note">正在加载…</text>
|
||||
<template v-if="selectedId.length > 0 && detail != null">
|
||||
<text class="record-title">{{ detail!.record.type == 'personal' ? '个人' : '企业' }}评估 · #{{ detail!.record.id }}</text>
|
||||
<text class="record-status">{{ detail!.record.statusLabel }}</text>
|
||||
<text class="archive-note">{{ detail!.record.message }}</text>
|
||||
<text class="record-time">提交时间:{{ detail!.record.submittedAt }}</text>
|
||||
<text class="action" @click="openAnalysis">{{ detail!.record.status == 'succeeded' ? '查看分析报告' : '查看分析状态 / 重试' }}</text>
|
||||
<view class="detail-row" v-for="field in detail!.fields" :key="field.label"><text class="field-label">{{ field.label }}</text><text class="field-value">{{ field.value }}</text></view>
|
||||
<text class="action" @click="backToList">返回记录列表</text>
|
||||
</template>
|
||||
<template v-if="selectedId.length == 0">
|
||||
<text v-if="!loading && error.length == 0 && records.length == 0" class="empty-state">暂无已提交的评估记录</text>
|
||||
<view class="record-card" v-for="item in records" :key="item.id" @click="openRecord(item.id)">
|
||||
<text class="record-title">{{ item.type == 'personal' ? '个人' : '企业' }}评估 · #{{ item.id }}</text>
|
||||
<text class="record-status">{{ item.statusLabel }}</text>
|
||||
<text class="record-time">{{ item.city }} · {{ item.submittedAt }}</text>
|
||||
<text class="action">查看已提交资料</text>
|
||||
</view>
|
||||
<text v-if="hasMore && !loading" class="action" @click="loadMore">加载更多</text>
|
||||
<text v-if="records.length > 0 && !hasMore && !loading" class="archive-note">已显示全部记录</text>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { setUserStorage } from '@/services/storage.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; setUserStorage('assessmentActiveType', next); refresh() }
|
||||
const edit = (step: number) => { setUserStorage('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)
|
||||
}
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
|
||||
import { AssessmentRecord, AssessmentDetail, fetchAssessments, fetchAssessmentDetail } from '@/packages/finance/services/records.uts'
|
||||
const { pageStyle, contentStyle } = usePageLayout()
|
||||
const records = ref<AssessmentRecord[]>([])
|
||||
const detail = ref<AssessmentDetail | null>(null)
|
||||
const selectedId = ref(''); const loading = ref(false); const error = ref(''); const hasMore = ref(false)
|
||||
let cursor = ''; let generation = 0; let visible = false
|
||||
const sameOwner = (token: string): boolean => { const session = getSession(); return session != null && session['token'] == token }
|
||||
const refresh = async () => {
|
||||
const session = getSession()
|
||||
if (session == null) { records.value = []; detail.value = null; uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||
const owner = session['token'] as string
|
||||
const request = ++generation
|
||||
loading.value = true; error.value = ''
|
||||
try {
|
||||
if (selectedId.value.length > 0) {
|
||||
const result = await fetchAssessmentDetail(selectedId.value)
|
||||
if (visible && request == generation && sameOwner(owner)) detail.value = result
|
||||
} else {
|
||||
const result = await fetchAssessments('')
|
||||
if (visible && request == generation && sameOwner(owner)) { records.value = result.items; hasMore.value = result.hasMore; cursor = result.nextCursor }
|
||||
}
|
||||
} catch (err) { if (visible && request == generation) error.value = err instanceof Error ? err.message : '加载失败,请重试' }
|
||||
finally { if (request == generation) loading.value = false }
|
||||
}
|
||||
const loadMore = async () => {
|
||||
if (loading.value || !hasMore.value) return
|
||||
const session = getSession()
|
||||
if (session == null) { refresh(); return }
|
||||
const owner = session['token'] as string
|
||||
const request = ++generation
|
||||
loading.value = true; error.value = ''
|
||||
try {
|
||||
const result = await fetchAssessments(cursor)
|
||||
if (visible && request == generation && sameOwner(owner)) {
|
||||
const incoming = result.items.filter((item: AssessmentRecord): boolean => !records.value.some((old: AssessmentRecord): boolean => old.id == item.id))
|
||||
records.value = records.value.concat(incoming); hasMore.value = result.hasMore; cursor = result.nextCursor
|
||||
}
|
||||
} catch (err) { if (visible && request == generation) error.value = err instanceof Error ? err.message : '加载失败,请重试' }
|
||||
finally { if (request == generation) loading.value = false }
|
||||
}
|
||||
const start = (type: string) => { clearAssessmentFlow(); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type }) }
|
||||
const openRecord = (id: string) => { uni.navigateTo({ url: '/packages/finance/archive/archive?assessmentId=' + encodeURIComponent(id) }) }
|
||||
const openAnalysis = () => {
|
||||
const page = detail.value?.record.status == 'succeeded' ? 'report/report' : 'analyzing/analyzing'
|
||||
uni.navigateTo({ url: '/packages/finance/' + page + '?assessmentId=' + encodeURIComponent(selectedId.value) })
|
||||
}
|
||||
const backToList = () => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/archive/archive') uni.navigateBack()
|
||||
else uni.redirectTo({ url: '/packages/finance/archive/archive' })
|
||||
}
|
||||
onBackPress((event): boolean => { if (event.from == 'navigateBack' || selectedId.value.length == 0) return false; backToList(); return true })
|
||||
onLoad((options) => { if (options != null) selectedId.value = options['assessmentId'] ?? '' })
|
||||
onShow(() => { visible = true; refresh() })
|
||||
// 离开时清空敏感详情;迟到的请求不能把其他账户的数据写回页面。
|
||||
onHide(() => { visible = false; generation++; records.value = []; detail.value = null })
|
||||
onUnload(() => { visible = false; generation++ })
|
||||
</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; }
|
||||
.archive-content { margin: 28rpx; padding: 30rpx; border-radius: 18rpx; background-color: #fff; }
|
||||
.archive-title { font-size: 32rpx; font-weight: 600; color: #23344e; }
|
||||
.archive-note { margin: 24rpx 0 30rpx; color: #738198; font-size: 26rpx; line-height: 42rpx; }
|
||||
.start-actions { flex-direction: row; margin-bottom: 20rpx; }
|
||||
.start-button { flex: 1; margin: 6rpx; font-size: 27rpx; }
|
||||
.record-card { padding: 28rpx 0; border-bottom: 1rpx solid #edf1f6; }
|
||||
.record-title { font-size: 30rpx; font-weight: 600; color: #23344e; }
|
||||
.record-status { margin-top: 16rpx; color: #0868f7; font-size: 27rpx; }
|
||||
.record-time { margin-top: 14rpx; color: #738198; font-size: 23rpx; line-height: 36rpx; }
|
||||
.action { color: #0868f7; font-size: 27rpx; padding: 24rpx 0; }
|
||||
.notice { color: #ad432d; font-size: 27rpx; line-height: 40rpx; }
|
||||
.empty-state { padding: 50rpx 0; color: #738198; font-size: 28rpx; }
|
||||
.detail-row { flex-direction: row; padding: 24rpx 0; border-bottom: 1rpx solid #edf1f6; }
|
||||
.field-label { width: 210rpx; font-size: 26rpx; color: #738198; }
|
||||
.field-value { flex: 1; text-align: right; font-size: 27rpx; color: #23344e; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user