ソースを参照

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

suwenjiang 2 ヶ月 前
コミット
d1ce0bc3b7
65 ファイル変更2095 行追加260 行削除
  1. 27 4
      src/assets/iconfont/demo_index.html
  2. 8 4
      src/assets/iconfont/iconfont.css
  3. 0 0
      src/assets/iconfont/iconfont.js
  4. 7 0
      src/assets/iconfont/iconfont.json
  5. 2 0
      src/assets/iconfont/iconfont.svg
  6. BIN
      src/assets/iconfont/iconfont.ttf
  7. BIN
      src/assets/iconfont/iconfont.woff
  8. BIN
      src/assets/iconfont/iconfont.woff2
  9. BIN
      src/assets/img/car/car_banner_tmp.png
  10. BIN
      src/assets/img/car/car_from_type_self copy.png
  11. BIN
      src/assets/img/car/car_from_type_self.png
  12. BIN
      src/assets/img/car/car_from_type_third copy.png
  13. BIN
      src/assets/img/car/car_from_type_third.png
  14. BIN
      src/assets/img/car/car_home_card_1.png
  15. BIN
      src/assets/img/car/car_home_card_2.png
  16. BIN
      src/assets/img/car/car_home_card_3.png
  17. BIN
      src/assets/img/car/car_home_card_4.png
  18. BIN
      src/assets/img/car/car_home_card_5.png
  19. BIN
      src/assets/img/car/car_home_intro_1.png
  20. BIN
      src/assets/img/car/car_home_intro_2.png
  21. BIN
      src/assets/img/car/car_home_intro_3.png
  22. BIN
      src/assets/img/car/car_home_intro_4.png
  23. BIN
      src/assets/img/car/car_selected_bg.png
  24. BIN
      src/assets/img/car/car_type_all.png
  25. BIN
      src/assets/img/car/car_type_bus.png
  26. BIN
      src/assets/img/car/car_type_jiaoche.png
  27. BIN
      src/assets/img/car/car_type_mpv.png
  28. BIN
      src/assets/img/car/car_type_other.png
  29. BIN
      src/assets/img/car/car_type_suv.png
  30. 14 0
      src/components/Car/Home/Banner.vue
  31. 188 0
      src/components/Car/Home/BaseFilter.vue
  32. 62 0
      src/components/Car/Home/IntroSection1.vue
  33. 51 0
      src/components/Car/Home/IntroSection2.vue
  34. 199 0
      src/components/Car/Search/BaseFilter.vue
  35. 53 0
      src/components/Car/Search/CarFilter.vue
  36. 63 0
      src/components/Car/Search/CarList/Item.vue
  37. 97 0
      src/components/Car/Search/CarList/index.vue
  38. 53 0
      src/components/Car/Search/DriverList/Item.vue
  39. 132 0
      src/components/Car/Search/DriverList/index.vue
  40. 56 0
      src/components/Car/Search/TypeTab.vue
  41. 34 42
      src/components/Footer/index.vue
  42. 4 2
      src/components/Home/Banner.vue
  43. 0 39
      src/components/Home/Menu_old.vue
  44. 4 4
      src/components/Home/TravelMenu/Item.vue
  45. 3 3
      src/components/Home/TravelNotes/Item.vue
  46. 1 1
      src/components/Home/TravelNotes/index.vue
  47. 104 72
      src/components/NavigationBar/LeftMenu.vue
  48. 18 0
      src/components/NavigationBar/LeftMenuItem.vue
  49. 17 18
      src/components/NavigationBar/index.client.vue
  50. 18 11
      src/components/Profile/TravelOrders/Item.vue
  51. 2 0
      src/components/Profile/TravelOrders/index.vue
  52. 41 2
      src/components/TravelProjectDetail/BookInfo.vue
  53. 21 0
      src/middleware/car-search.global.js
  54. 14 0
      src/pages/car/index.client.vue
  55. 75 0
      src/pages/car/search.client.vue
  56. 203 0
      src/pages/car/submit-order.client.vue
  57. 1 1
      src/pages/index.vue
  58. 173 0
      src/pages/profile/car-order/[id].client.vue
  59. 187 0
      src/pages/profile/car-orders.vue
  60. 33 28
      src/pages/profile/index.vue
  61. 29 8
      src/pages/profile/travel-order/[id].vue
  62. 46 8
      src/pages/profile/travel-orders.vue
  63. 18 1
      src/pages/t/[id].client.vue
  64. 19 2
      src/pages/travel-projects/index.client.vue
  65. 18 10
      src/stores/useAuth.js

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

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

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

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

ファイルの差分が大きいため隠しています
+ 0 - 0
src/assets/iconfont/iconfont.js


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

@@ -6,6 +6,13 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "4767011",
+      "name": "right",
+      "font_class": "right",
+      "unicode": "e7eb",
+      "unicode_decimal": 59371
+    },
+    {
       "icon_id": "4767059",
       "name": "menu",
       "font_class": "menu",

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

@@ -14,6 +14,8 @@
     />
       <missing-glyph />
       
+      <glyph glyph-name="right" unicode="&#59371;" d="M765.7 409.2L314.9 761.3c-5.3 4.1-12.9 0.4-12.9-6.3v-77.3c0-4.9 2.3-9.6 6.1-12.6l360-281.1-360-281.1c-3.9-3-6.1-7.7-6.1-12.6V13c0-6.7 7.7-10.4 12.9-6.3l450.8 352.1c16.4 12.8 16.4 37.6 0 50.4z"  horiz-adv-x="1024" />
+      
       <glyph glyph-name="menu" unicode="&#59380;" d="M904 736H120c-4.4 0-8-3.6-8-8v-64c0-4.4 3.6-8 8-8h784c4.4 0 8 3.6 8 8v64c0 4.4-3.6 8-8 8zM904 112H120c-4.4 0-8-3.6-8-8v-64c0-4.4 3.6-8 8-8h784c4.4 0 8 3.6 8 8v64c0 4.4-3.6 8-8 8zM904 424H120c-4.4 0-8-3.6-8-8v-64c0-4.4 3.6-8 8-8h784c4.4 0 8 3.6 8 8v64c0 4.4-3.6 8-8 8z"  horiz-adv-x="1024" />
       
       <glyph glyph-name="message" unicode="&#59274;" d="M512 384m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM712 384m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM312 384m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM925.2 557.6c-22.6 53.7-55 101.9-96.3 143.3-41.3 41.3-89.5 73.8-143.3 96.3C630.6 820.3 572.2 832 512 832h-2c-60.6-0.3-119.3-12.3-174.5-35.9-53.3-22.8-101.1-55.2-142-96.5-40.9-41.3-73-89.3-95.2-142.8-23-55.4-34.6-114.3-34.3-174.9 0.3-69.4 16.9-138.3 48-199.9v-152c0-25.4 20.6-46 46-46h152.1c61.6-31.1 130.5-47.7 199.9-48h2.1c59.9 0 118 11.6 172.7 34.3 53.5 22.3 101.6 54.3 142.8 95.2 41.3 40.9 73.8 88.7 96.5 142 23.6 55.2 35.6 113.9 35.9 174.5 0.3 60.9-11.5 120-34.8 175.6z m-151.1-438C704 50.2 611 12 512 12h-1.7c-60.3 0.3-120.2 15.3-173.1 43.5l-8.4 4.5H188V200.8l-4.5 8.4C155.3 262.1 140.3 322 140 382.3c-0.4 99.7 37.7 193.3 107.6 263.8 69.8 70.5 163.1 109.5 262.8 109.9h1.7c50 0 98.5-9.7 144.2-28.9 44.6-18.7 84.6-45.6 119-80 34.3-34.3 61.3-74.4 80-119 19.4-46.2 29.1-95.2 28.9-145.8-0.6-99.6-39.7-192.9-110.1-262.7z"  horiz-adv-x="1024" />

BIN
src/assets/iconfont/iconfont.ttf


BIN
src/assets/iconfont/iconfont.woff


BIN
src/assets/iconfont/iconfont.woff2


BIN
src/assets/img/car/car_banner_tmp.png


BIN
src/assets/img/car/car_from_type_self copy.png


BIN
src/assets/img/car/car_from_type_self.png


BIN
src/assets/img/car/car_from_type_third copy.png


BIN
src/assets/img/car/car_from_type_third.png


BIN
src/assets/img/car/car_home_card_1.png


BIN
src/assets/img/car/car_home_card_2.png


BIN
src/assets/img/car/car_home_card_3.png


BIN
src/assets/img/car/car_home_card_4.png


BIN
src/assets/img/car/car_home_card_5.png


BIN
src/assets/img/car/car_home_intro_1.png


BIN
src/assets/img/car/car_home_intro_2.png


BIN
src/assets/img/car/car_home_intro_3.png


BIN
src/assets/img/car/car_home_intro_4.png


BIN
src/assets/img/car/car_selected_bg.png


BIN
src/assets/img/car/car_type_all.png


BIN
src/assets/img/car/car_type_bus.png


BIN
src/assets/img/car/car_type_jiaoche.png


BIN
src/assets/img/car/car_type_mpv.png


BIN
src/assets/img/car/car_type_other.png


BIN
src/assets/img/car/car_type_suv.png


+ 14 - 0
src/components/Car/Home/Banner.vue

@@ -0,0 +1,14 @@
+<template>
+  <van-swipe :autoplay="3000" :show-indicators="false">
+    <van-swipe-item v-for="item in 1">
+      <img
+        class="aspect-[375/166] w-full object-cover"
+        src="~/assets/img/car/car_banner_tmp.png"
+      />
+    </van-swipe-item>
+  </van-swipe>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped></style>

+ 188 - 0
src/components/Car/Home/BaseFilter.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="w-full rounded-xl bg-[#94adb4] px-12 pb-12 pt-20">
+    <div class="text-xl font-bold text-white">逍遥甄选,自由包车</div>
+    <div
+      class="bg-white rounded-xl pt-10 pb-15 mt-10 px-20 text-black-3 text-base"
+    >
+      <div
+        @click="startPlaceOption.show = true"
+        class="flex items-center justify-between py-15"
+      >
+        <span>起点:{{ startPlaceLabel }}</span>
+        <span class="iconfont icon-right"></span>
+      </div>
+      <van-divider />
+      <div class="flex items-center justify-between space-x-30">
+        <div
+          @click="startDateOption.show = true"
+          class="flex-1 py-15 flex items-center"
+        >
+          <span class="flex-1">用车时间:{{ formData.startDate }}</span>
+          <span class="iconfont icon-right"></span>
+        </div>
+        <div
+          @click="daysOption.show = true"
+          class="w-85 flex items-center py-15"
+        >
+          <span>天数:</span>
+          <span class="flex-1">{{ formData.days }}天</span>
+          <span class="iconfont icon-right"></span>
+        </div>
+      </div>
+      <van-divider />
+      <div class="flex items-center justify-center">
+        <van-button
+          @click="handleSearch"
+          color="#FD9A00"
+          round
+          class="w-[80%]"
+          style="font-size: 16px; margin-top: 10px"
+          >查询</van-button
+        >
+      </div>
+    </div>
+    <van-popup
+      v-model:show="startPlaceOption.show"
+      round
+      position="bottom"
+      teleport="body"
+      :style="{ height: '60%' }"
+    >
+      <van-cascader
+        v-model="startPlaceOption.activedId"
+        title="请选择起点"
+        :options="startPlaceOption.options"
+        @close="startPlaceOption.show = false"
+        :field-names="{
+          text: 'menuName',
+          value: 'id',
+          children: 'children',
+        }"
+        @finish="startPlaceOnFinish"
+      />
+    </van-popup>
+    <van-popup
+      v-model:show="startDateOption.show"
+      round
+      position="bottom"
+      teleport="body"
+    >
+      <van-date-picker
+        title="选择日期"
+        @confirm="handleStartDateConfirm"
+        :min-date="new Date()"
+      />
+    </van-popup>
+    <van-popup
+      v-model:show="daysOption.show"
+      round
+      position="bottom"
+      teleport="body"
+    >
+      <van-picker
+        title="选择天数"
+        :columns="daysOption.options"
+        @confirm="handleDaysConfirm"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup>
+const route = useRoute();
+
+const formData = reactive({
+  startDate: "",
+  startPlace: "",
+  days: "",
+});
+
+// 起点
+const startPlaceOption = reactive({
+  show: false,
+  options: [],
+  selectedOptions: [],
+  activedId: "",
+});
+const startPlaceLabel = computed(() => {
+  return startPlaceOption.selectedOptions
+    .map((item) => item.menuName)
+    .join("/");
+});
+async function getStartPlaceOptions() {
+  const { data } = await request(
+    "/website/app/tourCarCategory/carContractTree"
+  );
+  startPlaceOption.options = tree(data);
+}
+function tree(list) {
+  return list.map((item) => {
+    return {
+      ...item,
+      children: item.children.length ? tree(item.children) : null,
+    };
+  });
+}
+function startPlaceOnFinish({ selectedOptions, value }) {
+  startPlaceOption.show = false;
+  startPlaceOption.selectedOptions = selectedOptions;
+  formData.startPlace = value;
+}
+
+// 用车时间
+const startDateOption = reactive({
+  show: false,
+  activedValue: null,
+  selectedValues: [],
+});
+function handleStartDateConfirm({ selectedValues }) {
+  startDateOption.show = false;
+  startDateOption.selectedValues = selectedValues;
+  formData.startDate = selectedValues.join("-");
+}
+
+// 用车天数
+const daysOption = reactive({
+  show: false,
+  options: Array.from({ length: 10 }, (_, i) => i).map((item) => ({
+    text: `${item + 1}天`,
+    value: `${item + 1}`,
+  })),
+});
+
+function handleDaysConfirm({ selectedOptions }) {
+  daysOption.show = false;
+  formData.days = selectedOptions[0].value;
+}
+
+watchEffect(() => {
+  formData.startDate = route.query.startDate;
+  formData.startPlace = route.query.startPlace;
+  formData.days = route.query.days || 1;
+});
+
+function handleSearch() {
+  if (!formData.startPlace) {
+    showToast("请选择起点");
+    return;
+  }
+  if (!formData.startDate) {
+    showToast("请选择用车时间");
+    return;
+  }
+  if (!formData.days) {
+    showToast("请选择天数");
+    return;
+  }
+  navigateTo({
+    path: "/car/search",
+    query: formData,
+  });
+}
+
+onMounted(() => {
+  getStartPlaceOptions();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 62 - 0
src/components/Car/Home/IntroSection1.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="flex flex-col items-center pt-15">
+    <div class="text-xl font-bold text-black-3">逍遥游包车</div>
+    <div class="mt-15 grid grid-cols-3 gap-8">
+      <div
+        v-for="(item, index) in data"
+        class="relative cursor-pointer overflow-hidden transition-all"
+        :class="{ 'col-span-2': index === 0 }"
+      >
+        <img
+          :src="item.image"
+          class="h-98 w-full object-fill"
+          :class="[index === 0 ? 'aspect-[221/96]' : 'aspect-[105/96]']"
+        />
+        <div
+          class="absolute bottom-0 left-0 right-0 top-0 bg-gradient-to-r from-[#57A6E1FF] to-[#ffffff00] pl-10 pt-10 text-2xl font-bold text-white"
+        >
+          <div class="text-base">{{ item.title }}</div>
+          <div class="text-sm opacity-60">{{ item.subTitle }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import car_home_card_1 from "~/assets/img/car/car_home_card_1.png";
+import car_home_card_2 from "~/assets/img/car/car_home_card_2.png";
+import car_home_card_3 from "~/assets/img/car/car_home_card_3.png";
+import car_home_card_4 from "~/assets/img/car/car_home_card_4.png";
+import car_home_card_5 from "~/assets/img/car/car_home_card_5.png";
+
+const data = [
+  {
+    image: car_home_card_1,
+    title: "专车服务",
+    subTitle: "专车服务绝不拼车套车",
+  },
+  {
+    image: car_home_card_2,
+    title: "车型任选",
+    subTitle: "以最全车型满足所有用车需求。",
+  },
+  {
+    image: car_home_card_3,
+    title: "一价全包",
+    subTitle: "无其他费用",
+  },
+  {
+    image: car_home_card_4,
+    title: "旅游包车",
+    subTitle: "尊享全天候VIP服务",
+  },
+  {
+    image: car_home_card_5,
+    title: "专属跟单",
+    subTitle: "正规资质车辆证件齐全",
+  },
+];
+</script>
+
+<style lang="scss" scoped></style>

+ 51 - 0
src/components/Car/Home/IntroSection2.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="grid grid-cols-2 gap-10">
+    <div
+      v-for="item in data"
+      class="relative aspect-[160/99] rounded-5xl bg-cover bg-center bg-no-repeat"
+      :style="{ backgroundImage: `url(${item.image})` }"
+    >
+      <div class="absolute left-10 top-10">
+        <div class="text-xl font-bold text-black-3">{{ item.title }}</div>
+        <div class="text-sm text-black-9">{{ item.subTitle }}</div>
+        <div
+          class="mt-5 flex h-18 w-48 text-sm items-center justify-center rounded-full bg-[#c9ddff] text-[#2B7BFF]"
+        >
+          逍遥游
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import car_home_intro_1 from "~/assets/img/car/car_home_intro_1.png";
+import car_home_intro_2 from "~/assets/img/car/car_home_intro_2.png";
+import car_home_intro_3 from "~/assets/img/car/car_home_intro_3.png";
+import car_home_intro_4 from "~/assets/img/car/car_home_intro_4.png";
+
+const data = [
+  {
+    image: car_home_intro_1,
+    title: "24小时客服",
+    subTitle: "在线售后服务,出行无忧",
+  },
+  {
+    image: car_home_intro_2,
+    title: "经验司机",
+    subTitle: "6年以上老司机培训上岗",
+  },
+  {
+    image: car_home_intro_3,
+    title: "行程自由",
+    subTitle: "随走随停行程自由随意!",
+  },
+  {
+    image: car_home_intro_4,
+    title: "航班追踪",
+    subTitle: "追踪航班动态,航变无忧",
+  },
+];
+</script>
+
+<style lang="scss" scoped></style>

+ 199 - 0
src/components/Car/Search/BaseFilter.vue

@@ -0,0 +1,199 @@
+<template>
+  <div>
+    <div
+      @click="isExpand = !isExpand"
+      class="h-40 w-full rounded-xl px-20 text-white font-semibold bg-gradient-to-r from-[#FD9A00] to-[#FFCC7C] flex items-center justify-between"
+    >
+      <div class="flex items-center space-x-15">
+        <span>{{ startPlaceLabel }}</span>
+        <span>{{ formData.startDate }}</span>
+        <span>包车{{ formData.days }}天</span>
+      </div>
+      <div
+        class="w-20 h-20 bg-[#FD9A00] flex items-center rounded-full justify-center"
+      >
+        <span
+          class="iconfont icon-caret-down text-white"
+          :class="{ 'rotate-180': isExpand }"
+        ></span>
+      </div>
+    </div>
+    <div v-show="isExpand">
+      <van-cell-group>
+        <van-cell
+          title="选择起点"
+          :value="startPlaceLabel"
+          is-link
+          @click="startPlaceOption.show = true"
+        />
+        <van-cell
+          title="选择用车时间"
+          :value="formData.startDate"
+          is-link
+          @click="startDateOption.show = true"
+        />
+        <van-cell
+          title="选择天数"
+          :value="`${formData.days}天`"
+          is-link
+          @click="daysOption.show = true"
+        />
+      </van-cell-group>
+    </div>
+    <van-popup
+      v-model:show="startPlaceOption.show"
+      round
+      position="bottom"
+      :style="{ height: '40%' }"
+    >
+      <van-cascader
+        v-model="startPlaceOption.activedId"
+        title="请选择起点"
+        :options="startPlaceOption.options"
+        @close="startPlaceOption.show = false"
+        :field-names="{
+          text: 'menuName',
+          value: 'id',
+          children: 'children',
+        }"
+        @finish="startPlaceOnFinish"
+      />
+    </van-popup>
+    <van-popup v-model:show="startDateOption.show" round position="bottom">
+      <van-date-picker
+        title="选择日期"
+        @confirm="handleStartDateConfirm"
+        :min-date="new Date()"
+      />
+    </van-popup>
+    <van-popup v-model:show="daysOption.show" round position="bottom">
+      <van-picker
+        title="选择天数"
+        :columns="daysOption.options"
+        @confirm="handleDaysConfirm"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup>
+const isExpand = ref(false);
+
+const route = useRoute();
+
+const formData = reactive({
+  startDate: "",
+  startPlace: "",
+  days: "",
+});
+
+watchEffect(() => {
+  formData.startDate = route.query.startDate;
+  formData.startPlace = route.query.startPlace;
+  formData.days = route.query.days;
+});
+
+// watch(
+//   formData,
+//   () => {
+//     console.log(formData);
+//   },
+//   { deep: true, immediate: true }
+// );
+
+// 起点
+const startPlaceOption = reactive({
+  show: false,
+  options: [],
+  selectedOptions: [],
+  activedId: "",
+});
+const startPlaceLabel = computed(() => {
+  return findTextInTree(startPlaceOption.options, formData.startPlace);
+});
+async function getStartPlaceOptions() {
+  const { data } = await request(
+    "/website/app/tourCarCategory/carContractTree"
+  );
+  startPlaceOption.options = tree(data);
+}
+function tree(list) {
+  return list.map((item) => {
+    return {
+      ...item,
+      children: item.children.length ? tree(item.children) : null,
+    };
+  });
+}
+function startPlaceOnFinish({ selectedOptions, value }) {
+  startPlaceOption.show = false;
+  startPlaceOption.selectedOptions = selectedOptions;
+  navigateTo({
+    path: "/car/search",
+    replace: true,
+    query: {
+      ...route.query,
+      startPlace: value,
+    },
+  });
+}
+function findTextInTree(tree, id) {
+  for (let i = 0; i < tree.length; i++) {
+    if (tree[i].id === id) {
+      return tree[i].menuName;
+    }
+    if (tree[i].children && tree[i].children.length) {
+      const text = findTextInTree(tree[i].children, id);
+      if (text) {
+        return text;
+      }
+    }
+  }
+}
+
+// 用车时间
+const startDateOption = reactive({
+  show: false,
+  activedValue: null,
+  selectedValues: [],
+});
+function handleStartDateConfirm({ selectedValues }) {
+  startDateOption.show = false;
+  startDateOption.selectedValues = selectedValues;
+  navigateTo({
+    path: "/car/search",
+    replace: true,
+    query: {
+      ...route.query,
+      startDate: selectedValues.join("-"),
+    },
+  });
+}
+
+// 用车天数
+const daysOption = reactive({
+  show: false,
+  options: Array.from({ length: 10 }, (_, i) => i).map((item) => ({
+    text: `${item + 1}天`,
+    value: `${item + 1}`,
+  })),
+});
+
+function handleDaysConfirm({ selectedOptions }) {
+  daysOption.show = false;
+  navigateTo({
+    path: "/car/search",
+    replace: true,
+    query: {
+      ...route.query,
+      days: selectedOptions[0].value,
+    },
+  });
+}
+
+onMounted(() => {
+  getStartPlaceOptions();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 53 - 0
src/components/Car/Search/CarFilter.vue

@@ -0,0 +1,53 @@
+<template>
+  <div>
+    <van-dropdown-menu
+      style="--van-dropdown-menu-shadow: none"
+      active-color="#fd9a00"
+    >
+      <van-dropdown-item v-model="category" :options="categoryList" />
+    </van-dropdown-menu>
+  </div>
+</template>
+
+<script setup>
+const route = useRoute();
+
+const category = ref(null);
+
+watchEffect(() => {
+  category.value = route.query.carModel ?? null;
+});
+
+watch(category, (val) => {
+  navigateTo({
+    replace: true,
+    query: {
+      ...route.query,
+      carModel: val,
+    },
+  });
+});
+
+const categoryList = ref([]);
+async function getCarCategoryList() {
+  const { data } = await request("/website/app/tourCarCategory/carModelList");
+  const options = data.dataList.map((e) => {
+    return {
+      text: e.typeName,
+      value: e.id,
+    };
+  });
+  categoryList.value = [
+    {
+      text: "车型不限",
+      value: null,
+    },
+    ...options,
+  ];
+}
+onMounted(() => {
+  getCarCategoryList();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 63 - 0
src/components/Car/Search/CarList/Item.vue

@@ -0,0 +1,63 @@
+<template>
+  <div
+    class="box-border flex cursor-pointer space-x-12 rounded-5xl border-[3px] bg-white transition-all hover:shadow-card"
+    :class="[active ? 'border-primary shadow-card' : 'border-white']"
+  >
+    <van-image
+      :src="itemData.image"
+      width="127"
+      height="127"
+      radius="10px"
+      fit="cover"
+      class="shrink-0"
+    />
+    <div class="flex w-0 flex-1 flex-col">
+      <div class="truncate text-xl font-semibold text-black-3">
+        {{ itemData.name }}
+      </div>
+      <div class="truncate text-base text-black-6">
+        {{ itemData.seatNumber }}座
+      </div>
+      <div class="truncate text-base text-black-6">
+        {{ itemData.luggageNumber }}行李(建议24寸行李箱)
+      </div>
+      <div class="flex items-center space-x-10">
+        <span class="text-base font-semibold text-black-6">评分</span>
+        <van-rate disabled :size="14" v-model="itemData.score" />
+      </div>
+      <div class="flex items-center justify-between">
+        <div>
+          <div class="truncate text-3xl font-semibold text-[#FF1D1D]">
+            {{ itemData.price }}{{ itemData.unit }}
+          </div>
+          <div class="shrink-0 text-sm text-black-9">(10小时)</div>
+        </div>
+        <van-button
+          @click="$emit('onSelect')"
+          type="primary"
+          color="#FD9A00"
+          size="small"
+          style="width: 70px; font-size: 14px"
+          round
+          >选择</van-button
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineEmits(["onSelect"]);
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({}),
+  },
+  active: {
+    type: Boolean,
+    default: false,
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 97 - 0
src/components/Car/Search/CarList/index.vue

@@ -0,0 +1,97 @@
+<template>
+  <div>
+    <van-empty
+      v-if="!listData.length && !loading"
+      image="search"
+      description="暂无相关车型"
+    />
+    <van-list
+      v-else-if="listData.length"
+      class=""
+      v-model:loading="loading"
+      :finished="finished"
+      finished-text="-- 没有更多了 --"
+      @load="onLoadMore"
+      :immediate-check="false"
+    >
+      <CarSearchCarListItem
+        v-for="item in listData"
+        :key="item.id"
+        :item-data="item"
+        @on-select="$emit('on-select', item)"
+      >
+      </CarSearchCarListItem>
+    </van-list>
+  </div>
+</template>
+
+<script setup>
+defineEmits(["on-select"]);
+
+const route = useRoute();
+
+const requestQuery = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  model: computed(() => route.query.carModel),
+  startDate: computed(() => route.query.startDate),
+  belongTab: computed(() => route.query.startPlace),
+  days: computed(() => route.query.days),
+  type: computed(() => route.query.type || 1),
+  count: true,
+});
+const listData = ref([]);
+
+const loading = ref(false);
+
+const finished = ref(false);
+
+async function getList() {
+  try {
+    loading.value = true;
+    showLoadingToast({
+      message: "加载中...",
+      duration: 100000,
+    });
+    const { data } = await request("/website/app/tourCarCategory/list", {
+      query: requestQuery,
+    });
+    listData.value = listData.value.concat(data.dataList);
+    loading.value = false;
+    if (data.totalCount <= listData.value.length) {
+      finished.value = true;
+    }
+  } finally {
+    closeToast();
+    loading.value = false;
+  }
+}
+
+function reSearch() {
+  requestQuery.pageNum = 1;
+  finished.value = false;
+  listData.value = [];
+  getList();
+}
+
+function onLoadMore() {
+  requestQuery.pageNum++;
+  getList();
+}
+
+watch(
+  [
+    () => route.query.startPlace,
+    () => route.query.startDate,
+    () => route.query.days,
+    () => route.query.type,
+    () => route.query.carModel,
+  ],
+  () => {
+    reSearch();
+  },
+  { immediate: true }
+);
+</script>
+
+<style lang="scss" scoped></style>

+ 53 - 0
src/components/Car/Search/DriverList/Item.vue

@@ -0,0 +1,53 @@
+<template>
+  <div
+    class="flex pb-10 cursor-pointer space-x-10 bg-white"
+    :class="[active ? 'border-primary shadow-card' : 'border-white']"
+  >
+    <van-image
+      :src="itemData.image"
+      height="127"
+      width="127"
+      radius="15px"
+      fit="cover"
+      class="shrink-0"
+    />
+    <div class="flex w-0 flex-1 flex-col text-base text-black-6">
+      <div class="truncate text-xl font-semibold text-black-3">
+        姓名:{{ itemData.name }}
+      </div>
+      <div class="truncate">性别:{{ itemData.sexDictMap?.name }}</div>
+      <div class="truncate">年龄:{{ itemData.age }}</div>
+      <div class="truncate">驾龄:{{ itemData.drivingYears }}年</div>
+      <div class="flex items-center justify-between">
+        <div>
+          <span class="">评分:</span>
+          <van-rate size="14" disabled v-model="itemData.score" />
+        </div>
+        <van-button
+          @click="$emit('onSelect')"
+          type="primary"
+          color="#FD9A00"
+          size="small"
+          style="width: 70px; font-size: 14px"
+          round
+          >选择</van-button
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({}),
+  },
+  active: {
+    type: Boolean,
+    default: false,
+  },
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 132 - 0
src/components/Car/Search/DriverList/index.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="h-full flex flex-col">
+    <div class="shrink-0 px-15">
+      <div class="flex items-center justify-between">
+        <div class="py-10 txxt-base text-black-3 font-semibold">选择司机</div>
+        <span
+          @click="$emit('close')"
+          class="iconfont icon-close pl-10 py-5"
+        ></span>
+      </div>
+      <van-divider />
+      <van-dropdown-menu
+        style="--van-dropdown-menu-shadow: none"
+        active-color="#fd9a00"
+      >
+        <van-dropdown-item v-model="driverSex" :options="sexOptions" />
+        <van-dropdown-item v-model="driveAge" :options="ageOptions" />
+      </van-dropdown-menu>
+    </div>
+
+    <div class="flex-1 overflow-scroll px-15">
+      <van-empty
+        v-if="!listData.length && !loading"
+        image="search"
+        description="暂无相关司机"
+      />
+      <div v-else-if="listData.length">
+        <CarSearchDriverListItem
+          v-for="item in listData"
+          :key="item.id"
+          :item-data="item"
+          @click="$emit('onSelect', item)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineEmits(["close", "onSelect"]);
+
+const route = useRoute();
+
+const driverSex = ref(null);
+const driveAge = ref(null);
+const sexOptions = [
+  {
+    text: "性别不限",
+    value: null,
+  },
+  {
+    text: "男",
+    value: 1,
+  },
+  {
+    text: "女",
+    value: 2,
+  },
+];
+const ageOptions = [
+  {
+    text: "年龄不限",
+    value: null,
+  },
+  {
+    text: "20-30岁",
+    value: "20,30",
+  },
+  {
+    text: "30-35岁",
+    value: "30,35",
+  },
+  {
+    text: "35-40岁",
+    value: "35,40",
+  },
+  {
+    text: "40-45岁",
+    value: "40,45",
+  },
+  {
+    text: "45岁以上",
+    value: "45",
+  },
+];
+
+const requestQuery = reactive({
+  pageNum: 1,
+  pageSize: 9999,
+  startDate: computed(() => route.query.startDate),
+  belongTab: computed(() => route.query.startPlace),
+  days: computed(() => route.query.days),
+  type: computed(() => route.query.type || 1),
+  // sex: computed(() => route.query.driverSex),
+  // age: computed(() => route.query.driverAge),
+  count: true,
+});
+
+const listData = ref([]);
+const loading = ref(false);
+
+async function getList() {
+  try {
+    loading.value = true;
+    showLoadingToast({
+      message: "加载中...",
+      duration: 100000,
+    });
+    const { data } = await request("/website/app/tourCarDriver/list", {
+      query: {
+        ...requestQuery,
+        sex: driverSex.value,
+        age: driveAge.value,
+      },
+    });
+    listData.value = data.dataList;
+  } finally {
+    loading.value = false;
+    closeToast();
+  }
+}
+
+watch([driverSex, driveAge], () => {
+  getList();
+});
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 56 - 0
src/components/Car/Search/TypeTab.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="flex items-center justify-center space-x-12">
+    <div
+      v-for="item in typeList"
+      :key="item.label"
+      class="box-border flex h-44 flex-1 cursor-pointer items-center justify-center space-x-5 rounded-full border-[1px] border-[#FD9A00] transition-all"
+      :class="[
+        type == item.value
+          ? 'bg-primary text-white'
+          : 'bg-white text-[#FD9A00]',
+      ]"
+      @click="handleClick(item)"
+    >
+      <img :src="item.image" class="h-34 w-58" alt="" srcset="" />
+      <span class="text-base font-semibold">{{ item.label }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import car_from_type_self from "~/assets/img/car/car_from_type_self.png";
+import car_from_type_third from "~/assets/img/car/car_from_type_third.png";
+
+const typeList = [
+  {
+    image: car_from_type_self,
+    value: 1,
+    label: "自营",
+  },
+  {
+    image: car_from_type_third,
+    value: 2,
+    label: "三方",
+  },
+];
+
+const route = useRoute();
+
+const type = computed(() => route.query.type ?? typeList[0].value);
+
+/**
+ * Change the type of car in query string and navigate to the new page.
+ * @param {Object} item - The item which is clicked.
+ * @param {Number} item.value - The value of type.
+ */
+function handleClick(item) {
+  navigateTo({
+    query: {
+      ...route.query,
+      type: item.value,
+    },
+  });
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 34 - 42
src/components/Footer/index.vue

@@ -3,65 +3,54 @@
     <div class="py-15">
       <div class="flex items-center space-x-10">
         <div class="w-3 h-17 bg-primary"></div>
-        <span class="text-black-3 text-xl font-semibold">逍遥游旅游网</span>
+        <span class="text-black-3 text-xl font-semibold"
+          >我们足迹踏过的地区</span
+        >
       </div>
-      <div class="text-sm text-black-6 mt-10 leading-[22px]">
-        全球年轻一代更喜欢用的旅游网站,年轻旅行者共同打造的"旅行神器",多个全球旅游目的地,多篇旅游日记供您参考。
+      <div class="flex flex-wrap gap-10 mt-10">
+        <NuxtLink
+          :to="`/travel-projects?area=${item.parentId}&country=${item.id}`"
+          v-for="item in hotCountryData"
+          class="text-sm text-black-6"
+        >
+          {{ item.menuName }}
+        </NuxtLink>
       </div>
     </div>
     <van-divider />
     <div class="py-15">
       <div class="flex items-center space-x-10">
         <div class="w-3 h-17 bg-primary"></div>
-        <span class="text-black-3 text-xl font-semibold">关于我们</span>
+        <span class="text-black-3 text-xl font-semibold">逍遥游旅游网</span>
       </div>
-      <div class="flex items-center text-sm text-black-6 mt-10 space-x-20">
-        <div>隐私政策</div>
-        <div>用户协议</div>
+      <div
+        class="text-sm text-black-6 flex flex-col items-center mt-10 leading-[26px]"
+      >
+        <div>因为年轻,所以旅行。</div>
+        <div>一个与您共同成长的旅行日记网站。</div>
       </div>
     </div>
     <van-divider />
     <div class="py-15">
       <div class="flex items-center space-x-10">
         <div class="w-3 h-17 bg-primary"></div>
-        <span class="text-black-3 text-xl font-semibold"
-          >我们足迹踏过的地区</span
-        >
+        <span class="text-black-3 text-xl font-semibold">关于我们</span>
       </div>
-      <div class="flex flex-wrap space-x-20 mt-10">
-        <div
-          v-for="item in [
-            '欧洲',
-            '非洲',
-            '亚洲',
-            '澳洲',
-            '中东',
-            '南美洲',
-            '美洲',
-          ]"
-          class="text-sm text-black-6"
-        >
-          {{ item }}
-        </div>
+      <div class="flex items-center text-sm text-black-6 mt-10 space-x-20">
+        <div>隐私政策</div>
+        <div>用户协议</div>
       </div>
     </div>
     <van-divider />
-    <div class="flex items-center py-15 justify-around text-black-6 text-sm">
-      <div class="flex flex-col items-center">
-        <img
-          src="~/assets/img/home/contract_travel_qrcode.png"
-          class="h-70 w-70 object-contain"
-          alt=""
-        />
-        <span class="mt-5">旅游顾问(扫码添加)</span>
-      </div>
-      <div class="flex flex-col items-center">
-        <img
-          src="~/assets/img/home/contract_laowu_qrcode.png"
-          class="h-70 w-70 object-contain"
-          alt=""
-        />
-        <span class="mt-5">劳务顾问(扫码添加)</span>
+    <div class="flex items-center py-15 text-black-6 text-sm">
+      <img
+        src="~/assets/img/home/contract_travel_qrcode.png"
+        class="h-70 w-70 object-contain"
+        alt=""
+      />
+      <div class="flex flex-col items-center ml-40">
+        <span class="text-black-6 text-xl">扫一扫</span>
+        <span class="mt-5 text-sm">旅游顾问(扫码添加)</span>
       </div>
     </div>
     <div class="text-black-6 text-sm">
@@ -76,6 +65,9 @@
   </div>
 </template>
 
-<script setup></script>
+<script setup>
+const { data } = await useMyFetch(`website/basic/directoryList?isHotspot=1`);
+const hotCountryData = computed(() => data.value?.dataList ?? []);
+</script>
 
 <style lang="scss" scoped></style>

+ 4 - 2
src/components/Home/Banner.vue

@@ -6,7 +6,7 @@
   >
     <van-swipe-item v-for="item in bannerList" :key="item.id">
       <img
-        class="object-cover w-full aspect-[1920/697] rounded-xl"
+        class="object-cover w-full aspect-[1053/612] rounded-xl"
         :src="item.imgUrlsAfterConvert[0]"
       />
     </van-swipe-item>
@@ -14,7 +14,9 @@
 </template>
 
 <script setup>
-const { data } = await useMyFetch(`website/basic/bannerList?belongTab=10`);
+const { data } = await useMyFetch(
+  `website/basic/bannerList?belongTab=10&type=1`
+);
 const bannerList = computed(() => data.value?.dataList);
 </script>
 

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

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

+ 4 - 4
src/components/Home/TravelMenu/Item.vue

@@ -1,19 +1,19 @@
 <template>
   <NuxtLink
     :to="itemData.to"
-    class="flex cursor-pointer items-center justify-between rounded-xl border bg-white p-10 px-5 transition-all"
+    class="flex cursor-pointer items-center justify-between rounded-xl border bg-white p-10 px-10 transition-all"
   >
     <div>
       <div class="text-base font-bold text-black-3">
         {{ itemData.title }}
       </div>
-      <div class="text-sm text-black-6">{{ itemData.subTitle }}</div>
+      <!-- <div class="text-sm text-black-6">{{ itemData.subTitle }}</div>
       <div
         class="mt-5 h-2 w-18"
         :style="{ backgroundColor: itemData.color }"
-      ></div>
+      ></div> -->
     </div>
-    <img :src="itemData.icon" class="h-23 w-23 object-contain" alt="" />
+    <img :src="itemData.icon" class="h-20 w-20 object-contain" alt="" />
   </NuxtLink>
 </template>
 

+ 3 - 3
src/components/Home/TravelNotes/Item.vue

@@ -2,14 +2,14 @@
   <NuxtLink
     :to="`/yj/${itemData.id}`"
     target="_blank"
-    class="relative cursor-pointer rounded-xl bg-white p-10"
+    class="relative cursor-pointer rounded-xl bg-white p-8"
   >
     <div
-      class="absolute -top-20 left-20 flex h-30 w-30 items-center justify-center rounded-xl bg-white"
+      class="absolute top-0 left-20 z-10 flex h-25 w-25 items-center justify-center rounded-md bg-[#ffffff80]"
     >
       <img
         src="@/assets/img/home/home_travel_note_leaf.png"
-        class="h-25 w-25 object-cover"
+        class="h-20 w-20 object-cover"
       />
     </div>
     <van-image

+ 1 - 1
src/components/Home/TravelNotes/index.vue

@@ -35,7 +35,7 @@
           </NuxtLink>
         </div>
       </div>
-      <div class="mt-35 grid grid-cols-2 gap-x-10 gap-y-40">
+      <div class="mt-20 grid grid-cols-2 gap-x-10 gap-y-12">
         <HomeTravelNotesItem
           v-for="item in travelNotesList"
           :key="item"

+ 104 - 72
src/components/NavigationBar/LeftMenu.vue

@@ -1,62 +1,80 @@
 <template>
-  <div class="pt-70 px-20 pb-30 flex flex-col h-screen relative">
+  <div class="pt-70 pb-30 flex flex-col h-screen relative">
     <div @click="visible = false" class="absolute right-20 top-50">
       <span class="iconfont icon-close"></span>
     </div>
-    <NuxtLink v-if="!token" to="/login" class="flex items-center space-x-15">
+    <div class="px-20">
+      <NuxtLink v-if="!token" to="/login" class="flex items-center space-x-15">
+        <div
+          class="flex items-center justify-center bg-[#d9d9d9] rounded-full h-60 w-60"
+        >
+          <span
+            class="iconfont icon-profile text-black-6"
+            style="font-size: 30px"
+          ></span>
+        </div>
+        <span class="text-black-6 text-base">登录</span>
+      </NuxtLink>
+
       <div
-        class="flex items-center justify-center bg-[#d9d9d9] rounded-full h-60 w-60"
+        @click="handleToProfile"
+        v-else-if="token"
+        class="flex items-center space-x-15"
       >
-        <span
-          class="iconfont icon-profile text-black-6"
-          style="font-size: 30px"
-        ></span>
-      </div>
-      <span class="text-black-6 text-base">登录</span>
-    </NuxtLink>
-
-    <div
-      @click="handleToProfile"
-      v-else-if="token"
-      class="flex items-center space-x-15"
-    >
-      <van-image
-        :src="userInfo.headImageUrl || defaultAvatar"
-        height="60"
-        width="60"
-        radius="30px"
-        class="shrink-0"
-      ></van-image>
-      <div class="flex-1 flex flex-col">
-        <div class="text-black-3 text-base break-all">
-          {{ userInfo.showName }}
-        </div>
-        <div v-if="userInfo.personalSign" class="text-black-6 text-sm">
-          <span class="text-black-6">个性签名:</span>
-          <span class="text-black-3 break-all">{{
-            userInfo.personalSign
-          }}</span>
+        <van-image
+          :src="userInfo.headImageUrl || defaultAvatar"
+          height="60"
+          width="60"
+          radius="30px"
+          class="shrink-0"
+        ></van-image>
+        <div class="flex-1 flex flex-col">
+          <div class="text-black-3 text-base break-all">
+            {{ userInfo.showName }}
+          </div>
+          <div v-if="userInfo.personalSign" class="text-black-6 text-sm">
+            <span class="text-black-6">个性签名:</span>
+            <span class="text-black-3 break-all">{{
+              userInfo.personalSign
+            }}</span>
+          </div>
         </div>
       </div>
     </div>
 
-    <div class="flex flex-col mt-20 flex-1 overflow-scroll">
-      <div
+    <div class="flex flex-col mt-20 px-20 flex-1 overflow-scroll">
+      <LeftMenuItem
         v-for="item in menuData"
-        :key="item.title"
+        :title="item.title"
+        :icon="item.icon"
         @click="handleClickMenu(item)"
-        class="flex items-center h-50 space-x-5 border-b hover:bg-[#fff8e7]"
-      >
-        <img :src="item.icon" class="w-23 h-23" alt="" srcset="" />
-        <span class="text-base text-black">{{ item.title }}</span>
+      />
+      <div v-if="token">
+        <LeftMenuItem
+          title="我的"
+          :icon="menu_profile"
+          @click="isProfileMenuExpanded = !isProfileMenuExpanded"
+        >
+          <template #right>
+            <span class="iconfont icon-caret-down"></span>
+          </template>
+        </LeftMenuItem>
+        <div v-show="isProfileMenuExpanded">
+          <LeftMenuItem
+            v-for="item in profileMenu"
+            :title="item.title"
+            @click="handleClickMenu(item)"
+          />
+        </div>
       </div>
     </div>
 
     <van-button
       v-if="token"
+      round=""
       @click="handleLogout"
       plain
-      style="width: 100%; margin-top: 20px"
+      style="margin: 20px 20px"
       >退出登录</van-button
     >
   </div>
@@ -74,6 +92,7 @@ import menu_travel_project from "@/assets/img/navbar/menu_travel_project.png";
 import menu_visa from "@/assets/img/navbar/menu_visa.png";
 import menu_profile from "@/assets/img/navbar/menu_profile.png";
 import defaultAvatar from "~/assets/img/default_avatar.png";
+import LeftMenuItem from "./LeftMenuItem.vue";
 
 const visible = defineModel("visible");
 
@@ -93,12 +112,6 @@ const writeNoteMenu = {
   to: "/note-create",
 };
 
-const profileMenu = {
-  title: "我的",
-  icon: menu_profile,
-  to: "/profile",
-};
-
 const fixedMenuData = [
   {
     title: "首页",
@@ -115,33 +128,52 @@ const fixedMenuData = [
     icon: menu_travel_project,
     to: "/travel-projects",
   },
-  // {
-  //   title: "签证居留",
-  //   icon: menu_visa,
-  //   to: "/visa",
-  // },
-  // {
-  //   title: "全球包车",
-  //   icon: menu_car,
-  //   to: "/car",
-  // },
-  // {
-  //   title: "买房卖房",
-  //   icon: menu_house,
-  //   to: "/house",
-  // },
-  // {
-  //   title: "出国劳务",
-  //   icon: menu_labour,
-  //   to: "/labour",
-  // },
-  // {
-  //   title: "门票代订",
-  //   icon: menu_tickets,
-  //   to: "/labour",
-  // },
+  {
+    title: "签证居留",
+    icon: menu_visa,
+    to: "/visa",
+  },
+  {
+    title: "全球包车",
+    icon: menu_car,
+    to: "/car",
+  },
+  {
+    title: "买房卖房",
+    icon: menu_house,
+    to: "/house",
+  },
 ];
 
+const profileMenu = [
+  {
+    title: "旅游订单",
+    to: "/profile/travel-orders",
+  },
+  {
+    title: "包车订单",
+    to: "/profile/car-orders",
+  },
+  {
+    title: "签证订单",
+    to: "/profile/visa-orders",
+  },
+  {
+    title: "我的收藏",
+    to: "/profile/collection",
+  },
+  {
+    title: "我的游记",
+    to: "/profile/notes",
+  },
+  {
+    title: "我的评论",
+    to: "/",
+  },
+];
+
+const isProfileMenuExpanded = ref(false);
+
 watch(
   token,
   (val) => {
@@ -149,7 +181,7 @@ watch(
       menuData.value = fixedMenuData;
     } else {
       userInfoStore.getUserInfo();
-      menuData.value = [...fixedMenuData, writeNoteMenu, profileMenu];
+      menuData.value = [...fixedMenuData, writeNoteMenu];
     }
   },
   {

+ 18 - 0
src/components/NavigationBar/LeftMenuItem.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="h-50 flex items-center border-b shrink-0">
+    <div class="flex items-center justify-center w-23 h-23">
+      <img v-if="icon" :src="icon" class="w-full h-full" />
+    </div>
+    <span class="flex-1 ml-5 text-base text-black">{{ title }}</span>
+    <slot name="right"></slot>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  icon: String,
+  title: String,
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 17 - 18
src/components/NavigationBar/index.client.vue

@@ -9,33 +9,32 @@
     <div
       class="absolute right-15 top-1/2 -translate-y-1/2 flex items-center space-x-20"
     >
-      <template v-if="token">
-        <NuxtLink to="/profile" class="flex items-center text-base space-x-5">
+      <div v-if="token" class="flex items-center space-x-10">
+        <NuxtLink to="/profile" class="flex items-center">
           <van-image
             :src="userInfo.headImageUrl || defaultAvatar"
             round
             height="26"
             width="26"
           />
-          <span class="max-w-70 truncate text-black-6">{{
-            userInfo.showName
-          }}</span>
         </NuxtLink>
-      </template>
+        <NuxtLink to="/profile" class="max-w-70 truncate text-black-6">
+          我的订单
+        </NuxtLink>
+      </div>
 
-      <template v-else-if="!token">
-        <NuxtLink
-          :to="`/login?redirect=${route.fullPath}`"
-          class="flex items-center text-black-6 space-x-5"
+      <NuxtLink
+        v-else-if="!token"
+        :to="`/login?redirect=${route.fullPath}`"
+        class="flex items-center text-black-6 space-x-5"
+      >
+        <div
+          class="w-26 h-26 rounded-full bg-[#d9d9d9] text-black-6 flex items-center justify-center"
         >
-          <div
-            class="w-26 h-26 rounded-full bg-[#d9d9d9] text-black-6 flex items-center justify-center"
-          >
-            <span class="iconfont icon-profile"></span>
-          </div>
-          <span class="text-base">请登录</span>
-        </NuxtLink>
-      </template>
+          <span class="iconfont icon-profile"></span>
+        </div>
+        <span class="text-base">请登录</span>
+      </NuxtLink>
       <img
         @click="handleClickMenu"
         src="~/assets/img/navbar/nav_menu.png"

+ 18 - 11
src/components/Profile/TravelOrders/Item.vue

@@ -5,21 +5,26 @@
   >
     <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
-        >
+        <span class="font-semibold">{{ itemData.projectTitle }}</span>
+        <span class="text-[#FD9A00]">&nbsp;{{ itemData.countTimes }}</span>
       </div>
       <div class="mt-5">
-        日期:{{ $dayjs(itemData.startDate).format("YYYY-MM-DD") }} 至
-        {{ $dayjs(itemData.endDate).format("YYYY-MM-DD") }}
+        日期:{{ $dayjs(itemData.startDate).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 class="flex items-center justify-between">
+        <div class="mt-5">
+          在线付:<span class="text-xl text-[#ff5555] font-semibold"
+            >{{ itemData.totalAmount }}{{ itemData.currency }}</span
+          >
+        </div>
+        <van-button
+          @click.prevent="$emit('onCancel')"
+          type="primary"
+          color="#FD9A00"
+          plain=""
+          size="small"
+          >取消订单</van-button
         >
       </div>
     </div>
@@ -38,6 +43,8 @@ defineProps({
     default: () => {},
   },
 });
+
+defineEmits(["onCancel"]);
 </script>
 
 <style lang="scss" scoped></style>

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

@@ -5,6 +5,7 @@
         v-for="item in orders"
         :key="item.id"
         :itemData="item"
+        @onCancel="$emit('onCancel', item)"
       />
     </div>
   </template>
@@ -20,6 +21,7 @@ defineProps({
     default: () => [],
   },
 });
+defineEmits(["onCancel"]);
 </script>
 
 <style lang="scss" scoped></style>

+ 41 - 2
src/components/TravelProjectDetail/BookInfo.vue

@@ -7,10 +7,10 @@
     >
       <span class="flex items-center space-x-10">
         <span>出发:{{ startDate }}</span>
-        <span class="text-[#FF2222]"
+        <!-- <span class="text-[#FF2222]"
           >{{ calendarData[startDate]?.adultPrice || "???"
           }}{{ detailData.priceUnit }}</span
-        >
+        > -->
       </span>
       <span
         class="iconfont icon-caret-down text-primary"
@@ -42,6 +42,41 @@
         ></span>
       </div>
     </div>
+
+    <div class="mt-15">
+      <div class="pb-10">联系方式</div>
+      <van-form label-width="70">
+        <van-cell-group>
+          <van-field
+            v-model="customerName"
+            label="联系人"
+            maxlength="50"
+            placeholder="请输入联系人姓名"
+          />
+          <van-field
+            v-model="customerMobile"
+            label="联系电话"
+            type="tel"
+            maxlength="20"
+            placeholder="请输入联系电话"
+          />
+          <van-field
+            v-model="customerMobileStandby"
+            label="备用电话"
+            type="tel"
+            maxlength="20"
+            placeholder="请输入备用联系人电话"
+          />
+          <van-field
+            v-model="customerWechat"
+            label="微信"
+            maxlength="50"
+            placeholder="请输入微信"
+          />
+        </van-cell-group>
+      </van-form>
+    </div>
+
     <div class="mt-15">
       <div class="">预定须知</div>
       <div class="mt-10 flex flex-col space-y-3 text-sm text-black-6">
@@ -105,6 +140,10 @@ const props = defineProps({
 const startDate = defineModel("startDate");
 const adultNumber = defineModel("adultNumber");
 const childrenNumber = defineModel("childrenNumber");
+const customerName = defineModel("customerName");
+const customerMobile = defineModel("customerMobile");
+const customerMobileStandby = defineModel("customerMobileStandby");
+const customerWechat = defineModel("customerWechat");
 
 // 选择成人人数
 const showAdultNumberPicker = ref(false);

+ 21 - 0
src/middleware/car-search.global.js

@@ -0,0 +1,21 @@
+export default defineNuxtRouteMiddleware((to, from) => {
+  if (to.path != "/car/search") return;
+
+  const dayjs = useDayjs();
+
+  const startDate = to.query.startDate;
+  const startPlace = to.query.startPlace;
+  const days = to.query.days;
+
+  if (startDate && startPlace && days) {
+    return;
+  }
+  return navigateTo({
+    path: "/car/search",
+    query: {
+      startDate: startDate || dayjs().format("YYYY-MM-DD"),
+      startPlace: startPlace || 99,
+      days: days || 1,
+    },
+  });
+});

+ 14 - 0
src/pages/car/index.client.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="pb-50">
+    <CarHomeBanner />
+    <div class="px-15 -translate-y-15">
+      <CarHomeBaseFilter />
+      <CarHomeIntroSection1 />
+      <CarHomeIntroSection2 class="mt-15" />
+    </div>
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped></style>

+ 75 - 0
src/pages/car/search.client.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="px-15 pt-15">
+    <CarSearchBaseFilter />
+    <CarSearchTypeTab class="mt-10" />
+    <div class="py-10 text-base text-black-3 font-semibold">选择车型</div>
+    <van-divider />
+    <CarSearchCarFilter />
+    <CarSearchCarList @on-select="handleSelectCar" />
+    <van-popup
+      v-model:show="selectDriverOption.show"
+      position="bottom"
+      :style="{ height: '80%' }"
+      ><CarSearchDriverList
+        v-if="selectDriverOption.show"
+        @on-select="handleSelectDriver"
+        @close="selectDriverOption.show = false"
+    /></van-popup>
+  </div>
+</template>
+
+<script setup>
+const route = useRoute();
+
+const selectedCar = ref({});
+const selectedDriver = ref({});
+
+const selectDriverOption = reactive({
+  show: false,
+});
+
+async function handleSelectCar(car) {
+  selectedCar.value = car;
+  if (route.query.type == "2") {
+    // 三方
+    // 获取司机信息
+    showLoadingToast({
+      message: "加载中...",
+      duration: 100000,
+    });
+    try {
+      const { data } = await request("/website/app/tourCarDriver/list", {
+        query: {
+          id: car.relationDriverId,
+        },
+      });
+      selectedDriver.value = data.dataList.length ? data.dataList[0] : {};
+      toBookOrder();
+      closeToast();
+    } catch (error) {
+      closeToast();
+    }
+  } else {
+    // 自营
+    selectDriverOption.show = true;
+  }
+}
+
+function handleSelectDriver(driver) {
+  selectedDriver.value = driver;
+  toBookOrder();
+}
+
+function toBookOrder() {
+  navigateTo({
+    path: "/car/submit-order",
+    query: {
+      ...route.query,
+      carInfo: JSON.stringify(selectedCar.value),
+      driverInfo: JSON.stringify(selectedDriver.value),
+    },
+  });
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 203 - 0
src/pages/car/submit-order.client.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="bg-[#f8f8f8] min-h-screen pb-60 px-15 pt-15 text-black-3">
+    <div class="flex space-x-15 rounded-xl bg-white p-10">
+      <img
+        :src="carInfo.image"
+        class="h-110 w-110 shrink-0 rounded-xl object-cover"
+      />
+      <div class="flex-1 flex flex-col space-y-5">
+        <div class="flex items-center space-x-20 text-xl">
+          <span class="font-bold">{{ carInfo.name }}</span>
+          <span class="text-xl text-black-6">
+            {{ carInfo.seatNumber }}座.{{ carInfo.luggageNumber }}行李</span
+          >
+        </div>
+        <div class="text-sm">出发时间:{{ startTime }}</div>
+        <div class="text-sm">用车天数:{{ days }}天</div>
+        <div class="text-sm">起点:{{ driverInfo.belongTabDictMap.name }}</div>
+        <div class="text-sm">行程中可根据您的需求随时停</div>
+      </div>
+    </div>
+
+    <div class="mt-15 flex space-x-15 rounded-xl bg-white p-10">
+      <img
+        :src="driverInfo.image"
+        class="h-110 w-110 shrink-0 rounded-xl object-cover"
+      />
+      <div class="flex-1 text-sm flex flex-col space-y-5">
+        <div class="flex items-center space-x-20 text-xl font-semibold">
+          {{ driverInfo.name }}
+        </div>
+        <div>性别:{{ driverInfo.sexDictMap.name }}</div>
+        <div>年龄:{{ driverInfo.age }}</div>
+        <div>驾龄:{{ driverInfo.drivingYears }}年</div>
+        <div class="flex items-center space-x-10 text-sm text-black-6">
+          <span>好评度</span>
+          <van-rate size="14" disabled v-model="driverInfo.score" />
+        </div>
+      </div>
+    </div>
+
+    <div class="mt-15 rounded-xl bg-white p-10">
+      <div>
+        <span class="text-2xl font-semibold text-black-3">乘车信息</span>
+        <span class="mt-5 text-sm text-black-6"
+          >(司机将提前联系顾客确定行程)</span
+        >
+      </div>
+      <van-form label-width="50" class="mt-10">
+        <van-field
+          v-model="formData.connectPhone"
+          name="手机号"
+          type="tel"
+          label="手机号"
+          maxlength="20"
+          placeholder="手机号"
+          :rules="[{ required: true, message: '请填写手机号' }]"
+        />
+        <van-field
+          v-model="formData.connectName"
+          name="姓名"
+          label="姓名"
+          maxlength="50"
+          placeholder="姓名"
+          :rules="[{ required: true, message: '请填写姓名' }]"
+        />
+        <van-field
+          v-model="formData.connectWechat"
+          name="微信"
+          label="微信"
+          maxlength="50"
+          placeholder="微信"
+          :rules="[]"
+        />
+        <van-field name="radio" label="性别">
+          <template #input>
+            <van-radio-group
+              v-model="formData.connectSex"
+              direction="horizontal"
+            >
+              <van-radio name="1">先生</van-radio>
+              <van-radio name="2">女士</van-radio>
+            </van-radio-group>
+          </template>
+        </van-field>
+        <van-field
+          v-model="formData.remark"
+          rows="3"
+          label="备注"
+          type="textarea"
+          maxlength="200"
+          show-word-limit
+          placeholder="选填,请将你的特殊需求告诉我们"
+        />
+      </van-form>
+    </div>
+    <div
+      class="fixed bottom-0 left-0 right-0 shadow-[0px_3px_12px_1px_rgba(0,0,0,0.11);] bg-white h-60 text-base font-semibold flex items-center px-30"
+    >
+      <span class="">订单金额:</span>
+      <span class="text-[#FF1D1D]">¥{{ orderPrice }}</span>
+      <van-button
+        class="flex-1"
+        style="font-size: 16px; margin-left: 50px"
+        type="primary"
+        color="#FD9A00"
+        @click="handleSubmit"
+        >提交</van-button
+      >
+    </div>
+  </div>
+</template>
+
+<script setup>
+const route = useRoute();
+
+const useUserInfo = useUserInfoStore();
+const { userInfo } = storeToRefs(useUserInfo);
+
+const startTime = computed(() => route.query.startDate ?? "");
+
+const days = computed(() => route.query.days ?? 0);
+
+const startPlace = computed(() => route.query.startPlace ?? 0);
+
+const carInfo = computed(() => {
+  try {
+    return JSON.parse(route.query.carInfo);
+  } catch (error) {
+    return {};
+  }
+});
+
+const driverInfo = computed(() => {
+  try {
+    return JSON.parse(route.query.driverInfo);
+  } catch (error) {
+    return {};
+  }
+});
+
+const formData = reactive({
+  categoryId: carInfo.value.id,
+  driverId: driverInfo.value.id,
+  startTime: startTime.value,
+  days: days.value,
+  startPlaceId: startPlace.value,
+  connectPhone: null,
+  connectName: null,
+  connectSex: null,
+  connectWechat: null,
+  remark: null,
+});
+
+const orderPrice = computed(() => {
+  return (days.value || 1) * (carInfo.value.price || 0);
+});
+
+watchEffect(() => {
+  formData.connectPhone = userInfo.value.mobile;
+});
+
+function onSubmit(val) {
+  console.log(val);
+}
+
+/**
+ * 提交订单
+ * @async
+ * @returns {Promise<void>}
+ * @example
+ * handleSubmit()
+ */
+async function handleSubmit() {
+  if (!formData.connectPhone) {
+    showToast("请填写手机号");
+    return;
+  }
+  if (!formData.connectName) {
+    showToast("请填写姓名");
+    return;
+  }
+  try {
+    await request("/website/app/tourCarOrder/add", {
+      method: "post",
+      body: formData,
+    });
+    showConfirmDialog({
+      title: "温馨提示",
+      message: "提交成功,您可在个人中心-包车订单中查询进度",
+      confirmButtonText: "去查看",
+    })
+      .then(() => {
+        navigateTo({
+          path: "/profile/car-orders",
+          replace: true,
+        });
+      })
+      .catch(() => {});
+  } catch (error) {}
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 1 - 1
src/pages/index.vue

@@ -3,7 +3,7 @@
     <div class="px-15 bg-white pb-15">
       <HomeBanner />
       <HomeTravelMenu class="mt-10" />
-      <!-- <HomeMenu class="mt-10" /> -->
+      <HomeMenu class="mt-10" />
     </div>
     <HomeTravelNotes />
     <div class="bg-white mt-20 px-15 pt-15 pb-30">

+ 173 - 0
src/pages/profile/car-order/[id].client.vue

@@ -0,0 +1,173 @@
+<template>
+  <div
+    v-if="orderInfo"
+    class="text-base text-black-3 min-h-screen pb-50 pt-15 px-15 bg-[#F8F8F8]"
+  >
+    <div class="pr-50 bg-white rounded-xl p-10">
+      <div class="flex space-x-15">
+        <img
+          :src="orderInfo.carImage"
+          class="h-110 w-110 shrink-0 rounded-xl object-cover"
+        />
+        <div class="flex-1">
+          <div class="text-xl">
+            <span class="font-bold">{{ orderInfo.carName }}</span>
+            <span class="text-black-6">
+              &nbsp;&nbsp;{{ orderInfo.carSeatNumber }}座.{{
+                orderInfo.carLuggageNumber
+              }}行李</span
+            >
+          </div>
+          <div class="mt-5">
+            起点:{{ orderInfo.startPlaceIdDictMap?.name }}
+          </div>
+          <div class="mt-5">行程中可根据您的需求随时停</div>
+        </div>
+      </div>
+      <div
+        v-if="isScored"
+        class="flex text-primary mt-10 font-semibold shrink-0 items-center space-x-10"
+      >
+        <span>已打分</span>
+        <van-rate
+          disabled
+          v-model="orderInfo.categoryMark"
+          size="large"
+          disabled-color="#FD9A00"
+        />
+      </div>
+    </div>
+
+    <div class="p-10 bg-white mt-15 rounded-xl">
+      <div class="flex space-x-15">
+        <img
+          :src="orderInfo.driverImage"
+          class="h-110 w-110 shrink-0 rounded-xl object-cover"
+        />
+        <div class="flex-1 flex flex-col space-y-5">
+          <div class="flex items-center space-x-20 text-xl font-semibold">
+            {{ orderInfo.driverName }}
+          </div>
+          <div class="">性别:{{ orderInfo.driverSexDictMap?.name }}</div>
+          <div class="">年龄:{{ orderInfo.driverAge }}</div>
+          <div class="">驾龄:{{ orderInfo.drivingYears }}年</div>
+        </div>
+      </div>
+      <div
+        v-if="isScored"
+        class="flex text-primary mt-10 font-semibold shrink-0 items-center space-x-10"
+      >
+        <span>已打分</span>
+        <van-rate
+          disabled
+          v-model="orderInfo.driveMark"
+          size="large"
+          disabled-color="#FD9A00"
+        />
+      </div>
+    </div>
+
+    <div class="p-10 bg-white mt-15 rounded-xl">
+      <div class="text-xl font-bold">订单信息</div>
+      <div class="mt-15 flex flex-col space-y-5">
+        <div>
+          <span class="label">订单号:</span>
+          <span>{{ orderInfo.orderNo }}</span>
+        </div>
+        <div>
+          <span class="label">下单时间:</span>
+          <span>{{ $dayjs(orderInfo.createTime).format("YYYY-MM-DD") }}</span>
+        </div>
+        <div>
+          <span class="label">订单金额:</span>
+          <span class="text-[#FF1717]"
+            >{{ orderInfo.totalPrice }}{{ orderInfo.totalPriceUnit }}</span
+          >
+        </div>
+        <div>
+          <span class="label">订单状态:</span>
+          <span>{{ orderInfo.stateDictMap?.name }}</span>
+        </div>
+        <div>
+          <span class="label">起点:</span>
+          <span>{{ orderInfo.startPlaceIdDictMap?.name }}</span>
+        </div>
+        <div>
+          <span class="label">开始时间:</span>
+          <span>{{ $dayjs(orderInfo.startTime).format("YYYY-MM-DD") }}</span>
+        </div>
+        <div>
+          <span class="label">结束时间:</span>
+          <span>{{ $dayjs(orderInfo.endTime).format("YYYY-MM-DD") }}</span>
+        </div>
+        <div>
+          <span class="label">包车天数:</span>
+          <span>{{ orderInfo.days }}天</span>
+        </div>
+        <div>
+          <span class="label">包车类型:</span>
+          <span>{{ orderInfo.categoryTypeDictMap?.name }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="p-10 bg-white mt-15 rounded-xl">
+      <div class="text-xl font-bold">乘车人信息</div>
+      <div class="mt-15 flex flex-col space-y-5">
+        <div>
+          <span class="label">包车人:</span>
+          <span>{{ orderInfo.connectName }}</span>
+        </div>
+        <div>
+          <span class="label">电话:</span>
+          <span>{{ orderInfo.connectPhone }}</span>
+        </div>
+        <div>
+          <span class="label">微信:</span>
+          <span>{{ orderInfo.connectWechat }}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="p-10 bg-white mt-15 rounded-xl">
+      <div class="text-xl font-bold">备注</div>
+      <div class="mt-15">
+        {{ orderInfo.remark || "暂无" }}
+      </div>
+    </div>
+  </div>
+  <div v-else></div>
+</template>
+
+<script setup>
+const id = useRouteParam("id");
+
+const orderInfo = ref();
+
+onMounted(() => {
+  getDetail();
+});
+
+/**
+ * Get the detail of the current car order
+ *
+ * @returns {Promise<void>} resolves when the detail is fetched
+ */
+async function getDetail() {
+  const { data } = await request("/website/app/tourCarOrder/view", {
+    query: {
+      id: id.value,
+    },
+  });
+  orderInfo.value = data;
+}
+
+const isScored = computed(() => {
+  return orderInfo.value.categoryMark && orderInfo.value.driveMark;
+});
+</script>
+
+<style lang="scss" scoped>
+.label {
+  @apply font-bold;
+}
+</style>

+ 187 - 0
src/pages/profile/car-orders.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="flex flex-col bg-[#f8f8f8] px-15 pt-15 space-y-12">
+    <NuxtLink
+      v-for="item in orders"
+      :key="item.id"
+      :to="`/profile/car-order/${item.id}`"
+      class="relative cursor-pointer bg-white text-sm text-black-3 rounded-xl overflow-hidden"
+    >
+      <div class="p-15">
+        <div class="flex items-center">
+          <span class="font-semibold">订单号:</span>
+          <span>{{ item.orderNo }}</span>
+          <div
+            class="flex h-20 ml-10 items-center justify-center rounded-full border border-current px-5 text-sm text-primary"
+          >
+            {{ formatStatus(item.state) }}
+          </div>
+        </div>
+        <div class="mt-5 flex items-center space-x-15 text-xl">
+          <span class="font-bold text-black-3">{{ item.carName }}</span>
+          <span
+            >{{ item.carSeatNumber }}座.{{ item.carLuggageNumber }}行李</span
+          >
+        </div>
+        <div class="mt-5 flex items-center space-x-20">
+          <span
+            >出发时间:{{ $dayjs(item.startTime).format("YYYY-MM-DD") }}</span
+          >
+          <span>用车天数:{{ item.days }}天</span>
+        </div>
+        <div class="mt-10 flex items-center text-sm justify-between">
+          <div class="flex items-center space-x-10">
+            <div
+              class="flex h-20 items-center justify-center rounded-full bg-[#DDDDDD] px-10"
+            >
+              起点
+            </div>
+            <span>{{ item.startPlaceIdDictMap?.name ?? "" }}</span>
+            <span>{{ item.categoryTypeDictMap?.name ?? "" }}</span>
+          </div>
+          <div>
+            <span class="text-[#FF1717] font-semibold"
+              >{{ item.totalPrice }}{{ item.totalPriceUnit }}</span
+            >
+          </div>
+        </div>
+      </div>
+      <div
+        v-if="!item.driveMark && !item.categoryMark"
+        @click.prevent="handleScore(item)"
+        class="bg-primary flex items-center pl-15 py-5 space-x-5"
+      >
+        <div
+          class="flex w-60 h-26 text-base font-semibold items-center rounded-full justify-center text-white bg-[#ffb440]"
+        >
+          去打分
+        </div>
+        <span class="iconfont icon-arrow_right text-white"></span>
+      </div>
+    </NuxtLink>
+    <van-popup
+      v-model:show="scoreDialogOptions.show"
+      round
+      closeable
+      :style="{ height: '260px', width: '80%', padding: '20px' }"
+    >
+      <div>
+        <div class="text-xl font-semibold text-black-3">请您为该订单打分</div>
+        <van-form label-width="50" class="mt-20">
+          <van-field name="rate" label="车型">
+            <template #input>
+              <van-rate v-model="scoreDialogOptions.categoryMark" />
+            </template>
+          </van-field>
+          <van-field name="rate" label="司机">
+            <template #input>
+              <van-rate v-model="scoreDialogOptions.driveMark" />
+            </template>
+          </van-field>
+          <van-button
+            type="primary"
+            round=""
+            color="#FD9A00"
+            @click="handleScoreConfirm"
+            style="width: 100%; margin-top: 20px"
+            >提交</van-button
+          >
+        </van-form>
+      </div>
+    </van-popup>
+  </div>
+</template>
+
+<script setup>
+onMounted(() => {
+  getList();
+});
+
+const orders = ref([]);
+
+async function getList() {
+  const { data } = await request("/website/app/tourCarOrder/list", {
+    query: {
+      pageNum: 1,
+      pageSize: 1000,
+    },
+  });
+  orders.value = data.dataList;
+}
+
+const orderStatusEnum = [
+  {
+    value: 1,
+    label: "已下单",
+  },
+  {
+    value: 2,
+    label: "已取消",
+  },
+  {
+    value: 3,
+    label: "已完成",
+  },
+];
+
+/**
+ * Returns the label corresponding to a given order status value.
+ *
+ * @param {number} status - The status value of the order.
+ * @returns {string} The label associated with the given status.
+ */
+function formatStatus(status) {
+  return orderStatusEnum.find((item) => item.value == status).label;
+}
+
+const scoreDialogOptions = reactive({
+  show: false,
+  id: null,
+  categoryMark: 0,
+  driveMark: 0,
+});
+
+/**
+ * Handles the event of a score being clicked.
+ *
+ * @param {Object} item - The order item associated with the score that was clicked.
+ */
+function handleScore(item) {
+  scoreDialogOptions.show = true;
+  scoreDialogOptions.id = item.id;
+}
+
+/**
+ * Handles the confirmation of a score dialog.
+ *
+ * When the user clicks the confirm button on the score dialog, this function is called.
+ * It is responsible for sending the score data to the server and updating the state of the component.
+ */
+async function handleScoreConfirm() {
+  if (!scoreDialogOptions.categoryMark) {
+    showToast("请给车型打分~");
+    return;
+  }
+  if (!scoreDialogOptions.driveMark) {
+    showToast("请给司机打分~");
+    return;
+  }
+  try {
+    await request("/website/app/tourCarOrder/markOrder", {
+      method: "post",
+      body: {
+        id: scoreDialogOptions.id,
+        categoryMark: scoreDialogOptions.categoryMark,
+        driveMark: scoreDialogOptions.driveMark,
+      },
+    });
+    showToast("打分成功");
+    scoreDialogOptions.show = false;
+    scoreDialogOptions.id = null;
+    scoreDialogOptions.categoryMark = 0;
+    scoreDialogOptions.driveMark = 0;
+    getList();
+  } catch (error) {}
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 33 - 28
src/pages/profile/index.vue

@@ -18,13 +18,13 @@
           </span>
         </NuxtLink>
         <div class="text-xl break-all font-semibold mt-15">
-          个性签名:{{ userInfo.personalSign || '暂未填写' }}
+          个性签名:{{ 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">
+    <div class="bg-white rounded-xl mx-20 mt-20 py-20">
+      <div class="text-xl font-semibold ml-20">常用功能</div>
+      <div class="grid grid-cols-4 gap-y-20 mt-20">
         <NuxtLink
           :to="item.to"
           class="flex flex-col items-center"
@@ -40,49 +40,54 @@
 </template>
 
 <script setup>
-import defaultAvatar from '~/assets/img/default_avatar.png'
-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'
-import profile_my_comment from '~/assets/img/profile/profile_my_comment.png'
+import defaultAvatar from "~/assets/img/default_avatar.png";
+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";
+import profile_my_comment from "~/assets/img/profile/profile_my_comment.png";
 
-const userInfoStore = useUserInfoStore()
-const { userInfo } = storeToRefs(userInfoStore)
+const userInfoStore = useUserInfoStore();
+const { userInfo } = storeToRefs(userInfoStore);
 
 onMounted(() => {
-  userInfoStore.getUserInfo()
-})
+  userInfoStore.getUserInfo();
+});
 
 const menuData = [
   {
     icon: profile_travel_order,
-    label: '旅游订单',
-    to: '/profile/travel-orders'
+    label: "旅游订单",
+    to: "/profile/travel-orders",
   },
   {
     icon: profile_travel_note,
-    label: '我的游记',
-    to: '/profile/notes'
+    label: "我的游记",
+    to: "/profile/notes",
   },
   {
     icon: profile_colection,
-    label: '我的收藏',
-    to: '/profile/collection'
+    label: "我的收藏",
+    to: "/profile/collection",
   },
   {
     icon: profile_visa_order,
-    label: '签证订单',
-    to: '/profile/visa-orders'
+    label: "签证订单",
+    to: "/profile/visa-orders",
+  },
+  {
+    icon: profile_car_order,
+    label: "包车订单",
+    to: "/profile/car-orders",
   },
   {
     icon: profile_my_comment,
-    label: '我的评论',
-    to: '/profile/my-comment'
-  }
-]
+    label: "我的评论",
+    to: "/profile/my-comment",
+  },
+];
 </script>
 
 <style lang="scss" scoped></style>

+ 29 - 8
src/pages/profile/travel-order/[id].vue

@@ -9,7 +9,7 @@
           class="h-30 w-30 object-contain"
         />
         <span class="text-3xl font-semibold text-[#ff3535]">{{
-          orderDetailInfo.orderStatus === 0 ? "未完成" : "已完成"
+          fomartStatus(orderDetailInfo.orderStatus)
         }}</span>
       </div>
       <div class="mt-10">订单号:{{ orderDetailInfo.orderNo }}</div>
@@ -27,17 +27,15 @@
           orderDetailInfo?.tourismProjectVo?.projectTitle || ""
         }}</span>
       </div>
-      <div>
-        预定日期:{{ $dayjs(orderDetailInfo.createTime).format("YYYY-MM-DD") }}
-      </div>
+      <div>预定时间:{{ orderDetailInfo.createTime }}</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="mt-15 rounded-xl bg-white p-20">
       <div class="flex items-center">
         <img
           src="~/assets/img/order/order_user.webp"
@@ -47,8 +45,12 @@
         />
         <span class="ml-5 text-xl font-semibold text-black-3">顾客信息</span>
       </div>
-      <div>顾客姓名:{{ orderDetailInfo.customerName }}</div>
-      <div>联系方式:{{ orderDetailInfo.customerMobile }}</div>
+      <div class="flex flex-col space-y-5 mt-15">
+        <div>顾客姓名:{{ orderDetailInfo.customerName }}</div>
+        <div>联系电话:{{ orderDetailInfo.customerMobile }}</div>
+        <div>备用电话:{{ orderDetailInfo.customerMobileStandby }}</div>
+        <div>微信:{{ orderDetailInfo.customerWechat }}</div>
+      </div>
     </div>
 
     <div
@@ -80,6 +82,25 @@ const loading = ref(false);
 
 const orderDetailInfo = ref({});
 
+const orderStatusEnum = [
+  {
+    value: "0",
+    label: "未确认",
+  },
+  {
+    value: "1",
+    label: "未完成",
+  },
+  {
+    value: "2",
+    label: "已完成",
+  },
+];
+
+function fomartStatus(status) {
+  return orderStatusEnum.find((item) => item.value == status)?.label;
+}
+
 async function getOrderDetail() {
   loading.value = true;
   const { data } = await request(

+ 46 - 8
src/pages/profile/travel-orders.vue

@@ -1,14 +1,13 @@
 <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
+        v-for="item in orderStatusEnum"
+        :key="item.id"
+        :title="item.label"
+        :name="item.value"
+      >
+        <ProfileTravelOrders :orders="orderList" @onCancel="handleCancel" />
       </van-tab>
     </van-tabs>
   </div>
@@ -38,6 +37,45 @@ async function getOrderList() {
     loading.value = false;
   }
 }
+
+const orderStatusEnum = [
+  {
+    value: "",
+    label: "全部",
+  },
+  {
+    value: "0",
+    label: "未确认",
+  },
+  {
+    value: "1",
+    label: "未完成",
+  },
+  {
+    value: "2",
+    label: "已完成",
+  },
+];
+
+function handleCancel(item) {
+  console.log(item);
+  showConfirmDialog({
+    title: "温馨提示",
+    message: "取消取消该订单吗?",
+  })
+    .then(async () => {
+      try {
+        await request("/website/tourism/myOrder/delete", {
+          method: "POST",
+          body: {
+            id: item.id,
+          },
+        });
+        getOrderList();
+      } catch (error) {}
+    })
+    .catch(() => {});
+}
 </script>
 
 <style lang="scss" scoped></style>

+ 18 - 1
src/pages/t/[id].client.vue

@@ -10,6 +10,10 @@
         v-model:startDate="bookInfo.startDate"
         v-model:adultNumber="bookInfo.adultNumber"
         v-model:childrenNumber="bookInfo.childrenNumber"
+        v-model:customerName="bookInfo.customerName"
+        v-model:customerMobile="bookInfo.customerMobile"
+        v-model:customerMobileStandby="bookInfo.customerMobileStandby"
+        v-model:customerWechat="bookInfo.customerWechat"
         :calendar-data="calendarData"
         :detail-data="detailData"
       />
@@ -37,6 +41,10 @@ const bookInfo = reactive({
   startDate: null,
   adultNumber: 1,
   childrenNumber: 0,
+  customerName: "",
+  customerMobile: "",
+  customerMobileStandby: "",
+  customerWechat: "",
 });
 
 const { data: detailData, status } = await useMyFetch(
@@ -100,8 +108,17 @@ async function handleSubmit() {
 
 const submitLoading = ref(false);
 function handleSubmitInfo() {
+  if (!bookInfo.customerName) {
+    showToast("请输入联系人姓名");
+    return;
+  }
+  if (!bookInfo.customerMobile) {
+    showToast("请输入手机号");
+    return;
+  }
+
   submitLoading.value = true;
-  request("website/tourism/project/bookProject", {
+  request("/website/tourism/myOrder/add", {
     method: "post",
     body: {
       tourBookInfoDto: {

+ 19 - 2
src/pages/travel-projects/index.client.vue

@@ -40,6 +40,8 @@
 </template>
 
 <script setup>
+const route = useRoute();
+
 const currentArea = ref({});
 const currentCountry = ref({});
 const currentAreaFilterLabel = computed(() => {
@@ -123,11 +125,26 @@ async function getFilterAddress() {
   });
   data.unshift({ text: "全部", id: "" });
   areaOptions.value = data;
+  // 根据路由参数设置默认选中
+  // 查找url传入的洲
+  const areaIndex = data.findIndex((item) => item.id == route.query.area);
+  if (areaIndex > -1) {
+    currentArea.value = data[areaIndex];
+    activeAreaIndex.value = areaIndex;
+    // 查找url传入的国家
+    const countryIndex = currentArea.value.children.findIndex(
+      (item) => item.id == route.query.country
+    );
+    if (countryIndex > -1) {
+      currentCountry.value = currentArea.value.children[countryIndex];
+      activeCountryId.value = currentCountry.value.id;
+    }
+  }
 }
 
-onMounted(() => {
+onMounted(async () => {
+  await getFilterAddress();
   getList();
-  getFilterAddress();
 });
 </script>
 

+ 18 - 10
src/stores/useAuth.js

@@ -1,19 +1,27 @@
-import { skipHydrate } from 'pinia'
-import { useStorage } from '@vueuse/core'
+import { skipHydrate } from "pinia";
+import { useStorage } from "@vueuse/core";
 
-export const useAuthStore = defineStore('auth', () => {
-  const token = useStorage('token', null)
+export const useAuthStore = defineStore("auth", () => {
+  const token = useStorage("token", null);
 
-  const hasToken = computed(() => Boolean(token.value))
+  const hasToken = computed(() => Boolean(token.value));
 
-  const setToken = (val) => (token.value = val)
+  const setToken = (val) => {
+    token.value = val;
+  };
 
-  const cleanToken = () => (token.value = null)
+  const cleanToken = () => {
+    token.value = null;
+  };
+
+  // watch(token, (val) => {
+  //   console.log("token", val);
+  // });
 
   return {
     token: skipHydrate(token),
     hasToken,
     setToken,
-    cleanToken
-  }
-})
+    cleanToken,
+  };
+});

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません