Commit a96c3fee87e3ab70dbf04c014c5530cd931a7c53
1 parent
5359076c
feat: 开发邮件发送,prod2-55/56
Showing
5 changed files
with
330 additions
and
248 deletions
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
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 "../components/MainTitle.vue"; |
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 | ||
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( | ... | ... |