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

677 lines
18 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="priceLow"
  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. @confirm="onStartPriceChange"
  39. @clear="onStartPriceChange(null)"
  40. ></uv-input>
  41. </view>
  42. <view class="split"></view>
  43. <view class="range-item">
  44. <uv-input
  45. v-model="priceHigh"
  46. type="number"
  47. inputAlign="center"
  48. placeholder="结束价格"
  49. placeholderStyle="color: #181818; font-size: 28rpx; font-weight: 400;"
  50. :customStyle="{
  51. backgroundColor: 'transparent',
  52. padding: '0',
  53. boxSizing: 'border-box',
  54. fontSize: '28rpx',
  55. border: 'none',
  56. }"
  57. fontSize="28rpx"
  58. :clearable="true"
  59. @confirm="onEndPriceChange"
  60. @clear="onEndPriceChange(null)"
  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. {{ dateLow ? $dayjs(dateLow).format('YYYY-MM-DD') : '开始日期' }}
  69. <button v-if="dateLow" 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. {{ dateHigh ? $dayjs(dateHigh).format('YYYY-MM-DD') : '结束日期' }}
  76. <button v-if="dateHigh" 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="dateLow"
  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="dateHigh"
  94. mode="date"
  95. title="结束日期"
  96. confirmColor="#00A9FF"
  97. round="32rpx"
  98. :minDate="dateLow || 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 v-model="queryParams.sort" @change="onSortChange"></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. priceLow: null,
  184. priceHigh: null,
  185. dateLow: null,
  186. dateHigh: null,
  187. minTime: new Date().getTime(),
  188. queryParams: {
  189. pageNo: 1,
  190. pageSize: 1000,
  191. sort: 'comprehensive',
  192. },
  193. categoryList: [],
  194. filters: [],
  195. addressOptionsConfig: {},
  196. isFold: true,
  197. }
  198. },
  199. async onLoad({ categoryId }) {
  200. if(uni.getStorageSync('token')){
  201. this.$store.commit('getUserInfo')
  202. }
  203. await Promise.allSettled([this.fetchCategoryList(), this.fetchFilters()])
  204. console.log('categoryList', this.categoryList)
  205. console.log('filters', this.filters)
  206. await this.initList()
  207. if(this.categoryList.length > 0 && categoryId){
  208. setTimeout(() => {
  209. this.current = this.categoryList.findIndex(item => item.id === categoryId)
  210. }, 800)
  211. // this.$nextTick(() => {
  212. // this.current = this.categoryList.findIndex(item => item.id === categoryId)
  213. // })
  214. }
  215. },
  216. methods: {
  217. async fetchCategoryList() {
  218. try {
  219. this.categoryList = (await this.$fetch('queryCategoryList', { pageSize: 1000 }))?.records?.map(item => ({ id: item.id, name: item.title, children: [] }))
  220. } catch(err) {
  221. this.categoryList = []
  222. }
  223. },
  224. async fetchFilters() {
  225. let fetchs = [
  226. this.$fetch('queryAddressList', { pageNo: 1, pageSize: 1000, pid: '0' }),
  227. this.$fetch('queryAgeList', { pageNo: 1, pageSize: 1000 }),
  228. this.$fetch('queryTimeList', { pageNo: 1, pageSize: 1000 }),
  229. ]
  230. const results = (await Promise.allSettled(fetchs))
  231. .map(res => {
  232. return res.value.records.map(item => {
  233. return {
  234. id: item.id,
  235. label: item.title,
  236. }
  237. })
  238. })
  239. console.log('results', results)
  240. fetchs = results[0].map(item => {
  241. return this.$fetch('queryAddressList', { pageNo: 1, pageSize: 1000, pid: item.id })
  242. })
  243. const addressResults = (await Promise.allSettled(fetchs))
  244. .map(res => {
  245. return res.value.records.map(item => {
  246. return {
  247. id: item.id,
  248. label: item.title,
  249. }
  250. })
  251. })
  252. console.log('addressResults', addressResults)
  253. const addressOptionsConfig = addressResults.reduce((obj, records, index) => {
  254. obj[results[0][index].id] = records
  255. return obj
  256. }, {})
  257. this.addressOptionsConfig = addressOptionsConfig
  258. console.log('addressOptionsConfig', addressOptionsConfig)
  259. this.filters = [
  260. {
  261. id: '001',
  262. key: 'frontier',
  263. label: '国境',
  264. options: results[0],
  265. },
  266. {
  267. id: '002',
  268. key: 'addressId',
  269. label: '目的地',
  270. options: [
  271. {
  272. label: '全部',
  273. },
  274. ...addressOptionsConfig[results[0][0].id]
  275. ],
  276. },
  277. {
  278. id: '003',
  279. key: 'ageId',
  280. label: '适合年龄',
  281. options: [
  282. {
  283. label: '全部',
  284. },
  285. ...results[1]
  286. ],
  287. },
  288. {
  289. id: '004',
  290. key: 'timeId',
  291. label: '活动时长',
  292. options: [
  293. {
  294. label: '全部',
  295. },
  296. ...results[2],
  297. ],
  298. },
  299. {
  300. id: '005',
  301. key: 'price',
  302. label: '价格区间',
  303. },
  304. {
  305. id: '006',
  306. key: 'time',
  307. label: '出发日期',
  308. },
  309. ]
  310. console.log('filters', this.filters)
  311. this.filters.forEach(item => {
  312. const { key, options } = item
  313. if (!options?.length || !options[0]?.id) {
  314. return
  315. }
  316. this.queryParams[key] = options[0].id
  317. })
  318. },
  319. async queryProductList(categoryId) {
  320. try {
  321. const {
  322. frontier,
  323. addressId,
  324. sort,
  325. ...params
  326. } = this.queryParams
  327. params.addressId = addressId || frontier
  328. params.categoryId = categoryId
  329. switch(sort) {
  330. // 销量排序(saleOrder):0-从高到低 1-从低到高
  331. case 'sale-desc': // 销量排序 - 降序
  332. params.saleOrder = '0'
  333. break
  334. case 'sale-asc': // 销量排序 - 升序
  335. params.saleOrder = '1'
  336. break
  337. // 价格排序(priceOrder):0-从高到低 1-从低到高
  338. case 'price-desc': // 销量排序 - 降序
  339. params.priceOrder = '0'
  340. break
  341. case 'price-asc': // 销量排序 - 升序
  342. params.priceOrder = '1'
  343. break
  344. default:
  345. break
  346. }
  347. return (await this.$fetch('queryActivityList', params))?.records || []
  348. } catch (err) {
  349. return []
  350. }
  351. },
  352. async initList() {
  353. console.log('queryParams', this.queryParams)
  354. const results = await Promise.allSettled(this.categoryList.map(category => { return this.queryProductList(category.id) }))
  355. results.forEach((result, index) => {
  356. this.categoryList[index].children = result.value || []
  357. })
  358. console.log('categoryList', this.categoryList)
  359. },
  360. change(e) {
  361. this.current = e
  362. },
  363. onClickFilter(key, val) {
  364. if (val) {
  365. this.queryParams[key] = val
  366. } else {
  367. delete this.queryParams[key]
  368. }
  369. if (key === 'frontier') {
  370. this.filters[1].options = [
  371. {
  372. label: '全部',
  373. },
  374. ...this.addressOptionsConfig[val]
  375. ]
  376. delete this.queryParams.addressId
  377. }
  378. this.initList()
  379. },
  380. onStartPriceChange(value) {
  381. if (value) {
  382. this.queryParams.priceLow = value
  383. } else {
  384. delete this.queryParams.priceLow
  385. }
  386. this.initList()
  387. },
  388. onEndPriceChange(value) {
  389. if (value) {
  390. this.queryParams.priceHigh = value
  391. } else {
  392. delete this.queryParams.priceHigh
  393. }
  394. this.initList()
  395. },
  396. openStartDatePicker() {
  397. this.$refs.startDatePicker?.[0]?.open?.();
  398. },
  399. onStartDateChange(e) {
  400. const date = e.value
  401. this.queryParams.dateLow = $dayjs(date).format('YYYY-MM-DD')
  402. const { dateHigh } = this.queryParams
  403. if (dateHigh && this.$dayjs(date).isAfter(dateHigh, 'day')) {
  404. this.dateHigh = null
  405. delete this.queryParams.dateHigh
  406. }
  407. this.initList()
  408. },
  409. onClearStartDate() {
  410. this.dateLow = null
  411. delete this.queryParams.dateLow
  412. this.initList()
  413. },
  414. openEndDatePicker() {
  415. this.$refs.endDatePicker?.[0]?.open?.();
  416. },
  417. onEndDateChange(e) {
  418. const date = e.value
  419. this.queryParams.dateHigh = $dayjs(date).format('YYYY-MM-DD')
  420. const { dateLow } = this.queryParams
  421. if (dateLow && this.$dayjs(date).isBefore(dateLow, 'day')) {
  422. this.dateLow = null
  423. delete this.queryParams.dateLow
  424. }
  425. this.initList()
  426. },
  427. onClearEndDate() {
  428. this.dateHigh = null
  429. delete this.queryParams.dateHigh
  430. this.initList()
  431. },
  432. onSortChange(sort) {
  433. console.log('onSortChange', sort)
  434. this.initList()
  435. },
  436. }
  437. }
  438. </script>
  439. <style scoped lang="scss">
  440. .page__view {
  441. height: 100vh;
  442. background: linear-gradient(#DAF3FF, #FBFEFF 200rpx, #FBFEFF);
  443. /deep/ .uv-popup {
  444. z-index: 1000000 !important;
  445. }
  446. }
  447. .header {
  448. width: 100%;
  449. padding: 0 32rpx;
  450. box-sizing: border-box;
  451. }
  452. .filter {
  453. &-header {
  454. display: flex;
  455. flex-direction: column;
  456. justify-content: flex-end;
  457. height: 176rpx;
  458. padding-bottom: 12rpx;
  459. box-sizing: border-box;
  460. .bar {
  461. display: grid;
  462. grid-template-columns: repeat(3, 1fr);
  463. .btn {
  464. display: inline-flex;
  465. align-items: center;
  466. column-gap: 8rpx;
  467. padding: 8rpx 30rpx;
  468. font-size: 28rpx;
  469. font-weight: 500;
  470. color: #FFFFFF;
  471. background: #00A9FF;
  472. border: 2rpx solid #00A9FF;
  473. border-radius: 64rpx;
  474. &-icon {
  475. width: 32rpx;
  476. height: auto;
  477. }
  478. &.is-fold {
  479. font-weight: 400;
  480. color: #191919;
  481. background: #D8F2FF;
  482. border-color: #00A9FF66;
  483. }
  484. }
  485. .title {
  486. text-align: center;
  487. font-size: 32rpx;
  488. font-weight: 600;
  489. color: #191919;
  490. }
  491. }
  492. }
  493. &-content {
  494. margin-top: 24rpx;
  495. .btn {
  496. &-fold {
  497. margin-top: 32rpx;
  498. width: 100%;
  499. }
  500. &-icon {
  501. width: 40rpx;
  502. height: auto;
  503. }
  504. }
  505. }
  506. &-item {
  507. display: flex;
  508. & + & {
  509. margin-top: 32rpx;
  510. }
  511. &-label {
  512. width: 156rpx;
  513. min-height: 64rpx;
  514. line-height: 64rpx;
  515. flex: none;
  516. }
  517. &-content {
  518. flex: 1;
  519. .option {
  520. margin-top: 6rpx;
  521. display: flex;
  522. flex-wrap: wrap;
  523. gap: 24rpx;
  524. &-item {
  525. padding: 8rpx 16rpx;
  526. font-size: 28rpx;
  527. color: #181818;
  528. border-radius: 4rpx;
  529. &.is-active {
  530. color: #FFFFFF;
  531. background: #00A9FF;
  532. }
  533. }
  534. }
  535. .range {
  536. margin: 4rpx 0;
  537. column-gap: 8rpx;
  538. border-bottom: 2rpx solid #EEEEEE;
  539. &-item {
  540. width: 220rpx;
  541. box-sizing: border-box;
  542. }
  543. .split {
  544. padding: 0 24rpx;
  545. font-size: 32rpx;
  546. line-height: 1;
  547. color: #8B8B8B;
  548. }
  549. &.price {
  550. .range-item {
  551. padding: 4rpx 0;
  552. }
  553. }
  554. &.time {
  555. .range-item {
  556. // width: 220rpx;
  557. padding: 8rpx 0;
  558. text-align: center;
  559. font-size: 28rpx;
  560. color: #181818;
  561. .btn-clear {
  562. margin: 6rpx 0;
  563. float: right;
  564. }
  565. }
  566. }
  567. }
  568. }
  569. }
  570. }
  571. .sort {
  572. width: 100%;
  573. height: 116rpx;
  574. padding: 24rpx 0;
  575. box-sizing: border-box;
  576. }
  577. .main {
  578. /deep/ .uv-vtabs,
  579. /deep/ .uv-vtabs__bar,
  580. /deep/ .uv-vtabs__content {
  581. height: calc(100vh - 292rpx - #{$tabbar-height} - env(safe-area-inset-bottom)) !important;
  582. }
  583. /deep/ .uv-vtabs__bar {
  584. background: #F6F6F6 !important;
  585. }
  586. /deep/ .uv-vtabs__bar-item {
  587. padding: 48rpx 32rpx;
  588. }
  589. /deep/ .uv-vtabs__content {
  590. padding: 24rpx 24rpx 0 24rpx;
  591. box-sizing: border-box;
  592. background: linear-gradient(#DAF3FF, #F4F4F4 250rpx, #F4F4F4);
  593. }
  594. }
  595. .card {
  596. & + & {
  597. margin-top: 32rpx;
  598. }
  599. &:last-child {
  600. padding-bottom: 24rpx;
  601. }
  602. }
  603. </style>