1. 详情页剩余部分
1.1 周边地图
地图使用步骤
- 首先在百度开放平台注册为开发者,获取AK
- 在html引入百度地图API文件
<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=您的密钥"></script>
//AK:ArMTS9ea6ivqfI5GKkh4p5AqDe7BlmMM
// <script type="text/javascript" src="https://api.map.baidu.com/getscript?v=3.0&type=webgl&ak=ArMTS9ea6ivqfI5GKkh4p5AqDe7BlmMM"></script>
- 初始化地图逻辑
首先创建地图实例,之后用一个Point坐标点和缩放级别来初始化地图
var map = new BMapGL.Map("container"); // 创建地图实例
var point = new BMapGL.Point(116.404, 39.915); // 创建点坐标
map.centerAndZoom(point, 15); // 初始化地图,设置中心点坐标和地图级别
detail使用detail-map组件
import DetailMap from './components/detail_07-map.vue'
<!-- 位置周边 -->
<template v-if="mainPart?.dynamicModule?.positionModule">
<detail-map :map="mainPart.dynamicModule.positionModule"></detail-map>
</template>
- detail-map组件
<script setup>
import { ref, onMounted } from 'vue'
import detailSection from '@/components/detail-section/detail-section.vue';
const props = defineProps({
map: {
type: Object,
default: () => ({})
}
})
const mapRef = ref()
onMounted(() => {
const map = new BMapGL.Map(mapRef.value); // 创建地图实例
const point = new BMapGL.Point(props.map.longitude, props.map.latitude); // 创建点坐标
map.centerAndZoom(point, 15); // 初始化地图,设置中心点坐标和地图级别
const marker = new BMapGL.Marker(point)
map.addOverlay(marker)
})
</script>
<template>
<div class="map">
<detail-section title="位置周边" more="查看更多周边信息">
<div class="baidu" ref="mapRef">百度地图</div>
</detail-section>
</div>
</template>
<style lang="less" scoped>
.map {
.baidu {
height: 300px;
border-radius: 6px;
}
}
</style>
1.2 价格说明及结尾
- detail页面引入
import DetailIntroduction from './components/detail_08-introduction.vue'
<!-- 价格说明 -->
<template v-if="mainPart">
<detail-introduction :introduction="mainPart"></detail-introduction>
</template>
- detail-introduction组件
<script setup>
defineProps({
introduction: {
type: Object,
default: () => ({})
}
})
</script>
<template>
<div class="intro">
<div class="top">
<div class="title">{{ introduction.introductionModule.title }}</div>
<div class="content">
{{ introduction.introductionModule.introduction }}
</div>
</div>
</div>
<div class="footer">
<div class="ensure">
<img :src="introduction.ensureModule.icon" alt="">
</div>
<div class="ensure-bottom">
<div class="business-license">{{ introduction.businessLicenseModuleTitle }}</div>
<div class="info-txt">民宿预定频道由本平台提供服务</div>
</div>
</div>
</template>
<style lang="less" scoped>
.intro {
border-top: 10px solid #f2f3f4;
background-color: #fff;
padding: 16px 12px;
.title {
font-size: 14px;
color: #333;
line-height: 17px;
font-weight: 600;
}
.content {
font-size: 12px;
color: #666;
margin-top: 10px;
line-height: 15px;
}
}
.footer {
background-color: #f2f3f4;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.ensure {
display: flex;
align-items: center;
justify-content: center;
width: 130px;
margin-top: 30px;
padding: 10px 0;
img {
width: 100%;
}
}
.ensure-bottom {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 12px;
color: #7688a7;
line-height: 12px;
margin-bottom: 18px;
.business-license {
margin-top: 4px;
font-weight: 500;
margin-bottom: 9px;
}
}
}
</style>
2. 详情页tabControl
2.1 useScroll的完善监听滚动
优化了document窗口滚动或元素滚动
import { onMounted, onUnmounted, ref } from "vue"
import { throttle } from 'underscore'
export default function useScroll(elRef) {
let el = window
const isReachBottom = ref(false)
const scrollTop = ref(0)
const scrollHeight = ref(0)
const clientHeight = ref(0)
// 节流
const scrollListenerHandler = throttle(() => {
console.log('正在进行滚动');
if(el === window){
clientHeight.value = document.documentElement.clientHeight
scrollTop.value = document.documentElement.scrollTop
scrollHeight.value = document.documentElement.scrollHeight
} else {
clientHeight.value = el.clientHeight
scrollTop.value = el.scrollTop
scrollHeight.value = el.scrollHeight
}
// 滚动到底部,自动加载更多
// console.log("监听到滚动");
if (Math.ceil(scrollTop.value + clientHeight.value) >= scrollHeight.value) {
// 滚动到底部
console.log("滚动到底部了");
isReachBottom.value = true
}
}, 100)
onMounted(() => {
if(elRef) el = elRef.value
el.addEventListener("scroll", scrollListenerHandler)
})
onUnmounted(() => {
el.removeEventListener("scroll", scrollListenerHandler)
})
return { isReachBottom, scrollTop, scrollHeight, clientHeight }
}
2.2 实现点击后滚动到对应的位置
1.控制tabControl的显示隐藏
2.对要监听的组件实例的处理
3.自动设置title,以后想要新加tab,只需要在组件上加上属性name和ref即可
4.点击定位到具体位置
5.滚动中监听到达高度切换tab
- detail页面
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { getDetailInfos } from '@/services'
import { ref, computed, reactive, onMounted } from 'vue';
import DetailSwipe from './components/detail_01-swipe.vue'
import DetailInfos from './components/detail_02-infos.vue'
import DetailFacility from './components/detail_03-facility.vue'
import DetailLandlord from './components/detail_04-landlord.vue'
import DetailComment from './components/detail_05-comment.vue'
import DetailNotice from './components/detail_06-notice.vue'
import DetailMap from './components/detail_07-map.vue'
import DetailIntroduction from './components/detail_08-introduction.vue'
import useScroll from '@/hooks/useScroll';
const router = useRouter()
const route = useRoute()
// 监听返回按钮的点击
const onClickLeft = () => {
router.back()
}
// 发送网络请求
const houseId = route.params.id
const detailInfos = ref({})
const mainPart = computed(() => detailInfos.value?.mainPart)
getDetailInfos(houseId).then(res => {
if (!res.data) {
console.log("小主,数据请求超时,请重新刷新~~");
}
detailInfos.value = res.data
})
//=> tabControl的相关操作
// 1.控制tabControl的显示隐藏
const detailRef = ref()
const { scrollTop } = useScroll(detailRef)
const showTabControl = computed(() => {
return scrollTop.value >= 300
})
// 2. 定义
const TabActive = ref(0)
const tabs = ref()
// const names = ["infos", "facility", "landlord", "comment", "notice", "map"]
// 3.对要监听的组件实例的处理
const sectionEles = ref({})
const getSectionRef = (value) => {
// 点击返回,组件被卸载的时候也会执行这个方法,null的时候会报错
if(!value) return;
const name = value.$el.getAttribute("name")
sectionEles.value[name] = value.$el
}
// 自动设置title,以后想要新加tab,只需要在组件上加上属性name和ref即可
// const titles = ["概述", "设施", "房东", "点评", "须知", "周边"]
const titles = computed(() => {
return Object.keys(sectionEles.value)
})
// 4.点击定位到具体位置
const onClickTab = (value) => {
const title = value?.title
let top = sectionEles.value[title]?.offsetTop
if(title !== "概述") {
top = top - 44
}
detailRef.value.scrollTo({
top: top,
behavior: "smooth"
})
}
// 5.滚动中监听到达高度切换tab
</script>
<template>
<div class="detail top-page" ref="detailRef">
<!-- tabControl -->
<div class="tabControl">
<van-tabs
v-model:active="TabActive"
sticky
title-active-color="#ff9645"
ref="tabs"
v-if="showTabControl"
@click-tab="onClickTab"
>
<van-tab v-for="(item, index) in titles" :key="index" :title="item" ></van-tab>
</van-tabs>
</div>
<!-- 导航栏 -->
<van-nav-bar title="房屋详情" left-text="旅途" left-arrow @click-left="onClickLeft" />
<div class="main" v-if="mainPart" v-memo="[mainPart]">
<!-- 轮播图 -->
<detail-swipe :swipe-data="mainPart.topModule.housePicture.housePics"></detail-swipe>
<!-- 详情 -->
<detail-infos name="概述" :ref="getSectionRef" :top-infos="mainPart.topModule"></detail-infos>
<!-- 房屋设施 -->
<detail-facility name="设施" :ref="getSectionRef"
:house-facility="mainPart.dynamicModule.facilityModule.houseFacility"></detail-facility>
<!-- 房东介绍 -->
<detail-landlord name="房东" :ref="getSectionRef" :landlord="mainPart.dynamicModule.landlordModule"
ref="landlordRef"></detail-landlord>
<!-- 房客点评 -->
<detail-comment name="点评" :ref="getSectionRef" :comment="mainPart.dynamicModule.commentModule"></detail-comment>
<!-- 预定须知 -->
<detail-notice name="须知" :ref="getSectionRef" :notice="mainPart.dynamicModule.rulesModule"></detail-notice>
<!-- 位置周边 -->
<detail-map name="周边" :ref="getSectionRef" :map="mainPart.dynamicModule.positionModule"></detail-map>
<!-- 价格说明 -->
<detail-introduction :introduction="mainPart"></detail-introduction>
</div>
</div>
</template>
<style lang="less" scoped>
.tabControl {
.van-tabs {
position: fixed;
z-index: 20;
}
:deep(.van-tabs__wrap) {
width: 415px;
}
}
.detail {
position: relative;
}
</style>
2.3 页面滚动匹配tabControl索引
/*
5.页面滚动,滚动到一定的位置时,显示正确的tabControl的索引(标题)
+ 监听滚动的位置 scrollTop
+ 利用scrollTop去匹配正确的位置
+ 变化的scrollTop,newValue 在数组中匹配第一个大于的值,找到跳出循环
+ 找到的index -1即可
+ 最后一个匹配不到,可以默认索引为最后一个
+ 第一个索引为-1,可以不考虑,因为在第一次的时候scrollTop还没有变化,依然是最初设置的0
*/
watch(scrollTop, (newValue) => {
// 1.获取所有区域的offsetTop数组
const elem = Object.values(sectionEles.value)
const offsetValues = elem.map(elem => elem.offsetTop)
// 2.根据newValue去匹配想要的索引
// 最后一个匹配不到,可以默认索引为最后一个
let index = offsetValues.length -1
for(let i = 0; i < offsetValues.length; i++){
let curValue = offsetValues[i]
if(curValue > newValue + 44) {
index = i -1
break
}
}
// 如果拿到的是组件实例,可以调用子组件的方法和属性,可以修改默认index
TabActive.value = index
})
2.4 索引匹配算法-随机数在数组中的索引
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button class="btn">随机数</button>
<script>
// 1.定义的数组
const values = [100, 300, 500, 800, 1000]
// 2.按钮的点击
// 获取元素
const btnEl = document.querySelector('.btn')
btnEl.onclick = function () {
let currentValue = Math.floor(Math.random() * 1500)
matchIndex(currentValue)
}
// 3.随机currentValue匹配索引
function matchIndex(currentValue) {
let index = values.length - 1
for (let i = 0; i < values.length; i++) {
const value = values[i]
if (value > currentValue) {
index = i - 1
break
}
}
if (index === -1) return
console.log("value:", currentValue, "index:", index)
}
/*
匹配400在values中是哪一个位置
+ [300, 500] 最后算法找到500的索引,我们需要的是300的索引,所以index-1即可
let currentValue = 400
let index = values.length - 1
for(let i = 0; i < values.length; i++) {
const value = values[i]
if (value > currentValue) {
index = i - 1
break
}
}
console.log(index);
*/
</script>
</body>
</html>
2.5 点击tabs跳动bug处理及tabControl的动态图
// 4.点击定位到具体位置
let isClick = false
let currentTop = -1
const onClickTab = (value) => {
const title = value?.title
let top = sectionEles.value[title]?.offsetTop
top = title === "概述" ? top -34 : top -44
// 点击时设置为true
isClick = true
// 点击时记录要滚动的位置
currentTop = top
detailRef.value.scrollTo({
top: top,
behavior: "smooth"
})
}
/*
5.页面滚动,滚动到一定的位置时,显示正确的tabControl的索引(标题)
+ 监听滚动的位置 scrollTop
+ 利用scrollTop去匹配正确的位置
+ 变化的scrollTop,newValue 在数组中匹配第一个大于的值,找到跳出循环
+ 找到的index -1即可
+ 最后一个匹配不到,可以默认索引为最后一个
+ 第一个索引为-1,可以不考虑,因为在第一次的时候scrollTop还没有变化,依然是最初设置的0
*/
watch(scrollTop, (newValue) => {
// 如果点击滚动到了目标位置,改变标志,继续执行滚动逻辑
if(Math.round(newValue) === currentTop){
isClick = false
}
// 如果是点击操作,下面的滚动逻辑不执行 解决点击跳动问题
if(isClick) return
// 1.获取所有区域的offsetTop数组
const elem = Object.values(sectionEles.value)
const offsetValues = elem.map(elem => elem.offsetTop)
// 2.根据newValue去匹配想要的索引
// 最后一个匹配不到,可以默认索引为最后一个
let index = offsetValues.length -1
let curValue = null
for(let i = 0; i < offsetValues.length; i++){
curValue = offsetValues[i]
if(curValue > newValue + 44) {
index = i -1
break
}
}
// 如果拿到的是组件实例,可以调用子组件的方法和属性,可以修改默认index
// 如果已经等于了就不再次赋值
// if(TabActive.value === index) return
TabActive.value = index
})
2.6 详情页actionBar
- detail页面使用组件
import ActionBar from './components/detail_09-actionbar.vue'
<div class="actionBar" v-if="detailInfos.currentHouse">
<action-bar :action-bar="detailInfos.currentHouse"></action-bar>
</div>
- action-bar
<script setup>
import { computed } from 'vue';
const props = defineProps({
actionBar: {
type: Object,
default: () => ({})
}
})
const discountNum = computed(() => {
const prod = props.actionBar.productPrice.replace("¥", "")
const final = props.actionBar.finalPrice
return prod - final
})
</script>
<template>
<div class="action-bar">
<div class="bottom-price">
<div class="bottom-price-show">
<div class="chat">
<img src="@/assets/img/detail/icon_chat.png" alt="">
<div class="txt">聊天</div>
</div>
<div class="count">
<div class="price-main">
<div class="final">¥{{ actionBar.finalPrice }}</div>
<div class="final-right">
<div class="priceMark">{{ actionBar.priceMark }}</div>
<div class="product-price">{{ actionBar.productPrice }}</div>
</div>
</div>
<div class="discount">
<div class="discount-num">已减{{ discountNum }}元</div>
<div class="txt">明细<van-icon name="arrow-down" /></div>
</div>
</div>
</div>
<div class="bottom-price-buttom">
<img src="@/assets/img/detail/icon_order.png" alt="">
<div class="txt">预订当前房源</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.action-bar {
position: fixed;
left: 0;
bottom: 0;
z-index: 101;
.bottom-price {
width: 100vw;
height: 60px;
background-color: #fff;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
.bottom-price-show {
display: flex;
flex: 1;
.chat {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 42px;
border-right: 1px solid #e9e9e9;
padding: 10px 15px;
font-size: 10px;
color: #333;
img {
width: 17px;
height: 17px;
margin-bottom: 6px;
}
}
.count {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
padding-left: 12px;
color: #666;
font-size: 10px;
.price-main {
display: flex;
align-items: flex-end;
.final {
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
margin-right: 5px;
}
.final-right {
display: flex;
font-size: 10px;
line-height: 17px;
.product-price {
margin-left: 5px;
text-decoration: line-through;
}
}
}
.discount {
display: flex;
.discount-num {
height: 15px;
display: inline-block;
background: rgba(255, 150, 69, .1);
border-radius: 2px;
padding: 2px;
font-size: 10px;
color: var(--primary-color);
margin-top: 2px;
white-space: nowrap;
margin-right: 10px;
}
.txt {
margin-top: 4px;
font-size: 11px;
color: var(--primary-color);
}
}
}
}
.bottom-price-buttom {
display: flex;
align-items: center;
justify-content: center;
width: 150px;
padding: 20px 0;
color: #fff;
font-size: 18px;
background: var(--theme-linear-gradient);
img {
width: 14px;
height: 18px;
vertical-align: middle;
margin-right: 5px;
}
}
}
}
</style>
- detail-introducton底部样式修改
.ensure-bottom {
margin-bottom: 90px;
}
3.切换页面的问题
3.1 切换页面的keep-alive操作
这种写法会报错
<keep-alive includes="home">
<router-view></router-view>
</keep-alive>
采用以下做法
1.给要缓存的组件加name属性
<router-view v-slot="props">
<keep-alive include="home">
<component :is="props.Component"></component>
</keep-alive>
</router-view>
3.2切换页面自动发送网络请求的问题
因为监听window窗口滚动到底部发送网络请求加载更多,会造成切换页面自动监听到底部并发送网络请求 可以给home元素y方向滚动,添加ref
const homeRef = ref()
const { isReachBottom, scrollTop } = useScroll(homeRef)
<div class="home" ref="homeRef"></div>
.home {
// 让元素滚动
height: 100vh;
overflow-y: auto;
padding-bottom: 60px;
box-sizing: border-box;
}
3.3 设置视口和px to vw浏览器适配
- 视口设置
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
- 浏览器适配
yarn add postcss-px-to-viewport -D
postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375
}
}
}
4.编写价格和人数区域点击显示弹出层
<script setup>
import { ref } from 'vue'
import useMain from '@/stores/modules/main';
const mainStore = useMain()
// 价格
const showPriceBottom = ref(false)
const priceRange = ref({
'¥100以下': [0, 100],
'¥100-200': [100, 200],
'¥200-300': [200, 300],
'¥300-400': [300, 400],
'¥400-600': [400, 600],
'¥600-1000': [600, 1000],
'¥1000-2000': [1000, 2000],
'¥2000以上': [2000, 2000]
})
const sliderValue = ref([0, 2000])
const priceClick = () => {
showPriceBottom.value = true
}
const onChange = (value) => {
console.log(value);
}
// 人数
const showPeopleBottom = ref(false)
const showIcon = ref(10)
const peopleOptions = [
{ text: '1人', value: 0 },
{ text: '2人', value: 1 },
{ text: '3人', value: 2 },
{ text: '4人', value: 3 },
{ text: '5人', value: 4 },
{ text: '6人', value: 5 },
{ text: '7人', value: 6 },
{ text: '8人', value: 7 },
{ text: '9人', value: 8 },
{ text: '10+人', value: 9 },
{ text: '不限人数', value: 10 }
]
const peopleClick = () => {
showPeopleBottom.value = true
}
const peopleItemClick = (index) => {
showIcon.value = index
}
</script>
<template>
<div class="item price-counter bottom-gray-line">
<div class="start" @click="priceClick">价格不限</div>
<div class="end" @click="peopleClick">人数不限
</div>
</div>
<van-popup v-model:show="showPriceBottom" round position="bottom" closeable :style="{ height: '60%' }">
<div class="card">
<div class="title">价格</div>
<div class="select">
<div class="tip">价格区间 ¥0-不限</div>
<van-slider v-model="sliderValue" bar-height="4px" active-color="#ff9645" range max="2000" @change="onChange" />
<div class="range">
<template v-for="(value, key, index) in priceRange" :key="index">
<div class="item">{{ key }}</div>
</template>
</div>
</div>
<div class="price-btn">
<van-button type="primary" class="clear">清空</van-button>
<van-button type="primary" class="confirm">确认</van-button>
</div>
</div>
</van-popup>
<van-popup v-model:show="showPeopleBottom" :overlay="false" position="bottom" closeable close-icon-position="top-left"
:style="{ height: '100%' }">
<div class="people">
<div class="title">选择入住人数</div>
<template v-for="(item, index) in peopleOptions" :key="item.value">
<div class="item" @click="peopleItemClick(index)">
<div class="text">{{ item.text }}</div>
<div class="icon" v-show="index === showIcon">
<van-icon name="success" color="#ff9645" />
</div>
</div>
</template>
</div>
</van-popup>
</template>
<style lang="less" scoped>
.price-counter {
.start {
border-right: 1px solid var(--line-color);
}
}
.item {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 44px;
padding: 0 20px;
color: #999;
.start {
flex: 1;
display: flex;
height: 44px;
align-items: center;
}
.end {
min-width: 30%;
padding-left: 20px;
}
.date {
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
color: #999;
}
.time {
margin-top: 3px;
color: #333;
font-size: 15px;
font-weight: 500;
}
}
}
.card {
padding: 20px;
height: 90%;
overflow-y: hidden;
background-color: #f2f3f4;
.title {
color: #333;
font-weight: 600;
margin-bottom: 10px;
}
.--van-popup-close-icon-color {
color: rgb(153, 153, 153);
font-size: 18px;
}
.select {
background-color: #fff;
padding: 10px;
height: 220px;
border-radius: 9px;
font-size: 10px;
color: #666;
.van-slider {
position: relative;
top: 0px;
left: 15px;
margin-top: 30px;
width: 90%;
}
.range {
display: flex;
flex-wrap: wrap;
margin-top: 18px;
.item {
height: 23px;
padding: 5px 10px;
background-color: #f8f8f8;
margin-right: 20px;
margin-top: 15px;
font-size: 12px;
border-radius: 15px;
color: #666;
}
}
}
.van-button {
border: none;
border-radius: 20px;
}
.price-btn {
margin-top: 30px;
.clear {
height: 40px;
padding: 0 25px;
background-color: #fff;
color: #333;
font-size: 15px;
text-align: center;
line-height: 40px;
}
.confirm {
min-width: 240px;
margin-left: 10px;
background: var(--primary-color);
font-weight: 500;
color: #fff;
}
}
}
.people {
height: 100vh;
background-color: #fff;
.--van-popup-close-icon-color {
width: 11px;
height: 11px;
color: #333;
}
.title {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
font-size: 15px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f6f6f6;
color: #333;
}
}
</style>
5.价格部分功能完善
- 点击显示弹出框
- 点击下面按钮或者拖动滑块,头部自动显示价格
- 点击清空,头部显示
¥0-不限 - 点击确定,把数据保存到store,弹出框隐藏
- 价格回显
- 右边可以清空选择的价格
<script setup>
import { ref, computed } from 'vue'
import useMain from '@/stores/modules/main';
import { storeToRefs } from 'pinia';
const mainStore = useMain()
// store管理数据
const { sPrice, ePrice } = storeToRefs(mainStore)
// 价格
// 定义
const showPriceBottom = ref(false)
const priceRange = ref({
'¥100以下': [0, 100],
'¥100-200': [100, 200],
'¥200-300': [200, 300],
'¥300-400': [300, 400],
'¥400-600': [400, 600],
'¥600-1000': [600, 1000],
'¥1000-2000': [1000, 2000],
'¥2000以上': [2000, 2000]
})
// 滑块初始值
const sliderValue = ref([0, 2000])
// 点击后存储值
const changeValue = ref([])
// const sPrice = ref(0)
// const ePrice = ref(0)
// 点击后弹出框显示
const priceClick = () => {
showPriceBottom.value = true
}
// 把值给changeValue保存
// 1.点击item
const priceItemClick = (value) => {
sliderValue.value = value
changeValue.value = value
}
// 2.手动拖动滑块的值
const onChange = (value) => {
changeValue.value = value
}
// card price动态设置
const getCardPrice = () => {
let len = changeValue.value.length
// 第一次进来数组没有长度 设置值是价格不限
if (len === 0 || changeValue.value === '价格不限') {
return `¥0-不限`
}
return `¥${changeValue.value[0]}-¥${changeValue.value[1]}`
}
// 清空
const clear = () => {
sliderValue.value = [0, 2000]
changeValue.value = `价格不限`
}
// 确认
const confirm = () => {
// 复原store数据
if (changeValue.value == '价格不限') {
sPrice.value = 0
ePrice.value = 0
showPriceBottom.value = false
return
}
// 给store设置数据
sPrice.value = changeValue.value[0]
ePrice.value = changeValue.value[1]
showPriceBottom.value = false
}
// 判断是否已经选择有用数据
const isSelected = computed(() => {
return changeValue.value.length !== 0 && changeValue.value !== '价格不限'
})
// 清空选择的价格
const isClear = ref(false)
const clearSelect = () => {
sliderValue.value = [0, 2000]
changeValue.value.splice(0, 2)
sPrice.value = 0
ePrice.value = 0
isClear.value = true
}
// 人数
const showPeopleBottom = ref(false)
const showIcon = ref(10)
const peopleOptions = [
{ text: '1人', value: 0 },
{ text: '2人', value: 1 },
{ text: '3人', value: 2 },
{ text: '4人', value: 3 },
{ text: '5人', value: 4 },
{ text: '6人', value: 5 },
{ text: '7人', value: 6 },
{ text: '8人', value: 7 },
{ text: '9人', value: 8 },
{ text: '10+人', value: 9 },
{ text: '不限人数', value: 10 }
]
const peopleClick = () => {
showPeopleBottom.value = true
}
const peopleItemClick = (index) => {
showIcon.value = index
}
</script>
<template>
<div class="item price-counter bottom-gray-line">
<div class="start" :class="{ selected: isSelected }">
<div class="startTxt" @click="priceClick">{{ mainStore.getPriceStart }}</div>
<van-icon name="close" v-if="isSelected" @click="clearSelect" />
</div>
<div class="end" @click="peopleClick">人数不限
</div>
</div>
<van-popup v-model:show="showPriceBottom" round position="bottom" closeable :style="{ height: '60%' }">
<div class="card">
<div class="title">价格</div>
<div class="select">
<div class="tip">价格区间 {{ getCardPrice() }}</div>
<van-slider v-model="sliderValue" bar-height="4px" active-color="#ff9645" range max="2000" @change="onChange" />
<div class="range">
<template v-for="(value, key, index) in priceRange" :key="index">
<div class="item" @click="priceItemClick(value)">{{ key }}</div>
</template>
</div>
</div>
<div class="price-btn">
<van-button type="primary" class="clear" @click="clear">清空</van-button>
<van-button type="primary" class="confirm" @click="confirm">确认</van-button>
</div>
</div>
</van-popup>
<van-popup v-model:show="showPeopleBottom" :overlay="false" position="bottom" closeable close-icon-position="top-left"
:style="{ height: '100%' }">
<div class="people">
<div class="title">选择入住人数</div>
<template v-for="(item, index) in peopleOptions" :key="item.value">
<div class="item" @click="peopleItemClick(index)">
<div class="text">{{ item.text }}</div>
<div class="icon" v-show="index === showIcon">
<van-icon name="success" color="#ff9645" />
</div>
</div>
</template>
</div>
</van-popup>
</template>
<style lang="less" scoped>
.price-counter {
.start {
display: flex;
align-items: center;
justify-content: space-between;
border-right: 1px solid var(--line-color);
&.selected {
color: #333;
}
.startTxt {
width: 160px;
height: 100%;
line-height: 45px;
}
.van-icon {
position: relative;
top: 1px;
right: 15px;
}
}
}
.item {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 44px;
padding: 0 20px;
color: #999;
.start {
flex: 1;
display: flex;
height: 44px;
align-items: center;
}
.end {
min-width: 30%;
padding-left: 20px;
}
.date {
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
color: #999;
}
.time {
margin-top: 3px;
color: #333;
font-size: 15px;
font-weight: 500;
}
}
}
.card {
padding: 20px;
height: 90%;
overflow-y: hidden;
background-color: #f2f3f4;
.title {
color: #333;
font-weight: 600;
margin-bottom: 10px;
}
.--van-popup-close-icon-color {
color: rgb(153, 153, 153);
font-size: 18px;
}
.select {
background-color: #fff;
padding: 10px;
height: 220px;
border-radius: 9px;
font-size: 10px;
color: #666;
.van-slider {
position: relative;
top: 0px;
left: 15px;
margin-top: 30px;
width: 90%;
}
.range {
display: flex;
flex-wrap: wrap;
margin-top: 18px;
.item {
height: 23px;
padding: 5px 10px;
background-color: #f8f8f8;
margin-right: 20px;
margin-top: 15px;
font-size: 12px;
border-radius: 15px;
color: #666;
}
}
}
.van-button {
border: none;
border-radius: 20px;
}
.price-btn {
margin-top: 30px;
.clear {
height: 40px;
padding: 0 25px;
background-color: #fff;
color: #333;
font-size: 15px;
text-align: center;
line-height: 40px;
}
.confirm {
min-width: 240px;
margin-left: 10px;
background: var(--primary-color);
font-weight: 500;
color: #fff;
}
}
}
.people {
height: 100vh;
background-color: #fff;
.--van-popup-close-icon-color {
width: 11px;
height: 11px;
color: #333;
}
.title {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
font-size: 15px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f6f6f6;
color: #333;
}
}
</style>
6.选择人数功能实现,完成了价格人数模块编写
<script setup>
import { ref, computed } from 'vue'
import useMain from '@/stores/modules/main';
import { storeToRefs } from 'pinia';
const mainStore = useMain()
// store管理数据
const { sPrice, ePrice, selectPeople } = storeToRefs(mainStore)
// 价格
// 定义
const showPriceBottom = ref(false)
const priceRange = ref({
'¥100以下': [0, 100],
'¥100-200': [100, 200],
'¥200-300': [200, 300],
'¥300-400': [300, 400],
'¥400-600': [400, 600],
'¥600-1000': [600, 1000],
'¥1000-2000': [1000, 2000],
'¥2000以上': [2000, 2000]
})
// 滑块初始值
const sliderValue = ref([0, 2000])
// 点击后存储值
const changeValue = ref([])
// const sPrice = ref(0)
// const ePrice = ref(0)
// 点击后弹出框显示
const priceClick = () => {
showPriceBottom.value = true
}
// 把值给changeValue保存
// 1.点击item
const priceItemClick = (value) => {
sliderValue.value = value
changeValue.value = value
}
// 2.手动拖动滑块的值
const onChange = (value) => {
changeValue.value = value
}
// card price动态设置
const getCardPrice = () => {
let len = changeValue.value.length
// 第一次进来数组没有长度 设置值是价格不限
if (len === 0 || changeValue.value === '价格不限') {
return `¥0-不限`
}
return `¥${changeValue.value[0]}-¥${changeValue.value[1]}`
}
// 清空
const clear = () => {
sliderValue.value = [0, 2000]
changeValue.value = `价格不限`
}
// 确认
const confirm = () => {
// 复原store数据
if (changeValue.value == '价格不限') {
sPrice.value = 0
ePrice.value = 0
showPriceBottom.value = false
return
}
// 给store设置数据
sPrice.value = changeValue.value[0]
ePrice.value = changeValue.value[1]
showPriceBottom.value = false
}
// 判断是否已经选择有用数据
const isSelected = computed(() => {
return changeValue.value.length !== 0 && changeValue.value !== '价格不限'
})
// 清空选择的价格
const isClear = ref(false)
const clearSelect = () => {
sliderValue.value = [0, 2000]
changeValue.value.splice(0, 2)
sPrice.value = 0
ePrice.value = 0
isClear.value = true
}
// 人数
const showPeopleBottom = ref(false)
const showIcon = ref(10)
const peopleOptions = [
{ text: '1人', value: 0 },
{ text: '2人', value: 1 },
{ text: '3人', value: 2 },
{ text: '4人', value: 3 },
{ text: '5人', value: 4 },
{ text: '6人', value: 5 },
{ text: '7人', value: 6 },
{ text: '8人', value: 7 },
{ text: '9人', value: 8 },
{ text: '10+人', value: 9 },
{ text: '人数不限', value: 10 }
]
const peopleClick = () => {
showPeopleBottom.value = true
}
const peopleItemClick = (index, num) => {
showIcon.value = index
selectPeople.value = num
showPeopleBottom.value = false
}
const isPeopleSelect = computed(() => selectPeople.value !== '人数不限')
const clearPeopleSelect = () => {
selectPeople.value = '人数不限'
}
</script>
<template>
<div class="item price-counter bottom-gray-line">
<div class="start" :class="{ selected: isSelected }">
<div class="startTxt" @click="priceClick">{{ mainStore.getPriceStart }}</div>
<van-icon name="close" v-if="isSelected" @click="clearSelect" />
</div>
<div class="end" :class="{ active: isPeopleSelect }">
<div class="endTxt" @click="peopleClick">{{ selectPeople }}</div>
<van-icon name="close" v-if="isPeopleSelect" @click="clearPeopleSelect" />
</div>
</div>
<van-popup v-model:show="showPriceBottom" round position="bottom" closeable :style="{ height: '60%' }">
<div class="card">
<div class="title">价格</div>
<div class="select">
<div class="tip">价格区间 {{ getCardPrice() }}</div>
<van-slider v-model="sliderValue" bar-height="4px" active-color="#ff9645" range max="2000" @change="onChange" />
<div class="range">
<template v-for="(value, key, index) in priceRange" :key="index">
<div class="item" @click="priceItemClick(value)">{{ key }}</div>
</template>
</div>
</div>
<div class="price-btn">
<van-button type="primary" class="clear" @click="clear">清空</van-button>
<van-button type="primary" class="confirm" @click="confirm">确认</van-button>
</div>
</div>
</van-popup>
<van-popup v-model:show="showPeopleBottom" :overlay="false" position="bottom" closeable close-icon-position="top-left"
:style="{ height: '100%' }">
<div class="people">
<div class="title">选择入住人数</div>
<template v-for="(item, index) in peopleOptions" :key="item.value">
<div class="item" @click="peopleItemClick(index, item.text)">
<div class="text">{{ item.text }}</div>
<div class="icon" v-show="index === showIcon">
<van-icon name="success" color="#ff9645" />
</div>
</div>
</template>
</div>
</van-popup>
</template>
<style lang="less" scoped>
.price-counter {
.start {
display: flex;
align-items: center;
justify-content: space-between;
border-right: 1px solid var(--line-color);
&.selected {
color: #333;
}
.startTxt {
width: 160px;
height: 100%;
line-height: 45px;
}
.van-icon {
position: relative;
top: 1px;
right: 15px;
}
}
.end {
display: flex;
align-items: center;
justify-content: space-between;
&.active {
color: #333;
}
.endTxt {
width: 80%;
}
}
}
.item {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 44px;
padding: 0 20px;
color: #999;
.start {
flex: 1;
display: flex;
height: 44px;
align-items: center;
}
.end {
min-width: 30%;
padding-left: 20px;
}
.date {
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
color: #999;
}
.time {
margin-top: 3px;
color: #333;
font-size: 15px;
font-weight: 500;
}
}
}
.card {
padding: 20px;
height: 90%;
overflow-y: hidden;
background-color: #f2f3f4;
.title {
color: #333;
font-weight: 600;
margin-bottom: 10px;
}
.--van-popup-close-icon-color {
color: rgb(153, 153, 153);
font-size: 18px;
}
.select {
background-color: #fff;
padding: 10px;
height: 220px;
border-radius: 9px;
font-size: 10px;
color: #666;
.van-slider {
position: relative;
top: 0px;
left: 15px;
margin-top: 30px;
width: 90%;
}
.range {
display: flex;
flex-wrap: wrap;
margin-top: 18px;
.item {
height: 23px;
padding: 5px 10px;
background-color: #f8f8f8;
margin-right: 20px;
margin-top: 15px;
font-size: 12px;
border-radius: 15px;
color: #666;
}
}
}
.van-button {
border: none;
border-radius: 20px;
}
.price-btn {
margin-top: 30px;
.clear {
height: 40px;
padding: 0 25px;
background-color: #fff;
color: #333;
font-size: 15px;
text-align: center;
line-height: 40px;
}
.confirm {
min-width: 240px;
margin-left: 10px;
background: var(--primary-color);
font-weight: 500;
color: #fff;
}
}
}
.people {
height: 100vh;
background-color: #fff;
.--van-popup-close-icon-color {
width: 11px;
height: 11px;
color: #333;
}
.title {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
font-size: 15px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f6f6f6;
color: #333;
}
}
</style>
7.点击search-keyword和search-bar组件跳转到keyword-suggests页面
import { useRouter } from 'vue-router';
const router = useRouter()
//给组件添加事件即可
const keywordClick = () => {
router.push('/keyword-suggests')
}
8.keyword-suggests页面的展示
8.1 配置路由表
{
path: "/keyword-suggests",
component: () => import("@/views/keyword-suggests/keyword-suggests.vue")
}
8.2 管理api 和 store请求数据
- 封装api
// search.js
// 测试用的配置,需要在vite.config.js中配置代理
const TSET_URL = "http://localhost:5173/api";
const _apitsp = "1657957399020";
const _fasTraceId = "1657957399023KiYJenCT_6Th0R8QmkS3QDMD3wNhFaJsfn97x";
// 发起网络请求
export function getGuessulike() {
return hyRequest.post({
url:
TSET_URL +
`/bnbapp-node-h5/h5/suggest/guessulike/bnb?_apitsp=${_apitsp}&_fasTraceId=${_fasTraceId}
`,
method: "post",
data: { cityId: 45, keywordTryRecommend: 2 },
});
}
- store请求数据 keyword-suggests.js
import { getGuessulike, keywordsearchsuggest} from "@/services";
import { defineStore } from "pinia";
const usekeywordsuggests = defineStore("keywordsuggests", {
state: () => ({
keywordsuggests: [],
isDownIdx: '',
type: '',
suggestName: ''
}),
actions: {
//页面调用即可发送请求
async fetchKeywordsuggestsData(){
const res = await getGuessulike()
this.keywordsuggests = res.data
},
}
})
export default usekeywordsuggests
8.3 keyword-suggests页面
<script setup>
import { ref } from 'vue'
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
import searchCategory from './components/search-category.vue';
import SearchContent from './components/search-content.vue'
const router = useRouter()
const keywordsuggestsStore = usekeywordsuggests()
const { keywordsuggests } = storeToRefs(keywordsuggestsStore)
// 发送网络请求
keywordsuggestsStore.fetchKeywordsuggestsData()
// 1.搜索框的功能
const searchValue = ref("")
// 点击取消返回上一个页面
const cancelClick = () => {
router.back()
}
</script>
<template>
<div class="keyword-suggests top-page">
<!-- 搜索框 -->
<van-search v-model="searchValue" placeholder="搜索广州的景点/地标/房源" shape="round" show-action
@cancel="cancelClick"></van-search>
<!-- 滚动区域 -->
<div class="search-result-panel">
<template v-for="(item, index) in keywordsuggests.groups" :key="index">
<search-category :category="item" :index="index"></search-category>
<search-content :content="item" :index="index"></search-content>
</template>
</div>
</div>
</template>
<style lang="less" scoped>
.van-search {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 9;
}
.keyword-suggests {
height: 100vh;
overflow-y: auto;
margin-top: 54px;
}
</style>
8.4 两个组件
- search-category
<script setup>
import { ref } from 'vue';
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import { storeToRefs } from 'pinia';
const props = defineProps({
category: {
type: Object,
default: () => ({})
},
index: {
type: Number,
default: 0
}
})
const keywordsuggestsStore = usekeywordsuggests()
const { isDownIdx, type } = storeToRefs(keywordsuggestsStore)
const isToggle = ref(true)
const moreClick = () => {
isToggle.value = !isToggle.value
if (!isToggle.value) {
isDownIdx.value = props.index
type.value = 'show'
} else {
type.value = 'hidden'
}
}
</script>
<template>
<div class="search-category">
<span class="name">{{ category.name }}</span>
<div class="more" @click="moreClick">
<template v-if="isToggle">
<div class="down">展开</div>
<van-icon name="arrow-down" />
</template>
<template v-else>
<div class="down">收起</div>
<van-icon name="arrow-up" />
</template>
</div>
</div>
</template>
<style lang="less" scoped>
.search-category {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
margin-top: 20px;
.name {
font-weight: 500;
font-size: 16px;
color: #333;
&::before {
position: relative;
top: 1px;
content: "";
display: inline-block;
width: 3px;
height: 15px;
margin-right: 10px;
border-radius: 4px;
background-color: #ff9645;
box-shadow: 4px 0 10px 0 rgb(255 150 69 / 30%);
}
}
.more {
display: flex;
align-items: center;
.down {
font-weight: 500;
font-size: 10px;
color: #666;
line-height: 20px;
height: 20;
margin-right: 5px;
}
}
}
</style>
- search-content
<script setup>
import { computed } from 'vue'
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import { storeToRefs } from 'pinia';
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
index: {
type: Number,
default: 0
}
})
const keywordsuggestsStore = usekeywordsuggests()
const { isDownIdx, type, suggestName } = storeToRefs(keywordsuggestsStore)
const isToggle = computed(() => {
return isDownIdx.value === props.index && type.value === 'show'
})
</script>
<template>
<div class="content" :class="{ toggle: isToggle }">
<template v-for="(item, index) in content.suggests" :key="index">
<div class="search-suggests" :class="{ active: content.name == '热门搜索' }">
<div class="item">{{ item.name }}</div>
</div>
</template>
</div>
</template>
<style lang="less" scoped>
.content {
padding: 11px 10px 0 20px;
margin-bottom: 16px;
max-height: 68px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
&.toggle {
max-height: 168px;
}
.search-suggests {
margin-left: 10px;
margin-bottom: 10px;
font-size: 12px;
border-radius: 14px;
padding: 5px 12px;
background-color: rgb(240, 243, 246);
&.active {
background-color: #fff4ec;
}
}
}
</style>
9.首页和keyword-suggests页面跳转到search页面
- keyword-suggests页面
// 点击跳转到search页面
const itemClick = (clickName) => {
suggestName.value = clickName
router.push({
path: '/search',
query: {
startDate: startDate.value,
endDate: endDate.value,
cityName: currentCity.value.cityName,
clickName: clickName,
selectPeople: selectPeople.value
}
})
}
- 首页hot-suggest
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
const router = useRouter()
const itemClick = (itemTxt) => {
suggestName.value = itemTxt
router.push({
path: "/search",
query: {
clickName: itemTxt
}
})
}
- search-btn
<script setup>
import useCity from '@/stores/modules/city';
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import useMain from '@/stores/modules/main';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
const mainStore = useMain()
const { startDate, endDate, sPrice, ePrice, selectPeople } = storeToRefs(mainStore)
const cityStore = useCity()
const { currentCity } = storeToRefs(cityStore)
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
// 搜索并且跳转到search页面
const router = useRouter()
const searchBtnClick = () => {
router.push({
path: "/search",
query: {
startDate: startDate.value,
endDate: endDate.value,
cityName: currentCity.value.cityName,
clickName: suggestName.value,
sPrice: sPrice.value,
ePrice: ePrice.value,
selectPeople: selectPeople.value
}
})
}
</script>
<template>
<div class="item search-btn" @click="searchBtnClick">
<div class="btn">开始搜索</div>
</div>
</template>
<style lang="less" scoped>
.search-btn {
.btn {
width: 442px;
height: 38px;
max-height: 50px;
font-weight: 500;
font-size: 18px;
line-height: 38px;
text-align: center;
border-radius: 20px;
color: #fff;
background-image: var(--theme-linear-gradient);
}
}
.item {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 44px;
padding: 0 20px;
color: #999;
.start {
flex: 1;
display: flex;
height: 44px;
align-items: center;
}
.end {
min-width: 30%;
padding-left: 20px;
}
.date {
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
color: #999;
}
.time {
margin-top: 3px;
color: #333;
font-size: 15px;
font-weight: 500;
}
}
}
</style>
10.点击热门区域回显到home页面
<script setup>
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter()
const keywordClick = () => {
router.push('/keyword-suggests')
}
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
const getsuggestName = computed(() => {
if (suggestName.value === '') return `关键字/位置/民宿名`
return suggestName.value
})
const clearSuggestName = () => {
suggestName.value = ''
}
</script>
<template>
<div class="item keyword bottom-gray-line" :class="{ active: suggestName !== '' }">
<div class="txt" @click="keywordClick">{{ getsuggestName }}</div>
<van-icon name="close" v-if="suggestName !== ''" @click="clearSuggestName" />
</div>
</template>
<style lang="less" scoped>
.keyword {
&.active {
color: #333;
}
.txt {
width: 90%;
}
.van-icon {
// margin-left: 10px;
}
}
.item {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 44px;
padding: 0 20px;
color: #999;
.start {
flex: 1;
display: flex;
height: 44px;
align-items: center;
}
.end {
min-width: 30%;
padding-left: 20px;
}
.date {
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
color: #999;
}
.time {
margin-top: 3px;
color: #333;
font-size: 15px;
font-weight: 500;
}
}
}
</style>
11.search页面开发
11.1 进一步强化search-bar组件
- 增加当前城市
- 搜索按钮等的样式修改
<script setup>
import useMain from '@/stores/modules/main';
import { storeToRefs } from 'pinia';
import { formatMonthDay } from '@/utils/format_data'
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
const props = defineProps({
isSearch: {
type: Boolean,
default: false
},
city: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
})
const mainStore = useMain()
const { startDate, endDate } = storeToRefs(mainStore)
const startDateStr = computed(() => formatMonthDay(startDate.value, "MM.DD"))
const endDateStr = computed(() => formatMonthDay(endDate.value, "MM.DD"))
const router = useRouter()
const keywordClick = () => {
router.push('/keyword-suggests')
}
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
const getsuggestName = computed(() => {
if (suggestName.value === '') return props.title
return suggestName.value
})
const clearSuggestName = () => {
suggestName.value = ''
}
const isShowRightIcon = computed(() => {
if (props.isSearch) return false
return suggestName.value === ''
})
</script>
<template>
<div class="search">
<div class="position" v-if="isSearch">
<slot class="current-city">{{ city }}</slot>
</div>
<div class="select-time">
<div class="start">
<div class="name">住</div>
<div class="date">{{ startDateStr }}</div>
</div>
<div class="end">
<div class="name">离</div>
<div class="date">{{ endDateStr }}</div>
<i class="icon-search-arrow"></i>
</div>
</div>
<div class="search-inner" :class="{ active: suggestName !== '' }">
<div class="keyword" @click="keywordClick" :style="{ width: !isShowRightIcon ? '100%' : '' }">{{ getsuggestName }}
</div>
<van-icon name="close" v-if="suggestName !== ''" @click="clearSuggestName" />
</div>
<div class="right-icon" v-if="isShowRightIcon">
<i class="icon-search"></i>
</div>
</div>
</template>
<style lang="less" scoped>
.search {
position: relative;
display: flex;
height: 40px;
border-radius: 6px;
background-color: #f2f4f6;
color: #999;
.position {
display: flex;
align-items: center;
justify-content: center;
padding: 0 5px 0 10px;
font-size: 14px;
font-weight: 500;
color: #333;
border-right: 1px solid #fff;
}
.select-time {
display: flex;
flex-direction: column;
border-right: 1px solid #fff;
align-items: flex-start;
justify-content: center;
font-size: 10px;
padding: 5px 0 5px 5px;
.start,
.end {
display: flex;
margin: 0 2px;
font-weight: 500;
justify-content: center;
align-items: flex-end;
.date {
margin: 0 2px;
color: #333;
}
}
.end {
.icon-search-arrow {
position: relative;
top: -2px;
display: inline-block;
width: 5px;
height: 5px;
background-image: url(@/assets/img/home/home-sprite.png);
background-size: 207px 192px;
background-position: -201px -157px;
}
}
}
.search-inner {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
flex: 1;
font-size: 14px;
&.active {
color: #333;
width: 157px;
}
.keyword {
width: 90%;
font-size: 12px;
font-weight: 500;
}
}
.right-icon {
.icon-search {
position: absolute;
right: 5px;
top: 10px;
width: 24px;
height: 24px;
background-image: url(@/assets/img/home/home-sprite.png);
background-size: 207px 192px;
background-position: -29px -151px;
}
}
}
</style>
11.2 实现导航栏
<script setup>
import SearchBar from '@/components/search-bar/index.vue'
import { useRoute, useRouter } from 'vue-router';
const route = useRoute()
const router = useRouter()
const city = route.query.cityName
const back = () => {
router.back()
}
</script>
<template>
<div class="search top-page">
<!-- 导航栏 -->
<div class="nav">
<div class="go-back" @click="back">
<van-icon name="arrow-left" color="#ff9645" :size="25" />
</div>
<div class="search-bar">
<search-bar title="搜索广州的景点、地标、房源" :is-search="true" :city="city" />
</div>
<div class="right">
<i class="right-icon"></i>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.search {
// padding: 0 20px;
.nav {
position: fixed;
top: 0;
left: 0;
width: 335px;
background-color: #fff;
padding: 5px 20px;
z-index: 9;
display: flex;
align-items: center;
justify-content: center;
.go-back {
position: absolute;
top: 12px;
left: 10px;
}
.right {
position: absolute;
top: 7px;
right: 6px;
line-height: 13px;
padding: 10px;
.right-icon {
display: inline-block;
width: 20px;
height: 15px;
background-image: url(@/assets/img/home/home-sprite.png);
background-size: 207px 192px;
background-position: -166px -104px;
}
}
}
}
</style>
11.3 实现下拉菜单位置的第一项数据展示
<script setup>
import { useRoute } from 'vue-router';
import NavBar from './component/nav-bar.vue'
import useSearch from '@/stores/modules/search'
import { storeToRefs } from 'pinia';
import { ref, computed } from 'vue';
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
// 发送网络请求
const searchStore = useSearch()
searchStore.fetchSearchtopData()
const route = useRoute()
//=> nav
// 获取路由信息
const city = route.query.cityName
const title = `搜索${city}的景点、地标、房源`
// let clickName = route.query.clickName
//=> top
const { searchConditions } = storeToRefs(searchStore)
const positionRef = ref();
const sortRef = ref()
const filterRef = ref()
const activeIndex = ref(0)
const rightActiveIndex = ref()
const items = [
{ text: '热门推荐' },
{ text: '观光景点' },
{ text: '商圈' },
{ text: '行政区' },
{ text: '机场/车站' },
{ text: '高校' },
{ text: '医院' },
]
const positionData = computed(() => {
return searchConditions.value[0]?.subGroups
})
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
let clickName = suggestName.value
// 计算clickName
const getclickName = computed(() => {
if (suggestName.value === '') {
return clickName = route.query.clickName
}
return clickName = suggestName.value
})
const near = ["1km内", "2km内", "3km内", "4km内", "5km内"]
const curNear = ref()
const nearClick = (index) => {
curNear.value = index
}
// 点击选中并改变上面的初始值
const rightClick = (index, label) => {
rightActiveIndex.value = index
// 修改公共的值
suggestName.value = label
// 修改下拉菜单的值
clickName = label
}
</script>
<template>
<div class="search top-page">
<!-- 导航栏 -->
<nav-bar :title="title" :city="city" />
<div class="top">
<div class="top-condition">
<van-dropdown-menu active-color="#ff9645">
<van-dropdown-item :title="getclickName" ref="positionRef">
<div class="content">
<div class="near">
<div class="title">{{ clickName }}的附近</div>
<div class="text">
<template v-for="(item, index) in near" :key="index">
<div class="item" @click="nearClick(index)" :class="{ active: curNear === index }">{{ item }}</div>
</template>
</div>
</div>
<van-tree-select v-model:main-active-index="activeIndex" height="95vw" :items="items">
<template #content>
<!-- <van-image v-if="activeIndex === 0" src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg" />
<van-image v-if="activeIndex === 1" src="https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg" /> -->
<template v-for="(item, index) in positionData" :key="index">
<div class="right">
<template v-for="(iten, idx) in item.items" :key="idx">
<div class="right-content" @click="rightClick(idx, iten.label)"
:class="{ active: rightActiveIndex === idx }">
<div class="sub" :class="{ subactive: rightActiveIndex === idx }">{{ iten.label }}</div>
<div class="sup">{{ iten.percentageUser }}</div>
</div>
</template>
</div>
</template>
</template>
</van-tree-select>
</div>
</van-dropdown-item>
<van-dropdown-item title="排序" ref="sortRef">
1
</van-dropdown-item>
<van-dropdown-item title="筛选" ref="filterRef">
1
</van-dropdown-item>
</van-dropdown-menu>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.search {
height: 100vh;
overflow-y: auto;
.top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px 0 25px;
margin-top: 50px;
height: 44px;
.top-condition {
width: 100%;
:deep(.van-dropdown-menu__bar) {
display: flex;
justify-content: space-between;
box-shadow: none;
}
:deep(.van-dropdown-menu__title) {
color: #333;
font-size: 13px;
font-weight: 500;
}
.content {
.near {
padding: 0 10px;
color: #666;
background: linear-gradient(#f2f3f4, #fff);
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.title {
font-size: 14px;
color: #333;
line-height: 20px;
margin-bottom: 10px;
padding-top: 10px;
}
.text {
display: flex;
align-items: center;
justify-content: space-between;
.item {
padding: 5px 10px;
background-color: #f2f3f4;
border-radius: 15px;
&.active {
color: #ff9645;
border: 1px solid #ff9645;
background-color: #fffcf5;
font-weight: 500;
}
}
}
}
.right {
margin: 0 20px 0 12px;
.right-content {
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
height: 55px;
padding: 0 10px 0 10px;
font-size: 14px;
&.active {
font-weight: 500;
background-color: #fffcf5;
}
.sub {
color: #666;
&.subactive {
color: #ff9645;
}
}
.sup {
line-height: 20px;
font-size: 11px;
color: #999;
}
}
}
}
}
}
}
</style>
11.4 划分组件
- search页面
<script setup>
import { useRoute } from 'vue-router';
import useSearch from '@/stores/modules/search'
import NavBar from './component/nav-bar.vue'
import DropdownSelect from './component/dropdown-select.vue'
// 发送网络请求
const searchStore = useSearch()
searchStore.fetchSearchtopData()
const route = useRoute()
// 获取路由信息
const city = route.query.cityName
const title = `搜索${city}的景点、地标、房源`
</script>
<template>
<div class="search top-page">
<!-- 导航栏 -->
<nav-bar :title="title" :city="city" />
<!-- select section -->
<dropdown-select></dropdown-select>
</div>
</template>
<style lang="less" scoped>
.search {
height: 100vh;
overflow-y: auto;
}
</style>
- dropdown-select组件
<script setup>
import TopCondition from '@/components/top-condition/index.vue'
</script>
<template>
<div class="top">
<top-condition />
</div>
</template>
<style lang="less" scoped>
.top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px 0 25px;
margin-top: 50px;
height: 44px;
}
</style>
- top-condition-content公共组件
<script setup>
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import { storeToRefs } from 'pinia';
import { ref } from 'vue'
defineProps({
activeIndex: {
type: Number,
default: 0
},
itemData: {
type: Array,
default: () => ([])
}
})
const rightActiveIndex = ref()
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
// let clickName = suggestName.value
// 点击选中并改变上面的初始值
const rightClick = (index, label) => {
rightActiveIndex.value = index
// 修改公共的值
suggestName.value = label
// 修改下拉菜单的值
clickName = label
// 点击后隐藏下拉框
positionRef.value.toggle()
}
</script>
<template>
<template v-if="activeIndex === activeIndex" v-for="(item, index) in itemData" :key="index">
<div class="right">
<template v-for="(iten, idx) in item.items" :key="idx">
<div class="right-content" @click="rightClick(idx, iten.label)" :class="{ active: rightActiveIndex === idx }">
<div class="sub" :class="{ subactive: rightActiveIndex === idx }">{{ iten.label }}</div>
<div class="sup">{{ iten.percentageUser }}</div>
</div>
</template>
</div>
</template>
</template>
<style lang="less" scoped>
.right {
margin: 0 20px 0 12px;
.right-content {
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
height: 55px;
padding: 0 10px 0 10px;
font-size: 14px;
&.active {
font-weight: 500;
background-color: #fffcf5;
}
.sub {
color: #666;
&.subactive {
color: #ff9645;
}
}
.sup {
line-height: 20px;
font-size: 11px;
color: #999;
}
}
}
</style>
- top-condition公共组件
<script setup>
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia';
import { ref, computed } from 'vue';
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import useSearch from '@/stores/modules/search';
import near from '@/components/near/index.vue';
import TopConditionContent from '@/components/top-condition-content/index.vue'
const searchStore = useSearch()
const { searchConditions } = storeToRefs(searchStore)
const positionRef = ref()
const sortRef = ref()
const filterRef = ref()
const activeIndex = ref(0)
const items = [
{ text: '热门推荐' },
{ text: '观光景点' },
{ text: '商圈' },
{ text: '行政区' },
{ text: '机场/车站' },
{ text: '高校' },
{ text: '医院' },
]
const positionData = computed(() => {
return searchConditions.value[0]?.subGroups
})
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
let clickName = suggestName.value
// 计算clickName
const route = useRoute()
const getclickName = computed(() => {
if (suggestName.value === '') {
return clickName = route.query.clickName
}
return clickName = suggestName.value
})
</script>
<template>
<div class="top-condition">
<van-dropdown-menu active-color="#ff9645">
<van-dropdown-item :title="getclickName" ref="positionRef">
<div class="content">
<near :click-name="clickName" />
<van-tree-select v-model:main-active-index="activeIndex" height="95vw" :items="items">
<template #content>
<top-condition-content :active-index="0" :item-data="positionData"/>
</template>
</van-tree-select>
</div>
</van-dropdown-item>
<van-dropdown-item title="排序" ref="sortRef">
1
</van-dropdown-item>
<van-dropdown-item title="筛选" ref="filterRef">
1
</van-dropdown-item>
</van-dropdown-menu>
</div>
</template>
<style lang="less" scoped>
.top-condition {
width: 100%;
:deep(.van-dropdown-menu__bar) {
display: flex;
justify-content: space-between;
box-shadow: none;
}
:deep(.van-dropdown-menu__title) {
color: #333;
font-size: 13px;
font-weight: 500;
}
}
</style>
11.5 完成位置功能,复用top-condition-content组件
- top-condition
<script setup>
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia';
import { ref, computed } from 'vue';
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import useSearch from '@/stores/modules/search';
import near from '@/components/near/index.vue';
import TopConditionContent from '@/components/top-condition-content/index.vue'
const searchStore = useSearch()
const { searchConditions } = storeToRefs(searchStore)
const positionRef = ref()
const positionData = computed(() => {
return searchConditions.value[0]?.subGroups
})
const activeIndex = ref(0)
const activeInnerIndex = ref(0)
const items = [
{ text: '热门推荐' },
{ text: '观光景点' },
{ text: '商圈' },
{ text: '行政区' },
{ text: '地铁线路' },
{ text: '机场/车站' },
{ text: '高校' },
{ text: '医院' },
]
const subwayItems = [
{ text: '飞机场' },
{ text: '火车站' },
{ text: '汽车站' },
]
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
let clickName = suggestName.value
// 计算clickName
const route = useRoute()
const getclickName = computed(() => {
if (suggestName.value === '') {
if (route.query.clickName === '') {
return '位置'
}
return clickName = route.query.clickName
}
return clickName = suggestName.value
})
const sortRef = ref()
const filterRef = ref()
// const active = ref(0);
</script>
<template>
<div class="top-condition">
<van-dropdown-menu active-color="#ff9645">
<van-dropdown-item :title="getclickName" ref="positionRef">
<div class="content">
<near :click-name="clickName" />
<van-tree-select v-model:main-active-index="activeIndex" height="100vw" :items="items">
<template #content>
<top-condition-content :active-index="activeIndex === 0" :item-data="positionData[0].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 1" :item-data="positionData[1].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 2" :item-data="positionData[2].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 3" :item-data="positionData[3].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 4" :item-data="positionData[4].subGroups"
:position-ref="positionRef" />
<van-tree-select v-model:main-active-index="activeInnerIndex" height="100vw" :items="subwayItems"
v-if="activeIndex === 5">
<template #content>
<top-condition-content :active-index="activeInnerIndex === 0"
:item-data="positionData[5].subGroups[0].items" :position-ref="positionRef" />
<top-condition-content :active-index="activeInnerIndex === 1"
:item-data="positionData[5].subGroups[1].items" :position-ref="positionRef" />
<top-condition-content :active-index="activeInnerIndex === 2"
:item-data="positionData[5].subGroups[2].items" :position-ref="positionRef" />
</template>
</van-tree-select>
<top-condition-content :active-index="activeIndex === 6" :item-data="positionData[6].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 7" :item-data="positionData[7].items"
:position-ref="positionRef" />
</template>
</van-tree-select>
</div>
</van-dropdown-item>
<van-dropdown-item title="排序" ref="sortRef">
1
</van-dropdown-item>
<van-dropdown-item title="筛选" ref="filterRef">
1
</van-dropdown-item>
</van-dropdown-menu>
</div>
</template>
<style lang="less" scoped>
.top-condition {
width: 100%;
:deep(.van-dropdown-menu__bar) {
display: flex;
justify-content: space-between;
box-shadow: none;
}
:deep(.van-dropdown-menu__title) {
color: #333;
font-size: 13px;
font-weight: 500;
}
}
</style>
- top-condition-content
<script setup>
import usekeywordsuggests from '@/stores/modules/keyword-suggests';
import { storeToRefs } from 'pinia';
import { ref } from 'vue'
const props = defineProps({
activeIndex: {
type: Boolean,
default: true
},
itemData: {
type: Array,
default: () => ([])
},
clickName: {
type: String,
default: ''
},
positionRef: {
type: Object,
default: () => ({})
}
})
const rightActiveIndex = ref()
const keywordsuggestsStore = usekeywordsuggests()
const { suggestName } = storeToRefs(keywordsuggestsStore)
let clickName = suggestName.value
// 点击选中并改变上面的初始值
const rightClick = (index, label) => {
rightActiveIndex.value = index
// 修改公共的值
suggestName.value = label
// 修改下拉菜单的值
// props.clickName = label
clickName = label
// 点击后隐藏下拉框
props.positionRef.toggle()
}
</script>
<template>
<div class="right">
<template v-for="(item, index) in itemData" :key="index">
<div class="right-content" @click="rightClick(index, item.label)" :class="{ active: rightActiveIndex === index }"
v-if="activeIndex">
<div class="sub" :class="{ subactive: rightActiveIndex === index }">{{ item.label }}</div>
<div class="sup" v-if="item.percentageUser">{{ item.percentageUser }}</div>
</div>
</template>
</div>
</template>
<style lang="less" scoped>
.right {
margin: 0 20px 0 12px;
.right-content {
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
height: 55px;
padding: 0 10px 0 10px;
font-size: 14px;
&.active {
font-weight: 500;
background-color: #fffcf5;
}
.sub {
color: #666;
&.subactive {
color: #ff9645;
}
}
.sup {
line-height: 20px;
font-size: 11px;
color: #999;
}
}
}
</style>
11.6 提取组件
把位置的代码提取组件
- top-position-treeselect
<script setup>
import useSearch from '@/stores/modules/search';
import { storeToRefs } from 'pinia';
import { ref, computed } from 'vue';
import TopConditionContent from '@/components/top-condition-content/index.vue'
defineProps({
positionRef: {
type: Object,
default: () => ({})
}
})
const searchStore = useSearch()
const { searchConditions } = storeToRefs(searchStore)
const positionData = computed(() => {
return searchConditions.value[0]?.subGroups
})
const activeIndex = ref(0)
const activeInnerIndex = ref(0)
const items = [
{ text: '热门推荐' },
{ text: '观光景点' },
{ text: '商圈' },
{ text: '行政区' },
{ text: '地铁线路' },
{ text: '机场/车站' },
{ text: '高校' },
{ text: '医院' },
]
const subwayItems = [
{ text: '飞机场' },
{ text: '火车站' },
{ text: '汽车站' },
]
</script>
<template>
<van-tree-select v-model:main-active-index="activeIndex" height="100vw" :items="items">
<template #content>
<top-condition-content :active-index="activeIndex === 0" :item-data="positionData[0].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 1" :item-data="positionData[1].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 2" :item-data="positionData[2].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 3" :item-data="positionData[3].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 4" :item-data="positionData[4].subGroups"
:position-ref="positionRef" />
<van-tree-select v-model:main-active-index="activeInnerIndex" height="100vw" :items="subwayItems"
v-if="activeIndex === 5">
<template #content>
<top-condition-content :active-index="activeInnerIndex === 0" :item-data="positionData[5].subGroups[0].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeInnerIndex === 1" :item-data="positionData[5].subGroups[1].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeInnerIndex === 2" :item-data="positionData[5].subGroups[2].items"
:position-ref="positionRef" />
</template>
</van-tree-select>
<top-condition-content :active-index="activeIndex === 6" :item-data="positionData[6].items"
:position-ref="positionRef" />
<top-condition-content :active-index="activeIndex === 7" :item-data="positionData[7].items"
:position-ref="positionRef" />
</template>
</van-tree-select>
</template>
<style lang="less" scoped></style>
- top-condition使用组件
import TopPositionTreeselect from '@/components/top-position-treeselect.vue/index.vue'
<top-position-treeselect :position-ref="positionRef"/>
11.7 排序
- top-sort
<script setup>
import { ref } from 'vue'
const props = defineProps({
itemData: {
type: Array,
default: () => ([])
},
})
const sortRef = ref()
const curIndex = ref(0)
const title = ref('欢迎度排序')
const sortItemClick = (label, index) => {
title.value = label
curIndex.value = index
sortRef.value.toggle()
}
</script>
<template>
<van-dropdown-item :title="title" ref="sortRef">
<div class="sort-section">
<template v-for="(item, index) in itemData">
<div class="item" @click="sortItemClick(item.label, index)" :class="{ active: curIndex === index }">
<div class="text">{{ item.label }}</div>
<van-icon name="success" />
</div>
</template>
</div>
</van-dropdown-item>
</template>
<style lang="less" scoped>
.sort-section {
padding: 0 10px;
background-color: #fff;
.item {
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
padding: 0 20px;
font-size: 14px;
color: #666;
&.active {
background-color: rgba(255, 176, 0, 0.04);
color: #ff9645;
}
}
}
</style>
- top-condition使用
import TopSort from '@/components/top-sort/index.vue'
<top-sort :item-data="searchConditions[1]?.items" />