Bladeren bron

feat:1.新增写游记

suwenjiang 3 maanden geleden
bovenliggende
commit
a5be4cd89e

+ 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

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>

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

@@ -12,7 +12,7 @@
         </div>
         <div class="flex items-center space-x-10">
           <NuxtLink
-            to="/note-create"
+            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>

+ 7 - 6
src/components/Profile/Notes/Draft/index.vue

@@ -1,21 +1,22 @@
 <template>
   <div class="min-h-400 pb-10">
     <!-- v-loading="loading" -->
-    <!-- <ProfileNotesEmpty v-if="!loading && !draftList.length" /> -->
+    <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]">
-          【您好,您还有 1000{{ draftList?.length }}篇草稿没有完成,我们期待您的大作哦~】
+          【您好,您还有{{ draftList?.length }}篇草稿没有完成,我们期待您的大作哦~】
         </p>
       </div>
       <div class="grid grid-cols-1">
-        <ProfileNotesDraftItem @on-delete="handleDelete(item)" />
-        <!-- v-for="item in draftList"
+        <ProfileNotesDraftItem 
+          v-for="item in draftList"
           :key="item.id"
           :data="item"
-          @on-delete="handleDelete(item)" -->
+          @on-delete="handleDelete(item)"  />
+     
       </div>
     </div>
   </div>
@@ -59,7 +60,7 @@ async function handleDelete(item) {
 }
 
 onMounted(() => {
-  // getNotesList()
+  getNotesList()
 })
 </script>
 

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