list.vue 12 KB

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