Răsfoiți Sursa

feat: 首页

songzhen 3 luni în urmă
părinte
comite
4b2da2b8e8
48 a modificat fișierele cu 858 adăugiri și 207 ștergeri
  1. 6 4
      nuxt.config.ts
  2. 27 4
      src/assets/iconfont/demo_index.html
  3. 8 4
      src/assets/iconfont/iconfont.css
  4. 0 0
      src/assets/iconfont/iconfont.js
  5. 7 0
      src/assets/iconfont/iconfont.json
  6. 2 0
      src/assets/iconfont/iconfont.svg
  7. BIN
      src/assets/iconfont/iconfont.ttf
  8. BIN
      src/assets/iconfont/iconfont.woff
  9. BIN
      src/assets/iconfont/iconfont.woff2
  10. BIN
      src/assets/img/home/contract_laowu_qrcode.png
  11. BIN
      src/assets/img/home/contract_travel_qrcode.png
  12. BIN
      src/assets/img/home/home_menu_aozhouyou.png
  13. BIN
      src/assets/img/home/home_menu_car.png
  14. BIN
      src/assets/img/home/home_menu_feizhouyou.png
  15. BIN
      src/assets/img/home/home_menu_house.png
  16. BIN
      src/assets/img/home/home_menu_meizhouyou.png
  17. BIN
      src/assets/img/home/home_menu_ouzhouyou.png
  18. BIN
      src/assets/img/home/home_menu_visa.png
  19. BIN
      src/assets/img/home/home_menu_yazhouyou.png
  20. BIN
      src/assets/img/home/home_menu_zhongdongyou.png
  21. BIN
      src/assets/img/home/home_plane.png
  22. BIN
      src/assets/img/home/home_travel_note_leaf.png
  23. BIN
      src/assets/img/home/original_label.png
  24. 81 0
      src/components/Footer/index.vue
  25. 3 7
      src/components/Home/Banner.vue
  26. 37 0
      src/components/Home/HotCountry/index.vue
  27. 0 28
      src/components/Home/HotDestination.vue
  28. 0 34
      src/components/Home/HotTravelProject.vue
  29. 31 0
      src/components/Home/HotTravelProjects/Item.vue
  30. 44 0
      src/components/Home/HotTravelProjects/index.vue
  31. 0 39
      src/components/Home/Menu.vue
  32. 39 0
      src/components/Home/Menu/index.vue
  33. 31 0
      src/components/Home/TravelMenu/Item.vue
  34. 65 0
      src/components/Home/TravelMenu/index.vue
  35. 41 0
      src/components/Home/TravelNotes/Item.vue
  36. 54 0
      src/components/Home/TravelNotes/index.vue
  37. 12 19
      src/components/Navbar/index.vue
  38. 0 60
      src/components/Travel/ProjectItem/index.vue
  39. 43 0
      src/components/TravelProject/Item.vue
  40. 18 0
      src/components/TravelProjectDetail/Banner.vue
  41. 41 0
      src/components/TravelProjectDetail/BaseInfo.vue
  42. 97 0
      src/components/TravelProjectDetail/BookInfo.vue
  43. 14 0
      src/layouts/default.vue
  44. 12 6
      src/pages/index.vue
  45. 4 1
      src/pages/login/index.client.vue
  46. 20 0
      src/pages/t/[id].vue
  47. 114 0
      src/pages/travel-projects/index.client.vue
  48. 7 1
      src/utils/index.js

+ 6 - 4
nuxt.config.ts

@@ -41,15 +41,17 @@ export default defineNuxtConfig({
           name: "charset",
           content: "utf-8",
         },
-        // {
-        //   name: "viewport",
-        //   content: "width=device-width,width=1250,maximum-scale=1.0",
-        // },
+        {
+          name: "viewport",
+          content:
+            "width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover",
+        },
         {
           name: "renderer",
           content: "webkit",
         },
       ],
+      link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.svg" }],
     },
   },
   css: ["@/assets/css/tailwind.css", "./src/assets/iconfont/iconfont.css"],

+ 27 - 4
src/assets/iconfont/demo_index.html

@@ -55,6 +55,12 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe7f4;</span>
+                <div class="name">menu</div>
+                <div class="code-name">&amp;#xe7f4;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe78a;</span>
                 <div class="name">message</div>
                 <div class="code-name">&amp;#xe78a;</div>
@@ -180,10 +186,10 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1731999413855') format('woff2'),
-       url('iconfont.woff?t=1731999413855') format('woff'),
-       url('iconfont.ttf?t=1731999413855') format('truetype'),
-       url('iconfont.svg?t=1731999413855#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1733989943569') format('woff2'),
+       url('iconfont.woff?t=1733989943569') format('woff'),
+       url('iconfont.ttf?t=1733989943569') format('truetype'),
+       url('iconfont.svg?t=1733989943569#iconfont') format('svg');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -210,6 +216,15 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-menu"></span>
+            <div class="name">
+              menu
+            </div>
+            <div class="code-name">.icon-menu
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-message"></span>
             <div class="name">
               message
@@ -400,6 +415,14 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-menu"></use>
+                </svg>
+                <div class="name">menu</div>
+                <div class="code-name">#icon-menu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-message"></use>
                 </svg>
                 <div class="name">message</div>

+ 8 - 4
src/assets/iconfont/iconfont.css

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4723464 */
-  src: url('iconfont.woff2?t=1731999413855') format('woff2'),
-       url('iconfont.woff?t=1731999413855') format('woff'),
-       url('iconfont.ttf?t=1731999413855') format('truetype'),
-       url('iconfont.svg?t=1731999413855#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1733989943569') format('woff2'),
+       url('iconfont.woff?t=1733989943569') format('woff'),
+       url('iconfont.ttf?t=1733989943569') format('truetype'),
+       url('iconfont.svg?t=1733989943569#iconfont') format('svg');
 }
 
 .iconfont {
@@ -14,6 +14,10 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-menu:before {
+  content: "\e7f4";
+}
+
 .icon-message:before {
   content: "\e78a";
 }

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
src/assets/iconfont/iconfont.js


+ 7 - 0
src/assets/iconfont/iconfont.json

@@ -6,6 +6,13 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "4767059",
+      "name": "menu",
+      "font_class": "menu",
+      "unicode": "e7f4",
+      "unicode_decimal": 59380
+    },
+    {
       "icon_id": "4765866",
       "name": "message",
       "font_class": "message",

+ 2 - 0
src/assets/iconfont/iconfont.svg

@@ -14,6 +14,8 @@
     />
       <missing-glyph />
       
+      <glyph glyph-name="menu" unicode="&#59380;" d="M904 736H120c-4.4 0-8-3.6-8-8v-64c0-4.4 3.6-8 8-8h784c4.4 0 8 3.6 8 8v64c0 4.4-3.6 8-8 8zM904 112H120c-4.4 0-8-3.6-8-8v-64c0-4.4 3.6-8 8-8h784c4.4 0 8 3.6 8 8v64c0 4.4-3.6 8-8 8zM904 424H120c-4.4 0-8-3.6-8-8v-64c0-4.4 3.6-8 8-8h784c4.4 0 8 3.6 8 8v64c0 4.4-3.6 8-8 8z"  horiz-adv-x="1024" />
+      
       <glyph glyph-name="message" unicode="&#59274;" d="M512 384m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM712 384m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM312 384m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM925.2 557.6c-22.6 53.7-55 101.9-96.3 143.3-41.3 41.3-89.5 73.8-143.3 96.3C630.6 820.3 572.2 832 512 832h-2c-60.6-0.3-119.3-12.3-174.5-35.9-53.3-22.8-101.1-55.2-142-96.5-40.9-41.3-73-89.3-95.2-142.8-23-55.4-34.6-114.3-34.3-174.9 0.3-69.4 16.9-138.3 48-199.9v-152c0-25.4 20.6-46 46-46h152.1c61.6-31.1 130.5-47.7 199.9-48h2.1c59.9 0 118 11.6 172.7 34.3 53.5 22.3 101.6 54.3 142.8 95.2 41.3 40.9 73.8 88.7 96.5 142 23.6 55.2 35.6 113.9 35.9 174.5 0.3 60.9-11.5 120-34.8 175.6z m-151.1-438C704 50.2 611 12 512 12h-1.7c-60.3 0.3-120.2 15.3-173.1 43.5l-8.4 4.5H188V200.8l-4.5 8.4C155.3 262.1 140.3 322 140 382.3c-0.4 99.7 37.7 193.3 107.6 263.8 69.8 70.5 163.1 109.5 262.8 109.9h1.7c50 0 98.5-9.7 144.2-28.9 44.6-18.7 84.6-45.6 119-80 34.3-34.3 61.3-74.4 80-119 19.4-46.2 29.1-95.2 28.9-145.8-0.6-99.6-39.7-192.9-110.1-262.7z"  horiz-adv-x="1024" />
       
       <glyph glyph-name="smile" unicode="&#59267;" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM664 363h-48.1c-4.2 0-7.8-3.2-8.1-7.4C604 306.1 562.5 267 512 267s-92.1 39.1-95.8 88.6c-0.3 4.2-3.9 7.4-8.1 7.4H360c-4.6 0-8.2-3.8-8-8.4 4.4-84.3 74.5-151.6 160-151.6s155.6 67.3 160 151.6c0.2 4.6-3.4 8.4-8 8.4z"  horiz-adv-x="1024" />

BIN
src/assets/iconfont/iconfont.ttf


BIN
src/assets/iconfont/iconfont.woff


BIN
src/assets/iconfont/iconfont.woff2


BIN
src/assets/img/home/contract_laowu_qrcode.png


BIN
src/assets/img/home/contract_travel_qrcode.png


BIN
src/assets/img/home/home_menu_aozhouyou.png


BIN
src/assets/img/home/home_menu_car.png


BIN
src/assets/img/home/home_menu_feizhouyou.png


BIN
src/assets/img/home/home_menu_house.png


BIN
src/assets/img/home/home_menu_meizhouyou.png


BIN
src/assets/img/home/home_menu_ouzhouyou.png


BIN
src/assets/img/home/home_menu_visa.png


BIN
src/assets/img/home/home_menu_yazhouyou.png


BIN
src/assets/img/home/home_menu_zhongdongyou.png


BIN
src/assets/img/home/home_plane.png


BIN
src/assets/img/home/home_travel_note_leaf.png


BIN
src/assets/img/home/original_label.png


+ 81 - 0
src/components/Footer/index.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="bg-[#FFFBF7] px-15 pb-30">
+    <div class="py-15">
+      <div class="flex items-center space-x-10">
+        <div class="w-3 h-17 bg-primary"></div>
+        <span class="text-black-3 text-xl font-semibold">逍遥游旅游网</span>
+      </div>
+      <div class="text-sm text-black-6 mt-10 leading-[22px]">
+        全球年轻一代更喜欢用的旅游网站,年轻旅行者共同打造的"旅行神器",多个全球旅游目的地,多篇旅游日记供您参考。
+      </div>
+    </div>
+    <van-divider />
+    <div class="py-15">
+      <div class="flex items-center space-x-10">
+        <div class="w-3 h-17 bg-primary"></div>
+        <span class="text-black-3 text-xl font-semibold">关于我们</span>
+      </div>
+      <div class="flex items-center text-sm text-black-6 mt-10 space-x-20">
+        <div>隐私政策</div>
+        <div>用户协议</div>
+      </div>
+    </div>
+    <van-divider />
+    <div class="py-15">
+      <div class="flex items-center space-x-10">
+        <div class="w-3 h-17 bg-primary"></div>
+        <span class="text-black-3 text-xl font-semibold"
+          >我们足迹踏过的地区</span
+        >
+      </div>
+      <div class="flex flex-wrap space-x-20 mt-10">
+        <div
+          v-for="item in [
+            '欧洲',
+            '非洲',
+            '亚洲',
+            '澳洲',
+            '中东',
+            '南美洲',
+            '美洲',
+          ]"
+          class="text-sm text-black-6"
+        >
+          {{ item }}
+        </div>
+      </div>
+    </div>
+    <van-divider />
+    <div class="flex items-center py-15 justify-around text-black-6 text-sm">
+      <div class="flex flex-col items-center">
+        <img
+          src="~/assets/img/home/contract_travel_qrcode.png"
+          class="h-70 w-70 object-contain"
+          alt=""
+        />
+        <span class="mt-5">旅游顾问(扫码添加)</span>
+      </div>
+      <div class="flex flex-col items-center">
+        <img
+          src="~/assets/img/home/contract_laowu_qrcode.png"
+          class="h-70 w-70 object-contain"
+          alt=""
+        />
+        <span class="mt-5">劳务顾问(扫码添加)</span>
+      </div>
+    </div>
+    <div class="text-black-6 text-sm">
+      <span>©2024 xiaoyaotravel.com All Rights Reserved</span>
+      <NuxtLink to="https://beian.miit.gov.cn/" target="_blank"
+        >鲁ICP备2024119076号-1</NuxtLink
+      >
+      <NuxtLink to="https://www.12377.cn/" target="_blank"
+        >违法和不良信息举报</NuxtLink
+      >
+    </div>
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped></style>

+ 3 - 7
src/components/Home/Banner.vue

@@ -1,12 +1,8 @@
 <template>
-  <van-swipe :autoplay="3000" :show-indicators="false" class="h-250">
-    <van-swipe-item
-      class="h-full w-full"
-      v-for="item in bannerList"
-      :key="item.id"
-    >
+  <van-swipe :autoplay="3000" :show-indicators="false">
+    <van-swipe-item class="" v-for="item in bannerList" :key="item.id">
       <img
-        class="object-cover w-full h-full"
+        class="object-cover w-full aspect-[1920/697] rounded-xl"
         :src="item.imgUrlsAfterConvert[0]"
       />
     </van-swipe-item>

+ 37 - 0
src/components/Home/HotCountry/index.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="flex justify-center">
+    <div class="w-wrap">
+      <div class="flex flex-col items-center space-y-5">
+        <div class="font-bold text-black-3 text-xl">热门目的地</div>
+        <div class="h-4 w-38 rounded-full bg-primary"></div>
+      </div>
+      <div class="grid grid-cols-2 gap-10 mt-20">
+        <NuxtLink
+          v-for="item in hotCountryData"
+          :to="`/travel-notes?area=${item.parentId}&country=${item.id}`"
+          class="relative cursor-pointer overflow-hidden rounded-lg transition-all"
+        >
+          <van-image
+            :src="formatImgSrc(item.hotPictureUrlsAfterConvert)"
+            fit="cover"
+            class="w-full aspect-[224/215] object-cover"
+            alt=""
+            srcset=""
+          />
+          <div
+            class="absolute bottom-0 left-0 right-0 flex h-37 items-center bg-gradient-to-b from-transparent to-[#020202] pl-10"
+          >
+            <span class="text-l text-white"> {{ item.menuName }}</span>
+          </div>
+        </NuxtLink>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const { data } = await useMyFetch(`website/basic/directoryList?isHotspot=1`);
+const hotCountryData = computed(() => (data.value?.dataList ?? []).slice(0, 4));
+</script>
+
+<style lang="scss" scoped></style>

+ 0 - 28
src/components/Home/HotDestination.vue

@@ -1,28 +0,0 @@
-<template>
-  <div>
-    <div>
-      <span class="text-xl font-semibold">热门目的地</span>
-      <div class="mt-10 grid grid-cols-3 gap-10">
-        <NuxtLink
-          class="flex flex-col items-center space-y-5"
-          v-for="item in hotDestinationList"
-          :key="item.id"
-        >
-          <img
-            :src="item.hotPictureUrlsAfterConvert[0]"
-            class="aspect-[106/81] object-cover rounded-lg"
-            alt=""
-          />
-          <span class="text-black-3 text-base">{{ item.menuName }}</span>
-        </NuxtLink>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-const { data } = await useMyFetch(`website/basic/directoryList?isHotspot=1`);
-const hotDestinationList = computed(() => data.value?.dataList);
-</script>
-
-<style lang="scss" scoped></style>

+ 0 - 34
src/components/Home/HotTravelProject.vue

@@ -1,34 +0,0 @@
-<template>
-  <div>
-    <div>
-      <span class="text-xl font-semibold">热门项目</span>
-    </div>
-    <div class="grid grid-cols-2 gap-10 pt-10">
-      <NuxtLink
-        class="relative rounded-lg overflow-hidden"
-        v-for="item in hotTravelProjectList"
-        :key="item.id"
-        :to="`/yj/${item.id}`"
-      >
-        <img
-          :src="item.homeHotPicturesAfterConvert[0]"
-          class="aspect-[286/322] object-cover"
-        />
-        <div
-          class="flex items-center px-10 absolute bottom-0 left-0 right-0 h-45 bg-gradient-to-b from-white to-[#020202] text-white"
-        >
-          <span class="truncate text-base">{{ item.shortTitle }}</span>
-        </div>
-      </NuxtLink>
-    </div>
-  </div>
-</template>
-
-<script setup>
-const { data } = await useMyFetch(
-  `website/tourism/projectTravelNotes/homeList?pageNum=1&pageSize=4`
-);
-const hotTravelProjectList = computed(() => data.value?.dataList);
-</script>
-
-<style lang="scss" scoped></style>

+ 31 - 0
src/components/Home/HotTravelProjects/Item.vue

@@ -0,0 +1,31 @@
+<template>
+  <NuxtLink
+    :to="`/t/${itemData.id}`"
+    target="_blank"
+    class="flex cursor-pointer flex-col"
+  >
+    <van-image
+      :src="formatImgSrc(itemData.homeHotPicturesAfterConvert)"
+      class="aspect-[226/254] object-cover"
+      radius="10px"
+      fit="cover"
+    />
+    <span class="mt-5 truncate text-xl font-bold text-black-3">
+      {{ itemData.shortTitle }}
+    </span>
+    <span class="mt-5 truncate text-base text-black-6">{{
+      itemData.shortDescription
+    }}</span>
+  </NuxtLink>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => {},
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 44 - 0
src/components/Home/HotTravelProjects/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="w-full">
+    <div class="relative mx-auto w-wrap">
+      <div class="flex items-center justify-between">
+        <div class="flex items-center space-x-5">
+          <img src="~/assets/img/home/home_plane.png" class="h-25 w-25" />
+          <div>
+            <div class="text-sm font-bold text-black-3">热门项目</div>
+            <div class="h-2 w-50 bg-primary"></div>
+            <div class="text-sm text-primary">Popular projectss</div>
+          </div>
+        </div>
+        <div class="flex items-center space-x-10">
+          <NuxtLink
+            to="/travel-projects"
+            class="flex h-30 px-15 cursor-pointer items-center justify-center space-x-5 rounded-full bg-primary"
+          >
+            <span class="text-sm font-bold text-white">了解更多</span>
+            <span
+              class="iconfont icon-arrow_right text-white"
+              style="font-size: 14px"
+            ></span>
+          </NuxtLink>
+        </div>
+      </div>
+      <div class="mt-15 grid grid-cols-2 gap-x-10 gap-y-20">
+        <HomeHotTravelProjectsItem
+          v-for="item in travelProjectList"
+          :key="item"
+          :item-data="item"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const { data } = await useMyFetch(
+  `website/tourism/project/list?isHotspot=1&pageNum=1&pageSize=4`
+);
+const travelProjectList = computed(() => data.value?.dataList ?? []);
+</script>
+
+<style lang="scss" scoped></style>

+ 0 - 39
src/components/Home/Menu.vue

@@ -1,39 +0,0 @@
-<template>
-  <div class="flex item-center justify-around">
-    <NuxtLink
-      v-for="item in menuData"
-      :key="item.title"
-      :to="item.to"
-      class="flex flex-col items-center space-y-5"
-    >
-      <img :src="item.icon" class="w-50 h-50" alt="" srcset="" />
-      <span class="text-sm text-black-3">{{ item.title }}</span>
-    </NuxtLink>
-  </div>
-</template>
-
-<script setup>
-import HomeFoodIcon from "@/assets/img/home_food.png";
-import HomeTravleIcon from "@/assets/img/home_travel.png";
-import HomeLabourIcon from "@/assets/img/home_Labour.png";
-
-const menuData = [
-  {
-    icon: HomeFoodIcon,
-    title: "境外美食",
-    to: "/food",
-  },
-  {
-    icon: HomeTravleIcon,
-    title: "境外旅游",
-    to: "/travel",
-  },
-  {
-    icon: HomeLabourIcon,
-    title: "出国劳务",
-    to: "/labour",
-  },
-];
-</script>
-
-<style lang="scss" scoped></style>

+ 39 - 0
src/components/Home/Menu/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="flex items-center justify-around">
+    <NuxtLink
+      :to="`/${item.to}`"
+      v-for="item in menuData"
+      :key="item.title"
+      class="flex items-center space-x-5"
+    >
+      <img :src="item.icon" class="w-20 h-20" alt="" srcset="" />
+      <span class="text-sm text-black-6">{{ item.title }}</span>
+    </NuxtLink>
+  </div>
+</template>
+
+<script setup>
+import home_menu_car from "~/assets/img/home/home_menu_car.png";
+import home_menu_house from "~/assets/img/home/home_menu_house.png";
+import home_menu_visa from "~/assets/img/home/home_menu_visa.png";
+
+const menuData = [
+  {
+    title: "全球包车",
+    to: "car",
+    icon: home_menu_car,
+  },
+  {
+    title: "签证居留",
+    to: "visa",
+    icon: home_menu_house,
+  },
+  {
+    title: "买房卖房",
+    to: "house",
+    icon: home_menu_visa,
+  },
+];
+</script>
+
+<style lang="scss" scoped></style>

+ 31 - 0
src/components/Home/TravelMenu/Item.vue

@@ -0,0 +1,31 @@
+<template>
+  <NuxtLink
+    :to="itemData.to"
+    class="flex cursor-pointer items-center justify-between rounded-xl border bg-white p-10 px-5 transition-all"
+  >
+    <div>
+      <div class="text-base font-bold text-black-3">
+        {{ itemData.title }}
+      </div>
+      <div class="text-sm text-black-6">{{ itemData.subTitle }}</div>
+      <div
+        class="mt-5 h-2 w-18"
+        :style="{ backgroundColor: itemData.color }"
+      ></div>
+    </div>
+    <img :src="itemData.icon" class="h-23 w-23 object-contain" alt="" />
+  </NuxtLink>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => {
+      return {};
+    },
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 65 - 0
src/components/Home/TravelMenu/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="grid grid-cols-3 gap-10">
+    <HomeTravelMenuItem
+      v-for="item in menuData"
+      :key="item.title"
+      :item-data="item"
+    />
+  </div>
+</template>
+
+<script setup>
+import HomeMenuOuzhouyouIcon from "~/assets/img/home/home_menu_ouzhouyou.png";
+import HomeMenuYazhouyouIcon from "~/assets/img/home/home_menu_yazhouyou.png";
+import HomeMenuAozhouyouIcon from "~/assets/img/home/home_menu_aozhouyou.png";
+import HomeMenuZhongdongyouIcon from "~/assets/img/home/home_menu_zhongdongyou.png";
+import HomeMenuFeizhouyouIcon from "~/assets/img/home/home_menu_feizhouyou.png";
+import HomeMenuMeizhouyouIcon from "~/assets/img/home/home_menu_meizhouyou.png";
+
+const menuData = ref([
+  {
+    title: "欧洲游",
+    subTitle: "旅行,更自在",
+    color: "#fd9a00",
+    icon: HomeMenuOuzhouyouIcon,
+    to: "/travel-notes?area=11",
+  },
+  {
+    title: "亚洲游",
+    subTitle: "别等,就现在",
+    color: "#e42928",
+    icon: HomeMenuYazhouyouIcon,
+    to: "/travel-notes?area=12",
+  },
+  {
+    title: "澳洲游",
+    subTitle: "自由,在远方",
+    color: "#61c8ee",
+    icon: HomeMenuAozhouyouIcon,
+    to: "/travel-notes?area=15",
+  },
+  {
+    title: "中东游",
+    subTitle: "人生,入逆旅",
+    color: "#fd9a00",
+    icon: HomeMenuZhongdongyouIcon,
+    to: "/travel-notes?area=17",
+  },
+  {
+    title: "非洲游",
+    subTitle: "远行,遇见美",
+    color: "#3cc873",
+    icon: HomeMenuFeizhouyouIcon,
+    to: "/travel-notes?area=13",
+  },
+  {
+    title: "美洲游",
+    subTitle: "暂游,桃源里",
+    color: "#fa8446",
+    icon: HomeMenuMeizhouyouIcon,
+    to: "/travel-notes?area=14",
+  },
+]);
+</script>
+
+<style lang="scss" scoped></style>

+ 41 - 0
src/components/Home/TravelNotes/Item.vue

@@ -0,0 +1,41 @@
+<template>
+  <NuxtLink
+    :to="`/yj/${itemData.id}`"
+    target="_blank"
+    class="relative cursor-pointer rounded-xl bg-white p-10"
+  >
+    <div
+      class="absolute -top-20 left-20 flex h-30 w-30 items-center justify-center rounded-xl bg-white"
+    >
+      <img
+        src="@/assets/img/home/home_travel_note_leaf.png"
+        class="h-25 w-25 object-cover"
+      />
+    </div>
+    <van-image
+      :src="formatImgSrc(itemData.homeHotPicturesAfterConvert)"
+      radius="10px"
+      fit="cover"
+      class="w-full aspect-[230/172] object-cover"
+    />
+    <div class="text-base font-bold flex items-center text-primary mt-5">
+      <img
+        v-if="itemData.isOriginal === 1"
+        src="~/assets/img/home/original_label.png"
+        class="inline-block h-20 object-cover"
+      />
+      <span class="truncate">{{ itemData.projectTitle }}</span>
+    </div>
+  </NuxtLink>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 54 - 0
src/components/Home/TravelNotes/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="w-full">
+    <div class="relative mx-auto w-wrap">
+      <div class="flex items-center justify-between">
+        <div class="flex items-center space-x-5">
+          <img src="~/assets/img/home/home_plane.png" class="h-25 w-25" />
+          <div>
+            <div class="text-sm font-bold text-black-3">旅行游记</div>
+            <div class="h-2 w-50 bg-primary"></div>
+            <div class="text-sm text-primary">Hot items</div>
+          </div>
+        </div>
+        <div class="flex items-center space-x-10">
+          <NuxtLink
+            to="/note-create"
+            class="flex h-30 px-15 cursor-pointer items-center justify-center space-x-5 rounded-full bg-primary"
+          >
+            <span class="text-sm font-bold text-white">写游记</span>
+            <span
+              class="iconfont icon-edit-square text-white"
+              style="font-size: 14px"
+            ></span>
+          </NuxtLink>
+          <NuxtLink
+            to="/travel-notes"
+            class="flex h-30 px-15 cursor-pointer items-center justify-center space-x-5 rounded-full bg-primary"
+          >
+            <span class="text-sm font-bold text-white">了解更多</span>
+            <span
+              class="iconfont icon-arrow_right text-white"
+              style="font-size: 14px"
+            ></span>
+          </NuxtLink>
+        </div>
+      </div>
+      <div class="mt-35 grid grid-cols-2 gap-x-10 gap-y-40">
+        <HomeTravelNotesItem
+          v-for="item in travelNotesList"
+          :key="item"
+          :item-data="item"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const { data } = await useMyFetch(
+  `website/tourism/projectTravelNotes/homeList?pageNum=1&pageSize=6`
+);
+const travelNotesList = computed(() => data.value?.dataList ?? []);
+</script>
+
+<style lang="scss" scoped></style>

+ 12 - 19
src/components/Navbar/index.vue

@@ -1,26 +1,19 @@
 <template>
-  <van-nav-bar
-    :title="title"
-    border
-    left-arrow
-    placeholder
-    fixed
-    safe-area-inset-top
-    @click-left="onClickLeft"
-  />
+  <div class="h-60 bg-white justify-between w-full fixed z-[999]">
+    <NuxtLink to="/" class="absolute top-1/2 -translate-y-1/2 left-15">
+      <img src="~/assets/img/logo.png" class="h-40 object-contain" />
+    </NuxtLink>
+    <div
+      class="absolute right-15 top-1/2 -translate-y-1/2"
+      @click="handleClickMenu"
+    >
+      <span class="iconfont icon-menu" style="font-size: 24px"></span>
+    </div>
+  </div>
 </template>
 
 <script setup>
-defineProps({
-  title: {
-    type: String,
-    default: "标题",
-  },
-});
-
-function onClickLeft() {
-  navigateBack();
-}
+function handleClickMenu() {}
 </script>
 
 <style lang="scss" scoped></style>

+ 0 - 60
src/components/Travel/ProjectItem/index.vue

@@ -1,60 +0,0 @@
-<template>
-  <NuxtLink class="flex py-10 border-b space-x-10" :to="`/yj/${itemData.id}`">
-    <van-image
-      :src="
-        itemData.tourismUrlsAfterConvert.length
-          ? itemData.tourismUrlsAfterConvert[0]
-          : ''
-      "
-      fit="cover"
-      class="w-108 h-114 shrink rounded-lg overflow-hidden"
-    />
-    <div class="flex-1 flex flex-col">
-      <div class="min-h-85">
-        <div class="text-base text-black-3 line-clamp-2 font-semibold">
-          {{ itemData.projectTitle }}
-        </div>
-        <div class="flex flex-wrap gap-8 mt-5">
-          <div
-            v-for="item in lableList"
-            :key="item"
-            class="flex h-18 items-center justify-center rounded-sm border border-[#FD9A00] px-5 text-sm text-[#FD9A00]"
-          >
-            {{ item }}
-          </div>
-        </div>
-        <div class="mt-5 flex items-center space-x-10 text-sm text-[#666666B2]">
-          <span v-if="itemData.startPlace">{{ itemData.startPlace }}出发</span>
-          <span>{{ itemData.countTimes }}</span>
-        </div>
-      </div>
-      <!-- <div class="flex items-center justify-between">
-        <div class="text-[#ff0000] text-base" v-if="itemData.price">
-          <span>¥</span>
-          <span class="text-5xl font-semibold">{{ itemData.price }}</span>
-          <span>起</span>
-        </div>
-        <div
-          class="flex items-center justify-center h-20 w-60 text-white text-sm bg-primary rounded-full"
-        >
-          立即预定
-        </div>
-      </div> -->
-    </div>
-  </NuxtLink>
-</template>
-
-<script setup>
-const props = defineProps({
-  itemData: {
-    type: Object,
-    default: () => {},
-  },
-});
-
-const lableList = computed(() => {
-  return props.itemData.projectLabel?.split("&") ?? [];
-});
-</script>
-
-<style lang="scss" scoped></style>

+ 43 - 0
src/components/TravelProject/Item.vue

@@ -0,0 +1,43 @@
+<template>
+  <NuxtLink :to="`t/${itemData.id}`" class="flex space-x-10 py-10 border-b">
+    <van-image
+      :src="formatImgSrc(itemData.tourismUrlsAfterConvert)"
+      radius="10px"
+      class="w-145 h-103 shrink-0"
+    />
+    <div class="flex-1 w-0">
+      <div class="truncate">{{ itemData.projectTitle }}</div>
+      <div v-if="projectLabels.length" class="flex flex-wrap gap-5 mt-5">
+        <div
+          v-for="label in projectLabels"
+          :key="label"
+          class="text-sm bg-[#FD9A00] h-18 text-white flex items-center justify-center rounded-full px-5"
+        >
+          {{ label }}
+        </div>
+      </div>
+      <div class="text-sm text-black-6 line-clamp-2 mt-5">
+        {{ itemData.remarks }}
+      </div>
+      <div class="text-[#FF2020] flex items-center space-x-5">
+        <span>{{ itemData.price }}</span>
+        <span>{{ itemData.priceUnit }}</span>
+      </div>
+    </div>
+  </NuxtLink>
+</template>
+
+<script setup>
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const projectLabels = computed(() => {
+  return props.itemData.projectLabel.split("&").filter((item) => Boolean(item));
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 18 - 0
src/components/TravelProjectDetail/Banner.vue

@@ -0,0 +1,18 @@
+<template>
+  <van-swipe :autoplay="3000" :show-indicators="true" indicator-color="#FD9A00">
+    <van-swipe-item class="" v-for="item in bannerList" :key="item.id">
+      <img class="object-cover w-full aspect-[2271/1184]" :src="item" />
+    </van-swipe-item>
+  </van-swipe>
+</template>
+
+<script setup>
+defineProps({
+  bannerList: {
+    type: Array,
+    default: () => [],
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 41 - 0
src/components/TravelProjectDetail/BaseInfo.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="p-15">
+    <div class="text-xl font-semibold text-black-3">
+      {{ detailData.projectTitle }}
+    </div>
+    <div class="mt-5 flex flex-wrap items-center gap-7">
+      <div
+        v-for="item in lableList"
+        :key="item"
+        class="flex h-20 items-center justify-center rounded-sm border border-current px-5 text-sm text-primary"
+      >
+        {{ item }}
+      </div>
+    </div>
+    <div class="text-black-6 text-sm mt-10">{{ detailData.remarks }}</div>
+    <div class="mt-10">
+      <span class="text-xl font-semibold text-[#FF2222]">
+        <template v-if="detailData.price"
+          >{{ detailData.price }} {{ detailData.priceUnit }}</template
+        >
+        <template v-else>???? {{ detailData.priceUnit }}</template>
+      </span>
+      <span class="text-black-9">/人起</span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  detailData: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const lableList = computed(
+  () => props.detailData.projectLabel?.split("&") ?? []
+);
+</script>
+
+<style lang="scss" scoped></style>

+ 97 - 0
src/components/TravelProjectDetail/BookInfo.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="px-20 pt-15 text-black-3 text-base">
+    <div class="">日期选择</div>
+    <div
+      class="h-40 rounded-full border-[2px] px-15 mt-10 border-[#f7f7f7] flex items-center justify-between"
+    >
+      <span>出发:2023/12/12</span>
+      <span
+        class="iconfont icon-caret-down text-primary"
+        style="font-size: 24px"
+      ></span>
+    </div>
+    <div class="mt-15">
+      <div class="">人数选择</div>
+      <div
+        @click="showAdultNumberPicker = true"
+        class="h-40 rounded-full border-[2px] px-15 mt-10 border-[#f7f7f7] flex items-center justify-between"
+      >
+        <span>成人:2</span>
+        <span class="flex-1 text-primary ml-20">12周岁及以上</span>
+        <span
+          class="iconfont icon-caret-down text-primary"
+          style="font-size: 24px"
+        ></span>
+      </div>
+      <div
+        class="h-40 rounded-full border-[2px] px-15 mt-10 border-[#f7f7f7] flex items-center justify-between"
+      >
+        <span>儿童:2</span>
+        <span class="flex-1 text-primary ml-20">2-12周岁(不含)</span>
+        <span
+          class="iconfont icon-caret-down text-primary"
+          style="font-size: 24px"
+        ></span>
+      </div>
+    </div>
+    <van-popup
+      v-model:show="showAdultNumberPicker"
+      destroy-on-close
+      round
+      position="bottom"
+    >
+      <van-picker
+        :model-value="pickerValue"
+        :columns="adultNumberPickerOptions"
+        @cancel="showAdultNumberPicker = false"
+        @confirm="onAdultNumberConfirm"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup>
+const id = useRouteParam("id");
+
+const dayjs = useDayjs();
+
+const bookInfo = reactive({
+  startDate: null,
+  adultNumber: 1,
+  childrenNumber: 0,
+});
+
+const calendarData = ref({});
+
+async function getCalendarData() {
+  const { data } = await request("/website/tourism/project/viewDatePrice", {
+    query: {
+      projectId: id.value,
+    },
+  });
+  calendarData.value = data.tourismProjectDatePriceVos ?? {};
+  const minDay = dayjs.min(
+    Object.keys(calendarData.value).map((e) => dayjs(e))
+  );
+  bookInfo.startDate =
+    minDay === null ? null : dayjs(minDay).format("YYYY-MM-DD");
+}
+
+const showAdultNumberPicker = ref(false);
+const adultNumberPickerOptions = Array.from({ length: 9 }, (_, i) => i).map(
+  (e) => ({
+    text: `${e}人`,
+    value: e,
+  })
+);
+function onAdultNumberConfirm(value) {}
+
+const showChildrenNumberPicker = ref(false);
+function onChildrenNumberConfirm(value) {}
+
+onMounted(() => {
+  getCalendarData();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 14 - 0
src/layouts/default.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="">
+    <!-- <NavBar v-show="isNavBarShow" /> -->
+    <NavBar />
+    <div class="pt-60">
+      <slot></slot>
+    </div>
+    <!-- <Footer /> -->
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped></style>

+ 12 - 6
src/pages/index.vue

@@ -1,11 +1,17 @@
 <template>
-  <div>
-    <HomeBanner />
-    <div class="px-15 pb-30">
-      <HomeHotTravelProject class="mt-20" />
-      <HomeHotDestination class="mt-20" />
+  <div class="bg-[#F1F1F1]">
+    <div class="px-15 bg-white pb-15">
+      <HomeBanner />
+      <HomeTravelMenu class="mt-10" />
+      <!-- <HomeMenu class="mt-10" /> -->
+    </div>
+    <div class="px-15 mt-15">
+      <HomeTravelNotes />
+    </div>
+    <div class="bg-white mt-20 px-15 pt-15 pb-30">
+      <HomeHotTravelProjects />
+      <HomeHotCountry class="mt-30" />
     </div>
-    <Tabbar />
   </div>
 </template>
 

+ 4 - 1
src/pages/login/index.client.vue

@@ -16,7 +16,10 @@
           :rules="[{ required: true, message: '请输入手机号' }]"
         >
           <template #label>
-            <div @click="countryCodeOption.show = true">+{{ countryCode }}</div>
+            <div @click="countryCodeOption.show = true">
+              <span>+{{ countryCode }}</span>
+              <span class="iconfont icon-caret-down"></span>
+            </div>
           </template>
         </van-field>
         <van-field

+ 20 - 0
src/pages/t/[id].vue

@@ -0,0 +1,20 @@
+<template>
+  <div>
+    <TravelProjectDetailBanner
+      :banner-list="detailData?.tourismFile?.fileUrlsAfterConvert"
+    />
+    <TravelProjectDetailBaseInfo :detail-data="detailData" />
+    <div class="h-10 bg-[#E6E6E6]"></div>
+    <TravelProjectDetailBookInfo />
+  </div>
+</template>
+
+<script setup>
+const id = useRouteParam("id");
+
+const { data: detailData, status } = await useMyFetch(
+  `website/tourism/project/detail?id=${id.value}`
+);
+</script>
+
+<style lang="scss" scoped></style>

+ 114 - 0
src/pages/travel-projects/index.client.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="px-15">
+    <van-sticky :offset-top="60">
+      <div class="h-40 flex items-center justify-end bg-white">
+        <div @click="filterOption.show = true" class="text-black-6 text-base">
+          <span>{{ filterLabel }}</span>
+          <span class="iconfont icon-caret-down"></span>
+        </div>
+      </div>
+    </van-sticky>
+    <van-empty
+      v-if="!listData.length && !loading"
+      image="search"
+      description="暂无相关旅游项目"
+    />
+    <van-list
+      v-else-if="listData.length"
+      class=""
+      v-model:loading="loading"
+      :finished="finished"
+      finished-text="-- 没有更多了 --"
+      @load="onLoadMore"
+      :immediate-check="false"
+    >
+      <TravelProjectItem
+        v-for="item in listData"
+        :key="item.id"
+        :item-data="item"
+      >
+      </TravelProjectItem>
+    </van-list>
+    <van-popup v-model:show="filterOption.show" round position="bottom">
+      <van-cascader
+        title="请选择国家"
+        :options="filterOption.options"
+        @finish="onFinish"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup>
+const requestQuery = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  areaId: "",
+  countryId: "",
+});
+const listData = ref([]);
+
+const loading = ref(false);
+
+const finished = ref(false);
+
+async function getList() {
+  loading.value = true;
+  const { data } = await request("/website/tourism/project/list", {
+    query: requestQuery,
+  });
+  listData.value = listData.value.concat(data.dataList);
+  loading.value = false;
+  if (data.totalCount <= listData.value.length) {
+    finished.value = true;
+  }
+}
+
+function onLoadMore() {
+  requestQuery.pageNum++;
+  getList();
+}
+
+// 筛选国家
+const filterOption = reactive({
+  show: false,
+  options: [],
+});
+
+async function getFilterAddress() {
+  const { data } = await request(
+    "/website/tourism/projectTravelNotes/travelNotesDirectoryList"
+  );
+  data.forEach((item) => {
+    item.text = item.areaName;
+    item.value = item.areaId;
+    item.children.forEach((subItem) => {
+      subItem.text = subItem.countryName;
+      subItem.value = subItem.countryId;
+    });
+    item.children.unshift({ text: "全部", value: "" });
+  });
+  data.unshift({ text: "全部", value: "" });
+  filterOption.options = data;
+}
+
+const filterLabel = ref("区域选择");
+function onFinish({ selectedOptions }) {
+  filterOption.show = false;
+  filterLabel.value = selectedOptions.map((item) => item.text).join("/");
+  requestQuery.areaId = selectedOptions[0]?.value;
+  requestQuery.countryId =
+    selectedOptions.length > 1 ? selectedOptions[1]?.value : "";
+  requestQuery.pageNum = 1;
+  finished.value = false;
+  listData.value = [];
+  getList();
+}
+
+onMounted(() => {
+  getList();
+  getFilterAddress();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 7 - 1
src/utils/index.js

@@ -1,4 +1,10 @@
 const setIntervalImmediately = (fn, duration) =>
   setInterval((() => (fn(), fn))(), duration);
 
-export { setIntervalImmediately };
+function formatImgSrc(srcArr) {
+  if (Array.isArray(srcArr) && srcArr.length > 0) {
+    return srcArr[0] ?? "";
+  }
+  return "";
+}
+export { setIntervalImmediately, formatImgSrc };

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff