鸿宇研学生前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

765 lines
19 KiB

  1. <template>
  2. <view class="page__view">
  3. <view class="header">
  4. <view class="filter">
  5. <view class="filter-header">
  6. <view class="bar">
  7. <view>
  8. <button :class="['btn', isFold ? 'is-fold' : '']" @click="isFold = !isFold">
  9. <text>筛选</text>
  10. <image class="btn-icon" :src="isFold ? '/static/image/icon-arrow-down.png' : '/static/image/icon-arrow-up-light.png'" mode="widthFix"></image>
  11. </button>
  12. </view>
  13. <view class="title">分类</view>
  14. </view>
  15. </view>
  16. <view v-if="!isFold" class="filter-content">
  17. <view class="filter-item" v-for="filter in filters" :key="filter.id">
  18. <view class="filter-item-label">{{ `${filter.label}` }}</view>
  19. <view class="filter-item-content">
  20. <template v-if="filter.key === 'price'">
  21. <view class="flex range price">
  22. <view class="range-item">
  23. <uv-input
  24. v-model="startPrice"
  25. type="number"
  26. inputAlign="center"
  27. placeholder="开始价格"
  28. placeholderStyle="color: #181818; font-size: 28rpx; font-weight: 400;"
  29. :customStyle="{
  30. backgroundColor: 'transparent',
  31. padding: '0',
  32. boxSizing: 'border-box',
  33. fontSize: '28rpx',
  34. border: 'none',
  35. }"
  36. fontSize="28rpx"
  37. :clearable="true"
  38. ></uv-input>
  39. </view>
  40. <view class="split"></view>
  41. <view class="range-item">
  42. <uv-input
  43. v-model="endPrice"
  44. type="number"
  45. inputAlign="center"
  46. placeholder="结束价格"
  47. placeholderStyle="color: #181818; font-size: 28rpx; font-weight: 400;"
  48. :customStyle="{
  49. backgroundColor: 'transparent',
  50. padding: '0',
  51. boxSizing: 'border-box',
  52. fontSize: '28rpx',
  53. border: 'none',
  54. }"
  55. fontSize="28rpx"
  56. :clearable="true"
  57. ></uv-input>
  58. </view>
  59. </view>
  60. </template>
  61. <template v-else-if="filter.key === 'time'">
  62. <view class="flex range time">
  63. <view class="range-item" @click="openStartDatePicker">
  64. {{ startDate ? $dayjs(startDate).format('YYYY-MM-DD') : '开始日期' }}
  65. <button v-if="startDate" class="btn btn-clear" @click.stop="onClearStartDate">
  66. <uv-icon name="close-circle" color="#B5B5B5" size="28rpx"></uv-icon>
  67. </button>
  68. </view>
  69. <view class="split"></view>
  70. <view class="range-item" @click="openEndDatePicker">
  71. {{ endDate ? $dayjs(endDate).format('YYYY-MM-DD') : '结束日期' }}
  72. <button v-if="endDate" class="btn btn-clear" @click.stop="onClearEndDate">
  73. <uv-icon name="close-circle" color="#B5B5B5" size="28rpx"></uv-icon>
  74. </button>
  75. </view>
  76. </view>
  77. <uv-datetime-picker
  78. ref="startDatePicker"
  79. v-model="startDate"
  80. mode="date"
  81. title="开始日期"
  82. confirmColor="#00A9FF"
  83. round="32rpx"
  84. :minDate="minTime"
  85. @confirm="onStartDateChange"
  86. ></uv-datetime-picker>
  87. <uv-datetime-picker
  88. ref="endDatePicker"
  89. v-model="endDate"
  90. mode="date"
  91. title="结束日期"
  92. confirmColor="#00A9FF"
  93. round="32rpx"
  94. :minDate="startDate || minTime"
  95. @confirm="onEndDateChange"
  96. ></uv-datetime-picker>
  97. </template>
  98. <template v-else>
  99. <view class="option">
  100. <view
  101. v-for="option in filter.options"
  102. :key="option.id"
  103. :class="['option-item', option.id == queryParams[filter.key] ? 'is-active' : '']"
  104. @click="onClickFilter(filter.key, option.id)"
  105. >
  106. {{ option.label }}
  107. </view>
  108. </view>
  109. </template>
  110. </view>
  111. </view>
  112. <button class="flex btn btn-fold" @click="isFold = false">
  113. <image class="btn-icon" src="@/static/image/icon-arrow-up.png" mode="widthFix"></image>
  114. </button>
  115. </view>
  116. </view>
  117. <view class="sort">
  118. <sortBar v-model="queryParams.sort" @change="onSortChange"></sortBar>
  119. </view>
  120. </view>
  121. <!-- 分类商品列表 -->
  122. <view class="main" >
  123. <uv-vtabs
  124. :list="categoryList"
  125. keyName="name"
  126. :current="current"
  127. :chain="true"
  128. @change="change"
  129. barWidth="177rpx"
  130. barBgColor="#F5F5F5"
  131. :barItemStyle="{
  132. color: '#1D2129',
  133. fontSize: '28rpx',
  134. fontWeight: 400,
  135. }"
  136. :barItemActiveStyle="{
  137. color: '#00A9FF',
  138. fontWeight: 600,
  139. backgroundColor: '#FFFFFF',
  140. }"
  141. :barItemActiveLineStyle="{
  142. background: '#00A9FF',
  143. margin: '48rpx 4rpx',
  144. borderRadius: '4rpx',
  145. }"
  146. >
  147. <uv-vtabs-item v-for="(item, index) in categoryList" :index="index" :key="item.id">
  148. <template v-if="item.children.length">
  149. <view class="card" v-for="product in item.children" :key="product.id" >
  150. <productCard :data="product" ></productCard>
  151. </view>
  152. </template>
  153. <template v-else>
  154. <uv-empty text="还没有呢"/>
  155. </template>
  156. </uv-vtabs-item>
  157. </uv-vtabs>
  158. </view>
  159. <!-- tabbar -->
  160. <tabber select="category" />
  161. </view>
  162. </template>
  163. <script>
  164. // import mixinsList from '@/mixins/list.js'
  165. import { mapState } from 'vuex'
  166. import tabber from '@/components/base/tabbar.vue'
  167. import sortBar from '@/components/product/sortBar.vue'
  168. import productCard from '@/components/product/productCard.vue'
  169. export default {
  170. // mixins: [mixinsList],
  171. components: {
  172. sortBar,
  173. productCard,
  174. tabber,
  175. },
  176. data() {
  177. return {
  178. current: 0,
  179. startPrice: null,
  180. endPrice: null,
  181. startDate: null,
  182. endDate: null,
  183. minTime: new Date().getTime(),
  184. queryParams: {
  185. pageNo: 1,
  186. pageSize: 1000,
  187. // todo
  188. sort: 'comprehensive',
  189. },
  190. categoryList: [],
  191. filters: [],
  192. isFold: true,
  193. }
  194. },
  195. async onLoad({ categoryId }) {
  196. await Promise.allSettled([this.fetchCategoryList(), this.fetchFilters()])
  197. console.log('categoryList', this.categoryList)
  198. console.log('filters', this.filters)
  199. await this.initList()
  200. if(this.categoryList.length > 0 && categoryId){
  201. setTimeout(() => {
  202. this.current = this.categoryList.findIndex(item => item.id === categoryId)
  203. }, 800)
  204. // this.$nextTick(() => {
  205. // this.current = this.categoryList.findIndex(item => item.id === categoryId)
  206. // })
  207. }
  208. },
  209. methods: {
  210. async fetchCategoryList() {
  211. this.categoryList = [
  212. {
  213. id: '001',
  214. name: '国际游',
  215. children: [],
  216. },
  217. {
  218. id: '002',
  219. name: '夏令营',
  220. children: [],
  221. },
  222. {
  223. id: '003',
  224. name: '周末营',
  225. children: [],
  226. },
  227. {
  228. id: '004',
  229. name: '周边游',
  230. children: [],
  231. },
  232. {
  233. id: '005',
  234. name: '定制游',
  235. children: [],
  236. },
  237. {
  238. id: '006',
  239. name: '周末活动',
  240. children: [],
  241. },
  242. {
  243. id: '007',
  244. name: '亲子活动',
  245. children: [],
  246. },
  247. {
  248. id: '008',
  249. name: '社会实践',
  250. children: [],
  251. },
  252. {
  253. id: '009',
  254. name: '主题研学',
  255. children: [],
  256. },
  257. {
  258. id: '010',
  259. name: '研学交流',
  260. children: [],
  261. },
  262. {
  263. id: '011',
  264. name: '假期专项',
  265. children: [],
  266. },
  267. {
  268. id: '012',
  269. name: '本地研学',
  270. children: [],
  271. },
  272. ]
  273. return
  274. try {
  275. this.categoryList = (await this.$fetch('getCategoryList', { pageSize: 1000 }))?.records?.map(item => ({ id: item.id, name: item.name, children: [] }))
  276. } catch(err) {
  277. this.categoryList = []
  278. }
  279. },
  280. async fetchFilters() {
  281. this.filters = [
  282. {
  283. id: '001',
  284. key: 'frontier',
  285. label: '国境',
  286. options: [
  287. {
  288. id: '00101',
  289. label: '国内',
  290. },
  291. {
  292. id: '00102',
  293. label: '国外',
  294. },
  295. ],
  296. },
  297. {
  298. id: '002',
  299. key: 'addressId',
  300. label: '目的地',
  301. options: [
  302. {
  303. label: '全部',
  304. },
  305. {
  306. id: '00201',
  307. label: '上海',
  308. },
  309. {
  310. id: '00202',
  311. label: '北京',
  312. },
  313. {
  314. id: '00203',
  315. label: '浙江省',
  316. },
  317. {
  318. id: '00204',
  319. label: '广东省',
  320. },
  321. {
  322. id: '00205',
  323. label: '广西省',
  324. },
  325. {
  326. id: '00206',
  327. label: '云南省',
  328. },
  329. ],
  330. },
  331. {
  332. id: '003',
  333. key: 'ageId',
  334. label: '适合年龄',
  335. options: [
  336. {
  337. label: '全部',
  338. },
  339. {
  340. id: '00301',
  341. label: '6-10岁',
  342. },
  343. {
  344. id: '00302',
  345. label: '11-14岁',
  346. },
  347. {
  348. id: '00303',
  349. label: '15-16岁',
  350. },
  351. {
  352. id: '00304',
  353. label: '17-18岁',
  354. },
  355. ],
  356. },
  357. {
  358. id: '004',
  359. key: 'timeId',
  360. label: '活动时长',
  361. options: [
  362. {
  363. label: '全部',
  364. },
  365. {
  366. id: '00401',
  367. label: '1日',
  368. },
  369. {
  370. id: '00402',
  371. label: '多日',
  372. },
  373. {
  374. id: '00403',
  375. label: '寒假',
  376. },
  377. {
  378. id: '00404',
  379. label: '暑假',
  380. },
  381. ],
  382. },
  383. {
  384. id: '005',
  385. key: 'price',
  386. label: '价格区间',
  387. },
  388. {
  389. id: '006',
  390. key: 'time',
  391. label: '出发日期',
  392. },
  393. ]
  394. this.filters.forEach(item => {
  395. const { key, options } = item
  396. if (!options?.length || !options[0]?.id) {
  397. return
  398. }
  399. this.queryParams[key] = options[0].id
  400. })
  401. // todo: fetch
  402. },
  403. async queryProductList(categoryId) {
  404. return [
  405. {
  406. id: '001',
  407. image: '/static/image/temp-20.png',
  408. title: '新疆天山行7/9日丨醉美伊犁&吐鲁番双套餐',
  409. tagList: ['国内游','7-9天','12岁+'],
  410. priceDiscount: 688.99,
  411. priceOrigin: 1200,
  412. applyNum: 4168,
  413. },
  414. {
  415. id: '002',
  416. image: '/static/image/temp-20.png',
  417. title: '坝上双草原6日|乌兰布统+锡林郭勒+长城',
  418. tagList: ['国内游','7-9天','12岁+'],
  419. priceDiscount: 688.99,
  420. priceOrigin: 1200,
  421. applyNum: 4168,
  422. },
  423. {
  424. id: '003',
  425. image: '/static/image/temp-20.png',
  426. title: '牛湖线探秘 | 清远牛湖线徒步,探秘天坑与大草原',
  427. tagList: ['国内游','7-9天','12岁+'],
  428. priceDiscount: 688.99,
  429. priceOrigin: 1200,
  430. applyNum: 4168,
  431. },
  432. {
  433. id: '004',
  434. image: '/static/image/temp-20.png',
  435. title: '低海拔藏区草原,汉藏文化大穿越',
  436. tagList: ['国内游','7-9天','12岁+'],
  437. priceDiscount: 688.99,
  438. priceOrigin: 1200,
  439. applyNum: 4168,
  440. },
  441. {
  442. id: '005',
  443. image: '/static/image/temp-20.png',
  444. title: '新丝路到敦煌7日 | 甘青轻松穿越,沙漠+草原',
  445. tagList: ['国内游','7-9天','12岁+'],
  446. priceDiscount: 688.99,
  447. priceOrigin: 1200,
  448. applyNum: 4168,
  449. },
  450. {
  451. id: '006',
  452. image: '/static/image/temp-20.png',
  453. title: '呼伦贝尔6/8日|经典or环线双套餐可选',
  454. tagList: ['国内游','7-9天','12岁+'],
  455. priceDiscount: 688.99,
  456. priceOrigin: 1200,
  457. applyNum: 4168,
  458. },
  459. ]
  460. try {
  461. return (await this.$fetch('queryActivityList', { ...this.queryParams, categoryId }))?.records || []
  462. } catch (err) {
  463. return []
  464. }
  465. },
  466. async initList() {
  467. console.log('queryParams', this.queryParams)
  468. const results = await Promise.allSettled(this.categoryList.map(category => { return this.queryProductList(category.id) }))
  469. results.forEach((result, index) => {
  470. this.categoryList[index].children = result.value || []
  471. })
  472. console.log('categoryList', this.categoryList)
  473. },
  474. change(e) {
  475. this.current = e
  476. },
  477. search(){
  478. // todo: set filter
  479. this.initList()
  480. },
  481. onClickFilter(key, val) {
  482. if (val) {
  483. this.queryParams[key] = val
  484. } else {
  485. delete this.queryParams[key]
  486. }
  487. this.initList()
  488. },
  489. openStartDatePicker() {
  490. this.$refs.startDatePicker?.[0]?.open?.();
  491. },
  492. onStartDateChange(e) {
  493. const date = e.value
  494. this.queryParams.startDate = date
  495. const { endDate } = this.queryParams
  496. if (endDate && this.$dayjs(date).isAfter(endDate, 'day')) {
  497. this.endDate = null
  498. delete this.queryParams.endDate
  499. }
  500. this.initList()
  501. },
  502. onClearStartDate() {
  503. this.startDate = null
  504. delete this.queryParams.startDate
  505. this.initList()
  506. },
  507. openEndDatePicker() {
  508. this.$refs.endDatePicker?.[0]?.open?.();
  509. },
  510. onEndDateChange(e) {
  511. const date = e.value
  512. this.queryParams.endDate = date
  513. const { startDate } = this.queryParams
  514. if (startDate && this.$dayjs(date).isBefore(startDate, 'day')) {
  515. this.startDate = null
  516. delete this.queryParams.startDate
  517. }
  518. this.initList()
  519. },
  520. onClearEndDate() {
  521. this.endDate = null
  522. delete this.queryParams.endDate
  523. this.initList()
  524. },
  525. onSortChange(sort) {
  526. console.log('onSortChange', sort)
  527. // todo set sort
  528. this.getData()
  529. },
  530. }
  531. }
  532. </script>
  533. <style scoped lang="scss">
  534. .page__view {
  535. height: 100vh;
  536. background: linear-gradient(#DAF3FF, #FBFEFF 200rpx, #FBFEFF);
  537. /deep/ .uv-popup {
  538. z-index: 1000000 !important;
  539. }
  540. }
  541. .header {
  542. width: 100%;
  543. padding: 0 32rpx;
  544. box-sizing: border-box;
  545. }
  546. .filter {
  547. &-header {
  548. display: flex;
  549. flex-direction: column;
  550. justify-content: flex-end;
  551. height: 176rpx;
  552. padding-bottom: 12rpx;
  553. box-sizing: border-box;
  554. .bar {
  555. display: grid;
  556. grid-template-columns: repeat(3, 1fr);
  557. .btn {
  558. display: inline-flex;
  559. align-items: center;
  560. column-gap: 8rpx;
  561. padding: 8rpx 30rpx;
  562. font-size: 28rpx;
  563. font-weight: 500;
  564. color: #FFFFFF;
  565. background: #00A9FF;
  566. border: 2rpx solid #00A9FF;
  567. border-radius: 64rpx;
  568. &-icon {
  569. width: 32rpx;
  570. height: auto;
  571. }
  572. &.is-fold {
  573. font-weight: 400;
  574. color: #191919;
  575. background: #D8F2FF;
  576. border-color: #00A9FF66;
  577. }
  578. }
  579. .title {
  580. text-align: center;
  581. font-size: 32rpx;
  582. font-weight: 600;
  583. color: #191919;
  584. }
  585. }
  586. }
  587. &-content {
  588. margin-top: 24rpx;
  589. .btn {
  590. &-fold {
  591. margin-top: 32rpx;
  592. width: 100%;
  593. }
  594. &-icon {
  595. width: 40rpx;
  596. height: auto;
  597. }
  598. }
  599. }
  600. &-item {
  601. display: flex;
  602. & + & {
  603. margin-top: 32rpx;
  604. }
  605. &-label {
  606. width: 156rpx;
  607. min-height: 64rpx;
  608. line-height: 64rpx;
  609. flex: none;
  610. }
  611. &-content {
  612. flex: 1;
  613. .option {
  614. margin-top: 6rpx;
  615. display: flex;
  616. flex-wrap: wrap;
  617. gap: 24rpx;
  618. &-item {
  619. padding: 8rpx 16rpx;
  620. font-size: 28rpx;
  621. color: #181818;
  622. border-radius: 4rpx;
  623. &.is-active {
  624. color: #FFFFFF;
  625. background: #00A9FF;
  626. }
  627. }
  628. }
  629. .range {
  630. margin: 4rpx 0;
  631. column-gap: 8rpx;
  632. border-bottom: 2rpx solid #EEEEEE;
  633. &-item {
  634. width: 220rpx;
  635. box-sizing: border-box;
  636. }
  637. .split {
  638. padding: 0 24rpx;
  639. font-size: 32rpx;
  640. line-height: 1;
  641. color: #8B8B8B;
  642. }
  643. &.price {
  644. .range-item {
  645. padding: 4rpx 0;
  646. }
  647. }
  648. &.time {
  649. .range-item {
  650. // width: 220rpx;
  651. padding: 8rpx 0;
  652. text-align: center;
  653. font-size: 28rpx;
  654. color: #181818;
  655. .btn-clear {
  656. margin: 6rpx 0;
  657. float: right;
  658. }
  659. }
  660. }
  661. }
  662. }
  663. }
  664. }
  665. .sort {
  666. width: 100%;
  667. height: 116rpx;
  668. padding: 24rpx 0;
  669. box-sizing: border-box;
  670. }
  671. .main {
  672. /deep/ .uv-vtabs,
  673. /deep/ .uv-vtabs__bar,
  674. /deep/ .uv-vtabs__content {
  675. height: calc(100vh - 292rpx - #{$tabbar-height} - env(safe-area-inset-bottom)) !important;
  676. }
  677. /deep/ .uv-vtabs__bar {
  678. background: #F6F6F6 !important;
  679. }
  680. /deep/ .uv-vtabs__bar-item {
  681. padding: 48rpx 32rpx;
  682. }
  683. /deep/ .uv-vtabs__content {
  684. padding: 24rpx 24rpx 0 24rpx;
  685. box-sizing: border-box;
  686. background: linear-gradient(#DAF3FF, #F4F4F4 250rpx, #F4F4F4);
  687. }
  688. }
  689. .card {
  690. & + & {
  691. margin-top: 32rpx;
  692. }
  693. &:last-child {
  694. padding-bottom: 24rpx;
  695. }
  696. }
  697. </style>