cart.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <template>
  2. <view :style="themeColor()">
  3. <view class="bg-page h-screen overflow-hidden flex flex-col" v-if="!loading">
  4. <view v-if="!info" class="empty-page">
  5. <image class="img" :src="img('static/resource/images/system/login.png')" mode="aspectFit" />
  6. <view class="desc">暂未登录</view>
  7. <button shape="circle" plain="true" class="btn" @click="toLogin">去登录</button>
  8. </view>
  9. <view v-else-if="!cartList.length" class="empty-page">
  10. <image class="img" :src="img('addon/mall/cart-empty.png')" model="aspectFit" />
  11. <view class="desc">赶紧去逛逛, 购买心仪的商品吧</view>
  12. <button shape="circle" plain="true" class="btn" @click="redirect({ url: '/addon/mall/pages/goods/list' })">去逛逛</button>
  13. </view>
  14. <block v-else>
  15. <view class="flex-1 h-0">
  16. <scroll-view class="scroll-height box-border" :scroll-y="true">
  17. <view class="py-[var(--top-m)] sidebar-marign">
  18. <view class="box-border" v-if="cartList.length">
  19. <view class="bg-[#fff] flex px-[var(--pad-sidebar-m)] justify-between items-center h-[80rpx] box-border font-400 text-[24rpx] mb-[24rpx] leading-[30rpx] rounded-[var(--rounded-big)]">
  20. <view class="flex items-center text-[24rpx] text-[#333]">
  21. <text>共<text class="text-[28rpx] mx-[2rpx] text-[var(--price-text-color)]">{{checkedNum }}</text>件商品</text>
  22. </view>
  23. <text @click="isEdit = !isEdit" class="text-[var(--text-color-light6)] text-[24rpx]">{{ isEdit ? '完成' : '管理' }}</text>
  24. </view>
  25. <view v-for="(item, index) in cartList" :key="index" class="bg-[#fff] mb-[20rpx] rounded-[var(--rounded-big)] pb-[10rpx]">
  26. <view class="flex px-[var(--pad-sidebar-m)] py-[var(--pad-top-m)] border-0 border-[#f0f0f0] border-solid border-b-[2rpx] items-baseline">
  27. <text v-if="item.disabled" class="iconfont text-color text-[34rpx] mr-[20rpx] w-[34rpx] h-[34rpx] rounded-[17rpx]" :class="{ 'iconxuanze1 ':item.checked,'bg-[#F5F5F5]':!item.checked}"></text>
  28. <text v-else class="iconfont text-color text-[34rpx] mr-[20rpx] w-[34rpx] h-[34rpx] rounded-[17rpx]" :class="{ 'iconxuanze1 ':item.checked,'bg-[#F5F5F5]':!item.checked}" @click="isClickSite(item)"></text>
  29. <text class="iconfont iconVector-25 text-[32rpx]"></text>
  30. <text class="ml-[12rpx] text-[28rpx] flex-1 truncate">{{item.site_name }}</text>
  31. </view>
  32. <u-swipe-action ref="swipeActiveRef">
  33. <block v-for="(subItem, subIndex) in item.goods_list" :key="subIndex">
  34. <view v-if="subItem.goodsSku" class="py-[20rpx] overflow-hidden w-full">
  35. <u-swipe-action-item :options="cartOptions" @click="swipeClick(index,subIndex,subItem)">
  36. <view class="flex px-[var(--pad-sidebar-m)]">
  37. <text class="self-center w-[34rpx] h-[34rpx] rounded-[17rpx] mr-[32rpx] bg-[#F5F5F5] overflow-hidden flex-shrink-0" v-if="subItem.disabled"></text>
  38. <text v-else class="self-center iconfont text-color text-[34rpx] mr-[32rpx] w-[34rpx] h-[34rpx] rounded-[17rpx] overflow-hidden flex-shrink-0" :class="{ 'iconxuanze1':subItem.checked,'bg-[#F5F5F5]':!subItem.checked}" @click="changeItem(item,subItem)">
  39. </text>
  40. <view class="relative w-[200rpx] h-[200rpx] rounded-[var(--goods-rounded-big)] overflow-hidden">
  41. <u--image class="rounded-[var(--goods-rounded-big)] overflow-hidden" width="200rpx" height="200rpx" @click="toDetail(subItem)" :src="img(subItem.goodsSku.sku_image_thumb_mid||'')" mode="aspectFill">
  42. <template #error>
  43. <image class="w-[200rpx] h-[200rpx] rounded-[var(--goods-rounded-big)] overflow-hidden" :src="img('static/resource/images/diy/shop_default.jpg')" mode="aspectFill"></image>
  44. </template>
  45. </u--image>
  46. <view v-if="subItem.disabled" class="absolute top-0 right-0 left-0 bottom-0 flex justify-center items-center bg-black bg-opacity-50 text-white rounded-[var(--goods-rounded-big)]">已下架</view>
  47. </view>
  48. <view class="flex flex-1 flex-wrap ml-[20rpx]">
  49. <view class="w-[100%] flex flex-col items-baseline">
  50. <view class="text-[#333] text-[28rpx] max-h-[80rpx] leading-[40rpx] multi-hidden font-400">
  51. {{ subItem.goods.goods_name }}
  52. </view>
  53. <view class="box-border max-w-[376rpx] mt-[10rpx] px-[14rpx] h-[36rpx] leading-[36rpx] truncate text-[var(--text-color-light6)] bg-[#F5F5F5] text-[22rpx] rounded-[20rpx]" v-if="subItem.goodsSku && subItem.goodsSku.sku_spec_format">
  54. {{ subItem.goodsSku.sku_spec_format }}
  55. </view>
  56. </view>
  57. <view class="flex justify-between items-end self-end w-[100%]">
  58. <view class="flex items-end text-[var(--price-text-color)] leading-[40rpx] price-font">
  59. <view class="text-[var(--price-text-color)] price-font">
  60. <text class="text-[24rpx] font-500">¥</text>
  61. <text class="text-[40rpx] font-500">{{ parseFloat(goodsPrice(subItem)).toFixed(2).split('.')[0] }}</text>
  62. <text class="text-[24rpx] font-500">.{{ parseFloat(goodsPrice(subItem)).toFixed(2).split('.')[1] }}</text>
  63. <image class="h-[24rpx] ml-[6rpx]" v-if="priceType(subItem) == 'member_price'" :src="img('addon/mall/VIP.png')" mode="heightFix" />
  64. <image class="h-[24rpx] ml-[6rpx]" v-if="priceType(subItem) == 'discount_price'" :src="img('addon/mall/discount.png')" mode="heightFix" />
  65. </view>
  66. </view>
  67. <u-number-box v-model="subItem.num" :min="numLimit(subItem).min"
  68. :max="numLimit(subItem).max" integer :step="1" input-width="68rpx"
  69. input-height="52rpx" button-size="52rpx" disabledInput :disabled="subItem.disabled" :longPress="false"
  70. @change="numChange($event, subItem)">
  71. <template #minus>
  72. <text
  73. :class="{ 'text-[var(--text-color-light9)]': subItem.num === numLimit(subItem).min, 'text-[#303133]': subItem.num !== numLimit(subItem).min }"
  74. class="text-[24rpx] font-500 nc-iconfont nc-icon-jianV6xx"></text>
  75. </template>
  76. <template #input>
  77. <text class="px-[6rpx] box-border overflow-hidden text-[#303133] text-[24rpx] mx-[10rpx] w-[72rpx] h-[44rpx] bg-[var(--temp-bg)] leading-[44rpx] text-center rounded-[6rpx]">{{ subItem.num}}
  78. </text>
  79. </template>
  80. <template #plus>
  81. <text
  82. :class="{ 'text-[var(--text-color-light9)]': subItem.num === numLimit(subItem).max, ' text-[#303133]': subItem.num !== numLimit(subItem).max }"
  83. class="text-[24rpx] font-500 nc-iconfont nc-icon-jiahaoV6xx"></text>
  84. </template>
  85. </u-number-box>
  86. </view>
  87. </view>
  88. </view>
  89. </u-swipe-action-item>
  90. </view>
  91. </block>
  92. </u-swipe-action>
  93. </view>
  94. </view>
  95. </view>
  96. </scroll-view>
  97. </view>
  98. </block>
  99. </view>
  100. <!-- #ifdef H5 -->
  101. <view v-if="cartList.length" class="flex h-[96rpx] items-center bg-[#fff] fixed left-0 right-0 bottom-[50px] pl-[30rpx] pr-[20rpx] box-solid mb-ios justify-between">
  102. <view class="flex items-center" @click="allChange">
  103. <text class="self-center iconfont text-color text-[34rpx] mr-[10rpx] w-[34rpx] h-[34rpx] rounded-[17rpx] overflow-hidden flex-shrink-0" :class=" isSelectAll ? 'iconxuanze1' : 'bg-[#F5F5F5]'"></text>
  104. <text class="font-400 text-[#303133] text-[26rpx]">全选</text>
  105. </view>
  106. <view class="flex items-center">
  107. <view class="flex-1 flex items-center justify-between" v-if="!isEdit">
  108. <view class="flex items-center mr-[20rpx] text-[var(--price-text-color)] leading-[44rpx]">
  109. <view class="font-400 text-[#303133] text-[28rpx]">合计:</view>
  110. <text class="text-[var(--price-text-color)] price-font text-[32rpx] font-bold">
  111. ¥{{parseFloat(total) }}
  112. </text>
  113. </view>
  114. <button class="w-[188rpx] h-[70rpx] font-500 text-[26rpx] leading-[70rpx] !text-[#fff] m-0 rounded-full primary-btn-bg remove-border" @click="settlement">结算</button>
  115. </view>
  116. <view class="flex-1 flex items-center justify-end" v-else>
  117. <button class="w-[188rpx] h-[70rpx] font-500 text-[26rpx] leading-[70rpx] !text-[#fff] m-0 rounded-full primary-btn-bg remove-border" @click="deleteCartFn">删除</button>
  118. </view>
  119. </view>
  120. </view>
  121. <!-- #endif -->
  122. <!-- #ifndef H5 -->
  123. <view v-if="cartList.length" class="pl-[30rpx] pr-[20rpx] flex h-[96rpx] items-center bg-[#fff] fixed left-0 right-0 bottom-[100rpx] box-solid mb-ios justify-between">
  124. <view class="flex items-center" @click="allChange">
  125. <text class="self-center iconfont text-color text-[30rpx] mr-[20rpx] w-[34rpx] h-[34rpx] rounded-[17rpx] overflow-hidden flex-shrink-0" :class=" isSelectAll ? 'iconxuanze1' : 'bg-[#F5F5F5]'"></text>
  126. <text class="font-400 text-[#303133] text-[26rpx]">全选</text>
  127. </view>
  128. <view class="flex items-center">
  129. <view class="flex-1 flex items-center justify-between" v-if="!isEdit">
  130. <view class="flex items-center mr-[67rpx] text-[var(--price-text-color)] leading-[44rpx]">
  131. <view class="font-400 text-[#303133] text-[28rpx]">合计:</view>
  132. <text class="text-[var(--price-text-color)] price-font text-[32rpx] font-bold">
  133. ¥{{ parseFloat(total) }}
  134. </text>
  135. </view>
  136. <button class="w-[188rpx] h-[70rpx] font-500 text-[26rpx] leading-[72rpx] !text-[#fff] m-0 rounded-full primary-btn-bg remove-border" @click="settlement">结算</button>
  137. </view>
  138. <view class="flex-1 flex items-center justify-end" v-else>
  139. <button class="w-[188rpx] h-[70rpx] font-500 text-[26rpx] leading-[72rpx] !text-[#fff] m-0 rounded-full primary-btn-bg remove-border" @click="deleteCartFn">删除</button>
  140. </view>
  141. </view>
  142. </view>
  143. <!-- #endif -->
  144. <u-loading-page bg-color="rgb(248,248,248)" :loading="loading" loadingText=""></u-loading-page>
  145. <tabbar />
  146. <!-- 强制绑定手机号 -->
  147. <bind-mobile ref="bindMobileRef" />
  148. </view>
  149. </template>
  150. <script setup lang="ts">
  151. import { ref, computed, watch,toRaw,nextTick } from 'vue'
  152. import useMemberStore from '@/stores/member'
  153. import { useLogin } from '@/hooks/useLogin'
  154. import { onShow } from '@dcloudio/uni-app'
  155. import { img, redirect, getToken } from '@/utils/common'
  156. import useCartStore from '@/addon/mall/stores/cart'
  157. import { getCartGoodsList } from '@/addon/mall/api/cart'
  158. import bindMobile from '@/components/bind-mobile/bind-mobile.vue';
  159. import {t} from "@/locale";
  160. const memberStore = useMemberStore()
  161. const info = computed(() => memberStore.info)
  162. const loading = ref(true)
  163. const optionLoading = ref(false)
  164. const total = ref('0.00')
  165. const cartList = ref<object[]>([])
  166. const isEdit = ref(false)
  167. const cartStore = useCartStore();
  168. const getCartGoodsListFn = () => {
  169. getCartGoodsList({}).then(({data}) => {
  170. cartList.value = []
  171. loading.value = false
  172. cartList.value = Object.values(data).map(item => {
  173. item.checked = false
  174. item.goods_list.map(subItem =>{
  175. if (subItem.goods.status == 1 && subItem.goods.delete_time == 0) {
  176. subItem.checked = false
  177. } else {
  178. subItem.disabled = true
  179. }
  180. return subItem
  181. })
  182. item.disabled = item.goods_list.every(subItem => subItem.disabled)
  183. return item
  184. })
  185. isSelectAll.value = false
  186. allChange()
  187. }).catch((err)=>{
  188. if(err.code==401){
  189. cartList.value = []
  190. loading.value = false
  191. }
  192. })
  193. }
  194. onShow(() => {
  195. getCartGoodsListFn()
  196. cartStore.getList();
  197. })
  198. // 总共数量
  199. // 选择数量
  200. const checkedNum = computed(() => {
  201. let num = 0
  202. cartList.value.forEach(item => {
  203. item.goods_list.forEach(subItem=>{
  204. if(subItem.checked){
  205. num += subItem.num
  206. }
  207. })
  208. })
  209. return num
  210. })
  211. // 计算总价
  212. watch(() => cartList.value, () => {
  213. let value = 0
  214. cartList.value.forEach((item,index) => {
  215. item.goods_list.forEach(subItem => {
  216. if (subItem.checked && subItem.goodsSku){
  217. let price: any = 0;
  218. if (subItem.goods.is_discount) {
  219. price = subItem.goodsSku.sale_price // 折扣价
  220. } else if (subItem.goods.member_discount && getToken()) {
  221. price = subItem.goodsSku.member_price // 会员价
  222. } else {
  223. price = subItem.goodsSku.price
  224. }
  225. value += parseFloat(price) * subItem.num
  226. }
  227. })
  228. })
  229. total.value = value.toFixed(2)
  230. }, { deep: true })
  231. const toLogin = () => {
  232. useLogin().setLoginBack({ url: '/addon/mall/pages/goods/cart' })
  233. }
  234. const toDetail = (data: any) => {
  235. redirect({ url: '/addon/mall/pages/goods/detail', param: { goods_id: data.goods_id } })
  236. }
  237. const numChange = (event, subItem) => {
  238. uni.$u.debounce((event) => {
  239. const data = subItem
  240. cartStore.increase({
  241. id: data.id,
  242. goods_id: data.goods_id,
  243. sku_id: data.sku_id,
  244. stock: data.goodsSku.stock,
  245. sale_price: data.goodsSku.sale_price,
  246. num: data.num,
  247. site_id: data.site_id
  248. }, 0);
  249. }, 500)
  250. }
  251. const numLimit = (data) => {
  252. return {
  253. min: 1,
  254. max: data.goodsSku.stock || data.num
  255. }
  256. }
  257. const cartOptions = ref([
  258. {
  259. text: t('delete'),
  260. style: {
  261. backgroundColor: '#F56C6C'
  262. }
  263. }
  264. ])
  265. const swipeActiveRef = ref(null)
  266. const swipeClick = (index:any,subIndex:any,subItem:any) => {
  267. if (optionLoading.value) return
  268. optionLoading.value = true
  269. cartStore.delete(subItem.id, () => {
  270. cartList.value[index].goods_list.splice(subIndex, 1)
  271. if(cartList.value[index].goods_list.length == 0){
  272. cartList.value.splice(index, 1)
  273. }
  274. nextTick(()=>{
  275. if(swipeActiveRef.value){
  276. swipeActiveRef.value[index].closeOther()
  277. }
  278. })
  279. optionLoading.value = false
  280. })
  281. }
  282. //判断是否全选
  283. const isSelectAll = ref(false)
  284. const isallclick = ()=>{
  285. const isActve = cartList.value.every((item) => {
  286. return item.checked
  287. })
  288. if (isActve) { isSelectAll.value = true } else { isSelectAll.value= false }
  289. }
  290. // 选择单个商品
  291. const changeItem = (data,value) =>{
  292. value.checked = !value.checked
  293. let arr =[];
  294. data.goods_list.forEach((item) => {
  295. if(item.checked != undefined){
  296. arr.push(item)
  297. }
  298. })
  299. const isActve = arr.every((item) => {
  300. return item.checked
  301. })
  302. if (isActve) { data.checked = true } else { data.checked = false }
  303. isallclick()
  304. }
  305. // 选择店铺
  306. const isClickSite = (data) =>{
  307. data.checked = !data.checked
  308. if (data.checked) {
  309. data.goods_list.forEach(item => {
  310. if(!item.disabled){
  311. item.checked = true
  312. }
  313. })
  314. } else {
  315. data.goods_list.forEach(item => {
  316. if(!item.disabled){
  317. item.checked = false
  318. }
  319. });
  320. }
  321. isallclick()
  322. }
  323. // 全选
  324. const allChange = () =>{
  325. isSelectAll.value = !isSelectAll.value
  326. if (isSelectAll.value) {
  327. cartList.value.forEach(item => {
  328. item.checked = item.goods_list.some((item) => {
  329. return !item.disabled
  330. })
  331. item.goods_list.forEach(subItem => {
  332. if(!subItem.disabled){
  333. subItem.checked = true
  334. }
  335. })
  336. })
  337. } else {
  338. cartList.value.forEach(item => {
  339. item.checked = false
  340. item.goods_list.forEach(subItem => {
  341. if(!subItem.disabled){
  342. subItem.checked = false
  343. }
  344. })
  345. })
  346. }
  347. }
  348. //强制绑定手机号
  349. const bindMobileRef = ref(null)
  350. /**
  351. * 结算
  352. */
  353. const settlement = () => {
  354. if(uni.getStorageSync('isbindmobile')){
  355. bindMobileRef.value.open()
  356. return false
  357. }
  358. if (!checkedNum.value) {
  359. uni.showToast({ title: '还没有选择商品', icon: 'none' })
  360. return
  361. }
  362. let body = {}
  363. cartList.value.forEach(item => {
  364. body[item.site_id] = {}
  365. })
  366. for(let key in body){
  367. cartList.value.forEach(item => {
  368. if (item.site_id == key) {
  369. body[key].cart_ids = []
  370. item.goods_list.forEach(subItem => {
  371. if (subItem.checked) {
  372. body[key].cart_ids.push(subItem.id)
  373. }
  374. })
  375. }
  376. })
  377. if (!body[key].cart_ids.length){
  378. delete body[key]
  379. }
  380. }
  381. uni.setStorage({
  382. key: 'orderCreateData',
  383. data: {
  384. body: body
  385. },
  386. success() {
  387. redirect({ url: '/addon/mall/pages/order/payment' })
  388. }
  389. })
  390. }
  391. /**
  392. * 删除
  393. */
  394. const deleteCartFn = () => {
  395. if (!checkedNum.value) {
  396. uni.showToast({ title: '还没有选择商品', icon: 'none' })
  397. return
  398. }
  399. if (optionLoading.value) return
  400. optionLoading.value = true
  401. const ids:any = []
  402. if(isSelectAll.value){
  403. cartList.value.forEach(item => {
  404. item.goods_list.forEach(subItem => {
  405. ids.push(subItem.id)
  406. })
  407. })
  408. }else{
  409. cartList.value.forEach(item => {
  410. item.goods_list.forEach(subItem => {
  411. if (subItem.checked) {
  412. ids.push(subItem.id)
  413. }
  414. })
  415. })
  416. }
  417. cartStore.delete(ids, () => {
  418. getCartGoodsListFn()
  419. optionLoading.value = false
  420. })
  421. }
  422. // 价格类型
  423. let priceType = (data:any) =>{
  424. let type = "";
  425. if(data.goods.is_discount){
  426. type = 'discount_price'// 折扣
  427. }else if(data.goods.member_discount && getToken()){
  428. type = 'member_price' // 会员价
  429. }else{
  430. type = ""
  431. }
  432. return type;
  433. }
  434. // 商品价格
  435. let goodsPrice = (data:any) =>{
  436. let price = "0.00";
  437. if(data.goods.is_discount){
  438. price = data.goodsSku.sale_price?data.goodsSku.sale_price:data.goodsSku.price // 折扣价
  439. }else if(data.goods.member_discount && getToken()){
  440. price = data.goodsSku.member_price?data.goodsSku.member_price:data.goodsSku.price // 会员价
  441. }else{
  442. price = data.goodsSku.price
  443. }
  444. return price;
  445. }
  446. </script>
  447. <style lang="scss" scoped>
  448. .remove-border {
  449. &::after {
  450. border: none;
  451. }
  452. }
  453. :deep(uni-page) {
  454. background: var(--page-bg-color);
  455. }
  456. uni-page-body {
  457. height: 100%;
  458. }
  459. .text-color {
  460. color: var(--primary-color);
  461. }
  462. .bg-color {
  463. background-color: var(--primary-color);
  464. }
  465. :deep(.tab-bar-placeholder) {
  466. display: none !important;
  467. }
  468. :deep(.u-tabbar__placeholder) {
  469. display: none !important;
  470. }
  471. /* #ifdef H5 */
  472. .scroll-height {
  473. height: calc(100vh - 100rpx - 50px - constant(safe-area-inset-bottom));
  474. height: calc(100vh - 100rpx - 50px - env(safe-area-inset-bottom));
  475. }
  476. /* #endif */
  477. /* #ifndef H5 */
  478. .scroll-height {
  479. height: calc(100vh - 200rpx - constant(safe-area-inset-bottom));
  480. height: calc(100vh - 200rpx - env(safe-area-inset-bottom));
  481. }
  482. /* #endif */
  483. .text-ellipsis{
  484. display: -webkit-box;
  485. -webkit-box-orient: vertical;
  486. -webkit-line-clamp: 2;
  487. overflow: hidden;
  488. }
  489. :deep(.u-swipe-action-item__right__button__wrapper){
  490. padding:0 10rpx !important;
  491. }
  492. :deep(.u-swipe-action-item__right__button__wrapper__text){
  493. font-size:24rpx !important;
  494. }
  495. </style>