Browse Source

feat:个人中心

songzhen 3 months ago
parent
commit
e180fbfb2a
38 changed files with 1108 additions and 125 deletions
  1. 1 0
      nuxt.config.ts
  2. 1 0
      package.json
  3. 90 1
      pnpm-lock.yaml
  4. BIN
      src/assets/img/order/order_status.webp
  5. BIN
      src/assets/img/order/order_time.webp
  6. BIN
      src/assets/img/order/order_user.webp
  7. BIN
      src/assets/img/order/order_user_info_bg.webp
  8. BIN
      src/assets/img/profile/profile_banner.png
  9. BIN
      src/assets/img/profile/profile_car_order.png
  10. BIN
      src/assets/img/profile/profile_colection.png
  11. BIN
      src/assets/img/profile/profile_labour_order.png
  12. BIN
      src/assets/img/profile/profile_travel_note.png
  13. BIN
      src/assets/img/profile/profile_travel_order.png
  14. BIN
      src/assets/img/profile/profile_visa_order.png
  15. BIN
      src/assets/img/profile/travel_order_time.png
  16. BIN
      src/assets/img/travel_projects_detail/travel_detail_maidian.png
  17. BIN
      src/assets/img/travel_projects_detail/travel_detail_tip.png
  18. 0 0
      src/components/Home/Menu_old.vue
  19. 6 6
      src/components/Navbar/LeftMenu.vue
  20. 10 0
      src/components/Navbar/index.vue
  21. 41 0
      src/components/Profile/Collection/TravelNoteCell.vue
  22. 43 0
      src/components/Profile/TravelOrders/Item.vue
  23. 25 0
      src/components/Profile/TravelOrders/index.vue
  24. 66 33
      src/components/TravelProjectDetail/BookInfo.vue
  25. 70 0
      src/components/TravelProjectDetail/BookInfoCalendar.vue
  26. 33 0
      src/components/TravelProjectDetail/BottomBar.vue
  27. 43 0
      src/components/TravelProjectDetail/Details.vue
  28. 24 0
      src/components/TravelProjectDetail/SellingPoint.vue
  29. 16 0
      src/components/TravelProjectDetail/SpecialTips.vue
  30. 65 0
      src/pages/profile/collection.client.vue
  31. 75 0
      src/pages/profile/index.vue
  32. 97 0
      src/pages/profile/travel-order/[id].vue
  33. 43 0
      src/pages/profile/travel-orders.vue
  34. 158 0
      src/pages/profile/userInfo.vue
  35. 146 0
      src/pages/t/[id].client.vue
  36. 0 20
      src/pages/t/[id].vue
  37. 54 64
      src/pages/travel-projects/index.client.vue
  38. 1 1
      src/utils/request.js

+ 1 - 0
nuxt.config.ts

@@ -6,6 +6,7 @@ export default defineNuxtConfig({
     "nuxt-swiper",
     "@vueuse/nuxt",
     "dayjs-nuxt",
+    "@samk-dev/nuxt-vcalendar",
     [
       "@pinia/nuxt",
       {

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
   },
   "dependencies": {
     "@pinia/nuxt": "^0.5.4",
+    "@samk-dev/nuxt-vcalendar": "^1.0.4",
     "@vueuse/core": "^12.0.0",
     "@vueuse/nuxt": "^12.0.0",
     "dayjs": "^1.11.13",

+ 90 - 1
pnpm-lock.yaml

@@ -11,6 +11,9 @@ importers:
       '@pinia/nuxt':
         specifier: ^0.5.4
         version: 0.5.4(magicast@0.3.5)(rollup@4.22.5)(vue@3.5.10)
+      '@samk-dev/nuxt-vcalendar':
+        specifier: ^1.0.4
+        version: 1.0.4(magicast@0.3.5)(rollup@4.22.5)(vue@3.5.10)
       '@vueuse/core':
         specifier: ^12.0.0
         version: 12.0.0
@@ -257,6 +260,10 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
+  '@babel/runtime@7.26.0':
+    resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/standalone@7.25.6':
     resolution: {integrity: sha512-Kf2ZcZVqsKbtYhlA7sP0z5A3q5hmCVYMKMWRWNK/5OVwHIve3JY1djVRmIVAx8FMueLIfZGKQDIILK2w8zO4mg==}
     engines: {node: '>=6.9.0'}
@@ -920,6 +927,9 @@ packages:
   '@polka/url@1.0.0-next.28':
     resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
 
+  '@popperjs/core@2.11.8':
+    resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+
   '@rollup/plugin-alias@5.1.1':
     resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==}
     engines: {node: '>=14.0.0'}
@@ -1094,6 +1104,9 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@samk-dev/nuxt-vcalendar@1.0.4':
+    resolution: {integrity: sha512-vl2VzELpqSheJmuG1ZehggieEb2E8YRkw+DDVl2UrLBj6z1ll3CHL0Kjrjg5Bg1UylmQO3yBdpTyjESP/tSj/Q==}
+
   '@sindresorhus/merge-streams@2.3.0':
     resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
     engines: {node: '>=18'}
@@ -1108,9 +1121,15 @@ packages:
   '@types/http-proxy@1.17.15':
     resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==}
 
+  '@types/lodash@4.17.13':
+    resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
+
   '@types/node@22.7.4':
     resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==}
 
+  '@types/resize-observer-browser@0.1.11':
+    resolution: {integrity: sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==}
+
   '@types/resolve@1.20.2':
     resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
 
@@ -1668,6 +1687,15 @@ packages:
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
+  date-fns-tz@2.0.1:
+    resolution: {integrity: sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==}
+    peerDependencies:
+      date-fns: 2.x
+
+  date-fns@2.30.0:
+    resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
+    engines: {node: '>=0.11'}
+
   dayjs-nuxt@2.1.11:
     resolution: {integrity: sha512-KDDNiET7KAKf6yzL3RaPWq5aV7ql9QTt5fIDYv+4eOegDmnEQGjwkKYADDystsKtPjt7QZerpVbhC96o3BIyqQ==}
 
@@ -3003,6 +3031,9 @@ packages:
     resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
     engines: {node: '>=4'}
 
+  regenerator-runtime@0.14.1:
+    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
   require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
@@ -3443,6 +3474,12 @@ packages:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  v-calendar@3.1.2:
+    resolution: {integrity: sha512-QDWrnp4PWCpzUblctgo4T558PrHgHzDtQnTeUNzKxfNf29FkCeFpwGd9bKjAqktaa2aJLcyRl45T5ln1ku34kg==}
+    peerDependencies:
+      '@popperjs/core': ^2.0.0
+      vue: ^3.2.0
+
   vant@4.9.7:
     resolution: {integrity: sha512-bFkjF6zZf8l6WxPwDn6vd2PZISpUhm4u3fBJStUh+BDll7yaAv610j56DDYoL3HM8tws9nanGN4WBbTrKToMrA==}
     peerDependencies:
@@ -3584,6 +3621,11 @@ packages:
     peerDependencies:
       vue: ^3.2.0
 
+  vue-screen-utils@1.0.0-beta.13:
+    resolution: {integrity: sha512-EJ/8TANKhFj+LefDuOvZykwMr3rrLFPLNb++lNBqPOpVigT2ActRg6icH9RFQVm4nHwlHIHSGm5OY/Clar9yIg==}
+    peerDependencies:
+      vue: ^3.2.0
+
   vue@3.5.10:
     resolution: {integrity: sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==}
     peerDependencies:
@@ -3940,6 +3982,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@babel/runtime@7.26.0':
+    dependencies:
+      regenerator-runtime: 0.14.1
+
   '@babel/standalone@7.25.6': {}
 
   '@babel/standalone@7.26.4': {}
@@ -4290,7 +4336,7 @@ snapshots:
 
   '@nuxt/devtools-kit@1.5.1(magicast@0.3.5)(rollup@4.22.5)(vite@5.4.8(@types/node@22.7.4)(sass@1.79.4)(terser@5.34.1))':
     dependencies:
-      '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.22.5)
+      '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.22.5)
       '@nuxt/schema': 3.13.2(rollup@4.22.5)
       execa: 7.2.0
       vite: 5.4.8(@types/node@22.7.4)(sass@1.79.4)(terser@5.34.1)
@@ -4621,6 +4667,8 @@ snapshots:
 
   '@polka/url@1.0.0-next.28': {}
 
+  '@popperjs/core@2.11.8': {}
+
   '@rollup/plugin-alias@5.1.1(rollup@4.22.5)':
     optionalDependencies:
       rollup: 4.22.5
@@ -4744,6 +4792,18 @@ snapshots:
   '@rollup/rollup-win32-x64-msvc@4.22.5':
     optional: true
 
+  '@samk-dev/nuxt-vcalendar@1.0.4(magicast@0.3.5)(rollup@4.22.5)(vue@3.5.10)':
+    dependencies:
+      '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.22.5)
+      '@popperjs/core': 2.11.8
+      v-calendar: 3.1.2(@popperjs/core@2.11.8)(vue@3.5.10)
+    transitivePeerDependencies:
+      - magicast
+      - rollup
+      - supports-color
+      - vue
+      - webpack-sources
+
   '@sindresorhus/merge-streams@2.3.0': {}
 
   '@trysound/sax@0.2.0': {}
@@ -4754,10 +4814,14 @@ snapshots:
     dependencies:
       '@types/node': 22.7.4
 
+  '@types/lodash@4.17.13': {}
+
   '@types/node@22.7.4':
     dependencies:
       undici-types: 6.19.8
 
+  '@types/resize-observer-browser@0.1.11': {}
+
   '@types/resolve@1.20.2': {}
 
   '@types/web-bluetooth@0.0.20': {}
@@ -5459,6 +5523,14 @@ snapshots:
 
   csstype@3.1.3: {}
 
+  date-fns-tz@2.0.1(date-fns@2.30.0):
+    dependencies:
+      date-fns: 2.30.0
+
+  date-fns@2.30.0:
+    dependencies:
+      '@babel/runtime': 7.26.0
+
   dayjs-nuxt@2.1.11(magicast@0.3.5)(rollup@4.22.5):
     dependencies:
       '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.22.5)
@@ -6892,6 +6964,8 @@ snapshots:
     dependencies:
       redis-errors: 1.2.0
 
+  regenerator-runtime@0.14.1: {}
+
   require-directory@2.1.1: {}
 
   resolve-from@5.0.0: {}
@@ -7418,6 +7492,17 @@ snapshots:
 
   util-deprecate@1.0.2: {}
 
+  v-calendar@3.1.2(@popperjs/core@2.11.8)(vue@3.5.10):
+    dependencies:
+      '@popperjs/core': 2.11.8
+      '@types/lodash': 4.17.13
+      '@types/resize-observer-browser': 0.1.11
+      date-fns: 2.30.0
+      date-fns-tz: 2.0.1(date-fns@2.30.0)
+      lodash: 4.17.21
+      vue: 3.5.10
+      vue-screen-utils: 1.0.0-beta.13(vue@3.5.10)
+
   vant@4.9.7(vue@3.5.10):
     dependencies:
       '@vant/popperjs': 1.3.0
@@ -7546,6 +7631,10 @@ snapshots:
       '@vue/devtools-api': 6.6.4
       vue: 3.5.10
 
+  vue-screen-utils@1.0.0-beta.13(vue@3.5.10):
+    dependencies:
+      vue: 3.5.10
+
   vue@3.5.10:
     dependencies:
       '@vue/compiler-dom': 3.5.10

BIN
src/assets/img/order/order_status.webp


BIN
src/assets/img/order/order_time.webp


BIN
src/assets/img/order/order_user.webp


BIN
src/assets/img/order/order_user_info_bg.webp


BIN
src/assets/img/profile/profile_banner.png


BIN
src/assets/img/profile/profile_car_order.png


BIN
src/assets/img/profile/profile_colection.png


BIN
src/assets/img/profile/profile_labour_order.png


BIN
src/assets/img/profile/profile_travel_note.png


BIN
src/assets/img/profile/profile_travel_order.png


BIN
src/assets/img/profile/profile_visa_order.png


BIN
src/assets/img/profile/travel_order_time.png


BIN
src/assets/img/travel_projects_detail/travel_detail_maidian.png


BIN
src/assets/img/travel_projects_detail/travel_detail_tip.png


+ 0 - 0
src/components/Home/Menu.vue → src/components/Home/Menu_old.vue


+ 6 - 6
src/components/Navbar/LeftMenu.vue

@@ -12,7 +12,11 @@
       <span class="text-black-6 text-base">登录</span>
     </NuxtLink>
 
-    <div v-else-if="token" class="flex items-center space-x-15">
+    <NuxtLink
+      to="/profile"
+      v-else-if="token"
+      class="flex items-center space-x-15"
+    >
       <van-image
         :src="userInfo.headImageUrl"
         height="60"
@@ -27,7 +31,7 @@
           <span class="text-black-3">{{ userInfo.personalSign }}</span>
         </div>
       </div>
-    </div>
+    </NuxtLink>
 
     <div class="flex flex-col mt-20 divide-y flex-1 overflow-scroll">
       <div
@@ -62,8 +66,6 @@ import menu_travel_note from "@/assets/img/navbar/menu_travel_note.png";
 import menu_travel_project from "@/assets/img/navbar/menu_travel_project.png";
 import menu_visa from "@/assets/img/navbar/menu_visa.png";
 
-const emit = defineEmits("onHide");
-
 const authStore = useAuthStore();
 const { token } = storeToRefs(authStore);
 
@@ -137,13 +139,11 @@ function handleClickMenu(item) {
   navigateTo({
     path: item.to,
   });
-  emit("onHide");
 }
 
 function handleLogout() {
   try {
     request("/website/web/doLogout", { method: "post" });
-    emit("onHide");
   } finally {
     authStore.cleanToken();
     navigateTo("/");

+ 10 - 0
src/components/Navbar/index.vue

@@ -26,6 +26,16 @@ const isMenuShow = ref(false);
 function handleClickMenu() {
   isMenuShow.value = true;
 }
+
+const route = useRoute();
+
+watch(
+  route,
+  () => {
+    isMenuShow.value = false;
+  },
+  { deep: true }
+);
 </script>
 
 <style lang="scss" scoped></style>

+ 41 - 0
src/components/Profile/Collection/TravelNoteCell.vue

@@ -0,0 +1,41 @@
+<template>
+  <NuxtLink
+    :to="`/yj/${itemData.id}`"
+    class="bg-white p-10 box-border flex space-x-10 rounded-xl"
+  >
+    <van-image
+      :src="formatImgSrc(itemData.tourismUrlsAfterConvert)"
+      width="155px"
+      height="115px"
+      radius="10px"
+      class="shrink-0"
+    ></van-image>
+    <div class="flex-1 w-0">
+      <div class="truncate text-black-3 text-xl">
+        {{ itemData.projectTitle }}
+      </div>
+      <div class="line-clamp-3 mt-5 text-sm text-black-6">
+        {{ itemData.remarks }}
+      </div>
+      <div
+        @click.prevent="$emit('onCancel')"
+        class="flex items-center justify-center h-25 border-current text-[#FF4242] text-sm border w-64 rounded-md mt-10"
+      >
+        取消收藏
+      </div>
+    </div>
+  </NuxtLink>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => {},
+  },
+});
+
+const emit = defineEmits(["onCancel"]);
+</script>
+
+<style lang="scss" scoped></style>

+ 43 - 0
src/components/Profile/TravelOrders/Item.vue

@@ -0,0 +1,43 @@
+<template>
+  <NuxtLink
+    :to="`/profile/travel-order/${itemData.id}`"
+    class="bg-white rounded-xl overflow-hidden text-black-3 text-base"
+  >
+    <div class="px-15 pt-15">
+      <div class="text-xl">
+        <span class="font-semibold">{{
+          itemData.tourismProjectVo.projectTitle
+        }}</span>
+        <span class="text-[#FD9A00]"
+          >&nbsp;{{ itemData.tourismProjectVo.countTimes }}</span
+        >
+      </div>
+      <div class="mt-5">
+        日期:{{ $dayjs(itemData.startDate).format("YYYY-MM-DD") }} 至
+        {{ $dayjs(itemData.endDate).format("YYYY-MM-DD") }}
+      </div>
+      <div class="mt-8">联系人:{{ itemData.customerName }}</div>
+      <div class="mt-5">
+        在线付:<span class="text-xl text-[#ff5555] font-semibold"
+          >{{ itemData.totalAmount }}{{ itemData.currency }}</span
+        >
+      </div>
+    </div>
+    <div
+      class="bg-[#FD9A00] pl-20 text-sm mt-10 h-35 text-white flex items-center"
+    >
+      订单号:{{ itemData.orderNo }}
+    </div>
+  </NuxtLink>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => {},
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 25 - 0
src/components/Profile/TravelOrders/index.vue

@@ -0,0 +1,25 @@
+<template>
+  <template v-if="orders.length">
+    <div class="px-15 flex flex-col space-y-15 mt-20">
+      <ProfileTravelOrdersItem
+        v-for="item in orders"
+        :key="item.id"
+        :itemData="item"
+      />
+    </div>
+  </template>
+  <template v-if="!orders.length">
+    <van-empty description="暂无旅游订单" />
+  </template>
+</template>
+
+<script setup>
+defineProps({
+  orders: {
+    type: Array,
+    default: () => [],
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 66 - 33
src/components/TravelProjectDetail/BookInfo.vue

@@ -1,10 +1,11 @@
 <template>
-  <div class="px-20 pt-15 text-black-3 text-base">
+  <div class="pt-15 text-black-3 text-base">
     <div class="">日期选择</div>
     <div
+      @click="showCalendarPicker = true"
       class="h-40 rounded-full border-[2px] px-15 mt-10 border-[#f7f7f7] flex items-center justify-between"
     >
-      <span>出发:2023/12/12</span>
+      <span>出发:{{ startDate }}</span>
       <span
         class="iconfont icon-caret-down text-primary"
         style="font-size: 24px"
@@ -16,7 +17,7 @@
         @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>成人:{{ adultNumber }}</span>
         <span class="flex-1 text-primary ml-20">12周岁及以上</span>
         <span
           class="iconfont icon-caret-down text-primary"
@@ -24,9 +25,10 @@
         ></span>
       </div>
       <div
+        @click="showChildrenNumberPicker = true"
         class="h-40 rounded-full border-[2px] px-15 mt-10 border-[#f7f7f7] flex items-center justify-between"
       >
-        <span>儿童:2</span>
+        <span>儿童:{{ childrenNumber }}</span>
         <span class="flex-1 text-primary ml-20">2-12周岁(不含)</span>
         <span
           class="iconfont icon-caret-down text-primary"
@@ -34,6 +36,15 @@
         ></span>
       </div>
     </div>
+    <div class="mt-15">
+      <div class="">预定须知</div>
+      <div class="mt-10 flex flex-col space-y-3 text-sm text-black-6">
+        <span>*该产品最少1人起订,成人+儿童最多支持16人</span>
+        <span>*此线路因服务能力有限,无法接待婴儿(14天-2周岁(不含))出行</span>
+        <span>*出于安全考虑,18岁以下未成年人需要至少一名成年旅客陪同</span>
+      </div>
+    </div>
+
     <van-popup
       v-model:show="showAdultNumberPicker"
       destroy-on-close
@@ -41,42 +52,51 @@
       position="bottom"
     >
       <van-picker
-        :model-value="pickerValue"
         :columns="adultNumberPickerOptions"
         @cancel="showAdultNumberPicker = false"
         @confirm="onAdultNumberConfirm"
       />
     </van-popup>
+    <van-popup
+      v-model:show="showChildrenNumberPicker"
+      destroy-on-close
+      round
+      position="bottom"
+    >
+      <van-picker
+        :columns="childrenNumberPickerOptions"
+        @cancel="showChildrenNumberPicker = false"
+        @confirm="onChildrenNumberConfirm"
+      />
+    </van-popup>
+    <van-popup
+      v-model:show="showCalendarPicker"
+      destroy-on-close
+      round
+      position="bottom"
+    >
+      <TravelProjectDetailBookInfoCalendar
+        v-model:date="startDate"
+        :calendar-data="calendarData"
+        @on-close="showCalendarPicker = false"
+      />
+    </van-popup>
   </div>
 </template>
 
 <script setup>
-const id = useRouteParam("id");
-
-const dayjs = useDayjs();
-
-const bookInfo = reactive({
-  startDate: null,
-  adultNumber: 1,
-  childrenNumber: 0,
+const props = defineProps({
+  calendarData: {
+    type: Object,
+    default: () => {},
+  },
 });
 
-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 startDate = defineModel("startDate");
+const adultNumber = defineModel("adultNumber");
+const childrenNumber = defineModel("childrenNumber");
 
+// 选择成人人数
 const showAdultNumberPicker = ref(false);
 const adultNumberPickerOptions = Array.from({ length: 9 }, (_, i) => i).map(
   (e) => ({
@@ -84,14 +104,27 @@ const adultNumberPickerOptions = Array.from({ length: 9 }, (_, i) => i).map(
     value: e,
   })
 );
-function onAdultNumberConfirm(value) {}
+function onAdultNumberConfirm({ selectedValues }) {
+  adultNumber.value = selectedValues[0];
+  showAdultNumberPicker.value = false;
+}
 
+// 选择儿童人数
 const showChildrenNumberPicker = ref(false);
-function onChildrenNumberConfirm(value) {}
+const childrenNumberPickerOptions = Array.from({ length: 9 }, (_, i) => i).map(
+  (e) => ({
+    text: `${e}人`,
+    value: e,
+  })
+);
+function onChildrenNumberConfirm({ selectedValues }) {
+  childrenNumber.value = selectedValues[0];
+  showChildrenNumberPicker.value = false;
+}
 
-onMounted(() => {
-  getCalendarData();
-});
+const showCalendarPicker = ref(false);
+
+// 选择日历
 </script>
 
 <style lang="scss" scoped></style>

+ 70 - 0
src/components/TravelProjectDetail/BookInfoCalendar.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="flex flex-col">
+    <div
+      class="flex h-36 w-full items-center justify-center bg-[#FFF8F2] text-sm text-[#FD9A00]"
+    >
+      以下价格为1成人起价 ·计价可能有延迟,请以下单时为准
+    </div>
+    <VCalendar expanded borderless>
+      <template #day-content="{ day }">
+        <div
+          class="flex h-60 w-full flex-col items-center rounded-md pt-10 transition-all"
+          :class="[
+            { 'text-black-c': !isAvailableDate(day.id) },
+            { 'hover:bg-primary hover:text-white': isAvailableDate(day.id) },
+            isAvailableDate(day.id) ? 'cursor-pointer' : 'cursor-not-allowed',
+            $dayjs(date).isSame(day.date, 'day')
+              ? 'bg-primary text-white'
+              : 'bg-transparent text-black-3',
+          ]"
+          @click="isAvailableDate(day.id) && handleDayClick(day)"
+        >
+          <div class="text-base font-semibold">{{ day.day }}</div>
+          <div v-if="isAvailableDate(day.id)" class="scale-90 text-sm">
+            <template v-if="calendarData[day.id]?.adultPrice"
+              >¥{{ calendarData[day.id]?.adultPrice }}起</template
+            >
+            <template v-else>¥????起</template>
+          </div>
+        </div>
+      </template>
+    </VCalendar>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  calendarData: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const emit = defineEmits(["onClose"]);
+
+const availableDateList = computed(() => Object.keys(props.calendarData ?? []));
+
+function isAvailableDate(date) {
+  return availableDateList.value.includes(date);
+}
+
+const date = defineModel("date");
+
+function handleDayClick(data) {
+  date.value = data.id;
+  emit("onClose");
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-calendar-table thead th) {
+  padding: 5px 0;
+}
+:deep(.el-calendar__body) {
+  padding: 0px 10px 10px;
+}
+:deep(.el-calendar-table .el-calendar-day) {
+  padding: 0px;
+  height: 65px;
+}
+</style>

+ 33 - 0
src/components/TravelProjectDetail/BottomBar.vue

@@ -0,0 +1,33 @@
+<template>
+  <div
+    class="fixed bottom-0 px-20 left-0 right-0 h-70 z-50 bg-white shadow-[0px_2px_14px_1px_rgba(0,0,0,0.12)] flex justify-center items-center"
+  >
+    <span class="text-black-6">总价:</span>
+    <span class="text-[#FF2222] text-3xl font-semibold">¥{{ totalPrice }}</span>
+    <van-button
+      @click="$emit('onOk')"
+      class="flex-1"
+      :loading="loading"
+      style="
+        background-color: #fd9a00;
+        color: #fff;
+        font-weight: bold;
+        margin-left: 30px;
+      "
+      >立即预定</van-button
+    >
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  totalPrice: {
+    type: String,
+    default: "",
+  },
+  loading: Boolean,
+});
+defineEmits(["onOk"]);
+</script>
+
+<style lang="scss" scoped></style>

+ 43 - 0
src/components/TravelProjectDetail/Details.vue

@@ -0,0 +1,43 @@
+<template>
+  <van-tabs color="#FD9A00">
+    <van-tab title="行程路线">
+      <div v-html="content" class="pt-10"></div>
+    </van-tab>
+    <van-tab title="费用说明">
+      <div v-html="costDescription" class="pt-10"></div>
+    </van-tab>
+    <van-tab title="预定须知">
+      <div v-html="bookingNotice" class="pt-10"></div>
+    </van-tab>
+  </van-tabs>
+</template>
+
+<script setup>
+const props = defineProps({
+  detailData: {
+    type: Object,
+    default: () => {},
+  },
+});
+
+const content = computed(() => {
+  return props.detailData?.tourismContent?.content?.replace(
+    /<img/g,
+    "<img style='width:100%; height:auto;'"
+  );
+});
+const costDescription = computed(() => {
+  return props.detailData?.tourismContent?.costDescription?.replace(
+    /<img/g,
+    "<img style='width:100%; height:auto;'"
+  );
+});
+const bookingNotice = computed(() => {
+  return props.detailData?.tourismContent?.bookingNotice?.replace(
+    /<img/g,
+    "<img style='width:100%; height:auto;'"
+  );
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 24 - 0
src/components/TravelProjectDetail/SellingPoint.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="">
+    <img
+      src="~/assets/img/travel_projects_detail/travel_detail_maidian.png"
+      width="40"
+      height="20"
+    />
+    <div
+      class="text-black-6 text-sm mt-5"
+      v-html="detailData.sellingPoint?.replace(/\n/g, '<br/>')"
+    ></div>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  detailData: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 16 - 0
src/components/TravelProjectDetail/SpecialTips.vue

@@ -0,0 +1,16 @@
+<template>
+  <div>
+    <img
+      src="~/assets/img/travel_projects_detail/travel_detail_maidian.png"
+      width="40"
+      height="20"
+    />
+    <div class="text-black-6 text-sm mt-5">
+      因景区/场馆标准不一样,儿童价不含景区/场馆门票费用,如产生儿童门票费用,游客可自行到景区/场馆购买门票或由服务人员代为购买
+    </div>
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped></style>

+ 65 - 0
src/pages/profile/collection.client.vue

@@ -0,0 +1,65 @@
+<template>
+  <template v-if="!listData.length && !loading">
+    <van-empty description="暂无收藏得游记" />
+  </template>
+  <template v-else-if="listData.length">
+    <div class="bg-[#f8f8f8] px-15 min-h-screen pt-15 flex flex-col space-y-15">
+      <ProfileCollectionTravelNoteCell
+        v-for="item in listData"
+        :key="item.id"
+        :itemData="item"
+        @on-cancel="handleCancel(item)"
+      />
+    </div>
+  </template>
+</template>
+
+<script setup>
+onMounted(() => {
+  getCollectionList();
+});
+
+const listData = ref([]);
+
+const { loading, setLoading } = useLoading();
+
+async function getCollectionList() {
+  setLoading(true);
+  const { data } = await request(
+    "/website/tourism/projectTravelNotes/userCollectTravelNotesList",
+    {
+      query: {
+        pageSize: 999,
+        pageNum: 1,
+      },
+    }
+  );
+  setLoading(false);
+  listData.value = data.dataList;
+}
+
+function handleCancel(item) {
+  showConfirmDialog({
+    title: "提示",
+    message: "确认取消收藏吗",
+  }).then(() => {
+    request(
+      "/website/tourism/projectTravelNotes/userCollectTravelNotesUpdate",
+      {
+        method: "post",
+        body: {
+          travelNotesId: item.id,
+          type: 0,
+        },
+      }
+    )
+      .then(() => {
+        showToast("取消收藏成功");
+        getCollectionList();
+      })
+      .catch(() => {});
+  });
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 75 - 0
src/pages/profile/index.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="min-h-screen bg-[#f8f8f8]">
+    <div
+      class="h-250 relative w-full bg-[url('~/assets/img/profile/profile_banner.png')] bg-no-repeat bg-cover"
+    >
+      <div class="absolute left-20 bottom-10">
+        <div class="flex items-center space-x-10">
+          <van-image
+            :src="userInfo.headImageUrl"
+            width="75px"
+            height="75px"
+            fit="cover"
+            radius="37.5px"
+          />
+          <span class="text-2xl font-semibold text-white">
+            {{ userInfo.showName }}</span
+          >
+        </div>
+        <div class="text-xl text-white font-semibold mt-15">
+          个性签名:{{ userInfo.personalSign || "暂未填写" }}
+        </div>
+      </div>
+    </div>
+    <div class="bg-white rounded-xl mx-20 mt-20 p-20">
+      <div class="text-xl font-semibold">常用功能</div>
+      <div class="grid grid-cols-3 gap-y-15 mt-20">
+        <NuxtLink
+          :to="item.to"
+          class="flex flex-col items-center"
+          v-for="item in menuData"
+          :key="item.to"
+        >
+          <img :src="item.icon" class="w-40 h-40" alt="" srcset="" />
+          <div class="text-black-3 text-sm mt-5">{{ item.label }}</div>
+        </NuxtLink>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import profile_travel_order from "~/assets/img/profile/profile_travel_order.png";
+import profile_labour_order from "~/assets/img/profile/profile_labour_order.png";
+import profile_travel_note from "~/assets/img/profile/profile_travel_note.png";
+import profile_colection from "~/assets/img/profile/profile_colection.png";
+import profile_car_order from "~/assets/img/profile/profile_car_order.png";
+import profile_visa_order from "~/assets/img/profile/profile_visa_order.png";
+
+const userInfoStore = useUserInfoStore();
+const { userInfo } = storeToRefs(userInfoStore);
+
+onMounted(() => {
+  userInfoStore.getUserInfo();
+});
+
+const menuData = [
+  {
+    icon: profile_travel_order,
+    label: "旅游订单",
+    to: "/profile/travel-orders",
+  },
+  {
+    icon: profile_travel_note,
+    label: "我的游记",
+    to: "/profile/notes",
+  },
+  {
+    icon: profile_colection,
+    label: "我的收藏",
+    to: "/profile/collection",
+  },
+];
+</script>
+
+<style lang="scss" scoped></style>

+ 97 - 0
src/pages/profile/travel-order/[id].vue

@@ -0,0 +1,97 @@
+<template>
+  <div
+    class="text-base bg-[#f8f8f8] min-h-screen px-15 pt-15 pb-20 text-black-6"
+  >
+    <div class="rounded-xl bg-white p-15">
+      <div class="flex items-center">
+        <img
+          src="~/assets/img/order/order_status.webp"
+          class="h-30 w-30 object-contain"
+        />
+        <span class="text-3xl font-semibold text-[#ff3535]">{{
+          orderDetailInfo.orderStatus === 0 ? "未完成" : "已完成"
+        }}</span>
+      </div>
+      <div class="mt-10">订单号:{{ orderDetailInfo.orderNo }}</div>
+    </div>
+
+    <div class="mt-15 flex flex-col space-y-10 rounded-xl bg-white p-20">
+      <div class="flex items-center">
+        <img
+          src="~/assets/img/order/order_time.webp"
+          class="h-25 w-25 object-contain"
+          alt=""
+          srcset=""
+        />
+        <span class="ml-5 text-xl font-semibold text-black-3">{{
+          orderDetailInfo?.tourismProjectVo?.projectTitle || ""
+        }}</span>
+      </div>
+      <div>
+        预定日期:{{ $dayjs(orderDetailInfo.createTime).format("YYYY-MM-DD") }}
+      </div>
+      <div>
+        行程日期:{{
+          $dayjs(orderDetailInfo.departureDate).format("YYYY-MM-DD")
+        }}-{{ $dayjs(orderDetailInfo.endDate).format("YYYY-MM-DD") }}
+      </div>
+    </div>
+
+    <div class="mt-15 flex flex-col space-y-10 rounded-xl bg-white p-20">
+      <div class="flex items-center">
+        <img
+          src="~/assets/img/order/order_user.webp"
+          class="h-25 w-25 object-contain"
+          alt=""
+          srcset=""
+        />
+        <span class="ml-5 text-xl font-semibold text-black-3">顾客信息</span>
+      </div>
+      <div>顾客姓名:{{ orderDetailInfo.customerName }}</div>
+      <div>联系方式:{{ orderDetailInfo.customerMobile }}</div>
+    </div>
+
+    <div
+      v-for="(item, index) in orderDetailInfo.detailList"
+      :key="item"
+      class="rounded-xl bg-white p-20 mt-20"
+    >
+      <div class="text-base text-black-3">预定信息</div>
+      <div class="flex pt-10">
+        <div class="text-xl font-semibold text-black-3">
+          旅客{{ index + 1 }}
+        </div>
+        <div class="mt-2 ml-15 flex flex-col space-y-15">
+          <div>中文姓名:{{ item.chineseName }}</div>
+          <div>英文姓名:{{ item.englishName }}</div>
+          <div>国籍:{{ item.nationality }}</div>
+          <div>出生日期:{{ item.birthday }}</div>
+          <div>手机号码:{{ item.mobile }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const id = useRouteParam("id");
+
+const loading = ref(false);
+
+const orderDetailInfo = ref({});
+
+async function getOrderDetail() {
+  loading.value = true;
+  const { data } = await request(
+    `/website/tourism/myOrder/detail?orderId=${id.value}`
+  );
+  orderDetailInfo.value = data;
+  loading.value = true;
+}
+
+onMounted(() => {
+  getOrderDetail();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 43 - 0
src/pages/profile/travel-orders.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="min-h-screen bg-[#f8f8f8]">
+    <van-tabs v-model:active="current" color="#FD9A00" sticky>
+      <van-tab title="全部订单" name="">
+        <ProfileTravelOrders :orders="orderList" />
+      </van-tab>
+      <van-tab title="未完成" name="0">
+        <ProfileTravelOrders :orders="orderList" />
+      </van-tab>
+      <van-tab title="已完成" name="1">
+        <ProfileTravelOrders :orders="orderList" />
+      </van-tab>
+    </van-tabs>
+  </div>
+</template>
+
+<script setup>
+const current = ref(null);
+const loading = ref(false);
+const orderList = ref([]);
+
+watch(current, (val) => {
+  getOrderList();
+});
+
+async function getOrderList() {
+  loading.value = true;
+  try {
+    const { data } = await request("/website/tourism/myOrder/list", {
+      query: {
+        orderStatus: current.value,
+        pageNum: 1,
+        pageSize: 999,
+      },
+    });
+    orderList.value = data.dataList;
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 158 - 0
src/pages/profile/userInfo.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="bg-[#f8f8f8] pt-30">
+    <van-form>
+      <van-cell-group inset>
+        <van-field name="uploader" label="头像">
+          <template #input>
+            <div @click="handleChangeAvatar">
+              <van-image
+                v-if="userInfo.headImageUrl"
+                :src="userInfo.headImageUrl"
+                width="75px"
+                height="75px"
+                radius="37.5px"
+              />
+              <div
+                v-else
+                class="w-75 h-75 rounded-full bg-black-d flex items-center justify-center"
+              >
+                <span
+                  class="iconfont icon-profile text-black-6"
+                  style="font-size: 36px"
+                ></span>
+              </div>
+            </div>
+          </template>
+        </van-field>
+        <van-field
+          v-model="userInfo.showName"
+          name="昵称"
+          label="昵称"
+          placeholder="昵称"
+          :rules="[{ required: true, message: '请填写昵称' }]"
+        />
+        <van-field name="radio" label="性别">
+          <template #input>
+            <van-radio-group v-model="userInfo.sex" direction="horizontal">
+              <van-radio name="1">男</van-radio>
+              <van-radio name="2">女</van-radio>
+            </van-radio-group>
+          </template>
+        </van-field>
+        <van-field
+          v-model="userInfo.email"
+          name="邮箱"
+          label="邮箱"
+          placeholder="邮箱"
+          :rules="[{ required: true, message: '请填写邮箱' }]"
+        />
+        <van-field
+          v-model="userInfo.job"
+          name="职业"
+          label="职业"
+          placeholder="职业"
+        />
+        <van-field
+          v-model="userInfo.address"
+          name="居住地"
+          label="居住地"
+          placeholder="居住地"
+        />
+        <van-field
+          v-model="userInfo.personalSign"
+          rows="3"
+          autosize
+          label="个性签名"
+          maxlength="200"
+          type="textarea"
+          placeholder="请输入留言"
+          show-word-limit
+        />
+      </van-cell-group>
+      <div style="margin: 16px">
+        <van-button
+          @click="handleSubmit"
+          round
+          block
+          style="background-color: #fa8446; color: #fff; font-size: 18px"
+        >
+          提交
+        </van-button>
+      </div>
+    </van-form>
+  </div>
+</template>
+
+<script setup>
+const userInfoStore = useUserInfoStore();
+const { userInfo } = storeToRefs(userInfoStore);
+
+const form = reactive({
+  showName: null,
+  email: null,
+  sex: null,
+  email: null,
+  headImageUrl: null,
+  address: null,
+  job: null,
+  personalSign: null,
+});
+
+watch(
+  userInfo,
+  () => {
+    form.showName = userInfo.value.showName;
+    form.email = userInfo.value.email;
+    form.sex = userInfo.value.sex;
+    form.headImageUrl = userInfo.value.headImageUrl;
+    form.address = userInfo.value.address;
+    form.job = userInfo.value.job;
+    form.personalSign = userInfo.value.personalSign;
+  },
+  {
+    immediate: true,
+    deep: true,
+  }
+);
+
+onMounted(() => {
+  userInfoStore.getUserInfo();
+});
+
+const { open, onChange } = useFileDialog({
+  accept: ".png,.png,.jpeg,.JPG,Png ",
+});
+
+function handleChangeAvatar() {
+  open();
+}
+
+onChange(async (files) => {
+  if (!files.length) return;
+  const formData = new FormData();
+  formData.append("uploadFile", files[0]);
+  formData.append("asImage", true);
+  formData.append("fieldName", "headImageUrl");
+  try {
+    const { data } = await request("/website/tourism/user/upload", {
+      method: "post",
+      body: formData,
+    });
+    form.headImageUrl = data.fileUrl;
+    userInfoStore.getUserInfo();
+  } catch (error) {}
+});
+
+async function handleSubmit() {
+  try {
+    await request("/website/tourism/user/update", {
+      method: "post",
+      body: form,
+    });
+    showToast("保存成功");
+    userInfoStore.getUserInfo();
+  } catch (error) {}
+}
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,146 @@
+<template>
+  <div class="pb-120">
+    <TravelProjectDetailBanner
+      :banner-list="detailData?.tourismFile?.fileUrlsAfterConvert"
+    />
+    <TravelProjectDetailBaseInfo :detail-data="detailData" />
+    <div class="h-10 bg-[#E6E6E6]"></div>
+    <div class="px-15">
+      <TravelProjectDetailBookInfo
+        v-model:startDate="bookInfo.startDate"
+        v-model:adultNumber="bookInfo.adultNumber"
+        v-model:childrenNumber="bookInfo.childrenNumber"
+        :calendar-data="calendarData"
+      />
+      <TravelProjectDetailSellingPoint
+        v-if="detailData?.sellingPoint"
+        :detail-data="detailData"
+        class="mt-20"
+      />
+      <TravelProjectDetailSpecialTips class="mt-20" />
+      <TravelProjectDetailDetails :detail-data="detailData" class="mt-20" />
+    </div>
+    <TravelProjectDetailBottomBar
+      :total-price="totalPrice"
+      :loading="submitLoading"
+      @onOk="handleSubmit"
+    />
+  </div>
+</template>
+
+<script setup>
+const id = useRouteParam("id");
+
+const bookInfo = reactive({
+  startDate: null,
+  adultNumber: 1,
+  childrenNumber: 0,
+});
+
+const { data: detailData, status } = await useMyFetch(
+  `website/tourism/project/detail?id=${id.value}`
+);
+
+const dayjs = useDayjs();
+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 totalPrice = ref("");
+
+watch(
+  bookInfo,
+  () => {
+    // 计算总价
+    const calendarInfo = calendarData.value[bookInfo.startDate] ?? {
+      adultPrice: 0,
+      childrenPrice: 0,
+    };
+    totalPrice.value = `${
+      bookInfo.adultNumber * calendarInfo.adultPrice +
+      bookInfo.childrenNumber * calendarInfo.childrenPrice
+    }`;
+  },
+  { deep: true }
+);
+
+const route = useRoute();
+
+const useAuth = useAuthStore();
+
+const { token } = storeToRefs(useAuth);
+
+async function handleSubmit() {
+  if (!token.value) {
+    await navigateTo({
+      path: "/login",
+      replace: true,
+      query: {
+        redirect: route.fullPath,
+      },
+    });
+
+    return;
+  }
+  handleSubmitInfo();
+}
+
+const submitLoading = ref(false);
+function handleSubmitInfo() {
+  submitLoading.value = true;
+  request("website/tourism/project/bookProject", {
+    method: "post",
+    body: {
+      tourBookInfoDto: {
+        projectId: id.value,
+        type: "1",
+        ...bookInfo,
+      },
+    },
+  })
+    .then(({ data }) => {
+      submitLoading.value = false;
+      if (data === 1) {
+        showDialog({
+          title: "预定成功",
+          message: "恭喜预定成功我们会尽快和您取得联系",
+        }).then(() => {});
+      } else if (data === 2) {
+        showDialog({
+          title: "您已预定",
+          message: "我们会尽快和您取得联系",
+        }).then(() => {
+          // on close
+        });
+      }
+    })
+    .catch(() => {
+      submitLoading.value = false;
+    });
+}
+
+onMounted(() => {
+  getCalendarData();
+});
+
+// watch(
+//   bookInfo,
+//   () => {
+//     console.log(bookInfo);
+//   },
+//   { deep: true }
+// );
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -1,20 +0,0 @@
-<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>

+ 54 - 64
src/pages/travel-projects/index.client.vue

@@ -2,21 +2,20 @@
   <div class="">
     <van-sticky :offset-top="60">
       <van-dropdown-menu ref="menuRef">
-        <van-dropdown-item title="区域选择">
+        <van-dropdown-item :title="currentAreaFilterLabel" ref="areaFilterRef">
           <van-tree-select
-            v-model:active-id="activeId"
-            v-model:main-active-index="activeIndex"
-            :items="items"
+            v-model:active-id="activeCountryId"
+            v-model:main-active-index="activeAreaIndex"
+            style="
+              --van-tree-select-item-active-color: #ff9300;
+              --van-tree-select-nav-background: #ff9300;
+            "
+            :items="areaOptions"
+            @click-nav="handleAreaClick"
+            @click-item="handleCountryClick"
           />
         </van-dropdown-item>
       </van-dropdown-menu>
-
-      <!-- <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"
@@ -40,43 +39,24 @@
         </TravelProjectItem>
       </van-list>
     </div>
-    <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 activeId = ref(1);
-const activeIndex = ref(0);
-const items = [
-  {
-    text: "浙江",
-    children: [
-      { text: "杭州", id: 1 },
-      { text: "温州", id: 2 },
-      { text: "宁波", id: 3 },
-    ],
-  },
-  {
-    text: "江苏",
-    children: [
-      { text: "南京", id: 4 },
-      { text: "无锡", id: 5 },
-      { text: "徐州", id: 6 },
-    ],
-  },
-  { text: "福建", disabled: true },
-];
+const currentArea = ref({});
+const currentCountry = ref({});
+const currentAreaFilterLabel = computed(() => {
+  if (!currentArea.value?.id) {
+    return "全部区域";
+  }
+  return `${currentArea.value.text || ""}${currentCountry.value.text || ""}`;
+});
+
 const requestQuery = reactive({
   pageNum: 1,
   pageSize: 10,
-  areaId: "",
-  countryId: "",
+  areaId: computed(() => currentArea.value?.id ?? ""),
+  countryId: computed(() => currentCountry.value?.id ?? ""),
 });
 const listData = ref([]);
 
@@ -101,11 +81,34 @@ function onLoadMore() {
   getList();
 }
 
-// 筛选国家
-const filterOption = reactive({
-  show: false,
-  options: [],
-});
+// 地区筛选
+const areaFilterRef = ref(null);
+const activeAreaIndex = ref(0);
+const activeCountryId = ref(0);
+const areaOptions = ref([]);
+
+function handleAreaClick(index) {
+  activeCountryId.value = null;
+  if (index === 0) {
+    currentArea.value = areaOptions.value[index];
+    currentCountry.value = {};
+    areaFilterRef.value.toggle();
+    reSearch();
+  }
+}
+
+function handleCountryClick(item) {
+  currentArea.value = areaOptions.value[activeAreaIndex.value];
+  currentCountry.value = item;
+  areaFilterRef.value.toggle();
+  reSearch();
+}
+
+function reSearch() {
+  requestQuery.pageNum = 1;
+  listData.value = [];
+  getList();
+}
 
 async function getFilterAddress() {
   const { data } = await request(
@@ -113,28 +116,15 @@ async function getFilterAddress() {
   );
   data.forEach((item) => {
     item.text = item.areaName;
-    item.value = item.areaId;
+    item.id = item.areaId;
     item.children.forEach((subItem) => {
       subItem.text = subItem.countryName;
-      subItem.value = subItem.countryId;
+      subItem.id = subItem.countryId;
     });
-    item.children.unshift({ text: "全部", value: "" });
+    item.children.unshift({ text: "全部", id: "" });
   });
-  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();
+  data.unshift({ text: "全部", id: "" });
+  areaOptions.value = data;
 }
 
 onMounted(() => {

+ 1 - 1
src/utils/request.js

@@ -11,7 +11,7 @@ export function request(url, options = {}) {
 
     headers: {
       ...options.headers,
-      "Content-Type": "application/json",
+      // "Content-Type": "application/json",
       // deviceType: config.public.deviceType,
       authorization: authStore.token,
     },