feat: 完成所有页面功能和用户认证系统
✨ 新增功能: - Work 和 News 页面实现筛选功能 - Contact 页面联系表单提交功能(加载状态、成功/失败反馈) - Newsletter 订阅功能(邮箱验证、状态反馈) - 移动端汉堡菜单功能(全屏菜单、动画效果) - 用户登录/注册系统(支持账号/邮箱/手机号三合一) - 用户状态管理(Pinia store + localStorage 持久化) - 登录和注册弹窗组件(表单验证、错误提示) - 用户头像和下拉菜单(桌面端和移动端) 🎨 优化改进: - Footer 显示问题修复(多重备用机制) - 联系信息更新(邮箱改为 chenchun@virtheart.com,手机改为微信) - 完整的国际化支持(中英文双语) - 响应式设计优化 🔧 技术实现: - GSAP 动画系统 - 表单验证和反馈 - 状态持久化 - 自动生成用户头像 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm px-4"
|
||||
@click.self="closeModal"
|
||||
>
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="relative w-full max-w-md bg-white rounded-2xl shadow-2xl overflow-hidden"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="absolute top-4 right-4 w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-100 transition-colors z-10"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon name="ph:x" class="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div class="p-8">
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-black mb-2">{{ t('auth.register') }}</h2>
|
||||
<p class="text-gray-600 mb-8">{{ t('auth.registerDescription') }}</p>
|
||||
|
||||
<form @submit.prevent="handleRegister" class="space-y-5">
|
||||
<div>
|
||||
<label for="reg-username" class="block text-sm font-semibold text-gray-800 mb-2">
|
||||
{{ t('auth.username') }}
|
||||
</label>
|
||||
<input
|
||||
id="reg-username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
required
|
||||
:disabled="isRegistering"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-black focus:ring-2 focus:ring-black/5 outline-none transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:placeholder="t('auth.usernamePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg-email" class="block text-sm font-semibold text-gray-800 mb-2">
|
||||
{{ t('auth.email') }}
|
||||
</label>
|
||||
<input
|
||||
id="reg-email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
:disabled="isRegistering"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-black focus:ring-2 focus:ring-black/5 outline-none transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:placeholder="t('auth.emailPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg-phone" class="block text-sm font-semibold text-gray-800 mb-2">
|
||||
{{ t('auth.phone') }}
|
||||
</label>
|
||||
<input
|
||||
id="reg-phone"
|
||||
v-model="phone"
|
||||
type="tel"
|
||||
:disabled="isRegistering"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-black focus:ring-2 focus:ring-black/5 outline-none transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:placeholder="t('auth.phonePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg-password" class="block text-sm font-semibold text-gray-800 mb-2">
|
||||
{{ t('auth.password') }}
|
||||
</label>
|
||||
<input
|
||||
id="reg-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
:disabled="isRegistering"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-black focus:ring-2 focus:ring-black/5 outline-none transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:placeholder="t('auth.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="reg-confirm-password" class="block text-sm font-semibold text-gray-800 mb-2">
|
||||
{{ t('auth.confirmPassword') }}
|
||||
</label>
|
||||
<input
|
||||
id="reg-confirm-password"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
:disabled="isRegistering"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-black focus:ring-2 focus:ring-black/5 outline-none transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:placeholder="t('auth.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息 -->
|
||||
<div v-if="errorMessage" class="p-3 rounded-lg bg-red-50 border border-red-200 text-red-800 text-sm">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 成功消息 -->
|
||||
<div v-if="successMessage" class="p-3 rounded-lg bg-green-50 border border-green-200 text-green-800 text-sm">
|
||||
{{ successMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isRegistering || !username || !email || !password || !confirmPassword"
|
||||
class="w-full py-3 rounded-xl font-semibold text-white transition-all duration-300 flex items-center justify-center gap-2"
|
||||
:class="isRegistering || !username || !email || !password || !confirmPassword
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-black hover:bg-gray-800 shadow-lg hover:shadow-xl'"
|
||||
>
|
||||
<Icon v-if="isRegistering" name="ph:circle-notch" class="w-5 h-5 animate-spin" />
|
||||
<span>{{ isRegistering ? t('auth.registering') : t('auth.registerButton') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 返回登录链接 -->
|
||||
<p class="mt-6 text-center text-sm text-gray-600">
|
||||
{{ t('auth.alreadyHaveAccount') }}
|
||||
<button @click="switchToLogin" class="text-black font-semibold hover:underline">
|
||||
{{ t('auth.backToLogin') }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
switchToLogin: []
|
||||
}>()
|
||||
|
||||
const modalRef = ref<HTMLElement | null>(null)
|
||||
const username = ref('')
|
||||
const email = ref('')
|
||||
const phone = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const isRegistering = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const successMessage = ref('')
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
if (!isRegistering.value) {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到登录
|
||||
const switchToLogin = () => {
|
||||
emit('switchToLogin')
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async () => {
|
||||
if (isRegistering.value) return
|
||||
|
||||
// 验证密码匹配
|
||||
if (password.value !== confirmPassword.value) {
|
||||
errorMessage.value = t('auth.passwordMismatch')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号格式(如果填写了)
|
||||
if (phone.value && !/^1[3-9]\d{9}$/.test(phone.value)) {
|
||||
errorMessage.value = t('auth.invalidPhone')
|
||||
return
|
||||
}
|
||||
|
||||
isRegistering.value = true
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
|
||||
try {
|
||||
const result = await userStore.register({
|
||||
username: username.value,
|
||||
email: email.value,
|
||||
phone: phone.value,
|
||||
password: password.value
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// 注册成功
|
||||
successMessage.value = t('auth.registerSuccess')
|
||||
|
||||
// 清空表单
|
||||
username.value = ''
|
||||
email.value = ''
|
||||
phone.value = ''
|
||||
password.value = ''
|
||||
confirmPassword.value = ''
|
||||
|
||||
// 2秒后关闭并自动登录
|
||||
setTimeout(() => {
|
||||
closeModal()
|
||||
}, 2000)
|
||||
} else {
|
||||
errorMessage.value = result.error || t('auth.registerError')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Register error:', error)
|
||||
errorMessage.value = t('auth.registerError')
|
||||
} finally {
|
||||
isRegistering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听打开状态,重置表单
|
||||
watch(() => props.isOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
username.value = ''
|
||||
email.value = ''
|
||||
phone.value = ''
|
||||
password.value = ''
|
||||
confirmPassword.value = ''
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
// 禁止背景滚动
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
// 恢复滚动
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
|
||||
// ESC 键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && props.isOpen) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加键盘事件监听
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active .relative,
|
||||
.modal-leave-active .relative {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from .relative {
|
||||
transform: scale(0.95) translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-leave-to .relative {
|
||||
transform: scale(0.95) translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user