cart.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. <template>
  2. <view class="cart">
  3. <tui-skeleton v-if="isRequest" :loadingType="3" :isLoading="true"></tui-skeleton>
  4. <view class="sticky-top">
  5. <!-- 自定义顶部 -->
  6. <cm-header
  7. :rightButton="null"
  8. :leftButtonGroup="leftButtonGroup"
  9. title="购物车"
  10. @leftClick="handleLeftClick"
  11. @rightClick="handleRightClick"
  12. ></cm-header>
  13. <view style="height: 8rpx;background-color: #fff;"></view>
  14. <template v-if="!isDeleted && (shopList.length !== 0 || expiredProducts.length !== 0)">
  15. <!-- 顶部 -->
  16. <view class="goods-total">
  17. <view class="total">共{{ kindCount }}件商品</view>
  18. <tui-button
  19. :size="24"
  20. width="88rpx"
  21. height="42rpx"
  22. shape="circle"
  23. :plain="true"
  24. type="base"
  25. @click="isDeleted = true"
  26. >
  27. 删除
  28. </tui-button>
  29. </view>
  30. <view class="receive-coupon">
  31. <view class="tip-text" v-text="couponTipText"></view>
  32. <tui-button
  33. :size="24"
  34. width="88rpx"
  35. height="42rpx"
  36. shape="circle"
  37. type="base"
  38. @click="visiable = true"
  39. >
  40. 领券
  41. </tui-button>
  42. </view>
  43. </template>
  44. </view>
  45. <!-- 购物车列表为空 -->
  46. <template v-if="shopList.length === 0 && expiredProducts.length === 0">
  47. <tui-no-data :imgUrl="staticUrl + 'icon-empty-cart.png'" :imgHeight="230" :imgWidth="290">
  48. <view class="empty-tip">购物车空空的,快去逛逛吧~</view>
  49. </tui-no-data>
  50. </template>
  51. <template v-else>
  52. <!-- 有效商品 -->
  53. <view class="supplier-area" v-for="item in shopList" :key="item.shopId">
  54. <cm-cart-supplier-area
  55. :shopInfo="item"
  56. @change="checkedProductChange"
  57. @countChange="onCountChange"
  58. ref="supplierArea"
  59. ></cm-cart-supplier-area>
  60. </view>
  61. <!-- 失效商品 -->
  62. <view class="supplier-area" v-if="expiredProducts.length > 0">
  63. <cm-cart-expired-area :expiredList="expiredProducts" @clear="onClearExp"></cm-cart-expired-area>
  64. </view>
  65. <view style="height: 100rpx;"></view>
  66. <!-- 安全区域 -->
  67. <cm-safe-area-bottom></cm-safe-area-bottom>
  68. <!-- 购物车导航 -->
  69. <cm-cart-navbar
  70. :safeArea="true"
  71. :isCheckedAll="isChekedAll"
  72. :isDeleted="isDeleted"
  73. :data="navbarData"
  74. @all="onCheckedAll"
  75. @submit="onSumit"
  76. @cancel="isDeleted = false"
  77. @remove="removeModal = true"
  78. ></cm-cart-navbar>
  79. </template>
  80. <!-- 优惠券列表 -->
  81. <cm-coupon-popup
  82. :visiable="visiable"
  83. :list="couponList"
  84. :couponTabs="couponTabs"
  85. :hasSafeArea="true"
  86. @close="visiable = false"
  87. @change="onCouponChange"
  88. @couponClick="onCouponClick"
  89. hasTabs="true"
  90. translateY="34px"
  91. ></cm-coupon-popup>
  92. <!-- 确认弹框 -->
  93. <tui-modal :show="removeModal" content="确认删除选中的商品吗?" @click="onConfirmRemove"></tui-modal>
  94. <!-- 清空失效商品确认 -->
  95. <tui-modal :show="removeExpModal" content="确认清空失效商品?" @click="onConfirmRemoveExp"></tui-modal>
  96. </view>
  97. </template>
  98. <script>
  99. import { fetchCartInfo } from '@/services/api/cart.js'
  100. import { fetchCouponListByProductIds } from '@/services/api/coupon.js'
  101. import { arrayUnique } from '@/common/utils.js'
  102. import { mapGetters, mapActions } from 'vuex'
  103. import CouponUtils from '@/common/couponUtils.js'
  104. // 业务帮助函数
  105. import {
  106. totalAllCheckedProduct,
  107. computeTotalPrice,
  108. initFormatCouponList,
  109. makeCouponUseTip
  110. } from '@/common/business.helper.js'
  111. const resetData = () => ({
  112. leftButtonGroup: [
  113. {
  114. icon: 'icon-daohangfanhui',
  115. color: '#333'
  116. },
  117. {
  118. icon: 'icon-fanhuishouye',
  119. color: '#333'
  120. }
  121. ],
  122. isRequest: true,
  123. isDeleted: false,
  124. expiredProducts: [], // 失效商品列表
  125. shopList: [], // 供应商&&商品列表
  126. checkedShopMap: {},
  127. isChekedAll: false,
  128. removeModal: false,
  129. removeExpModal: false,
  130. // 商品价格
  131. checkedProductList: [],
  132. allPrice: 0, // 商品总价
  133. // 优惠券列表
  134. visiable: false,
  135. currentTab: 0,
  136. receiveCouponList: [], // 已领取优惠券
  137. ableCouponList: [], // 可领取优惠券
  138. couponTabs: [],
  139. currentCoupon: null,
  140. nextCoupon: null,
  141. couponTipText: '', // 可用优惠券提示
  142. // 保存变动
  143. changeRef: null
  144. })
  145. export default {
  146. data() {
  147. return resetData()
  148. },
  149. computed: {
  150. ...mapGetters(['userId', 'kindCount']),
  151. couponList() {
  152. return this.currentTab === 0 ? this.receiveCouponList : this.ableCouponList
  153. },
  154. navbarData() {
  155. const result = {
  156. finallyPrice: 0, // 实际应付
  157. couponAmount: 0,
  158. allPrice: 0,
  159. discountedPrice: 0,
  160. count: 0
  161. }
  162. if (this.currentCoupon) {
  163. result.couponAmount = this.currentCoupon.couponAmount
  164. result.discountedPrice = this.currentCoupon.couponAmount
  165. }
  166. result.allPrice = this.allPrice
  167. if (this.allPrice - result.discountedPrice > 0) {
  168. result.finallyPrice = this.allPrice - result.couponAmount
  169. }
  170. result.count = this.checkedProductList.length
  171. console.log(result)
  172. return result
  173. }
  174. },
  175. onPullDownRefresh() {
  176. this.resetData()
  177. this.fetchCartInfo()
  178. },
  179. onShow() {
  180. this.resetData()
  181. this.fetchCartInfo()
  182. },
  183. methods: {
  184. // 自定义导航触发事件
  185. handleLeftClick() {},
  186. handleRightClick() {},
  187. headerNavInit(info) {
  188. this.headerNavHeight = info.height
  189. },
  190. ...mapActions('cart', ['removeFromCart']),
  191. // 初始化数据
  192. resetData() {
  193. const data = resetData()
  194. for (let key in data) {
  195. this[key] = data[key]
  196. }
  197. },
  198. // 清空失效商品
  199. onClearExp() {
  200. this.removeExpModal = true
  201. },
  202. // 确认清空失效商品
  203. async onConfirmRemoveExp(e) {
  204. // removeProductFromCart
  205. if (!e.index) return
  206. try {
  207. await this.removeFromCart(this.expiredProducts.map(product => product.cartId))
  208. this.resetData()
  209. this.fetchCartInfo()
  210. } catch (e) {
  211. console.log('删除失效商品失败')
  212. } finally {
  213. this.removeExpModal = false
  214. }
  215. },
  216. // 提交订单
  217. onSumit() {
  218. if (this.checkedProductList.length <= 0) {
  219. return this.$toast.error('请选择商品')
  220. }
  221. const params = {}
  222. params.allPrice = this.allPrice
  223. params.allCount = this.checkedProductList.length
  224. params.cartIds = this.checkedProductList.map(product => product.cartId)
  225. params.productIds = this.checkedProductList.map(product => product.productId)
  226. params.productCount = ''
  227. uni.setStorageSync('COMMIT_CART_INFO', params)
  228. this.$router.navigateTo('order/order-create')
  229. },
  230. // 优惠券列表类型更换
  231. onCouponChange(index) {
  232. this.currentTab = index
  233. },
  234. // 优惠券点击事件
  235. onCouponClick(coupon) {
  236. if (coupon.controlType === 'receive') {
  237. this.fetchCouponList()
  238. } else {
  239. this.visiable = false
  240. }
  241. },
  242. // 设置couponTab数据
  243. makeCouponTabs() {
  244. return [
  245. {
  246. name: '已领取优惠券',
  247. num: this.receiveCouponList.length,
  248. isDot: false,
  249. disabled: false
  250. },
  251. {
  252. name: '可领取优惠券',
  253. num: this.receiveCouponList.ableCouponList,
  254. isDot: false,
  255. disabled: false
  256. }
  257. ]
  258. },
  259. // 获取购物车商品可用优惠券
  260. async fetchCouponList() {
  261. const productIds = []
  262. this.shopList.forEach(shop => shop.productList.forEach(product => productIds.push(product.productId)))
  263. try {
  264. const res = await fetchCouponListByProductIds({ userId: this.userId, productIds: productIds.join(',') })
  265. this.receiveCouponList = initFormatCouponList(res.data.receiveCouponList, 'search', true)
  266. this.ableCouponList = initFormatCouponList(res.data.ableCouponList, 'receive', true)
  267. this.couponTabs = this.makeCouponTabs()
  268. } catch (e) {
  269. console.log(e)
  270. }
  271. },
  272. // 获取购物车信息
  273. async fetchCartInfo() {
  274. try {
  275. const res = await fetchCartInfo({ userId: this.userId })
  276. this.expiredProducts = res.data.products
  277. this.shopList = res.data.shopList
  278. // 获取优惠券列表
  279. if (this.shopList.length > 0) {
  280. // 获取优惠券列表
  281. this.fetchCouponList()
  282. }
  283. return res
  284. } catch (e) {
  285. return e
  286. } finally {
  287. this.isRequest = false
  288. }
  289. },
  290. // 确认删除
  291. async onConfirmRemove({ index }) {
  292. if (!index) return (this.removeModal = false)
  293. const ids = this.getCheckedProductIds()
  294. const cartIds = this.shopList.reduce((prev, item) => {
  295. item.productList.forEach(prod => {
  296. if (ids.includes(prod.productId.toString())) {
  297. prev.push(prod.cartId)
  298. }
  299. })
  300. return prev
  301. }, [])
  302. try {
  303. await this.removeFromCart(cartIds)
  304. this.fetchCartInfo()
  305. } catch (e) {
  306. console.log(e)
  307. } finally {
  308. this.isDeleted = false
  309. this.removeModal = false
  310. }
  311. },
  312. // 选中/取消全部商品
  313. onCheckedAll() {
  314. if (this.isChekedAll) {
  315. this.$refs.supplierArea.forEach(item => item.unSelectAll())
  316. } else {
  317. this.$refs.supplierArea.forEach(item => item.selectAll())
  318. }
  319. },
  320. // 选中商品变化
  321. checkedProductChange(e) {
  322. this.changeRef = e
  323. this.handleCartManager()
  324. },
  325. // 修改商品checked属性
  326. updatePorudctChecked(row) {
  327. const shopInfo = this.shopList.find(item => row.shopId === item.shopId)
  328. if (!shopInfo) return
  329. shopInfo.productList.forEach(item => {
  330. if (row.checkedList.includes(item.productId.toString())) {
  331. this.$set(item, 'checked', true)
  332. } else {
  333. this.$set(item, 'checked', false)
  334. }
  335. })
  336. },
  337. // 购物车数据处理器
  338. handleCartManager() {
  339. if (!this.changeRef) return
  340. this.updatePorudctChecked(this.changeRef)
  341. // 保存选中商品id与供应商id的k-v关系
  342. this.$set(this.checkedShopMap, this.changeRef.shopId, this.changeRef.checkedList)
  343. this.isChekedAll = this.validateIsChekedAll()
  344. // 获取已选商品列表
  345. this.checkedProductList = totalAllCheckedProduct(this.shopList)
  346. // 计算商品总价
  347. this.allPrice = computeTotalPrice(this.checkedProductList)
  348. const couponUtils = new CouponUtils(this.receiveCouponList, this.checkedProductList)
  349. this.currentCoupon = couponUtils.bestCoupon
  350. this.nextCoupon = couponUtils.secondCoupon
  351. this.couponTipText = makeCouponUseTip(this.currentCoupon, this.nextCoupon, this.allPrice)
  352. },
  353. // 商品数量变化
  354. async onCountChange(e) {
  355. await this.$store.dispatch('cart/updateProductCount', {
  356. cartId: e.cartId,
  357. productCount: e.count
  358. })
  359. await this.fetchCartInfo()
  360. this.handleCartManager()
  361. },
  362. // 验证是否全选
  363. validateIsChekedAll() {
  364. return this.shopList.every(item => {
  365. const checkedList = this.checkedShopMap[item.shopId] || []
  366. return item.productList.length === checkedList.length
  367. })
  368. },
  369. // 获取已选商品id列表
  370. getCheckedProductIds() {
  371. const list = []
  372. for (let key in this.checkedShopMap) {
  373. list.push(...this.checkedShopMap[key])
  374. }
  375. return list
  376. }
  377. }
  378. }
  379. </script>
  380. <style scoped lang="scss">
  381. .goods-total {
  382. @extend .cm-flex-between;
  383. background: #f7f7f7;
  384. padding: 0 24rpx;
  385. height: 80rpx;
  386. .total {
  387. font-size: 30rpx;
  388. color: #333;
  389. }
  390. }
  391. .receive-coupon {
  392. @extend .cm-flex-between;
  393. background: #fff;
  394. padding: 0 24rpx;
  395. height: 80rpx;
  396. .tip-text {
  397. font-size: 26rpx;
  398. color: #ff457b;
  399. white-space: nowrap;
  400. overflow: hidden;
  401. text-overflow: ellipsis;
  402. }
  403. }
  404. .supplier-area {
  405. margin-bottom: 24rpx;
  406. }
  407. .remove-view {
  408. position: relative;
  409. width: 100vw;
  410. height: 100vh;
  411. box-sizing: border-box;
  412. .shop-list {
  413. height: 100%;
  414. background-color: #f7f7f7;
  415. box-sizing: border-box;
  416. }
  417. .icon {
  418. margin-right: 6rpx;
  419. text {
  420. display: inline-block;
  421. font-size: 30rpx;
  422. margin-left: 4rpx;
  423. color: #333;
  424. }
  425. }
  426. .navbar {
  427. position: absolute;
  428. width: 100%;
  429. bottom: 0;
  430. left: 0;
  431. z-index: 98;
  432. min-height: 100rpx;
  433. box-sizing: border-box;
  434. padding: 0 24rpx;
  435. background: #fff;
  436. border-top: 1rpx solid #eee;
  437. .content {
  438. height: 100rpx;
  439. @extend .cm-flex-between;
  440. .control {
  441. width: 444rpx;
  442. @extend .cm-flex-between;
  443. }
  444. }
  445. }
  446. }
  447. </style>