Files
airz-uniapp/pages/agreement/agreement.uvue
T
hujun c95482a0f3 feat: 实现协议全文与同意记录功能
新增协议全文后台管理、版本化发布及本人同意记录功能,支持可撤销授权;完善资料与授权相关边界提示,调整评估提交时的资料与授权校验逻辑,同步更新相关页面文案与路由。
2026-10-14 14:53:13 +08:00

109 lines
8.7 KiB
Plaintext

<template>
<view class="page page-white" :style="pageStyle()">
<AppHeader :title="pageTitle" />
<scroll-view scroll-y :style="contentStyle(96, false)">
<view v-if="loading" class="empty-content"><text class="empty-copy">正在加载协议全文…</text></view>
<view v-else-if="unavailable" class="empty-content"><text class="empty-title">协议尚未正式发布</text><text class="empty-copy">正式正文和版本发布后可在此查看。当前无法记录此协议的同意。</text><button class="primary-button empty-button" @click="reload">重新查看</button></view>
<view v-else-if="error.length > 0" class="empty-content"><text class="empty-title">协议加载失败</text><text class="empty-copy">{{ error }}</text><button class="primary-button empty-button" @click="reload">重新加载</button></view>
<view v-else-if="available" class="agreement-content" :style="bottomStyle(40)">
<text class="agreement-title">{{ agreement.title }}</text>
<text class="agreement-meta">版本 {{ agreement.version }}</text>
<text class="agreement-meta">生效时间:{{ consentTime(agreement.effectiveAt) }}</text>
<text v-if="version.length > 0" class="history-note">您正在查看指定版本的协议全文。</text>
<text class="agreement-body">{{ agreement.content }}</text>
<view v-if="optionalConsentScope(agreement.scope)" class="consent-area">
<text class="consent-note">此处仅记录您的同意意向,不会上传资料、查询征信或发起外部数据查询。您可在“个人中心 → 隐私与授权”查看记录、撤销可选授权。</text>
<text v-if="version.length > 0" class="consent-note">同意新授权时,请查看当前有效版本。</text>
<text v-if="actionMessage.length > 0" class="action-message">{{ actionMessage }}</text>
<button v-if="version.length == 0 && !granted" class="primary-button action-button" :disabled="submitting" @click="confirmConsent">{{ submitting ? '正在记录…' : (pendingId.length > 0 ? '用原请求重试同意' : (loggedIn ? '我已阅读,同意并记录' : '登录后记录同意')) }}</button>
<button v-if="version.length > 0" class="secondary-button action-button" @click="openCurrent">查看当前版本</button>
<button class="secondary-button action-button" @click="openRecords">查看我的同意记录</button>
</view>
</view>
</scroll-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 { Agreement, emptyAgreement, fetchAgreement, agreementRoute, consentTitle, consentTime, optionalConsentScope, agreementUnavailable, agreementChanged, grantConsent, consentRequestId, sameConsentSession } from '@/services/agreements.uts'
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
const agreement = ref<Agreement>(emptyAgreement())
const pageTitle = ref('协议全文'); const loading = ref(false); const available = ref(false); const unavailable = ref(false); const error = ref('')
const loggedIn = ref(false); const submitting = ref(false); const granted = ref(false); const actionMessage = ref(''); const pendingId = ref('')
const version = ref('')
let code = ''; let visible = false; let generation = 0; let actionGeneration = 0; let accountTimer = 0
let pendingAgreement: Agreement | null = null; let ownerToken = ''; let ownerId = ''
const resetPending = () => { pendingId.value = ''; pendingAgreement = null; granted.value = false; submitting.value = false; actionMessage.value = ''; actionGeneration++ }
const syncSession = () => {
const session = getSession(); const token = session?.getString('token') ?? ''; const userId = session?.getString('userId') ?? ''
if (token != ownerToken || userId != ownerId) resetPending()
ownerToken = token; ownerId = userId; loggedIn.value = session != null
}
const sameOwner = (token: string, userId: string): boolean => {
const current = getSession()
return visible && sameConsentSession(token, userId, current?.getString('token') ?? '', current?.getString('userId') ?? '')
}
async function reload() {
const request = ++generation; loading.value = true; available.value = false; unavailable.value = false; error.value = ''
try {
const result = await fetchAgreement(code, version.value)
if (!visible || request != generation) return
agreement.value = result; available.value = true; pageTitle.value = consentTitle(result.code)
} catch (err) {
if (!visible || request != generation) return
if (agreementUnavailable(err)) unavailable.value = true
else error.value = err instanceof Error ? err.message : '请检查网络后重试'
} finally { if (request == generation) loading.value = false }
}
const loginRoute = (): string => '/packages/account/login/login?resumeAgreement=1&agreementCode=' + code
async function confirmConsent() {
if (!visible || submitting.value || !available.value || version.value.length > 0) return
syncSession()
if (!loggedIn.value) { uni.navigateTo({ url: loginRoute() }); return }
const token = ownerToken; const userId = ownerId; const request = ++actionGeneration
if (pendingAgreement == null) { pendingAgreement = agreement.value; pendingId.value = consentRequestId() }
const pending = pendingAgreement!
submitting.value = true; actionMessage.value = ''
try {
const result = await grantConsent(pending, pendingId.value)
if (request != actionGeneration || !sameOwner(token, userId)) return
pendingId.value = ''; pendingAgreement = null; granted.value = result.revokedAt.length == 0
actionMessage.value = result.revokedAt.length > 0 ? '此前同意已撤销;重新同意需再次点击,不会自动记录新的同意。' : '同意已记录,未发起外部查询。'
} catch (err) {
if (request != actionGeneration || !sameOwner(token, userId)) return
if (agreementChanged(err)) {
pendingId.value = ''; pendingAgreement = null; actionMessage.value = '协议已更新,请重新阅读当前版本后再次同意。'; await reload()
} else actionMessage.value = (err instanceof Error ? err.message : '记录失败') + ';请用原请求重试确认。'
} finally { if (request == actionGeneration) submitting.value = false }
}
const openRecords = () => { if (visible) uni.navigateTo({ url: getSession() == null ? '/packages/account/login/login?resumeConsents=1' : '/packages/account/consents/consents' }) }
const openCurrent = () => { const url = agreementRoute(code); if (visible && url.length > 0) uni.redirectTo({ url }) }
onLoad((options) => { code = options?.['code'] ?? ''; version.value = options?.['version'] ?? ''; pageTitle.value = consentTitle(code) })
onShow(() => {
visible = true; syncSession()
// 已完成回执可能已在记录页被撤销;返回后重新展示显式操作,未知请求仍沿用原键。
if (pendingAgreement == null) { granted.value = false; actionMessage.value = '' }
clearInterval(accountTimer); accountTimer = setInterval(() => { if (visible) syncSession() }, 1000); reload()
})
const hidePage = () => { visible = false; generation++; actionGeneration++; submitting.value = false; clearInterval(accountTimer) }
onHide(() => hidePage())
onUnload(() => { hidePage(); resetPending() })
</script>
<style>
.agreement-content { padding: 30rpx; }
.agreement-title { color: #172640; font-size: 38rpx; line-height: 56rpx; font-weight: 700; }
.agreement-meta { color: #66758a; font-size: 23rpx; line-height: 36rpx; margin-top: 12rpx; }
.history-note { padding: 20rpx; margin-top: 22rpx; background-color: #f2f6fc; color: #526680; font-size: 24rpx; line-height: 38rpx; border-radius: 12rpx; }
.agreement-body { color: #40536d; font-size: 28rpx; line-height: 50rpx; margin-top: 32rpx; white-space: pre-wrap; }
.consent-area { margin-top: 36rpx; padding-top: 24rpx; border-top: 1rpx solid #e8eef6; }
.consent-note { color: #66758a; font-size: 24rpx; line-height: 40rpx; margin-top: 12rpx; }
.action-message { color: #31567d; font-size: 25rpx; line-height: 40rpx; margin-top: 20rpx; }
.action-button { margin-top: 24rpx; font-size: 27rpx; }
.empty-content { padding: 100rpx 40rpx; align-items: center; }
.empty-title { color: #172640; font-size: 32rpx; font-weight: 700; }
.empty-copy { color: #66758a; font-size: 26rpx; line-height: 42rpx; margin-top: 20rpx; }
.empty-button { width: 100%; margin-top: 38rpx; }
</style>