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

681 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. search(){
  364. // todo: set filter
  365. this.initList()
  366. },
  367. onClickFilter(key, val) {
  368. if (val) {
  369. this.queryParams[key] = val
  370. } else {
  371. delete this.queryParams[key]
  372. }
  373. if (key === 'frontier') {
  374. this.filters[1].options = [
  375. {
  376. label: '全部',
  377. },
  378. ...this.addressOptionsConfig[val]
  379. ]
  380. delete this.queryParams.addressId
  381. }
  382. this.initList()
  383. },
  384. onStartPriceChange(value) {
  385. if (value) {
  386. this.queryParams.priceLow = value
  387. } else {
  388. delete this.queryParams.priceLow
  389. }
  390. this.initList()
  391. },
  392. onEndPriceChange(value) {
  393. if (value) {
  394. this.queryParams.priceHigh = value
  395. } else {
  396. delete this.queryParams.priceHigh
  397. }
  398. this.initList()
  399. },
  400. openStartDatePicker() {
  401. this.$refs.startDatePicker?.[0]?.open?.();
  402. },
  403. onStartDateChange(e) {
  404. const date = e.value
  405. this.queryParams.dateLow = $dayjs(date).format('YYYY-MM-DD')
  406. const { dateHigh } = this.queryParams
  407. if (dateHigh && this.$dayjs(date).isAfter(dateHigh, 'day')) {
  408. this.dateHigh = null
  409. delete this.queryParams.dateHigh
  410. }
  411. this.initList()
  412. },
  413. onClearStartDate() {
  414. this.dateLow = null
  415. delete this.queryParams.dateLow
  416. this.initList()
  417. },
  418. openEndDatePicker() {
  419. this.$refs.endDatePicker?.[0]?.open?.();
  420. },
  421. onEndDateChange(e) {
  422. const date = e.value
  423. this.queryParams.dateHigh = $dayjs(date).format('YYYY-MM-DD')
  424. const { dateLow } = this.queryParams
  425. if (dateLow && this.$dayjs(date).isBefore(dateLow, 'day')) {
  426. this.dateLow = null
  427. delete this.queryParams.dateLow
  428. }
  429. this.initList()
  430. },
  431. onClearEndDate() {
  432. this.dateHigh = null
  433. delete this.queryParams.dateHigh
  434. this.initList()
  435. },
  436. onSortChange(sort) {
  437. console.log('onSortChange', sort)
  438. this.initList()
  439. },
  440. }
  441. }
  442. </script>
  443. <style scoped lang="scss">
  444. .page__view {
  445. height: 100vh;
  446. background: linear-gradient(#DAF3FF, #FBFEFF 200rpx, #FBFEFF);
  447. /deep/ .uv-popup {
  448. z-index: 1000000 !important;
  449. }
  450. }
  451. .header {
  452. width: 100%;
  453. padding: 0 32rpx;
  454. box-sizing: border-box;
  455. }
  456. .filter {
  457. &-header {
  458. display: flex;
  459. flex-direction: column;
  460. justify-content: flex-end;
  461. height: 176rpx;
  462. padding-bottom: 12rpx;
  463. box-sizing: border-box;
  464. .bar {
  465. display: grid;
  466. grid-template-columns: repeat(3, 1fr);
  467. .btn {
  468. display: inline-flex;
  469. align-items: center;
  470. column-gap: 8rpx;
  471. padding: 8rpx 30rpx;
  472. font-size: 28rpx;
  473. font-weight: 500;
  474. color: #FFFFFF;
  475. background: #00A9FF;
  476. border: 2rpx solid #00A9FF;
  477. border-radius: 64rpx;
  478. &-icon {
  479. width: 32rpx;
  480. height: auto;
  481. }
  482. &.is-fold {
  483. font-weight: 400;
  484. color: #191919;
  485. background: #D8F2FF;
  486. border-color: #00A9FF66;
  487. }
  488. }
  489. .title {
  490. text-align: center;
  491. font-size: 32rpx;
  492. font-weight: 600;
  493. color: #191919;
  494. }
  495. }
  496. }
  497. &-content {
  498. margin-top: 24rpx;
  499. .btn {
  500. &-fold {
  501. margin-top: 32rpx;
  502. width: 100%;
  503. }
  504. &-icon {
  505. width: 40rpx;
  506. height: auto;
  507. }
  508. }
  509. }
  510. &-item {
  511. display: flex;
  512. & + & {
  513. margin-top: 32rpx;
  514. }
  515. &-label {
  516. width: 156rpx;
  517. min-height: 64rpx;
  518. line-height: 64rpx;
  519. flex: none;
  520. }
  521. &-content {
  522. flex: 1;
  523. .option {
  524. margin-top: 6rpx;
  525. display: flex;
  526. flex-wrap: wrap;
  527. gap: 24rpx;
  528. &-item {
  529. padding: 8rpx 16rpx;
  530. font-size: 28rpx;
  531. color: #181818;
  532. border-radius: 4rpx;
  533. &.is-active {
  534. color: #FFFFFF;
  535. background: #00A9FF;
  536. }
  537. }
  538. }
  539. .range {
  540. margin: 4rpx 0;
  541. column-gap: 8rpx;
  542. border-bottom: 2rpx solid #EEEEEE;
  543. &-item {
  544. width: 220rpx;
  545. box-sizing: border-box;
  546. }
  547. .split {
  548. padding: 0 24rpx;
  549. font-size: 32rpx;
  550. line-height: 1;
  551. color: #8B8B8B;
  552. }
  553. &.price {
  554. .range-item {
  555. padding: 4rpx 0;
  556. }
  557. }
  558. &.time {
  559. .range-item {
  560. // width: 220rpx;
  561. padding: 8rpx 0;
  562. text-align: center;
  563. font-size: 28rpx;
  564. color: #181818;
  565. .btn-clear {
  566. margin: 6rpx 0;
  567. float: right;
  568. }
  569. }
  570. }
  571. }
  572. }
  573. }
  574. }
  575. .sort {
  576. width: 100%;
  577. height: 116rpx;
  578. padding: 24rpx 0;
  579. box-sizing: border-box;
  580. }
  581. .main {
  582. /deep/ .uv-vtabs,
  583. /deep/ .uv-vtabs__bar,
  584. /deep/ .uv-vtabs__content {
  585. height: calc(100vh - 292rpx - #{$tabbar-height} - env(safe-area-inset-bottom)) !important;
  586. }
  587. /deep/ .uv-vtabs__bar {
  588. background: #F6F6F6 !important;
  589. }
  590. /deep/ .uv-vtabs__bar-item {
  591. padding: 48rpx 32rpx;
  592. }
  593. /deep/ .uv-vtabs__content {
  594. padding: 24rpx 24rpx 0 24rpx;
  595. box-sizing: border-box;
  596. background: linear-gradient(#DAF3FF, #F4F4F4 250rpx, #F4F4F4);
  597. }
  598. }
  599. .card {
  600. & + & {
  601. margin-top: 32rpx;
  602. }
  603. &:last-child {
  604. padding-bottom: 24rpx;
  605. }
  606. }
  607. </style>