267 lines
9.8 KiB
Vue
267 lines
9.8 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Hero Section -->
|
|
<section class="aws-gradient text-white section-padding">
|
|
<div class="container-custom text-center">
|
|
<h1 class="text-5xl font-bold mb-6">
|
|
{{ $t('contact.title') }}
|
|
</h1>
|
|
<p class="text-xl text-white/90 max-w-3xl mx-auto">
|
|
{{ $t('contact.subtitle') }}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Contact Content -->
|
|
<section class="section-padding bg-white">
|
|
<div class="container-custom">
|
|
<div class="grid lg:grid-cols-2 gap-12">
|
|
<!-- Contact Form -->
|
|
<div>
|
|
<h2 class="text-3xl font-bold mb-6">Send us a message</h2>
|
|
<form @submit.prevent="submitForm" class="space-y-6">
|
|
<div>
|
|
<label :for="'name'" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ $t('contact.form.name') }}
|
|
</label>
|
|
<input
|
|
id="name"
|
|
v-model="form.name"
|
|
type="text"
|
|
required
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-aws-orange focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label :for="'email'" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ $t('contact.form.email') }}
|
|
</label>
|
|
<input
|
|
id="email"
|
|
v-model="form.email"
|
|
type="email"
|
|
required
|
|
:class="['w-full px-4 py-3 rounded-lg focus:ring-2 focus:ring-aws-orange focus:border-transparent', emailError ? 'border-red-500' : 'border-gray-300']"
|
|
/>
|
|
<p v-if="emailError" class="mt-1 text-sm text-red-600">{{ emailError }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label :for="'subject'" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ $t('contact.form.subject') }}
|
|
</label>
|
|
<input
|
|
id="subject"
|
|
v-model="form.subject"
|
|
type="text"
|
|
required
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-aws-orange focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label :for="'message'" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ $t('contact.form.message') }}
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
v-model="form.message"
|
|
rows="6"
|
|
required
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-aws-orange focus:border-transparent"
|
|
></textarea>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
:disabled="isSubmitting || !isEmailValid"
|
|
class="btn-primary w-full text-lg py-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span v-if="isSubmitting">Sending...</span>
|
|
<span v-else>{{ $t('contact.form.submit') }}</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Contact Info -->
|
|
<div>
|
|
<h2 class="text-3xl font-bold mb-6">Get in touch</h2>
|
|
|
|
<!-- Social Links -->
|
|
<div class="space-y-6 mb-8">
|
|
<div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
|
|
<div class="w-12 h-12 bg-aws-orange rounded-lg flex items-center justify-center">
|
|
<MessageCircle class="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 class="font-semibold text-lg">{{ $t('contact.telegram') }}</h3>
|
|
<a
|
|
href="https://t.me/pinnovatecloud"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-aws-orange hover:underline"
|
|
>
|
|
@pinnovatecloud
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
|
|
<div class="w-12 h-12 bg-aws-orange rounded-lg flex items-center justify-center">
|
|
<Phone class="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 class="font-semibold text-lg">{{ $t('contact.whatsapp') }}</h3>
|
|
<a
|
|
href="https://wa.me/19174029875"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-aws-orange hover:underline"
|
|
>
|
|
+1 9174029875
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Additional Info -->
|
|
<div class="bg-aws-blue text-white p-6 rounded-lg">
|
|
<h3 class="text-xl font-semibold mb-4">Why choose us?</h3>
|
|
<ul class="space-y-2">
|
|
<li class="flex items-center">
|
|
<Check class="w-5 h-5 text-aws-orange mr-3 flex-shrink-0" />
|
|
<span>24/7 Expert Support</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<Check class="w-5 h-5 text-aws-orange mr-3 flex-shrink-0" />
|
|
<span>Fast Response Time</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<Check class="w-5 h-5 text-aws-orange mr-3 flex-shrink-0" />
|
|
<span>Custom Solutions</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<Check class="w-5 h-5 text-aws-orange mr-3 flex-shrink-0" />
|
|
<span>Competitive Pricing</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Toast Notification -->
|
|
<transition name="fade">
|
|
<div v-if="toast.open" :class="['fixed bottom-6 right-6 z-50 rounded-lg shadow-lg px-5 py-4', toast.type === 'success' ? 'bg-green-600 text-white' : 'bg-red-600 text-white']">
|
|
<div class="flex items-start space-x-3">
|
|
<svg v-if="toast.type === 'success'" class="w-5 h-5 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
<svg v-else class="w-5 h-5 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<div>
|
|
<p class="font-semibold">{{ toast.title }}</p>
|
|
<p class="text-sm opacity-90">{{ toast.message }}</p>
|
|
</div>
|
|
<button class="ml-3 text-white/90 hover:text-white" @click="toast.open = false">✕</button>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { MessageCircle, Phone, Check } from 'lucide-vue-next'
|
|
|
|
// i18n
|
|
const { t, locale } = useI18n()
|
|
|
|
// SEO
|
|
useSeoMeta({
|
|
title: () => t('seo.contact.title'),
|
|
description: () => t('seo.contact.description')
|
|
})
|
|
|
|
const config = useRuntimeConfig()
|
|
const isSubmitting = ref(false)
|
|
const emailError = ref('')
|
|
const isEmailValid = computed(() => {
|
|
const value = String(form.value.email || '').trim()
|
|
// RFC 5322 简化邮箱校验
|
|
const ok = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)
|
|
emailError.value = ok ? '' : (value ? 'Invalid email format' : '')
|
|
return ok
|
|
})
|
|
const toast = reactive({ open: false, type: 'success', title: '', message: '' })
|
|
|
|
const form = ref({
|
|
name: '',
|
|
email: '',
|
|
subject: '',
|
|
message: ''
|
|
})
|
|
|
|
const submitForm = async () => {
|
|
isSubmitting.value = true
|
|
|
|
try {
|
|
if (!isEmailValid.value) {
|
|
emailError.value = 'Invalid email format'
|
|
throw new Error('invalid_email')
|
|
}
|
|
const fd = new FormData()
|
|
// 更友好的字段标签(供邮件展示)
|
|
fd.append('🧑 姓名', form.value.name)
|
|
fd.append('✉️ 邮箱', form.value.email)
|
|
fd.append('🧩 主题', form.value.subject)
|
|
fd.append('💬 消息', form.value.message)
|
|
// 兼容性保留原始字段(便于自动解析和统计)
|
|
fd.append('name', form.value.name)
|
|
fd.append('email', form.value.email)
|
|
fd.append('subject', form.value.subject)
|
|
fd.append('message', form.value.message)
|
|
// 自定义邮件主题/语言/回复地址
|
|
fd.append('_subject', `📨 ${t('contact.title')} | ${form.value.subject}`)
|
|
fd.append('_language', locale.value)
|
|
fd.append('_replyto', form.value.email)
|
|
|
|
const response = await fetch(config.public.formspreeEndpoint, {
|
|
method: 'POST',
|
|
headers: { Accept: 'application/json' },
|
|
body: fd
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Formspree error: ${response.status}`)
|
|
}
|
|
|
|
// Reset form
|
|
form.value = {
|
|
name: '',
|
|
email: '',
|
|
subject: '',
|
|
message: ''
|
|
}
|
|
|
|
// Custom toast success
|
|
toast.type = 'success'
|
|
toast.title = t('contact.title')
|
|
toast.message = 'Your message has been sent successfully.'
|
|
toast.open = true
|
|
} catch (error) {
|
|
console.error('Error submitting form:', error)
|
|
// Custom toast error
|
|
toast.type = 'error'
|
|
toast.title = 'Send failed'
|
|
toast.message = 'There was an error sending your message. Please try again later.'
|
|
toast.open = true
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fade-enter-active, .fade-leave-active { transition: opacity .2s ease; }
|
|
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
</style>
|