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

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