| <template> | |
| 	<view class="page__view"> | |
| 
 | |
|     <view class="header"> | |
|       <view class="filter"> | |
|         <view class="filter-header"> | |
|           <view class="bar"> | |
|             <view> | |
|               <button :class="['btn', isFold ? 'is-fold' : '']" @click="isFold = !isFold"> | |
|                 <text>筛选</text> | |
|                 <image class="btn-icon" :src="isFold ? '/static/image/icon-arrow-down.png' : '/static/image/icon-arrow-up-light.png'" mode="widthFix"></image> | |
|               </button> | |
|             </view> | |
|             <view class="title">分类</view> | |
|           </view> | |
|         </view> | |
|         <view v-if="!isFold" class="filter-content"> | |
|           <view class="filter-item" v-for="filter in filters" :key="filter.id"> | |
|             <view class="filter-item-label">{{ `${filter.label}:` }}</view> | |
|             <view class="filter-item-content"> | |
|               <template v-if="filter.key === 'price'"> | |
|                 <view class="flex range price"> | |
|                   <view class="range-item"> | |
|                     <uv-input  | |
|                       v-model="startPrice" | |
|                       type="number" | |
|                       inputAlign="center" | |
|                       placeholder="开始价格" | |
|                       placeholderStyle="color: #181818; font-size: 28rpx; font-weight: 400;" | |
|                       :customStyle="{ | |
|                         backgroundColor: 'transparent', | |
|                         padding: '0', | |
|                         boxSizing: 'border-box', | |
|                         fontSize: '28rpx', | |
|                         border: 'none', | |
|                       }" | |
|                       fontSize="28rpx" | |
|                       :clearable="true" | |
|                     ></uv-input> | |
|                   </view> | |
|                   <view class="split">至</view> | |
|                   <view class="range-item"> | |
|                     <uv-input  | |
|                       v-model="endPrice" | |
|                       type="number" | |
|                       inputAlign="center" | |
|                       placeholder="结束价格" | |
|                       placeholderStyle="color: #181818; font-size: 28rpx; font-weight: 400;" | |
|                       :customStyle="{ | |
|                         backgroundColor: 'transparent', | |
|                         padding: '0', | |
|                         boxSizing: 'border-box', | |
|                         fontSize: '28rpx', | |
|                         border: 'none', | |
|                       }" | |
|                       fontSize="28rpx" | |
|                       :clearable="true" | |
|                     ></uv-input> | |
|                   </view> | |
|                 </view> | |
|               </template> | |
|               <template v-else-if="filter.key === 'time'"> | |
|                 <view class="flex range time"> | |
|                   <view class="range-item" @click="openStartDatePicker"> | |
|                     {{ startDate ? $dayjs(startDate).format('YYYY-MM-DD') : '开始日期' }} | |
|                     <button v-if="startDate" class="btn btn-clear" @click.stop="onClearStartDate"> | |
|                       <uv-icon name="close-circle" color="#B5B5B5" size="28rpx"></uv-icon> | |
|                     </button> | |
|                   </view> | |
|                   <view class="split">至</view> | |
|                   <view class="range-item" @click="openEndDatePicker"> | |
|                     {{ endDate ? $dayjs(endDate).format('YYYY-MM-DD') : '结束日期' }} | |
|                     <button v-if="endDate" class="btn btn-clear" @click.stop="onClearEndDate"> | |
|                       <uv-icon name="close-circle" color="#B5B5B5" size="28rpx"></uv-icon> | |
|                     </button> | |
|                   </view> | |
|                 </view> | |
|                 <uv-datetime-picker  | |
|                   ref="startDatePicker"  | |
|                   v-model="startDate"  | |
|                   mode="date"  | |
|                   title="开始日期" | |
|                   confirmColor="#00A9FF" | |
|                   round="32rpx" | |
|                   :minDate="minTime" | |
|                   @confirm="onStartDateChange" | |
|                 ></uv-datetime-picker> | |
|                 <uv-datetime-picker  | |
|                   ref="endDatePicker"  | |
|                   v-model="endDate"  | |
|                   mode="date"  | |
|                   title="结束日期" | |
|                   confirmColor="#00A9FF" | |
|                   round="32rpx" | |
|                   :minDate="startDate || minTime" | |
|                   @confirm="onEndDateChange" | |
|                 ></uv-datetime-picker> | |
|               </template> | |
|               <template v-else> | |
|                 <view class="option"> | |
|                   <view  | |
|                     v-for="option in filter.options"  | |
|                     :key="option.id" | |
|                     :class="['option-item', option.id == queryParams[filter.key] ? 'is-active' : '']" | |
|                     @click="onClickFilter(filter.key, option.id)" | |
|                   > | |
|                     {{ option.label }} | |
|                   </view> | |
|                 </view> | |
|               </template> | |
|             </view> | |
|           </view> | |
|  | |
|           <button class="flex btn btn-fold" @click="isFold = false"> | |
|             <image class="btn-icon" src="@/static/image/icon-arrow-up.png" mode="widthFix"></image> | |
|           </button> | |
|         </view> | |
|       </view> | |
|  | |
|       <view class="sort"> | |
|         <sortBar v-model="queryParams.sort" @change="onSortChange"></sortBar> | |
|       </view> | |
|     </view> | |
|      | |
| 		<!-- 分类商品列表 --> | |
| 		<view class="main" > | |
|  | |
| 			<uv-vtabs | |
| 				:list="categoryList"  | |
| 				keyName="name"  | |
| 				:current="current" | |
| 				:chain="true" | |
| 				@change="change" | |
| 				 | |
| 				barWidth="177rpx" | |
| 				barBgColor="#F5F5F5" | |
| 				:barItemStyle="{ | |
| 					color: '#1D2129', | |
| 					fontSize: '28rpx', | |
| 					fontWeight: 400, | |
| 				}" | |
| 				:barItemActiveStyle="{ | |
| 					color: '#00A9FF', | |
| 					fontWeight: 600, | |
| 					backgroundColor: '#FFFFFF', | |
| 				}" | |
| 				:barItemActiveLineStyle="{ | |
| 					background: '#00A9FF', | |
| 					margin: '48rpx 4rpx', | |
|           borderRadius: '4rpx', | |
| 				}" | |
| 			> | |
| 				<uv-vtabs-item v-for="(item, index) in categoryList" :index="index" :key="item.id"> | |
| 					<template v-if="item.children.length"> | |
|             <view class="card" v-for="product in item.children" :key="product.id" > | |
|               <productCard :data="product" ></productCard> | |
|             </view> | |
| 					</template> | |
| 					<template v-else> | |
| 						<uv-empty text="还没有呢"/> | |
| 					</template> | |
| 				</uv-vtabs-item> | |
| 			</uv-vtabs> | |
| 		</view> | |
|  | |
| 		<!-- tabbar --> | |
| 		<tabber select="category" /> | |
| 	</view> | |
| </template> | |
|  | |
| <script> | |
|  | |
| 	// import mixinsList from '@/mixins/list.js' | |
| 	import { mapState } from 'vuex' | |
|  | |
| 	import tabber from '@/components/base/tabbar.vue' | |
| 	import sortBar from '@/components/product/sortBar.vue' | |
| 	import productCard from '@/components/product/productCard.vue' | |
| 	 | |
|   export default { | |
| 		// mixins: [mixinsList], | |
| 		components: { | |
|       sortBar, | |
| 			productCard, | |
| 			tabber, | |
| 		}, | |
| 		data() { | |
| 			return { | |
| 				current: 0, | |
|         startPrice: null, | |
|         endPrice: null, | |
|         startDate: null, | |
|         endDate: null, | |
|         minTime: new Date().getTime(), | |
| 				queryParams: { | |
| 					pageNo: 1, | |
| 					pageSize: 1000, | |
| 					// todo | |
|           sort: 'comprehensive', | |
| 				}, | |
| 				categoryList: [], | |
|         filters: [], | |
|         isFold: true, | |
| 			} | |
| 		}, | |
| 		async onLoad({ categoryId }) { | |
| 			if(uni.getStorageSync('token')){ | |
| 				this.$store.commit('getUserInfo') | |
| 			} | |
|  | |
|       await Promise.allSettled([this.fetchCategoryList(), this.fetchFilters()]) | |
|       console.log('categoryList', this.categoryList) | |
|       console.log('filters', this.filters) | |
|  | |
| 			await this.initList() | |
|  | |
| 			if(this.categoryList.length > 0 && categoryId){ | |
|         setTimeout(() => { | |
|           this.current = this.categoryList.findIndex(item => item.id === categoryId) | |
|         }, 800) | |
|         // this.$nextTick(() => { | |
|         //   this.current = this.categoryList.findIndex(item => item.id === categoryId) | |
|         // }) | |
| 			} | |
| 		}, | |
| 		methods: { | |
| 			async fetchCategoryList() { | |
| 				try { | |
| 					this.categoryList = (await this.$fetch('queryCategoryList', { pageSize: 1000 }))?.records?.map(item => ({ id: item.id, name: item.title, children: [] })) | |
| 				} catch(err) { | |
| 					this.categoryList = [] | |
| 				} | |
|         return | |
|  | |
|         this.categoryList = [ | |
|           { | |
|             "key": "1962345168240185345", | |
|             "title": "国际游", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345225345634305", | |
|             "title": "夏令营", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345290571255810", | |
|             "title": "周末营", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345372007862273", | |
|             "title": "周边游", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345497681793025", | |
|             "title": "定制游", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345589524467714", | |
|             "title": "周末活动", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345642188148737", | |
|             "title": "亲子活动", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962345709817106434", | |
|             "title": "主题研学", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962346300198948866", | |
|             "title": "社会实践", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962346769759670273", | |
|             "title": "研学交流", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962346834884628481", | |
|             "title": "周末研学", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962346960097185793", | |
|             "title": "假期专享", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           }, | |
|           { | |
|             "key": "1962347024639135745", | |
|             "title": "本地研学", | |
|             "icon": null, | |
|             "parentId": "0", | |
|             "value": null, | |
|             "code": null, | |
|             "children": null, | |
|             "leaf": true | |
|           } | |
|         ].map(item => { | |
|           const { key, title } = item | |
|  | |
|           return { | |
|             id: key, | |
|             name: title, | |
|             children: [] | |
|           } | |
|         }) | |
|  | |
| 			}, | |
|       async fetchFilters() { | |
|  | |
|         this.filters = [ | |
|           { | |
|             id: '001', | |
|             key: 'frontier', | |
|             label: '国境', | |
|             options: [ | |
|               { | |
|                 id: '00101', | |
|                 label: '国内', | |
|               }, | |
|               { | |
|                 id: '00102', | |
|                 label: '国外', | |
|               }, | |
|             ], | |
|           }, | |
|           { | |
|             id: '002', | |
|             key: 'addressId', | |
|             label: '目的地', | |
|             options: [ | |
|               { | |
|                 label: '全部', | |
|               }, | |
|               { | |
|                 id: '00201', | |
|                 label: '上海', | |
|               }, | |
|               { | |
|                 id: '00202', | |
|                 label: '北京', | |
|               }, | |
|               { | |
|                 id: '00203', | |
|                 label: '浙江省', | |
|               }, | |
|               { | |
|                 id: '00204', | |
|                 label: '广东省', | |
|               }, | |
|               { | |
|                 id: '00205', | |
|                 label: '广西省', | |
|               }, | |
|               { | |
|                 id: '00206', | |
|                 label: '云南省', | |
|               }, | |
|             ], | |
|           }, | |
|           { | |
|             id: '003', | |
|             key: 'ageId', | |
|             label: '适合年龄', | |
|             options: [ | |
|               { | |
|                 label: '全部', | |
|               }, | |
|               { | |
|                 id: '00301', | |
|                 label: '6-10岁', | |
|               }, | |
|               { | |
|                 id: '00302', | |
|                 label: '11-14岁', | |
|               }, | |
|               { | |
|                 id: '00303', | |
|                 label: '15-16岁', | |
|               }, | |
|               { | |
|                 id: '00304', | |
|                 label: '17-18岁', | |
|               }, | |
|             ], | |
|           }, | |
|           { | |
|             id: '004', | |
|             key: 'timeId', | |
|             label: '活动时长', | |
|             options: [ | |
|               { | |
|                 label: '全部', | |
|               }, | |
|               { | |
|                 id: '00401', | |
|                 label: '1日', | |
|               }, | |
|               { | |
|                 id: '00402', | |
|                 label: '多日', | |
|               }, | |
|               { | |
|                 id: '00403', | |
|                 label: '寒假', | |
|               }, | |
|               { | |
|                 id: '00404', | |
|                 label: '暑假', | |
|               }, | |
|             ], | |
|           }, | |
|           { | |
|             id: '005', | |
|             key: 'price', | |
|             label: '价格区间', | |
|           }, | |
|           { | |
|             id: '006', | |
|             key: 'time', | |
|             label: '出发日期', | |
|           }, | |
|         ] | |
|  | |
|         this.filters.forEach(item => { | |
|           const { key, options } = item | |
|            | |
|           if (!options?.length || !options[0]?.id) { | |
|             return | |
|           } | |
|  | |
|           this.queryParams[key] = options[0].id | |
|         }) | |
|  | |
|         // todo: fetch | |
|       }, | |
| 			async queryProductList(categoryId) { | |
| 				try { | |
| 					return (await this.$fetch('queryActivityList', { ...this.queryParams, categoryId }))?.records || [] | |
| 				} catch (err) { | |
| 					return [] | |
| 				} | |
| 			}, | |
| 			async initList() { | |
|         console.log('queryParams', this.queryParams) | |
|         const results = await Promise.allSettled(this.categoryList.map(category => { return this.queryProductList(category.id) })) | |
|          | |
|         results.forEach((result, index) => { | |
|           this.categoryList[index].children = result.value || [] | |
|         }) | |
|          | |
|         console.log('categoryList', this.categoryList) | |
| 			}, | |
| 			change(e) { | |
| 				this.current = e | |
| 			}, | |
| 			search(){ | |
|         // todo: set filter | |
| 				this.initList() | |
| 			}, | |
|       onClickFilter(key, val) { | |
|         if (val) { | |
|           this.queryParams[key] = val | |
|         } else { | |
|           delete this.queryParams[key] | |
|         } | |
|         this.initList() | |
|       }, | |
|       openStartDatePicker() { | |
|         this.$refs.startDatePicker?.[0]?.open?.(); | |
|       }, | |
|       onStartDateChange(e) { | |
|         const date = e.value | |
|  | |
|         this.queryParams.startDate = date | |
|  | |
|         const { endDate } = this.queryParams | |
|  | |
|         if (endDate && this.$dayjs(date).isAfter(endDate, 'day')) { | |
|           this.endDate = null | |
|           delete this.queryParams.endDate | |
|         } | |
|  | |
|         this.initList() | |
|       }, | |
|       onClearStartDate() { | |
|         this.startDate = null | |
|  | |
|         delete this.queryParams.startDate | |
|  | |
|         this.initList() | |
|       }, | |
|       openEndDatePicker() { | |
|         this.$refs.endDatePicker?.[0]?.open?.(); | |
|       }, | |
|       onEndDateChange(e) { | |
|         const date = e.value | |
|  | |
|         this.queryParams.endDate = date | |
|  | |
|         const { startDate } = this.queryParams | |
|  | |
|         if (startDate && this.$dayjs(date).isBefore(startDate, 'day')) { | |
|           this.startDate = null | |
|           delete this.queryParams.startDate | |
|         } | |
|  | |
|         this.initList() | |
|       }, | |
|       onClearEndDate() { | |
|         this.endDate = null | |
|  | |
|         delete this.queryParams.endDate | |
|  | |
|         this.initList() | |
|       }, | |
|       onSortChange(sort) { | |
|         console.log('onSortChange', sort) | |
| 				// todo set sort | |
|         this.getData() | |
|       }, | |
| 		} | |
| 	} | |
| </script> | |
|  | |
| <style scoped lang="scss"> | |
| 	.page__view { | |
|     height: 100vh; | |
| 	  background: linear-gradient(#DAF3FF, #FBFEFF 200rpx, #FBFEFF); | |
| 
 | |
|     /deep/ .uv-popup { | |
|       z-index: 1000000 !important; | |
|     } | |
| 	} | |
|    | |
|   .header { | |
|     width: 100%; | |
|     padding: 0 32rpx; | |
|     box-sizing: border-box; | |
|   } | |
| 
 | |
|   .filter { | |
| 
 | |
|     &-header { | |
|       display: flex; | |
|       flex-direction: column; | |
|       justify-content: flex-end; | |
|       height: 176rpx; | |
|       padding-bottom: 12rpx; | |
|       box-sizing: border-box; | |
| 
 | |
|       .bar { | |
|         display: grid; | |
|         grid-template-columns: repeat(3, 1fr); | |
| 
 | |
|         .btn { | |
|           display: inline-flex; | |
|           align-items: center; | |
|           column-gap: 8rpx; | |
|           padding: 8rpx 30rpx; | |
|           font-size: 28rpx; | |
|           font-weight: 500; | |
|           color: #FFFFFF; | |
|           background: #00A9FF; | |
|           border: 2rpx solid #00A9FF; | |
|           border-radius: 64rpx; | |
| 
 | |
|           &-icon { | |
|             width: 32rpx; | |
|             height: auto; | |
|           } | |
| 
 | |
|           &.is-fold { | |
|             font-weight: 400; | |
|             color: #191919; | |
|             background: #D8F2FF; | |
|             border-color: #00A9FF66; | |
|           } | |
|         } | |
| 
 | |
|         .title { | |
|           text-align: center; | |
|           font-size: 32rpx; | |
|           font-weight: 600; | |
|           color: #191919; | |
|         } | |
|       } | |
|     } | |
| 
 | |
|     &-content { | |
|       margin-top: 24rpx; | |
| 
 | |
|       .btn { | |
|         &-fold { | |
|           margin-top: 32rpx; | |
|           width: 100%; | |
|         } | |
| 
 | |
|         &-icon { | |
|           width: 40rpx; | |
|           height: auto; | |
|         } | |
|       } | |
|     } | |
| 
 | |
|     &-item { | |
|       display: flex; | |
| 
 | |
|       & + & { | |
|         margin-top: 32rpx; | |
|       } | |
| 
 | |
|       &-label { | |
|         width: 156rpx; | |
|         min-height: 64rpx; | |
|         line-height: 64rpx; | |
|         flex: none; | |
|       } | |
| 
 | |
|       &-content { | |
|         flex: 1; | |
| 
 | |
|         .option { | |
|           margin-top: 6rpx; | |
|           display: flex; | |
|           flex-wrap: wrap; | |
|           gap: 24rpx; | |
| 
 | |
|           &-item { | |
|             padding: 8rpx 16rpx; | |
|             font-size: 28rpx; | |
|             color: #181818; | |
|             border-radius: 4rpx; | |
| 
 | |
|             &.is-active { | |
|               color: #FFFFFF; | |
|               background: #00A9FF; | |
|             } | |
|           } | |
|         } | |
| 
 | |
|         .range { | |
|           margin: 4rpx 0; | |
|           column-gap: 8rpx; | |
|           border-bottom: 2rpx solid #EEEEEE; | |
|            | |
|           &-item { | |
|             width: 220rpx; | |
|             box-sizing: border-box; | |
|           } | |
| 
 | |
|           .split { | |
|             padding: 0 24rpx; | |
|             font-size: 32rpx; | |
|             line-height: 1; | |
|             color: #8B8B8B; | |
|           } | |
|            | |
|           &.price { | |
|             .range-item { | |
|               padding: 4rpx 0; | |
|             } | |
|           } | |
| 
 | |
|           &.time { | |
|             .range-item { | |
|               // width: 220rpx; | |
|               padding: 8rpx 0; | |
|               text-align: center; | |
|               font-size: 28rpx; | |
|               color: #181818; | |
| 
 | |
|               .btn-clear { | |
|                 margin: 6rpx 0; | |
|                 float: right; | |
|               } | |
|             } | |
|           } | |
|         } | |
|       } | |
|     } | |
|   } | |
| 
 | |
|   .sort { | |
|     width: 100%; | |
|     height: 116rpx; | |
|     padding: 24rpx 0; | |
|     box-sizing: border-box; | |
|   } | |
| 
 | |
|   .main { | |
| 		/deep/ .uv-vtabs, | |
| 		/deep/ .uv-vtabs__bar, | |
| 		/deep/ .uv-vtabs__content { | |
| 			height: calc(100vh - 292rpx - #{$tabbar-height} - env(safe-area-inset-bottom)) !important; | |
| 		} | |
| 
 | |
| 		/deep/ .uv-vtabs__bar { | |
| 			background: #F6F6F6 !important; | |
| 		} | |
| 
 | |
|     /deep/ .uv-vtabs__bar-item { | |
|       padding: 48rpx 32rpx; | |
|     } | |
| 
 | |
| 		/deep/ .uv-vtabs__content { | |
|       padding: 24rpx 24rpx 0 24rpx; | |
|       box-sizing: border-box; | |
|       background: linear-gradient(#DAF3FF, #F4F4F4 250rpx, #F4F4F4); | |
|     } | |
|   } | |
| 
 | |
|   .card { | |
|     & + & { | |
|       margin-top: 32rpx; | |
|     } | |
|      | |
|     &:last-child { | |
|       padding-bottom: 24rpx; | |
|     } | |
|   } | |
| 
 | |
| </style> |