鸿宇研学生前端代码
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.

844 lines
22 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. "key": "1962345168240185345",
  214. "title": "国际游",
  215. "icon": null,
  216. "parentId": "0",
  217. "value": null,
  218. "code": null,
  219. "children": null,
  220. "leaf": true
  221. },
  222. {
  223. "key": "1962345225345634305",
  224. "title": "夏令营",
  225. "icon": null,
  226. "parentId": "0",
  227. "value": null,
  228. "code": null,
  229. "children": null,
  230. "leaf": true
  231. },
  232. {
  233. "key": "1962345290571255810",
  234. "title": "周末营",
  235. "icon": null,
  236. "parentId": "0",
  237. "value": null,
  238. "code": null,
  239. "children": null,
  240. "leaf": true
  241. },
  242. {
  243. "key": "1962345372007862273",
  244. "title": "周边游",
  245. "icon": null,
  246. "parentId": "0",
  247. "value": null,
  248. "code": null,
  249. "children": null,
  250. "leaf": true
  251. },
  252. {
  253. "key": "1962345497681793025",
  254. "title": "定制游",
  255. "icon": null,
  256. "parentId": "0",
  257. "value": null,
  258. "code": null,
  259. "children": null,
  260. "leaf": true
  261. },
  262. {
  263. "key": "1962345589524467714",
  264. "title": "周末活动",
  265. "icon": null,
  266. "parentId": "0",
  267. "value": null,
  268. "code": null,
  269. "children": null,
  270. "leaf": true
  271. },
  272. {
  273. "key": "1962345642188148737",
  274. "title": "亲子活动",
  275. "icon": null,
  276. "parentId": "0",
  277. "value": null,
  278. "code": null,
  279. "children": null,
  280. "leaf": true
  281. },
  282. {
  283. "key": "1962345709817106434",
  284. "title": "主题研学",
  285. "icon": null,
  286. "parentId": "0",
  287. "value": null,
  288. "code": null,
  289. "children": null,
  290. "leaf": true
  291. },
  292. {
  293. "key": "1962346300198948866",
  294. "title": "社会实践",
  295. "icon": null,
  296. "parentId": "0",
  297. "value": null,
  298. "code": null,
  299. "children": null,
  300. "leaf": true
  301. },
  302. {
  303. "key": "1962346769759670273",
  304. "title": "研学交流",
  305. "icon": null,
  306. "parentId": "0",
  307. "value": null,
  308. "code": null,
  309. "children": null,
  310. "leaf": true
  311. },
  312. {
  313. "key": "1962346834884628481",
  314. "title": "周末研学",
  315. "icon": null,
  316. "parentId": "0",
  317. "value": null,
  318. "code": null,
  319. "children": null,
  320. "leaf": true
  321. },
  322. {
  323. "key": "1962346960097185793",
  324. "title": "假期专享",
  325. "icon": null,
  326. "parentId": "0",
  327. "value": null,
  328. "code": null,
  329. "children": null,
  330. "leaf": true
  331. },
  332. {
  333. "key": "1962347024639135745",
  334. "title": "本地研学",
  335. "icon": null,
  336. "parentId": "0",
  337. "value": null,
  338. "code": null,
  339. "children": null,
  340. "leaf": true
  341. }
  342. ].map(item => {
  343. const { key, title } = item
  344. return {
  345. id: key,
  346. name: title,
  347. children: []
  348. }
  349. })
  350. return
  351. try {
  352. this.categoryList = (await this.$fetch('getCategoryList', { pageSize: 1000 }))?.records?.map(item => ({ id: item.id, name: item.name, children: [] }))
  353. } catch(err) {
  354. this.categoryList = []
  355. }
  356. },
  357. async fetchFilters() {
  358. this.filters = [
  359. {
  360. id: '001',
  361. key: 'frontier',
  362. label: '国境',
  363. options: [
  364. {
  365. id: '00101',
  366. label: '国内',
  367. },
  368. {
  369. id: '00102',
  370. label: '国外',
  371. },
  372. ],
  373. },
  374. {
  375. id: '002',
  376. key: 'addressId',
  377. label: '目的地',
  378. options: [
  379. {
  380. label: '全部',
  381. },
  382. {
  383. id: '00201',
  384. label: '上海',
  385. },
  386. {
  387. id: '00202',
  388. label: '北京',
  389. },
  390. {
  391. id: '00203',
  392. label: '浙江省',
  393. },
  394. {
  395. id: '00204',
  396. label: '广东省',
  397. },
  398. {
  399. id: '00205',
  400. label: '广西省',
  401. },
  402. {
  403. id: '00206',
  404. label: '云南省',
  405. },
  406. ],
  407. },
  408. {
  409. id: '003',
  410. key: 'ageId',
  411. label: '适合年龄',
  412. options: [
  413. {
  414. label: '全部',
  415. },
  416. {
  417. id: '00301',
  418. label: '6-10岁',
  419. },
  420. {
  421. id: '00302',
  422. label: '11-14岁',
  423. },
  424. {
  425. id: '00303',
  426. label: '15-16岁',
  427. },
  428. {
  429. id: '00304',
  430. label: '17-18岁',
  431. },
  432. ],
  433. },
  434. {
  435. id: '004',
  436. key: 'timeId',
  437. label: '活动时长',
  438. options: [
  439. {
  440. label: '全部',
  441. },
  442. {
  443. id: '00401',
  444. label: '1日',
  445. },
  446. {
  447. id: '00402',
  448. label: '多日',
  449. },
  450. {
  451. id: '00403',
  452. label: '寒假',
  453. },
  454. {
  455. id: '00404',
  456. label: '暑假',
  457. },
  458. ],
  459. },
  460. {
  461. id: '005',
  462. key: 'price',
  463. label: '价格区间',
  464. },
  465. {
  466. id: '006',
  467. key: 'time',
  468. label: '出发日期',
  469. },
  470. ]
  471. this.filters.forEach(item => {
  472. const { key, options } = item
  473. if (!options?.length || !options[0]?.id) {
  474. return
  475. }
  476. this.queryParams[key] = options[0].id
  477. })
  478. // todo: fetch
  479. },
  480. async queryProductList(categoryId) {
  481. try {
  482. return (await this.$fetch('queryActivityList', { ...this.queryParams, categoryId }))?.records || []
  483. } catch (err) {
  484. return []
  485. }
  486. return [
  487. {
  488. id: '001',
  489. image: '/static/image/temp-20.png',
  490. title: '新疆天山行7/9日丨醉美伊犁&吐鲁番双套餐',
  491. tagList: ['国内游','7-9天','12岁+'],
  492. priceDiscount: 688.99,
  493. priceOrigin: 1200,
  494. applyNum: 4168,
  495. },
  496. {
  497. id: '002',
  498. image: '/static/image/temp-20.png',
  499. title: '坝上双草原6日|乌兰布统+锡林郭勒+长城',
  500. tagList: ['国内游','7-9天','12岁+'],
  501. priceDiscount: 688.99,
  502. priceOrigin: 1200,
  503. applyNum: 4168,
  504. },
  505. {
  506. id: '003',
  507. image: '/static/image/temp-20.png',
  508. title: '牛湖线探秘 | 清远牛湖线徒步,探秘天坑与大草原',
  509. tagList: ['国内游','7-9天','12岁+'],
  510. priceDiscount: 688.99,
  511. priceOrigin: 1200,
  512. applyNum: 4168,
  513. },
  514. {
  515. id: '004',
  516. image: '/static/image/temp-20.png',
  517. title: '低海拔藏区草原,汉藏文化大穿越',
  518. tagList: ['国内游','7-9天','12岁+'],
  519. priceDiscount: 688.99,
  520. priceOrigin: 1200,
  521. applyNum: 4168,
  522. },
  523. {
  524. id: '005',
  525. image: '/static/image/temp-20.png',
  526. title: '新丝路到敦煌7日 | 甘青轻松穿越,沙漠+草原',
  527. tagList: ['国内游','7-9天','12岁+'],
  528. priceDiscount: 688.99,
  529. priceOrigin: 1200,
  530. applyNum: 4168,
  531. },
  532. {
  533. id: '006',
  534. image: '/static/image/temp-20.png',
  535. title: '呼伦贝尔6/8日|经典or环线双套餐可选',
  536. tagList: ['国内游','7-9天','12岁+'],
  537. priceDiscount: 688.99,
  538. priceOrigin: 1200,
  539. applyNum: 4168,
  540. },
  541. ]
  542. },
  543. async initList() {
  544. console.log('queryParams', this.queryParams)
  545. const results = await Promise.allSettled(this.categoryList.map(category => { return this.queryProductList(category.id) }))
  546. results.forEach((result, index) => {
  547. this.categoryList[index].children = result.value || []
  548. })
  549. console.log('categoryList', this.categoryList)
  550. },
  551. change(e) {
  552. this.current = e
  553. },
  554. search(){
  555. // todo: set filter
  556. this.initList()
  557. },
  558. onClickFilter(key, val) {
  559. if (val) {
  560. this.queryParams[key] = val
  561. } else {
  562. delete this.queryParams[key]
  563. }
  564. this.initList()
  565. },
  566. openStartDatePicker() {
  567. this.$refs.startDatePicker?.[0]?.open?.();
  568. },
  569. onStartDateChange(e) {
  570. const date = e.value
  571. this.queryParams.startDate = date
  572. const { endDate } = this.queryParams
  573. if (endDate && this.$dayjs(date).isAfter(endDate, 'day')) {
  574. this.endDate = null
  575. delete this.queryParams.endDate
  576. }
  577. this.initList()
  578. },
  579. onClearStartDate() {
  580. this.startDate = null
  581. delete this.queryParams.startDate
  582. this.initList()
  583. },
  584. openEndDatePicker() {
  585. this.$refs.endDatePicker?.[0]?.open?.();
  586. },
  587. onEndDateChange(e) {
  588. const date = e.value
  589. this.queryParams.endDate = date
  590. const { startDate } = this.queryParams
  591. if (startDate && this.$dayjs(date).isBefore(startDate, 'day')) {
  592. this.startDate = null
  593. delete this.queryParams.startDate
  594. }
  595. this.initList()
  596. },
  597. onClearEndDate() {
  598. this.endDate = null
  599. delete this.queryParams.endDate
  600. this.initList()
  601. },
  602. onSortChange(sort) {
  603. console.log('onSortChange', sort)
  604. // todo set sort
  605. this.getData()
  606. },
  607. }
  608. }
  609. </script>
  610. <style scoped lang="scss">
  611. .page__view {
  612. height: 100vh;
  613. background: linear-gradient(#DAF3FF, #FBFEFF 200rpx, #FBFEFF);
  614. /deep/ .uv-popup {
  615. z-index: 1000000 !important;
  616. }
  617. }
  618. .header {
  619. width: 100%;
  620. padding: 0 32rpx;
  621. box-sizing: border-box;
  622. }
  623. .filter {
  624. &-header {
  625. display: flex;
  626. flex-direction: column;
  627. justify-content: flex-end;
  628. height: 176rpx;
  629. padding-bottom: 12rpx;
  630. box-sizing: border-box;
  631. .bar {
  632. display: grid;
  633. grid-template-columns: repeat(3, 1fr);
  634. .btn {
  635. display: inline-flex;
  636. align-items: center;
  637. column-gap: 8rpx;
  638. padding: 8rpx 30rpx;
  639. font-size: 28rpx;
  640. font-weight: 500;
  641. color: #FFFFFF;
  642. background: #00A9FF;
  643. border: 2rpx solid #00A9FF;
  644. border-radius: 64rpx;
  645. &-icon {
  646. width: 32rpx;
  647. height: auto;
  648. }
  649. &.is-fold {
  650. font-weight: 400;
  651. color: #191919;
  652. background: #D8F2FF;
  653. border-color: #00A9FF66;
  654. }
  655. }
  656. .title {
  657. text-align: center;
  658. font-size: 32rpx;
  659. font-weight: 600;
  660. color: #191919;
  661. }
  662. }
  663. }
  664. &-content {
  665. margin-top: 24rpx;
  666. .btn {
  667. &-fold {
  668. margin-top: 32rpx;
  669. width: 100%;
  670. }
  671. &-icon {
  672. width: 40rpx;
  673. height: auto;
  674. }
  675. }
  676. }
  677. &-item {
  678. display: flex;
  679. & + & {
  680. margin-top: 32rpx;
  681. }
  682. &-label {
  683. width: 156rpx;
  684. min-height: 64rpx;
  685. line-height: 64rpx;
  686. flex: none;
  687. }
  688. &-content {
  689. flex: 1;
  690. .option {
  691. margin-top: 6rpx;
  692. display: flex;
  693. flex-wrap: wrap;
  694. gap: 24rpx;
  695. &-item {
  696. padding: 8rpx 16rpx;
  697. font-size: 28rpx;
  698. color: #181818;
  699. border-radius: 4rpx;
  700. &.is-active {
  701. color: #FFFFFF;
  702. background: #00A9FF;
  703. }
  704. }
  705. }
  706. .range {
  707. margin: 4rpx 0;
  708. column-gap: 8rpx;
  709. border-bottom: 2rpx solid #EEEEEE;
  710. &-item {
  711. width: 220rpx;
  712. box-sizing: border-box;
  713. }
  714. .split {
  715. padding: 0 24rpx;
  716. font-size: 32rpx;
  717. line-height: 1;
  718. color: #8B8B8B;
  719. }
  720. &.price {
  721. .range-item {
  722. padding: 4rpx 0;
  723. }
  724. }
  725. &.time {
  726. .range-item {
  727. // width: 220rpx;
  728. padding: 8rpx 0;
  729. text-align: center;
  730. font-size: 28rpx;
  731. color: #181818;
  732. .btn-clear {
  733. margin: 6rpx 0;
  734. float: right;
  735. }
  736. }
  737. }
  738. }
  739. }
  740. }
  741. }
  742. .sort {
  743. width: 100%;
  744. height: 116rpx;
  745. padding: 24rpx 0;
  746. box-sizing: border-box;
  747. }
  748. .main {
  749. /deep/ .uv-vtabs,
  750. /deep/ .uv-vtabs__bar,
  751. /deep/ .uv-vtabs__content {
  752. height: calc(100vh - 292rpx - #{$tabbar-height} - env(safe-area-inset-bottom)) !important;
  753. }
  754. /deep/ .uv-vtabs__bar {
  755. background: #F6F6F6 !important;
  756. }
  757. /deep/ .uv-vtabs__bar-item {
  758. padding: 48rpx 32rpx;
  759. }
  760. /deep/ .uv-vtabs__content {
  761. padding: 24rpx 24rpx 0 24rpx;
  762. box-sizing: border-box;
  763. background: linear-gradient(#DAF3FF, #F4F4F4 250rpx, #F4F4F4);
  764. }
  765. }
  766. .card {
  767. & + & {
  768. margin-top: 32rpx;
  769. }
  770. &:last-child {
  771. padding-bottom: 24rpx;
  772. }
  773. }
  774. </style>