Hanye官网
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

contact.vue 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <template>
  2. <div class="w-full h-[55px] sm:h-[72px]"></div>
  3. <div
  4. 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"
  5. >
  6. <div class="max-w-screen-xl mx-auto">
  7. <h1 class="text-4xl md:text-6xl mb-12 text-center font-normal text-white">
  8. {{ $t("contact.title") }}
  9. </h1>
  10. <div class="grid grid-cols-1 gap-10 lg:grid-cols-2 lg:gap-16">
  11. <!-- 联系表单 -->
  12. <div
  13. 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"
  14. >
  15. <div
  16. 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"
  17. ></div>
  18. <div class="relative p-6 sm:p-8 lg:p-10">
  19. <div class="mb-8">
  20. <h2
  21. class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block"
  22. >
  23. {{ $t("contact.form.title") }}
  24. </h2>
  25. <div
  26. class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2"
  27. ></div>
  28. </div>
  29. <ErrorBoundary :error="error">
  30. <form @submit.prevent="submitForm" class="space-y-10">
  31. <div class="relative">
  32. <input
  33. type="text"
  34. id="name"
  35. v-model="formData.name"
  36. 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]"
  37. :class="[
  38. formErrors.name
  39. ? 'border-red-500 focus:border-red-500'
  40. : 'border-gray-600 focus:border-blue-500',
  41. ]"
  42. :placeholder="$t('contact.name')"
  43. required
  44. :aria-invalid="formErrors.name ? 'true' : 'false'"
  45. aria-describedby="name-error"
  46. />
  47. <label
  48. for="name"
  49. 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"
  50. :class="[
  51. formErrors.name
  52. ? 'text-red-400 peer-focus:text-red-400'
  53. : 'text-gray-400 peer-focus:text-blue-400',
  54. ]"
  55. >{{ $t("contact.name") }}</label
  56. >
  57. <p
  58. v-if="formErrors.name"
  59. id="name-error"
  60. class="mt-1.5 text-xs text-red-400 sm:text-sm"
  61. >
  62. {{ formErrors.name }}
  63. </p>
  64. </div>
  65. <div class="relative">
  66. <input
  67. type="email"
  68. id="email"
  69. v-model="formData.email"
  70. 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]"
  71. :class="[
  72. formErrors.email
  73. ? 'border-red-500 focus:border-red-500'
  74. : 'border-gray-600 focus:border-blue-500',
  75. ]"
  76. :placeholder="$t('contact.email')"
  77. required
  78. :aria-invalid="formErrors.email ? 'true' : 'false'"
  79. aria-describedby="email-error"
  80. />
  81. <label
  82. for="email"
  83. 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"
  84. :class="[
  85. formErrors.email
  86. ? 'text-red-400 peer-focus:text-red-400'
  87. : 'text-gray-400 peer-focus:text-blue-400',
  88. ]"
  89. >{{ $t("contact.email") }}</label
  90. >
  91. <p
  92. v-if="formErrors.email"
  93. id="email-error"
  94. class="mt-1.5 text-xs text-red-400 sm:text-sm"
  95. >
  96. {{ formErrors.email }}
  97. </p>
  98. </div>
  99. <div class="relative">
  100. <textarea
  101. id="message"
  102. v-model="formData.message"
  103. 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]"
  104. :class="[
  105. formErrors.message
  106. ? 'border-red-500 focus:border-red-500'
  107. : 'border-gray-600 focus:border-blue-500',
  108. ]"
  109. :placeholder="$t('contact.message')"
  110. required
  111. rows="5"
  112. :aria-invalid="formErrors.message ? 'true' : 'false'"
  113. aria-describedby="message-error"
  114. ></textarea>
  115. <label
  116. for="message"
  117. 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"
  118. :class="[
  119. formErrors.message
  120. ? 'text-red-400 peer-focus:text-red-400'
  121. : 'text-gray-400 peer-focus:text-blue-400',
  122. ]"
  123. >{{ $t("contact.message") }}</label
  124. >
  125. <p
  126. v-if="formErrors.message"
  127. id="message-error"
  128. class="mt-1.5 text-xs text-red-400 sm:text-sm"
  129. >
  130. {{ formErrors.message }}
  131. </p>
  132. </div>
  133. <!-- Captcha Section -->
  134. <div class="relative pt-2">
  135. <div class="flex items-center space-x-3">
  136. <!-- Captcha Input -->
  137. <div class="flex-grow relative">
  138. <input
  139. type="text"
  140. id="captcha"
  141. v-model="captcha.userInput.value"
  142. 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]"
  143. :class="[
  144. captcha.error.value
  145. ? 'border-red-500 focus:border-red-500'
  146. : 'border-gray-600 focus:border-blue-500',
  147. ]"
  148. :placeholder="$t('contact.form.captchaLabel')"
  149. required
  150. autocomplete="off"
  151. aria-describedby="captcha-error"
  152. :aria-invalid="captcha.error.value ? 'true' : 'false'"
  153. />
  154. <label
  155. for="captcha"
  156. 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"
  157. :class="[
  158. captcha.error.value
  159. ? 'text-red-400 peer-focus:text-red-400'
  160. : 'text-gray-400 peer-focus:text-blue-400',
  161. ]"
  162. >{{ $t("contact.form.captchaLabel") }}</label
  163. >
  164. </div>
  165. <!-- Captcha Image/SVG -->
  166. <div
  167. 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"
  168. v-html="captcha.captchaSvg.value"
  169. @click="captcha.generateCaptcha()"
  170. :title="$t('contact.form.captchaRefresh')"
  171. style="line-height: 0"
  172. ></div>
  173. <!-- Refresh Button -->
  174. <button
  175. type="button"
  176. @click="captcha.generateCaptcha()"
  177. 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"
  178. :aria-label="$t('contact.form.captchaRefresh')"
  179. :title="$t('contact.form.captchaRefresh')"
  180. >
  181. <svg
  182. xmlns="http://www.w3.org/2000/svg"
  183. class="h-5 w-5"
  184. fill="none"
  185. viewBox="0 0 24 24"
  186. stroke="currentColor"
  187. stroke-width="2"
  188. >
  189. <path
  190. stroke-linecap="round"
  191. stroke-linejoin="round"
  192. 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"
  193. />
  194. </svg>
  195. </button>
  196. </div>
  197. <p
  198. v-if="captcha.error.value"
  199. id="captcha-error"
  200. class="mt-1.5 text-xs text-red-400 sm:text-sm"
  201. >
  202. {{
  203. captcha.error.value === "请输入验证码"
  204. ? $t("contact.validation.captchaRequired")
  205. : $t("contact.validation.captchaIncorrect")
  206. }}
  207. </p>
  208. </div>
  209. <div class="pt-6">
  210. <button
  211. type="submit"
  212. 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"
  213. :disabled="isLoading"
  214. >
  215. <span
  216. v-if="isLoading"
  217. class="flex items-center justify-center"
  218. >
  219. <span
  220. class="animate-spin h-5 w-5 border-2 border-white rounded-full border-t-transparent mr-2.5"
  221. ></span>
  222. {{ $t("contact.form.submitLoading") }}
  223. </span>
  224. <span v-else>{{ $t("contact.submit") }}</span>
  225. </button>
  226. </div>
  227. <div
  228. v-if="submitSuccess"
  229. 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"
  230. role="alert"
  231. >
  232. <svg
  233. xmlns="http://www.w3.org/2000/svg"
  234. class="h-5 w-5 flex-shrink-0"
  235. viewBox="0 0 20 20"
  236. fill="currentColor"
  237. >
  238. <path
  239. fill-rule="evenodd"
  240. 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"
  241. clip-rule="evenodd"
  242. />
  243. </svg>
  244. <span>{{ $t("contact.form.successMessage") }}</span>
  245. </div>
  246. </form>
  247. </ErrorBoundary>
  248. </div>
  249. </div>
  250. <!-- 联系信息 -->
  251. <div
  252. 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"
  253. >
  254. <div
  255. 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"
  256. ></div>
  257. <div class="relative p-6 sm:p-8 lg:p-10">
  258. <div class="mb-8">
  259. <h2
  260. class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block"
  261. >
  262. {{ $t("contact.info.title") }}
  263. </h2>
  264. <div
  265. class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2"
  266. ></div>
  267. </div>
  268. <div class="space-y-8">
  269. <div class="flex items-center">
  270. <div
  271. 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"
  272. >
  273. <svg
  274. xmlns="http://www.w3.org/2000/svg"
  275. class="h-6 w-6"
  276. viewBox="0 0 20 20"
  277. fill="currentColor"
  278. aria-hidden="true"
  279. >
  280. <path
  281. fill-rule="evenodd"
  282. 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"
  283. clip-rule="evenodd"
  284. />
  285. </svg>
  286. </div>
  287. <div class="ml-4">
  288. <h3 class="text-lg font-medium text-gray-100">
  289. {{ $t("contact.info.addressLabel") }}
  290. </h3>
  291. <p class="mt-1 text-base text-gray-400">
  292. {{ $t("contact.info.addressValue1") }}<br />
  293. {{ $t("contact.info.addressValue2") }}
  294. </p>
  295. </div>
  296. </div>
  297. <div class="flex items-center">
  298. <div
  299. 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"
  300. >
  301. <svg
  302. xmlns="http://www.w3.org/2000/svg"
  303. class="h-6 w-6"
  304. viewBox="0 0 20 20"
  305. fill="currentColor"
  306. aria-hidden="true"
  307. >
  308. <path
  309. 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"
  310. />
  311. </svg>
  312. </div>
  313. <div class="ml-4">
  314. <h3 class="text-lg font-medium text-gray-100">
  315. {{ $t("contact.info.phoneLabel") }}
  316. </h3>
  317. <p class="mt-1 text-base text-gray-400">+86 123 456 7890</p>
  318. </div>
  319. </div>
  320. <div class="flex items-center">
  321. <div
  322. 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"
  323. >
  324. <svg
  325. xmlns="http://www.w3.org/2000/svg"
  326. class="h-6 w-6"
  327. viewBox="0 0 20 20"
  328. fill="currentColor"
  329. aria-hidden="true"
  330. >
  331. <path
  332. d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"
  333. />
  334. <path
  335. d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"
  336. />
  337. </svg>
  338. </div>
  339. <div class="ml-4">
  340. <h3 class="text-lg font-medium text-gray-100">
  341. {{ $t("contact.info.emailLabel") }}
  342. </h3>
  343. <p class="mt-1 text-base text-gray-400">
  344. contact@example.com
  345. </p>
  346. </div>
  347. </div>
  348. <div class="flex items-center">
  349. <div
  350. 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"
  351. >
  352. <svg
  353. xmlns="http://www.w3.org/2000/svg"
  354. class="h-6 w-6"
  355. viewBox="0 0 20 20"
  356. fill="currentColor"
  357. aria-hidden="true"
  358. >
  359. <path
  360. fill-rule="evenodd"
  361. 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"
  362. clip-rule="evenodd"
  363. />
  364. </svg>
  365. </div>
  366. <div class="ml-4">
  367. <h3 class="text-lg font-medium text-gray-100">
  368. {{ $t("contact.info.hoursLabel") }}
  369. </h3>
  370. <p class="mt-1 text-base text-gray-400">
  371. {{ $t("contact.info.hoursValue1") }}<br />
  372. {{ $t("contact.info.hoursValue2") }}
  373. </p>
  374. </div>
  375. </div>
  376. </div>
  377. </div>
  378. </div>
  379. </div>
  380. </div>
  381. </div>
  382. </template>
  383. <script setup lang="ts">
  384. /**
  385. * 联系我们页面
  386. * 提供联系表单和联系信息
  387. */
  388. import { useErrorHandler } from "~/composables/useErrorHandler";
  389. import { useCaptcha } from "~/composables/useCaptcha";
  390. import { useI18n } from "vue-i18n";
  391. const { error, isLoading, wrapAsync } = useErrorHandler();
  392. const captcha = useCaptcha();
  393. const submitSuccess = ref(false);
  394. const { t } = useI18n();
  395. // 表单数据
  396. const formData = reactive({
  397. name: "",
  398. email: "",
  399. message: "",
  400. });
  401. // 表单错误
  402. const formErrors = reactive({
  403. name: "",
  404. email: "",
  405. message: "",
  406. });
  407. /**
  408. * 验证表单输入
  409. * @returns 表单是否有效
  410. */
  411. function validateForm(): boolean {
  412. let isValid = true;
  413. // 重置错误
  414. formErrors.name = "";
  415. formErrors.email = "";
  416. formErrors.message = "";
  417. // 验证姓名
  418. if (!formData.name.trim()) {
  419. formErrors.name = t("contact.validation.nameRequired");
  420. isValid = false;
  421. }
  422. // 验证邮箱
  423. if (!formData.email.trim()) {
  424. formErrors.email = t("contact.validation.emailRequired");
  425. isValid = false;
  426. } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
  427. formErrors.email = t("contact.validation.emailInvalid");
  428. isValid = false;
  429. }
  430. // 验证消息
  431. if (!formData.message.trim()) {
  432. formErrors.message = t("contact.validation.messageRequired");
  433. isValid = false;
  434. }
  435. return isValid;
  436. }
  437. /**
  438. * 提交表单
  439. */
  440. async function submitForm() {
  441. // 重置成功状态
  442. submitSuccess.value = false;
  443. // 验证表单(姓名、邮箱、消息)
  444. if (!validateForm()) {
  445. return;
  446. }
  447. // 验证验证码
  448. if (!captcha.validateCaptcha()) {
  449. return;
  450. }
  451. // 提交表单数据
  452. await wrapAsync(async () => {
  453. // 模拟API请求
  454. console.log("Form Data:", formData);
  455. console.log("Captcha Validated!");
  456. await new Promise((resolve) => setTimeout(resolve, 1500));
  457. // 模拟成功响应
  458. submitSuccess.value = true;
  459. // 清空表单和验证码
  460. formData.name = "";
  461. formData.email = "";
  462. formData.message = "";
  463. captcha.generateCaptcha(); // 成功后也刷新验证码
  464. return true;
  465. });
  466. }
  467. // SEO优化
  468. useHead({
  469. title: t("contact.meta.title"),
  470. meta: [
  471. {
  472. name: "description",
  473. content: t("contact.meta.description"),
  474. },
  475. ],
  476. });
  477. </script>