| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- <template>
- <view class="cart-page">
- <view v-if="!loggedIn" class="cart-guest">
- <u-empty mode="car" text="登录后查看购物车" icon-size="100" />
- <button class="cart-btn-primary" @click="goLogin">去登录</button>
- </view>
- <template v-else>
- <view v-if="loading && !groups.length" class="cart-loading">
- <u-loading-icon mode="circle" />
- </view>
- <view v-else-if="loadFailed && !groups.length" class="cart-error">
- <u-empty mode="list" text="加载失败" icon-size="80" />
- <text class="cart-retry" @click="loadCart">点击重试</text>
- </view>
- <view v-else-if="!groups.length" class="cart-empty">
- <u-empty mode="car" :text="emptyText" icon-size="100" />
- <button class="cart-btn-primary" @click="goHome">去逛逛</button>
- </view>
- <template v-else>
- <view v-if="showInvalidBar" class="cart-toolbar">
- <text class="cart-toolbar__btn" @click="onCleanInvalid">清理失效商品</text>
- <text v-if="hasCheckedAny" class="cart-toolbar__btn" @click="onDeleteChecked">删除选中</text>
- </view>
- <scroll-view class="cart-scroll" scroll-y :style="{ height: scrollHeight }">
- <cart-shop-group
- v-for="group in groups"
- :key="group.shopId"
- :group="group"
- @shop-check="onShopCheck"
- @shop-click="onShopClick"
- @check-change="onItemCheck"
- @quantity-change="onQuantityChange"
- @delete="onDeleteItem"
- @goods-click="onGoodsClick"
- />
- </scroll-view>
- <view class="cart-footer">
- <view class="cart-footer__check" @click="onToggleSelectAll">
- <view class="check-icon" :class="{ 'check-icon--on': selectAllChecked }">
- <u-icon v-if="selectAllChecked" name="checkbox-mark" color="#fff" size="14" />
- </view>
- <text class="cart-footer__label">全选</text>
- </view>
- <view class="cart-footer__sum">
- <text class="cart-footer__sum-label">合计:</text>
- <text class="cart-footer__sum-price">¥{{ checkedSummary.checkedAmountText }}</text>
- </view>
- <button
- class="cart-footer__btn"
- :disabled="!checkedSummary.checkedCount"
- @click="onCheckout"
- >
- 去结算({{ checkedSummary.checkedCount }})
- </button>
- </view>
- </template>
- </template>
- </view>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { getToken } from '@/utils/auth'
- import {
- getCartList,
- updateCartChecked,
- updateCartQuantity,
- removeCartItem,
- removeCartItems,
- cleanInvalidCart,
- prepareCartCheckout
- } from '@/api/cart'
- import {
- mapCartList,
- hasInvalidCartItems,
- getCheckedShopIds,
- getCheckedPurchasableIds,
- getAllPurchasableItems
- } from '@/utils/cartDisplay'
- import { goCartCheckout } from '@/utils/checkoutNav'
- import { CART_EMPTY_TEXT, CART_MSG_CROSS_SHOP } from '@/constants/cart'
- import { goGoodsDetail } from '@/utils/goodsDetail'
- import { goShopHome } from '@/utils/shopNav'
- import { PAGE_LOGIN, PAGE_HOME } from '@/utils/pageRoute'
- import CartShopGroup from '@/components/cart/CartShopGroup.vue'
- const loggedIn = ref(false)
- const loading = ref(false)
- const loadFailed = ref(false)
- const groups = ref([])
- const checkedSummary = ref({
- checkedCount: 0,
- checkedQuantity: 0,
- checkedAmountText: '0'
- })
- const scrollHeight = ref('500px')
- const emptyText = CART_EMPTY_TEXT
- const actionLock = ref(false)
- const showInvalidBar = computed(() => hasInvalidCartItems(groups.value))
- const allPurchasable = computed(() => getAllPurchasableItems(groups.value))
- const selectAllChecked = computed(() => {
- const list = allPurchasable.value
- return list.length > 0 && list.every((item) => item.checked)
- })
- const hasCheckedAny = computed(() =>
- groups.value.some((g) => (g.items || []).some((item) => item.checked))
- )
- function calcScrollHeight() {
- try {
- const sys = uni.getSystemInfoSync()
- const h = sys.windowHeight || 600
- scrollHeight.value = `${h - 120}px`
- } catch (e) {
- scrollHeight.value = '500px'
- }
- }
- function applyCartData(data) {
- const mapped = mapCartList(data)
- groups.value = mapped.groups
- checkedSummary.value = mapped.checkedSummary
- }
- async function loadCart() {
- loading.value = true
- loadFailed.value = false
- try {
- const res = await getCartList()
- applyCartData(res.data)
- } catch (e) {
- loadFailed.value = true
- if (!groups.value.length) {
- groups.value = []
- }
- } finally {
- loading.value = false
- }
- }
- function buildCheckedPayload(items) {
- return items.map((item) => ({
- cartItemId: item.cartItemId,
- checked: !!item.checked
- }))
- }
- async function syncChecked(items) {
- if (!items.length || actionLock.value) return
- actionLock.value = true
- try {
- await updateCartChecked(buildCheckedPayload(items))
- await loadCart()
- } catch (e) {
- await loadCart()
- } finally {
- actionLock.value = false
- }
- }
- function onItemCheck(item, checked) {
- syncChecked([{ ...item, checked }])
- }
- function onShopCheck(group, checked) {
- const items = (group.items || [])
- .filter((row) => row.purchasable)
- .map((row) => ({ ...row, checked }))
- if (!items.length) return
- syncChecked(items)
- }
- function onToggleSelectAll() {
- const next = !selectAllChecked.value
- const items = allPurchasable.value.map((row) => ({ ...row, checked: next }))
- if (!items.length) return
- syncChecked(items)
- }
- async function onQuantityChange(item, quantity) {
- if (actionLock.value) return
- actionLock.value = true
- try {
- await updateCartQuantity(item.cartItemId, quantity)
- await loadCart()
- } catch (e) {
- await loadCart()
- } finally {
- actionLock.value = false
- }
- }
- function confirmRemove(onConfirm) {
- uni.showModal({
- title: '提示',
- content: '确定移出该商品吗?',
- success: (res) => {
- if (res.confirm) onConfirm()
- }
- })
- }
- async function onDeleteItem(item) {
- confirmRemove(async () => {
- try {
- await removeCartItem(item.cartItemId)
- uni.showToast({ title: '已移出', icon: 'none' })
- await loadCart()
- } catch (e) {
- // request 已提示
- }
- })
- }
- function onDeleteChecked() {
- const ids = []
- groups.value.forEach((g) => {
- ;(g.items || []).forEach((item) => {
- if (item.checked) ids.push(item.cartItemId)
- })
- })
- if (!ids.length) {
- uni.showToast({ title: '请先勾选商品', icon: 'none' })
- return
- }
- uni.showModal({
- title: '提示',
- content: `确定移出选中的 ${ids.length} 件商品吗?`,
- success: async (res) => {
- if (!res.confirm) return
- try {
- await removeCartItems(ids)
- uni.showToast({ title: '已移出', icon: 'none' })
- await loadCart()
- } catch (e) {
- // request 已提示
- }
- }
- })
- }
- async function onCleanInvalid() {
- uni.showModal({
- title: '提示',
- content: '确定清理全部失效商品吗?',
- success: async (res) => {
- if (!res.confirm) return
- try {
- const result = await cleanInvalidCart()
- const n = (result.data && result.data.removedCount) || 0
- uni.showToast({ title: n ? `已清理${n}件` : '暂无失效商品', icon: 'none' })
- await loadCart()
- } catch (e) {
- // request 已提示
- }
- }
- })
- }
- async function onCheckout() {
- const ids = getCheckedPurchasableIds(groups.value)
- if (!ids.length) {
- uni.showToast({ title: '请先勾选要结算的商品', icon: 'none' })
- return
- }
- const shopIds = getCheckedShopIds(groups.value)
- if (shopIds.size > 1) {
- uni.showToast({ title: CART_MSG_CROSS_SHOP, icon: 'none' })
- return
- }
- try {
- await prepareCartCheckout(ids)
- goCartCheckout(ids)
- } catch (e) {
- // request 已提示
- }
- }
- function onShopClick(group) {
- if (group && group.shopId) {
- goShopHome(group.shopId)
- }
- }
- function onGoodsClick(item) {
- if (item && item.goodsId) {
- goGoodsDetail(item.goodsId)
- }
- }
- function goLogin() {
- uni.navigateTo({ url: PAGE_LOGIN })
- }
- function goHome() {
- uni.switchTab({ url: PAGE_HOME })
- }
- onShow(() => {
- loggedIn.value = !!getToken()
- calcScrollHeight()
- if (!loggedIn.value) {
- groups.value = []
- return
- }
- loadCart()
- })
- </script>
- <style lang="scss" scoped>
- .cart-page {
- height: calc(100vh - 288rpx);
- background: #f5f6f8;
- padding-bottom: env(safe-area-inset-bottom);
- }
- .cart-guest,
- .cart-empty,
- .cart-loading,
- .cart-error {
- padding: 120rpx 48rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .cart-btn-primary {
- margin-top: 40rpx;
- width: 320rpx;
- height: 80rpx;
- line-height: 80rpx;
- background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
- color: #fff;
- font-size: 28rpx;
- border-radius: 40rpx;
- border: none;
- }
- .cart-retry {
- margin-top: 24rpx;
- font-size: 28rpx;
- color: #2e7d32;
- }
- .cart-toolbar {
- display: flex;
- justify-content: flex-end;
- gap: 24rpx;
- padding: 16rpx 24rpx;
- background: #fff;
- }
- .cart-toolbar__btn {
- font-size: 26rpx;
- color: #2e7d32;
- }
- .cart-scroll {
- padding: 16rpx 24rpx;
- box-sizing: border-box;
- }
- .cart-footer {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 100rpx;
- display: flex;
- align-items: center;
- padding: 16rpx 24rpx;
- padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
- background: #fff;
- border-top: 1rpx solid #eee;
- z-index: 10;
- }
- .cart-footer__check {
- display: flex;
- align-items: center;
- margin-right: 16rpx;
- }
- .check-icon {
- width: 36rpx;
- height: 36rpx;
- border: 2rpx solid #ccc;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fff;
- }
- .check-icon--on {
- background: #2e7d32;
- border-color: #2e7d32;
- }
- .cart-footer__label {
- margin-left: 8rpx;
- font-size: 26rpx;
- color: #333;
- }
- .cart-footer__sum {
- flex: 1;
- text-align: right;
- margin-right: 16rpx;
- }
- .cart-footer__sum-label {
- font-size: 26rpx;
- color: #333;
- }
- .cart-footer__sum-price {
- font-size: 34rpx;
- color: #e53935;
- font-weight: 600;
- }
- .cart-footer__btn {
- flex-shrink: 0;
- min-width: 200rpx;
- height: 72rpx;
- line-height: 72rpx;
- padding: 0 28rpx;
- background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
- color: #fff;
- font-size: 28rpx;
- border-radius: 36rpx;
- border: none;
- }
- .cart-footer__btn[disabled] {
- opacity: 0.5;
- }
- </style>
|