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 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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 @submit="handleSubmit" class="flex flex-col gap-6">
  39. <div class="relative">
  40. <input
  41. v-model="form.name"
  42. type="text"
  43. id="name"
  44. 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"
  45. :placeholder="t('contact.name')"
  46. required
  47. />
  48. <label
  49. for="name"
  50. 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"
  51. >
  52. {{ t("contact.name") }}
  53. </label>
  54. </div>
  55. <div class="relative">
  56. <input
  57. v-model="form.email"
  58. type="email"
  59. id="email"
  60. 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"
  61. :placeholder="t('contact.email')"
  62. required
  63. />
  64. <label
  65. for="email"
  66. 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"
  67. >
  68. {{ t("contact.email") }}
  69. </label>
  70. </div>
  71. <div class="relative">
  72. <textarea
  73. v-model="form.message"
  74. id="message"
  75. 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"
  76. :placeholder="t('contact.message')"
  77. required
  78. ></textarea>
  79. <label
  80. for="message"
  81. 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"
  82. >
  83. {{ t("contact.message") }}
  84. </label>
  85. </div>
  86. <!-- 验证码部分 -->
  87. <div class="relative pt-2">
  88. <div class="flex items-center space-x-3">
  89. <div class="flex-grow relative">
  90. <input
  91. type="text"
  92. id="captcha"
  93. v-model="captcha.state.userInput"
  94. 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]"
  95. :class="[
  96. captcha.error.value
  97. ? 'border-red-500 focus:border-red-500'
  98. : 'border-gray-600 focus:border-cyan-400',
  99. ]"
  100. :placeholder="t('contact.captcha')"
  101. required
  102. autocomplete="off"
  103. aria-describedby="captcha-error"
  104. :aria-invalid="captcha.error.value ? 'true' : 'false'"
  105. />
  106. <label
  107. for="captcha"
  108. 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"
  109. :class="[
  110. captcha.error.value
  111. ? 'text-red-400 peer-focus:text-red-400'
  112. : 'text-gray-400 peer-focus:text-cyan-400',
  113. ]"
  114. >
  115. {{ t("contact.captcha") }}
  116. </label>
  117. </div>
  118. <div
  119. 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"
  120. v-html="captcha.captchaSvg.value"
  121. @click="refreshCaptcha"
  122. :title="t('contact.refreshCaptcha')"
  123. style="line-height: 0"
  124. ></div>
  125. <button
  126. type="button"
  127. @click="refreshCaptcha"
  128. 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"
  129. :aria-label="t('contact.refreshCaptcha')"
  130. :title="t('contact.refreshCaptcha')"
  131. >
  132. <svg
  133. xmlns="http://www.w3.org/2000/svg"
  134. class="h-5 w-5"
  135. fill="none"
  136. viewBox="0 0 24 24"
  137. stroke="currentColor"
  138. stroke-width="2"
  139. >
  140. <path
  141. stroke-linecap="round"
  142. stroke-linejoin="round"
  143. 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"
  144. />
  145. </svg>
  146. </button>
  147. </div>
  148. <p
  149. v-if="captcha.error.value"
  150. id="captcha-error"
  151. class="mt-1.5 text-xs text-red-400 sm:text-sm"
  152. >
  153. {{ captcha.error.value }}
  154. </p>
  155. </div>
  156. <!-- 表单提交状态提示 -->
  157. <div
  158. v-if="formStatus.show"
  159. class="px-1 py-2 rounded-md transition-all duration-300"
  160. :class="[
  161. formStatus.type === 'success'
  162. ? 'bg-green-500/20 text-green-400'
  163. : 'bg-red-500/20 text-red-400',
  164. ]"
  165. >
  166. {{ formStatus.message }}
  167. </div>
  168. <button
  169. type="submit"
  170. 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"
  171. :disabled="isSubmitting"
  172. >
  173. {{
  174. isSubmitting
  175. ? t("contact.submitting")
  176. : t("contact.submit")
  177. }}
  178. </button>
  179. </form>
  180. </div>
  181. <!-- 公司信息和营业时间 -->
  182. <div class="flex flex-col gap-8">
  183. <div
  184. class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
  185. >
  186. <div class="text-white text-3xl font-medium mb-6">
  187. {{ t("about.companyInfo.name") }}
  188. </div>
  189. <div class="flex flex-col gap-4">
  190. <div class="flex items-center gap-4">
  191. <div class="text-white/60 text-base font-normal">
  192. {{ t("about.overview.companyName") }}
  193. </div>
  194. <div class="text-white text-base font-normal">
  195. {{ t("about.companyInfo.companyName") }}
  196. </div>
  197. </div>
  198. <div class="flex items-center gap-4">
  199. <div class="text-white/60 text-base font-normal">
  200. {{ t("about.overview.location") }}
  201. </div>
  202. <div class="text-white text-base font-normal">
  203. {{ t("about.companyInfo.location") }}
  204. </div>
  205. </div>
  206. <div class="flex items-center gap-4">
  207. <div class="text-white/60 text-base font-normal">
  208. {{ t("about.overview.tel") }}
  209. </div>
  210. <div class="text-white text-base font-normal">
  211. +86-024-83990696
  212. </div>
  213. </div>
  214. <div class="flex items-center gap-4">
  215. <div class="text-white/60 text-base font-normal">
  216. {{ t("about.overview.email") }}
  217. </div>
  218. <div class="text-white text-base font-normal">
  219. hanye@hanye.cn
  220. </div>
  221. </div>
  222. </div>
  223. </div>
  224. <div
  225. class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
  226. >
  227. <div class="text-white text-3xl font-medium mb-6">
  228. {{ t("about.overview.businessHours") }}
  229. </div>
  230. <div class="text-white text-base font-normal">
  231. {{ t("about.companyInfo.businessHours") }}
  232. </div>
  233. </div>
  234. </div>
  235. </div>
  236. </div>
  237. </div>
  238. </div>
  239. </ErrorBoundary>
  240. </div>
  241. </template>
  242. <script setup lang="ts">
  243. /**
  244. * 联系页面
  245. * 展示公司信息和联系表单
  246. */
  247. import { useErrorHandler } from "~/composables/useErrorHandler";
  248. import { useCaptcha } from "~/composables/useCaptcha";
  249. const { t, locale } = useI18n();
  250. const homepagePath = computed(() => {
  251. return locale.value === "zh" ? "" : `/${locale.value}`;
  252. });
  253. const { error, isLoading, wrapAsync } = useErrorHandler();
  254. const captcha = useCaptcha();
  255. const isSubmitting = ref(false);
  256. const submitSuccess = ref(false);
  257. // 确保页面载入后验证码正确初始化
  258. onMounted(() => {
  259. // 给页面元素加载的时间
  260. setTimeout(() => {
  261. refreshCaptcha();
  262. }, 100);
  263. });
  264. // 表单状态信息
  265. const formStatus = reactive({
  266. show: false,
  267. type: "success",
  268. message: "",
  269. });
  270. /**
  271. * 刷新验证码
  272. */
  273. function refreshCaptcha() {
  274. captcha.generateCaptcha();
  275. }
  276. // 显示表单状态信息
  277. function showFormStatus(type: "success" | "error", message: string) {
  278. formStatus.show = true;
  279. formStatus.type = type;
  280. formStatus.message = message;
  281. // 5秒后自动隐藏
  282. setTimeout(() => {
  283. formStatus.show = false;
  284. }, 5000);
  285. }
  286. const form = ref({
  287. name: "",
  288. email: "",
  289. message: "",
  290. });
  291. /**
  292. * 处理表单提交
  293. */
  294. async function handleSubmit(event: Event) {
  295. // 阻止表单默认提交行为,防止重复提交
  296. event.preventDefault();
  297. if (isSubmitting.value) {
  298. return; // 如果已经在提交中,不重复处理
  299. }
  300. isSubmitting.value = true;
  301. submitSuccess.value = false;
  302. try {
  303. // 验证验证码
  304. const isCaptchaValid = captcha.validateCaptcha();
  305. if (!isCaptchaValid) {
  306. isSubmitting.value = false;
  307. return;
  308. }
  309. // 使用单独的异步函数发送邮件,减少嵌套
  310. await sendEmail();
  311. } catch (error) {
  312. console.error("Error during form submission:", error);
  313. showFormStatus("error", t("contact.submitError"));
  314. } finally {
  315. isSubmitting.value = false;
  316. }
  317. }
  318. /**
  319. * 发送邮件的函数
  320. */
  321. async function sendEmail() {
  322. try {
  323. const result = await wrapAsync(async () => {
  324. const message_template = `
  325. 📨 来自Hanye官网的联系表单
  326. 姓名:${form.value.name}
  327. 邮箱:${form.value.email}
  328. 消息内容:
  329. ${form.value.message}
  330. 提交时间:${new Date().toLocaleString()}
  331. 来源页面:${window.location.href}
  332. ---
  333. 此邮件由Hanye官网的联系表单自动发送,请勿回复。
  334. `;
  335. const title = `您有一封来自Hanye官网的联系表单`;
  336. const receiveEmail = "hanye@hanye.cn";
  337. // 对包含HTML标签的参数进行URL编码
  338. const encodedMessage = encodeURIComponent(message_template);
  339. const encodedTitle = encodeURIComponent(title);
  340. // 发送邮件
  341. const response = await fetch(
  342. `https://digital.sohomall.jp/prod-api/system/zsEmail/sendEmailWeb?receiveEmail=${receiveEmail}&context=${encodedMessage}&title=${encodedTitle}`,
  343. {
  344. method: "GET",
  345. headers: {
  346. "Content-Type": "application/json",
  347. },
  348. }
  349. );
  350. if (response.status === 200) {
  351. // 清空表单
  352. form.value = {
  353. name: "",
  354. email: "",
  355. message: "",
  356. };
  357. captcha.state.userInput = ""; // 清空验证码输入
  358. captcha.generateCaptcha(); // 成功后刷新验证码
  359. submitSuccess.value = true;
  360. showFormStatus("success", t("contact.submitSuccess"));
  361. return { success: true };
  362. } else {
  363. showFormStatus("error", t("contact.submitError"));
  364. throw new Error("Failed to send email");
  365. }
  366. });
  367. return result;
  368. } catch (error) {
  369. console.error("Error sending email:", error);
  370. showFormStatus("error", t("contact.submitError"));
  371. throw error;
  372. }
  373. }
  374. // SEO优化
  375. useHead({
  376. title: t("contact.title") + " - Hanye",
  377. meta: [
  378. {
  379. name: "description",
  380. content: t("contact.description"),
  381. },
  382. {
  383. name: "keywords",
  384. content: t("contact.keywords"),
  385. },
  386. ],
  387. });
  388. </script>