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

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