Browse Source

Merge branch 'dev' of http://1.94.207.143:3000/xyy/xyy-m into dev

qiao 3 months ago
parent
commit
0dad986801
48 changed files with 1556 additions and 406 deletions
  1. 1 0
      nuxt.config.ts
  2. 1 0
      package.json
  3. 90 1
      pnpm-lock.yaml
  4. 9 0
      src/assets/img/note-create/delete.svg
  5. 4 0
      src/assets/img/note-create/img.svg
  6. BIN
      src/assets/img/order/order_status.webp
  7. BIN
      src/assets/img/order/order_time.webp
  8. BIN
      src/assets/img/order/order_user.webp
  9. BIN
      src/assets/img/order/order_user_info_bg.webp
  10. BIN
      src/assets/img/profile/profile_banner.png
  11. BIN
      src/assets/img/profile/profile_car_order.png
  12. BIN
      src/assets/img/profile/profile_colection.png
  13. BIN
      src/assets/img/profile/profile_labour_order.png
  14. BIN
      src/assets/img/profile/profile_travel_note.png
  15. BIN
      src/assets/img/profile/profile_travel_order.png
  16. BIN
      src/assets/img/profile/profile_visa_order.png
  17. BIN
      src/assets/img/profile/travel_order_time.png
  18. BIN
      src/assets/img/travel_projects_detail/travel_detail_maidian.png
  19. BIN
      src/assets/img/travel_projects_detail/travel_detail_tip.png
  20. 72 65
      src/components/CreateNote/Form.vue
  21. 13 25
      src/components/CreateNote/HeaderBanner.vue
  22. 12 0
      src/components/CreateNote/InsertTitleSection.vue
  23. 0 0
      src/components/Home/Menu_old.vue
  24. 6 6
      src/components/NavigationBar/LeftMenu.vue
  25. 11 1
      src/components/NavigationBar/index.vue
  26. 41 0
      src/components/Profile/Collection/TravelNoteCell.vue
  27. 43 0
      src/components/Profile/TravelOrders/Item.vue
  28. 25 0
      src/components/Profile/TravelOrders/index.vue
  29. 1 1
      src/components/TravelProject/Item.vue
  30. 66 33
      src/components/TravelProjectDetail/BookInfo.vue
  31. 70 0
      src/components/TravelProjectDetail/BookInfoCalendar.vue
  32. 33 0
      src/components/TravelProjectDetail/BottomBar.vue
  33. 43 0
      src/components/TravelProjectDetail/Details.vue
  34. 24 0
      src/components/TravelProjectDetail/SellingPoint.vue
  35. 16 0
      src/components/TravelProjectDetail/SpecialTips.vue
  36. 1 1
      src/layouts/default.vue
  37. 0 2
      src/pages/labour/index.vue
  38. 29 44
      src/pages/note-create-start/index.client.vue
  39. 306 142
      src/pages/note-create/index.client.vue
  40. 65 0
      src/pages/profile/collection.client.vue
  41. 75 0
      src/pages/profile/index.vue
  42. 97 0
      src/pages/profile/travel-order/[id].vue
  43. 43 0
      src/pages/profile/travel-orders.vue
  44. 158 0
      src/pages/profile/userInfo.vue
  45. 146 0
      src/pages/t/[id].client.vue
  46. 0 20
      src/pages/t/[id].vue
  47. 54 64
      src/pages/travel-projects/index.client.vue
  48. 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

+ 9 - 0
src/assets/img/note-create/delete.svg

@@ -0,0 +1,9 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="delete">
+<g id="union">
+<path d="M6 12V6H7V12H6Z" fill="black" fill-opacity="0.9"/>
+<path d="M9 6V12H10V6H9Z" fill="black" fill-opacity="0.9"/>
+<path d="M10.5 3H14V4H13V14C13 14.5523 12.5523 15 12 15H4C3.44772 15 3 14.5523 3 14V4H2V3H5.5L5.5 1.8C5.5 1.35817 5.85817 1 6.3 1H9.7C10.1418 1 10.5 1.35817 10.5 1.8V3ZM6.5 3H9.5L9.5 2L6.5 2V3ZM4 4V14H12V4H4Z" fill="black" fill-opacity="0.9"/>
+</g>
+</g>
+</svg>

File diff suppressed because it is too large
+ 4 - 0
src/assets/img/note-create/img.svg


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


+ 72 - 65
src/components/CreateNote/Form.vue

@@ -8,7 +8,6 @@
       <van-field
         v-model="departureTime"
         :label-width="labelWidth"
-       
         name="calendar"
         clearable
         autocomplete="off"
@@ -17,9 +16,12 @@
         :rules="[{ required: true, message: '请选择出发时间' }]"
       >
         <template #label>
-          <div class="w-full flex items-center text-16 h-full font-400 text-[#000]/[0.9]"
-            >出发时间 <span class="ml-3 text-16 text-[#D54941]">*</span></div
+          <div
+            class="w-full box-border flex items-center text-16 h-full font-400 text-[#000]/[0.9]"
           >
+            出发时间
+            <span class="ml-2 text-16 text-[#D54941]">*</span>
+          </div>
         </template>
         <template #right-icon>
           <div class="w-24 h-24">
@@ -48,12 +50,14 @@
       :rules="[{ required: true, message: '请输入您的天数' }]"
     >
       <template #label>
-        <span class="text-16 font-400 text-[#000]/[0.9]"
-          >出发天数 <span class="text-16 text-[#D54941]">*</span></span
-        >
+        <span class="text-16 font-400 text-[#000]/[0.9]">
+          出发天数
+          <span class="text-16 text-[#D54941]">*</span>
+        </span>
       </template>
     </van-field>
     <van-field
+      :label-width="labelWidth"
       v-model="role"
       label="人物关系"
       clearable
@@ -71,10 +75,11 @@
       :rules="[{ required: true, message: '请输入您的天数' }]"
     >
       <template #label>
-        <span class="text-16 font-400 text-[#000]/[0.9]"
-          >目的地<span class="text-white">的</span>
-          <span class="text-16 text-[#D54941]">*</span></span
-        >
+        <span class="text-16 font-400 text-[#000]/[0.9]">
+          目的地
+          <span class="text-white">的</span>
+          <span class="text-16 text-[#D54941]">*</span>
+        </span>
       </template>
     </van-field>
     <van-popup v-model:show="showPicker" destroy-on-close position="bottom">
@@ -85,14 +90,20 @@
         @cancel="showPicker = false"
       />
     </van-popup>
-    <van-field  name="rate" :label-width="labelWidth" label="推荐指数">
+    <van-field name="rate" :label-width="labelWidth" label="推荐指数">
       <template #input>
-        <van-rate color="#ffd21e" v-model="recommendationRate" clearable />
+        <van-rate
+          color="#ffd21e"
+          void-icon="star"
+          void-color="#eee"
+          v-model="recommendationRate"
+          clearable
+        />
       </template>
     </van-field>
 
     <van-field
-    :label-width="labelWidth"
+      :label-width="labelWidth"
       v-model="averageCost"
       name="averageCost"
       autocomplete="off"
@@ -100,11 +111,10 @@
       maxlength="10"
       label="平均费用"
       placeholder="请输入平均费用"
-    >
-    </van-field>
+    ></van-field>
 
     <van-field
-    :label-width="labelWidth"
+      :label-width="labelWidth"
       is-link
       v-model="travelMode"
       clearable
@@ -112,8 +122,7 @@
       label="出行方式"
       placeholder="请选择出行方式"
       @click="showtravelMode = true"
-    >
-    </van-field>
+    ></van-field>
     <van-popup v-model:show="showtravelMode" destroy-on-close position="bottom">
       <van-picker
         :columns="travelModeOptions"
@@ -135,78 +144,76 @@
 </template>
 
 <script setup>
-import calendar from "~/assets//img/note-create/calendar.png";
-const  labelWidth ='68px'
+import calendar from '~/assets//img/note-create/calendar.png'
+const labelWidth = '68px'
 // 出发时间
-const minDate = new Date(2010, 0, 1);
-const maxDate = new Date();
+const minDate = new Date(2010, 0, 1)
+const maxDate = new Date()
 
-const showCalendar = ref(false);
+const showCalendar = ref(false)
 
 // 展示时间的格式
 const onConfirm = (date) => {
-  departureTime.value = date ? `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` : "";
-  showCalendar.value = false;
-};
+  departureTime.value = date
+    ? `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
+    : ''
+  showCalendar.value = false
+}
 
 // 目的地
-const showPicker = ref(false);
+const showPicker = ref(false)
 const onConfirmAddr = ({ selectedValues, selectedOptions }) => {
-  showPicker.value = false;
-  endPlace.value = selectedOptions[selectedValues.length - 1].menuName;
-};
+  showPicker.value = false
+  endPlace.value = selectedOptions[selectedValues.length - 1].menuName
+}
 
 // 出行方式
-const showtravelMode = ref(false);
+const showtravelMode = ref(false)
 const onConfirmTravelMode = ({ selectedValues, selectedOptions }) => {
-  showtravelMode.value = false;
-  travelMode.value = selectedOptions[selectedValues.length - 1].name;
-};
-
-const departureTime = defineModel("departureTime");
-const countTimes = defineModel("countTimes");
-const endPlace = defineModel("endPlace");
-const role = defineModel("role");
-const averageCost = defineModel("averageCost");
-const recommendationRate = defineModel("recommendationRate");
-const travelMode = defineModel("travelMode");
-const travelNumber = defineModel("travelNumber");
+  showtravelMode.value = false
+  travelMode.value = selectedOptions[selectedValues.length - 1].name
+}
+
+const departureTime = defineModel('departureTime')
+const countTimes = defineModel('countTimes')
+const endPlace = defineModel('endPlace')
+const role = defineModel('role')
+const averageCost = defineModel('averageCost')
+const recommendationRate = defineModel('recommendationRate')
+const travelMode = defineModel('travelMode')
+const travelNumber = defineModel('travelNumber')
 
 onMounted(() => {
-  getPlaceOptions();
-  getTravelModeOptions();
-});
+  getPlaceOptions()
+  getTravelModeOptions()
+})
 
 const placeOptionProps = {
-  text: "menuName",
-  value: "id",
-  children: "tourWriteBelongTabVoList",
-};
+  text: 'menuName',
+  value: 'id',
+  children: 'tourWriteBelongTabVoList'
+}
 const travelModeOptionsProps = {
-  text: "name",
-  value: "id",
-};
+  text: 'name',
+  value: 'id'
+}
 
 // 获取目的地
-const placeOptions = ref([]);
+const placeOptions = ref([])
 async function getPlaceOptions() {
-  const { data } = await request(
-    "/website/tourism/publishTravelNotes/getWriteBelongTab"
-  );
-  console.log(data, "placeOptions");
+  const { data } = await request('/website/tourism/publishTravelNotes/getWriteBelongTab')
+  console.log(data, 'placeOptions')
 
-  placeOptions.value = data;
+  placeOptions.value = data
 }
 
 // 获取出行方式
-const travelModeOptions = ref([]);
+const travelModeOptions = ref([])
 async function getTravelModeOptions() {
-  const { data } = await request(
-    "/admin/app/extra/listDict?dictCode=TourTravelMode"
-  );
-  console.log(data, "115");
+  const { data } = await request('/admin/app/extra/listDict?dictCode=TourTravelMode')
+  console.log(data, '115')
 
-  travelModeOptions.value = data;
+  travelModeOptions.value = data
 }
 </script>
 

+ 13 - 25
src/components/CreateNote/HeaderBanner.vue

@@ -2,34 +2,25 @@
   <div style="height: calc(100vw * 0.59)" class="w-full">
     <!-- v-if="!bannerUrl" -->
     <div
-      
       class="flex h-full w-full items-center justify-center bg-[url('~/assets/img/note-create/note_create_banner_bg.png')]"
     >
-
-    <!-- <van-uploader    reupload  v-model="fileList" max-count="1" :preview-size="['100%', 221]">
+      <!-- <van-uploader    reupload  v-model="fileList" max-count="1" :preview-size="['100%', 221]">
     </van-uploader> -->
       <!-- <van-button icon="plus" type="primary">上传文件</van-button> -->
 
       <!-- <input type="file" class="van-uploader__input" value="1213" accept="image/*"> -->
       <div
         @click="handleSelectImage"
-        class="flex h-40 w-150  items-center justify-center space-x-10 rounded-full border-2 border-white"
+        class="flex h-40 w-150 active:bg-[#000]/[0.1] bg-[#FFFFFF]/[0.2] items-center justify-center space-x-10 rounded-full border-2 border-white"
       >
-        
-          <div class="w-16 h-16">
-            <van-image
-            width="100%"
-            height="100%"
-            :src="icon_image_fill"
-          />
-          </div>
-          <!-- <van-uploader v-model="fileList" multiple :max-count="1" /> -->
-        
-        <span  class="text-sm font-normal pt-4  text-white">设置游记头图</span>
+        <div class="w-16 h-16">
+          <van-image width="100%" height="100%" :src="icon_image_fill" />
+        </div>
+        <!-- <van-uploader v-model="fileList" multiple :max-count="1" /> -->
+
+        <span class="text-sm font-normal pt-4 text-[#FF9300]">设置游记头图</span>
       </div>
     </div>
-   
-   
 
     <!-- <div v-else class="relative h-full w-full">
       <img :src="bannerUrl" class="h-full w-full" alt="" />
@@ -72,7 +63,7 @@
 
 <script setup>
 import { useFileDialog } from '@vueuse/core'
-import icon_image_fill from "~/assets/img/note-create/icon-image-fill.png";
+import icon_image_fill from '~/assets/img/note-create/icon-image-fill.png'
 // import { VueCropper } from 'vue-cropper'
 // import 'vue-cropper/dist/index.css'
 
@@ -112,13 +103,10 @@ async function handleCropperOk() {
       formData.append('uploadFile', data)
       formData.append('asImage', true)
       formData.append('fieldName', 'travelNotesBanner')
-      const res = await request(
-        '/admin/app/tourismProjectTravelNotesWrite/upload',
-        {
-          method: 'post',
-          body: formData
-        }
-      )
+      const res = await request('/admin/app/tourismProjectTravelNotesWrite/upload', {
+        method: 'post',
+        body: formData
+      })
       const url = res.data.fileUrl
       bannerUrl.value = url
       cropperDialogVisible.value = false

+ 12 - 0
src/components/CreateNote/InsertTitleSection.vue

@@ -3,6 +3,18 @@
     class="relative box-border flex min-h-60 items-center justify-between space-x-10 rounded-lg border border-dashed border-primary bg-white px-10 py-10"
     ref="target"
   >
+    <van-cell-group class="border" inset>
+      <van-field
+        v-model="title"
+        rows="1"
+        autosize
+        label-width="5"
+        type="textarea"
+        placeholder="请输入段落标题..."
+        maxlength="50"
+        show-word-limit
+      ></van-field>
+    </van-cell-group>
     <span class="text-3xl text-black-3">{{ title }}</span>
     <div
       v-show="!isOutside"

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


+ 6 - 6
src/components/Navbar/LeftMenu.vue → src/components/NavigationBar/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("/");

+ 11 - 1
src/components/Navbar/index.vue → src/components/NavigationBar/index.vue

@@ -15,7 +15,7 @@
       position="left"
       :style="{ height: '100%', width: '70%' }"
     >
-      <NavBarLeftMenu v-if="isMenuShow" @on-hide="isMenuShow = false" />
+      <NavigationBarLeftMenu v-if="isMenuShow" @on-hide="isMenuShow = false" />
     </van-popup>
   </div>
 </template>
@@ -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>

+ 1 - 1
src/components/TravelProject/Item.vue

@@ -19,7 +19,7 @@
       <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">
+      <div class="text-[#FF2020] flex items-center space-x-5 mt-5">
         <span>{{ itemData.price }}</span>
         <span>{{ itemData.priceUnit }}</span>
       </div>

+ 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>

+ 1 - 1
src/layouts/default.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="">
-    <NavBar />
+    <NavigationBar />
     <div class="pt-60">
       <slot></slot>
     </div>

+ 0 - 2
src/pages/labour/index.vue

@@ -1,7 +1,5 @@
 <template>
   <div>
-    <!-- <Navbar title="出国劳务" /> -->
-
     <div class="px-15">
       <van-swipe
         :autoplay="3000"

+ 29 - 44
src/pages/note-create-start/index.client.vue

@@ -1,56 +1,44 @@
 <template>
-
-    <div
-    class="relative h-full  pt-180 h-screen w-full  bg-[url('~/assets/img/note-create/note_create_bg.png')] bg-cover  bg-cover"
+  <!--  -->
+  <div
+    class="relative pt-180 h-screen w-full bg-[url('~/assets/img/note-create/note_create_bg.png')] bg-cover bg-cover"
   >
-  <!-- absolute  left-1/2 -translate-x-1/2-->
- 
     <NuxtLink
       to="/note-create"
-      class=" block mx-auto  pt-25 px-10  flex h-174 w-274  cursor-pointer flex-col items-center justify-center rounded-[30px] bg-[#fff1db] transition-all hover:opacity-90"
+      class="block mx-auto pt-25 px-10 flex h-174 w-274 cursor-pointer flex-col items-center justify-center rounded-[30px] bg-[#fff1db] transition-all hover:opacity-90"
     >
-      <img
-        src="~/assets/img/note-create/note_create_entry.png"
-        class="h-auto w-full" 
-      />
-      <!-- <span class="text-[8px] text-black-9"> 
-        夕阳下的金色狂野,是我心中最美的风景,也是我旅行的意义。
-      </span> -->
-      <div class="  -mt-5 text-2xl font-semibold text-primary">旅行日记</div>
+      <img src="~/assets/img/note-create/note_create_entry.png" class="h-auto w-full" />
+
+      <div class="-mt-5 text-2xl font-semibold text-primary">旅行日记</div>
     </NuxtLink>
-  
-   <!-- absolute top-460  left-1/2  -translate-x-1/2-->
+
     <NuxtLink
       to="/profile/notes?tab=draft"
-      class=" w-165 box-border mx-auto  mt-90 flex h-60 cursor-pointer   items-center space-x-5 rounded-xl bg-[#fff1c7] px-20 hover:opacity-80"
+      class="w-165 box-border mx-auto mt-90 flex h-60 cursor-pointer items-center space-x-5 rounded-xl bg-[#fff1c7] px-20 hover:opacity-80"
     >
-    <img
-        src="~/assets/img/note-create/note_create_book.png"
-        class="h-29 w-23"
-        alt=""
-        srcset=""
-      />
-      <span class="text-sm pl-5  text-black-6">我的草稿箱({{ draftTotal }})</span>
-     
+      <img src="~/assets/img/note-create/note_create_book.png" class="h-29 w-23" alt="" srcset="" />
+      <span class="text-sm pl-5 text-black-6">我的草稿箱({{ draftTotal }})</span>
     </NuxtLink>
-    <!-- bottom-60 left-1/2 -translate-x-1/2 -->
-    <div
-      class="mx-auto  mt-30  flex w-280 items-center justify-center"
-    >
+
+    <div class="mx-auto mt-30 flex w-280 items-center justify-center">
       <img
         src="~/assets/img/note-create/note_create_start_icon.png"
         class="h-45 w-45"
         alt=""
         srcset=""
       />
-      
-      <span class="text-sm text-white opacity-60"
-        >世界年轻一代更喜欢用的旅游网站 年轻旅行者共同打造的"旅行神器”</span
-      >
+
+      <span class="text-sm text-white opacity-60">
+        世界年轻一代更喜欢用的旅游网站 年轻旅行者共同打造的"旅行神器”
+      </span>
     </div>
+    <!-- absolute  left-1/2 -translate-x-1/2-->
+    <!-- absolute top-460  left-1/2  -translate-x-1/2-->
+    <!-- bottom-60 left-1/2 -translate-x-1/2 -->
+    <!-- <span class="text-[8px] text-black-9"> 
+        夕阳下的金色狂野,是我心中最美的风景,也是我旅行的意义。
+      </span> -->
   </div>
-
-  
 </template>
 
 <script setup>
@@ -61,16 +49,13 @@ onMounted(() => {
 const draftTotal = ref(0)
 
 async function getDraftList() {
-  const { data } = await request(
-    '/website/tourism/publishTravelNotes/getDraftList',
-    {
-      query: {
-        pageNum: 1,
-        pageSize: 10000,
-        type: 0
-      }
+  const { data } = await request('/website/tourism/publishTravelNotes/getDraftList', {
+    query: {
+      pageNum: 1,
+      pageSize: 10000,
+      type: 0
     }
-  )
+  })
   draftTotal.value = data.totalCount
 }
 </script>

+ 306 - 142
src/pages/note-create/index.client.vue

@@ -1,8 +1,7 @@
 <template>
-  <div v-if="!loading"  class="box-border pb-80">
+  <div v-if="!loading" class="box-border pb-80">
     <CreateNoteHeaderBanner v-model:bannerUrl="noteJson.travelNotesBanner" />
-   
-  
+
     <CreateNoteForm
       v-model:departureTime="noteJson.departureTime"
       v-model:countTimes="noteJson.countTimes"
@@ -14,60 +13,205 @@
       v-model:travelNumber="noteJson.travelNumber"
     />
 
-    
-    <div class="flex items-center pl-16 w-full h-40 ">
+    <div class="flex items-center pl-16 pt-16 w-full h-40">
       <div class="w-2 h-14 bg-[#FF9300] mr-16"></div>
       <h1 class="text-sm font-bold">编辑游记文章</h1>
     </div>
-  
-    <van-cell-group class="border"  inset>
-      <van-field
-        v-model="noteJson.projectTitle"
-        rows="1"
-        autosize
-        label-width="5"
-        type="textarea"
-        placeholder="从这里开始游记大标题..."
-        maxlength="50"
-        show-word-limit
+    <div class="mb-12">
+      <van-cell-group class="border" inset>
+        <van-field
+          v-model="noteJson.projectTitle"
+          rows="1"
+          autosize
+          type="textarea"
+          placeholder="从这里开始游记大标题..."
+          maxlength="50"
+          show-word-limit
+        ></van-field>
+      </van-cell-group>
+    </div>
+
+    <template v-for="(item, index) in noteJson.travelNotesContent" :key="item.tmpId">
+      <div v-if="item.type === defaultSectionTitle.type" class="h-50 mb-12 relative box-border">
+        <van-cell-group class="border h-full" inset>
+          <van-field
+            v-model="item.content"
+            rows="1"
+            autosize
+            class="text-base"
+            type="textarea"
+            trigger="onChange"
+            validate-trigger="onChange"
+            placeholder="请输入段落标题..."
+            @click-input="handleInsertOrEditTitleIndex(index)"
+            @update:model-value="handleInsertOrEditTitle"
+          >
+            <template #extra>
+              <div class="w-16 h-16 z-0">
+                <img
+                  @click="handleDeleteTitle(index)"
+                  class="w-full h-full"
+                  src="~/assets/img/note-create/delete.svg"
+                  alt=""
+                />
+              </div>
+            </template>
+          </van-field>
+        </van-cell-group>
+      </div>
+
+      <div
+        v-else-if="item.type === defaultSectionContent.type"
+        class="h-100 mb-12 relative box-border"
+      >
+        <van-cell-group class="border h-full" inset>
+          <van-field
+            v-model="item.content"
+            rows="1"
+            autosize
+            class="text-base border"
+            type="textarea"
+            trigger="onChange"
+            validate-trigger="onChange"
+            placeholder="请在这里输入游记正文..."
+            @click-input="handleInsertOrEditTitleIndex(index)"
+            @update:model-value="handleInsertOrEditTitle"
+          >
+            <template #extra>
+              <div class="w-16 h-16 z-0">
+                <img
+                  @click="handleDeleteTitle(index)"
+                  class="w-full h-full"
+                  src="../../assets/img/note-create/delete.svg"
+                  alt=""
+                />
+              </div>
+            </template>
+          </van-field>
+        </van-cell-group>
+      </div>
+
+      <div
+        v-else-if="item.type === defaultSectionImage.type"
+        class="relative box-border h-203 mb-12"
       >
-     </van-field>
-    </van-cell-group>
+        <van-cell-group class="border h-full" inset>
+          <!-- <van-field>
+            <template #input>
+              <van-uploader v-model="value">
+                <template #default>
+                  <div class="w-full h-full flex justify-center items-center">
+                    <button
+                      class="box-border active:bg-[#000]/[0.1] text-base flex justify-center items-center px-28 py-10 rounded-full border"
+                    >
+                      <div class="h-16 w-16 mr-8">
+                        <img
+                          class="w-full h-full"
+                          src="../../assets/img/note-create/img.svg"
+                          alt=""
+                        />
+                      </div>
+
+                      插入图片
+                    </button>
+                  </div>
+                </template>
+              </van-uploader>
+            </template>
+          </van-field> -->
+
+          <div class="w-full h-full flex justify-center items-center">
+            <van-uploader v-model="fileList" :after-read="afterRead" :multiple="false" >
+              <button
+                @click="handleInsertOrEditTitleIndex(index)"
+                class="box-border active:bg-[#000]/[0.1] text-base flex justify-center items-center px-28 py-10 rounded-full border"
+              >
+                <div class="h-16 w-16 mr-8">
+                  <img class="w-full h-full" src="~/assets/img/note-create/img.svg" alt="" />
+                </div>
+
+                插入图片
+              </button>
+            </van-uploader>
+            
+          </div>
+        </van-cell-group>
+        <div v-if="item.content ">
+              <img :src="item.content" alt="Uploaded Image" />
+        </div>
 
+        <div class="w-86 flex justify-between items-center h-16 absolute top-[10px] right-26 z-1">
+          <span @click="handleSaveCover(item)" class="text-sm text-black-3">设为封面图</span>
+          <div class="w-16 h-16 inline-block">
+            <img
+              @click="handleDeleteImage(index)"
+              class="w-full h-full"
+              src="../../assets/img/note-create/delete.svg"
+              alt=""
+            />
+          </div>
+        </div>
+      </div>
+    </template>
+
+    <div class="px-16 flex justify-between">
+      <button
+        @click="handleInsertOrEditTitleOk(defaultSectionTitle)"
+        class="w-110 active:bg-[#000]/[0.1] h-44 box-border text-base text-black-3 flex justify-center rounded-md items-center bg-[#F3F3F3]"
+      >
+        <van-icon name="plus" class="mr-5" color="#FF9300" />
+        插入小标题
+      </button>
+      <button
+        @click="handleInsertOrEditTitleOk(defaultSectionContent)"
+        class="w-110 h-44 active:bg-[#000]/[0.1] box-border text-base text-black-3 flex justify-center rounded-md items-center bg-[#F3F3F3]"
+      >
+        <van-icon name="plus" class="mr-5" color="#FF9300" />
+        插入内容
+      </button>
+      <!-- @click="handleInsertOrEditTitleOk(defaultSectionImage)" -->
+      <button
+        @click="handleInsertOrEditTitleOk(defaultSectionImage)"
+        class="w-110 active:bg-[#000]/[0.1] h-44 box-border text-base text-black-3 flex justify-center rounded-md items-center bg-[#F3F3F3]"
+      >
+        <van-icon name="plus" class="mr-5" color="#FF9300" />
+        插入图片
+      </button>
+    </div>
 
-    <div  class="fixed box-border p-16 shadow-[0_-4px_4px_0px_rgba(0,0,0,0.1)] bottom-0 left-0 w-full h-80 flex justify-between bg-white items-center">
+    <div
+      class="fixed box-border p-16 shadow-[0_-4px_4px_0px_rgba(0,0,0,0.1)] bottom-0 left-0 w-full h-80 flex justify-between bg-white items-center"
+    >
       <div class="flex justify-start items-center h-50">
-        <div class="w-50  text-center">
+        <div class="w-50 text-center"  @click="handleSaveDraft">
           <van-icon name="notes-o" size="24px" />
           <p class="text-sm text-black-3">草稿</p>
         </div>
-        <div class="w-50  text-center">
+        <div class="w-50 text-center">
           <van-icon name="eye-o" size="24px" />
           <p class="text-sm text-black-3">预览</p>
         </div>
-       
       </div>
 
-      
-        <van-button  class="w-full h-full flex  items-center"  size="large" type="primary" round color="#FD9A00"  block> 
-         <div class=" inline-block w-22 h-22 ">
-          <van-image
-        width="100%"
-        height="100%"
-        :src="upload"
-      />
-      </div> 发布
-         </van-button>
-
-  
+      <van-button
+        class="w-full h-full flex items-center"
+        size="large"
+        type="primary"
+        round
+        color="#FD9A00"
+        block
+      >
+        <template #icon>
+          <van-image width="22" height="22" :src="upload" />
+        </template>
+        发布
+      </van-button>
     </div>
-    <div
-      class="flex justify-center "
-    >
+
+    <div class="flex justify-center">
       <div class="mx-auto mt-30 flex w-wrap space-x-50">
-        <!-- <div class="min-h-360 w-[860px] pb-80"> -->
-          <div>
-            <!-- <VueDraggable v-model="noteJson.travelNotesContent">
+        <div>
+          <!-- <VueDraggable v-model="noteJson.travelNotesContent">
               <template
                 v-for="(item, index) in noteJson.travelNotesContent"
                 :key="item.tmpId"
@@ -93,23 +237,13 @@
                 </template>
               </template>
             </VueDraggable> -->
-            <!-- <CreateNoteBottomActions
+          <!-- <CreateNoteBottomActions
               class="mt-50"
               :publishLoading="publishLoading"
               @on-preview="handlePreview"
               @on-publish="handlePublish"
             /> -->
-          </div>
-        <!-- </div> -->
-        <!-- <el-affix :offset="20">
-          <CreateNoteLeftActions
-            @on-insert-title="handleInsertOrEditTitle"
-            @on-insert-content="handleInsertContent"
-            @on-insert-img="handleInsertImage"
-           
-          />
-           @on-save-draft="handleSaveDraft"
-        </el-affix> -->
+        </div>
       </div>
     </div>
 
@@ -133,17 +267,15 @@
     <!-- <CreateNotePublishResultModal
       v-model:visible="publishResultModalOptions.show"
     /> -->
-  
   </div>
 </template>
 
 <script setup>
-import upload  from '../../assets/img/note-create/upload.svg'
+import upload from '~/assets/img/note-create/upload.svg'
 import { cloneDeep } from 'lodash-es'
 // import { VueDraggable } from 'vue-draggable-plus'
 import { nanoid } from 'nanoid'
 
-
 const { loading, setLoading } = useLoading()
 loading.value = false
 
@@ -188,30 +320,30 @@ watch(noteJson, () => {}, { deep: true })
 // )
 
 // 获取草稿详情
-// async function getNoteDetail() {
-//   try {
-//     setLoading(true)
-//     const res = await request(
-//       `/website/tourism/publishTravelNotes/getDraftDetail?writeId=${id.value}`
-//     )
-//     const data = res.data ?? {}
-//     Object.keys(noteJson).forEach((key) => {
-//       noteJson[key] = data[key]
-//       noteJson.travelNotesContent = data.travelNotesContent ?? []
-//       if (noteJson.travelNotesContent.length === 0) {
-//         noteJson.travelNotesContent.push({
-//           type: defaultSectionContent.type,
-//           content: '',
-//           tmpId: nanoid()
-//         })
-//       }
-//     })
+async function getNoteDetail() {
+  try {
+    setLoading(true)
+    const res = await request(
+      `/website/tourism/publishTravelNotes/getDraftDetail?writeId=${id.value}`
+    )
+    const data = res.data ?? {}
+    Object.keys(noteJson).forEach((key) => {
+      noteJson[key] = data[key]
+      noteJson.travelNotesContent = data.travelNotesContent ?? []
+      if (noteJson.travelNotesContent.length === 0) {
+        noteJson.travelNotesContent.push({
+          type: defaultSectionContent.type,
+          content: '',
+          tmpId: nanoid()
+        })
+      }
+    })
 
-//     setLoading(false)
-//   } catch (error) {
-//     setLoading(false)
-//   }
-// }
+    setLoading(false)
+  } catch (error) {
+    setLoading(false)
+  }
+}
 
 /************ 插入段落标题逻辑 ********** */
 
@@ -220,32 +352,25 @@ const insertTilteOptions = reactive({
   content: null,
   editIndex: null
 })
+const editIndex = ref(null)
 
 // 点击编辑或者新增段落标题,弹出dialog
-function handleInsertOrEditTitle(index) {
-  if (index === null || index === undefined) {
-    // 新增
-    insertTilteOptions.editIndex = null
-    insertTilteOptions.content = null
-  } else {
-    // 编辑
-    insertTilteOptions.editIndex = index
-    insertTilteOptions.content = noteJson.travelNotesContent[index].content
-  }
-  insertTilteOptions.show = true
+function handleInsertOrEditTitleIndex(index) {
+  editIndex.value = index
+}
+
+// 点击编辑或者新增段落标题,弹出dialog
+function handleInsertOrEditTitle(value) {
+  noteJson.travelNotesContent[editIndex.value].content = value
 }
 
 // 确认编辑或者新增段落标题
 function handleInsertOrEditTitleOk(newTitle) {
-  if (insertTilteOptions.editIndex === null) {
-    noteJson.travelNotesContent.push({
-      type: defaultSectionTitle.type,
-      content: newTitle,
-      tmpId: nanoid()
-    })
-  } else {
-    noteJson.travelNotesContent[insertTilteOptions.editIndex].content = newTitle
-  }
+  // if (insertTilteOptions.editIndex === null) {
+  noteJson.travelNotesContent.push({
+    ...newTitle,
+    tmpId: nanoid()
+  })
 }
 
 // 删除段落标题
@@ -264,28 +389,27 @@ function handleInsertContent() {
   )
 }
 
-function handleDeleteContent(index) {
-  noteJson.travelNotesContent.splice(index, 1)
-}
+// function handleDeleteContent(index) {
+//   noteJson.travelNotesContent.splice(index, 1)
+// }
 
 /******************插入图片逻辑*******************/
 const insertImageOptions = reactive({
   show: false
 })
+
 function handleInsertImage() {
   insertImageOptions.show = true
 }
 
-function handleInsertImageOk(fileUrlList) {
-  const imageList = fileUrlList.map((e) => ({
-    type: defaultSectionImage.type,
-    content: e.fileUrl,
-    tmpId: nanoid()
-  }))
-  noteJson.travelNotesContent = (noteJson.travelNotesContent ?? []).concat(
-    imageList
-  )
-}
+// function handleInsertImageOk(fileUrlList) {
+//   const imageList = fileUrlList.map((e) => ({
+//     type: defaultSectionImage.type,
+//     content: e.fileUrl,
+//     tmpId: nanoid()
+//   }))
+//   noteJson.travelNotesContent = (noteJson.travelNotesContent ?? []).concat(imageList)
+// }
 
 function handleDeleteImage(index) {
   if (noteJson.travelNotesContent[index].type === 'image') {
@@ -301,23 +425,66 @@ function handleSaveCover(item) {
     }
     item.cover = 1
   })
-  ElMessage.success('设置封面成功')
+  showToast('设置封面成功')
 }
 
-//保存为草稿
-// async function handleSaveDraft() {
-//   try {
-//     await request('/website/tourism/publishTravelNotes/saveDraft', {
-//       method: 'post',
-//       body: {
-//         ...noteJson,
-//         id: id.value
-//       }
-//     })
-//     ElMessage.success('草稿保存成功')
-//   } finally {
-//   }
-// }
+const fileList = ref([])
+// 图片上传
+const afterRead = (file) => {
+      // 此时可以自行将文件上传至服务器
+      uploadFile(file)
+      
+
+};
+
+const useAuth = useAuthStore()
+const { token } = storeToRefs(useAuth)
+
+const uploadUrl = `${import.meta.env.VITE_APP_BASE_URL}/admin/app/tourismProjectTravelNotesWrite/upload`
+
+const uploadFile = (file) => {
+  // 使用 FormData 来包装文件,模拟表单提交
+  const formData = new FormData();
+  formData.append('file', file);
+  // 这里假设使用 axios 进行文件上传
+  request(uploadUrl, formData, {
+    headers: {
+      Authorization:token,
+      'Content-Type': 'multipart/form-data'
+    }
+  }).then(res => {
+    console.log(res,'img1111');
+    // noteJson.travelNotesContent.map(el=>{
+    //   if (el.type==='image') {
+    //     noteJson.travelNotesContent[editIndex.value].content=''
+    //   }
+    // })
+    
+    
+    // 处理上传成功的逻辑
+    // Toast('上传成功');
+  }).catch(err=> {
+    console.log(err,'img');
+    
+    // 处理上传失败的逻辑
+    // Toast('上传失败,请重试');
+  });
+};
+
+// 保存为草稿
+async function handleSaveDraft() {
+  try {
+    await request('/website/tourism/publishTravelNotes/saveDraft', {
+      method: 'post',
+      body: {
+        ...noteJson,
+        // id: id?.value
+      }
+    })
+    showToast('草稿保存成功')
+  } finally {
+  }
+}
 
 // 预览
 const previewOptions = reactive({
@@ -342,39 +509,36 @@ const publishResultModalOptions = reactive({
 
 // 发布
 async function handlePublish() {
-
   if (!noteJson.travelNotesBanner) {
     showNotify({
-    message: '请设置游记头图',
-    duration: 3000,
-    });
+      message: '请设置游记头图',
+      duration: 3000
+    })
     return
   }
   if (!noteJson.projectTitle) {
     showNotify({
-    message: '请输入游记标题',
-    duration: 3000,
-    });
+      message: '请输入游记标题',
+      duration: 3000
+    })
     return
   }
   if (!noteJson.endPlace) {
     showNotify({
-    message: '请选择目的地',
-    duration: 3000,
-    });
+      message: '请选择目的地',
+      duration: 3000
+    })
     return
   }
   if (noteJson.travelNotesContent.length === 0) {
     showNotify({
-    message: '游记内容不能为空',
-    duration: 3000,
-    });
+      message: '游记内容不能为空',
+      duration: 3000
+    })
     return
   }
 
-  const { data: isPerfect } = await request(
-    '/website/tourism/publishTravelNotes/isPerfect'
-  )
+  const { data: isPerfect } = await request('/website/tourism/publishTravelNotes/isPerfect')
   if (isPerfect === 0) {
     // 需要收集个人信息
     userInfoOptions.show = true

+ 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,
     },

Some files were not shown because too many files changed in this diff