list.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <!-- 秒杀活动列表 -->
  2. <template>
  3. <s-layout navbar="inner" :bgStyle="{ color: 'rgb(245,28,19)' }">
  4. <!--顶部背景图-->
  5. <view class="page-bg" :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"></view>
  6. <!-- 时间段轮播图 -->
  7. <view class="header" v-if="activeTimeConfig?.sliderPicUrls?.length > 0">
  8. <swiper indicator-dots="true" autoplay="true" :circular="true" interval="3000" duration="1500"
  9. indicator-color="rgba(255,255,255,0.6)" indicator-active-color="#fff">
  10. <block v-for="(picUrl, index) in activeTimeConfig.sliderPicUrls" :key="index">
  11. <swiper-item class="borRadius14">
  12. <image :src="picUrl" class="slide-image borRadius14" lazy-load />
  13. </swiper-item>
  14. </block>
  15. </swiper>
  16. </view>
  17. <!-- 时间段列表 -->
  18. <view class="flex align-center justify-between ss-p-25">
  19. <!-- 左侧图标 -->
  20. <view class="time-icon">
  21. <!-- TODO 芋艿:图片统一维护 -->
  22. <image class="ss-w-100 ss-h-100" src="http://mall.yudao.iocoder.cn/static/images/priceTag.png" />
  23. </view>
  24. <scroll-view class="time-list" :scroll-into-view="activeTimeElId" scroll-x scroll-with-animation>
  25. <view v-for="(config, index) in timeConfigList" :key="index"
  26. :class="['item', { active: activeTimeIndex === index}]" :id="`timeItem${index}`"
  27. @tap="handleChangeTimeConfig(index,config.id)">
  28. <!-- 活动起始时间 -->
  29. <view class="time">{{ config.startTime }}</view>
  30. <!-- 活动状态 -->
  31. <view class="status">{{ config?.status }}</view>
  32. </view>
  33. </scroll-view>
  34. </view>
  35. <!-- 内容区 -->
  36. <view class="list-content">
  37. <!-- 活动倒计时 -->
  38. <view class="content-header ss-flex-col ss-col-center ss-row-center">
  39. <view class="content-header-box ss-flex ss-row-center">
  40. <view class="countdown-box ss-flex" v-if="activeTimeConfig?.status === TimeStatusEnum.STARTED">
  41. <view class="countdown-title ss-m-r-12">距结束</view>
  42. <view class="ss-flex countdown-time">
  43. <view class="ss-flex countdown-h">{{ countDown.h }}</view>
  44. <view class="ss-m-x-4">:</view>
  45. <view class="countdown-num ss-flex ss-row-center">{{ countDown.m }}</view>
  46. <view class="ss-m-x-4">:</view>
  47. <view class="countdown-num ss-flex ss-row-center">{{ countDown.s }}</view>
  48. </view>
  49. </view>
  50. <view v-else> {{ activeTimeConfig?.status }} </view>
  51. </view>
  52. </view>
  53. <!-- 活动列表 -->
  54. <scroll-view class="scroll-box" :style="{ height: pageHeight + 'rpx' }" scroll-y="true"
  55. :scroll-with-animation="false" :enable-back-to-top="true">
  56. <view class="goods-box ss-m-b-20" v-for="activity in activityList" :key="activity.id">
  57. <s-goods-column size="lg" :data="{ ...activity, price: activity.seckillPrice }"
  58. :goodsFields="goodsFields" :seckillTag="true">
  59. <!-- 抢购进度 -->
  60. <template #activity>
  61. <view class="limit">限量 <text class="ss-m-l-5">{{ activity.stock}}
  62. {{activity.unitName}}</text></view>
  63. <su-progress :percentage="activity.percent" strokeWidth="10" textInside isAnimate />
  64. </template>
  65. <!-- 抢购按钮 -->
  66. <template #cart>
  67. <button
  68. :class="['ss-reset-button cart-btn', { disabled: activeTimeConfig?.status === TimeStatusEnum.END}]"
  69. v-if="activeTimeConfig?.status === TimeStatusEnum.WAIT_START">
  70. <span>未开始</span>
  71. </button>
  72. <button
  73. :class="['ss-reset-button cart-btn', { disabled: activeTimeConfig?.status === TimeStatusEnum.END}]"
  74. @click="sheep.$router.go('/pages/goods/seckill', { id: activity.id })"
  75. v-else-if='activeTimeConfig?.status === TimeStatusEnum.STARTED'>
  76. <span>马上抢</span>
  77. </button>
  78. <button
  79. :class="['ss-reset-button cart-btn', { disabled: activeTimeConfig?.status === TimeStatusEnum.END}]"
  80. v-else>
  81. <span>已结束</span>
  82. </button>
  83. </template>
  84. </s-goods-column>
  85. </view>
  86. <uni-load-more v-if="activityTotal > 0" :status="loadStatus" :content-text="{
  87. contentdown: '上拉加载更多',
  88. }" @tap="loadMore" />
  89. </scroll-view>
  90. </view>
  91. </s-layout>
  92. </template>
  93. <script setup>
  94. import {
  95. reactive,
  96. computed,
  97. ref,
  98. nextTick
  99. } from 'vue';
  100. import {
  101. onLoad,
  102. onReachBottom
  103. } from '@dcloudio/uni-app';
  104. import sheep from '@/sheep';
  105. import {
  106. useDurationTime
  107. } from '@/sheep/hooks/useGoods';
  108. import SeckillApi from "@/sheep/api/promotion/seckill";
  109. import dayjs from "dayjs";
  110. import {
  111. TimeStatusEnum
  112. } from "@/sheep/util/const";
  113. // 计算页面高度
  114. const {
  115. safeAreaInsets,
  116. safeArea
  117. } = sheep.$platform.device;
  118. const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
  119. const pageHeight = (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sheep.$platform.navbar - 350;
  120. const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-header.png');
  121. // 商品控件显示的字段(不显示库存、销量。改为显示自定义的进度条)
  122. const goodsFields = {
  123. name: {
  124. show: true
  125. },
  126. introduction: {
  127. show: true
  128. },
  129. price: {
  130. show: true
  131. },
  132. marketPrice: {
  133. show: true
  134. },
  135. };
  136. //#region 时间段
  137. // 时间段列表
  138. const timeConfigList = ref([])
  139. // 查询时间段
  140. const getSeckillConfigList = async () => {
  141. const {
  142. data
  143. } = await SeckillApi.getSeckillConfigList()
  144. const now = dayjs();
  145. const today = now.format('YYYY-MM-DD')
  146. const select = ref([])
  147. // 判断时间段的状态
  148. data.forEach((config, index) => {
  149. const startTime = dayjs(`${today} ${config.startTime}`)
  150. const endTime = dayjs(`${today} ${config.endTime}`)
  151. select.value[index] = config.id;
  152. if (now.isBefore(startTime)) {
  153. config.status = TimeStatusEnum.WAIT_START;
  154. // select.value[index] = config.id;
  155. } else if (now.isAfter(endTime)) {
  156. config.status = TimeStatusEnum.END;
  157. // select.value[index] = config.id;
  158. } else {
  159. config.status = TimeStatusEnum.STARTED;
  160. // select.value[index] = config.id;
  161. activeTimeIndex.value = index
  162. }
  163. })
  164. timeConfigList.value = data
  165. // 默认选中进行中的活动
  166. handleChangeTimeConfig(activeTimeIndex.value, select.value[activeTimeIndex.value]);
  167. // 滚动到进行中的时间段
  168. scrollToTimeConfig(activeTimeIndex.value)
  169. }
  170. // 滚动到指定时间段
  171. const activeTimeElId = ref('') // 当前选中的时间段的元素ID
  172. const scrollToTimeConfig = (index) => {
  173. nextTick(() => activeTimeElId.value = `timeItem${index}`)
  174. }
  175. // 切换时间段
  176. const activeTimeIndex = ref(0) // 当前选中的时间段的索引
  177. const activeTimeConfig = computed(() => timeConfigList.value[activeTimeIndex.value]) // 当前选中的时间段
  178. const handleChangeTimeConfig = (index, id) => {
  179. activeTimeIndex.value = index
  180. // 查询活动列表
  181. activityPageParams.pageNo = 1
  182. activityPageParams.configId = id;
  183. activityList.value = []
  184. getActivityList();
  185. }
  186. // 倒计时
  187. const countDown = computed(() => {
  188. const endTime = activeTimeConfig.value?.endTime
  189. if (endTime) {
  190. return useDurationTime(`${dayjs().format('YYYY-MM-DD')} ${endTime}`);
  191. }
  192. });
  193. //#endregion
  194. //#region 分页查询活动列表
  195. // 查询活动列表
  196. const activityPageParams = reactive({
  197. configId: 0, // 时间段 ID
  198. pageNo: 1, // 页码
  199. pageSize: 5, // 每页数量
  200. })
  201. const activityTotal = ref(0) // 活动总数
  202. const activityList = ref([]) // 活动列表
  203. const loadStatus = ref('') // 页面加载状态
  204. async function getActivityList() {
  205. loadStatus.value = 'loading';
  206. const {
  207. data
  208. } = await SeckillApi.getSeckillActivityPage(activityPageParams)
  209. data.list.forEach(activity => {
  210. // 计算抢购进度
  211. activity.percent = parseInt(100 * (activity.totalStock - activity.stock) / activity.totalStock);
  212. })
  213. activityList.value = activityList.value.concat(...data.list);
  214. activityTotal.value = data.total;
  215. loadStatus.value = activityList.value.length < activityTotal.value ? 'more' : 'noMore';
  216. }
  217. // 加载更多
  218. function loadMore() {
  219. if (loadStatus.value !== 'noMore') {
  220. activityPageParams.pageNo += 1
  221. getActivityList();
  222. }
  223. }
  224. // 上拉加载更多
  225. onReachBottom(() => loadMore());
  226. //#endregion
  227. // 页面初始化
  228. onLoad(async () => {
  229. await getSeckillConfigList()
  230. });
  231. </script>
  232. <style lang="scss" scoped>
  233. // 顶部背景图
  234. .page-bg {
  235. width: 100%;
  236. height: 458rpx;
  237. background: v-bind(headerBg) no-repeat;
  238. background-size: 100% 100%;
  239. }
  240. // 时间段轮播图
  241. .header {
  242. width: 710rpx;
  243. height: 330rpx;
  244. margin: -276rpx auto 0 auto;
  245. border-radius: 14rpx;
  246. overflow: hidden;
  247. swiper {
  248. height: 330rpx !important;
  249. border-radius: 14rpx;
  250. overflow: hidden;
  251. }
  252. image {
  253. width: 100%;
  254. height: 100%;
  255. border-radius: 14rpx;
  256. overflow: hidden;
  257. img {
  258. border-radius: 14rpx;
  259. }
  260. }
  261. }
  262. // 时间段列表:左侧图标
  263. .time-icon {
  264. width: 75rpx;
  265. height: 70rpx;
  266. }
  267. // 时间段列表
  268. .time-list {
  269. width: 596rpx;
  270. white-space: nowrap;
  271. // 时间段
  272. .item {
  273. display: inline-block;
  274. font-size: 20rpx;
  275. color: #666;
  276. text-align: center;
  277. box-sizing: border-box;
  278. margin-right: 30rpx;
  279. width: 130rpx;
  280. // 开始时间
  281. .time {
  282. font-size: 36rpx;
  283. font-weight: 600;
  284. color: #333;
  285. }
  286. // 选中的时间段
  287. &.active {
  288. .time {
  289. color: var(--ui-BG-Main);
  290. }
  291. // 状态
  292. .status {
  293. height: 30rpx;
  294. line-height: 30rpx;
  295. border-radius: 15rpx;
  296. width: 128rpx;
  297. background: linear-gradient(90deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
  298. color: #fff;
  299. }
  300. }
  301. }
  302. }
  303. // 内容区
  304. .list-content {
  305. position: relative;
  306. z-index: 3;
  307. margin: 0 20rpx 0 20rpx;
  308. background: #fff;
  309. border-radius: 20rpx 20rpx 0 0;
  310. .content-header {
  311. width: 100%;
  312. border-radius: 20rpx 20rpx 0 0;
  313. height: 150rpx;
  314. background: linear-gradient(180deg, #fff4f7, #ffe6ec);
  315. .content-header-box {
  316. width: 678rpx;
  317. height: 64rpx;
  318. background: rgba($color: #fff, $alpha: 0.66);
  319. border-radius: 32px;
  320. // 场次倒计时内容
  321. .countdown-title {
  322. font-size: 28rpx;
  323. font-weight: 500;
  324. color: #333333;
  325. line-height: 28rpx;
  326. }
  327. // 场次倒计时
  328. .countdown-time {
  329. font-size: 28rpx;
  330. color: rgba(#ed3c30, 0.23);
  331. // 场次倒计时:小时部分
  332. .countdown-h {
  333. font-size: 24rpx;
  334. font-family: OPPOSANS;
  335. font-weight: 500;
  336. color: #ffffff;
  337. padding: 0 4rpx;
  338. height: 40rpx;
  339. background: rgba(#ed3c30, 0.23);
  340. border-radius: 6rpx;
  341. }
  342. // 场次倒计时:分钟、秒
  343. .countdown-num {
  344. font-size: 24rpx;
  345. font-family: OPPOSANS;
  346. font-weight: 500;
  347. color: #ffffff;
  348. width: 40rpx;
  349. height: 40rpx;
  350. background: rgba(#ed3c30, 0.23);
  351. border-radius: 6rpx;
  352. }
  353. }
  354. }
  355. }
  356. // 活动列表
  357. .scroll-box {
  358. height: 900rpx;
  359. // 活动
  360. .goods-box {
  361. position: relative;
  362. // 抢购按钮
  363. .cart-btn {
  364. position: absolute;
  365. bottom: 10rpx;
  366. right: 20rpx;
  367. z-index: 11;
  368. height: 44rpx;
  369. line-height: 50rpx;
  370. padding: 0 20rpx;
  371. border-radius: 25rpx;
  372. font-size: 24rpx;
  373. color: #fff;
  374. background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
  375. &.disabled {
  376. background: $gray-b;
  377. color: #fff;
  378. }
  379. }
  380. // 秒杀限量商品数
  381. .limit {
  382. font-size: 22rpx;
  383. color: $dark-9;
  384. margin-bottom: 5rpx;
  385. }
  386. }
  387. }
  388. }
  389. </style>