feat(融资): 新增产品对比页面及相关接口实现
新增产品对比页面 compare/compare,支持同一评估下最多3个产品的横向对比,展示额度、期限、利率、适用条件等核心指标及匹配详情。 新增 matches.uts 服务层,封装匹配列表、详情、公共产品查询接口。 新增 account 分包的 latest-report.uts 服务,用于查找用户最新成功报告。 调整个人中心页面,将“评估记录”改为“开始评估”,新增最近报告直达功能。 更新文档,标记匹配相关接口及原型07、09页面已实现,调整页面数量至26页。 移除 finance 分包中旧的 fetchMatches、fetchProduct 模拟数据接口,统一使用正式API。
This commit is contained in:
@@ -1,103 +1,252 @@
|
||||
<template>
|
||||
<view class="page" :style="pageStyle()">
|
||||
<AppHeader title="产品详情">
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<text v-if="available" class="share" @click="shareProduct">分享</text>
|
||||
<!-- #endif -->
|
||||
</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>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="product-actions"><text class="share" @click="shareProduct">分享产品</text></view>
|
||||
<!-- #endif -->
|
||||
<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">个人信用融资 · 产品信息</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>
|
||||
<view class="page product-page" :style="pageStyle()">
|
||||
<AppHeader :title="assessmentId.length > 0 ? '银行匹配详情' : '产品详情'" :customBack="true" @back="back" />
|
||||
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||
<view v-if="pageActive" class="product-content">
|
||||
<text v-if="loading" class="muted loading">正在读取产品资料…</text>
|
||||
<view v-if="error.length > 0" class="error-panel"><text class="heading">暂时无法读取</text><text class="body">{{ error }}</text><button class="secondary" @click="load">重新加载</button><text class="link" @click="back">返回选择产品</text></view>
|
||||
<template v-if="product != null">
|
||||
<view class="bank-header">
|
||||
<view class="bank-mark"><image class="bank-icon" src="/static/mobile/icons-generated/building.png" mode="aspectFit" /></view>
|
||||
<view class="bank-info">
|
||||
<view class="bank-title-row"><text class="bank-name">{{ product!.bank }}</text><text v-if="match != null" :class="match!.state == 'eligible' ? 'badge good' : (match!.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ match!.state == 'eligible' ? '可供参考' : (match!.state == 'ineligible' ? '暂不推荐' : '待核实') }}</text></view>
|
||||
<text v-if="match != null" :class="match!.state == 'eligible' ? 'state good-text' : 'state attention-text'">{{ match!.stateLabel }}</text>
|
||||
<text class="product-name">{{ product!.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="product!.demo" class="demo">演示数据 · 非真实金融产品</text>
|
||||
<view class="tabs">
|
||||
<view v-if="assessmentId.length > 0" :class="tab == 'explanation' ? 'tab active' : 'tab'" @click="switchTab('explanation')"><text :class="tab == 'explanation' ? 'tab-label selected' : 'tab-label'">AI解释</text></view>
|
||||
<view :class="tab == 'details' ? 'tab active' : 'tab'" @click="switchTab('details')"><text :class="tab == 'details' ? 'tab-label selected' : 'tab-label'">产品详情</text></view>
|
||||
<view :class="tab == 'conditions' ? 'tab active' : 'tab'" @click="switchTab('conditions')"><text :class="tab == 'conditions' ? 'tab-label selected' : 'tab-label'">申请条件</text></view>
|
||||
</view>
|
||||
<template v-if="tab == 'explanation' && match != null">
|
||||
<view class="reason-section">
|
||||
<text class="heading good-text">推荐依据({{ match!.positive.length }}条已满足条件)</text>
|
||||
<text v-if="match!.positive.length == 0" class="empty-note">暂无已确认满足的条件,不补充未经验证的推荐理由。</text>
|
||||
<view v-for="(reason, index) in positiveVisible" :key="reason.id" class="reason-row">
|
||||
<text class="number green-number">{{ index + 1 }}</text>
|
||||
<view class="reason-content"><text class="reason-title">{{ reason.text }}</text><text class="reason-detail">{{ reason.explanation.length > 0 ? reason.explanation : reason.evidence.join(';') }}</text><text class="evidence-toggle" @click="openReason = openReason == reason.id ? '' : reason.id">{{ openReason == reason.id ? '收起依据' : '查看依据' }}</text><view v-if="openReason == reason.id" class="evidence-box"><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view></view>
|
||||
</view>
|
||||
<text v-if="match!.positive.length > 3" class="expand" @click="expanded = !expanded">{{ expanded ? '收起' : '展开其余' + (match!.positive.length - 3) + '条依据' }}</text>
|
||||
</view>
|
||||
<view class="reason-section attention-section">
|
||||
<text class="heading attention-text">不推荐/需注意的点({{ attention.length }}条)</text>
|
||||
<text v-if="attention.length == 0" class="empty-note">已知规则中暂无不符或待核实项,仍需核验资料,不代表审批结果。</text>
|
||||
<view v-for="(reason, index) in attentionVisible" :key="reason.id" class="reason-row">
|
||||
<text class="number orange-number">{{ index + 1 }}</text>
|
||||
<view class="reason-content"><text class="reason-title">{{ reason.text }}</text><text class="reason-detail">{{ reason.explanation.length > 0 ? reason.explanation : reason.evidence.join(';') }}</text><text class="evidence-toggle" @click="openReason = openReason == reason.id ? '' : reason.id">{{ openReason == reason.id ? '收起依据' : '查看依据' }}</text><view v-if="openReason == reason.id" class="evidence-box"><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view></view>
|
||||
</view>
|
||||
<text v-if="attention.length > 3" class="expand" @click="attentionExpanded = !attentionExpanded">{{ attentionExpanded ? '收起' : '展开其余' + (attention.length - 3) + '条注意事项' }}</text>
|
||||
</view>
|
||||
<view class="ai-panel">
|
||||
<text class="ai-title">AI详细解释</text>
|
||||
<text v-if="match!.aiStatus == 'completed'" class="ai-note">已将逐条解释补充在上方,生成结果已保存,重新打开无需再次生成。</text>
|
||||
<text v-else-if="aiPending" class="ai-note">{{ pollStopped ? '生成仍在进行,可稍后查看状态。' : '正在解释本次匹配依据,请稍候。离开页面后任务仍会继续。' }}</text>
|
||||
<text v-else-if="match!.aiStatus == 'failed'" class="ai-note">解释生成未完成,规则依据不受影响。{{ match!.aiCanRetry ? '可以重新尝试。' : '本次已达到重试上限。' }}</text>
|
||||
<text v-else class="ai-note">当前为规则依据。可让AI逐条说明条件含义和资料核对建议。</text>
|
||||
<text v-if="aiError.length > 0" class="attention-text ai-note">{{ aiError }}</text>
|
||||
<button v-if="!aiPending && match!.aiStatus != 'completed' && (match!.aiStatus != 'failed' || match!.aiCanRetry)" class="ai-button" :disabled="aiBusy" @click="generateExplanation">{{ aiBusy ? '正在提交…' : (match!.aiStatus == 'failed' ? '重试AI解释' : '生成AI详细解释') }}</button>
|
||||
<button v-if="aiPending && (aiError.length > 0 || pollStopped)" class="ai-button" :disabled="aiBusy" @click="checkExplanation">重新查询状态</button>
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="tab == 'details'">
|
||||
<view class="details-section"><text class="heading">产品信息</text>
|
||||
<view class="row"><text class="label">额度范围</text><text class="value emphasis">{{ product!.amount }}</text></view>
|
||||
<view class="row"><text class="label">年化利率</text><text class="value">{{ product!.rate }}</text></view>
|
||||
<view class="row"><text class="label">贷款期限</text><text class="value">{{ product!.term }}</text></view>
|
||||
<view class="row"><text class="label">适用主体</text><text class="value">{{ product!.objects }}</text></view>
|
||||
<view class="row"><text class="label">适用地区</text><text class="value">{{ product!.regions }}</text></view>
|
||||
<view class="row"><text class="label">有效期</text><text class="value">{{ product!.valid }}</text></view>
|
||||
<view class="row"><text class="label">还款方式</text><text class="value">未提供</text></view>
|
||||
<view class="row"><text class="label">办理渠道</text><text class="value">未提供</text></view>
|
||||
<view class="row"><text class="label">审批时长</text><text class="value">未提供</text></view>
|
||||
</view>
|
||||
<view class="details-section"><text class="heading">资料来源</text><text class="body">{{ product!.source }}</text><text class="evidence">引用:{{ product!.sourceRef }}</text><text class="evidence">核对日期:{{ product!.checkedOn }}</text></view>
|
||||
</template>
|
||||
<template v-if="tab == 'conditions'">
|
||||
<view class="details-section"><text class="heading">产品准入条件</text><text class="muted">公开条件不代表个人已符合,缺少核验依据时仍需核实。</text><text v-for="(condition, index) in product!.conditions" :key="index" class="condition">{{ condition }}</text></view>
|
||||
<view class="details-section"><text class="heading">资料准备说明</text><text class="body">{{ product!.materials }}</text><text class="muted">仅提供材料引导,不向机构提交申请。实际条件和材料须向机构核实。</text></view>
|
||||
</template>
|
||||
<view class="footnotes"><text class="record-note">{{ assessmentId.length > 0 ? '本次评估 #' + assessmentId : '公共产品资料' }} · 产品版本 v{{ product!.version }}</text><text v-if="match != null" class="record-note">{{ match!.availabilityLabel }},历史参数与结论保留。</text><text v-if="match != null && !canPrepare" class="record-note">{{ match!.state != 'eligible' ? '请先核对不符条件或待核实项。' : '当前产品不可用或状态未知,暂不提供准备入口。' }}</text><text class="record-note">自填信息未经核验;本页面不承诺额度、放款或获批。</text><text v-if="assessmentId.length > 0" class="link" @click="toMatches">返回本次匹配</text><text v-else class="link" @click="history">选择评估查看匹配结果</text></view>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-if="pageActive && product != null" class="action-bar" :style="bottomStyle(22)">
|
||||
<template v-if="match != null && canPrepare"><button class="footer-button secondary" @click="openCompare">查看同类产品对比</button><button class="footer-button primary" @click="prepare">{{ tab == 'conditions' ? '返回本次匹配' : '查看资料准备' }}</button></template>
|
||||
<template v-else-if="match != null"><button class="footer-button secondary" @click="switchTab(tab == 'details' ? 'explanation' : 'details')">{{ tab == 'details' ? '查看匹配解释' : '查看产品详情' }}</button><button class="footer-button primary" @click="openCompare">{{ selectedIds.includes(productId) ? '查看产品对比' : '加入产品对比' }}</button></template>
|
||||
<button v-else class="footer-button primary" @click="switchTab(tab == 'conditions' ? 'details' : 'conditions')">{{ tab == 'conditions' ? '查看产品详情' : '查看申请条件' }}</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' }) }
|
||||
})
|
||||
}
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { MatchView, MatchReason, ProductView, fetchMatchDetail, fetchPublicProduct, validRecordId, requestProductExplanation } from '@/packages/finance/services/matches.uts'
|
||||
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery } from '@/packages/finance/services/compare.uts'
|
||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||
const assessmentId = ref(''); const productId = ref(''); const tab = ref('details')
|
||||
const selectedIds = ref<string[]>([]); let from = ''
|
||||
const product = ref<ProductView | null>(null); const match = ref<MatchView | null>(null)
|
||||
const loading = ref(false); const error = ref(''); const expanded = ref(false)
|
||||
const pageActive = ref(false); const attentionExpanded = ref(false); const openReason = ref('')
|
||||
const aiBusy = ref(false); const aiError = ref(''); const pollStopped = ref(false)
|
||||
let timer: number = 0; let pollCount = 0; let requestKey = ''
|
||||
let routeError = ''; let visible = false; let generation = 0
|
||||
const positiveVisible = computed((): MatchReason[] => match.value == null ? [] : (expanded.value ? match.value!.positive : match.value!.positive.slice(0, 3)))
|
||||
const attention = computed((): MatchReason[] => match.value == null ? [] : match.value!.blocking.concat(match.value!.cautions))
|
||||
const attentionVisible = computed((): MatchReason[] => attentionExpanded.value ? attention.value : attention.value.slice(0, 3))
|
||||
const aiPending = computed((): boolean => match.value != null && ['queued','running'].includes(match.value!.aiStatus))
|
||||
const canPrepare = computed((): boolean => match.value != null && match.value!.state == 'eligible' && match.value!.availability == 'available')
|
||||
const load = async () => {
|
||||
stopPoll(); pollCount = 0; pollStopped.value = false; aiError.value = ''
|
||||
const request = ++generation; product.value = null; match.value = null; error.value = ''; expanded.value = false
|
||||
if (routeError.length > 0) { error.value = routeError; return }
|
||||
const session = getSession()
|
||||
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||
const token = session.getString('token') ?? ''; loading.value = true
|
||||
try {
|
||||
// 带评估编号始终读取冻结结果;404/越权/失败不能降级为公共资料。
|
||||
if (assessmentId.value.length > 0) {
|
||||
const result = await fetchMatchDetail(assessmentId.value, productId.value)
|
||||
if (visible && generation == request && getSession()?.getString('token') == token) { match.value = result; product.value = result.product; schedulePoll() }
|
||||
} else {
|
||||
const result = await fetchPublicProduct(productId.value)
|
||||
if (visible && generation == request && getSession()?.getString('token') == token) product.value = result
|
||||
}
|
||||
} catch (err) { if (visible && generation == request) error.value = err instanceof Error ? err.message : '产品读取失败' }
|
||||
finally { if (generation == request) loading.value = false }
|
||||
}
|
||||
// 替换当前路由保留栏目、来源与对比选择,刷新后仍停留在同一栏。
|
||||
const switchTab = (value: string) => {
|
||||
if (value == tab.value) return
|
||||
const query = assessmentId.value.length > 0 ? compareQuery(assessmentId.value, selectedIds.value) + '&' : '?'
|
||||
uni.redirectTo({ url: '/packages/finance/product/product' + query + 'productId=' + productId.value + '&tab=' + value + '&from=' + from })
|
||||
}
|
||||
const prepare = () => { if (canPrepare.value) { if (tab.value == 'conditions') toMatches(); else switchTab('conditions') } }
|
||||
const stopPoll = () => { if (timer != 0) clearTimeout(timer); timer = 0 }
|
||||
const current = (request: number, token: string): boolean => visible && generation == request && getSession()?.getString('token') == token
|
||||
const refreshExplanation = async () => {
|
||||
if (aiBusy.value || match.value == null) return
|
||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||
aiError.value = ''; aiBusy.value = true
|
||||
try {
|
||||
const result = await fetchMatchDetail(assessmentId.value, productId.value)
|
||||
if (current(request, token)) { match.value = result; schedulePoll() }
|
||||
} catch (err) { if (current(request, token)) aiError.value = err instanceof Error ? err.message : '解释状态读取失败,请重试'
|
||||
} finally { if (current(request, token)) aiBusy.value = false }
|
||||
}
|
||||
const schedulePoll = () => {
|
||||
stopPoll()
|
||||
if (!visible || !aiPending.value) return
|
||||
if (++pollCount > 60) { pollStopped.value = true; return }
|
||||
timer = setTimeout(() => { timer = 0; refreshExplanation() }, 3000)
|
||||
}
|
||||
const checkExplanation = () => { pollCount = 0; pollStopped.value = false; refreshExplanation() }
|
||||
const generateExplanation = async () => {
|
||||
if (aiBusy.value || match.value == null || aiPending.value || match.value!.aiStatus == 'completed') return
|
||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||
// 网络结果不确定时保留同一个幂等键,只有收到明确失败状态后才开始新的尝试。
|
||||
if (requestKey.length == 0) requestKey = 'explain_' + Date.now().toString() + '_' + Math.random().toString(36).slice(2, 12)
|
||||
aiBusy.value = true; aiError.value = ''; pollCount = 0; pollStopped.value = false
|
||||
try {
|
||||
const result = await requestProductExplanation(assessmentId.value, productId.value, requestKey)
|
||||
if (current(request, token)) { match.value = result; requestKey = ''; schedulePoll() }
|
||||
} catch (err) { if (current(request, token)) aiError.value = err instanceof Error ? err.message : 'AI解释请求失败,请重试'
|
||||
} finally { if (current(request, token)) aiBusy.value = false }
|
||||
}
|
||||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||
const toMatches = () => { if (assessmentId.value.length > 0) uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, selectedIds.value) }); else history() }
|
||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else if (from == 'compare') uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value) }); else toMatches() }
|
||||
const openCompare = () => {
|
||||
if (match.value == null) return
|
||||
if (!selectedIds.value.includes(productId.value)) {
|
||||
if (selectedIds.value.length >= 3) { uni.showToast({ title: '已选满3项,请在对比页调整选择', icon: 'none' }); return }
|
||||
selectedIds.value.push(productId.value)
|
||||
}
|
||||
saveCompareSelection(assessmentId.value, selectedIds.value)
|
||||
uni.navigateTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value) })
|
||||
}
|
||||
onLoad((options) => {
|
||||
assessmentId.value = options?.['assessmentId'] ?? ''; const oldId = options?.['id'] ?? ''
|
||||
productId.value = options?.['productId'] ?? oldId
|
||||
if (options?.['assessmentId'] != null && !validRecordId(assessmentId.value)) routeError = '评估编号无效,请从报告重新进入。'
|
||||
if (!validRecordId(productId.value) || (oldId.length > 0 && oldId != productId.value)) routeError = '产品编号无效或冲突,请重新选择。'
|
||||
tab.value = options?.['tab'] ?? (assessmentId.value.length > 0 ? 'explanation' : 'details')
|
||||
if (!['explanation','details','conditions'].includes(tab.value) || (assessmentId.value.length == 0 && tab.value == 'explanation')) routeError = '页面栏目参数无效,请重新选择。'
|
||||
from = options?.['from'] ?? ''
|
||||
if (!['','matches','product','compare','report','next-step','archive'].includes(from)) routeError = '页面来源参数无效,请重新选择。'
|
||||
if (options?.['productIds'] != null) {
|
||||
try { saveCompareSelection(assessmentId.value, parseCompareIds(options!['productIds']!)) }
|
||||
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
||||
}
|
||||
})
|
||||
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value); load() })
|
||||
onHide(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; error.value = ''; aiError.value = ''; aiBusy.value = false; loading.value = false; openReason.value = '' })
|
||||
onUnload(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; requestKey = '' })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.product-actions { flex-direction: row; justify-content: flex-end; margin-bottom: 12rpx; }
|
||||
.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; }
|
||||
.product-page { background-color: #fff; }
|
||||
.product-content { padding: 18rpx 32rpx 200rpx; }
|
||||
.loading { padding: 36rpx 0; }
|
||||
.bank-header { flex-direction: row; align-items: center; padding: 20rpx 0 10rpx; }
|
||||
.bank-mark { width: 108rpx; height: 108rpx; border-radius: 54rpx; background-color: #edf5ff; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||
.bank-icon { width: 86rpx; height: 86rpx; }
|
||||
.bank-info { flex: 1; }
|
||||
.bank-title-row { flex-direction: row; flex-wrap: wrap; align-items: center; }
|
||||
.bank-name { color: #16253c; font-size: 33rpx; font-weight: 700; line-height: 48rpx; margin-right: 14rpx; }
|
||||
.badge { font-size: 21rpx; line-height: 32rpx; padding: 3rpx 12rpx; border-radius: 6rpx; }
|
||||
.good { color: #249a76; background-color: #e9f8f1; }
|
||||
.blocked { color: #697387; background-color: #f0f2f6; }
|
||||
.pending { color: #ac802d; background-color: #fff7e5; }
|
||||
.state { font-size: 24rpx; line-height: 36rpx; margin-top: 5rpx; }
|
||||
.product-name { color: #788294; font-size: 24rpx; line-height: 36rpx; margin-top: 3rpx; }
|
||||
.demo { color: #8d96a4; font-size: 21rpx; line-height: 32rpx; margin-top: 10rpx; }
|
||||
.tabs { flex-direction: row; border-bottom: 1rpx solid #e8edf4; margin-top: 18rpx; }
|
||||
.tab { flex: 1; align-items: center; padding: 22rpx 0; }
|
||||
.active { border-bottom: 4rpx solid #0868f7; }
|
||||
.tab-label { font-size: 26rpx; color: #687588; }
|
||||
.selected { color: #0868f7; font-weight: 700; }
|
||||
.reason-section { margin-top: 28rpx; }
|
||||
.attention-section { margin-top: 30rpx; }
|
||||
.heading { font-size: 28rpx; color: #243752; font-weight: 700; line-height: 42rpx; }
|
||||
.good-text { color: #259d79; }
|
||||
.attention-text { color: #e37b55; }
|
||||
.reason-row { flex-direction: row; align-items: flex-start; margin-top: 22rpx; }
|
||||
.number { width: 42rpx; height: 42rpx; border-radius: 21rpx; color: #fff; font-size: 25rpx; line-height: 42rpx; text-align: center; flex-shrink: 0; margin-right: 16rpx; margin-top: 2rpx; }
|
||||
.green-number { background-color: #35b38c; }
|
||||
.orange-number { background-color: #f49162; }
|
||||
.reason-content { flex: 1; }
|
||||
.reason-title { color: #27364c; font-size: 26rpx; line-height: 40rpx; font-weight: 600; }
|
||||
.reason-detail { color: #7b8594; font-size: 23rpx; line-height: 36rpx; margin-top: 4rpx; }
|
||||
.evidence-toggle { color: #8596ad; font-size: 21rpx; padding: 8rpx 0; }
|
||||
.evidence-box { background-color: #f5f8fc; border-radius: 8rpx; padding: 8rpx 14rpx 14rpx; margin-top: 4rpx; }
|
||||
.evidence { color: #7f8b9b; font-size: 22rpx; line-height: 36rpx; margin-top: 8rpx; }
|
||||
.expand { color: #4784c9; font-size: 23rpx; padding: 12rpx 0 0 58rpx; }
|
||||
.empty-note { color: #8c97a6; font-size: 23rpx; line-height: 38rpx; margin-top: 16rpx; }
|
||||
.ai-panel { background-color: #f0f6ff; border-radius: 12rpx; padding: 24rpx; margin-top: 34rpx; }
|
||||
.ai-title { color: #316aac; font-size: 25rpx; font-weight: 600; }
|
||||
.ai-note { color: #7890ab; font-size: 23rpx; line-height: 38rpx; margin-top: 10rpx; }
|
||||
.ai-button { font-size: 24rpx; color: #0868f7; background-color: #fff; border: 1rpx solid #d6e6fb; border-radius: 10rpx; margin-top: 20rpx; }
|
||||
.details-section { padding-top: 30rpx; }
|
||||
.row { flex-direction: row; border-bottom: 1rpx solid #f0f3f7; padding: 23rpx 0; }
|
||||
.label { color: #8190a3; width: 146rpx; font-size: 24rpx; line-height: 38rpx; }
|
||||
.value { flex: 1; text-align: right; color: #35465f; font-size: 25rpx; line-height: 38rpx; }
|
||||
.emphasis { color: #24578c; font-weight: 700; }
|
||||
.body { color: #53657e; font-size: 25rpx; line-height: 42rpx; margin-top: 18rpx; }
|
||||
.muted { color: #8694a7; font-size: 23rpx; line-height: 38rpx; margin-top: 12rpx; }
|
||||
.condition { padding: 20rpx 0; border-bottom: 1rpx solid #edf1f7; font-size: 25rpx; color: #435b77; line-height: 42rpx; }
|
||||
.footnotes { padding: 26rpx 0 0; margin-top: 28rpx; border-top: 1rpx solid #edf1f6; }
|
||||
.record-note { color: #939dac; font-size: 21rpx; line-height: 36rpx; }
|
||||
.link { font-size: 24rpx; color: #0868f7; padding: 22rpx 0; }
|
||||
.error-panel { padding: 20rpx 0; }
|
||||
.action-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 18rpx 32rpx 22rpx; flex-direction: row; background-color: #fff; border-top: 1rpx solid #edf1f7; }
|
||||
.footer-button { flex: 1; height: 80rpx; line-height: 80rpx; padding: 0 10rpx; font-size: 25rpx; border-radius: 12rpx; margin: 0; }
|
||||
.primary { background-color: #0868f7; color: #fff; }
|
||||
.secondary { background-color: #edf4ff; color: #1769ca; font-size: 25rpx; border-radius: 12rpx; }
|
||||
.action-bar .secondary { margin-right: 20rpx; }
|
||||
/* #ifndef APP */
|
||||
@media (min-width: 768px) { .product-content { max-width: 760px; margin: 0 auto; } .action-bar { max-width: 760px; margin: 0 auto; } }
|
||||
/* #endif */
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user