cart.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. <simple-safe-view>
  53. <!-- 有效商品 -->
  54. <view class="supplier-area" v-for="item in shopList" :key="item.shopId">
  55. <cm-cart-supplier-area
  56. :shopInfo="item"
  57. :isDeleted="isDeleted"
  58. @change="checkedProductChange"
  59. @countChange="onCountChange"
  60. @unitChange="onUnitChange"
  61. ref="supplierArea"
  62. ></cm-cart-supplier-area>
  63. </view>
  64. <!-- 失效商品 -->
  65. <view class="supplier-area" v-if="expiredProducts.length > 0">
  66. <cm-cart-expired-area
  67. :expiredList="expiredProducts"
  68. @clear="removeExpModal = true"
  69. ></cm-cart-expired-area>
  70. </view>
  71. <view style="height: 100rpx;"></view>
  72. </simple-safe-view>
  73. <!-- 购物车导航 -->
  74. <cm-cart-navbar
  75. :safeArea="true"
  76. :isCheckedAll="isChekedAll"
  77. :isDeleted="isDeleted"
  78. :data="navbarData"
  79. @all="onCheckedAll"
  80. @submit="onSumit"
  81. @cancel="isDeleted = false"
  82. @remove="removeModal = true"
  83. ></cm-cart-navbar>
  84. </template>
  85. <!-- 优惠券列表 -->
  86. <cm-coupon-popup
  87. :visiable="visiable"
  88. :list="couponList"
  89. :couponTabs="couponTabs"
  90. :hasSafeArea="true"
  91. @close="visiable = false"
  92. @change="onCouponChange"
  93. @couponClick="onCouponClick"
  94. hasTabs="true"
  95. translateY="34px"
  96. ></cm-coupon-popup>
  97. <!-- 确认弹框 -->
  98. <tui-modal :show="removeModal" content="确认删除选中的商品吗?" @click="onConfirmRemove"></tui-modal>
  99. <!-- 清空失效商品确认 -->
  100. <tui-modal :show="removeExpModal" content="确认清空失效商品?" @click="onConfirmRemoveExp"></tui-modal>
  101. <!-- 重新选择商品规格 -->
  102. <cm-goods-buy-popup
  103. v-model="goodsBuyPopup"
  104. :product="productInfo"
  105. :couponList="goodsCouponList"
  106. :useType="useType"
  107. :buttonType="buttonType"
  108. @confirm="onSaveNewUnit"
  109. @activityClick="handleShowActivity"
  110. v-if="productInfo"
  111. ></cm-goods-buy-popup>
  112. <!-- 活动价 -->
  113. <cm-goods-activity-popup
  114. ref="activitypPopup"
  115. :ladderList="ladderPriceList"
  116. @open="$refs.receiveBuyPopup.close()"
  117. @close="$refs.receiveBuyPopup.open()"
  118. ></cm-goods-activity-popup>
  119. </view>
  120. </template>
  121. <script>
  122. import { fetchCartInfo } from '@/services/api/cart.js'
  123. import { fetchCouponListByProductIds } from '@/services/api/coupon.js'
  124. import { arrayUnique } from '@/common/utils.js'
  125. import { mapGetters, mapActions } from 'vuex'
  126. import CouponUtils from '@/common/couponUtils.js'
  127. import { fetchPorductInfo, fetchCouponListByProduct } from '@/common/goods.helper.js'
  128. // 业务帮助函数
  129. import {
  130. totalAllCheckedProduct,
  131. computeTotalPrice,
  132. initFormatCouponList,
  133. makeCouponUseTip
  134. } from '@/common/business.helper.js'
  135. const resetData = () => ({
  136. leftButtonGroup: [
  137. {
  138. icon: 'icon-daohangfanhui',
  139. color: '#333'
  140. },
  141. {
  142. icon: 'icon-fanhuishouye',
  143. color: '#333'
  144. }
  145. ],
  146. isRequest: true,
  147. isDeleted: false,
  148. expiredProducts: [], // 失效商品列表
  149. shopList: [], // 供应商&&商品列表
  150. checkedShopMap: {},
  151. isChekedAll: false,
  152. removeModal: false,
  153. removeExpModal: false,
  154. // 商品价格
  155. checkedProductList: [],
  156. allPrice: 0, // 商品总价
  157. // 优惠券列表
  158. visiable: false,
  159. currentTab: 0,
  160. receiveCouponList: [], // 已领取优惠券
  161. ableCouponList: [], // 可领取优惠券
  162. couponTabs: [],
  163. currentCoupon: null,
  164. nextCoupon: null,
  165. couponTipText: '', // 可用优惠券提示
  166. // 保存变动
  167. changeRef: null,
  168. // 选择商品规格
  169. cartId: 0,
  170. goodsBuyPopup: false,
  171. useType: 'cart',
  172. buttonType: 'left',
  173. goodsCouponList: [],
  174. productInfo: null,
  175. ladderPriceList: []
  176. })
  177. export default {
  178. data() {
  179. return resetData()
  180. },
  181. computed: {
  182. ...mapGetters(['userId', 'kindCount']),
  183. couponList() {
  184. return this.currentTab === 0 ? this.receiveCouponList : this.ableCouponList
  185. },
  186. navbarData() {
  187. const result = {
  188. finallyPrice: 0, // 实际应付
  189. couponAmount: 0,
  190. allPrice: 0,
  191. discountedPrice: 0,
  192. count: 0
  193. }
  194. if (this.currentCoupon) {
  195. result.couponAmount = this.currentCoupon.couponAmount
  196. result.discountedPrice = this.currentCoupon.couponAmount
  197. }
  198. result.allPrice = this.allPrice
  199. if (this.allPrice - result.discountedPrice > 0) {
  200. result.finallyPrice = this.allPrice - result.couponAmount
  201. }
  202. result.count = this.checkedProductList.length
  203. return result
  204. }
  205. },
  206. onPullDownRefresh() {
  207. this.initPage()
  208. },
  209. onShow() {
  210. this.initPage()
  211. },
  212. methods: {
  213. // 自定义导航触发事件
  214. handleLeftClick() {},
  215. handleRightClick() {},
  216. headerNavInit(info) {
  217. this.headerNavHeight = info.height
  218. },
  219. ...mapActions('cart', ['removeFromCart']),
  220. // 页面初始化
  221. initPage() {
  222. this.resetData()
  223. this.fetchCartInfo()
  224. },
  225. // 初始化数据
  226. resetData() {
  227. const data = resetData()
  228. for (let key in data) {
  229. this[key] = data[key]
  230. }
  231. },
  232. // 确认清空失效商品
  233. async onConfirmRemoveExp(e) {
  234. // removeProductFromCart
  235. if (!e.index) return
  236. try {
  237. await this.removeFromCart(this.expiredProducts.map(product => product.cartId))
  238. this.initPage()
  239. } catch (e) {
  240. console.log('删除失效商品失败')
  241. } finally {
  242. this.removeExpModal = false
  243. }
  244. },
  245. // 提交订单
  246. onSumit() {
  247. // return uni.navigateTo({
  248. // url:'/pages/views/goods/Update-Maintenance'
  249. // })
  250. if (this.checkedProductList.length <= 0) {
  251. return this.$toast.error('请选择商品')
  252. }
  253. const params = {}
  254. params.allPrice = this.allPrice
  255. params.allCount = this.checkedProductList.length
  256. params.cartIds = this.checkedProductList.map(product => product.cartId)
  257. params.productIds = this.checkedProductList.map(product => product.productId)
  258. params.productCount = ''
  259. uni.setStorageSync('COMMIT_CART_INFO', params)
  260. this.$router.navigateTo('order/order-create')
  261. },
  262. // 优惠券列表类型更换
  263. onCouponChange(index) {
  264. this.currentTab = index
  265. },
  266. // 优惠券点击事件
  267. onCouponClick(coupon) {
  268. if (coupon.controlType === 'receive') {
  269. this.fetchCouponList()
  270. } else {
  271. this.visiable = false
  272. }
  273. },
  274. // 设置couponTab数据
  275. makeCouponTabs() {
  276. return [
  277. {
  278. name: '已领取优惠券',
  279. num: this.receiveCouponList.length,
  280. isDot: false,
  281. disabled: false
  282. },
  283. {
  284. name: '可领取优惠券',
  285. num: this.receiveCouponList.ableCouponList,
  286. isDot: false,
  287. disabled: false
  288. }
  289. ]
  290. },
  291. // 获取购物车商品可用优惠券
  292. async fetchCouponList() {
  293. const productIds = []
  294. this.shopList.forEach(shop => shop.productList.forEach(product => productIds.push(product.productId)))
  295. try {
  296. const res = await fetchCouponListByProductIds({ userId: this.userId, productIds: productIds.join(',') })
  297. this.receiveCouponList = initFormatCouponList(res.data.receiveCouponList, 'search', true)
  298. this.ableCouponList = initFormatCouponList(res.data.ableCouponList, 'receive', true)
  299. this.couponTabs = this.makeCouponTabs()
  300. } catch (e) {
  301. console.log(e)
  302. }
  303. },
  304. // 获取购物车信息
  305. async fetchCartInfo() {
  306. try {
  307. const res = await fetchCartInfo({ userId: this.userId })
  308. this.expiredProducts = res.data.products
  309. this.shopList = res.data.shopList
  310. // 获取优惠券列表
  311. if (this.shopList.length > 0) {
  312. // 获取优惠券列表
  313. this.fetchCouponList()
  314. }
  315. return res
  316. } catch (e) {
  317. return e
  318. } finally {
  319. this.isRequest = false
  320. }
  321. },
  322. // 确认删除
  323. async onConfirmRemove({ index }) {
  324. if (!index) return (this.removeModal = false)
  325. const ids = this.getCheckedProductIds()
  326. const cartIds = this.shopList.reduce((prev, item) => {
  327. item.productList.forEach(prod => {
  328. if (ids.includes(prod.productId.toString())) {
  329. prev.push(prod.cartId)
  330. }
  331. })
  332. return prev
  333. }, [])
  334. try {
  335. await this.removeFromCart(cartIds)
  336. this.fetchCartInfo()
  337. } catch (e) {
  338. console.log(e)
  339. } finally {
  340. this.isDeleted = false
  341. this.removeModal = false
  342. }
  343. },
  344. // 选中/取消全部商品
  345. onCheckedAll() {
  346. if (this.isChekedAll) {
  347. this.$refs.supplierArea.forEach(item => item.unSelectAll())
  348. } else {
  349. this.$refs.supplierArea.forEach(item => item.selectAll())
  350. }
  351. },
  352. // 选中商品变化
  353. checkedProductChange(e) {
  354. this.changeRef = e
  355. this.handleCartManager()
  356. },
  357. // 修改商品checked属性
  358. updatePorudctChecked(row) {
  359. const shopInfo = this.shopList.find(item => row.shopId === item.shopId)
  360. if (!shopInfo) return
  361. shopInfo.productList.forEach(item => {
  362. if (row.checkedList.includes(item.skuId.toString())) {
  363. this.$set(item, 'checked', true)
  364. } else {
  365. this.$set(item, 'checked', false)
  366. }
  367. })
  368. },
  369. // 购物车数据处理器
  370. handleCartManager() {
  371. if (!this.changeRef) return
  372. this.updatePorudctChecked(this.changeRef)
  373. // 保存选中商品id与供应商id的k-v关系
  374. this.$set(this.checkedShopMap, this.changeRef.shopId, this.changeRef.checkedList)
  375. this.isChekedAll = this.validateIsChekedAll()
  376. // 获取已选商品列表
  377. this.checkedProductList = totalAllCheckedProduct(this.shopList)
  378. // 计算商品总价
  379. this.allPrice = computeTotalPrice(this.checkedProductList)
  380. const couponUtils = new CouponUtils(this.receiveCouponList, this.checkedProductList)
  381. this.currentCoupon = couponUtils.bestCoupon
  382. this.nextCoupon = couponUtils.secondCoupon
  383. this.couponTipText = makeCouponUseTip(this.currentCoupon, this.nextCoupon, this.allPrice)
  384. },
  385. // 商品数量变化
  386. async onCountChange(e) {
  387. await this.$store.dispatch('cart/updateProductCount', {
  388. cartId: e.cartId,
  389. productCount: e.count
  390. })
  391. await this.fetchCartInfo()
  392. this.handleCartManager()
  393. },
  394. // 商品规格修改
  395. async onUnitChange(e) {
  396. this.cartId = e.cartId
  397. uni.showLoading({ title: '加载中...' })
  398. this.productInfo = await fetchProductDetail(e.productId)
  399. this.goodsCouponList = await fetchCouponListByProduct(e.productId)
  400. setTimeout(() => {
  401. this.goodsBuyPopup = true
  402. uni.hideLoading()
  403. }, 1000)
  404. },
  405. // 活动价弹窗
  406. handleShowActivity(current) {
  407. if (!current.ladderPriceList) return
  408. this.ladderPriceList = current.ladderPriceList
  409. this.$refs.receiveBuyPopup.open()
  410. },
  411. // 保存商品规格
  412. async onSaveNewUnit(e) {
  413. try {
  414. console.log(e)
  415. await this.$store.dispatch('cart/updateProductUnit', {
  416. newSkuId: e.sku.skuId,
  417. cartId: this.cartId,
  418. productCount: e.count
  419. })
  420. this.$toast.success('规格修改成功')
  421. this.initPage()
  422. } catch (e) {
  423. console.log(e)
  424. this.$toast.error('修改规格失败')
  425. }
  426. },
  427. // 验证是否全选
  428. validateIsChekedAll() {
  429. return this.shopList.every(item => {
  430. const checkedList = this.checkedShopMap[item.shopId] || []
  431. return item.productList.filter(item => item.stock !== 0 || this.isDeleted).length === checkedList.length
  432. })
  433. },
  434. // 获取已选商品id列表
  435. getCheckedProductIds() {
  436. const list = []
  437. for (let key in this.checkedShopMap) {
  438. list.push(...this.checkedShopMap[key])
  439. }
  440. return list
  441. }
  442. }
  443. }
  444. </script>
  445. <style scoped lang="scss">
  446. .goods-total {
  447. @extend .cm-flex-between;
  448. background: #f7f7f7;
  449. padding: 0 24rpx;
  450. height: 80rpx;
  451. .total {
  452. font-size: 30rpx;
  453. color: #333;
  454. }
  455. }
  456. .receive-coupon {
  457. @extend .cm-flex-between;
  458. background: #fff;
  459. padding: 0 24rpx;
  460. height: 80rpx;
  461. .tip-text {
  462. font-size: 26rpx;
  463. color: #ff457b;
  464. white-space: nowrap;
  465. overflow: hidden;
  466. text-overflow: ellipsis;
  467. }
  468. }
  469. .supplier-area {
  470. margin-bottom: 24rpx;
  471. }
  472. .remove-view {
  473. position: relative;
  474. width: 100vw;
  475. height: 100vh;
  476. box-sizing: border-box;
  477. .shop-list {
  478. height: 100%;
  479. background-color: #f7f7f7;
  480. box-sizing: border-box;
  481. }
  482. .icon {
  483. margin-right: 6rpx;
  484. text {
  485. display: inline-block;
  486. font-size: 30rpx;
  487. margin-left: 4rpx;
  488. color: #333;
  489. }
  490. }
  491. .navbar {
  492. position: absolute;
  493. width: 100%;
  494. bottom: 0;
  495. left: 0;
  496. z-index: 98;
  497. min-height: 100rpx;
  498. box-sizing: border-box;
  499. padding: 0 24rpx;
  500. background: #fff;
  501. border-top: 1rpx solid #eee;
  502. .content {
  503. height: 100rpx;
  504. @extend .cm-flex-between;
  505. .control {
  506. width: 444rpx;
  507. @extend .cm-flex-between;
  508. }
  509. }
  510. }
  511. }
  512. </style>