Files
airz-uniapp/packages/finance/report/report.uvue
T
hujun 3b2a0139e4 feat: 支持评估版本隔离的报告、匹配及PDF导出功能
新增revisionId参数到路由、服务及导出接口,实现不同评估版本的隔离访问。删除已废弃的需求相关服务文件,移除取消采集的融资金额、期限和用途字段,更新相关页面提示文本。同时更新文档,说明评估版本、最新资料及产品展示规则的调整,支持前台正常业务名称显示,去除环境标签。
2026-09-30 19:11:44 +08:00

282 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page" :style="pageStyle()">
<view class="status-bar" :style="statusStyle(0)"></view>
<view class="report-header">
<view class="header-back" @click="back"><AppIcon name="chevron-right" :size="40" class="back-icon" /></view>
<view class="header-title-area" :style="reportTitleStyle()"><text class="header-title">我的融资报告</text></view>
<view v-if="report != null" class="header-export" :style="navigationActionStyle(96, 1)"><ReportExport :assessmentId="assessmentId" :revisionId="revisionId" :toolbar="true" @authrequired="login" @sessionchanged="loadReport" @statuschange="exportStatus" /></view>
</view>
<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(report == null ? 178 : 314, false)">
<view class="report-content" :style="bottomStyle(24)">
<!-- 离页清空报告时只切换可见性,避免在页面销毁过程中重新挂载文本节点。 -->
<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">
<text v-if="report!.isPreviousResult" class="revision-notice">当前查看第 {{ report!.revisionNo }} 版结果。最新提交{{ report!.currentStatus == 'processing' ? '正在评估,成功后更新结果。' : (report!.currentStatus == 'failed' ? '评估失败,已保留上次成功结果。' : '已有新版本,可从档案查看。') }}</text>
<text v-show="downloadStatus.length > 0" class="download-status">{{ downloadStatus }}</text>
<view v-if="report!.version == 1" class="legacy"><text class="report-note">这是一份旧版自填报告,当时未采集融资需求、未计算完整度与准备状态,也未执行产品匹配。历史内容保持原样。</text></view>
<template v-if="tab == 'overview'">
<view :class="report!.readinessLabel == '可进入资料核对' ? 'readiness-band ready' : 'readiness-band attention'"><text class="readiness-label">融资准备状态</text><text class="readiness-title">{{ report!.readinessLabel }}</text></view>
<text class="summary">{{ report!.summary }}</text>
<view class="priority-section"><text class="section-title">{{ priorityItems.length > 0 ? '需重点完善的 ' + priorityItems.slice(0, 3).length.toString() + ' 项' : '资料核验与准备' }}</text><view v-for="(item, index) in priorityItems.slice(0, 3)" :key="item" class="priority-row"><text class="priority-number">{{ index + 1 }}</text><text class="priority-text">{{ item }}</text></view><text v-if="priorityItems.length == 0" class="report-note">本次暂无额外改善项,请按机构要求核实资料。</text><text v-if="priorityItems.length > 3" class="more-advice" @click="switchTab('improvements')">查看全部 {{ priorityItems.length }} 项改善建议 ›</text></view>
<!-- 首屏保持原型的状态、摘要和编号改善项;完整度与所有冻结依据按需展开。 -->
<view class="details-toggle" @click="detailsOpen = !detailsOpen"><text class="details-label">查看完整报告</text><text class="details-action">{{ detailsOpen ? '收起' : '展开' }}</text></view>
<template v-if="detailsOpen">
<view v-if="report!.suggestions.length > 0" class="panel"><text class="section-title">改善建议与依据</text><text v-for="item in report!.suggestions" :key="item" class="body-text">{{ item }}</text></view>
<view class="panel"><view class="panel-heading"><text class="section-title">资料完整度</text><text class="percent">{{ report!.completenessPercent == null ? '—' : report!.completenessPercent!.toString() + '%' }}</text></view><view v-if="report!.completenessPercent != null" class="progress-track"><view class="progress-fill" :style="'width:' + report!.completenessPercent!.toString() + '%;'"></view></view><text class="report-note">{{ report!.completenessText }}</text><text class="report-note">完整度不代表信用评分、资料真实性或获批概率。</text></view>
<view v-if="report!.demand.length > 0" class="panel"><text class="section-title">本次资金需求</text><view class="fact-row" v-for="fact in report!.demand" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view></view>
<view v-if="report!.version == 2" class="panel"><text class="section-title">准备状态依据</text><text v-if="report!.readinessBasis.length == 0" class="report-note">本次没有可展示的准备状态依据。</text><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text><text class="report-note">资料未经核验;完整度达到100%仍需核对机构要求。</text></view>
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '主要优势' : '资料解读' }}</text><text v-if="report!.observations.length == 0" class="report-note">暂无有充分依据的优势说明。</text><text class="body-text" v-for="(item, index) in report!.observations" :key="index">{{ item }}</text></view>
<view class="panel"><view class="panel-heading" @click="factsOpen = !factsOpen"><text class="section-title">本次提交的事实</text><text class="toggle">{{ factsOpen ? '收起' : '展开' }}</text></view><text class="report-note">来源:用户自填,未经外部核验;未填写不等于零。</text><template v-if="factsOpen"><view class="fact-row" v-for="fact in report!.facts" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view><text v-if="report!.facts.length == 0" class="report-note">暂无可展示的事实。</text></template></view>
<view v-if="report!.version == 2" class="panel"><view class="panel-heading" @click="completenessOpen = !completenessOpen"><text class="section-title">逐项填写情况</text><text class="toggle">{{ completenessOpen ? '收起' : '展开' }}</text></view><template v-if="completenessOpen"><view class="fact-row" v-for="item in report!.completenessItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text v-if="report!.completenessItems.length == 0" class="report-note">本次未评定逐项完整度。</text></template></view>
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '风险与核验边界' : '待补充信息' }}</text><text class="body-text" v-for="(item, index) in report!.warnings" :key="'system-' + index">{{ item }}</text><text class="body-text" v-for="(item, index) in report!.dataGaps" :key="index">{{ item }}</text><text v-if="report!.warnings.length == 0 && report!.dataGaps.length == 0" class="report-note">本次暂无额外提示,不代表资料已核验或无风险。</text></view>
</template>
</template>
<template v-else-if="tab == 'matches'">
<text class="headline">本次需求的可选方向</text><text class="intro">以下为评估时的条件判断。展示顺序不代表优劣,也不代表机构审批。</text>
<view v-if="report!.matching.status == 'completed'" class="stats"><view class="stat" @click="allMatches('eligible')"><text class="stat-number">{{ report!.matching.eligible }}</text><text class="stat-label">符合已知条件</text></view><view class="stat" @click="allMatches('ineligible')"><text class="stat-number">{{ report!.matching.ineligible }}</text><text class="stat-label">条件不符</text></view><view class="stat" @click="allMatches('unknown')"><text class="stat-number">{{ report!.matching.unknown }}</text><text class="stat-label">待核实</text></view></view>
<text class="report-note">{{ report!.matchingMessage }}</text>
<view v-if="report!.matching.status != 'completed'" class="panel"><text class="section-title">本次未执行产品匹配</text><text class="body-text">这与“已匹配但没有候选”不同。历史报告不会自动补算;需要采用新资料与当前产品条件时,请重新评估。</text></view>
<template v-else>
<text v-if="candidateLoading" class="body-text">正在读取历史候选…</text>
<view v-if="candidateError.length > 0" class="panel"><text class="section-title">候选读取失败</text><text class="body-text">{{ candidateError }}</text><button class="secondary" @click="loadCandidates">重试候选</button></view>
<view v-if="!candidateLoading && candidateError.length == 0 && candidates.length == 0" class="panel"><text class="section-title">暂无符合已知条件的产品</text><text class="body-text">本次已执行匹配,可查看条件不符或待核实原因。没有候选不代表永久无法融资。</text></view>
<view v-for="item in candidates" :key="item.id" class="product-card" @click="openProduct(item.id)"><view class="panel-heading"><text class="product-name">{{ item.product.name }}</text><text class="version">v{{ item.product.version }}</text></view><text class="report-note">{{ item.product.bank }} · {{ item.stateLabel }}</text><text class="amount">{{ item.product.amount }}</text><view class="metrics"><text class="metric">年化 {{ item.product.rate }}</text><text class="metric">{{ item.product.term }}</text></view><text class="reason" v-for="reason in item.positive.slice(0, 2)" :key="reason.id">{{ reason.text }}</text><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }};仍保留历史参数。</text><text class="action">查看匹配依据与详情 ›</text></view>
<text v-if="candidates.length > 0" class="report-note">摘要最多展示3项符合已知条件的产品,全部分组见完整匹配列表。</text>
<button class="primary" @click="allMatches('eligible')">查看全部匹配结果</button><view class="links"><text class="action" @click="allMatches('ineligible')">查看不符原因</text><text class="action" @click="allMatches('unknown')">查看待核实项</text></view>
</template>
</template>
<template v-else>
<text class="headline">改善建议</text><text class="intro">依据本次报告整理 · 改善不保证获批</text>
<view class="priority-section"><text class="section-title">{{ priorityItems.length > 0 ? '需重点完善的 ' + priorityItems.length.toString() + ' 项' : '资料核验与准备' }}</text><view v-for="(item, index) in priorityItems" :key="item" class="priority-row"><text class="priority-number">{{ index + 1 }}</text><text class="priority-text">{{ item }}</text></view><text v-if="priorityItems.length == 0" class="report-note">本次暂无额外改善项,请按机构要求核实资料。</text></view>
<view v-if="report!.suggestions.length > 0" class="panel"><view class="panel-heading" @click="detailsOpen = !detailsOpen"><text class="section-title">建议依据</text><text class="toggle">{{ detailsOpen ? '收起' : '展开' }}</text></view><template v-if="detailsOpen"><text v-for="item in report!.suggestions" :key="item" class="body-text">{{ item }}</text></template></view>
<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 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>
<view class="report-links"><text class="report-link" @click="nextStep">下一步行动 ›</text><text class="report-link" @click="consult">咨询AI ›</text></view>
<view class="details-toggle" @click="metadataOpen = !metadataOpen"><text class="metadata-toggle">报告信息</text><text class="details-action">{{ metadataOpen ? '收起' : '展开' }}</text></view>
<view v-if="metadataOpen" class="metadata"><text class="report-note">评估 #{{ assessmentId }} · 第 {{ report!.revisionNo }} 版 · {{ report!.generatedAt }}</text><text class="report-note">分析来源:用户自填资料 · {{ report!.modelCode }}</text><text class="report-note">报告已保存至融资档案。导出不含身份信息,请妥善保管下载文件。</text><text class="disclaimer">{{ report!.disclaimer }}</text><view class="report-links"><text class="report-link" @click="start">发起新评估</text><text class="report-link" @click="history">历史评估</text></view></view>
<text v-show="!metadataOpen" class="disclaimer">自填资料未经核验,结果仅供参考,不代表授信或获批。</text>
</template>
</view>
</scroll-view>
<view v-if="report != null" class="report-footer" :style="bottomStyle(24)"><button class="reevaluate-button" @click="editAssessment">重新评估</button></view>
</view>
</template>
<script setup lang="uts">
import AppIcon from '@/components/AppIcon.uvue'
import ReportExport from '@/packages/finance/components/ReportExport.uvue'
import { usePageLayout } from '@/services/layout.uts'
import { getSession } from '@/services/api.uts'
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
import { AnalysisReport, AssessmentField, fetchAnalysisReport } from '@/packages/finance/services/records.uts'
import { MatchPage, MatchView, MatchReason, fetchMatchPage, validRecordId } from '@/packages/finance/services/matches.uts'
type ReportTab = { id: string, label: string }
type AdviceGroup = { id: string, label: string, loading: boolean, error: string, items: MatchView[] }
const { contentStyle, pageStyle, statusStyle, bottomStyle, navigationTitleStyle, navigationActionStyle } = usePageLayout()
const reportTitleStyle = (): string => {
// 小程序标题须同时避让PDF按钮和右侧胶囊,窄屏也不能覆盖操作入口。
// #ifdef MP-WEIXIN
const windowWidth = uni.getWindowInfo().windowWidth
const capsule = uni.getMenuButtonBoundingClientRect()
const reserved = capsule.left > 0 && capsule.left < windowWidth ? windowWidth - capsule.left + 8 : 104
return 'left:50px;right:' + (reserved + 40) + 'px;'
// #endif
// #ifndef MP-WEIXIN
return navigationTitleStyle()
// #endif
}
const tabs: ReportTab[] = [{ id: 'overview', label: '评估概况' }, { id: 'matches', label: '匹配结果' }, { id: 'improvements', label: '改善建议' }]
const revisionId = ref(''); 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 detailsOpen = ref(false); const downloadStatus = ref('')
const exportStatus = (message: string) => { downloadStatus.value = message }
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 + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&tab=' + tab.value }) }
const missingItems = computed((): AssessmentField[] => report.value == null ? [] : report.value!.completenessItems.filter((item: AssessmentField): boolean => ['待补充','需核实'].includes(item.value)))
const priorityItems = computed((): string[] => {
const value = report.value
if (value == null) return []
// 使用已保存建议及真实缺失项,不补造原型中的固定三条或融资评分。
const items = value.suggestionSummaries.concat(missingItems.value.map((item: AssessmentField): string => (item.value == '待补充' ? '补充' : '核实') + item.label))
return items.filter((item: string, index: number): boolean => item.trim().length > 0 && items.indexOf(item) == index)
})
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; metadataOpen.value = false; detailsOpen.value = false; downloadStatus.value = '' }
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
if (getSession()?.getString('token') == token) return false
generation++; reset(); loading.value = false
if (getSession() == null) login()
else error.value = '账户已切换,请重新加载本人报告。'
return true
}
const verifyMatches = (page: MatchPage) => {
const value = report.value
if (value == null || page.assessmentId != assessmentId.value || page.revisionId != value.revisionId || page.resultVersion != value.version || page.status != 'completed' || page.snapshotId != value.matching.snapshotId || page.ruleVersion != value.matching.ruleVersion || page.orderingVersion != value.matching.orderingVersion || page.eligible != value.matching.eligible || page.ineligible != value.matching.ineligible || page.unknown != value.matching.unknown) throw new Error('产品结果与本报告的冻结版本不一致,请重新加载报告。')
}
const loadCandidates = async () => {
if (candidateLoading.value || report.value?.matching.status != 'completed') return
const request = generation; const token = getSession()?.getString('token') ?? ''
candidateLoading.value = true; candidateError.value = ''; candidates.value = []
try {
const result = await fetchMatchPage(assessmentId.value, 'eligible', '', revisionId.value)
if (!current(request, token)) { sessionChanged(request, token); return }
verifyMatches(result); candidates.value = result.items
} catch (err) { if (!sessionChanged(request, token)) candidateError.value = err instanceof Error ? err.message : '候选读取失败' }
finally { if (request == generation) candidateLoading.value = false }
}
const loadAdvice = async (id: string) => {
const group = adviceGroups.value.find((item: AdviceGroup): boolean => item.id == id)
if (group == null || group.loading || report.value?.matching.status != 'completed') return
const request = generation; const token = getSession()?.getString('token') ?? ''
group.loading = true; group.error = ''; group.items = []
try {
const result = await fetchMatchPage(assessmentId.value, id, '', revisionId.value)
if (!current(request, token)) { sessionChanged(request, token); return }
verifyMatches(result); group.items = result.items
} catch (err) { if (!sessionChanged(request, token)) group.error = err instanceof Error ? err.message : '产品依据读取失败' }
finally { if (request == generation) group.loading = false }
}
const loadReport = async () => {
const request = ++generation; reset(); error.value = ''; loading.value = false
if (routeError.value.length > 0) { error.value = routeError.value; return }
const session = getSession()
if (session == null) { login(); return }
const token = session.getString('token') ?? ''; loading.value = true
try {
const result = await fetchAnalysisReport(assessmentId.value, revisionId.value)
if (!current(request, token)) { sessionChanged(request, token); return }
report.value = result; revisionId.value = result.revisionId
if (tab.value == 'matches') loadCandidates()
if (tab.value == 'improvements') {
adviceGroups.value = [{ id: 'ineligible', label: '不符条件', loading: false, error: '', items: [] }, { id: 'unknown', label: '待核实项', loading: false, error: '', items: [] }]
if (result.matching.status == 'completed') { loadAdvice('ineligible'); loadAdvice('unknown') }
}
} catch (err) { if (!sessionChanged(request, token)) error.value = err instanceof Error ? err.message : '报告加载失败,请重试' }
finally { if (request == generation) loading.value = false }
}
// 切栏替换当前路由,刷新恢复所选栏目;只读取已保存快照,不提交评估或触发模型。
const switchTab = (value: string) => { if (value != tab.value && routeError.value.length == 0) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&tab=' + value }) }
const editAssessment = () => { uni.navigateTo({ url: '/packages/finance/assessment/assessment?editId=' + assessmentId.value }) }
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, [], 'report', '', revisionId.value) }) }
const nextStep = () => { if (report.value != null) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&section=actions' }) }
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/archive/archive' }) }
const allMatches = (group: string) => { uni.navigateTo({ url: '/packages/finance/matches/matches?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&group=' + group }) }
const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + id + '&tab=explanation&from=report' }) }
const start = () => {
const token = getSession()?.getString('token') ?? ''; const request = generation
uni.showActionSheet({ itemList: ['个人评估','企业评估'], success: (result) => {
if (!current(request, token)) return
clearAssessmentFlow()
uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + (result.tapIndex == 1 ? 'enterprise' : 'personal') })
} })
}
onLoad((options) => {
revisionId.value = options?.['revisionId'] ?? '';
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError.value = '版本编号无效,请返回重新选择。'
assessmentId.value = options?.['assessmentId'] ?? ''; tab.value = options?.['tab'] ?? 'overview'
if (!validRecordId(assessmentId.value)) routeError.value = '请从融资档案选择有效的评估记录。'
if (!['overview','matches','improvements'].includes(tab.value)) routeError.value = '报告栏目参数无效,请从融资档案重新进入。'
})
onShow(() => { visible = true; loadReport() })
// 离页丢弃个人数据;旧请求不得覆盖新页面或新登录账户。
onHide(() => { visible = false; generation++; reset(); loading.value = false; error.value = '' })
onUnload(() => { visible = false; generation++; reset() })
</script>
<style>
.page { background-color: #fff; }
.status-bar { background-color: #fff; }
.report-header { height: 96rpx; padding: 0 28rpx; flex-direction: row; align-items: center; justify-content: space-between; position: relative; background-color: #fff; }
.header-back { width: 64rpx; height: 96rpx; justify-content: center; align-items: flex-start; }
.back-icon { transform: rotate(180deg); }
.header-title-area { position: absolute; left: 140rpx; right: 140rpx; top: 0; bottom: 0; align-items: center; justify-content: center; }
.header-title { color: #17243b; font-size: 32rpx; font-weight: 700; }
.header-export { align-items: center; justify-content: center; }
/* #ifdef MP-WEIXIN */
.header-title { font-size: 27rpx; }
/* #endif */
.tabs { height: 82rpx; flex-direction: row; background-color: #fff; padding: 0 28rpx; border-bottom: 1rpx solid #eff2f6; }
.tab { flex: 1; align-items: center; justify-content: center; }
.active { border-bottom: 4rpx solid #0868f7; }
.tab-label { font-size: 26rpx; color: #737e90; }
.selected { color: #0868f7; font-weight: 700; }
.revision-notice { font-size: 24rpx; line-height: 38rpx; color: #886934; padding: 18rpx 22rpx; background-color: #fff8eb; border-radius: 12rpx; margin-bottom: 12rpx; }
.download-status { font-size: 23rpx; line-height: 36rpx; color: #547292; padding-bottom: 18rpx; }
.report-content { padding: 28rpx 30rpx 24rpx; }
.eyebrow { color: #778ba3; font-size: 22rpx; }
.headline { color: #1a3859; font-size: 34rpx; font-weight: 700; margin-top: 22rpx; }
.intro { color: #73869d; font-size: 24rpx; line-height: 40rpx; margin-top: 14rpx; }
.metadata-toggle { color: #8893a2; font-size: 22rpx; line-height: 36rpx; }
.metadata { padding: 0 0 12rpx; }
.readiness-band { padding: 28rpx 30rpx; min-height: 154rpx; background-color: #eef9f3; }
.attention { background-color: #fff8ed; }
.readiness-label { color: #526272; font-size: 25rpx; line-height: 38rpx; }
.readiness-title { color: #17a46b; font-size: 46rpx; line-height: 64rpx; font-weight: 700; margin-top: 9rpx; }
.attention .readiness-title { color: #a57925; }
.summary { font-size: 26rpx; line-height: 44rpx; color: #4d5869; margin-top: 24rpx; }
.priority-section { margin-top: 30rpx; }
.priority-row { flex-direction: row; align-items: flex-start; padding: 24rpx 0; border-bottom: 1rpx solid #edf0f4; min-height: 88rpx; }
.priority-number { width: 36rpx; height: 36rpx; border-radius: 18rpx; background-color: #f66f4e; text-align: center; color: #fff; font-size: 22rpx; line-height: 36rpx; margin-right: 18rpx; margin-top: 2rpx; flex-shrink: 0; }
.priority-text { flex: 1; font-size: 26rpx; line-height: 40rpx; color: #485568; }
.more-advice { font-size: 23rpx; line-height: 38rpx; color: #0868f7; margin-top: 18rpx; }
.details-toggle { flex-direction: row; align-items: center; justify-content: space-between; padding: 22rpx 0; border-bottom: 1rpx solid #edf0f4; }
.details-label { font-size: 24rpx; color: #738094; }
.details-action { font-size: 23rpx; color: #8794a5; }
.report-links { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 22rpx; }
.report-link { font-size: 24rpx; line-height: 38rpx; color: #4e77aa; padding: 8rpx 0; }
.report-footer { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx 24rpx; background-color: #fff; }
.reevaluate-button { width: 100%; height: 92rpx; line-height: 92rpx; font-size: 28rpx; font-weight: 600; border-radius: 14rpx; background-color: #0868f7; color: #fff; margin: 0; }
.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: 28rpx; padding: 0 0 24rpx; border-bottom: 1rpx solid #edf0f4; }
.panel-heading { flex-direction: row; justify-content: space-between; align-items: center; }
.section-title { color: #233750; font-size: 28rpx; font-weight: 700; line-height: 42rpx; }
.toggle { font-size: 24rpx; color: #0868f7; padding: 10rpx; }
.body-text { margin-top: 20rpx; font-size: 26rpx; color: #425b78; line-height: 44rpx; }
.report-note { font-size: 23rpx; line-height: 38rpx; color: #78889e; margin-top: 12rpx; }
.source-note { color: #8796a8; font-size: 22rpx; line-height: 36rpx; margin-top: 16rpx; }
.fact-row { flex-direction: row; padding: 22rpx 0; border-bottom: 1rpx solid #edf1f6; }
.fact-label { flex: 1; font-size: 25rpx; line-height: 39rpx; color: #738198; }
.fact-value { flex: 1; text-align: right; font-size: 25rpx; line-height: 39rpx; color: #233e5e; }
.legacy { background-color: #fff5e7; padding: 16rpx 22rpx 24rpx; border-radius: 14rpx; margin-top: 20rpx; }
.stats { flex-direction: row; padding: 26rpx 8rpx; border-top: 1rpx solid #edf0f4; border-bottom: 1rpx solid #edf0f4; margin-top: 26rpx; }
.stat { flex: 1; align-items: center; }
.stat-number { font-size: 39rpx; color: #1b5ebd; font-weight: 700; }
.stat-label { font-size: 22rpx; color: #5c7799; margin-top: 12rpx; }
.product-card { padding: 28rpx; border-radius: 14rpx; background-color: #fff; border: 1rpx solid #e0eaf7; margin-top: 24rpx; }
.product-name { flex: 1; color: #284566; font-size: 28rpx; line-height: 42rpx; font-weight: 700; }
.version { color: #8b9aab; font-size: 22rpx; margin-left: 14rpx; }
.demo { color: #9b762d; font-size: 22rpx; margin-top: 12rpx; }
.amount { color: #164b86; font-size: 38rpx; font-weight: 700; margin-top: 22rpx; }
.metrics { flex-direction: row; justify-content: space-between; margin-top: 16rpx; }
.metric { color: #5c7491; font-size: 23rpx; line-height: 37rpx; }
.reason { color: #6e8097; font-size: 23rpx; line-height: 38rpx; margin-top: 14rpx; }
.availability { color: #a07436; font-size: 23rpx; line-height: 38rpx; margin-top: 16rpx; }
.action { padding: 24rpx 0; color: #0868f7; font-size: 25rpx; }
.links { flex-direction: row; justify-content: space-between; }
.advice-product { border-top: 1rpx solid #e8eef6; padding-top: 24rpx; margin-top: 24rpx; }
.evidence { color: #8494a8; font-size: 22rpx; line-height: 36rpx; margin-top: 12rpx; }
.primary { background-color: #0868f7; color: #fff; font-size: 27rpx; border-radius: 12rpx; margin-top: 24rpx; }
.secondary { background-color: #eaf3ff; color: #0868f7; font-size: 25rpx; border-radius: 12rpx; margin-top: 22rpx; }
.disclaimer { color: #939caa; font-size: 21rpx; line-height: 34rpx; margin-top: 18rpx; }
/* #ifndef APP */
@media (min-width: 768px) {
.report-content { max-width: 720px; margin: 0 auto; }
.report-footer { align-items: center; }
.reevaluate-button { max-width: 690px; }
}
/* #endif */
</style>