Commit a96c3fee87e3ab70dbf04c014c5530cd931a7c53

Authored by boyang
1 parent 5359076c

feat: 开发邮件发送,prod2-55/56

nuxt.config.ts
... ... @@ -8,13 +8,16 @@ export default defineNuxtConfig({
8 8 link: [{ rel: "icon", type: "image/x-icon", href: "/fav.ico" }],
9 9 },
10 10 },
  11 +
11 12 css: ["~/assets/css/main.css"],
  13 +
12 14 postcss: {
13 15 plugins: {
14 16 tailwindcss: {},
15 17 autoprefixer: {},
16 18 },
17 19 },
  20 +
18 21 // runtimeConfig: {
19 22 // public: {
20 23 // baseURL: 'http://47.89.254.121:8002/shop' || 'http://39.108.227.113:8002' // Exposed to the frontend as well.
... ... @@ -26,10 +29,12 @@ export default defineNuxtConfig({
26 29 "@nuxtjs/i18n",
27 30 "@stefanobartoletti/nuxt-social-share",
28 31 ],
  32 +
29 33 // optional configuration, should be added manually
30 34 socialShare: {
31 35 // module options
32 36 },
  37 +
33 38 vuetify: {
34 39 moduleOptions: {
35 40 /* module specific options */
... ... @@ -47,6 +52,7 @@ export default defineNuxtConfig({
47 52 /* vuetify options */
48 53 },
49 54 },
  55 +
50 56 nitro: {
51 57 devProxy: {
52 58 "/shop": {
... ... @@ -55,6 +61,12 @@ export default defineNuxtConfig({
55 61 // target: process.env.BASE_URL || 'http://39.108.227.113:8002/shop', // 目标接口域名
56 62 changeOrigin: true, // 表示是否跨域
57 63 },
  64 + "/email": {
  65 + target: "http://47.89.254.121:8002/email", // 线上代理地址
  66 + // target: "http://127.0.0.1:8002/shop",
  67 + // target: process.env.BASE_URL || 'http://39.108.227.113:8002/shop', // 目标接口域名
  68 + changeOrigin: true, // 表示是否跨域
  69 + },
58 70 "/api/front/cal": {
59 71 target: "http://www.canrd.com/mshop/api/front/cal",
60 72 // proxy: "http://127.0.0.1:8002/shop/**",
... ... @@ -69,6 +81,11 @@ export default defineNuxtConfig({
69 81 // proxy: "http://127.0.0.1:8002/shop/**",
70 82 // proxy: process.env.BASE_URL || 'http://39.108.227.113:8002/shop/**'
71 83 },
  84 + "/email/**": {
  85 + proxy: "http://47.89.254.121:8002/email/**",
  86 + // proxy: "http://127.0.0.1:8002/shop/**",
  87 + // proxy: process.env.BASE_URL || 'http://39.108.227.113:8002/shop/**'
  88 + },
72 89  
73 90 "/api/front/cal/**": {
74 91 proxy: "http://www.canrd.com/mshop/api/front/cal/**",
... ... @@ -77,6 +94,7 @@ export default defineNuxtConfig({
77 94 },
78 95 },
79 96 },
  97 +
80 98 i18n: {
81 99 locales: [
82 100 { code: "en", iso: "en-US", file: "en.json" },
... ... @@ -86,4 +104,6 @@ export default defineNuxtConfig({
86 104 langDir: "locales/",
87 105 defaultLocale: "zh",
88 106 },
  107 +
  108 + compatibilityDate: "2024-12-30",
89 109 });
... ...
package-lock.json
... ... @@ -9,6 +9,8 @@
9 9 "dependencies": {
10 10 "@pinia/nuxt": "^0.5.1",
11 11 "@stefanobartoletti/nuxt-social-share": "^1.2.0",
  12 + "@vuelidate/core": "^2.0.3",
  13 + "@vuelidate/validators": "^2.0.4",
12 14 "lodash": "^4.17.21",
13 15 "nuxt": "^3.11.2",
14 16 "vue": "^3.4.27",
... ... @@ -2877,6 +2879,90 @@
2877 2879 "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz",
2878 2880 "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg=="
2879 2881 },
  2882 + "node_modules/@vuelidate/core": {
  2883 + "version": "2.0.3",
  2884 + "resolved": "https://registry.npmmirror.com/@vuelidate/core/-/core-2.0.3.tgz",
  2885 + "integrity": "sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==",
  2886 + "dependencies": {
  2887 + "vue-demi": "^0.13.11"
  2888 + },
  2889 + "peerDependencies": {
  2890 + "@vue/composition-api": "^1.0.0-rc.1",
  2891 + "vue": "^2.0.0 || >=3.0.0"
  2892 + },
  2893 + "peerDependenciesMeta": {
  2894 + "@vue/composition-api": {
  2895 + "optional": true
  2896 + }
  2897 + }
  2898 + },
  2899 + "node_modules/@vuelidate/core/node_modules/vue-demi": {
  2900 + "version": "0.13.11",
  2901 + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz",
  2902 + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
  2903 + "hasInstallScript": true,
  2904 + "bin": {
  2905 + "vue-demi-fix": "bin/vue-demi-fix.js",
  2906 + "vue-demi-switch": "bin/vue-demi-switch.js"
  2907 + },
  2908 + "engines": {
  2909 + "node": ">=12"
  2910 + },
  2911 + "funding": {
  2912 + "url": "https://github.com/sponsors/antfu"
  2913 + },
  2914 + "peerDependencies": {
  2915 + "@vue/composition-api": "^1.0.0-rc.1",
  2916 + "vue": "^3.0.0-0 || ^2.6.0"
  2917 + },
  2918 + "peerDependenciesMeta": {
  2919 + "@vue/composition-api": {
  2920 + "optional": true
  2921 + }
  2922 + }
  2923 + },
  2924 + "node_modules/@vuelidate/validators": {
  2925 + "version": "2.0.4",
  2926 + "resolved": "https://registry.npmmirror.com/@vuelidate/validators/-/validators-2.0.4.tgz",
  2927 + "integrity": "sha512-odTxtUZ2JpwwiQ10t0QWYJkkYrfd0SyFYhdHH44QQ1jDatlZgTh/KRzrWVmn/ib9Gq7H4hFD4e8ahoo5YlUlDw==",
  2928 + "dependencies": {
  2929 + "vue-demi": "^0.13.11"
  2930 + },
  2931 + "peerDependencies": {
  2932 + "@vue/composition-api": "^1.0.0-rc.1",
  2933 + "vue": "^2.0.0 || >=3.0.0"
  2934 + },
  2935 + "peerDependenciesMeta": {
  2936 + "@vue/composition-api": {
  2937 + "optional": true
  2938 + }
  2939 + }
  2940 + },
  2941 + "node_modules/@vuelidate/validators/node_modules/vue-demi": {
  2942 + "version": "0.13.11",
  2943 + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz",
  2944 + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
  2945 + "hasInstallScript": true,
  2946 + "bin": {
  2947 + "vue-demi-fix": "bin/vue-demi-fix.js",
  2948 + "vue-demi-switch": "bin/vue-demi-switch.js"
  2949 + },
  2950 + "engines": {
  2951 + "node": ">=12"
  2952 + },
  2953 + "funding": {
  2954 + "url": "https://github.com/sponsors/antfu"
  2955 + },
  2956 + "peerDependencies": {
  2957 + "@vue/composition-api": "^1.0.0-rc.1",
  2958 + "vue": "^3.0.0-0 || ^2.6.0"
  2959 + },
  2960 + "peerDependenciesMeta": {
  2961 + "@vue/composition-api": {
  2962 + "optional": true
  2963 + }
  2964 + }
  2965 + },
2880 2966 "node_modules/@vuetify/loader-shared": {
2881 2967 "version": "2.0.3",
2882 2968 "resolved": "https://registry.npmmirror.com/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz",
... ...
package.json
... ... @@ -12,6 +12,8 @@
12 12 "dependencies": {
13 13 "@pinia/nuxt": "^0.5.1",
14 14 "@stefanobartoletti/nuxt-social-share": "^1.2.0",
  15 + "@vuelidate/core": "^2.0.3",
  16 + "@vuelidate/validators": "^2.0.4",
15 17 "lodash": "^4.17.21",
16 18 "nuxt": "^3.11.2",
17 19 "vue": "^3.4.27",
... ...
pages/about.vue
1 1 <template>
2 2 <HotProducts />
3 3  
4   - <!-- <v-tabs
5   - class="tabs"
6   - v-model="tabRecom"
7   - style="margin-top: 25px"
8   - color="white"
9   - bg-color="#eeeeee"
10   - slider-color="blue-lighten-1"
11   - selected-class="active"
12   - v-if="recommendImages[0] !== null && !isMobile()"
13   - >
14   - <v-tab :value="1">Best Sellers</v-tab>
15   - </v-tabs>
16   - <div id="image-container" v-if="recommendImages[0] !== null && !isMobile()">
17   - <div class="recommend-left-box">
18   - <v-img
19   - src="https://m-canrd.oss-cn-shenzhen.aliyuncs.com/crmebimage/public/maintain/2024/09/14/76c987e13a334be481a346c19c2284f3qy4tjnxps7.png"
20   - alt="往左移"
21   - class="recommend-img-left"
22   - @click="toggleRowLeft"
23   - />
24   - </div>
25   - <div class="image-row" id="row1">
26   - <div
27   - v-for="(imageObj, index) in recommendImages"
28   - :key="'row1-' + index"
29   - class="imageTotal"
30   - >
31   - <a v-if="imageObj" :href="imageObj[0]?.productUrl" target="_blank">
32   - <img
33   - :src="imageObj[0]?.url"
34   - :alt="'Image ' + (index + 1)"
35   - class="item-imgHot"
36   - />
37   - <span class="image-name">
38   - {{ imageObj[0]?.name }}
39   - </span>
40   - </a>
41   - <div v-else style="width: 200px; height: 200px"></div>
42   - </div>
43   - </div>
44   - <div class="recommend-right-box">
45   - <v-img
46   - src="https://m-canrd.oss-cn-shenzhen.aliyuncs.com/crmebimage/public/maintain/2024/09/14/b5daa0a8f2f140f5b406e984c730a453iznzlekysg.png"
47   - alt="往右移"
48   - class="recommend-img-right"
49   - @click="toggleRowRight"
50   - />
51   - </div>
52   - </div>
53   - <div style="padding-left: 16px; padding-right: 16px; margin-bottom: 30px">
54   - <v-tabs
55   - class="tabs2"
56   - ref="tabs2"
57   - v-model="tabRecom"
58   - style="margin-top: 25px; margin-bottom: 20px"
59   - color="white"
60   - bg-color="#eeeeee"
61   - slider-color="blue-lighten-1"
62   - selected-class="active"
63   - v-if="isMobile()"
64   - >
65   - <v-tab :value="1">Best Sellers</v-tab>
66   - </v-tabs>
67   - <div class="tw-text-center" v-if="hotLoading && isMobile()">
68   - <v-progress-circular
69   - color="blue-lighten-2"
70   - indeterminate
71   - size="64"
72   - class="tw-m-auto"
73   - ></v-progress-circular>
74   - </div>
75   - <v-item-group multiple v-if="isMobile()">
76   - <v-row v-if="!hotLoading">
77   - <v-col
78   - v-for="(item, i) in recommendImages"
79   - :key="i"
80   - cols="6"
81   - lg="3"
82   - md="4"
83   - sm="6"
84   - >
85   - <div v-if="item !== null">
86   - <v-card :elevation="4" class="mx-auto" :href="item[0].productUrl">
87   - <v-img
88   - :src="item[0].url"
89   - :alt="item[0].name"
90   - eager
91   - class="d-block"
92   - />
93   - <v-card-text class="tw-text-left font-weight-medium title">
94   - <h4>{{ item[0].name }}</h4>
95   - </v-card-text>
96   - </v-card>
97   - </div>
98   - </v-col>
99   - </v-row>
100   - </v-item-group>
101   - <v-row v-if="isMobile()">
102   - <v-col>
103   - <v-pagination
104   - :size="isMobile() ? 'small' : 'default'"
105   - v-if="hotTotal"
106   - v-model="currentIndex"
107   - @update:modelValue="toggleRowMobile"
108   - :length="hotLength"
109   - rounded="0"
110   - class="tw-float-right tw-mt-[32px]"
111   - total-visible="5"
112   - ></v-pagination></v-col
113   - ></v-row>
114   - </div> -->
115 4 <v-container class="pa-0 pa-sm-4">
116 5 <div
117 6 class="my-8 my-sm-16 text-blue-darken-1 text-h4 tw-text-center"
... ... @@ -579,117 +468,6 @@ import MainTitle from &quot;../components/MainTitle.vue&quot;;
579 468 import HotProducts from "../components/HotProducts.vue";
580 469 import { isMobile } from "../utils";
581 470  
582   -const productStore = useProductListStore();
583   -const categoryStore = useCategoryStore();
584   -const loading = ref(false);
585   -const hotLoading = ref(false);
586   -const route = useRoute(); // 获取路由信息
587   -const router = useRouter(); // 获取路由信息
588   -const title = ref("");
589   -const keywordTitle = ref("");
590   -const maxPage = ref(1);
591   -const tabRecom = ref();
592   -const recommendList = ref();
593   -const recommendImages = ref();
594   -const currentIndex = ref(1);
595   -const hotTotal = ref(10);
596   -const isOrNotMobile = isMobile();
597   -
598   -// const loadHotProducts = async () => {
599   -// const pageSize = ref(5);
600   -// if (isOrNotMobile) {
601   -// pageSize.value = 4;
602   -// }
603   -// hotLoading.value = true;
604   -// let { data: hotProducts } = await useAsyncData(
605   -// "hotProducts",
606   -// () =>
607   -// $fetch("/shop/product/hotProducts", {
608   -// method: "GET",
609   -// params: {
610   -// pageNo: currentIndex.value,
611   -// pageSize: pageSize.value,
612   -// },
613   -// }),
614   -// {
615   -// server: true, // 仅在服务器端获取数据
616   -// }
617   -// );
618   -// hotTotal.value = hotProducts.value.data.total;
619   -// recommendList.value = hotProducts.value.data.records;
620   -// maxPage.value = hotProducts.value.data.pages;
621   -// // recommendImages.value = recommendList.value.slice(0, 10).map((item) => {
622   -// recommendImages.value = Array.from({ length: pageSize.value }).map(
623   -// (_, index) => {
624   -// const item = recommendList.value[index];
625   -// if (!item) {
626   -// return null;
627   -// }
628   -// // 检查 productimageliststore 是否为字符串格式,如果是,则尝试解析
629   -// if (typeof item.productimageliststore === "string") {
630   -// try {
631   -// item.productimageliststore = JSON.parse(item.productimageliststore);
632   -// } catch (error) {
633   -// item.productimageliststore = []; // 解析失败时,设置为空数组
634   -// }
635   -// }
636   -// const ree = (item.productimageliststore = item?.productimageliststore.map(
637   -// (productItem: ProductImage) => ({
638   -// ...productItem,
639   -// // url: `http://112.74.45.244:8100/api/show/image?fileKey=${item.fileKey}`,
640   -// url: `https://www.canrud.com/api/show/image?fileKey=${productItem.fileKey}&psize=p256`,
641   -// name: item.name,
642   -// productUrl: `https://www.canrud.com/products/detail/${item.id}`,
643   -// })
644   -// ));
645   -// return ree;
646   -// }
647   -// );
648   -// hotLoading.value = false;
649   -// };
650   -
651   -// let intervalId: any;
652   -// const toggleRowLeft = () => {
653   -// if (currentIndex.value !== 1) {
654   -// currentIndex.value--;
655   -// } else if (currentIndex.value == 1) {
656   -// currentIndex.value = maxPage.value;
657   -// }
658   -// startTimer();
659   -// };
660   -// const toggleRowRight = () => {
661   -// if (currentIndex.value < maxPage.value) {
662   -// currentIndex.value++;
663   -// } else if (currentIndex.value == maxPage.value) {
664   -// currentIndex.value = 1;
665   -// }
666   -// startTimer();
667   -// };
668   -// const startTimer = () => {
669   -// // 清除已有计时器,防止重复
670   -// clearInterval(intervalId);
671   -// intervalId = setInterval(() => {
672   -// toggleRowRight();
673   -// }, 5000); // 每6秒调用一次
674   -// };
675   -
676   -// onMounted(() => {
677   -// startTimer();
678   -// });
679   -// const toggleRowMobile = (value: number) => {
680   -// currentIndex.value = value;
681   -// };
682   -
683   -// const hotLength = computed(() =>
684   -// hotTotal.value ? Math.ceil(hotTotal.value / 4) : 0
685   -// );
686   -
687   -// watch(currentIndex, (newIndex) => {
688   -// loadHotProducts(); // Call loadHotProducts when currentIndex changes
689   -// });
690   -// // Initial load of hot products
691   -// await loadHotProducts(); // Load hot products the first time
692   -
693 471 useHead({
694 472 title: "About Us",
695 473 meta: [
... ...
pages/contact.vue
... ... @@ -3,12 +3,12 @@
3 3 Contact Us
4 4 </div>
5 5 <v-card class="pa-10 tw-max-w-[800px] tw-m-auto">
6   - <!-- <h3 class="text-h5 tw-mb-5">Official Web</h3>
  6 + <h3 class="text-h5 tw-mb-5">Official Web</h3>
7 7 <div class="tw-mb-10">
8 8 <label class="text-subtitle-1 tw-mr-4 tw-w-20 tw-inline-block">URL</label>
9 9 <span>http://www.canrud.com</span>
10 10 </div>
11   - <h3 class="text-h5 tw-mb-5">Technical Center</h3> -->
  11 + <h3 class="text-h5 tw-mb-5">Technical Center</h3>
12 12 <!-- <div>
13 13 <label class="text-subtitle-1 tw-mr-4 tw-w-20 tw-inline-block">QQ</label>
14 14 <span>3632191327</span>
... ... @@ -71,30 +71,30 @@
71 71 href="https://www.amazon.com/s?me=A3A2SQ086XUS66&marketplaceID=ATVPDKIKX0DER"
72 72 rel="noopener noreferrer"
73 73 style="display: flex; align-items: center; gap: 8px"
74   - ><svg
75   - t="1733207196625"
  74 + >
  75 + <svg
76 76 class="icon"
77 77 viewBox="0 0 1126 1024"
78   - version="1.1"
79   - xmlns="http://www.w3.org/2000/svg"
80   - p-id="7890"
81 78 width="20"
82 79 height="20"
  80 + aria-hidden="true"
83 81 >
84 82 <path
85 83 d="M2.008553 794.830142c3.382073-5.461063 8.7738-5.813248 16.349072-1.021335q256.233047 148.673652 557.663492 148.72648 200.998387 0 396.851576-74.949297l14.798359-6.589155c6.483499-2.818578 10.993664-4.686257 13.777024-6.096097 10.606261-4.122762 18.322407-2.149427 24.665032 6.096097 5.637156 8.173986 4.228417 15.785577-5.637156 22.550824-12.015 8.914674-28.187979 19.273305-47.249974 30.724809q-87.681873 52.376462-196.611486 81.07621c-71.879788 19.061995-143.054106 28.645819-212.149434 28.645819q-159.614488 0-302.316409-55.777245A840.520599 840.520599 0 0 1 7.046995 810.829231c-4.686257-3.488829-7.043693-7.043693-7.043693-10.323413a9.795136 9.795136 0 0 1 2.075688-5.745012z m308.411405-292.13277q0-70.822133 34.918008-121.063475c23.255193-33.367295 54.96612-58.736697 95.839351-75.860575 37.384401-15.750358 82.484948-27.02577 136.817135-33.825135 18.322407-2.149427 48.518939-4.827131 90.201094-8.173986v-17.37591c0-43.692909-4.932786-73.188374-14.09399-88.087986-14.197444-20.190086-36.649216-30.548717-67.651371-30.548718h-8.562489c-22.550824 2.149427-42.105877 9.196422-58.525386 21.598825-16.454727 12.684151-27.02577 29.597819-31.710926 51.478391-2.818578 14.09399-9.685078 21.845354-20.436616 23.959562l-118.389073-14.798359c-11.666117-2.818578-17.476063-8.456834-17.476063-18.322406a28.0372 28.0372 0 0 1 1.021336-7.043693q17.389117-90.907664 85.514836-135.302742C463.72704 20.401397 516.544833 3.522947 577.149209 0h25.369401c77.516943 0 138.931344 20.401397 182.658371 60.604376 6.342626 7.043693 12.684151 14.09399 19.026776 22.550824 5.637156 7.751364 10.535824 14.763141 13.283965 21.140984a90.185686 90.185686 0 0 1 9.161204 26.77814c2.818578 11.944563 4.932786 19.731145 6.342625 23.959563 1.409839 4.897568 2.924233 14.09399 3.559266 28.892349 0.45784 14.692704 0.950899 23.149538 0.950899 25.973618v248.049156a153.121084 153.121084 0 0 0 7.751364 48.659813c4.932786 14.692704 9.865573 25.369402 14.798359 31.675708l23.959562 31.675709c4.228417 6.377844 6.377844 12.015 6.377844 16.912567 0 5.637156-2.818578 10.606261-8.456834 14.763141-56.375959 49.328964-87.385818 76.107104-92.209647 80.335521q-11.627597 9.513388-29.597818 2.114208a284.646645 284.646645 0 0 1-24.700251-23.290411l-14.549629-16.312753c-2.818578-3.487729-7.786583-9.865573-14.904014-19.731146l-14.096191-20.436615c-38.053552 41.612818-75.297079 67.651371-112.751918 78.221313-23.222176 7.043693-51.337517 10.675597-85.973777 10.675597-52.150844 0.001101-95.843752-16.102543-129.664485-48.587175-33.825135-32.416396-50.736602-78.221313-50.736602-138.12242z m176.312444-20.58079c0 26.602048 6.589155 47.919125 19.978775 64.092104 13.38962 15.958367 31.710927 24.065218 54.258449 24.065218a65.995002 65.995002 0 0 0 9.161203-0.950899 54.539096 54.539096 0 0 1 7.786583-1.091772c28.85713-7.504835 50.736602-25.973618 66.915084-55.359026a147.597288 147.597288 0 0 0 16.912568-42.739809 183.627979 183.627979 0 0 0 6.342625-37.595712c0.704369-9.161203 0.704369-25.369402 0.70437-47.214755v-25.361698c-39.463391 0-69.730361 2.818578-90.201094 8.456834-59.898906 16.912568-90.201094 54.96612-90.201094 114.161757z m430.435684 330.116985a34.135498 34.135498 0 0 1 6.201752-7.997893c17.018223-11.416286 33.543387-19.273305 49.328964-23.501722 25.827242-6.23697 51.196643-10.429068 75.719701-11.275412a62.565604 62.565604 0 0 1 19.273305 1.409839c30.548717 2.818578 49.328964 7.892238 55.071775 15.503829 2.959452 4.228417 4.228417 10.711916 4.228417 18.322407v7.043693c0 23.959562-6.589155 52.147542-19.480213 84.563938-13.072654 32.416396-31.18265 58.631041-54.296969 78.926783-3.417292 2.818578-6.589155 4.228417-9.266859 4.228417a10.956245 10.956245 0 0 1-4.228417-0.563496c-4.228417-2.07899-5.038442-5.637156-2.99467-11.275412 25.362798-59.190134 37.875259-100.659878 37.875258-124.022927 0-7.043693-1.409839-12.684151-4.087543-16.17298-6.80817-7.788784-25.830543-12.081034-57.507352-12.081034q-17.123878 0-40.87213 2.149427c-17.058944 2.114209-32.874237 4.228417-46.968226 6.342625-4.228417 0-6.941339-0.669151-8.456834-2.07899-1.409839-1.409839-1.691587-2.219864-0.950899-3.631904a7.546657 7.546657 0 0 1 0.950899-2.959452z"
86 84 fill="#FF9900"
87   - p-id="7891"
88   - ></path></svg
89   - >Amazon</a
90   - >
  85 + ></path>
  86 + </svg>
  87 + Amazon
  88 + </a>
91 89 </span>
  90 +
92 91 <span class="tw-mt-2">
93 92 <a
94 93 href="https://canrd.en.alibaba.com/company_profile.html?spm=a2700.galleryofferlist.normal_offer.d_companyName.262213a0fqshG2"
95 94 rel="noopener noreferrer"
96 95 style="display: flex; align-items: center; gap: 8px"
97   - ><svg
  96 + >
  97 + <svg
98 98 t="1733207242907"
99 99 class="icon"
100 100 viewBox="0 0 1651 1024"
... ... @@ -108,9 +108,10 @@
108 108 d="M972.403613 749.997419c-59.986581 4.195097-54.172903-27.912258-18.531097-74.520774 81.259355-108.378839 231.787355-255.636645 238.558968-363.22271 9.348129-139.660387-131.138065-182.899613-275.819355-182.899612-100.64929 2.576516-204.833032 30.488774-275.819355 55.824516-244.504774 86.280258-397.708387 221.745548-494.988387 374.189419-100.64929 150.627097-69.367742 295.473548 148.050581 299.668645 164.203355-6.771613 275.026581-52.422194 386.64258-109.997419 0.792774 0-310.503226 88.856774-425.653677 23.717161-12.750452-6.804645-25.335742-16.152774-28.738065-42.28129 0-53.380129 88.097032-109.171613 139.726452-126.942968v-91.43329c104.018581 36.434581 226.733419 26.293677 331.742968-51.62942 3.402323 9.381161 6.771613 21.140645 5.945806 33.891097h17.771355c4.195097-36.467613-20.314839-71.944258-60.977548-74.520774 11.792516 9.348129 20.314839 16.945548 24.509935 23.717161l-1.585548 1.618581-0.825807 0.792774c-135.300129 94.835613-266.603355 50.803613-279.188645 48.227097l75.313549-73.728-21.140646-53.380129c149.867355-52.422194 273.408-90.640516 478.901678-126.909936l-45.980903-37.128258 23.717161-14.336c121.756903 33.858065 203.875097 59.193806 199.68 123.540645-1.618581 10.96671-5.945806 23.717161-12.750452 37.260388-36.302452 71.944258-142.897548 187.920516-186.136774 237.898322-27.879226 33.06529-55.824516 63.554065-75.313548 94.042839-21.933419 31.281548-33.032258 60.151742-33.858065 86.280258 4.195097 212.595613 631.279484-99.823484 754.820129-182.106839-180.157935 77.09729-375.642839 150.82529-588.07329 164.368516z m137.083871-488.547096c4.558452 8.390194 6.639484 18.696258 6.639484 30.819096a75.379613 75.379613 0 0 0-6.606452-30.819096z"
109 109 fill="#FF6600"
110 110 p-id="9033"
111   - ></path></svg
112   - >Alibaba</a
113   - >
  111 + ></path>
  112 + </svg>
  113 + Alibaba
  114 + </a>
114 115 </span>
115 116 <span class="tw-mt-2">
116 117 <a
... ... @@ -137,34 +138,104 @@
137 138 d="M406.293333 644.821333l-0.064-287.722666 276.693334 144.362666-276.629334 143.36z"
138 139 fill="#FFFFFF"
139 140 p-id="14165"
140   - ></path></svg
141   - >Youtube</a
142   - >
  141 + ></path>
  142 + </svg>
  143 + Youtube
  144 + </a>
143 145 </span>
144 146 <span class="tw-mt-2">
145 147 <a
146 148 href="https://x.com/canrdenerge?s=11"
147 149 rel="noopener noreferrer"
  150 + class="link-container"
148 151 style="display: flex; align-items: center; gap: 8px"
149   - ><svg
150   - t="1733207912677"
  152 + >
  153 + <svg
151 154 class="icon"
  155 + t="1733207912677"
152 156 viewBox="0 0 1024 1024"
153   - version="1.1"
154 157 xmlns="http://www.w3.org/2000/svg"
155   - p-id="22952"
156 158 width="20"
157 159 height="20"
158 160 >
159 161 <path
160 162 d="M1024 186.368a410.325333 410.325333 0 0 1-120.618667 33.877333 214.954667 214.954667 0 0 0 92.373334-119.125333 413.781333 413.781333 0 0 1-133.504 52.181333A207.189333 207.189333 0 0 0 708.949333 85.333333c-115.968 0-210.005333 96.426667-210.005333 215.424 0 16.896 1.792 33.28 5.376 49.066667-174.592-9.002667-329.386667-94.72-433.066667-225.152a219.306667 219.306667 0 0 0-28.416 108.373333c0 74.709333 37.12 140.672 93.44 179.328a206.250667 206.250667 0 0 1-95.146666-26.88v2.645334c0 104.405333 72.405333 191.488 168.533333 211.2a200.32 200.32 0 0 1-55.296 7.594666c-13.525333 0-26.752-1.28-39.552-3.84 26.709333 85.589333 104.277333 147.882667 196.224 149.546667A414.805333 414.805333 0 0 1 0 841.941333 584.96 584.96 0 0 0 322.048 938.666667c386.474667 0 597.76-328.192 597.76-612.906667 0-9.386667-0.213333-18.730667-0.554667-27.904 41.045333-30.378667 76.672-68.266667 104.746667-111.488"
161 163 fill="#55ACEE"
162   - p-id="22953"
163   - ></path></svg
164   - >Twitter</a
165   - >
  164 + ></path>
  165 + </svg>
  166 + Twitter
  167 + </a>
166 168 </span>
167 169 </div>
  170 + <div class="text-h5 tw-mb-5 tw-mt-5">Send an email to me</div>
  171 + <form>
  172 + <v-row>
  173 + <v-col cols="8" md="6">
  174 + <v-text-field
  175 + v-model="state.firstName"
  176 + :error-messages="v$.firstName.$errors.map((e) => e.$message)"
  177 + label="FirstName"
  178 + required
  179 + @blur="v$.firstName.$touch"
  180 + @input="v$.firstName.$touch"
  181 + ></v-text-field>
  182 + </v-col>
  183 +
  184 + <v-col cols="8" md="6">
  185 + <v-text-field
  186 + v-model="state.lastName"
  187 + :error-messages="v$.lastName.$errors.map((e) => e.$message)"
  188 + label="LastName"
  189 + required
  190 + @blur="v$.lastName.$touch"
  191 + @input="v$.lastName.$touch"
  192 + ></v-text-field>
  193 + </v-col>
  194 + </v-row>
  195 + <v-text-field
  196 + v-model="state.email"
  197 + :error-messages="v$.email.$errors.map((e) => e.$message)"
  198 + label="E-mail"
  199 + required
  200 + @blur="v$.email.$touch"
  201 + @input="v$.email.$touch"
  202 + ></v-text-field>
  203 + <v-text-field
  204 + v-model="state.text"
  205 + :error-messages="v$.text.$errors.map((e) => e.$message)"
  206 + label="message"
  207 + required
  208 + @blur="v$.text.$touch"
  209 + @input="v$.text.$touch"
  210 + ></v-text-field>
  211 + <div class="recaptcha-container" style="margin-bottom: 20px">
  212 + <!-- reCAPTCHA v2 Checkbox -->
  213 + <div id="recaptcha" class="g-recaptcha"></div>
  214 + <!-- 验证成功后显示的消息 -->
  215 + <!-- <div v-if="verified" class="success-message">验证通过!</div> -->
  216 + </div>
  217 + <!-- <v-btn class="me-4" @click="v$.$validate"> submit </v-btn> -->
  218 + <v-btn class="me-4" @click="handleSubmit"> submit </v-btn>
  219 + <v-btn @click="clear"> clear </v-btn>
  220 + </form>
  221 + <v-snackbar
  222 + v-model="snackbar"
  223 + :timeout="3000"
  224 + top
  225 + :style="{ top: '300px', position: 'fixed' }"
  226 + color="success"
  227 + >
  228 + Sent successfully!
  229 + </v-snackbar>
  230 + <v-snackbar
  231 + v-model="snackbarFailed"
  232 + :timeout="3000"
  233 + top
  234 + :style="{ top: '100px', position: 'fixed' }"
  235 + color="error"
  236 + >
  237 + Failed to send!
  238 + </v-snackbar>
168 239 <div style="margin-bottom: 10px">
169 240 <v-tabs
170 241 class="tabs2"
... ... @@ -296,6 +367,10 @@
296 367 </template>
297 368  
298 369 <script setup lang="ts">
  370 +import { ref, reactive } from "vue";
  371 +import { useVuelidate } from "@vuelidate/core";
  372 +import { email, required, maxLength } from "@vuelidate/validators";
  373 +
299 374 const productStore = useProductListStore();
300 375 const categoryStore = useCategoryStore();
301 376 const loading = ref(false);
... ... @@ -317,6 +392,127 @@ const recommendImagesMobile = ref({});
317 392 const currentIndexMobile = ref(1);
318 393 const hotLoadingMobile = ref(false);
319 394 const hotTotalMobile = ref(10);
  395 +const verified = ref(false); // 验证状态
  396 +const snackbar = ref(false);
  397 +const snackbarFailed = ref(false);
  398 +const initialState = {
  399 + firstName: "",
  400 + lastName: "",
  401 + email: "",
  402 + text: "",
  403 +};
  404 +
  405 +const state = reactive({
  406 + ...initialState,
  407 +});
  408 +
  409 +const handleSubmit = async () => {
  410 + if (verified.value) {
  411 + let { data } = await useAsyncData(
  412 + "sendEmail",
  413 + () =>
  414 + $fetch("/email/send", {
  415 + method: "POST",
  416 + body: {
  417 + firstName: state.firstName,
  418 + lastName: state.lastName,
  419 + email: state.email,
  420 + message: state.text,
  421 + subject: "",
  422 + },
  423 + }),
  424 + {
  425 + server: true, // 仅在服务器端获取数据
  426 + }
  427 + );
  428 + if (data.value.message == "成功") {
  429 + snackbar.value = true;
  430 + } else {
  431 + snackbarFailed.value = true;
  432 + }
  433 + } else {
  434 + snackbarFailed.value = true;
  435 + }
  436 +};
  437 +
  438 +const rules = {
  439 + name: { required },
  440 + firstName: { required },
  441 + lastName: { required },
  442 + email: { required, email },
  443 + text: { required, maxLength: maxLength(20) },
  444 +};
  445 +
  446 +const v$ = useVuelidate(rules, state);
  447 +
  448 +function clear() {
  449 + v$.value.$reset();
  450 +
  451 + for (const [key, value] of Object.entries(initialState)) {
  452 + state[key] = value;
  453 + }
  454 +}
  455 +
  456 +onMounted(() => {
  457 + // 动态加载 reCAPTCHA 脚本并初始化
  458 + loadRecaptchaScript(() => {
  459 + // 确保 grecaptcha 已经准备好
  460 + if (window.grecaptcha) {
  461 + // 强制等待一段时间,确保 #recaptcha 元素已经渲染
  462 + setTimeout(() => {
  463 + // 确保 #recaptcha 元素已经存在并且可用
  464 + const recaptchaElement = document.getElementById("recaptcha");
  465 + if (recaptchaElement) {
  466 + try {
  467 + window.grecaptcha.render("recaptcha", {
  468 + sitekey: "6Lcgd6kqAAAAAAm0mLcuLcjv3zz55hB6wu5gkZMe", // 替换为你的 Site Key
  469 + callback: onRecaptchaSuccess,
  470 + "error-callback": onRecaptchaError,
  471 + });
  472 + } catch (error) {}
  473 + } else {
  474 + }
  475 + }, 1000); // 延时 1 秒,确保 DOM 渲染完成
  476 + } else {
  477 + }
  478 + });
  479 +});
  480 +
  481 +// 验证成功回调
  482 +const onRecaptchaSuccess = (token) => {
  483 + verified.value = true; // 设置为已验证
  484 +};
  485 +
  486 +// 验证失败回调
  487 +const onRecaptchaError = () => {
  488 + verified.value = false; // 设置为未验证
  489 +};
  490 +
  491 +// 动态加载 reCAPTCHA 脚本
  492 +const loadRecaptchaScript = (callback) => {
  493 + if (document.getElementById("recaptcha-api")) {
  494 + callback(); // 如果脚本已加载,直接回调
  495 + return;
  496 + }
  497 +
  498 + const script = document.createElement("script");
  499 + script.id = "recaptcha-api";
  500 + script.src =
  501 + "https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit&hl=en";
  502 + script.async = true;
  503 + script.defer = true;
  504 +
  505 + // 脚本加载完成后的回调
  506 + script.onload = () => {
  507 + // 修改开始
  508 + callback();
  509 + }; // 修改结束
  510 +
  511 + // 脚本加载失败时的回调
  512 + script.onerror = () => {};
  513 +
  514 + document.head.appendChild(script);
  515 +};
320 516 const loadHotProducts = async () => {
321 517 hotLoading.value = true;
322 518 let { data: hotProducts } = await useAsyncData(
... ...