feat: 优化AI助手及报告对比界面,完成金额精度专项验证

- 调整环境配置切换至测试服
- 实现AI助手上下文折叠展开功能,优化界面布局
- 新增报告对比首屏短参数展示,简化信息呈现
- 调整报告页布局,优化导出组件为紧凑模式
- 完成原型08金额精度三轮真实专项验证,新增金额守卫逻辑
- 更新文档记录部署、数据库切换及验收进展
This commit is contained in:
hujun
2026-09-29 17:27:25 +08:00
parent 79feb03b19
commit 542c107317
10 changed files with 394 additions and 44 deletions
+21 -15
View File
@@ -4,12 +4,14 @@
<view class="tabs"><view v-for="item in tabs" :key="item.id" :class="tab == item.id ? 'tab active' : 'tab'" @click="switchTab(item.id)"><text :class="tab == item.id ? 'tab-label selected' : 'tab-label'">{{ item.label }}</text></view></view>
<scroll-view scroll-y :style="contentStyle(186, false)">
<view class="report-content">
<text class="eyebrow">评估记录 #{{ assessmentId.length > 0 ? assessmentId : '待选择' }}</text>
<!-- 离页清空报告时只切换可见性,避免在页面销毁过程中重新挂载文本节点。 -->
<text v-show="report == null" class="eyebrow">评估记录 #{{ assessmentId.length > 0 ? assessmentId : '待选择' }}</text>
<text v-if="loading" class="body-text">正在读取已保存报告…</text>
<view v-if="error.length > 0" class="panel"><text class="section-title">报告暂不可读取</text><text class="body-text">{{ error }}</text><button v-if="routeError.length == 0" class="secondary" @click="loadReport">重新加载报告</button><text class="action" @click="history">前往融资档案</text></view>
<template v-if="report != null">
<ReportExport :assessmentId="assessmentId" @authrequired="login" @sessionchanged="loadReport" />
<text class="report-note">生成于 {{ report!.generatedAt }} · 用户自填资料分析</text>
<ReportExport :assessmentId="assessmentId" :compact="true" @authrequired="login" @sessionchanged="loadReport" />
<text class="metadata-toggle" @click="metadataOpen = !metadataOpen">用户自填资料分析 · 报告信息{{ metadataOpen ? '收起' : '展开' }}</text>
<view v-if="metadataOpen" class="metadata"><text class="report-note">生成于 {{ report!.generatedAt }}</text><text class="report-note">报告已保存至融资档案。导出不含身份信息,请妥善保管下载文件。</text></view>
<view v-if="report!.version == 1" class="legacy"><text class="report-note">这是一份旧版自填报告,当时未采集融资需求、未计算完整度与准备状态,也未执行产品匹配。历史内容保持原样。</text></view>
<template v-if="tab == 'overview'">
<view class="hero">
@@ -43,9 +45,9 @@
</template>
</template>
<template v-else>
<text class="headline">让下一次准备更充分</text><text class="intro">依据本次报告和产品条件整理。补充资料后需重新提交新评估,改善不保证获批。</text>
<text class="headline">让下一次准备更充分</text><text class="intro">依据本次报告整理 · 改善不保证获批</text>
<view v-if="report!.version == 2" class="panel"><text class="section-title">优先补充与核实</text><text v-if="missingItems.length == 0" class="report-note">{{ report!.completenessPercent == null ? '本次未评定缺失字段,不能据此认为资料齐全。' : '本次字段清单没有待补充或格式待核实项,仍需准备核验材料。' }}</text><view class="fact-row" v-for="item in missingItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text></view>
<view class="panel"><text class="section-title">已保存的AI改善建议</text><text v-if="report!.suggestions.length == 0" class="report-note">暂无有依据的额外建议,不代表已经完成核验。</text><text class="body-text" v-for="(item, index) in report!.suggestions" :key="index">{{ item }}</text><text class="source-note">保留报告生成时的文本与依据,不重新调用模型。</text></view>
<view class="panel"><text class="section-title">已保存的AI改善建议</text><text v-if="report!.suggestions.length == 0" class="report-note">暂无有依据的额外建议,不代表已经完成核验。</text><text class="body-text" v-for="(item, index) in report!.suggestions" :key="index">{{ item }}</text><text class="source-note">保留报告生成时的文本与依据;补充资料后需发起新评估。</text></view>
<view v-if="report!.matching.status != 'completed'" class="panel"><text class="section-title">产品条件提示</text><text class="report-note">本次未执行产品匹配,没有可关联的产品改善依据。</text></view>
<template v-else><view v-for="group in adviceGroups" :key="group.id" class="panel"><text class="section-title">{{ group.label }}</text><text class="report-note">仅展示本组前3项产品的已有依据,可进入完整列表查看更多。</text><text v-if="group.loading" class="body-text">正在读取产品依据…</text><template v-if="group.error.length > 0"><text class="body-text">{{ group.error }}</text><button class="secondary" @click="loadAdvice(group.id)">重试{{ group.label }}</button></template><text v-if="!group.loading && group.error.length == 0 && group.items.length == 0" class="report-note">本组暂无产品依据。</text><view v-for="item in group.items" :key="item.id" class="advice-product"><text class="product-name" @click="openProduct(item.id)">{{ item.product.name }} ›</text><text class="source-note">{{ item.stateLabel }} · 评估时版本 v{{ item.product.version }}</text><view v-for="reason in adviceReasons(item)" :key="reason.id"><text class="body-text">{{ reason.text }}</text><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }}</text></view><text class="action" @click="allMatches(group.id)">查看全部{{ group.label }} ›</text></view></template>
</template>
@@ -73,13 +75,15 @@
const tabs: ReportTab[] = [{ id: 'overview', label: '评估概况' }, { id: 'matches', label: '匹配结果' }, { id: 'improvements', label: '改善建议' }]
const assessmentId = ref(''); const tab = ref('overview'); const loading = ref(false); const error = ref(''); const routeError = ref('')
const report = ref<AnalysisReport | null>(null); const factsOpen = ref(false); const completenessOpen = ref(false)
// 日期与导出说明按需展开,首屏优先呈现准备状态和当前栏目的内容。
const metadataOpen = ref(false)
const candidates = ref<MatchView[]>([]); const candidateLoading = ref(false); const candidateError = ref('')
const adviceGroups = ref<AdviceGroup[]>([])
let generation = 0; let visible = false
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeReport=1&assessmentId=' + assessmentId.value + '&tab=' + tab.value }) }
const missingItems = computed((): AssessmentField[] => report.value == null ? [] : report.value!.completenessItems.filter((item: AssessmentField): boolean => ['待补充','需核实'].includes(item.value)))
const adviceReasons = (item: MatchView): MatchReason[] => item.state == 'ineligible' ? item.blocking.concat(item.cautions) : item.cautions
const reset = () => { report.value = null; candidates.value = []; adviceGroups.value = []; candidateLoading.value = false; candidateError.value = ''; factsOpen.value = false; completenessOpen.value = false }
const reset = () => { report.value = null; candidates.value = []; adviceGroups.value = []; candidateLoading.value = false; candidateError.value = ''; factsOpen.value = false; completenessOpen.value = false; metadataOpen.value = false }
const current = (request: number, token: string): boolean => visible && request == generation && getSession()?.getString('token') == token
const sessionChanged = (request: number, token: string): boolean => {
if (!visible || request != generation) return true
@@ -168,16 +172,18 @@
.selected { color: #0868f7; font-weight: 700; }
.report-content { padding: 28rpx 28rpx 65rpx; }
.eyebrow { color: #778ba3; font-size: 22rpx; }
.headline { color: #1a3859; font-size: 36rpx; font-weight: 700; margin-top: 28rpx; }
.headline { color: #1a3859; font-size: 34rpx; font-weight: 700; margin-top: 22rpx; }
.intro { color: #73869d; font-size: 24rpx; line-height: 40rpx; margin-top: 14rpx; }
.hero { background-color: #155dd1; border-radius: 22rpx; padding: 32rpx; margin-top: 24rpx; }
.hero-label { color: #d8e8ff; font-size: 24rpx; }
.hero-title { color: #fff; font-size: 42rpx; font-weight: 700; line-height: 60rpx; margin-top: 14rpx; }
.hero-note { color: #d8e8ff; font-size: 22rpx; line-height: 36rpx; margin-top: 12rpx; }
.hero-divider { height: 1rpx; background-color: #4983dc; margin-top: 26rpx; margin-bottom: 24rpx; }
.percent { color: #fff; font-size: 38rpx; font-weight: 700; }
.progress-track { height: 12rpx; border-radius: 6rpx; background-color: #4983dc; margin-top: 18rpx; overflow: hidden; }
.progress-fill { height: 12rpx; background-color: #bde6ff; border-radius: 6rpx; }
.metadata-toggle { color: #78889e; font-size: 21rpx; line-height: 36rpx; padding-top: 8rpx; padding-bottom: 4rpx; }
.metadata { padding: 0 0 12rpx; }
.hero { background-color: #eef5ff; border: 1rpx solid #dfeafb; border-radius: 18rpx; padding: 26rpx; margin-top: 18rpx; }
.hero-label { color: #627c9c; font-size: 24rpx; }
.hero-title { color: #165cb9; font-size: 42rpx; font-weight: 700; line-height: 56rpx; margin-top: 8rpx; }
.hero-note { color: #7086a1; font-size: 22rpx; line-height: 34rpx; margin-top: 8rpx; }
.hero-divider { height: 1rpx; background-color: #d9e5f6; margin-top: 18rpx; margin-bottom: 16rpx; }
.percent { color: #285c9d; font-size: 30rpx; font-weight: 700; }
.progress-track { height: 8rpx; border-radius: 4rpx; background-color: #dbe7f8; margin-top: 12rpx; overflow: hidden; }
.progress-fill { height: 8rpx; background-color: #4c8beb; border-radius: 4rpx; }
.panel { margin-top: 24rpx; padding: 28rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; }
.panel-heading { flex-direction: row; justify-content: space-between; align-items: center; }
.section-title { color: #253f5e; font-size: 29rpx; font-weight: 700; line-height: 42rpx; }