本次提交包含以下修改: 1. 新增微信登录相关服务及页面逻辑,实现首次微信绑定手机号流程 2. 优化产品匹配卡片 UI,移除不必要状态显示 3. 调整首页 scroll-view 属性及 banner 背景样式 4. 完善积分规则校验逻辑,增加时区及策略格式验证 5. 更新协议草案文档,补充资料清单及功能说明 6. 修复分析结果路由路径,适配新的匹配结果页面 7. 调整品牌 Logo 安卓端字体对齐样式
298 lines
30 KiB
Plaintext
298 lines
30 KiB
Plaintext
<template>
|
||
<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="product!.logo.length > 0 ? product!.logo : '/static/mobile/icons-generated/building.png'" @error="product!.logo = ''" mode="aspectFit" /></view>
|
||
<view class="bank-info">
|
||
<view class="bank-title-row"><text v-if="product!.bank.length > 0" class="bank-name">{{ product!.bank }}</text><text v-if="match != null && matchPercentage(match!.state, match!.score).length > 0" :class="match!.state == 'eligible' ? 'badge good' : (match!.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ '匹配度 ' + matchPercentage(match!.state, match!.score) }}</text></view>
|
||
<text class="product-name">{{ product!.name }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="hasProductValue(product!.amount) || hasProductValue(product!.rate)" class="product-summary"><text v-if="hasProductValue(product!.amount)" class="summary-tag">额度范围 {{ product!.amount }}</text><text v-if="hasProductValue(product!.rate)" class="summary-tag">年化利率 {{ product!.rate }}</text></view>
|
||
<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!.state == 'eligible' ? '推荐理由(' + match!.positive.length + '条)' : '参考理由(' + 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">{{ reasonTitle(reason) }}</text><text v-if="reason.explanation.length > 0" class="reason-detail">{{ reason.explanation }}</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">{{ reasonTitle(reason) }}</text><text v-if="reason.explanation.length > 0" class="reason-detail">{{ reason.explanation }}</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="link" @click="consult">我有疑问,咨询AI ›</text>
|
||
<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>
|
||
<text v-if="!match!.aiCanRequest" class="ai-note">历史版本保留已保存的规则与解释。</text>
|
||
<button v-if="match!.aiCanRequest && !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 v-if="hasProductValue(product!.amount)" class="row"><text class="label">额度范围</text><text class="value emphasis">{{ product!.amount }}</text></view>
|
||
<view v-if="hasProductValue(product!.rate)" class="row"><text class="label">年化利率</text><text class="value">{{ product!.rate }}</text></view>
|
||
<view v-if="hasProductValue(product!.term)" 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 v-if="hasProductValue(product!.repayment)" class="row"><text class="label">还款方式</text><text class="value">{{ product!.repayment }}</text></view>
|
||
<view v-if="hasProductValue(product!.channel)" class="row"><text class="label">办理渠道</text><text class="value">{{ product!.channel }}</text></view>
|
||
<view v-if="hasProductValue(product!.approval)" class="row"><text class="label">审批时长</text><text class="value">{{ product!.approval }}</text></view>
|
||
</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><view v-for="(material, index) in product!.materialList" :key="index" class="material-item"><text class="condition">{{ material.name }} · {{ material.required ? '必需' : '补充' }} · {{ material.objects }}</text><text v-if="material.note.length > 0" class="evidence">{{ material.note }}</text></view><text v-if="product!.materialList.length == 0 || product!.materials != '未提供'" 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 v-if="match != null && match!.score != null" class="record-note">匹配度表示条件匹配程度,不代表获批概率。</text><text class="record-note">结果仅供参考,不代表审批结果;本页面不承诺额度、放款或获批。</text><text v-if="assessmentId.length > 0" class="link" @click="openCompare">查看同类产品对比</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="consultAdvisor">咨询顾问</button><button class="footer-button primary" @click="prepare">申请办理</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="consult">我有疑问,咨询AI</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 AppHeader from '@/components/AppHeader.uvue'
|
||
import { usePageLayout } from '@/services/layout.uts'
|
||
import { getSession } from '@/services/api.uts'
|
||
import { MatchView, MatchReason, MatchDetail, ProductView, fetchMatchDetailRecord, fetchPublicProduct, validRecordId, requestProductExplanation, hasProductValue as productHasValue, matchPercentage as productMatchPercentage } from '@/packages/finance/services/matches.uts'
|
||
const hasProductValue = (value: string): boolean => productHasValue(value)
|
||
const matchPercentage = (state: string, score: number | null): string => productMatchPercentage(state, score)
|
||
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery, sameMatchSnapshot, matchReturnPath } from '@/packages/finance/services/compare.uts'
|
||
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
|
||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||
const revisionId = ref(''); 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
|
||
let owner = ''; let accountTimer = 0; let frozenDetail: MatchDetail | null = null
|
||
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 reasonTitle = (reason: MatchReason): string => reason.text.replace('本产品已配置条件', '产品条件').replace(/[((]\s*匹配度\s*(?:[+\-−]|加|减)\s*\d+(?:\.\d+)?\s*分\s*[))]/g, '').trim()
|
||
function stopPoll(): void { if (timer != 0) clearTimeout(timer); timer = 0 }
|
||
const stopAccountTimer = () => { if (accountTimer != 0) clearInterval(accountTimer); accountTimer = 0 }
|
||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeMatch=1&matchPage=product&assessmentId=' + assessmentId.value + '&revisionId=' + revisionId.value + '&productIds=' + selectedIds.value.join(',') + '&productId=' + productId.value + '&tab=' + tab.value + '&from=' + from }) }
|
||
const current = (request: number, token: string): boolean => {
|
||
if (!visible || generation != request) return false
|
||
if (getSession()?.getString('token') == token) return true
|
||
generation++; stopPoll(); stopAccountTimer(); product.value = null; match.value = null; frozenDetail = null; openReason.value = ''; requestKey = ''; loading.value = false; aiBusy.value = false; aiError.value = ''
|
||
if (getSession() == null) login(); else error.value = '账户已切换,请重新加载本人产品资料。'
|
||
selectedIds.value = []
|
||
return false
|
||
}
|
||
const ready = (): boolean => current(generation, owner) && product.value != null && !loading.value
|
||
const updateUrl = () => {
|
||
// #ifdef WEB
|
||
window.history.replaceState(window.history.state, '', location.pathname + location.search + '#' + matchReturnPath('product', assessmentId.value, selectedIds.value.join(','), revisionId.value, productId.value, 'eligible', tab.value, from))
|
||
// #endif
|
||
}
|
||
const explanationError = (request: number, token: string, message: string) => {
|
||
if (current(request, token)) aiError.value = message
|
||
}
|
||
let pollAction: () => void = () => {}
|
||
function schedulePoll(): void {
|
||
stopPoll()
|
||
if (!visible || !aiPending.value) return
|
||
if (++pollCount > 60) { pollStopped.value = true; return }
|
||
timer = setTimeout(() => { timer = 0; pollAction() }, 3000)
|
||
}
|
||
async function refreshExplanation(): Promise<void> {
|
||
if (!ready() || aiBusy.value || match.value == null) return
|
||
const request = generation; const token = owner
|
||
aiError.value = ''; aiBusy.value = true
|
||
try {
|
||
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||
if (!current(request, token)) return
|
||
if (frozenDetail == null || !sameMatchSnapshot(frozenDetail!, detail)) throw new Error('解释状态与本次冻结结果不一致,请重新加载。')
|
||
match.value = detail.item; schedulePoll()
|
||
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : '解释状态读取失败,请重试')
|
||
} finally { if (current(request, token)) aiBusy.value = false }
|
||
}
|
||
pollAction = () => { refreshExplanation() }
|
||
const checkExplanation = () => { pollCount = 0; pollStopped.value = false; refreshExplanation() }
|
||
const generateExplanation = async () => {
|
||
if (!ready() || aiBusy.value || match.value == null || !match.value!.aiCanRequest || (match.value!.aiStatus == 'failed' && !match.value!.aiCanRetry) || aiPending.value || match.value!.aiStatus == 'completed') return
|
||
const request = generation; const token = owner
|
||
// 网络结果不确定时保留同一个幂等键,只有收到明确失败状态后才开始新的尝试。
|
||
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, revisionId.value)
|
||
if (current(request, token)) { match.value = result; requestKey = ''; schedulePoll() }
|
||
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : 'AI解释请求失败,请重试')
|
||
} finally { if (current(request, token)) aiBusy.value = false }
|
||
}
|
||
const load = async () => {
|
||
stopPoll(); stopAccountTimer(); pollCount = 0; pollStopped.value = false; aiError.value = ''; aiBusy.value = false
|
||
const request = ++generation; product.value = null; match.value = null; frozenDetail = null; error.value = ''; expanded.value = false; loading.value = false
|
||
if (routeError.length > 0) { error.value = routeError; return }
|
||
const session = getSession()
|
||
if (session == null) { login(); return }
|
||
const token = session.getString('token') ?? ''; if (owner.length > 0 && owner != token) { selectedIds.value = []; requestKey = '' }; owner = token; loading.value = true
|
||
accountTimer = setInterval(() => { current(generation, owner) }, 1000)
|
||
try {
|
||
// 带评估编号始终读取冻结结果;404/越权/失败不能降级为公共资料。
|
||
if (assessmentId.value.length > 0) {
|
||
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||
const result = detail.item
|
||
if (current(request, token)) { revisionId.value = detail.revisionId; frozenDetail = detail; match.value = result; product.value = result.product; saveCompareSelection(assessmentId.value, selectedIds.value, revisionId.value); updateUrl(); schedulePoll() }
|
||
} else {
|
||
const result = await fetchPublicProduct(productId.value)
|
||
if (current(request, token)) product.value = result
|
||
}
|
||
} catch (err) { if (current(request, token)) error.value = err instanceof Error ? err.message : '产品读取失败' }
|
||
finally { if (generation == request) loading.value = false }
|
||
}
|
||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||
// 仅携带选择ID;助手会重新校验归属并读取冻结事实,进入页面不会调用模型。
|
||
const consult = () => { if (ready() && match.value != null) uni.navigateTo({ url: assistantRoute(assessmentId.value, [productId.value], 'product', '', revisionId.value) }) }
|
||
function toMatches(): void { if (owner.length > 0 && !current(generation, owner)) return; if (assessmentId.value.length > 0) uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else history() }
|
||
// 替换当前路由保留栏目、来源与对比选择,刷新后仍停留在同一栏。
|
||
const switchTab = (value: string) => {
|
||
if (!ready() || value == tab.value) return
|
||
const query = assessmentId.value.length > 0 ? compareQuery(assessmentId.value, selectedIds.value, revisionId.value) + '&' : '?'
|
||
uni.redirectTo({ url: '/packages/finance/product/product' + query + 'productId=' + productId.value + '&tab=' + value + '&from=' + from })
|
||
}
|
||
// 下一步页重新核对当前可用性,避免沿用产品详情进入时的旧状态。
|
||
const consultAdvisor = () => {
|
||
if (!ready() || product.value == null) return
|
||
if (match.value != null && match.value!.availability != 'available') { uni.showToast({ title: '产品当前不可用,请联系平台核对。', icon: 'none' }); return }
|
||
const phone = product.value!.consultPhone
|
||
if (phone.length == 0) { uni.showToast({ title: '暂未配置顾问电话,请联系平台。', icon: 'none' }); return }
|
||
uni.makePhoneCall({ phoneNumber: phone })
|
||
}
|
||
const prepare = () => { if (ready() && canPrepare.value) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productId.value + '§ion=prepare' }) }
|
||
const back = () => { if (owner.length > 0 && !current(generation, owner)) return; if (getCurrentPages().length > 1) uni.navigateBack(); else if (from == 'compare') uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else toMatches() }
|
||
const openCompare = () => {
|
||
if (!ready() || 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, revisionId.value)
|
||
uni.navigateTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) })
|
||
}
|
||
onLoad((options) => {
|
||
revisionId.value = options?.['revisionId'] ?? '';
|
||
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError = '版本编号无效,请返回重新选择。'
|
||
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']!), revisionId.value) }
|
||
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
||
}
|
||
if (matchReturnPath('product', assessmentId.value, options?.['productIds'] ?? '', revisionId.value, productId.value, 'eligible', tab.value, from).length == 0) routeError = '产品页面参数无效,请返回重新选择。'
|
||
})
|
||
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value, revisionId.value); load() })
|
||
onHide(() => { pageActive.value = false; visible = false; stopPoll(); stopAccountTimer(); generation++; product.value = null; match.value = null; frozenDetail = null; error.value = ''; aiError.value = ''; aiBusy.value = false; loading.value = false; openReason.value = '' })
|
||
onUnload(() => { pageActive.value = false; visible = false; stopPoll(); stopAccountTimer(); generation++; product.value = null; match.value = null; frozenDetail = null; requestKey = '' })
|
||
</script>
|
||
<style>
|
||
.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; }
|
||
.product-summary { flex-direction: row; flex-wrap: wrap; margin-top: 12rpx; }
|
||
.summary-tag { color: #1670ed; background-color: #edf4ff; font-size: 23rpx; line-height: 38rpx; padding: 4rpx 12rpx; border-radius: 6rpx; margin-right: 10rpx; margin-top: 6rpx; }
|
||
.material-item { margin-top: 12rpx; }
|
||
.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: 28rpx; line-height: 42rpx; font-weight: 600; }
|
||
.reason-detail { color: #7b8594; font-size: 25rpx; line-height: 39rpx; 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>
|