sh-category-tabs.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <template>
  2. <!-- 分类选项卡 -->
  3. <view class="category-tabs-wrap">
  4. <!-- 吸顶 -->
  5. <view class="u-sticky-wrap" :class="[elClass]" :style="{ height: fixed ? height + 'px' : 'auto', backgroundColor: isSticky ? '#fff' : '#f6f6f6' }">
  6. <view
  7. class="u-sticky"
  8. :style="{ position: fixed ? 'fixed' : 'static', top: stickyTop + 'px', left: left + 'px', width: width == 'auto' ? 'auto' : width + 'px', zIndex: 1109 }"
  9. >
  10. <view class="tabs-wrap u-p-y-20" :style="isSticky ? 'border-bottom:1px solid #eee;background-color:#fff' : ''">
  11. <scroll-view scroll-x class="tabs-content" enable-flex scroll-with-animation :scroll-left="scrollLeft">
  12. <view class="u-flex u-row-cetner u-flex-1 u-col-center">
  13. <view class="tab-item u-flex-col u-col-center u-row-center" v-for="(item, index) in tabsList" :key="index" @tap="tabChange(index)">
  14. <view class="tab-title u-m-y-6" :class="{ 'title-active': tabCur == index }">{{ item.name }}</view>
  15. <view class="tab-des" :class="{ 'des-active': tabCur == index }">{{ item.description }}</view>
  16. </view>
  17. </view>
  18. </scroll-view>
  19. </view>
  20. </view>
  21. </view>
  22. <!-- 瀑布流 -->
  23. <view class="u-waterfall u-p-16" v-if="styleType == 1">
  24. <view id="u-left-column" class="u-column">
  25. <view class="goods-item u-m-b-16 u-flex u-row-center u-col-center" v-for="leftGoods in leftList" :key="leftGoods.id">
  26. <shopro-goods-card
  27. :detail="leftGoods"
  28. :type="leftGoods.activity_type || ''"
  29. :image="leftGoods.image"
  30. :title="leftGoods.title"
  31. :subtitle="leftGoods.subtitle"
  32. :price="leftGoods.price"
  33. :originPrice="leftGoods.original_price"
  34. :tagTextList="leftGoods.activity_discounts_tags"
  35. @click="$Router.push({ path: '/pages/goods/detail', query: { id: leftGoods.id } })"
  36. ></shopro-goods-card>
  37. </view>
  38. </view>
  39. <view id="u-right-column" class="u-column">
  40. <view class="goods-item u-m-b-16 u-flex u-row-center u-col-center" v-for="rightGoods in rightList" :key="rightGoods.id">
  41. <shopro-goods-card
  42. :detail="rightGoods"
  43. :type="rightGoods.activity_type || ''"
  44. :image="rightGoods.image"
  45. :title="rightGoods.title"
  46. :subtitle="rightGoods.subtitle"
  47. :price="rightGoods.price"
  48. :originPrice="rightGoods.original_price"
  49. :tagTextList="rightGoods.activity_discounts_tags"
  50. @click="$Router.push({ path: '/pages/goods/detail', query: { id: rightGoods.id } })"
  51. ></shopro-goods-card>
  52. </view>
  53. </view>
  54. </view>
  55. <!-- m -->
  56. <view class="big-card-wrap u-p-10" v-if="styleType == 2">
  57. <block v-for="item in goodsList" :key="item.id">
  58. <sh-goods-card
  59. :detail="item"
  60. :type="item.activity_type || ''"
  61. :image="item.image"
  62. :title="item.title"
  63. :subtitle="item.subtitle"
  64. :price="item.price"
  65. :originPrice="item.original_price"
  66. :sales="item.sales"
  67. :tagTextList="item.activity_discounts_tags"
  68. @click="$Router.push({ path: '/pages/goods/detail', query: { id: item.id } })"
  69. ></sh-goods-card>
  70. </block>
  71. </view>
  72. <view class="x-c" style="width: 100%;">
  73. <!-- 缺省页 -->
  74. <shopro-empty v-if="isEmpty" marginTop="200rpx" :image="$IMG_URL + '/imgs/empty/empty_goods.png'" tipText="暂无该商品,还有更多好货等着你噢~"></shopro-empty>
  75. <!-- 更多 -->
  76. <u-loadmore v-show="!isEmpty" height="80rpx" :status="loadStatus" icon-type="flower" color="#ccc" />
  77. </view>
  78. </view>
  79. </template>
  80. <script>
  81. /**
  82. * category-tabs
  83. * @description 一个可以吸顶的分类列表
  84. * @property {Boolean} enable = false - 是否吸顶,tabbar项中,组件不会自动销毁,需要自行开关
  85. * @property {Array} tabsList - 分类列表
  86. * @property {Array | Number} styleType - 卡片类型
  87. */
  88. let systemInfo = uni.getSystemInfoSync();
  89. // #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
  90. let menuButtonInfo = uni.getMenuButtonBoundingClientRect();
  91. // #endif
  92. import shGoodsCard from '../components/sh-goods-card.vue';
  93. export default {
  94. components: {
  95. shGoodsCard
  96. },
  97. data() {
  98. return {
  99. // 吸顶相关
  100. fixed: false,
  101. height: 'auto',
  102. elClass: this.$u.guid(),
  103. left: 0,
  104. width: 'auto',
  105. stickyTop: 0,
  106. isSticky: false, //是否吸顶
  107. // 瀑布流 350-330
  108. addTime: 100, //排序间隙时间
  109. leftHeight: 0,
  110. rightHeight: 0,
  111. leftList: [],
  112. rightList: [],
  113. tempList: [],
  114. tabCur: 0,
  115. scrollLeft: 0,
  116. isEmpty: true,
  117. categoryId: this.tabsList[0]?.id,
  118. goodsList: [],
  119. tabCurrent: 0,
  120. loadStatus: 'loadmore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
  121. currentPage: 1,
  122. lastPage: 1
  123. };
  124. },
  125. watch: {
  126. enable(val) {
  127. if (val == false) {
  128. this.fixed = false;
  129. this.disconnectObserver('contentObserver');
  130. } else {
  131. this.initObserver();
  132. }
  133. }
  134. },
  135. props: {
  136. tabsList: {
  137. type: Array,
  138. default: () => []
  139. },
  140. styleType: {
  141. type: [String, Number],
  142. default: 1
  143. },
  144. enable: {
  145. type: Boolean,
  146. default: false
  147. }
  148. },
  149. created() {
  150. this.getGoodsList();
  151. // 触底监听
  152. uni.$on('uOnReachBottom', data => {
  153. if (this.currentPage < this.lastPage) {
  154. this.currentPage += 1;
  155. this.getGoodsList();
  156. }
  157. });
  158. },
  159. mounted() {
  160. this.initObserver();
  161. },
  162. beforeDestroy() {
  163. this.disconnectObserver('contentObserver');
  164. },
  165. methods: {
  166. // 瀑布流相关
  167. async splitData() {
  168. if (!this.tempList.length) return;
  169. let item = this.tempList[0];
  170. if (!item) return;
  171. // 分左右
  172. if (this.leftHeight < this.rightHeight) {
  173. this.leftHeight += item.activity_discounts_tags.length ? 350 : 330;
  174. this.leftList.push(item);
  175. } else if (this.leftHeight > this.rightHeight) {
  176. this.rightHeight += item.activity_discounts_tags.length ? 350 : 330;
  177. this.rightList.push(item);
  178. } else {
  179. this.leftHeight += item.activity_discounts_tags.length ? 350 : 330;
  180. this.leftList.push(item);
  181. }
  182. // 移除临时列表的第一项,如果临时数组还有数据,继续循环
  183. this.tempList.splice(0, 1);
  184. if (this.tempList.length) {
  185. setTimeout(() => {
  186. this.splitData();
  187. }, this.addTime);
  188. }
  189. },
  190. clear() {
  191. this.leftList = [];
  192. this.rightList = [];
  193. this.leftHeight = 0;
  194. this.rightHeight = 0;
  195. this.tempList = [];
  196. },
  197. // 吸顶相关
  198. initObserver() {
  199. if (!this.enable) return;
  200. // #ifdef APP-PLUS || H5
  201. this.stickyTop = systemInfo.statusBarHeight + 44;
  202. // #endif
  203. // #ifdef MP
  204. let height = systemInfo.platform == 'ios' ? 44 : 48;
  205. let top = systemInfo.statusBarHeight + height;
  206. this.stickyTop = systemInfo.statusBarHeight + height;
  207. // #endif
  208. this.disconnectObserver('contentObserver');
  209. this.$uGetRect('.' + this.elClass).then(res => {
  210. this.height = res.height;
  211. this.left = res.left;
  212. this.width = res.width;
  213. this.$nextTick(() => {
  214. this.observeContent();
  215. });
  216. });
  217. },
  218. observeContent() {
  219. this.disconnectObserver('contentObserver');
  220. const contentObserver = this.createIntersectionObserver({
  221. thresholds: [0.95, 0.98, 1]
  222. });
  223. contentObserver.relativeToViewport({
  224. top: -this.stickyTop
  225. });
  226. contentObserver.observe('.' + this.elClass, res => {
  227. if (!this.enable) return;
  228. this.setFixed(res.boundingClientRect.top);
  229. });
  230. this.contentObserver = contentObserver;
  231. },
  232. setFixed(top) {
  233. const fixed = top < this.stickyTop;
  234. if (fixed) {
  235. this.isSticky = true;
  236. } else if (this.fixed) {
  237. this.isSticky = false;
  238. }
  239. this.fixed = fixed;
  240. },
  241. disconnectObserver(observerName) {
  242. const observer = this[observerName];
  243. observer && observer.disconnect();
  244. },
  245. // 商品列表
  246. getGoodsList() {
  247. let that = this;
  248. that.$http('goods.lists', {
  249. category_id: that.categoryId,
  250. page: that.currentPage
  251. }).then(res => {
  252. if (res.code === 1) {
  253. that.goodsList = [...that.goodsList, ...res.data.data];
  254. that.tempList = res.data.data;
  255. that.isEmpty = !that.goodsList.length;
  256. that.lastPage = res.data.last_page;
  257. that.loadStatus = that.currentPage < res.data.last_page ? 'loadmore' : 'nomore';
  258. that.splitData();
  259. }
  260. });
  261. },
  262. // tabs
  263. tabChange(index) {
  264. this.tabCur = index;
  265. this.scrollLeft = (index - 1) * 60;
  266. this.categoryId = this.tabsList[index].id;
  267. this.styleType === 1 && this.clear();
  268. this.goodsList = [];
  269. this.currentPage = 1;
  270. this.lastPage = 1;
  271. this.loadStatus = 'loadmore';
  272. this.getGoodsList();
  273. }
  274. }
  275. };
  276. </script>
  277. <style lang="scss" scoped>
  278. @mixin vue-flex($direction: row) {
  279. /* #ifndef APP-NVUE */
  280. display: flex;
  281. flex-direction: $direction;
  282. /* #endif */
  283. }
  284. .category-tabs-wrap {
  285. min-height: 1000rpx;
  286. // 吸顶
  287. .u-sticky-wrap {
  288. background-color: #fff;
  289. }
  290. // 瀑布流
  291. .u-waterfall {
  292. @include vue-flex;
  293. flex-direction: row;
  294. align-items: flex-start;
  295. }
  296. .u-column {
  297. @include vue-flex;
  298. flex: 1;
  299. flex-direction: column;
  300. height: auto;
  301. }
  302. }
  303. // 滑动分类
  304. .tabs-content {
  305. white-space: nowrap;
  306. .tab-item {
  307. min-height: 90rpx;
  308. display: inline-block;
  309. margin: 0 10rpx;
  310. padding: 0 20rpx;
  311. .tab-title {
  312. font-size: 28rpx;
  313. font-weight: 600;
  314. color: #333333;
  315. }
  316. .title-active {
  317. color: #a8700d;
  318. }
  319. .tab-des {
  320. font-size: 22rpx;
  321. font-weight: 400;
  322. color: #999999;
  323. text-align: center;
  324. }
  325. .des-active {
  326. background: linear-gradient(90deg, #e9b461, #eecc89);
  327. border-radius: 15rpx;
  328. padding: 2rpx 10rpx;
  329. color: #ffffff;
  330. }
  331. }
  332. }
  333. </style>