cart.vue 13 KB

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