feat: 实现协议全文与同意记录功能

新增协议全文后台管理、版本化发布及本人同意记录功能,支持可撤销授权;完善资料与授权相关边界提示,调整评估提交时的资料与授权校验逻辑,同步更新相关页面文案与路由。
This commit is contained in:
hujun
2026-10-14 14:53:13 +08:00
parent b418969e92
commit c95482a0f3
58 changed files with 3269 additions and 146 deletions
+9 -6
View File
@@ -12,16 +12,16 @@
<text v-if="loading && matches.length == 0" class="state-copy">正在读取本次匹配结果…</text>
<view v-if="error.length > 0" class="state-panel"><text class="state-title">暂时无法读取</text><text class="muted">{{ error }}</text><button class="secondary" @click="retry">重新加载</button><text class="link" @click="history">返回融资档案</text></view>
<view v-if="pageError.length > 0" class="state-panel"><text class="muted">{{ pageError }}</text><button class="secondary" :disabled="loading" @click="loadMore">重试加载更多</button></view>
<view v-if="!loading && error.length == 0 && matches.length == 0" class="state-panel"><text class="state-title">{{ status == 'unavailable' ? '本次未执行匹配' : (group == 'eligible' ? '暂无符合已知条件的产品' : (group == 'ineligible' ? '暂无条件不符的产品' : '暂无待核实的产品')) }}</text><text class="muted">{{ status == 'unavailable' ? '历史结果不会自动补算。可从融资档案发起新的评估。' : '可切换分组查看其他结果,或返回报告了解资料准备情况。' }}</text><text class="link" @click="report">查看本次报告</text></view>
<view v-if="!loading && error.length == 0 && matches.length == 0" class="state-panel"><text class="state-title">{{ status == 'unavailable' ? '本次未执行匹配' : (group == 'eligible' ? '暂无符合已知条件的产品' : (group == 'ineligible' ? '暂无条件不符的产品' : '暂无资料不足或待确认的产品')) }}</text><text class="muted">{{ status == 'unavailable' ? '历史结果不会自动补算。可从融资档案发起新的评估。' : '可切换分组查看其他结果,或返回报告了解资料准备情况。' }}</text><text class="link" @click="report">查看本次报告</text></view>
<view v-for="item in matches" :key="item.id" class="card" @click="openProduct(item.id)">
<view class="card-head"><view :class="bankMarkClass(item.product.bank)"><text class="mark-text">{{ item.product.bank.slice(0, 1) }}</text></view><view class="identity"><text class="bank-name">{{ item.product.bank }}</text><text class="product-name">{{ item.product.name }}</text></view><text :class="item.state == 'eligible' ? 'match-state good' : (item.state == 'ineligible' ? 'match-state blocked' : 'match-state pending')">{{ item.stateLabel }}</text><text class="card-arrow">›</text></view>
<view class="tags"><text class="tag">年化 {{ item.product.rate }}</text><text class="tag">{{ item.product.term }}</text><text class="tag">{{ item.product.objects }}</text></view>
<view class="card-head"><view :class="bankMarkClass(item.product.bank)"><image v-if="item.product.logo.length > 0" class="mark-logo" :src="item.product.logo" mode="aspectFit" @error="item.product.logo = ''" /><text v-else class="mark-text">{{ item.product.bank.length > 0 ? item.product.bank.slice(0, 1) : item.product.name.slice(0, 1) }}</text></view><view class="identity"><text v-if="item.product.bank.length > 0" class="bank-name">{{ item.product.bank }}</text><text class="product-name">{{ item.product.name }}</text></view><view class="card-status"><text :class="item.state == 'eligible' ? 'match-state good' : (item.state == 'ineligible' ? 'match-state blocked' : 'match-state pending')">{{ item.stateLabel }}</text><text v-if="matchPercentage(item.state, item.score).length > 0" class="match-score">匹配度 {{ matchPercentage(item.state, item.score) }}</text></view><text class="card-arrow">›</text></view>
<view class="tags"><text v-if="hasProductValue(item.product.rate)" class="tag">年化 {{ item.product.rate }}</text><text v-if="hasProductValue(item.product.term)" class="tag">{{ item.product.term }}</text><text class="tag">{{ item.product.objects }}</text></view>
<text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }};历史结果仍可查看。</text>
<view class="card-foot"><text class="detail-link">查看匹配依据</text><view class="choose-control" @click.stop="toggleCompare(item.id)"><text :class="selectedIds.includes(item.id) ? 'selection-box selected-box' : 'selection-box'">{{ selectedIds.includes(item.id) ? '✓' : '+' }}</text><text class="choose-label">{{ selectedIds.includes(item.id) ? '已选对比' : '加入对比' }}</text></view></view>
</view>
<button v-if="hasMore && pageError.length == 0" class="more-button" :disabled="loading" @click="loadMore">{{ loading ? '正在加载…' : '查看更多产品(共' + count(group) + '项)' }}</button>
<view v-if="error.length == 0 && matches.length > 0" class="compare-bar"><text class="compare-caption">已选 {{ selectedIds.length }} / 3 项</text><button class="compare-button" :disabled="selectedIds.length < 2" @click="openCompare">对比所选产品</button></view>
<text v-if="matches.length > 0 && !hasMore" class="end-note">已显示本组全部结果 · 顺序不代表产品优劣</text>
<text v-if="matches.length > 0 && !hasMore" class="end-note">已显示本组全部结果</text>
<text v-if="message.length > 0" class="muted notice">{{ message }}</text>
<text class="link" @click="report">返回本次报告</text>
</template>
@@ -33,11 +33,11 @@
import AppHeader from '@/components/AppHeader.uvue'
import { usePageLayout } from '@/services/layout.uts'
import { getSession } from '@/services/api.uts'
import { MatchView, MatchPage, fetchMatchPage, validRecordId } from '@/packages/finance/services/matches.uts'
import { MatchView, MatchPage, fetchMatchPage, validRecordId, hasProductValue, matchPercentage } from '@/packages/finance/services/matches.uts'
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery, matchReturnPath } from '@/packages/finance/services/compare.uts'
const { pageStyle, contentStyle } = usePageLayout()
type MatchGroup = { id: string, label: string }
const groups: MatchGroup[] = [{ id: 'eligible', label: '符合条件' }, { id: 'ineligible', label: '条件不符' }, { id: 'unknown', label: '待核实' }]
const groups: MatchGroup[] = [{ id: 'eligible', label: '符合条件' }, { id: 'ineligible', label: '条件不符' }, { id: 'unknown', label: '待确认' }]
const revisionId = ref(''); const assessmentId = ref(''); const group = ref('eligible'); const matches = ref<MatchView[]>([])
const selectedIds = ref<string[]>([])
const pageActive = ref(false)
@@ -153,6 +153,7 @@
.card { padding: 24rpx 22rpx 18rpx; background-color: #fff; border: 1rpx solid #e2e8ef; border-radius: 10rpx; margin-bottom: 20rpx; }
.card-head { flex-direction: row; align-items: center; }
.mark { width: 66rpx; height: 66rpx; border-radius: 33rpx; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0; }
.mark-logo { width: 52rpx; height: 52rpx; }
.mark-blue { background-color: #edf4fd; }
.mark-red { background-color: #fcedef; }
.mark-green { background-color: #eaf6f1; }
@@ -163,6 +164,8 @@
.bank-name { font-size: 28rpx; line-height: 40rpx; color: #24354b; font-weight: 600; }
.product-name { font-size: 22rpx; color: #7a899d; line-height: 34rpx; margin-top: 3rpx; }
.match-state { font-size: 20rpx; line-height: 30rpx; max-width: 140rpx; flex-shrink: 0; text-align: right; margin-left: 10rpx; }
.card-status { align-items: flex-end; margin-left: 8rpx; }
.match-score { font-size: 21rpx; line-height: 32rpx; color: #0868f7; margin-top: 4rpx; }
.card-arrow { font-size: 38rpx; color: #a3aebe; margin-left: 12rpx; }
.tags { flex-direction: row; flex-wrap: wrap; margin-top: 18rpx; }
.tag { background-color: #f2f5f9; color: #718197; font-size: 20rpx; line-height: 30rpx; padding: 4rpx 10rpx; border-radius: 4rpx; margin-right: 8rpx; margin-bottom: 6rpx; }