feat: 完善登录流程、UI适配及协议文档更新

本次提交包含以下修改:
1. 新增微信登录相关服务及页面逻辑,实现首次微信绑定手机号流程
2. 优化产品匹配卡片 UI,移除不必要状态显示
3. 调整首页 scroll-view 属性及 banner 背景样式
4. 完善积分规则校验逻辑,增加时区及策略格式验证
5. 更新协议草案文档,补充资料清单及功能说明
6. 修复分析结果路由路径,适配新的匹配结果页面
7. 调整品牌 Logo 安卓端字体对齐样式
This commit is contained in:
hujun
2026-10-22 11:24:15 +08:00
parent e5406c23a6
commit 2c9d3feab5
36 changed files with 1246 additions and 212 deletions
+43 -61
View File
@@ -29,29 +29,25 @@
</view>
<text class="overview-note">{{ orderingNote }}</text>
<view class="details-toggle" @click="detailsOpen = !detailsOpen"><text class="summary-title">详细条件与匹配依据</text><text class="detail-toggle-label">{{ detailsOpen ? '收起' : '展开' }}</text></view>
<text v-if="detailsOpen" class="scroll-hint">左右滑动查看各产品 · 长内容可在单元格内上下滑动</text>
<view v-if="detailsOpen" class="table">
<view class="rail"><view class="rail-head"><text class="rail-title">对比指标</text><text class="hint">同次评估</text></view><view v-for="row in rows" :key="row.id" class="label-cell" :style="rowStyle(row)"><text class="row-label">{{ row.label }}</text></view></view>
<scroll-view scroll-x class="horizontal">
<view class="columns" :style="columnsStyle()">
<view v-for="column in columns" :key="column.id" class="column">
<scroll-view scroll-y class="product-head"><view class="head-content">
<template v-if="usable(column)">
<image class="overview-logo" :src="column.data!.item.product.logo.length > 0 ? column.data!.item.product.logo : '/static/mobile/icons-generated/building.png'" mode="aspectFit" @error="column.data!.item.product.logo = ''" />
<text class="product-name">{{ column.data!.item.product.name }}</text>
<text class="hint">{{ column.data!.item.product.bank }} · v{{ column.data!.item.product.version }}</text>
<text :class="column.data!.item.state == 'eligible' ? 'state good' : (column.data!.item.state == 'ineligible' ? 'state blocked' : 'state pending')">{{ column.data!.item.stateLabel }}</text>
<text class="detail" @click="detail(column.id)">查看依据与详情 ›</text>
</template>
<template v-else><text class="product-name">产品 #{{ column.id }}</text><text class="hint">{{ column.loading ? '正在读取历史资料…' : problem(column) }}</text><button v-if="!column.loading" class="retry" @click="loadColumn(column.id)">重试此列</button></template>
<text class="remove" @click="remove(column.id)">移除</text>
</view></scroll-view>
<view v-for="row in rows" :key="row.id" class="cell" :style="rowStyle(row)">
<scroll-view scroll-y class="cell-scroll"><text :class="row.id == 'amount' ? 'cell-text amount' : 'cell-text'">{{ cell(column, row.id) }}</text></scroll-view>
</view>
</view>
<text v-if="detailsOpen" class="scroll-hint">各产品并排展示 · 向下查看完整条件与依据</text>
<view v-if="detailsOpen" class="overview-table detail-table">
<view class="overview-row">
<view class="overview-label"><text class="row-label">对比指标</text><text class="hint">同次评估</text></view>
<view v-for="column in columns" :key="column.id" class="overview-value detail-value">
<template v-if="usable(column)">
<image class="overview-logo" :src="column.data!.item.product.logo.length > 0 ? column.data!.item.product.logo : '/static/mobile/icons-generated/building.png'" mode="aspectFit" @error="column.data!.item.product.logo = ''" />
<text class="overview-name">{{ column.data!.item.product.name }}</text>
<text class="overview-bank">{{ column.data!.item.product.bank }} · v{{ column.data!.item.product.version }}</text>
<text class="overview-link" @click="detail(column.id)">查看依据与详情 ›</text>
</template>
<template v-else><text class="overview-name">产品 #{{ column.id }}</text><text class="hint">{{ column.loading ? '正在读取历史资料…' : problem(column) }}</text><text v-if="!column.loading" class="overview-link" @click="loadColumn(column.id)">重试此列</text></template>
<text class="overview-remove" @click="remove(column.id)">移除</text>
</view>
</scroll-view>
</view>
<view v-for="row in rows" :key="row.id" class="overview-row">
<view class="overview-label"><text class="row-label">{{ row.label }}</text></view>
<view v-for="column in columns" :key="column.id" class="overview-value detail-value"><text :class="row.id == 'amount' ? 'cell-text cell-amount' : 'cell-text'">{{ cell(column, row.id) }}</text></view>
</view>
</view>
</template>
<text class="footer-note">历史参数保留评估时版本;当前可用状态单独展示。缺失信息不会被推断为零。</text>
@@ -70,12 +66,12 @@
import { MatchDetail, MatchReason, validRecordId, fetchMatchDetailRecord, hasProductValue, matchPercentage } from '@/packages/finance/services/matches.uts'
import { parseCompareIds, saveCompareSelection, compareQuery, sameMatchSnapshot, compareReasonText } from '@/packages/finance/services/compare.uts'
type CompareColumn = { id: string, loading: boolean, error: string, data: MatchDetail | null }
type CompareRow = { id: string, label: string, height: number }
type CompareRow = { id: string, label: string }
const { pageStyle, contentStyle } = usePageLayout()
const revisionId = ref(''); const assessmentId = ref(''); const ids = ref<string[]>([]); const columns = ref<CompareColumn[]>([]); const routeError = ref('')
const detailsOpen = ref(false)
// 首屏并排展示短参数;长证据保留原宽表,展开后读取完整内容,不截断判断依据。
const overviewDefinitions: CompareRow[] = [{ id: 'state', label: '匹配状态', height: 0 }, { id: 'score', label: '匹配度', height: 0 }, { id: 'amount', label: '额度范围', height: 0 }, { id: 'term', label: '期限范围', height: 0 }, { id: 'rate', label: '年化利率', height: 0 }]
// 按指标组织整行,产品列均分可用宽度,长证据自然撑高行而不截断。
const overviewDefinitions: CompareRow[] = [{ id: 'score', label: '匹配度' }, { id: 'amount', label: '额度范围' }, { id: 'term', label: '期限范围' }, { id: 'rate', label: '年化利率' }, { id: 'availability', label: '当前状态' }]
let visible = false; let generation = 0
let owner = ''; let accountTimer = 0; let frozenDetail: MatchDetail | null = null
const stopAccountTimer = () => { if (accountTimer != 0) clearInterval(accountTimer); accountTimer = 0 }
@@ -93,16 +89,14 @@
// #endif
}
const detailRows: CompareRow[] = [
{ id: 'availability', label: '当前状态', height: 160 },
{ id: 'score', label: '匹配度', height: 120 },
{ id: 'amount', label: '额度范围', height: 120 }, { id: 'term', label: '期限范围', height: 120 }, { id: 'rate', label: '年化利率', height: 120 },
{ id: 'objects', label: '适用对象', height: 120 }, { id: 'regions', label: '适用地区', height: 140 },
{ id: 'positive', label: '推荐与参考理由', height: 360 }, { id: 'blocking', label: '不推荐原因', height: 300 }, { id: 'cautions', label: '注意事项', height: 300 },
{ id: 'conditions', label: '准入条件', height: 300 }, { id: 'repayment', label: '还款方式', height: 120 },
{ id: 'materials', label: '准备材料', height: 260 }, { id: 'channel', label: '办理渠道', height: 120 }, { id: 'source', label: '资料来源', height: 260 }
{ id: 'availability', label: '当前状态' },
{ id: 'score', label: '匹配度' },
{ id: 'amount', label: '额度范围' }, { id: 'term', label: '期限范围' }, { id: 'rate', label: '年化利率' },
{ id: 'objects', label: '适用对象' }, { id: 'regions', label: '适用地区' },
{ id: 'positive', label: '推荐与参考理由' }, { id: 'blocking', label: '不推荐原因' }, { id: 'cautions', label: '注意事项' },
{ id: 'conditions', label: '准入条件' }, { id: 'repayment', label: '还款方式' },
{ id: 'materials', label: '准备材料' }, { id: 'channel', label: '办理渠道' }, { id: 'source', label: '资料来源' }
]
const rowStyle = (row: CompareRow): string => 'height:' + row.height + 'rpx;'
const columnsStyle = (): string => 'width:' + (columns.value.length * 350) + 'rpx;'
const problem = (column: CompareColumn): string => {
if (column.error.length > 0) return column.error
if (column.data == null) return '资料尚未读取'
@@ -114,15 +108,15 @@
const complete = computed((): boolean => ids.value.length >= 2 && columns.value.length == ids.value.length && columns.value.every((c: CompareColumn): boolean => usable(c)))
const overviewClass = (column: CompareColumn, key: string): string => {
if (key == 'amount') return 'overview-text overview-amount'
if (key != 'state' || !usable(column)) return 'overview-text'
return 'overview-text ' + (column.data!.item.state == 'eligible' ? 'good' : (column.data!.item.state == 'ineligible' ? 'blocked' : 'pending'))
return 'overview-text'
}
const cell = (column: CompareColumn, key: string): string => {
if (!usable(column)) return column.loading ? '读取中…' : '本列未就绪'
const m = column.data!.item; const p = m.product
// 核心表也保留当前不可用提示,不能让历史符合条件被误读为现在仍可办理。
if (key == 'state') return m.stateLabel + (m.availability != 'available' ? ' · ' + m.availabilityLabel : '')
if (key == 'score') return matchPercentage(m.state, m.score) || '—'
if (key == 'score') {
const percentage = matchPercentage(m.state, m.score)
return percentage.length > 0 ? percentage : '—'
}
if (key == 'availability') return m.availabilityLabel + ';不改变历史结论'
if (key == 'positive') return compareReasonText(m.positive, '暂无已确认满足的条件')
if (key == 'blocking') return compareReasonText(m.blocking, '未发现已知不符条件;结果仅供参考')
@@ -136,7 +130,10 @@
if (key == 'materials') return p.materials
if (key == 'repayment') return hasProductValue(p.repayment) ? p.repayment : '—'
if (key == 'channel') return hasProductValue(p.channel) ? p.channel : '—'
if (key == 'source') return [hasProductValue(p.source) ? p.source : '', hasProductValue(p.sourceRef) ? '引用:' + p.sourceRef : '', hasProductValue(p.checkedOn) ? '核对日期:' + p.checkedOn : ''].filter((value: string): boolean => value.length > 0).join('\n') || '—'
if (key == 'source') {
const sourceText = [hasProductValue(p.source) ? p.source : '', hasProductValue(p.sourceRef) ? '引用:' + p.sourceRef : '', hasProductValue(p.checkedOn) ? '核对日期:' + p.checkedOn : ''].filter((value: string): boolean => value.length > 0).join('\n')
return sourceText.length > 0 ? sourceText : '—'
}
return '未提供'
}
const rowHasValue = (row: CompareRow): boolean => columns.value.some((column: CompareColumn): boolean => !usable(column) || hasProductValue(cell(column, row.id)))
@@ -229,33 +226,18 @@
.hint { color: #788ba2; font-size: 22rpx; line-height: 34rpx; margin-top: 8rpx; }
.add { color: #0868f7; background-color: #e6f0ff; font-size: 24rpx; border-radius: 12rpx; margin-left: 14rpx; }
.scroll-hint { font-size: 21rpx; color: #8494a7; margin-bottom: 16rpx; }
.table { flex-direction: row; border: 1rpx solid #dfe8f3; border-radius: 14rpx; overflow: hidden; background-color: #fff; }
.rail { width: 152rpx; flex-shrink: 0; background-color: #eef4fb; }
.rail-head { height: 350rpx; padding: 26rpx 16rpx; border-bottom: 1rpx solid #dfe8f3; }
.rail-title { color: #244566; font-size: 24rpx; font-weight: 700; }
.label-cell { padding: 24rpx 16rpx; border-bottom: 1rpx solid #dfe8f3; }
.row-label { font-size: 23rpx; line-height: 36rpx; color: #48627e; }
.horizontal { flex: 1; width: 0; }
.columns { flex-direction: row; }
/* #ifndef APP */
.columns { min-width: 100%; }
/* #endif */
.column { width: 350rpx; flex-grow: 1; flex-shrink: 0; border-left: 1rpx solid #e7edf5; }
.product-head { height: 350rpx; border-bottom: 1rpx solid #dfe8f3; background-color: #fafdff; }
.head-content { padding: 22rpx 20rpx; }
.product-name { font-size: 26rpx; line-height: 37rpx; color: #1d3d60; font-weight: 700; }
.state { font-size: 22rpx; margin-top: 12rpx; }
.good { color: #18805d; }
.blocked { color: #b26341; }
.pending { color: #9b751f; }
.demo { color: #8b97a8; font-size: 20rpx; margin-top: 6rpx; }
.detail { color: #0868f7; font-size: 23rpx; padding-top: 16rpx; }
.remove { color: #77889e; font-size: 22rpx; padding-top: 16rpx; }
.retry { font-size: 23rpx; color: #0868f7; background-color: #eaf3ff; margin-top: 12rpx; }
.cell { padding: 20rpx; border-bottom: 1rpx solid #e7edf5; }
.cell-scroll { flex: 1; height: 0; }
.cell-text { font-size: 24rpx; line-height: 39rpx; color: #455f7c; }
.amount { color: #154779; font-weight: 700; font-size: 27rpx; }
.detail-value { justify-content: flex-start; }
.cell-text { font-size: 23rpx; line-height: 36rpx; color: #455f7c; }
.cell-amount { color: #154779; font-weight: 700; }
/* #ifndef APP */
.detail-table .cell-text, .detail-table .overview-name, .detail-table .overview-bank { white-space: pre-wrap; word-break: break-all; }
/* #endif */
.footer-note { color: #8997a8; font-size: 22rpx; line-height: 36rpx; margin-top: 24rpx; }
.secondary { color: #0868f7; background-color: #eaf3ff; font-size: 26rpx; border-radius: 12rpx; margin-top: 20rpx; }
</style>