rank.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <template>
  2. <view class="min-h-[100vh]" :style="themeColor()">
  3. <!-- #ifdef MP-WEIXIN -->
  4. <top-tabbar :data="param" :isFill="false" />
  5. <!-- #endif -->
  6. <!-- 顶部图片 -->
  7. <view class="rank-head">
  8. <image class="w-[100%] h-[435rpx]" :src="img(rankConfig.rank_images)" mode="aspectFill"></image>
  9. <view class="content-box">
  10. <!-- 榜单分类按钮 -->
  11. <scroll-view scroll-x="true" class="category-slider" scroll-with-animation :scroll-into-view="'id' + activeIndex">
  12. <view class="category-con" :style="{ justifyContent: centered ? 'center' : 'flex-start' }">
  13. <view class="category-btn" v-for="(item, index) in rankList" :key="index" :id="'id' + index" @click="selectCategory(item, index)"
  14. :style="{ color: activeIndex === index ? rankConfig.select_color : rankConfig.no_color, background: activeIndex === index ? `linear-gradient(to right, ${rankConfig.select_bg_color_start}, ${rankConfig.select_bg_color_end})`: 'transparent'}">
  15. <view>{{ item.name }}</view>
  16. </view>
  17. </view>
  18. </scroll-view>
  19. <!-- <view class="content">
  20. <text class="text-[26rpx]">{{rankConfig.rank_name}}</text>
  21. </view> -->
  22. </view>
  23. <view class="side-tab" :style="{ top: topStyle }" @click="rankPopup = true" v-if="rankConfig.rank_remark">
  24. <text class="iconfont icona-paihangbangpc30 icon"></text>
  25. <text class="desc">{{ t('rankingRules') }}</text>
  26. </view>
  27. </view>
  28. <view class="rank-list p-[20rpx] relative -mt-[42rpx]">
  29. <!-- 列表 -->
  30. <mescroll-body ref="mescrollRef" :height="listHeight" @init="mescrollInit" :down="{ use: false }" @up="getRankGoodsListFn">
  31. <view class="bg-[#fff] flex rounded-[var(--rounded-mid)] p-[20rpx]"
  32. v-for="(item,index) in rankGoodsList" :key="item.goods_id"
  33. :class="{'mb-[20rpx]': (rankGoodsList.length-1) != index}" v-if="rankGoodsList.length"
  34. @click="toLink(item.goods_id)">
  35. <view class="w-[240rpx] h-[240rpx] flex items-center justify-center relative">
  36. <!-- 榜单排名图片 -->
  37. <image v-if="index < 5" class="absolute top-[7rpx] left-[10rpx] w-[50rpx] h-[58rpx]" :style="{ zIndex:9 }" :src="getRankBadge(item.rank_num)" mode="aspectFill"></image>
  38. <view class="absolute top-[15rpx] left-[10rpx] flex items-center justify-center w-[50rpx] h-[50rpx]" v-if="index < 5" :style="{ zIndex: 10 }">
  39. <text class="text-[24rpx] font-bold text-[#fff]">{{ index + 1 }}</text>
  40. </view>
  41. <image v-if="item.goods_cover_thumb_mid" class="w-[250rpx] h-[250rpx] rounded-[var(--rounded-mid)]"
  42. :src="img(item.goods_cover_thumb_mid)" :mode="'aspectFill'"
  43. @error="item.goods_cover_thumb_mid='static/resource/images/diy/shop_default.jpg'" />
  44. <image class="w-[240rpx] h-[240rpx] rounded-[var(--rounded-mid)]" v-else
  45. :src="img('static/resource/images/diy/shop_default.jpg')" :mode="'aspectFill'" />
  46. </view>
  47. <view class="flex flex-col flex-1 justify-between ml-[20rpx] pt-[4rpx]">
  48. <view class="text-[28rpx] text-[#333] leading-[40rpx] multi-hidden mb-[10rpx]">
  49. <view class="brand-tag" v-if="item.goods_brand" :style="diyGoods.baseTagStyle(item.goods_brand)">
  50. {{ item.goods_brand.brand_name }}
  51. </view>
  52. {{ item.goods_name }}
  53. </view>
  54. <view v-if="item.goods_label_name && item.goods_label_name.length" class="flex flex-wrap">
  55. <template v-for="(tagItem, tagIndex) in item.goods_label_name">
  56. <image class="img-tag" v-if="tagItem.style_type == 'icon' && tagItem.icon" :src="img(tagItem.icon)" mode="heightFix" @error="diyGoods.error(tagItem,'icon')" />
  57. <view class="base-tag" v-else-if="tagItem.style_type == 'diy' || !tagItem.icon" :style="diyGoods.baseTagStyle(tagItem)">{{ tagItem.label_name }}</view>
  58. </template>
  59. </view>
  60. <view class="flex items-center justify-between">
  61. <view class="text-[var(--price-text-color)] price-font flex items-baseline">
  62. <text class="text-[24rpx] font-500">¥</text>
  63. <text class="text-[40rpx] font-500">{{ diyGoods.goodsPrice(item).toFixed(2).split('.')[0] }}</text>
  64. <text class="text-[24rpx] font-500">.{{ diyGoods.goodsPrice(item).toFixed(2).split('.')[1] }}</text>
  65. </view>
  66. <view :id="'itemCart' + index" class="w-[102rpx] box-border text-center text-[#fff] primary-btn-bg h-[46rpx] text-[22rpx] leading-[46rpx] rounded-[100rpx]">去购买</view>
  67. </view>
  68. </view>
  69. </view><mescroll-empty v-if="!rankGoodsList.length && loading" :option="{tip : '暂无商品', btnText:'去逛逛'}"
  70. @emptyclick="redirect({ url: '/addon/mall/pages/goods/list'})"></mescroll-empty>
  71. </mescroll-body>
  72. <view @touchmove.prevent.stop>
  73. <u-popup :show="rankPopup" @close="closeFn" mode="center" round="var(--rounded-big)">
  74. <view class="w-[570rpx] px-[32rpx] popup-common center">
  75. <view class="title">{{ t('rankingRules') }}</view>
  76. <scroll-view :scroll-y="true" class="px-[30rpx] box-border max-h-[260rpx]">
  77. <view class="text-[28rpx] leading-[40rpx] mb-[20rpx]">{{ rankConfig.rank_remark }}</view>
  78. </scroll-view>
  79. <view class="btn-wrap !pt-[40rpx]">
  80. <button class="primary-btn-bg w-[480rpx] h-[70rpx] text-[26rpx] leading-[70rpx] rounded-[35rpx] !text-[#fff] font-500" @click="rankPopup = false">我知道了</button>
  81. </view>
  82. </view>
  83. </u-popup>
  84. </view>
  85. </view>
  86. </view>
  87. </template>
  88. <script setup lang="ts">
  89. import { reactive, ref, computed, nextTick } from 'vue'
  90. import { t } from '@/locale'
  91. import { redirect, img, pxToRpx } from '@/utils/common';
  92. import { getRankList, getRankGoodsList, getRankConfig } from '@/addon/mall/api/rank';
  93. import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue';
  94. import useMescroll from '@/components/mescroll/hooks/useMescroll.js';
  95. import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue'
  96. import { onLoad, onPageScroll, onReachBottom } from '@dcloudio/uni-app';
  97. import { topTabar } from '@/utils/topTabbar'
  98. import { useGoods } from '@/addon/mall/hooks/useGoods'
  99. const diyGoods = useGoods();
  100. const { mescrollInit, downCallback, getMescroll } = useMescroll(onPageScroll, onReachBottom);
  101. const mescrollRef = ref(null);
  102. const loading = ref<boolean>(false);
  103. // 获取系统状态栏的高度
  104. let menuButtonInfo: any = {};
  105. // 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
  106. // #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
  107. menuButtonInfo = uni.getMenuButtonBoundingClientRect();
  108. // #endif
  109. /********* 自定义头部 - start ***********/
  110. const topTabarObj = topTabar()
  111. let param = topTabarObj.setTopTabbarParam({ title: '' })
  112. const topStyle = computed(() => {
  113. let style = pxToRpx(Number(menuButtonInfo.height) + menuButtonInfo.top + 8) + 30 + 'rpx;'
  114. return style
  115. })
  116. /********* 自定义头部 - end ***********/
  117. // 获取系统信息
  118. const systemInfo = uni.getSystemInfoSync();
  119. const topImageHeight = 450;
  120. const screenHeight = systemInfo.windowHeight;
  121. // 将屏幕高度转换为 rpx
  122. const screenHeightInRpx = (screenHeight / systemInfo.screenWidth) * 750;
  123. // 计算列表高度
  124. const listHeight = computed(() => {
  125. const listHeightValue = screenHeightInRpx - topImageHeight;
  126. return `${ listHeightValue }rpx`;
  127. });
  128. /**************** 榜单规则 ********************/
  129. const rankPopup = ref(false)
  130. const closeFn = () => {
  131. rankPopup.value = false
  132. }
  133. const rankList = ref<Array<any>>([]);
  134. const rankGoodsList = ref<Array<any>>([]);
  135. const centered = ref(false); // 是否居中
  136. const calculateCentered = () => {
  137. nextTick(() => {
  138. const query = uni.createSelectorQuery();
  139. query.selectAll('.category-btn').boundingClientRect((rects) => {
  140. if (rects && rects.length > 0) {
  141. const totalWidth = rects.reduce((sum, rect) => sum + rect.width, 0);
  142. const screenWidth = uni.getSystemInfoSync().windowWidth;
  143. centered.value = totalWidth <= screenWidth; // 判断是否需要居中
  144. } else {
  145. console.error('Failed to get .category-btn elements.');
  146. }
  147. })
  148. .exec();
  149. });
  150. };
  151. // 加载分类数据
  152. const getRankListFn = (isFirstLoad = false) => {
  153. getRankList().then((res) => {
  154. rankList.value = res.data
  155. // 仅在首次加载时选择第一个分类
  156. if (rankId.value) {
  157. for (let i = 0; i < rankList.value.length; i++) {
  158. if (rankId.value == rankList.value[i].rank_id) {
  159. selectCategory(rankList.value[i], i);
  160. break;
  161. }
  162. }
  163. } else if (isFirstLoad && rankList.value && rankList.value.length) {
  164. selectCategory(rankList.value[0], 0);
  165. } else if (!rankList.value.length) {
  166. loading.value = true;
  167. }
  168. calculateCentered();
  169. }).catch((error) => {
  170. console.error("加载分类数据失败", error);
  171. })
  172. };
  173. const rankConfig = reactive({});
  174. // 榜单设置
  175. const getRankConfigFn = () => {
  176. getRankConfig().then((res: any) => {
  177. Object.assign(rankConfig, res.data); // 合并新数据
  178. });
  179. };
  180. // 当前选中的分类的索引
  181. const activeIndex = ref(0)
  182. // 当前选中的分类的rank_id
  183. const rankId = ref(0)
  184. // 当前选中的分类的goods_source
  185. const goodsSource = ref()
  186. // 点击分类按钮时,更新选中的分类
  187. function selectCategory(rank: any, index: any) {
  188. loading.value = false;
  189. // 清空之前选中的商品列表
  190. rankGoodsList.value = [];
  191. activeIndex.value = index
  192. rankId.value = rank.rank_id
  193. goodsSource.value = rank.goods_source
  194. getMescroll()?.resetUpScroll();
  195. }
  196. //获取榜单商品
  197. const getRankGoodsListFn = (mescroll: any) => {
  198. if (rankList.value.length == 0) return;
  199. loading.value = false;
  200. let data: object = {
  201. page: mescroll.num,
  202. limit: mescroll.size,
  203. rank_id: rankId.value
  204. };
  205. getRankGoodsList(data).then((res: any) => {
  206. let newArr = (res.data.data as Array<Object>).map((el: any) => {
  207. return el
  208. })
  209. let ifPage = true
  210. //设置列表数据
  211. if (mescroll.num == 1) {
  212. rankGoodsList.value = []; //如果是第一页需手动制空列表
  213. }
  214. rankGoodsList.value = rankGoodsList.value.concat(newArr);
  215. if (goodsSource.value == 'goods') {
  216. ifPage = false
  217. } else {
  218. ifPage = true
  219. }
  220. mescroll.endSuccess(newArr.length, ifPage);
  221. loading.value = true;
  222. }).catch(() => {
  223. loading.value = true;
  224. mescroll.endErr(); // 请求失败, 结束加载
  225. })
  226. }
  227. // 跳转商品详情
  228. const toLink = (goods_id: any) => {
  229. redirect({ url: '/addon/mall/pages/goods/detail', param: { goods_id } })
  230. }
  231. // 获取排行榜名次图片的函数
  232. function getRankBadge(sort: any) {
  233. switch (sort) {
  234. case 1:
  235. return img('addon/mall/rank/rank_first.png');
  236. case 2:
  237. return img('addon/mall/rank/rank_second.png');
  238. case 3:
  239. return img('addon/mall/rank/rank_third.png');
  240. default:
  241. return img('addon/mall/rank/rank.png');
  242. }
  243. }
  244. onLoad(async(option: any) => {
  245. rankId.value = option.rank_id || 0;
  246. getRankConfigFn()
  247. getRankListFn(true)
  248. })
  249. </script>
  250. <style lang="scss" scoped>
  251. @import '@/addon/mall/styles/common.scss';
  252. .rank-head {
  253. position: relative;
  254. .content-box {
  255. width: 100%;
  256. position: absolute;
  257. top: 328rpx;
  258. .category-slider {
  259. width: 95%;
  260. margin: 0 auto;
  261. line-height: 100rpx;
  262. white-space: nowrap;
  263. flex-direction: row;
  264. justify-content: center;
  265. align-items: center;
  266. .category-con {
  267. width: 100%;
  268. display: flex;
  269. align-items: center;
  270. .category-btn {
  271. display: inline-block;
  272. padding: 0 20rpx;
  273. height: 55rpx;
  274. text-align: center;
  275. line-height: 55rpx;
  276. justify-content: center;
  277. align-items: center;
  278. border-radius: 40rpx;
  279. font-size: 24rpx;
  280. margin-right: 20rpx;
  281. }
  282. }
  283. }
  284. // .content {
  285. // display: flex;
  286. // justify-content: center;
  287. // align-items: center;
  288. // border-radius: 30rpx;
  289. // padding: 0 20rpx;
  290. // height: 44rpx;
  291. // font-size: 26rpx;
  292. // color: var(--primary-color);
  293. // background: linear-gradient(to right, #FFEBD7, #FFFFFF, #FFEBD7);
  294. // }
  295. }
  296. }
  297. .rank-list {
  298. background: #F8F8F8;
  299. border-radius: 34rpx 34rpx 0 0;
  300. .bank-buying {
  301. width: 100rpx;
  302. height: 44rpx;
  303. border-radius: 10rpx;
  304. font-family: PingFang SC, PingFang SC;
  305. font-weight: 500;
  306. font-size: 24rpx;
  307. color: #FFFFFF;
  308. line-height: 44rpx;
  309. text-align: center;
  310. }
  311. }
  312. </style>