Prechádzať zdrojové kódy

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

qiao 2 mesiacov pred
rodič
commit
6086ca1443
87 zmenil súbory, kde vykonal 2875 pridanie a 208 odobranie
  1. 2 2
      .env.development
  2. 6 4
      nuxt.config.ts
  3. 27 4
      src/assets/iconfont/demo_index.html
  4. 8 4
      src/assets/iconfont/iconfont.css
  5. 0 0
      src/assets/iconfont/iconfont.js
  6. 7 0
      src/assets/iconfont/iconfont.json
  7. 2 0
      src/assets/iconfont/iconfont.svg
  8. BIN
      src/assets/iconfont/iconfont.ttf
  9. BIN
      src/assets/iconfont/iconfont.woff
  10. BIN
      src/assets/iconfont/iconfont.woff2
  11. BIN
      src/assets/img/home/contract_laowu_qrcode.png
  12. BIN
      src/assets/img/home/contract_travel_qrcode.png
  13. BIN
      src/assets/img/home/home_menu_aozhouyou.png
  14. BIN
      src/assets/img/home/home_menu_car.png
  15. BIN
      src/assets/img/home/home_menu_feizhouyou.png
  16. BIN
      src/assets/img/home/home_menu_house.png
  17. BIN
      src/assets/img/home/home_menu_meizhouyou.png
  18. BIN
      src/assets/img/home/home_menu_ouzhouyou.png
  19. BIN
      src/assets/img/home/home_menu_visa.png
  20. BIN
      src/assets/img/home/home_menu_yazhouyou.png
  21. BIN
      src/assets/img/home/home_menu_zhongdongyou.png
  22. BIN
      src/assets/img/home/home_plane.png
  23. BIN
      src/assets/img/home/home_travel_note_leaf.png
  24. BIN
      src/assets/img/home/original_label.png
  25. BIN
      src/assets/img/navbar/menu_car.png
  26. BIN
      src/assets/img/navbar/menu_create_note.png
  27. BIN
      src/assets/img/navbar/menu_home.png
  28. BIN
      src/assets/img/navbar/menu_house.png
  29. BIN
      src/assets/img/navbar/menu_loubar.png
  30. BIN
      src/assets/img/navbar/menu_tickets.png
  31. BIN
      src/assets/img/navbar/menu_travel_note.png
  32. BIN
      src/assets/img/navbar/menu_travel_project.png
  33. BIN
      src/assets/img/navbar/menu_visa.png
  34. BIN
      src/assets/img/note-create/calendar.png
  35. BIN
      src/assets/img/note-create/icon-image-fill.png
  36. BIN
      src/assets/img/note-create/note_create_bg.png
  37. BIN
      src/assets/img/note-create/note_create_entry.png
  38. BIN
      src/assets/img/note-create/note_create_entry3.png
  39. 8 0
      src/assets/img/note-create/upload.svg
  40. 27 0
      src/components/CreateNote/BottomActions.vue
  41. 213 0
      src/components/CreateNote/Form.vue
  42. 132 0
      src/components/CreateNote/HeaderBanner.vue
  43. 55 0
      src/components/CreateNote/InsertContentSection.vue
  44. 70 0
      src/components/CreateNote/InsertImageModal.vue
  45. 37 0
      src/components/CreateNote/InsertImageSection.vue
  46. 60 0
      src/components/CreateNote/InsertTitleModal.client.vue
  47. 38 0
      src/components/CreateNote/InsertTitleSection.vue
  48. 58 0
      src/components/CreateNote/LeftActions.vue
  49. 153 0
      src/components/CreateNote/PreviewModal.vue
  50. 57 0
      src/components/CreateNote/PublishResultModal.client.vue
  51. 136 0
      src/components/CreateNote/UserInfoModal.client.vue
  52. 81 0
      src/components/Footer/index.vue
  53. 3 7
      src/components/Home/Banner.vue
  54. 37 0
      src/components/Home/HotCountry/index.vue
  55. 0 28
      src/components/Home/HotDestination.vue
  56. 0 34
      src/components/Home/HotTravelProject.vue
  57. 31 0
      src/components/Home/HotTravelProjects/Item.vue
  58. 44 0
      src/components/Home/HotTravelProjects/index.vue
  59. 39 0
      src/components/Home/Menu/index.vue
  60. 31 0
      src/components/Home/TravelMenu/Item.vue
  61. 65 0
      src/components/Home/TravelMenu/index.vue
  62. 41 0
      src/components/Home/TravelNotes/Item.vue
  63. 54 0
      src/components/Home/TravelNotes/index.vue
  64. 154 0
      src/components/Navbar/LeftMenu.vue
  65. 22 17
      src/components/Navbar/index.vue
  66. 51 26
      src/components/Profile/Notes/Auditing/Item.vue
  67. 1 1
      src/components/Profile/Notes/Auditing/index.vue
  68. 65 0
      src/components/Profile/Notes/Draft/Item.vue
  69. 67 0
      src/components/Profile/Notes/Draft/index.vue
  70. 1 1
      src/components/Profile/Notes/Published/Item.vue
  71. 74 0
      src/components/Profile/Notes/Rejected/Item.vue
  72. 61 0
      src/components/Profile/Notes/Rejected/index.vue
  73. 0 60
      src/components/Travel/ProjectItem/index.vue
  74. 43 0
      src/components/TravelProject/Item.vue
  75. 18 0
      src/components/TravelProjectDetail/Banner.vue
  76. 41 0
      src/components/TravelProjectDetail/BaseInfo.vue
  77. 97 0
      src/components/TravelProjectDetail/BookInfo.vue
  78. 14 0
      src/layouts/default.vue
  79. 12 6
      src/pages/index.vue
  80. 4 1
      src/pages/login/index.client.vue
  81. 78 0
      src/pages/note-create-start/index.client.vue
  82. 0 6
      src/pages/note-create-start/index.vue
  83. 405 0
      src/pages/note-create/index.client.vue
  84. 2 2
      src/pages/profile/index/notes.vue
  85. 20 0
      src/pages/t/[id].vue
  86. 118 0
      src/pages/travel-projects/index.client.vue
  87. 5 5
      src/utils/index.js

+ 2 - 2
.env.development

@@ -1,7 +1,7 @@
 VITE_APP_ENV=development
 
 # VITE_APP_BASE_URL=http://192.168.101.101/api/
-# VITE_APP_BASE_URL=http://1.94.207.143:8082/
-VITE_APP_BASE_URL=http://192.168.1.204:8082
+VITE_APP_BASE_URL=http://1.94.207.143:8082/
+# VITE_APP_BASE_URL=http://192.168.1.204:8082
 
 VITE_APP_IM_USER_SUFFIX=dev

+ 6 - 4
nuxt.config.ts

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

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

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

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

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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
src/assets/iconfont/iconfont.js


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

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

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

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

BIN
src/assets/iconfont/iconfont.ttf


BIN
src/assets/iconfont/iconfont.woff


BIN
src/assets/iconfont/iconfont.woff2


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


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


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


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


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


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


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


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


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


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


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


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


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


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


BIN
src/assets/img/navbar/menu_car.png


BIN
src/assets/img/navbar/menu_create_note.png


BIN
src/assets/img/navbar/menu_home.png


BIN
src/assets/img/navbar/menu_house.png


BIN
src/assets/img/navbar/menu_loubar.png


BIN
src/assets/img/navbar/menu_tickets.png


BIN
src/assets/img/navbar/menu_travel_note.png


BIN
src/assets/img/navbar/menu_travel_project.png


BIN
src/assets/img/navbar/menu_visa.png


BIN
src/assets/img/note-create/calendar.png


BIN
src/assets/img/note-create/icon-image-fill.png


BIN
src/assets/img/note-create/note_create_bg.png


BIN
src/assets/img/note-create/note_create_entry.png


BIN
src/assets/img/note-create/note_create_entry3.png


+ 8 - 0
src/assets/img/note-create/upload.svg

@@ -0,0 +1,8 @@
+<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="upload">
+<g id="union">
+<path d="M5.63919 9.18198L10.8141 4.00704L10.8141 15.8125L12.1891 15.8125L12.1891 4.00705L17.3641 9.18198L18.3363 8.2097L11.8552 1.72855C11.6599 1.53329 11.3433 1.53329 11.1481 1.72855L4.66691 8.20971L5.63919 9.18198Z" fill="white"/>
+<path d="M3.25 15.125V17.875C3.25 18.6344 3.86561 19.25 4.625 19.25H18.375C19.1344 19.25 19.75 18.6344 19.75 17.875V15.125H18.375V17.875H4.625V15.125H3.25Z" fill="white"/>
+</g>
+</g>
+</svg>

+ 27 - 0
src/components/CreateNote/BottomActions.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="mx-auto flex w-wrap justify-center space-x-20">
+    <div
+      @click="$emit('onPreview')"
+      class="flex h-60 w-200 cursor-pointer items-center justify-center rounded-full border border-primary bg-white text-2xl font-semibold text-primary hover:opacity-80"
+    >
+      预览
+    </div>
+    <el-button
+      @click="$emit('onPublish')"
+      :loading="publishLoading"
+      type="primary"
+      size="large"
+      style="width: 200px; height: 60px; font-size: 18px; border-radius: 30px"
+      >发表游记</el-button
+    >
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  publishLoading: Boolean
+})
+defineEmits(['onPreview', 'onPublish'])
+</script>
+
+<style lang="scss" scoped></style>

+ 213 - 0
src/components/CreateNote/Form.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="box-border">
+    <div class="flex items-center pl-16 w-full h-44 pt-12">
+      <div class="w-2 h-14 bg-[#FF9300] mr-16"></div>
+      <h1 class="text-sm font-bold">填写游记信息</h1>
+    </div>
+    <div class="flex items-center justify-center">
+      <van-field
+        v-model="departureTime"
+        :label-width="labelWidth"
+       
+        name="calendar"
+        clearable
+        autocomplete="off"
+        placeholder="请选择出发时间"
+        @click="showCalendar = true"
+        :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
+          >
+        </template>
+        <template #right-icon>
+          <div class="w-24 h-24">
+            <van-image width="100%" height="100%" :src="calendar" />
+          </div>
+        </template>
+      </van-field>
+      <van-calendar
+        color="#FE8E2C"
+        :min-date="minDate"
+        :max-date="maxDate"
+        title="请选择出发时间"
+        type="single"
+        v-model:show="showCalendar"
+        @confirm="onConfirm"
+      />
+    </div>
+
+    <van-field
+      :label-width="labelWidth"
+      v-model="countTimes"
+      name="countTimes"
+      autocomplete="off"
+      clearable
+      placeholder="请输入您的天数 例:4天"
+      :rules="[{ required: true, message: '请输入您的天数' }]"
+    >
+      <template #label>
+        <span class="text-16 font-400 text-[#000]/[0.9]"
+          >出发天数 <span class="text-16 text-[#D54941]">*</span></span
+        >
+      </template>
+    </van-field>
+    <van-field
+      v-model="role"
+      label="人物关系"
+      clearable
+      autocomplete="off"
+      placeholder="例:情侣"
+    ></van-field>
+
+    <van-field
+      v-model="endPlace"
+      :label-width="labelWidth"
+      clearable
+      autocomplete="off"
+      placeholder="目的地"
+      @click="showPicker = true"
+      :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
+        >
+      </template>
+    </van-field>
+    <van-popup v-model:show="showPicker" destroy-on-close position="bottom">
+      <van-picker
+        :columns="placeOptions"
+        :columns-field-names="placeOptionProps"
+        @confirm="onConfirmAddr"
+        @cancel="showPicker = false"
+      />
+    </van-popup>
+    <van-field  name="rate" :label-width="labelWidth" label="推荐指数">
+      <template #input>
+        <van-rate color="#ffd21e" v-model="recommendationRate" clearable />
+      </template>
+    </van-field>
+
+    <van-field
+    :label-width="labelWidth"
+      v-model="averageCost"
+      name="averageCost"
+      autocomplete="off"
+      clearable
+      maxlength="10"
+      label="平均费用"
+      placeholder="请输入平均费用"
+    >
+    </van-field>
+
+    <van-field
+    :label-width="labelWidth"
+      is-link
+      v-model="travelMode"
+      clearable
+      autocomplete="off"
+      label="出行方式"
+      placeholder="请选择出行方式"
+      @click="showtravelMode = true"
+    >
+    </van-field>
+    <van-popup v-model:show="showtravelMode" destroy-on-close position="bottom">
+      <van-picker
+        :columns="travelModeOptions"
+        :columns-field-names="travelModeOptionsProps"
+        @confirm="onConfirmTravelMode"
+        @cancel="showtravelMode = false"
+      />
+    </van-popup>
+
+    <van-field
+      :label-width="labelWidth"
+      v-model="travelNumber"
+      label="游玩人数"
+      clearable
+      autocomplete="off"
+      placeholder="请输入游玩人数"
+    ></van-field>
+  </div>
+</template>
+
+<script setup>
+import calendar from "~/assets//img/note-create/calendar.png";
+const  labelWidth ='68px'
+// 出发时间
+const minDate = new Date(2010, 0, 1);
+const maxDate = new Date();
+
+const showCalendar = ref(false);
+
+// 展示时间的格式
+const onConfirm = (date) => {
+  departureTime.value = date ? `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` : "";
+  showCalendar.value = false;
+};
+
+// 目的地
+const showPicker = ref(false);
+const onConfirmAddr = ({ selectedValues, selectedOptions }) => {
+  showPicker.value = false;
+  endPlace.value = selectedOptions[selectedValues.length - 1].menuName;
+};
+
+// 出行方式
+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");
+
+onMounted(() => {
+  getPlaceOptions();
+  getTravelModeOptions();
+});
+
+const placeOptionProps = {
+  text: "menuName",
+  value: "id",
+  children: "tourWriteBelongTabVoList",
+};
+const travelModeOptionsProps = {
+  text: "name",
+  value: "id",
+};
+
+// 获取目的地
+const placeOptions = ref([]);
+async function getPlaceOptions() {
+  const { data } = await request(
+    "/website/tourism/publishTravelNotes/getWriteBelongTab"
+  );
+  console.log(data, "placeOptions");
+
+  placeOptions.value = data;
+}
+
+// 获取出行方式
+const travelModeOptions = ref([]);
+async function getTravelModeOptions() {
+  const { data } = await request(
+    "/admin/app/extra/listDict?dictCode=TourTravelMode"
+  );
+  console.log(data, "115");
+
+  travelModeOptions.value = data;
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 132 - 0
src/components/CreateNote/HeaderBanner.vue

@@ -0,0 +1,132 @@
+<template>
+  <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> -->
+      <!-- <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"
+      >
+        
+          <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>
+    </div>
+   
+   
+
+    <!-- <div v-else class="relative h-full w-full">
+      <img :src="bannerUrl" class="h-full w-full" alt="" />
+      <div
+        @click="open"
+        class="absolute bottom-0 left-0 flex cursor-pointer items-center space-x-8 bg-[#00000080] px-15 py-8 hover:opacity-90"
+      >
+        <span
+          class="iconfont icon-setting text-primary"
+          style="font-size: 20px"
+        ></span>
+        <span class="text-base text-primary">重新上传头图</span>
+      </div>
+    </div> -->
+    <!-- <el-dialog title="图片剪裁" v-model="cropperDialogVisible" width="1000px">
+      <div class="h-380 w-full">
+        <vueCropper
+          ref="cropperRef"
+          :img="cropperImageBase64"
+          autoCrop
+          :outputSize="1"
+          centerBox
+          fixed
+          fixedBox
+          :full="true"
+          :fixedNumber="[3.2, 1]"
+        ></vueCropper>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cropperDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleCropperOk" :loading="loading">
+            确认
+          </el-button>
+        </div>
+      </template>
+    </el-dialog> -->
+  </div>
+</template>
+
+<script setup>
+import { useFileDialog } from '@vueuse/core'
+import icon_image_fill from "~/assets/img/note-create/icon-image-fill.png";
+// import { VueCropper } from 'vue-cropper'
+// import 'vue-cropper/dist/index.css'
+
+const bannerUrl = defineModel('bannerUrl')
+
+const emit = defineEmits(['onSelectImage'])
+
+const { open, onChange } = useFileDialog({
+  accept: '.png,.png,.jpeg,.JPG,Png '
+})
+
+const cropperImageBase64 = ref('')
+const fileList = ref([])
+onChange((files) => {
+  if (!files.length) return
+  const reader = new FileReader()
+  reader.readAsDataURL(files[0])
+  reader.onload = () => {
+    cropperDialogVisible.value = true
+    cropperImageBase64.value = reader.result
+  }
+})
+
+function handleSelectImage() {
+  open()
+}
+
+const cropperRef = ref(null)
+const cropperDialogVisible = ref(false)
+const loading = ref(false)
+async function handleCropperOk() {
+  cropperRef.value?.getCropBlob(async (data) => {
+    try {
+      loading.value = true
+      // 此处需上传图片,保存URL
+      const formData = new FormData()
+      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 url = res.data.fileUrl
+      bannerUrl.value = url
+      cropperDialogVisible.value = false
+    } finally {
+      loading.value = false
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 55 - 0
src/components/CreateNote/InsertContentSection.vue

@@ -0,0 +1,55 @@
+<template>
+  <div
+    ref="target"
+    class="relative overflow-hidden rounded-lg border border-dashed border-primary bg-white px-10"
+  >
+    <textarea
+      class="min-h-200 w-full resize-none rounded-lg border-none py-10 text-base leading-[28px] text-black-3 outline-none"
+      ref="textarea"
+      v-model="input"
+      autofocus
+      placeholder="从这里开始游记正文..."
+    />
+    <div
+      v-show="!isOutside"
+      class="absolute right-0 top-0 flex h-26 items-center justify-center space-x-15 bg-[rgba(0,0,0,0.7)] px-12 transition-all"
+    >
+      <span
+        @click="$emit('onDelete')"
+        class="iconfont icon-delete cursor-pointer text-white"
+        style="font-size: 18px"
+      ></span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useMouseInElement } from '@vueuse/core'
+
+const target = ref(null)
+const { isOutside } = useMouseInElement(target)
+
+const model = defineModel()
+
+defineEmits(['onDelete'])
+
+const { textarea, input } = useTextareaAutosize()
+
+watchEffect(() => {
+  input.value = model.value
+})
+watch(input, () => {
+  model.value = input.value
+})
+</script>
+
+<style lang="scss" scoped>
+textarea {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+textarea::-webkit-scrollbar {
+  display: none;
+}
+</style>

+ 70 - 0
src/components/CreateNote/InsertImageModal.vue

@@ -0,0 +1,70 @@
+<template>
+  <div>
+    <el-dialog
+      v-model="visible"
+      title="选择图片"
+      width="820"
+      destroy-on-close
+      :z-index="9999"
+      @closed="onDialogClosed"
+    >
+      <div class="max-h-400 py-20">
+        <el-upload
+          v-model:file-list="fileList"
+          name="uploadFile"
+          :headers="{
+            Authorization: token
+          }"
+          :data="{
+            asImage: true,
+            fieldName: 'tourismTavelNotesUrl'
+          }"
+          multiple
+          :auto-upload="true"
+          :action="uploadUrl"
+          list-type="picture-card"
+        >
+          <span class="iconfont icon-plus" style="font-size: 26px"></span>
+        </el-upload>
+      </div>
+
+      <template #footer>
+        <div class="flex items-center justify-end">
+          <el-button @click="visible = false">取消</el-button>
+          <el-button type="primary" @click="handleOk"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+const visible = defineModel('visible', false)
+
+const emit = defineEmits(['onOk'])
+
+const useAuth = useAuthStore()
+const { token } = storeToRefs(useAuth)
+
+const uploadUrl = `${import.meta.env.VITE_APP_BASE_URL}/admin/app/tourismProjectTravelNotesWrite/upload`
+
+const fileList = ref([])
+
+function handleOk() {
+  const fileUrlList = fileList.value.map((e) => ({
+    fileUrl: e.response.data.fileUrl
+  }))
+  if (fileUrlList.length === 0) {
+    ElMessage.warning('请选择图片')
+    return
+  }
+  emit('onOk', fileUrlList)
+  visible.value = false
+}
+
+function onDialogClosed() {
+  fileList.value = []
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 37 - 0
src/components/CreateNote/InsertImageSection.vue

@@ -0,0 +1,37 @@
+<template>
+  <div
+    class="relative rounded-lg border border-dashed border-primary px-10 py-10"
+    ref="target"
+  >
+    <img :src="url" class="h-auto w-full" lazy alt="" srcset="" />
+    <div
+      v-show="!isOutside"
+      class="absolute right-0 top-0 flex h-26 items-center justify-center space-x-15 bg-[rgba(0,0,0,0.7)] px-12 transition-all"
+    >
+      <span
+        @click="$emit('onSaveCover')"
+        class="cursor-pointer text-base text-white"
+        >设为封面图</span
+      >
+      <span
+        @click="$emit('onDelete')"
+        class="iconfont icon-delete cursor-pointer text-white"
+        style="font-size: 18px"
+      ></span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useMouseInElement } from '@vueuse/core'
+defineProps({
+  url: String
+})
+
+const emit = defineEmits(['onDelete', 'onSaveCover'])
+
+const target = ref(null)
+const { isOutside } = useMouseInElement(target)
+</script>
+
+<style lang="scss" scoped></style>

+ 60 - 0
src/components/CreateNote/InsertTitleModal.client.vue

@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <el-dialog
+      v-model="visible"
+      title="插入标题"
+      width="600"
+      destroy-on-close
+      :z-index="9999"
+      @closed="onDialogClosed"
+    >
+      <div class="py-20">
+        <el-input
+          size="large"
+          maxlength="30"
+          v-model="inputValue"
+          show-word-limit=""
+          placeholder="请输入段落标题"
+        />
+      </div>
+
+      <template #footer>
+        <div class="flex items-center justify-end">
+          <el-button @click="visible = false">取消</el-button>
+          <el-button type="primary" @click="handleOk"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+const visible = defineModel('visible', false)
+
+const props = defineProps({
+  title: String
+})
+
+const inputValue = ref()
+
+watchEffect(() => {
+  inputValue.value = props.title
+})
+
+const emit = defineEmits(['onOk'])
+
+function handleOk() {
+  if (!inputValue.value) {
+    ElMessage.warning('请输入段落标题')
+    return
+  }
+  visible.value = false
+  emit('onOk', inputValue.value)
+}
+
+function onDialogClosed() {
+  inputValue.value = ''
+}
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,38 @@
+<template>
+  <div
+    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"
+  >
+    <span class="text-3xl text-black-3">{{ title }}</span>
+    <div
+      v-show="!isOutside"
+      class="absolute right-0 top-0 flex h-26 items-center justify-center space-x-15 bg-[rgba(0,0,0,0.7)] px-12 transition-all"
+    >
+      <span
+        @click="$emit('onEdit')"
+        class="iconfont icon-edit-square cursor-pointer text-white"
+        style="font-size: 18px"
+      ></span>
+      <span
+        @click="$emit('onDelete')"
+        class="iconfont icon-delete cursor-pointer text-white"
+        style="font-size: 18px"
+      ></span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useMouseInElement } from '@vueuse/core'
+
+const target = ref(null)
+const { isOutside } = useMouseInElement(target)
+
+const props = defineProps({
+  title: String
+})
+
+defineEmits(['onEdit', 'onDelete'])
+</script>
+
+<style lang="scss" scoped></style>

+ 58 - 0
src/components/CreateNote/LeftActions.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="mt-20 flex flex-col space-y-15 text-xl text-black-6">
+    <div
+      class="flex cursor-pointer items-center space-x-15"
+      @click="$emit('onInsertTitle')"
+    >
+      <img
+        src=" ~/assets/img/note-create/create_note_insert_title.png"
+        class="h-26 w-26"
+        alt=""
+        srcset=""
+      />
+      <span>插入段落标题</span>
+    </div>
+    <div
+      class="flex cursor-pointer items-center space-x-15"
+      @click="$emit('onInsertContent')"
+    >
+      <img
+        src=" ~/assets/img/note-create/create_note_insert_title.png"
+        class="h-26 w-26"
+        alt=""
+        srcset=""
+      />
+      <span>插入段落内容</span>
+    </div>
+    <div
+      class="flex cursor-pointer items-center space-x-15"
+      @click="$emit('onInsertImg')"
+    >
+      <img
+        src=" ~/assets/img/note-create/create_note_insert_img.png"
+        class="h-26 w-26"
+        alt=""
+        srcset=""
+      />
+      <span>插入图片</span>
+    </div>
+    <div
+      class="flex cursor-pointer items-center space-x-15"
+      @click="$emit('onSaveDraft')"
+    >
+      <img
+        src=" ~/assets/img/note-create/create_note_save_tmp.png"
+        class="h-26 w-26"
+        alt=""
+        srcset=""
+      />
+      <span>保存草稿</span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineEmits(['onInsertTitle', 'onInsertContent', 'onInsertImg', 'onSaveDraft'])
+</script>
+
+<style lang="scss" scoped></style>

+ 153 - 0
src/components/CreateNote/PreviewModal.vue

@@ -0,0 +1,153 @@
+<template>
+  <div>
+    <el-dialog
+      v-model="visible"
+      title="游记预览"
+      width="1000"
+      :z-index="9999"
+      destroy-on-close
+    >
+      <div class="max-h-[600px] overflow-auto">
+        <img
+          v-if="data.travelNotesBanner"
+          :src="data.travelNotesBanner"
+          class="h-auto w-full"
+        />
+        <img
+          v-else
+          src="~/assets/img/note-create/note_create_banner_bg.png"
+          class="h-auto w-full"
+        />
+        <div
+          class="mx-auto flex flex-1 items-center px-30 py-15 shadow-[0_15px_30px_0px_rgba(102,102,102,0.2)]"
+        >
+          <img
+            src="~/assets/img/travel_notes_detail/travel_note_icon.jpg"
+            class="h-50 w-50 shrink-0 object-cover"
+            alt=""
+            srcset=""
+          />
+          <div class="ml-10 flex-1">
+            <div class="text-3xl font-bold text-black-3">
+              {{ data.title ?? '游记标题' }}
+            </div>
+          </div>
+        </div>
+        <!-- <div
+          v-if="baseInfos.length"
+          class="mt-20 grid grid-cols-4 gap-y-15 rounded-xl border border-dashed border-black-9 px-15 py-15"
+        >
+          <div
+            v-for="item in baseInfos"
+            class="flex items-center space-x-2 text-base"
+          >
+            <img :src="item.icon" class="h-44 w-44 shrink-0" alt="" />
+            <span class="flex w-0 flex-1" :style="{ color: item.color }">
+              <span class="shrink-0 font-semibold">{{ item.lable }}/</span>
+              <span class="flex-1 truncate">{{ item.value }}</span>
+            </span>
+          </div>
+        </div> -->
+        <div class="mt-20">
+          <template v-for="item in data.travelNotesContent">
+            <div v-if="item.type === 'sectionTitle'">
+              <div class="py-10 text-3xl text-black-3">
+                {{ item.content }}
+              </div>
+            </div>
+            <div v-if="item.type === 'sectionContent'">
+              <div class="py-10 text-base leading-[28px] text-black-6">
+                {{ item.content }}
+              </div>
+            </div>
+            <div v-if="item.type === 'image'">
+              <el-image class="h-auto w-full py-10" :src="item.content" />
+            </div>
+          </template>
+        </div>
+      </div>
+      <template #footer>
+        <div class="flex items-center justify-center">
+          <el-button type="primary" @click="visible = false"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import travel_notes_detail_startdate from '~/assets/img/travel_notes_detail/travel_notes_detail_startdate.png'
+import travel_notes_detail_days from '~/assets/img/travel_notes_detail/travel_notes_detail_days.png'
+import travel_notes_detail_relation from '~/assets/img/travel_notes_detail/travel_notes_detail_relation.png'
+import travel_notes_detail_fee from '~/assets/img/travel_notes_detail/travel_notes_detail_fee.png'
+import travel_notes_detail_star from '~/assets/img/travel_notes_detail/travel_notes_detail_star.png'
+import travel_notes_detail_traffic from '~/assets/img/travel_notes_detail/travel_notes_detail_traffic.png'
+import travel_notes_detail_endplace from '~/assets/img/travel_notes_detail/travel_notes_detail_endplace.png'
+
+const visible = defineModel('visible', false)
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({})
+  },
+  travelModeLabel: String,
+  endPlace: String
+})
+
+const baseInfos = computed(() => {
+  const tmpList = []
+  if (props.data?.departureTime)
+    tmpList.push({
+      lable: '出发时间',
+      value: props.data?.departureTime,
+      color: '#4B99EA',
+      icon: travel_notes_detail_startdate
+    })
+  if (props.data?.countTimes)
+    tmpList.push({
+      lable: '出发天数',
+      value: props.data?.countTimes,
+      color: '#4B99EA',
+      icon: travel_notes_detail_days
+    })
+  if (props.data?.role)
+    tmpList.push({
+      lable: '人物关系',
+      value: props.data?.role,
+      color: '#4B99EA',
+      icon: travel_notes_detail_relation
+    })
+  if (props.data?.averageCost)
+    tmpList.push({
+      lable: '人均费用',
+      value: props.data?.averageCost,
+      color: '#4B99EA',
+      icon: travel_notes_detail_fee
+    })
+  if (props.travelModeLabel)
+    tmpList.push({
+      lable: '出行方式',
+      value: props.travelModeLabel,
+      color: '#4B99EA',
+      icon: travel_notes_detail_traffic
+    })
+  if (props.endPlace)
+    tmpList.push({
+      lable: '目的地',
+      value: props.data.endPlace,
+      color: '#4B99EA',
+      icon: travel_notes_detail_endplace
+    })
+  if (props.data?.recommendationRate)
+    tmpList.push({
+      lable: '推荐指数',
+      value: props.data?.recommendationRate,
+      color: '#facc58',
+      icon: travel_notes_detail_star
+    })
+  return tmpList
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 57 - 0
src/components/CreateNote/PublishResultModal.client.vue

@@ -0,0 +1,57 @@
+<template>
+  <div>
+    <el-dialog
+      style="border-radius: 20px"
+      v-model="visible"
+      width="550"
+      @closed="onDialogClosed"
+    >
+      <div
+        class="flex w-full flex-col items-center space-y-10 rounded-[20px] py-25"
+      >
+        <img
+          src="~/assets/img/note-create/note_create_success.png"
+          class="w-210"
+        />
+        <div class="text-2xl font-semibold text-primary">提交成功</div>
+        <div class="text-base text-black-6">
+          审核中,可在
+          <NuxtLink to="/profile/notes" class="text-primary underline"
+            >我的游记</NuxtLink
+          >
+          中查看审核结果
+        </div>
+        <div class="flex items-center justify-center space-x-20 pt-20">
+          <el-button type="primary" @click="handleNextNote">继续发布</el-button>
+          <el-button type="primary" @click="handleCheck">去查看</el-button>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+const visible = defineModel('visible', false)
+
+const emit = defineEmits(['submitOk'])
+
+function onDialogClosed() {}
+
+function handleNextNote() {
+  visible.value = false
+  navigateTo('/note-create', {
+    replace: true
+  })
+}
+
+function handleCheck() {
+  navigateTo('/profile/notes?tab=auditing', {
+    replace: true,
+    query: {
+      tab: 'auditing'
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 136 - 0
src/components/CreateNote/UserInfoModal.client.vue

@@ -0,0 +1,136 @@
+<template>
+  <div>
+    <el-dialog
+      style="--el-dialog-padding-primary: 0px; border-radius: 20px"
+      v-model="visible"
+      width="550"
+      destroy-on-close
+      :z-index="99"
+      :show-close="false"
+      @closed="onDialogClosed"
+    >
+      <div
+        class="relative flex w-full flex-col items-center rounded-[20px] bg-gradient-to-b from-[#ffe6c0] to-[#fffffe] py-30"
+      >
+        <img
+          src="~/assets/img/note-create/userinfo_modal.jpg"
+          class="absolute right-40 top-0 w-120"
+        />
+        <div class="text-2xl font-semibold text-black-3">
+          请完善您的个人信息
+        </div>
+        <div class="-ml-90 mt-20">
+          <el-form
+            ref="formRef"
+            :model="formData"
+            :rules="formRules"
+            label-width="100px"
+            style="--el-form-label-font-size: 13px; margin-top: 5px"
+          >
+            <el-form-item label="昵称:" prop="showName">
+              <el-input
+                v-model="formData.showName"
+                :maxlength="50"
+                style="width: 300px"
+              />
+            </el-form-item>
+            <el-form-item label="住址:" prop="address">
+              <el-input
+                v-model="formData.address"
+                :maxlength="100"
+                style="width: 300px"
+              />
+            </el-form-item>
+            <el-form-item label="邮箱:" prop="email">
+              <el-input
+                v-model="formData.email"
+                :maxlength="100"
+                style="width: 300px"
+              />
+            </el-form-item>
+            <el-form-item label="职业:" prop="job">
+              <el-input
+                v-model="formData.job"
+                :maxlength="100"
+                style="width: 300px"
+              />
+            </el-form-item>
+          </el-form>
+        </div>
+        <div
+          @click="handleSubmit"
+          class="mt-20 flex h-40 w-110 cursor-pointer items-center justify-center rounded-full bg-primary text-xl font-semibold text-white hover:opacity-80"
+        >
+          提交
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+const visible = defineModel('visible', false)
+
+const emit = defineEmits(['submitOk'])
+
+function onDialogClosed() {}
+
+const formData = reactive({
+  showName: '',
+  address: '',
+  email: '',
+  job: ''
+})
+
+const formRules = {
+  showName: [
+    {
+      required: true,
+      message: '请输入昵称',
+      trigger: 'blur'
+    }
+  ],
+  address: [
+    {
+      required: false,
+      message: '请输入住址',
+      trigger: 'blur'
+    }
+  ],
+  email: [
+    {
+      required: true,
+      message: '请输入邮箱',
+      trigger: 'blur'
+    }
+  ],
+  job: [
+    {
+      required: false,
+      message: '请输入职业',
+      trigger: 'blur'
+    }
+  ]
+}
+
+const formRef = ref(null)
+
+async function handleSubmit() {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (valid) {
+      try {
+        await request('/website/tourism/publishTravelNotes/savePerfect', {
+          method: 'post',
+          body: formData
+        })
+        visible.value = false
+        emit('submitOk')
+      } catch (error) {}
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped></style>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 154 - 0
src/components/Navbar/LeftMenu.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="pt-70 px-20 pb-30 flex flex-col h-screen">
+    <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 v-else-if="token" class="flex items-center space-x-15">
+      <van-image
+        :src="userInfo.headImageUrl"
+        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">{{ 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">{{ userInfo.personalSign }}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="flex flex-col mt-20 divide-y flex-1 overflow-scroll">
+      <div
+        v-for="item in menuData"
+        :key="item.title"
+        @click="handleClickMenu(item)"
+        class="flex items-center h-50 space-x-5"
+      >
+        <img :src="item.icon" class="w-23 h-23" alt="" srcset="" />
+        <span class="text-base text-black-3">{{ item.title }}</span>
+      </div>
+    </div>
+
+    <van-button
+      v-if="token"
+      @click="handleLogout"
+      plain
+      style="width: 100%; margin-top: 20px"
+      >退出登录</van-button
+    >
+  </div>
+</template>
+
+<script setup>
+import menu_travel_home from "~/assets/img/navbar/menu_home.png";
+import menu_car from "@/assets/img/navbar/menu_car.png";
+import menu_create_note from "@/assets/img/navbar/menu_create_note.png";
+import menu_house from "@/assets/img/navbar/menu_house.png";
+import menu_labour from "@/assets/img/navbar/menu_loubar.png";
+import menu_tickets from "@/assets/img/navbar/menu_tickets.png";
+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);
+
+const userInfoStore = useUserInfoStore();
+const { userInfo } = storeToRefs(userInfoStore);
+
+const needLoginMenus = [
+  {
+    title: "写游记",
+    icon: menu_create_note,
+    to: "/note-create",
+  },
+];
+
+const menuData = reactive([
+  {
+    title: "首页",
+    icon: menu_travel_home,
+    to: "/",
+  },
+  {
+    title: "游记",
+    icon: menu_travel_note,
+    to: "/travel-notes",
+  },
+  {
+    title: "旅游项目",
+    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",
+  // },
+]);
+
+watch(
+  token,
+  (val) => {
+    if (!val) return;
+    userInfoStore.getUserInfo();
+    menuData.splice(1, 0, ...needLoginMenus);
+  },
+  {
+    immediate: true,
+  }
+);
+
+function handleClickMenu(item) {
+  navigateTo({
+    path: item.to,
+  });
+  emit("onHide");
+}
+
+function handleLogout() {
+  try {
+    request("/website/web/doLogout", { method: "post" });
+    emit("onHide");
+  } finally {
+    authStore.cleanToken();
+    navigateTo("/");
+  }
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 22 - 17
src/components/Navbar/index.vue

@@ -1,25 +1,30 @@
 <template>
-  <van-nav-bar
-    :title="title"
-    border
-    left-arrow
-    placeholder
-    fixed
-    safe-area-inset-top
-    @click-left="onClickLeft"
-  />
+  <div class="h-60 bg-white justify-between w-full fixed z-[999]">
+    <NuxtLink to="/" class="absolute top-1/2 -translate-y-1/2 left-15">
+      <img src="~/assets/img/logo.png" class="h-40 object-contain" />
+    </NuxtLink>
+    <div
+      class="absolute right-15 top-1/2 -translate-y-1/2"
+      @click="handleClickMenu"
+    >
+      <span class="iconfont icon-menu" style="font-size: 24px"></span>
+    </div>
+
+    <van-popup
+      v-model:show="isMenuShow"
+      position="left"
+      :style="{ height: '100%', width: '70%' }"
+    >
+      <NavBarLeftMenu v-if="isMenuShow" @on-hide="isMenuShow = false" />
+    </van-popup>
+  </div>
 </template>
 
 <script setup>
-defineProps({
-  title: {
-    type: String,
-    default: "标题",
-  },
-});
+const isMenuShow = ref(false);
 
-function onClickLeft() {
-  navigateBack();
+function handleClickMenu() {
+  isMenuShow.value = true;
 }
 </script>
 

+ 51 - 26
src/components/Profile/Notes/Auditing/Item.vue

@@ -1,33 +1,54 @@
 <template>
-  <div class="group flex space-x-10 p-10 transition-all rounded-xl bg-[#FFF]">
-    <img
-      :src="formatImgSrc(data?.tourismUrlsAfterConvert) || noteDraftCoverBg"
-      class="aspect-[4/3] h-109 shrink-0 object-cover"
-    />
+  <van-swipe-cell>
+    <div class="group flex space-x-10 p-10 transition-all rounded-xl bg-[#FFF]">
+      <img
+        :src="formatImgSrc(data?.tourismUrlsAfterConvert) || noteDraftCoverBg"
+        class="aspect-[4/3] h-109 shrink-0 object-cover"
+      />
 
-    <div class="flex w-0 flex-1 flex-col justify-between">
-      <div class="mt-18">
-        <div class="line-clamp-1 break-all text-base font-semibold text-black-3">
-          {{ data?.projectTitle || '未命名草稿' }}
-        </div>
-        <div class="mt-2 text-sm text-black-3">
-          {{ $dayjs(data?.updateTime).format('YYYY/MM/DD') }}
-        </div>
-      </div>
-      <div @click="$emit('onRevoke')" class="flex mb-10 items-center space-x-5">
-        <div class="flex cursor-pointer items-center p-5 text-primary">
-          <span class="w-15 h-15">
-            <img
-              class="w-full h-full shrink-0 object-cover"
-              src="~/assets/img/note-create/cancel.svg"
-              alt=""
-            />
-          </span>
-          <span class="text-base">撤销审核</span>
+      <div class="flex w-0 flex-1 flex-col justify-between">
+        <div class="mt-15">
+          <div class="line-clamp-1 break-all text-base font-semibold text-black-3">
+            {{ data?.projectTitle || '未命名草稿' }}
+          </div>
+          <div class="mt-2 text-sm text-black-3">
+            {{ $dayjs(data?.updateTime).format('YYYY/MM/DD') }}
+          </div>
         </div>
+        <!-- <div @click="$emit('onRevoke')" class="flex mb-10 items-center space-x-5">
+          <div class="flex cursor-pointer items-center p-5 text-primary">
+            <span class="w-15 h-15">
+              <img
+                class="w-full h-full shrink-0 object-cover"
+                src="~/assets/img/note-create/cancel.svg"
+                alt=""
+              />
+            </span>
+            <span class="text-base">撤销审核</span>
+          </div>
+        </div> -->
       </div>
     </div>
-  </div>
+
+    <template #right>
+      <!-- <div> -->
+      <!-- <span class="w-15 h-15">
+          <img
+            class="w-full h-full shrink-0 object-cover"
+            src="~/assets/img/note-create/cancel.svg"
+            alt=""
+          />
+        </span> -->
+      <van-button
+        square
+        text="撤销审核"
+        @click="$emit('onRevoke')"
+        type="warning"
+        class="delete-button"
+      />
+      <!-- </div> -->
+    </template>
+  </van-swipe-cell>
 </template>
 
 <script setup>
@@ -52,4 +73,8 @@ function handleWrite() {
 }
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.delete-button {
+  height: 100%;
+}
+</style>

+ 1 - 1
src/components/Profile/Notes/Auditing/index.vue

@@ -2,7 +2,7 @@
   <div class="w-full min-h-400 pb-10">
     <!-- v-loading="loading" -->
     <!-- v-if="!loading && !draftList.length" -->
-    <ProfileNotesEmpty v-if="!draftList.length" />
+    <!-- <ProfileNotesEmpty v-if="!draftList.length" /> -->
     <!-- v-else-if="draftList.length" -->
     <!-- <div class="flex flex-col divide-y"> -->
     <div class="flex flex-col divide-y">

+ 65 - 0
src/components/Profile/Notes/Draft/Item.vue

@@ -0,0 +1,65 @@
+<template>
+  <van-swipe-cell>
+    <div class="group flex space-x-10 p-10 transition-all mb-10 bg-[#fff] rounded-xl">
+      <img
+        :src="formatImgSrc(data?.tourismUrlsAfterConvert) || noteDraftCoverBg"
+        class="aspect-[4/3] h-109 shrink-0 rounded-xl object-cover"
+      />
+
+      <div class="flex w-0 flex-1 flex-col justify-between">
+        <div class="pt-15">
+          <div class="line-clamp-2 break-all text-xl font-semibold text-black-3">
+            {{ data?.projectTitle || '未命名草稿' }}
+          </div>
+          <div class="mt-10 text-sm text-black-9">
+            {{ $dayjs(data?.updateTime).format('YYYY-MM-DD') }}
+          </div>
+        </div>
+        <!-- <div class="flex items-center space-x-10">
+          <div
+            @click="handleWrite"
+            class="flex cursor-pointer items-center space-x-5 rounded border border-[#f0f2f5] px-10 py-5 text-primary hover:bg-primary hover:text-white"
+          >
+            <span class="iconfont icon-edit-square" style="font-size: 14px"></span>
+            <span class="text-sm">继续写</span>
+          </div>
+          <div
+            @click="$emit('onDelete')"
+            class="iconfont icon-delete hidden cursor-pointer text-black-6 group-hover:block"
+            style="font-size: 18px"
+          ></div>
+        </div> -->
+      </div>
+    </div>
+    <template #right>
+      <van-button square text="继续写" @click="handleWrite" type="warning" class="delete-button" />
+      <van-button square text="删除" @click="handleWrite" type="danger" class="delete-button" />
+    </template>
+  </van-swipe-cell>
+</template>
+
+<script setup>
+import noteDraftCoverBg from '~/assets/img/note-create/note_draft_cover_bg.jpg'
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+function handleWrite() {
+  navigateTo({
+    path: '/note-create',
+    query: {
+      id: props.data.id
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.delete-button {
+  height: 100%;
+}
+</style>

+ 67 - 0
src/components/Profile/Notes/Draft/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="min-h-400 pb-10">
+    <!-- v-loading="loading" -->
+    <ProfileNotesEmpty v-if="!loading && !draftList.length" />
+    <!--  v-else-if="draftList.length" -->
+    <div>
+      <div class="text-black-3 px-10">
+        <p class="text-base font-bold">{{ draftList?.length }}篇草稿</p>
+        <p class="text-sm text-[#FF2929]">
+          【您好,您还有{{ draftList?.length }}篇草稿没有完成,我们期待您的大作哦~】
+        </p>
+      </div>
+      <div class="grid grid-cols-1">
+        <ProfileNotesDraftItem 
+          v-for="item in draftList"
+          :key="item.id"
+          :data="item"
+          @on-delete="handleDelete(item)"  />
+     
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const { loading, setLoading } = useLoading()
+loading.value = true
+
+const draftList = ref([])
+
+async function getNotesList() {
+  setLoading(true)
+  try {
+    const { data } = await request('/website/tourism/publishTravelNotes/getDraftList', {
+      query: {
+        pageNum: 1,
+        pageSize: 10000,
+        type: 0
+      }
+    })
+    draftList.value = data?.dataList
+  } finally {
+    setLoading(false)
+  }
+}
+
+async function handleDelete(item) {
+  ElMessageBox.confirm('确定删除吗?', '提示', {}).then(async () => {
+    try {
+      await request(`/website/tourism/publishTravelNotes/removeByDraftId`, {
+        method: 'post',
+        body: {
+          writeId: item.id
+        }
+      })
+      ElMessage.success('删除成功')
+      // getNotesList()
+    } catch (error) {}
+  })
+}
+
+onMounted(() => {
+  getNotesList()
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 1 - 1
src/components/Profile/Notes/Published/Item.vue

@@ -14,7 +14,7 @@
         <div class="min-h-70">
           <NuxtLink
             :to="`/yj/${data?.id}`"
-            class="cursor-pointer truncate break-all text-base font-bold text-black-3 transition-all hover:text-primary"
+            class="cursor-pointer truncate break-all text-base font-bold text-black-3 transition-all text-bold"
           >
             {{ data?.projectTitle }}
           </NuxtLink>

+ 74 - 0
src/components/Profile/Notes/Rejected/Item.vue

@@ -0,0 +1,74 @@
+<template>
+  <van-swipe-cell>
+    <div class="group flex space-x-10 p-10 transition-all mb-10 bg-[#fff] rounded-xl">
+      <img
+        :src="formatImgSrc(data?.tourismUrlsAfterConvert) || noteDraftCoverBg"
+        class="aspect-[4/3] h-109 shrink-0 object-cover"
+      />
+
+      <div class="flex w-0 flex-1 flex-col justify-between">
+        <div class="">
+          <div class="mt-15 line-clamp-2 break-all text-xl font-semibold text-black-3">
+            {{ data?.projectTitle || '未命名草稿' }}
+          </div>
+          <!-- <div class="mt-10 text-sm text-black-9">
+            {{ $dayjs(data?.updateTime).format('YYYY-MM-DD') }}
+          </div> -->
+          <div class="line-clamp-3 text-sm text-red-600">
+            审核未通过:{{ data?.reason || '内容可能涉嫌色情淫秽传播,请修改内容' }}
+          </div>
+        </div>
+        <!-- <div class="flex items-center space-x-10">
+          <div
+            @click="handleWrite"
+            class="flex cursor-pointer items-center space-x-5 rounded border border-[#f0f2f5] px-10 py-5 text-primary hover:bg-primary hover:text-white"
+          >
+            <span class="iconfont icon-edit-square" style="font-size: 14px"></span>
+            <span class="text-sm">编辑</span>
+          </div>
+          <div
+            @click="$emit('onDelete')"
+            class="iconfont icon-delete hidden cursor-pointer text-black-6 group-hover:block"
+            style="font-size: 18px"
+          ></div>
+        </div> -->
+      </div>
+    </div>
+    <template #right>
+      <van-button square text="编辑" @click="handleWrite" type="warning" class="delete-button" />
+      <van-button
+        square
+        text="删除"
+        @click="$emit('onDelete')"
+        type="danger"
+        class="delete-button"
+      />
+    </template>
+  </van-swipe-cell>
+</template>
+
+<script setup>
+import noteDraftCoverBg from '~/assets/img/note-create/note_draft_cover_bg.jpg'
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+function handleWrite() {
+  navigateTo({
+    path: '/note-create',
+    query: {
+      id: props?.data?.id
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.delete-button {
+  height: 100%;
+}
+</style>

+ 61 - 0
src/components/Profile/Notes/Rejected/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="min-h-400 pb-10">
+    <!--  v-loading="loading" -->
+    <!-- <ProfileNotesEmpty /> -->
+    <!-- v-if="!loading && !draftList.length" -->
+    <!-- v-else-if="draftList.length" -->
+    <!-- <div> -->
+    <div class="grid grid-cols-1">
+      <ProfileNotesRejectedItem />
+      <!-- v-for="item in draftList"
+        :key="item.id"
+        :data="item"
+        @on-delete="handleDelete(item)" -->
+    </div>
+    <!-- </div> -->
+  </div>
+</template>
+
+<script setup>
+const { loading, setLoading } = useLoading()
+loading.value = true
+
+const draftList = ref([])
+
+async function getNotesList() {
+  setLoading(true)
+  try {
+    const { data } = await request('/website/tourism/publishTravelNotes/getDraftList', {
+      query: {
+        pageNum: 1,
+        pageSize: 10000,
+        type: 2
+      }
+    })
+    draftList.value = data.dataList
+  } finally {
+    setLoading(false)
+  }
+}
+
+async function handleDelete(item) {
+  ElMessageBox.confirm('确定删除吗?', '提示', {}).then(async () => {
+    try {
+      await request(`/website/tourism/publishTravelNotes/removeByDraftId`, {
+        method: 'post',
+        body: {
+          writeId: item.id
+        }
+      })
+      ElMessage.success('删除成功')
+      getNotesList()
+    } catch (error) {}
+  })
+}
+
+onMounted(() => {
+  // getNotesList()
+})
+</script>
+
+<style lang="scss" scoped></style>

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

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

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

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

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

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

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

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

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

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

+ 14 - 0
src/layouts/default.vue

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

+ 12 - 6
src/pages/index.vue

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

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

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

+ 78 - 0
src/pages/note-create-start/index.client.vue

@@ -0,0 +1,78 @@
+<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"
+  >
+  <!-- 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"
+    >
+      <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>
+    </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"
+    >
+    <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"
+    >
+      <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
+      >
+    </div>
+  </div>
+
+  
+</template>
+
+<script setup>
+onMounted(() => {
+  getDraftList()
+})
+
+const draftTotal = ref(0)
+
+async function getDraftList() {
+  const { data } = await request(
+    '/website/tourism/publishTravelNotes/getDraftList',
+    {
+      query: {
+        pageNum: 1,
+        pageSize: 10000,
+        type: 0
+      }
+    }
+  )
+  draftTotal.value = data.totalCount
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 0 - 6
src/pages/note-create-start/index.vue

@@ -1,6 +0,0 @@
-<template>
-  <div>开始写游记</div>
-</template>
-
-<script setup></script>
-<style lang="scss" scoped></style>

+ 405 - 0
src/pages/note-create/index.client.vue

@@ -0,0 +1,405 @@
+<template>
+  <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"
+      v-model:endPlace="noteJson.endPlace"
+      v-model:role="noteJson.role"
+      v-model:travelMode="noteJson.travelMode"
+      v-model:averageCost="noteJson.averageCost"
+      v-model:recommendationRate="noteJson.recommendationRate"
+      v-model:travelNumber="noteJson.travelNumber"
+    />
+
+    
+    <div class="flex items-center pl-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
+      >
+     </van-field>
+    </van-cell-group>
+
+
+    <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">
+          <van-icon name="notes-o" size="24px" />
+          <p class="text-sm text-black-3">草稿</p>
+        </div>
+        <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>
+
+  
+    </div>
+    <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">
+              <template
+                v-for="(item, index) in noteJson.travelNotesContent"
+                :key="item.tmpId"
+              >
+                <CreateNoteInsertTitleSection
+                  v-if="item.type === defaultSectionTitle.type"
+                  :title="item.content"
+                  @onEdit="handleInsertOrEditTitle(index)"
+                  @onDelete="handleDeleteTitle(index)"
+                />
+                <template v-else-if="item.type === defaultSectionContent.type">
+                  <CreateNoteInsertContentSection
+                    v-model="item.content"
+                    @on-delete="handleDeleteContent(index)"
+                  />
+                </template>
+                <template v-else-if="item.type === defaultSectionImage.type">
+                  <CreateNoteInsertImageSection
+                    :url="item.content"
+                    @on-save-cover="handleSaveCover(item)"
+                    @on-delete="handleDeleteImage(index)"
+                  />
+                </template>
+              </template>
+            </VueDraggable> -->
+            <!-- <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>
+
+    <!-- <CreateNoteInsertTitleModal
+      v-model:visible="insertTilteOptions.show"
+      :title="insertTilteOptions.content"
+      @on-ok="handleInsertOrEditTitleOk"
+    /> -->
+    <!-- <CreateNoteInsertImageModal
+      v-model:visible="insertImageOptions.show"
+      @on-ok="handleInsertImageOk"
+    /> -->
+    <!-- <CreateNotePreviewModal
+      v-model:visible="previewOptions.show"
+      :data="noteJson"
+    /> -->
+    <!-- <CreateNoteUserInfoModal
+      v-model:visible="userInfoOptions.show"
+      @submit-ok="handleCollectUserInfoOk"
+    /> -->
+    <!-- <CreateNotePublishResultModal
+      v-model:visible="publishResultModalOptions.show"
+    /> -->
+  
+  </div>
+</template>
+
+<script setup>
+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
+
+const defaultSectionTitle = {
+  type: 'sectionTitle',
+  content: ''
+}
+const defaultSectionContent = {
+  type: 'sectionContent',
+  content: ''
+}
+const defaultSectionImage = {
+  type: 'image',
+  content: ''
+}
+
+const defaultNoteJson = {
+  travelNotesBanner: null,
+  projectTitle: null,
+  departureTime: null,
+  countTimes: null,
+  endPlace: null,
+  role: null,
+  travelMode: null,
+  averageCost: null,
+  recommendationRate: null,
+  travelNumber: null,
+  travelNotesContent: []
+}
+const noteJson = reactive(defaultNoteJson)
+
+watch(noteJson, () => {}, { deep: true })
+
+// const id = useRouteQuery('id')
+
+// watch(
+//   id,
+//   () => {
+//     // getNoteDetail()
+//   },
+//   { immediate: 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()
+//         })
+//       }
+//     })
+
+//     setLoading(false)
+//   } catch (error) {
+//     setLoading(false)
+//   }
+// }
+
+/************ 插入段落标题逻辑 ********** */
+
+const insertTilteOptions = reactive({
+  show: false,
+  content: null,
+  editIndex: 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 handleInsertOrEditTitleOk(newTitle) {
+  if (insertTilteOptions.editIndex === null) {
+    noteJson.travelNotesContent.push({
+      type: defaultSectionTitle.type,
+      content: newTitle,
+      tmpId: nanoid()
+    })
+  } else {
+    noteJson.travelNotesContent[insertTilteOptions.editIndex].content = newTitle
+  }
+}
+
+// 删除段落标题
+function handleDeleteTitle(index) {
+  noteJson.travelNotesContent.splice(index, 1)
+}
+
+/******************插入正文相关逻辑*******************/
+
+function handleInsertContent() {
+  noteJson.travelNotesContent.push(
+    cloneDeep({
+      ...defaultSectionContent,
+      tmpId: nanoid()
+    })
+  )
+}
+
+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 handleDeleteImage(index) {
+  if (noteJson.travelNotesContent[index].type === 'image') {
+    noteJson.travelNotesContent.splice(index, 1)
+  }
+}
+
+// 设为封面图
+function handleSaveCover(item) {
+  noteJson.travelNotesContent.forEach((e) => {
+    if (e.type === 'image') {
+      e.cover = 0
+    }
+    item.cover = 1
+  })
+  ElMessage.success('设置封面成功')
+}
+
+//保存为草稿
+// async function handleSaveDraft() {
+//   try {
+//     await request('/website/tourism/publishTravelNotes/saveDraft', {
+//       method: 'post',
+//       body: {
+//         ...noteJson,
+//         id: id.value
+//       }
+//     })
+//     ElMessage.success('草稿保存成功')
+//   } finally {
+//   }
+// }
+
+// 预览
+const previewOptions = reactive({
+  show: false
+})
+function handlePreview() {
+  previewOptions.show = true
+}
+
+// 收集个人信息
+const userInfoOptions = reactive({
+  show: false
+})
+
+function handleCollectUserInfoOk() {
+  // requestPublish()
+}
+
+const publishResultModalOptions = reactive({
+  show: false
+})
+
+// 发布
+async function handlePublish() {
+
+  if (!noteJson.travelNotesBanner) {
+    showNotify({
+    message: '请设置游记头图',
+    duration: 3000,
+    });
+    return
+  }
+  if (!noteJson.projectTitle) {
+    showNotify({
+    message: '请输入游记标题',
+    duration: 3000,
+    });
+    return
+  }
+  if (!noteJson.endPlace) {
+    showNotify({
+    message: '请选择目的地',
+    duration: 3000,
+    });
+    return
+  }
+  if (noteJson.travelNotesContent.length === 0) {
+    showNotify({
+    message: '游记内容不能为空',
+    duration: 3000,
+    });
+    return
+  }
+
+  const { data: isPerfect } = await request(
+    '/website/tourism/publishTravelNotes/isPerfect'
+  )
+  if (isPerfect === 0) {
+    // 需要收集个人信息
+    userInfoOptions.show = true
+  } else {
+    // requestPublish()
+  }
+}
+
+const publishLoading = ref(false)
+// async function requestPublish() {
+//   try {
+//     publishLoading.value = true
+//     await request('/website/tourism/publishTravelNotes/publishDraft', {
+//       method: 'post',
+//       body: {
+//         ...noteJson,
+//         id: id.value
+//       }
+//     })
+//     publishResultModalOptions.show = true
+//     publishLoading.value = false
+//   } catch (error) {
+//     publishLoading.value = false
+//   }
+// }
+</script>
+
+<style lang="scss" scoped></style>

+ 2 - 2
src/pages/profile/index/notes.vue

@@ -13,10 +13,10 @@
           <ProfileNotesAuditing />
         </div>
         <div v-else-if="tab === 'rejected'">
-          <!-- <ProfileNotesRejected /> -->
+          <ProfileNotesRejected />
         </div>
         <div v-else-if="tab === 'draft'">
-          <!-- <ProfileNotesDraft /> -->
+          <ProfileNotesDraft />
         </div>
       </van-pull-refresh>
     </div>

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

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

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

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

+ 5 - 5
src/utils/index.js

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

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov