109 lines
8.7 KiB
Plaintext
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>
|