Files
sub2api-cn-relay-manager/frontend/src/components/auth/LoginAgreementPrompt.vue
2026-05-07 17:35:05 +08:00

222 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
v-if="mode === 'checkbox' && documents.length > 0"
class="px-0.5"
>
<div class="flex items-start gap-2">
<input
id="login-agreement-consent"
type="checkbox"
:checked="accepted"
class="mt-[2px] h-4 w-4 flex-shrink-0 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-900"
@change="handleCheckboxChange"
/>
<div class="min-w-0 flex-1">
<p class="text-[13px] leading-5 text-gray-600 dark:text-dark-300">
<label
for="login-agreement-consent"
class="cursor-pointer text-gray-700 dark:text-dark-200"
>
我已阅读并同意
</label>
<template v-for="(doc, index) in documents" :key="doc.id || doc.title">
<RouterLink
:to="documentRoute(doc)"
target="_blank"
rel="noopener noreferrer"
class="font-medium text-primary-600 underline-offset-4 transition hover:text-primary-700 hover:underline dark:text-primary-300 dark:hover:text-primary-200"
>
{{ doc.title }}
</RouterLink>
<span v-if="index < documents.length - 1"></span>
</template>
</p>
</div>
</div>
</div>
<div
v-else-if="!accepted && documents.length > 0"
class="rounded-lg border border-primary-100 bg-primary-50/70 p-3 text-sm text-primary-900 dark:border-primary-500/20 dark:bg-primary-500/10 dark:text-primary-100"
>
<div class="flex items-start gap-3">
<Icon name="shield" size="sm" class="mt-0.5 flex-shrink-0 text-primary-600 dark:text-primary-300" />
<div class="min-w-0 flex-1">
<p class="font-medium">继续登录前需要先同意最新条款</p>
<p class="mt-1 text-primary-700 dark:text-primary-200/80">
未同意前账号密码输入和快捷登录会保持禁用
</p>
</div>
<button
type="button"
class="flex-shrink-0 rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-primary-700"
@click="emit('open')"
>
查看条款
</button>
</div>
</div>
<Teleport to="body">
<Transition name="agreement-fade">
<div
v-if="dialogVisible"
class="fixed inset-0 z-[140] flex items-center justify-center overflow-y-auto bg-gray-950/60 p-4 backdrop-blur-sm"
>
<div class="w-full max-w-[600px] overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/10 dark:bg-dark-900 dark:ring-white/10">
<div class="border-b border-gray-100 bg-white px-6 py-6 dark:border-dark-800 dark:bg-dark-900">
<div class="flex items-start gap-4">
<span class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-primary-50 text-primary-700 ring-1 ring-primary-100 dark:bg-primary-500/10 dark:text-primary-300 dark:ring-primary-500/20">
<Icon name="shield" size="md" />
</span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h2 class="text-xl font-bold tracking-normal text-gray-950 dark:text-white">
条款更新通知
</h2>
<span
v-if="updatedAt"
class="rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-600 dark:bg-dark-800 dark:text-dark-300"
>
{{ updatedAt }}
</span>
</div>
<p class="mt-2 text-sm leading-6 text-gray-600 dark:text-dark-300">
我们的服务条款已于 {{ updatedAt || '近期' }} 更新在继续使用服务之前请仔细阅读并同意以下条款
</p>
</div>
</div>
</div>
<div class="max-h-[58vh] overflow-y-auto px-6 py-5">
<div class="mb-3 flex items-center justify-between gap-3">
<p class="text-sm font-semibold text-gray-900 dark:text-white">相关文档</p>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<RouterLink
v-for="(doc, index) in documents"
:key="doc.id || doc.title"
:to="documentRoute(doc)"
target="_blank"
rel="noopener noreferrer"
class="group flex min-h-[72px] w-full items-center gap-3 rounded-xl border border-gray-200 bg-gray-50/70 px-4 py-3 text-left transition hover:-translate-y-0.5 hover:border-primary-200 hover:bg-white hover:shadow-sm dark:border-dark-700 dark:bg-dark-800/70 dark:hover:border-primary-500/30 dark:hover:bg-dark-800"
>
<span class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-white text-gray-700 ring-1 ring-gray-200 transition group-hover:bg-primary-50 group-hover:text-primary-700 group-hover:ring-primary-100 dark:bg-dark-900 dark:text-dark-200 dark:ring-dark-700 dark:group-hover:bg-primary-500/10 dark:group-hover:text-primary-200 dark:group-hover:ring-primary-500/20">
<Icon :name="documentIcon(index, doc.title)" size="sm" />
</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-semibold text-gray-950 dark:text-white">{{ doc.title }}</span>
</span>
<span class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-gray-400 transition group-hover:bg-primary-50 group-hover:text-primary-600 dark:group-hover:bg-primary-500/10 dark:group-hover:text-primary-300">
<Icon name="externalLink" size="sm" />
</span>
</RouterLink>
</div>
</div>
<div class="border-t border-gray-100 bg-gray-50/80 px-6 py-4 dark:border-dark-800 dark:bg-dark-950/60">
<div class="grid grid-cols-2 gap-3">
<button
type="button"
class="rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm font-semibold text-gray-700 transition hover:bg-gray-100 dark:border-dark-700 dark:bg-dark-800 dark:text-dark-200 dark:hover:bg-dark-700"
@click="emit('reject')"
>
拒绝
</button>
<button
type="button"
class="rounded-xl bg-primary-600 px-4 py-3 text-sm font-semibold text-white shadow-sm shadow-primary-600/20 transition hover:bg-primary-700"
@click="emit('accept')"
>
同意并继续
</button>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Icon from '@/components/icons/Icon.vue'
import type { LoginAgreementDocument } from '@/types'
const props = withDefaults(defineProps<{
accepted: boolean
documents: LoginAgreementDocument[]
mode: 'modal' | 'checkbox' | string
updatedAt?: string
visible: boolean
}>(), {
updatedAt: ''
})
const emit = defineEmits<{
accept: []
reject: []
open: []
}>()
const dialogVisible = computed(() => props.visible && documents.value.length > 0)
const documents = computed(() => props.documents.filter((doc) => doc.title.trim()))
const updatedAt = computed(() => props.updatedAt || '')
const accepted = computed(() => props.accepted)
const mode = computed(() => props.mode === 'checkbox' ? 'checkbox' : 'modal')
function documentRoute(doc: LoginAgreementDocument) {
return {
name: 'LegalDocument',
params: {
documentId: doc.id || doc.title,
},
}
}
function handleCheckboxChange(event: Event): void {
const checked = (event.target as HTMLInputElement).checked
if (checked) {
emit('accept')
} else {
emit('reject')
}
}
function documentIcon(index: number, title: string): 'document' | 'shield' | 'globe' | 'cog' {
if (title.includes('政策') || title.includes('隐私')) {
return 'shield'
}
if (title.includes('国家') || title.includes('地区')) {
return 'globe'
}
if (index === 3) {
return 'cog'
}
return 'document'
}
</script>
<style scoped>
.agreement-fade-enter-active,
.agreement-fade-leave-active {
transition: opacity 0.18s ease;
}
.agreement-fade-enter-from,
.agreement-fade-leave-to {
opacity: 0;
}
.agreement-fade-enter-active > div,
.agreement-fade-leave-active > div {
transition: transform 0.18s ease, opacity 0.18s ease;
}
.agreement-fade-enter-from > div,
.agreement-fade-leave-to > div {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
</style>