Hanye官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

contact.vue 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <template>
  2. <div>
  3. <div class="w-full h-[55px] sm:h-[72px]"></div>
  4. <ErrorBoundary :error="error">
  5. <div v-if="isLoading" class="flex justify-center py-12">
  6. <div
  7. class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent"
  8. ></div>
  9. </div>
  10. <div v-else>
  11. <div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-20">
  12. <div class="max-w-screen-2xl mx-auto">
  13. <nuxt-link
  14. :to="`${homepagePath}/`"
  15. class="justify-start text-white/60 text-base font-normal"
  16. >{{ t("common.home") }}</nuxt-link
  17. >
  18. <span class="text-white/60 text-base font-normal px-2"> / </span>
  19. <nuxt-link
  20. :to="`${homepagePath}/contact`"
  21. class="text-white text-base font-normal"
  22. >{{ t("contact.title") }}</nuxt-link
  23. >
  24. </div>
  25. </div>
  26. <div
  27. class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"
  28. >
  29. <div class="max-w-screen-2xl mx-auto">
  30. <div class="w-full grid grid-cols-1 md:grid-cols-2 gap-8">
  31. <!-- 联系表单 -->
  32. <div
  33. class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
  34. >
  35. <div class="text-white text-3xl font-medium mb-6">
  36. {{ t("contact.title") }}
  37. </div>
  38. <form
  39. @submit.prevent="handleSubmit"
  40. class="flex flex-col gap-6"
  41. >
  42. <div class="relative">
  43. <input
  44. v-model="form.name"
  45. type="text"
  46. id="name"
  47. 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] border-gray-600 focus:border-cyan-400"
  48. :placeholder="t('contact.name')"
  49. required
  50. />
  51. <label
  52. for="name"
  53. 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 text-gray-400 peer-focus:text-cyan-400"
  54. >
  55. {{ t("contact.name") }}
  56. </label>
  57. </div>
  58. <div class="relative">
  59. <input
  60. v-model="form.email"
  61. type="email"
  62. id="email"
  63. 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] border-gray-600 focus:border-cyan-400"
  64. :placeholder="t('contact.email')"
  65. required
  66. />
  67. <label
  68. for="email"
  69. 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 text-gray-400 peer-focus:text-cyan-400"
  70. >
  71. {{ t("contact.email") }}
  72. </label>
  73. </div>
  74. <div class="relative">
  75. <textarea
  76. v-model="form.message"
  77. id="message"
  78. 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] border-gray-600 focus:border-cyan-400"
  79. :placeholder="t('contact.message')"
  80. required
  81. ></textarea>
  82. <label
  83. for="message"
  84. 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 text-gray-400 peer-focus:text-cyan-400"
  85. >
  86. {{ t("contact.message") }}
  87. </label>
  88. </div>
  89. <!-- 验证码部分 -->
  90. <div class="relative pt-2">
  91. <div class="flex items-center space-x-3">
  92. <div class="flex-grow relative">
  93. <input
  94. type="text"
  95. id="captcha"
  96. v-model="captcha.userInput.value"
  97. 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]"
  98. :class="[
  99. captcha.error.value
  100. ? 'border-red-500 focus:border-red-500'
  101. : 'border-gray-600 focus:border-cyan-400',
  102. ]"
  103. :placeholder="t('contact.captcha')"
  104. required
  105. autocomplete="off"
  106. aria-describedby="captcha-error"
  107. :aria-invalid="captcha.error.value ? 'true' : 'false'"
  108. />
  109. <label
  110. for="captcha"
  111. 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"
  112. :class="[
  113. captcha.error.value
  114. ? 'text-red-400 peer-focus:text-red-400'
  115. : 'text-gray-400 peer-focus:text-cyan-400',
  116. ]"
  117. >
  118. {{ t("contact.captcha") }}
  119. </label>
  120. </div>
  121. <div
  122. class="flex-shrink-0 cursor-pointer select-none rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:shadow-md active:scale-100"
  123. v-html="captcha.captchaSvg.value"
  124. @click="captcha.generateCaptcha()"
  125. :title="t('contact.refreshCaptcha')"
  126. style="line-height: 0"
  127. ></div>
  128. <button
  129. type="button"
  130. @click="captcha.generateCaptcha()"
  131. class="flex-shrink-0 p-2 text-gray-500 hover:text-cyan-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400 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"
  132. :aria-label="t('contact.refreshCaptcha')"
  133. :title="t('contact.refreshCaptcha')"
  134. >
  135. <svg
  136. xmlns="http://www.w3.org/2000/svg"
  137. class="h-5 w-5"
  138. fill="none"
  139. viewBox="0 0 24 24"
  140. stroke="currentColor"
  141. stroke-width="2"
  142. >
  143. <path
  144. stroke-linecap="round"
  145. stroke-linejoin="round"
  146. 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"
  147. />
  148. </svg>
  149. </button>
  150. </div>
  151. <p
  152. v-if="captcha.error.value"
  153. id="captcha-error"
  154. class="mt-1.5 text-xs text-red-400 sm:text-sm"
  155. >
  156. {{ captcha.error.value }}
  157. </p>
  158. </div>
  159. <button
  160. type="submit"
  161. class="bg-cyan-400 text-zinc-900 relative text-base font-normal py-4 px-8 rounded-lg transition-all duration-300 hover:shadow-lg hover:bg-cyan-500"
  162. :disabled="isSubmitting"
  163. >
  164. {{
  165. isSubmitting
  166. ? t("contact.submitting")
  167. : t("contact.submit")
  168. }}
  169. </button>
  170. </form>
  171. </div>
  172. <!-- 公司信息和营业时间 -->
  173. <div class="flex flex-col gap-8">
  174. <div
  175. class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
  176. >
  177. <div class="text-white text-3xl font-medium mb-6">
  178. {{ t("about.companyInfo.name") }}
  179. </div>
  180. <div class="flex flex-col gap-4">
  181. <div class="flex items-center gap-4">
  182. <div class="text-white/60 text-base font-normal">
  183. {{ t("about.overview.companyName") }}
  184. </div>
  185. <div class="text-white text-base font-normal">
  186. {{ t("about.companyInfo.companyName") }}
  187. </div>
  188. </div>
  189. <div class="flex items-center gap-4">
  190. <div class="text-white/60 text-base font-normal">
  191. {{ t("about.overview.location") }}
  192. </div>
  193. <div class="text-white text-base font-normal">
  194. {{ t("about.companyInfo.location") }}
  195. </div>
  196. </div>
  197. <div class="flex items-center gap-4">
  198. <div class="text-white/60 text-base font-normal">
  199. {{ t("about.overview.tel") }}
  200. </div>
  201. <div class="text-white text-base font-normal">
  202. 86)024-8399-0696
  203. </div>
  204. </div>
  205. <div class="flex items-center gap-4">
  206. <div class="text-white/60 text-base font-normal">
  207. {{ t("about.overview.email") }}
  208. </div>
  209. <div class="text-white text-base font-normal">
  210. hanye@hanye.cn
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. <div
  216. class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
  217. >
  218. <div class="text-white text-3xl font-medium mb-6">
  219. {{ t("about.overview.businessHours") }}
  220. </div>
  221. <div class="text-white text-base font-normal">
  222. {{ t("about.companyInfo.businessHours") }}
  223. </div>
  224. </div>
  225. </div>
  226. </div>
  227. </div>
  228. </div>
  229. </div>
  230. </ErrorBoundary>
  231. </div>
  232. </template>
  233. <script setup lang="ts">
  234. /**
  235. * 联系页面
  236. * 展示公司信息和联系表单
  237. */
  238. import { useErrorHandler } from "~/composables/useErrorHandler";
  239. import { useCaptcha } from "~/composables/useCaptcha";
  240. import emailjs from "@emailjs/browser";
  241. const { t, locale } = useI18n();
  242. const homepagePath = computed(() => {
  243. return locale.value === "zh" ? "" : `/${locale.value}`;
  244. });
  245. const { error, isLoading, wrapAsync } = useErrorHandler();
  246. const captcha = useCaptcha();
  247. const isSubmitting = ref(false);
  248. const submitSuccess = ref(false);
  249. const form = ref({
  250. name: "",
  251. email: "",
  252. message: "",
  253. });
  254. /**
  255. * 处理表单提交
  256. * 使用 EmailJS 发送邮件
  257. */
  258. async function handleSubmit() {
  259. isSubmitting.value = true;
  260. submitSuccess.value = false;
  261. try {
  262. await wrapAsync(async () => {
  263. // 验证验证码
  264. if (!captcha.validateCaptcha()) {
  265. return;
  266. }
  267. // 发送邮件
  268. const response = await fetch(
  269. `https://192.168.1.104/dev-api/system/zsEmail/sendEmailWeb?receiveEmail=${form.value.email}&context=${form.value.message}&title=[Hanye Website]${form.value.name}`,
  270. {
  271. method: "GET",
  272. headers: {
  273. "Content-Type": "application/json",
  274. },
  275. }
  276. );
  277. if (response.status === 200) {
  278. // 清空表单
  279. form.value = {
  280. name: "",
  281. email: "",
  282. message: "",
  283. };
  284. captcha.generateCaptcha(); // 成功后刷新验证码
  285. submitSuccess.value = true;
  286. return { success: true };
  287. } else {
  288. throw new Error("Failed to send email");
  289. }
  290. });
  291. } catch (error) {
  292. console.error("Error sending email:", error);
  293. throw error;
  294. } finally {
  295. isSubmitting.value = false;
  296. }
  297. }
  298. // SEO优化
  299. useHead({
  300. title: t("contact.title") + " - Hanye",
  301. meta: [
  302. {
  303. name: "description",
  304. content: t("contact.description"),
  305. },
  306. {
  307. name: "keywords",
  308. content: t("contact.keywords"),
  309. },
  310. ],
  311. });
  312. </script>