123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 |
- <template>
- <div class="w-full h-[55px] sm:h-[72px]"></div>
- <div
- class="max-w-full px-4 py-16 md:px-8 lg:px-10 bg-gradient-to-br from-gray-900 via-gray-900 to-black text-gray-300 min-h-screen"
- >
- <div class="max-w-screen-xl mx-auto">
- <h1 class="text-4xl md:text-6xl mb-12 text-center font-normal text-white">
- {{ $t("contact.title") }}
- </h1>
-
- <div class="grid grid-cols-1 gap-10 lg:grid-cols-2 lg:gap-16">
- <!-- 联系表单 -->
- <div
- class="relative bg-gray-800/70 border border-gray-700 rounded-xl overflow-hidden shadow-2xl backdrop-blur-sm transition-all duration-300 ease-in-out hover:shadow-blue-500/30 hover:border-blue-500/50 group"
- >
- <div
- class="absolute inset-0 bg-gradient-to-r from-transparent via-blue-900/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
- ></div>
- <div class="relative p-6 sm:p-8 lg:p-10">
- <div class="mb-8">
- <h2
- class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block"
- >
- {{ $t("contact.form.title") }}
- </h2>
- <div
- class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2"
- ></div>
- </div>
-
- <ErrorBoundary :error="error">
- <form @submit.prevent="submitForm" class="space-y-10">
- <div class="relative">
- <input
- type="text"
- id="name"
- v-model="formData.name"
- class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px]"
- :class="[
- formErrors.name
- ? 'border-red-500 focus:border-red-500'
- : 'border-gray-600 focus:border-blue-500',
- ]"
- :placeholder="$t('contact.name')"
- required
- :aria-invalid="formErrors.name ? 'true' : 'false'"
- aria-describedby="name-error"
- />
- <label
- for="name"
- class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium"
- :class="[
- formErrors.name
- ? 'text-red-400 peer-focus:text-red-400'
- : 'text-gray-400 peer-focus:text-blue-400',
- ]"
- >{{ $t("contact.name") }}</label
- >
- <p
- v-if="formErrors.name"
- id="name-error"
- class="mt-1.5 text-xs text-red-400 sm:text-sm"
- >
- {{ formErrors.name }}
- </p>
- </div>
-
- <div class="relative">
- <input
- type="email"
- id="email"
- v-model="formData.email"
- class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px]"
- :class="[
- formErrors.email
- ? 'border-red-500 focus:border-red-500'
- : 'border-gray-600 focus:border-blue-500',
- ]"
- :placeholder="$t('contact.email')"
- required
- :aria-invalid="formErrors.email ? 'true' : 'false'"
- aria-describedby="email-error"
- />
- <label
- for="email"
- class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium"
- :class="[
- formErrors.email
- ? 'text-red-400 peer-focus:text-red-400'
- : 'text-gray-400 peer-focus:text-blue-400',
- ]"
- >{{ $t("contact.email") }}</label
- >
- <p
- v-if="formErrors.email"
- id="email-error"
- class="mt-1.5 text-xs text-red-400 sm:text-sm"
- >
- {{ formErrors.email }}
- </p>
- </div>
-
- <div class="relative">
- <textarea
- id="message"
- v-model="formData.message"
- class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent h-36 resize-none transition-colors duration-300 focus:border-b-[3px]"
- :class="[
- formErrors.message
- ? 'border-red-500 focus:border-red-500'
- : 'border-gray-600 focus:border-blue-500',
- ]"
- :placeholder="$t('contact.message')"
- required
- rows="5"
- :aria-invalid="formErrors.message ? 'true' : 'false'"
- aria-describedby="message-error"
- ></textarea>
- <label
- for="message"
- class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium"
- :class="[
- formErrors.message
- ? 'text-red-400 peer-focus:text-red-400'
- : 'text-gray-400 peer-focus:text-blue-400',
- ]"
- >{{ $t("contact.message") }}</label
- >
- <p
- v-if="formErrors.message"
- id="message-error"
- class="mt-1.5 text-xs text-red-400 sm:text-sm"
- >
- {{ formErrors.message }}
- </p>
- </div>
-
- <!-- Captcha Section -->
- <div class="relative pt-2">
- <div class="flex items-center space-x-3">
- <!-- Captcha Input -->
- <div class="flex-grow relative">
- <input
- type="text"
- id="captcha"
- v-model="captcha.userInput.value"
- class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px]"
- :class="[
- captcha.error.value
- ? 'border-red-500 focus:border-red-500'
- : 'border-gray-600 focus:border-blue-500',
- ]"
- :placeholder="$t('contact.form.captchaLabel')"
- required
- autocomplete="off"
- aria-describedby="captcha-error"
- :aria-invalid="captcha.error.value ? 'true' : 'false'"
- />
- <label
- for="captcha"
- class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium"
- :class="[
- captcha.error.value
- ? 'text-red-400 peer-focus:text-red-400'
- : 'text-gray-400 peer-focus:text-blue-400',
- ]"
- >{{ $t("contact.form.captchaLabel") }}</label
- >
- </div>
- <!-- Captcha Image/SVG -->
- <div
- class="flex-shrink-0 cursor-pointer rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:scale-105 hover:shadow-md active:scale-100"
- v-html="captcha.captchaSvg.value"
- @click="captcha.generateCaptcha()"
- :title="$t('contact.form.captchaRefresh')"
- style="line-height: 0"
- ></div>
- <!-- Refresh Button -->
- <button
- type="button"
- @click="captcha.generateCaptcha()"
- class="flex-shrink-0 p-2 text-gray-500 hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out"
- :aria-label="$t('contact.form.captchaRefresh')"
- :title="$t('contact.form.captchaRefresh')"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-5 w-5"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- stroke-width="2"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
- />
- </svg>
- </button>
- </div>
- <p
- v-if="captcha.error.value"
- id="captcha-error"
- class="mt-1.5 text-xs text-red-400 sm:text-sm"
- >
- {{
- captcha.error.value === "请输入验证码"
- ? $t("contact.validation.captchaRequired")
- : $t("contact.validation.captchaIncorrect")
- }}
- </p>
- </div>
-
- <div class="pt-6">
- <button
- type="submit"
- class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3.5 px-6 rounded-lg shadow-lg hover:shadow-xl hover:from-blue-500 hover:to-purple-500 transform hover:-translate-y-0.5 transition-all duration-300 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-md text-base tracking-wide focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 focus-visible:ring-offset-gray-900"
- :disabled="isLoading"
- >
- <span
- v-if="isLoading"
- class="flex items-center justify-center"
- >
- <span
- class="animate-spin h-5 w-5 border-2 border-white rounded-full border-t-transparent mr-2.5"
- ></span>
- {{ $t("contact.form.submitLoading") }}
- </span>
- <span v-else>{{ $t("contact.submit") }}</span>
- </button>
- </div>
-
- <div
- v-if="submitSuccess"
- class="mt-6 p-4 bg-green-600/20 text-green-300 rounded-lg border border-green-500/50 text-sm flex items-center space-x-2"
- role="alert"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-5 w-5 flex-shrink-0"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path
- fill-rule="evenodd"
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
- clip-rule="evenodd"
- />
- </svg>
- <span>{{ $t("contact.form.successMessage") }}</span>
- </div>
- </form>
- </ErrorBoundary>
- </div>
- </div>
-
- <!-- 联系信息 -->
- <div
- class="relative bg-gray-800/70 border border-gray-700 rounded-xl overflow-hidden shadow-2xl backdrop-blur-sm transition-all duration-300 ease-in-out hover:shadow-purple-500/30 hover:border-purple-500/50 group"
- >
- <div
- class="absolute inset-0 bg-gradient-to-r from-transparent via-purple-900/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
- ></div>
- <div class="relative p-6 sm:p-8 lg:p-10">
- <div class="mb-8">
- <h2
- class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block"
- >
- {{ $t("contact.info.title") }}
- </h2>
- <div
- class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2"
- ></div>
- </div>
-
- <div class="space-y-8">
- <div class="flex items-center">
- <div
- class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-6 w-6"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
- clip-rule="evenodd"
- />
- </svg>
- </div>
- <div class="ml-4">
- <h3 class="text-lg font-medium text-gray-100">
- {{ $t("contact.info.addressLabel") }}
- </h3>
- <p class="mt-1 text-base text-gray-400">
- {{ $t("contact.info.addressValue1") }}<br />
- {{ $t("contact.info.addressValue2") }}
- </p>
- </div>
- </div>
-
- <div class="flex items-center">
- <div
- class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-6 w-6"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z"
- />
- </svg>
- </div>
- <div class="ml-4">
- <h3 class="text-lg font-medium text-gray-100">
- {{ $t("contact.info.phoneLabel") }}
- </h3>
- <p class="mt-1 text-base text-gray-400">+86 123 456 7890</p>
- </div>
- </div>
-
- <div class="flex items-center">
- <div
- class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-6 w-6"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"
- />
- <path
- d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"
- />
- </svg>
- </div>
- <div class="ml-4">
- <h3 class="text-lg font-medium text-gray-100">
- {{ $t("contact.info.emailLabel") }}
- </h3>
- <p class="mt-1 text-base text-gray-400">
- contact@example.com
- </p>
- </div>
- </div>
-
- <div class="flex items-center">
- <div
- class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-6 w-6"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
- clip-rule="evenodd"
- />
- </svg>
- </div>
- <div class="ml-4">
- <h3 class="text-lg font-medium text-gray-100">
- {{ $t("contact.info.hoursLabel") }}
- </h3>
- <p class="mt-1 text-base text-gray-400">
- {{ $t("contact.info.hoursValue1") }}<br />
- {{ $t("contact.info.hoursValue2") }}
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
-
- <script setup lang="ts">
- /**
- * 联系我们页面
- * 提供联系表单和联系信息
- */
- import { useErrorHandler } from "~/composables/useErrorHandler";
- import { useCaptcha } from "~/composables/useCaptcha";
- import { useI18n } from "vue-i18n";
-
- const { error, isLoading, wrapAsync } = useErrorHandler();
- const captcha = useCaptcha();
- const submitSuccess = ref(false);
- const { t } = useI18n();
-
- // 表单数据
- const formData = reactive({
- name: "",
- email: "",
- message: "",
- });
-
- // 表单错误
- const formErrors = reactive({
- name: "",
- email: "",
- message: "",
- });
-
- /**
- * 验证表单输入
- * @returns 表单是否有效
- */
- function validateForm(): boolean {
- let isValid = true;
-
- // 重置错误
- formErrors.name = "";
- formErrors.email = "";
- formErrors.message = "";
-
- // 验证姓名
- if (!formData.name.trim()) {
- formErrors.name = t("contact.validation.nameRequired");
- isValid = false;
- }
-
- // 验证邮箱
- if (!formData.email.trim()) {
- formErrors.email = t("contact.validation.emailRequired");
- isValid = false;
- } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
- formErrors.email = t("contact.validation.emailInvalid");
- isValid = false;
- }
-
- // 验证消息
- if (!formData.message.trim()) {
- formErrors.message = t("contact.validation.messageRequired");
- isValid = false;
- }
-
- return isValid;
- }
-
- /**
- * 提交表单
- */
- async function submitForm() {
- // 重置成功状态
- submitSuccess.value = false;
-
- // 验证表单(姓名、邮箱、消息)
- if (!validateForm()) {
- return;
- }
-
- // 验证验证码
- if (!captcha.validateCaptcha()) {
- return;
- }
-
- // 提交表单数据
- await wrapAsync(async () => {
- // 模拟API请求
- console.log("Form Data:", formData);
- console.log("Captcha Validated!");
- await new Promise((resolve) => setTimeout(resolve, 1500));
-
- // 模拟成功响应
- submitSuccess.value = true;
-
- // 清空表单和验证码
- formData.name = "";
- formData.email = "";
- formData.message = "";
- captcha.generateCaptcha(); // 成功后也刷新验证码
-
- return true;
- });
- }
-
- // SEO优化
- useHead({
- title: t("contact.meta.title"),
- meta: [
- {
- name: "description",
- content: t("contact.meta.description"),
- },
- ],
- });
- </script>
|