Преглед изворни кода

Merge branch 'master' of gitee.com:tourist-mgt/xyy-mp

# Conflicts:
#	src/App.vue
#	src/pages.json
#	tailwind.config.js
songzhen пре 5 месеци
родитељ
комит
bf40625ba2

+ 4 - 0
src/App.vue

@@ -26,4 +26,8 @@ page {
   height: 100%;
   background-color: white;
 }
+/*每个页面公共css */
+::v-deep.rich-txt-img {
+  max-width: 100%;
+}
 </style>

+ 24 - 9
src/api/common.js

@@ -1,9 +1,9 @@
-import { request } from "@/utils/request.js";
+import { request } from '@/utils/request.js';
 
 export const getBannerList = (data) => {
   return request({
-    url: "/website/basic/bannerList",
-    method: "GET",
+    url: '/website/basic/bannerList',
+    method: 'GET',
     data,
     showLoading: false,
     showErrorToast: false,
@@ -11,16 +11,31 @@ export const getBannerList = (data) => {
 };
 
 export const getTravelProjectList = (data) => {
+  const query = `?${Object.keys(data)
+    .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
+    .join('&')}`;
   return request({
-    url: "/website/tourism/project/list",
-    method: "GET",
-    data,
+    url: `/website/tourism/project/list${query}`,
+    method: 'GET',
   });
 };
+
 export const getDirectoryList = (data) => {
+  const query = `?${Object.keys(data)
+    .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
+    .join('&')}`;
   return request({
-    url: "/website/basic/directoryList",
-    method: "GET",
-    data,
+    url: `/website/basic/directoryList${query}`,
+    method: 'GET',
+  });
+};
+
+export const getProjects = (data) => {
+  const query = `?${Object.keys(data)
+    .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
+    .join('&')}`;
+  return request({
+    url: `/website/basic/jobProject/list${query}`,
+    method: 'GET',
   });
 };

+ 8 - 0
src/api/labour.js

@@ -0,0 +1,8 @@
+import { request } from '@/utils/request.js';
+
+export const getLabourDetail = (id) => {
+  return request({
+    url: `website/basic/jobProject/detail?id=${id}`,
+    method: 'GET',
+  });
+};

+ 8 - 0
src/api/travel.js

@@ -0,0 +1,8 @@
+import { request } from '@/utils/request.js';
+
+export const getTravelDetail = (id) => {
+  return request({
+    url: `website/tourism/project/detail?id=${id}`,
+    method: 'GET',
+  });
+};

+ 9 - 0
src/components/Empty/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="w-full flex items-center justify-center min-h-200">
+    <div class="text-[#BFC8DB]">暂无数据</div>
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped></style>

+ 40 - 0
src/components/Home/Banner.vue

@@ -0,0 +1,40 @@
+<template>
+  <div>
+    <swiper class="swiper" circular autoplay :interval="5000">
+      <swiper-item
+        v-for="item in bannerList"
+        :key="item.id"
+        class="swiper-item"
+      >
+        <image :src="item.imgUrlsAfterConvert[0]" mode="aspectFill" />
+      </swiper-item>
+    </swiper>
+  </div>
+</template>
+
+<script setup>
+import { getBannerList } from '@/api/common';
+
+const bannerList = ref([]);
+async function requestBannerList() {
+  const { data } = await getBannerList({ belongTab: 10 });
+  bannerList.value = data.dataList;
+}
+
+onMounted(() => {
+  requestBannerList();
+});
+</script>
+
+<style lang="scss" scoped>
+.swiper {
+  height: 500rpx;
+  .swiper-item {
+    height: 100%;
+    image {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 61 - 0
src/components/Labour/JobItem.vue

@@ -0,0 +1,61 @@
+<template>
+  <view
+    class="flex space-x-10 py-15 border-b box-border"
+    @click="navigateToDetail"
+  >
+    <image
+      :src="itemData.jobUrlsAfterConvert[0]"
+      class="w-108 h-114 shrink"
+    />
+    <view class="flex-1 w-0">
+      <view class="text-base text-black-3 truncate font-semibold">
+        {{ itemData.jobTitle }}
+      </view>
+      <view class="text-sm text-[#999999] line-clamp-3 pt-5">
+        {{ itemData.remarks }}
+      </view>
+      <view class="flex items-center flex-wrap text-sm mt-5 gap-5 content-start">
+        <span
+          v-for="item in lableList"
+          :key="item"
+          class="bg-[#FFF5ED] text-[#555D6C] h-18 px-3 flex items-center justify-center"
+        >{{ item }}</span>
+      </view>
+      <view class="text-sm text-[#666666] mt-10 flex justify-end">
+        {{ itemData.updateTime.split(" ")[0] }}发布
+      </view>
+      <view class="flex justify-end">
+        <span class="text-xl font-semibold text-[#FF3D00]">
+          {{ itemData.salaryCap }}-{{ itemData.salaryFloor }}{{ itemData.salaryUnit }}
+        </span>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => {},
+  },
+});
+
+const lableList = computed(() => {
+  const tmpList = props.itemData.jobLabel?.split("&") ?? [];
+  return [
+    `招聘人数:${props.itemData.jobPeople}人`,
+    ...tmpList,
+    `${props.itemData.jobArea}`,
+  ];
+});
+
+// 导航到详情页
+const navigateToDetail = () => {
+  uni.navigateTo({
+    url: `/pages/labour/detail?id=${props.itemData.id}`
+  });
+};
+</script>

+ 53 - 0
src/components/Travel/FirstLevelDirectoryMenu/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="flex flex-col w-80 bg-[#F6F8FA] box-border">
+    <div
+      v-for="item in menuData"
+      :key="item.id"
+      @click="handleChange(item)"
+      class="h-40 flex items-center rounded-r-lg justify-start pl-10 transition-all text-base"
+      :class="[
+        currentMenu.id === item.id
+          ? 'bg-primary text-white'
+          : 'bg-transparent text-black-3',
+      ]"
+    >
+      {{ item.menuName }}
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { getDirectoryList } from '@/api/common';
+const emit = defineEmits(['change']);
+
+const dataList = ref([]);
+async function requestDirectoryList(parentId) {
+  const { data } = await getDirectoryList({parentId});
+  dataList.value = data.dataList;
+}
+
+const firstMenu = {
+  menuName: '全部',
+  id: null,
+};
+
+onLoad((option) => {
+  requestDirectoryList(option.parentId || 0);
+});
+
+const menuData = computed(() => {
+  return [
+    firstMenu,
+    ...(dataList.value.filter((x) => x.id != '16' && x.id != '10') ?? []),
+  ];
+});
+
+const currentMenu = ref(firstMenu);
+
+function handleChange(item) {
+  currentMenu.value = item;
+  emit('change', item);
+}
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,65 @@
+<template>
+  <div class="flex py-10 border-b space-x-10" @click="navigateToDetail">
+    <image
+      :src="itemData.homeHotPicturesAfterConvert ? itemData.homeHotPicturesAfterConvert[0] : defaultImage"
+      mode="widthFix"
+      class="w-108 h-114 shrink rounded-lg overflow-hidden"
+    />
+    <div class="ml-10 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>{{ itemData.startPlace }}出发</span>
+          <div class="h-10 w-1 bg-[#f3f3f3]"></div>
+          <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>
+  </div>
+</template>
+
+<script setup>
+import defaultImage from '../../../static/empty.png';
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => {},
+  },
+});
+
+const lableList = computed(() => {
+  return props.itemData.projectLabel?.split("&") ?? [];
+});
+
+// 导航到详情页
+const navigateToDetail = () => {
+  uni.navigateTo({
+    url: `/pages/travel/detail?id=${props.itemData.id}`
+  });
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 42 - 4
src/pages.json

@@ -72,6 +72,38 @@
       }
     },
     {
+      "path": "pages/travel/index",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "境外旅游"
+      }
+    },
+    {
+      "path": "pages/travel/list",
+      "style": {
+        "navigationBarTitleText": "搜索列表"
+      }
+    },
+    {
+      "path": "pages/labour/index",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "境外旅游"
+      }
+    },
+    {
+      "path": "pages/travel/detail",
+      "style": {
+        "navigationBarTitleText": "产品详情"
+      }
+    },
+    {
+      "path": "pages/labour/detail",
+      "style": {
+        "navigationBarTitleText": "出国劳务"
+      }
+    },
+    {
       "path": "pages/profile/index",
       "style": {
         "navigationBarTitleText": "我的"
@@ -91,10 +123,16 @@
         "text": "首页"
       },
       {
-        "pagePath": "pages/profile/index",
-        "iconPath": "static/img/tabbar_profile.png",
-        "selectedIconPath": "static/img/tabbar_profile_selected.png",
-        "text": "我的"
+        "pagePath": "pages/travel/index",
+        "iconPath": "static/img/tabbar_travel.png",
+        "selectedIconPath": "static/img/tabbar_travel_selected.png",
+        "text": "境外旅游"
+      },
+      {
+        "pagePath": "pages/labour/index",
+        "iconPath": "static/img/tabbar_labour.png",
+        "selectedIconPath": "static/img/tabbar_labour_selected.png",
+        "text": "出国劳务"
       }
     ]
   },

+ 9 - 2
src/pages/home/comps/HotDestination.vue

@@ -2,10 +2,10 @@
   <div class="hot-destination">
     <div class="header">
       <text class="title">热门目的地</text>
-      <text class="more">查看更多</text>
+      <!-- <text class="more">查看更多</text> -->
     </div>
     <div class="content">
-      <div class="wrap" v-for="item in data" :key="item.id">
+      <div class="wrap" v-for="item in data" :key="item.id" @click="navigateToDetail(item)">
         <image :src="item.hotPictureUrlsAfterConvert[0]" mode="scaleToFill" />
         <text>
           {{ item.menuName }}
@@ -22,6 +22,13 @@ defineProps({
     default: () => [],
   },
 });
+
+// 导航到详情页
+const navigateToDetail = (item) => {
+  uni.navigateTo({
+    url: `/pages/travel/list?id=${item.id}&menuName=${item.menuName}`
+  });
+};
 </script>
 
 <style lang="scss" scoped>

+ 10 - 2
src/pages/home/comps/HotProjects.vue

@@ -2,10 +2,10 @@
   <div class="hot-project">
     <div class="header">
       <text class="title">热门项目</text>
-      <text class="more">查看更多</text>
+      <!-- <text class="more">查看更多</text> -->
     </div>
     <div class="content">
-      <div class="wrap" v-for="item in data" :key="item.id">
+      <div class="wrap" v-for="item in data" :key="item.id" @click="handleToTravel(item)">
         <image :src="item.homeHotPicturesAfterConvert[0]" mode="aspectFill" />
         <div class="text-wrap">
           {{ item.shortTitle }}
@@ -22,6 +22,11 @@ defineProps({
     default: () => [],
   },
 });
+function handleToTravel(item) {
+  uni.navigateTo({
+    url: `/pages/travel/detail?id=${item.id}`,
+  });
+}
 </script>
 
 <style lang="scss" scoped>
@@ -67,6 +72,9 @@ defineProps({
         color: #fff;
         font-size: 14px;
         font-weight: bold;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
       }
     }
   }

+ 3 - 3
src/pages/home/comps/Menu.vue

@@ -1,15 +1,15 @@
 <template>
   <div class="menu">
     <div class="item" @click="handleToFood">
-      <image src="@/static/logo.png" mode="scaleToFill" />
+      <image src="@/static/img/home_food.png" mode="scaleToFill" />
       <text>境外美食</text>
     </div>
     <div class="item" @click="handleToTravel">
-      <image src="@/static/logo.png" mode="scaleToFill" />
+      <image src="@/static/img/home_travel.png" mode="scaleToFill" />
       <text>境外旅游</text>
     </div>
     <div class="item" @click="handleToLabour">
-      <image src="@/static/logo.png" mode="scaleToFill" />
+      <image src="@/static/img/home_labour.png" mode="scaleToFill" />
       <text>出国劳务</text>
     </div>
   </div>

+ 5 - 5
src/pages/home/index.vue

@@ -10,8 +10,8 @@
       </swiper-item>
     </swiper>
     <SizedBox height="20" />
-    <Menu />
-    <SizedBox height="20" />
+    <!-- <Menu /> -->
+    <!-- <SizedBox height="20" /> -->
     <HotProjects v-if="hotPojectList.length" :data="hotPojectList" />
     <SizedBox height="20" />
     <HotDestination
@@ -28,9 +28,9 @@ import {
   getTravelProjectList,
   getDirectoryList,
 } from "@/api/common";
-import { Menu } from "./comps/Menu.vue";
-import { HotProjects } from "./comps/HotProjects.vue";
-import { HotDestination } from "./comps/HotDestination.vue";
+import Menu from "./comps/Menu.vue";
+import HotProjects from "./comps/HotProjects.vue";
+import HotDestination from "./comps/HotDestination.vue";
 
 const checked = ref(true);
 

+ 91 - 0
src/pages/labour/detail.vue

@@ -0,0 +1,91 @@
+<template>
+  <div>
+    <div class="px-15 pt-10 pb-100">
+      <image
+        :src="detailData.jobUrlsAfterConvert[0]"
+        mode="widthFix"
+      />
+      <div class="flex items-center mt-10 justify-between">
+        <span class="text-base text-black-3 font-semibold">{{ detailData.jobTitle }}</span>
+        <span class="text-xl font-semibold text-[#FF3D00] shrink"
+          >{{ detailData.salaryCap }}-{{ detailData.salaryFloor }}{{ detailData.salaryUnit }}</span
+        >
+      </div>
+      <div class="flex items-center flex-wrap text-sm mt-5 gap-5 content-start">
+        <span
+          v-for="item in lableList"
+          :key="item"
+          class="bg-[#FFF5ED] text-[#555D6C] h-18 px-3 flex items-center justify-center"
+          >{{ item }}</span
+        >
+      </div>
+      <div class="mt-15 text-xl font-semibold text-black-3">职位详情</div>
+      <div v-html="detailData.jobContent?.content" class="mt-10 text-black-3 text-base"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { getLabourDetail } from '@/api/labour';
+import { useRoute } from 'vue-router';
+import { computed } from 'vue';
+const router = useRoute();
+const lableList = computed(() => {
+  const tmpList = detailData.value.jobLabel?.split('&') ?? [];
+  return [`招聘人数:${detailData.value.jobPeople}人`, ...tmpList, `${detailData.value.jobArea}`];
+});
+onLoad((options) => {
+  requestLabourDetail(options.id);
+});
+
+const detailData = ref({});
+async function requestLabourDetail(id) {
+  const { data } = await getLabourDetail(id);
+  detailData.value = data;
+}
+
+function richTxtFilter(content) {
+  if (!content) return '';
+  let val = content.replace(/<img/gi, '<img class="rich-txt-img" ');
+  return val;
+}
+</script>
+
+<style lang="scss" scoped>
+.project-detail {
+  padding: 10px 10px 30px 10px;
+}
+.project-title {
+  font-size: 20px;
+  line-height: 30px;
+  color: #333;
+  font-weight: bold;
+}
+.project-label {
+  border: 1px solid #fd9a00;
+  color: #fd9a00;
+  padding: 0 8px;
+  display: inline-block;
+  height: 22px;
+  line-height: 22px;
+  font-size: 14px;
+  margin-top: 15px;
+  & + .project-label {
+    margin-left: 10px;
+  }
+}
+.project-concat {
+  margin-top: 10px;
+  color: #666666b2;
+  div + div {
+    margin-top: 10px;
+  }
+}
+::v-deep rich-text {
+  margin-top: 15px;
+}
+
+image {
+  width: 100%;
+}
+</style>

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

@@ -0,0 +1,182 @@
+<template>
+  <div>
+    <!-- <Navbar title="出国劳务" /> -->
+
+    <div class="px-15 pt-15">
+      <swiper class="h-150 rounded-lg overflow-hidden" circular autoplay :interval="5000">
+        <swiper-item v-for="item in bannerList" :key="item.id" class="h-full w-full">
+          <image
+            class="object-cover w-full h-full"
+            :src="item.imgUrlsAfterConvert[0]"
+            mode="aspectFill"
+          />
+        </swiper-item>
+      </swiper>
+
+      <div class="pt-15 flex justify-between items-center">
+        <input
+          v-model="searchQuery.searchString"
+          placeholder="请输入搜索关键词"
+          shape="round"
+          @confirm="onSearch"
+          confirm-type="search"
+          class="flex-1"
+        />
+        <div style="color: rgb(170 176 191)" class="w-40 flex items-center justify-center" @click="handleFilter">
+          <!-- <icon name="filter-o" color="#fe8e2c" size="25" /> -->
+          筛选
+        </div>
+      </div>
+
+      <scroll-view
+        v-if="dataList.length"
+        type="list"
+        @scrolltolower="loadMore"
+        class="scroll-container"
+        scroll-y="true"
+      >
+        <LabourJobItem v-for="item in dataList" :key="item.id" :item-data="item"> </LabourJobItem>
+      </scroll-view>
+      <Empty v-else-if="!dataList.length && !loading" />
+    </div>
+    <!-- <Tabbar /> -->
+
+    <uni-popup ref="popupRef" background-color="#fff" :style="{ width: '80%', height: '100%' }">
+      <div class="flex flex-col justify-between h-full">
+        <div class="flex-1 pt-40 px-15">
+          <div>选择区域</div>
+          <div class="grid grid-cols-2 gap-15 pt-20">
+            <div
+              v-for="item in areaList"
+              :key="item.id"
+              @click="handleClick(item)"
+              class="h-36 flex items-center rounded-sm justify-center text-sm transition-all"
+              :class="[
+                currentArea.id === item.id
+                  ? 'bg-primary text-white'
+                  : 'bg-[#f2f4f8] text-[#72809F]',
+              ]"
+            >
+              {{ item.menuName }}
+            </div>
+          </div>
+        </div>
+        <div>
+          <div class="h-60 flex items-center bg-white justify-center border-t space-x-10 px-20">
+            <button class="w-full mr-10 h-28" plain @click="handleReset">重置</button>
+            <button class="w-full h-28" type="primary" @click="handleConfirm">确定</button>
+          </div>
+          <div class="safe-area-bottom"></div>
+        </div>
+      </div>
+    </uni-popup>
+  </div>
+</template>
+
+<script setup>
+import LabourJobItem from '@/components/Labour/JobItem.vue';
+import { getBannerList, getDirectoryList, getProjects } from '@/api/common';
+const bannerList = ref([]);
+async function requestBannerList() {
+  const { data } = await getBannerList({ belongTab: 10 });
+  bannerList.value = data.dataList;
+}
+
+const defaultFirstArea = {
+  id: 16,
+  menuName: '全部',
+};
+
+const searchQuery = reactive({
+  belongTab: defaultFirstArea.id,
+  searchString: '',
+  pageNum: 1,
+  pageSize: 10,
+});
+
+const dataList = ref([]);
+const loading = ref(true);
+const finished = ref(false);
+async function requestProjects() {
+  const { data } = await getProjects(searchQuery);
+  dataList.value = dataList.value.concat(data.dataList);
+  loading.value = false;
+  if (dataList.value.length >= data.totalCount) {
+    finished.value = true;
+  }
+}
+
+function loadMore() {
+  searchQuery.pageNum++;
+  requestProjects();
+}
+
+function onSearch() {
+  searchQuery.pageNum = 1;
+  dataList.value = [];
+  finished.value = false;
+  requestProjects();
+}
+
+onMounted(() => {
+  requestBannerList();
+  requestProjects();
+  getAreaList();
+});
+
+// 创建一个 ref 对象
+const popupRef = ref(null);
+// 侧边过滤逻辑
+const showFilter = ref(false);
+function handleFilter() {
+  popupRef.value.open('right');
+  showFilter.value = !showFilter.value;
+}
+const areaList = ref([]);
+const currentArea = ref(defaultFirstArea);
+async function getAreaList() {
+  const { data } = await getDirectoryList({ parentId: 16 });
+  areaList.value = [defaultFirstArea, ...data.dataList];
+}
+
+function handleClick(item) {
+  currentArea.value = item;
+}
+
+function handleReset() {
+  currentArea.value = defaultFirstArea;
+}
+function handleConfirm() {
+  searchQuery.belongTab = currentArea.value.id;
+  showFilter.value = false;
+  reSearch();
+}
+
+function reSearch() {
+  searchQuery.pageNum = 1;
+  dataList.value = [];
+  finished.value = false;
+  requestProjects();
+}
+</script>
+
+<style lang="scss" scoped>
+.safe-area-bottom {
+  // height: 50px;
+}
+.scroll-container {
+  height: calc(100vh - 200px);
+}
+button {
+  line-height: 28px;
+}
+input {
+  color: rgb(50, 50, 51);
+  border: 1px solid rgb(226 230 239);
+  background-color: rgb(226 230 239);
+  border-radius: 10px;
+  padding-left: 10px;
+  font-size: 14px;
+  height: 24px;
+}
+</style>

+ 96 - 0
src/pages/travel/detail.vue

@@ -0,0 +1,96 @@
+<template>
+  <div>
+    <div v-if="detailData.travelNotesBannerAfterConvert">
+      <image
+        :src="
+          detailData.travelNotesBannerAfterConvert?.length
+            ? detailData.travelNotesBannerAfterConvert[0]
+            : ''
+        "
+        mode="widthFix"
+      />
+      <div class="project-detail">
+        <div class="project-title">
+          {{ detailData.projectTitle }}
+        </div>
+        <div>
+          <div class="project-label" v-for="item in lableList" :key="item">
+            {{ item }}
+          </div>
+        </div>
+        <div class="project-concat">
+          <div>出行天数 {{ detailData.countTimes }}</div>
+          <div v-if="detailData.contactDescription">
+            专业一站式导游服务请联系{{ detailData.contactDescription }}
+          </div>
+        </div>
+        <rich-text class="richtext" :nodes="richTxtFilter(detailData.tourismContent.content)"></rich-text>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { getTravelDetail } from '@/api/travel';
+import { useRoute } from 'vue-router';
+import { computed } from 'vue';
+const router = useRoute();
+const lableList = computed(() => {
+  return detailData.value?.projectLabel?.split('&') ?? [];
+});
+onLoad((options) => {
+  requestTravelDetail(options.id);
+});
+
+const detailData = ref({});
+async function requestTravelDetail(id) {
+  const { data } = await getTravelDetail(id);
+  detailData.value = data;
+}
+
+function richTxtFilter(content) {
+    if (!content) return '';
+    let val = content.replace(/<img/gi, '<img class="rich-txt-img" ');
+    return val;
+}
+
+</script>
+
+<style lang="scss" scoped>
+.project-detail {
+  padding: 10px 10px 30px 10px;
+}
+.project-title {
+  font-size: 20px;
+  line-height: 30px;
+  color: #333;
+  font-weight: bold;
+}
+.project-label {
+  border: 1px solid #fd9a00;
+  color: #fd9a00;
+  padding: 0 8px;
+  display: inline-block;
+  height: 22px;
+  line-height: 22px;
+  font-size: 14px;
+  margin-top: 15px;
+  & + .project-label {
+    margin-left: 10px;
+  }
+}
+.project-concat {
+  margin-top: 10px;
+  color: #666666b2;
+  div + div {
+    margin-top: 10px;
+  }
+}
+::v-deep rich-text {
+  margin-top: 15px;
+}
+
+image {
+  width: 100%;
+}
+</style>

+ 81 - 0
src/pages/travel/index.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="flex flex-col h-screen-reduced">
+    <HomeBanner class="h-250 shrink" />
+    <div
+      class="z-[999] flex-1 flex overflow-auto pt-20 -mt-15 bg-white rounded-t-2xl"
+    >
+      <TravelFirstLevelDirectoryMenu
+        class="shrink"
+        @change="handleMenuChange"
+      />
+      <div class="flex-1 pt-10 flex flex-col">
+        <div class="text-black-3 pl-10 text-base font-semibold shrink">
+          热门目的地
+        </div>
+        <div
+          class="flex flex-wrap px-10 items-start gap-x-10 gap-y-15 mt-15 pb-30 flex-1 content-start overflow-auto"
+        >
+          <div
+            v-for="item in rightData"
+            :key="item"
+            @click="navigateToDetail(item)"
+            class="flex flex-col w-83 aspect-1 space-y-8 art items-center"
+          >
+            <image
+              :src="
+                item.hotPictureUrlsAfterConvert?.length
+                  ? item.hotPictureUrlsAfterConvert[0]
+                  : ''
+              "
+              class="empty-bc aspect-[84/63] h-62 w-full object-cover rounded-lg overflow-hidden"
+            />
+            <span class="text-sm text-black-3 truncate">{{
+              item.menuName
+            }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import HomeBanner from '../../components/Home/Banner.vue';
+import TravelFirstLevelDirectoryMenu from '../../components/Travel/FirstLevelDirectoryMenu/index.vue';
+import { getDirectoryList } from '@/api/common';
+
+function handleMenuChange(menu) {
+  getRightData(menu.id);
+}
+
+const rightData = ref([]);
+
+async function getRightData(id) {
+  const { data } = await getDirectoryList({
+    parentId: id ?? '',
+    isAll: !id ? 1 : 0,
+  });
+  rightData.value = data.dataList;
+}
+
+onLoad((option) => {
+  getRightData(option.parentId);
+});
+
+const navigateToDetail = (item) => {
+  uni.navigateTo({
+    url: `/pages/travel/list?id=${item.id}&menuName=${item.menuName}`,
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.h-screen-reduced {
+  height: calc(100vh - 50px);
+}
+.empty-bc {
+  background-image: url('@/static/empty.png');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+}
+</style>

+ 87 - 0
src/pages/travel/list.vue

@@ -0,0 +1,87 @@
+<template>
+  <div>
+    <div class="px-10 pt-20">
+      <input
+        v-model="searchStr"
+        placeholder="请输入搜索关键词"
+        confirm-type="search"
+        @confirm="onSearch"
+      />
+      <scroll-view
+        v-if="dataList.length"
+        type="list"
+        @scrolltolower="loadMore"
+        class="scroll-container"
+        scroll-y="true"
+      >
+        <TravelProjectItem v-for="item in dataList" :key="item.id" :item-data="item" />
+      </scroll-view>
+      <Empty v-else-if="!dataList.length && !loading" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import TravelProjectItem from '@/components/Travel/ProjectItem/index.vue';
+import Empty from '@/components/Empty/index.vue';
+import { getTravelProjectList } from '@/api/common';
+
+const id = ref(0);
+const searchStr = ref('');
+const dataList = ref([]);
+const loading = ref(false);
+const finished = ref(false);
+const pageNum = ref(1);
+const pageSize = ref(10);
+async function getProjects() {
+  const { data } = await getTravelProjectList({
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    belongTab: id.value,
+    searchString: searchStr.value,
+  });
+  dataList.value = dataList.value.concat(data.dataList);
+  loading.value = false;
+  if (dataList.value.length >= data.totalCount) {
+    finished.value = true;
+  } else {
+    finished.value = false;
+  }
+}
+
+function loadMore() {
+  pageNum.value++;
+  getProjects();
+}
+
+function onSearch() {
+  pageNum.value = 1;
+  dataList.value = [];
+  // finished.value = false;
+  getProjects();
+}
+
+onLoad((options) => {
+  id.value = options.id;
+  wx.setNavigationBarTitle({
+    title: options.menuName,
+    success: function (res) {
+    },
+    fail: function (err) {
+    },
+  });
+  getProjects();
+});
+</script>
+
+<style lang="scss" scoped>
+input {
+  color: rgb(50, 50, 51);
+  border: 1px solid rgb(226 230 239);
+  background-color: rgb(226 230 239);
+  border-radius: 10px;
+  padding-left: 10px;
+  font-size: 14px;
+  height: 24px;
+}
+</style>

BIN
src/static/empty.png


BIN
src/static/img/home_food.png


BIN
src/static/img/home_labour.png


BIN
src/static/img/home_travel.png


BIN
src/static/img/tabbar_home.png


BIN
src/static/img/tabbar_home_selected.png


BIN
src/static/img/tabbar_labour.png


BIN
src/static/img/tabbar_labour_selected.png


BIN
src/static/img/tabbar_travel.png


BIN
src/static/img/tabbar_travel_selected.png