feat: 实现协议全文与同意记录功能
新增协议全文后台管理、版本化发布及本人同意记录功能,支持可撤销授权;完善资料与授权相关边界提示,调整评估提交时的资料与授权校验逻辑,同步更新相关页面文案与路由。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user