Files
medical-mall/pages/main/cart.uvue
2026-05-14 15:28:09 +08:00

2597 lines
61 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- pages/main/cart.uvue -->
<template>
<view class="cart-page" :style="cartPageStyle">
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-container" :style="{ paddingRight: (navBarRight > 0 ? navBarRight : 16) + 'px' }">
<view class="nav-title-group">
<text class="nav-title">购物车</text>
<text class="nav-count">({{ cartItemCount }})</text>
</view>
<view class="nav-search-entry" @click="goToCartSearch">
<text class="nav-search-icon">⌕</text>
<text class="nav-search-text">搜索购物车商品</text>
</view>
</view>
<view class="cart-tabs-row">
<view class="cart-tabs-left">
<view
class="cart-tab"
:class="{ 'cart-tab-active': currentCartType == 'goods' }"
@click="switchCartType('goods')"
>
<text class="cart-tab-text">购物</text>
</view>
<view
class="cart-tab"
:class="{ 'cart-tab-active': currentCartType == 'service' }"
@click="switchCartType('service')"
>
<text class="cart-tab-text">服务</text>
</view>
</view>
<view class="cart-manage-btn" @click="toggleManageMode">
<text class="cart-manage-text">{{ isManageMode ? '完成' : '管理' }}</text>
</view>
</view>
<view class="cart-filter-row">
<view class="cart-filter-left">
<view
class="filter-chip"
:class="{ 'filter-chip-active': activeQuickFilter == 'discount' }"
@click="toggleQuickFilter('discount')"
>
<text class="filter-chip-icon">↓</text>
<text class="filter-chip-text">降价</text>
</view>
<view
class="filter-chip"
:class="{ 'filter-chip-active': activeQuickFilter == 'frequent' }"
@click="toggleQuickFilter('frequent')"
>
<text class="filter-chip-icon">↺</text>
<text class="filter-chip-text">常购</text>
</view>
</view>
<view class="filter-more-btn" @click="openFilterPanel">
<text class="filter-more-text">筛选</text>
<text class="filter-more-icon">⌯</text>
</view>
</view>
</view>
<!-- 导航栏占位符 - 需要包含statusBarHeight + 导航栏高度44px -->
<view class="navbar-placeholder" :style="{ height: navPlaceholderHeight + 'px' }"></view>
<!-- 购物车内容 -->
<scroll-view
:scroll-y="true"
class="cart-content"
:style="cartContentStyle"
:show-scrollbar="false"
:enhanced="true"
:bounces="true"
:lower-threshold="120"
@scroll="onRecommendScroll"
@scrolltolower="onRecommendScrollToLower"
>
<!-- 空购物车 -->
<view v-if="!loading && currentCartType == 'goods' && displayCartItems.length === 0" class="empty-cart">
<text class="empty-icon">🛒</text>
<text class="empty-title">{{ emptyTitle }}</text>
<text class="empty-desc">{{ emptyDesc }}</text>
<button v-if="showEmptyShoppingBtn" class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<view v-else-if="!loading && currentCartType == 'service' && displayServiceItems.length === 0" class="empty-cart">
<text class="empty-icon">🧰</text>
<text class="empty-title">{{ emptyTitle }}</text>
<text class="empty-desc">{{ emptyDesc }}</text>
</view>
<view v-else-if="currentCartType == 'service'" class="cart-list service-cart-list">
<view
v-for="item in displayServiceItems"
:key="item.id"
class="shop-group service-group"
>
<view class="service-header">
<text class="service-shop-name">{{ item.shopName }}</text>
<text class="service-status">{{ item.serviceStatus }}</text>
</view>
<view class="service-item">
<image class="service-image" :src="item.image" mode="aspectFill" />
<view class="service-info">
<text class="service-name" :lines="1">{{ item.name }}</text>
<text class="service-spec">{{ item.spec }}</text>
<text class="service-desc" :lines="2">{{ item.description }}</text>
<view class="service-footer">
<text class="service-price">¥{{ item.price }}</text>
<text class="service-action">查看详情</text>
</view>
</view>
</view>
</view>
</view>
<!-- 购物车商品列表 -->
<view v-else class="cart-list">
<view
v-for="group in displayCartGroups"
:key="group.shopId"
class="shop-group"
>
<!-- 店铺头部 -->
<view class="shop-header">
<view class="shop-select" @click="toggleShopSelect(group.shopId)">
<text v-if="isShopSelected(group.shopId)" class="selected-icon">✓</text>
<text v-else class="unselected-icon"></text>
</view>
<text class="shop-icon" @click="navigateToShop(group.shopId, group.merchantId)">🏪</text>
<text class="shop-name" :lines="1" @click="navigateToShop(group.shopId, group.merchantId)">{{ group.shopName }}</text>
<text class="shop-arrow" @click="navigateToShop(group.shopId, group.merchantId)">></text>
</view>
<!-- 店铺商品 -->
<view
v-for="item in group.items"
:key="item.id"
class="cart-item"
>
<view class="item-select" @click="toggleSelect(item.id)">
<text v-if="item.selected" class="selected-icon">✓</text>
<text v-else class="unselected-icon"></text>
</view>
<image
class="item-image"
:src="item.image"
mode="aspectFill"
@click="navigateToProduct(item)"
/>
<view class="item-info">
<view class="info-top">
<text class="item-name" :lines="1">{{ item.name }}</text>
<text class="item-spec">{{ item.spec }}</text>
</view>
<view class="item-footer">
<text class="item-price">¥{{ item.price }}</text>
<view class="quantity-control">
<text class="quantity-btn" @click="decreaseQuantity(item.id)">-</text>
<text class="quantity-value">{{ item.quantity }}</text>
<text class="quantity-btn" @click="increaseQuantity(item.id)">+</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 推荐商品 -->
<view v-if="currentCartType == 'goods' && recommendProducts.length > 0" class="recommend-section">
<view class="section-header">
<text class="section-title">猜你喜欢</text>
</view>
<view class="recommend-list">
<view
v-for="product in recommendProducts"
:key="product.id"
class="recommend-item"
@click="navigateToProduct(product)"
>
<view class="recommend-image-wrapper">
<image
class="recommend-image"
:src="product.image"
mode="aspectFill"
/>
</view>
<text class="recommend-name" :lines="2">{{ product.name }}</text>
<view class="recommend-bottom">
<text class="recommend-price">¥{{ product.price }}</text>
<view class="recommend-add-btn" @click.stop="addToCart(product)">
<text class="recommend-add-icon">+</text>
</view>
</view>
</view>
</view>
<view class="recommend-load-more" @click="loadRecommendProducts(false)">
<text v-if="recommendLoading" class="recommend-load-text">正在加载更多...</text>
<text v-else-if="!recommendHasMore && recommendInitialized" class="recommend-load-text">没有更多了</text>
<text v-else class="recommend-load-text">上拉加载更多</text>
</view>
</view>
<!-- 底部占位符:确保内容不被原生 TabBar 遮挡 -->
<view class="tabbar-safe-area"></view>
</scroll-view>
<view v-if="currentCartType == 'goods' && cartItems.length > 0" class="fixed-cart-settlement-bar">
<view class="settlement-inner">
<view class="settlement-left">
<view class="select-all" @click="toggleSelectAll">
<text v-if="allSelected" class="selected-icon">✓</text>
<text v-else class="unselected-icon"></text>
<text class="select-all-text">全选</text>
</view>
</view>
<view class="settlement-right">
<view v-if="!isManageMode" class="total-info">
<text class="total-text">合计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<button
v-if="!isManageMode"
class="checkout-btn"
@click="goToCheckout"
>
去结算({{ selectedCount }})
</button>
<button
v-else
class="delete-btn"
@click="deleteSelectedItems"
>
删除({{ selectedCount }})
</button>
</view>
</view>
</view>
<!-- 底部结算栏 - 已移除,移动到内容区域 -->
<!-- <view v-if="cartItems.length > 0" class="cart-footer">
<view class="footer-content">
<view class="footer-left">
<view class="select-all" @click="toggleSelectAll">
<text v-if="allSelected" class="selected-icon">✓</text>
<text v-else class="unselected-icon"></text>
<text class="select-all-text">全选</text>
</view>
</view>
<view class="footer-right">
<view v-if="!isManageMode" class="total-info">
<text class="total-text">合计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<button v-if="!isManageMode" class="checkout-btn" @click="goToCheckout">
去结算({{ selectedCount }})
</button>
<button v-else class="delete-btn" @click="deleteSelectedItems">
删除({{ selectedCount }})
</button>
</view>
</view>
</view> -->
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
type ModalSuccess = { confirm: boolean; cancel: boolean }
type LocalCartItem = {
id: string
shopId: string
shopName: string
name: string
price: number
originalPrice: number // 原价
memberPrice: number // 会员价
image: string
spec: string
quantity: number
selected: boolean
productId: string
skuId: string
merchantId: string
type?: string
hasDiscount?: boolean
discountAmount?: number
isFrequent?: boolean
buyCount?: number
isMock?: boolean
serviceStatus?: string
}
type ServiceCartItem = {
id: string
shopId: string
shopName: string
name: string
price: number
image: string
spec: string
serviceStatus: string
description: string
}
type CartGroup = {
shopId: string
shopName: string
merchantId: string
items: LocalCartItem[]
}
const compareStrings = (a: string, b: string): boolean => {
console.log('[compareStrings] a length:', a.length, 'b length:', b.length)
console.log('[compareStrings] a type:', typeof a, 'b type:', typeof b)
console.log('[compareStrings] a value:', JSON.stringify(a))
console.log('[compareStrings] b value:', JSON.stringify(b))
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
const aCode = a.charCodeAt(i)
const bCode = b.charCodeAt(i)
if (aCode != null && bCode != null && aCode !== bCode) {
console.log('[compareStrings] mismatch at index:', i, 'a:', aCode, 'b:', bCode)
return false
}
}
return true
}
type RecommendProduct = {
id: string
shopId: string
shopName: string
name: string
price: number
image: string
skuId: string
merchant_id: string
}
// 响应式数据
const cartItems = ref<LocalCartItem[]>([])
const recommendProducts = ref<RecommendProduct[]>([])
const recommendPage = ref<number>(1)
const recommendPageSize = ref<number>(8)
const recommendHasMore = ref<boolean>(true)
const recommendInitialized = ref<boolean>(false)
const loading = ref<boolean>(false)
const statusBarHeight = ref(0)
const isManageMode = ref(false)
const updatingItems = ref<Set<string>>(new Set()) // Track items being updated to prevent race conditions
const currentCartType = ref<string>('goods')
const activeQuickFilter = ref<string>('')
const recommendLoading = ref(false)
const lastRecommendLoadTime = ref<number>(0)
const recommendPendingLoad = ref<boolean>(false)
const recommendBottomLocked = ref<boolean>(false)
const recommendViewportHeight = ref<number>(0)
const pageWindowHeight = ref<number>(0)
const isAndroidApp = ref<boolean>(false)
const serviceMockItems = ref<ServiceCartItem[]>([
{
id: 'service-1',
shopId: 'service-shop-1',
shopName: '到店服务中心',
name: '空调深度清洗服务',
price: 199,
image: '/static/images/default.png',
spec: '上门服务|预约制',
serviceStatus: '待预约',
description: '含上门检测、深度清洗与基础保养'
},
{
id: 'service-2',
shopId: 'service-shop-2',
shopName: '家电保障服务',
name: '冰箱延保一年',
price: 89,
image: '/static/images/default.png',
spec: '电子保单|立即生效',
serviceStatus: '已加入',
description: '覆盖压缩机、电路板等核心部件'
}
])
// 小程序胶囊按钮信息类型
type CapsuleButtonInfo = {
left: number,
top: number,
right: number,
bottom: number,
width: number,
height: number
}
// 小程序胶囊按钮信息
const capsuleButtonInfo = ref<CapsuleButtonInfo | null>(null)
const navBarRight = ref(0) // 导航栏右侧预留空间
// 计算属性
const cartGroups = computed<CartGroup[]>(() => {
console.log('[cartGroups] 计算购物车分组, cartItems count:', cartItems.value.length)
const groups = new Map<string, CartGroup>()
cartItems.value.forEach((item: LocalCartItem) => {
console.log('[cartGroups] item:', item.id, 'shopId:', item.shopId, 'shopName:', item.shopName)
const shopKey = item.shopId
if (!groups.has(shopKey)) {
groups.set(shopKey, {
shopId: item.shopId,
shopName: item.shopName,
merchantId: item.merchantId,
items: []
})
}
const group = groups.get(shopKey)
if (group != null) {
group.items.push(item)
}
})
const groupArray: CartGroup[] = []
groups.forEach((value: CartGroup) => {
console.log('[cartGroups] group:', value.shopId, 'items count:', value.items.length)
groupArray.push(value)
})
return groupArray
})
const allSelected = computed(() => {
return cartItems.value.length > 0 && cartItems.value.every((item: LocalCartItem) => item.selected)
})
const cartItemCount = computed((): number => {
return cartItems.value.length
})
const displayCartItems = computed<LocalCartItem[]>(() => {
let list = cartItems.value
if (currentCartType.value == 'goods') {
list = list.filter((item: LocalCartItem) => {
const itemType = item.type
return itemType == null || itemType == '' || itemType == 'goods'
})
}
if (currentCartType.value == 'service') {
return []
}
if (activeQuickFilter.value == 'discount') {
list = list.filter((item: LocalCartItem) => {
const hasDiscount = item.hasDiscount == true
const discountAmount = item.discountAmount ?? 0
return hasDiscount || discountAmount > 0 || (item.memberPrice > 0 && item.memberPrice < item.price)
})
}
if (activeQuickFilter.value == 'frequent') {
list = list.filter((item: LocalCartItem) => {
const isFrequent = item.isFrequent == true
const buyCount = item.buyCount ?? 0
return isFrequent == true || (buyCount != null && buyCount > 1)
})
}
return list
})
const displayServiceItems = computed<ServiceCartItem[]>(() => {
let list = serviceMockItems.value
if (activeQuickFilter.value == 'discount') {
list = list.filter((item: ServiceCartItem) => item.price <= 199)
}
if (activeQuickFilter.value == 'frequent') {
list = list.filter((item: ServiceCartItem) => item.serviceStatus == '已加入')
}
return list
})
const displayCartGroups = computed<CartGroup[]>(() => {
const groups = new Map<string, CartGroup>()
displayCartItems.value.forEach((item: LocalCartItem) => {
const shopKey = item.shopId
if (!groups.has(shopKey)) {
groups.set(shopKey, {
shopId: item.shopId,
shopName: item.shopName,
merchantId: item.merchantId,
items: []
})
}
const group = groups.get(shopKey)
if (group != null) {
group.items.push(item)
}
})
const groupArray: CartGroup[] = []
groups.forEach((value: CartGroup) => {
groupArray.push(value)
})
return groupArray
})
const navPlaceholderHeight = computed((): number => {
return statusBarHeight.value + 44 + 52 + 46
})
const cartPageStyle = computed((): string => {
if (!isAndroidApp.value) {
return ''
}
if (pageWindowHeight.value <= 0) {
return ''
}
return 'height:' + pageWindowHeight.value + 'px;min-height:' + pageWindowHeight.value + 'px;'
})
const cartContentStyle = computed((): string => {
if (!isAndroidApp.value) {
return ''
}
if (pageWindowHeight.value <= 0) {
return ''
}
const contentHeight = Math.max(pageWindowHeight.value - navPlaceholderHeight.value, 240)
return 'height:' + contentHeight + 'px;min-height:' + contentHeight + 'px;'
})
const emptyTitle = computed((): string => {
if (currentCartType.value == 'service') {
return '暂无服务类购物车商品'
}
if (activeQuickFilter.value == 'discount') {
return '暂无降价商品'
}
if (activeQuickFilter.value == 'frequent') {
return '暂无常购商品'
}
return '购物车是空的'
})
const emptyDesc = computed((): string => {
if (currentCartType.value == 'service') {
return '可切换筛选条件,或等待服务购物车正式接入'
}
if (activeQuickFilter.value == 'discount') {
return '当前没有符合降价条件的商品'
}
if (activeQuickFilter.value == 'frequent') {
return '当前没有符合常购条件的商品'
}
return '快去挑选喜欢的商品吧'
})
const showEmptyShoppingBtn = computed((): boolean => {
return currentCartType.value == 'goods'
})
const selectedCount = computed(() => {
return cartItems.value.filter((item: LocalCartItem) => item.selected).reduce((sum: number, item: LocalCartItem) => sum + item.quantity, 0)
})
const totalPrice = computed(() => {
return cartItems.value
.filter((item: LocalCartItem) => item.selected)
.reduce((sum: number, item: LocalCartItem) => {
// 优先使用会员价,如果没有会员价则使用原价
const finalPrice = item.memberPrice > 0 && item.memberPrice < item.price ? item.memberPrice : item.price
return sum + finalPrice * item.quantity
}, 0)
.toFixed(2)
})
// 计算会员节省金额
const memberSavedAmount = computed(() => {
return cartItems.value
.filter((item: LocalCartItem) => item.selected && item.memberPrice > 0 && item.memberPrice < item.price)
.reduce((sum: number, item: LocalCartItem) => sum + (item.price - item.memberPrice) * item.quantity, 0)
.toFixed(2)
})
// 检查店铺是否全选
const isShopSelected = (shopId: string): boolean => {
const shopItems: LocalCartItem[] = []
for (let i = 0; i < cartItems.value.length; i++) {
if (compareStrings(cartItems.value[i].shopId, shopId)) {
shopItems.push(cartItems.value[i])
}
}
if (shopItems.length === 0) return false
for (let i = 0; i < shopItems.length; i++) {
if (!shopItems[i].selected) return false
}
return true
}
const toggleManageMode = () => {
isManageMode.value = !isManageMode.value
}
const switchCartType = (type: string) => {
currentCartType.value = type
if (type == 'service') {
isManageMode.value = false
}
}
const toggleQuickFilter = (filter: string) => {
if (activeQuickFilter.value == filter) {
activeQuickFilter.value = ''
} else {
activeQuickFilter.value = filter
}
}
const goToCartSearch = () => {
uni.navigateTo({
url: '/pages/main/cart-search/cart-search'
})
}
const openFilterPanel = () => {
uni.showActionSheet({
itemList: ['全部商品', '只看降价', '只看常购', '只看有货', '只看失效商品'],
success: (res) => {
console.log('filter index:', res.tapIndex)
if (res.tapIndex === 0) {
activeQuickFilter.value = ''
return
}
if (res.tapIndex === 1) {
activeQuickFilter.value = 'discount'
return
}
if (res.tapIndex === 2) {
activeQuickFilter.value = 'frequent'
return
}
uni.showToast({
title: '当前筛选项待接入',
icon: 'none'
})
}
})
}
// 初始化页面数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
pageWindowHeight.value = systemInfo.windowHeight ?? systemInfo.screenHeight ?? 0
recommendViewportHeight.value = Math.max(pageWindowHeight.value - navPlaceholderHeight.value - 120, 240)
// #ifdef APP-ANDROID
isAndroidApp.value = true
// #endif
// 获取小程序胶囊按钮信息
// #ifdef MP-WEIXIN
try {
const menuButton = uni.getMenuButtonBoundingClientRect()
if (menuButton != null) {
capsuleButtonInfo.value = {
left: menuButton.left,
top: menuButton.top,
right: menuButton.right,
bottom: menuButton.bottom,
width: menuButton.width,
height: menuButton.height
}
navBarRight.value = (systemInfo.screenWidth - menuButton.left) + 10
}
} catch (e) {
console.log('获取胶囊按钮信息失败', e)
navBarRight.value = 90
}
// #endif
}
// 生命周期
onMounted(() => {
initPage()
})
const mergeRecommendProducts = (oldList: RecommendProduct[], newList: RecommendProduct[]): RecommendProduct[] => {
const ids: string[] = []
const result: RecommendProduct[] = []
for (let i = 0; i < oldList.length; i++) {
ids.push(oldList[i].id)
result.push(oldList[i])
}
for (let i = 0; i < newList.length; i++) {
if (ids.indexOf(newList[i].id) < 0) {
ids.push(newList[i].id)
result.push(newList[i])
}
}
return result
}
const mockRecommendProducts = (page: number, pageSize: number): RecommendProduct[] => {
const list: RecommendProduct[] = []
if (page >= 5) {
return list
}
for (let i = 0; i < pageSize; i++) {
const index = (page - 1) * pageSize + i + 1
list.push({
id: 'mock-recommend-' + index,
shopId: 'mock-shop-' + index,
shopName: '商城推荐',
name: '猜你喜欢 推荐商品 ' + index,
price: 9.9 + index,
image: '/static/images/default.png',
skuId: '',
merchant_id: ''
})
}
return list
}
const fetchRecommendProducts = async (page: number, pageSize: number): Promise<RecommendProduct[]> => {
console.log('[cart推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
const hotResp = await supabaseService.searchProducts('', page, pageSize, 'sales')
console.log('[cart推荐] fetchRecommendProducts 完成 total=', hotResp.total, 'hasmore=', hotResp.hasmore, 'dataLength=', hotResp.data.length)
const rawList = hotResp.data
return rawList.map((p: Product): RecommendProduct => {
return {
id: p.id,
shopId: p.merchant_id ?? 'unknown',
shopName: p.shop_name ?? '商城推荐',
name: p.name,
price: p.base_price ?? p.market_price ?? 0,
image: p.main_image_url ?? p.image_url ?? '/static/images/default.png',
skuId: '',
merchant_id: p.merchant_id ?? ''
}
})
}
function toRecommendScrollJson(value: any | null): UTSJSONObject | null {
if (value == null) {
return null
}
if (value instanceof UTSJSONObject) {
return value as UTSJSONObject
}
const raw = JSON.stringify(value)
if (raw == '' || raw == 'null') {
return null
}
const parsed = JSON.parse(raw)
if (parsed == null) {
return null
}
return parsed as UTSJSONObject
}
function readRecommendScrollMetric(detail: UTSJSONObject | null, key: string): number {
if (detail == null) {
return 0
}
const value = detail.getNumber(key)
if (value != null) {
return value
}
return 0
}
async function loadRecommendProducts(reset: boolean): Promise<void> {
console.log('[cart推荐] loadRecommendProducts 入口 reset=', reset, 'page=', recommendPage.value, 'pageSize=', recommendPageSize.value, 'loading=', recommendLoading.value, 'hasMore=', recommendHasMore.value, 'oldLength=', recommendProducts.value.length)
if (recommendLoading.value) {
console.log('[cart推荐] 跳过:正在加载中')
if (!reset) {
recommendPendingLoad.value = true
}
return
}
if (!reset && !recommendHasMore.value) {
console.log('[cart推荐] 跳过:没有更多数据')
return
}
if (!reset) {
lastRecommendLoadTime.value = Date.now()
}
recommendLoading.value = true
if (reset) {
recommendPage.value = 1
recommendHasMore.value = true
}
try {
const page = recommendPage.value
const pageSize = recommendPageSize.value
const newList = await fetchRecommendProducts(page, pageSize)
console.log('[cart推荐] page=', page, '返回数量=', newList.length)
console.log('[cart推荐] 返回ID=', newList.map((item) => item.id).join(','))
const beforeLength = recommendProducts.value.length
let afterList: RecommendProduct[] = []
if (reset) {
afterList = newList
} else {
afterList = mergeRecommendProducts(recommendProducts.value, newList)
}
recommendProducts.value = afterList
console.log('[cart推荐] 追加前=', beforeLength, '追加后=', recommendProducts.value.length)
if (!reset && newList.length > 0 && afterList.length === beforeLength) {
console.warn('[cart推荐] 本次返回商品全部重复,请检查 searchProducts 分页是否生效 page=', page)
}
if (newList.length < pageSize) {
recommendHasMore.value = false
} else {
recommendPage.value = recommendPage.value + 1
}
if (!reset) {
recommendBottomLocked.value = false
}
recommendInitialized.value = true
} catch (error) {
console.error('加载推荐商品失败:', error)
if (reset && recommendProducts.value.length === 0) {
const mockList = mockRecommendProducts(1, recommendPageSize.value)
recommendProducts.value = mockList
recommendPage.value = 2
recommendHasMore.value = true
recommendInitialized.value = true
}
} finally {
recommendLoading.value = false
if (!reset && recommendPendingLoad.value && recommendHasMore.value) {
console.log('[cart推荐] 消费待续加载请求')
recommendPendingLoad.value = false
loadRecommendProducts(false)
}
}
}
function onRecommendScrollToLower(): void {
console.log('[cart推荐] scrolltolower 触发 currentCartType=', currentCartType.value, 'initialized=', recommendInitialized.value)
if (currentCartType.value != 'goods') {
console.log('[cart推荐] 跳过:当前不是 goods')
return
}
recommendBottomLocked.value = true
loadRecommendProducts(false)
}
function onRecommendScroll(event: any): void {
if (currentCartType.value != 'goods' || recommendLoading.value || !recommendHasMore.value) {
return
}
try {
const eventObj = toRecommendScrollJson(event)
let detailObj: UTSJSONObject | null = null
if (eventObj != null) {
detailObj = toRecommendScrollJson(eventObj.get('detail'))
}
if (detailObj == null) {
return
}
const scrollTop = readRecommendScrollMetric(detailObj, 'scrollTop')
const scrollHeight = readRecommendScrollMetric(detailObj, 'scrollHeight')
let clientHeight = readRecommendScrollMetric(detailObj, 'clientHeight')
if (clientHeight <= 0) {
clientHeight = recommendViewportHeight.value
}
console.log('[cart推荐] scroll事件 scrollTop=', scrollTop, 'scrollHeight=', scrollHeight, 'clientHeight=', clientHeight)
if (scrollHeight <= 0 || clientHeight <= 0) {
return
}
const distanceToBottom = scrollHeight - scrollTop - clientHeight
if (distanceToBottom > 260) {
recommendBottomLocked.value = false
}
if (distanceToBottom <= 180) {
console.log('[cart推荐] scroll 兜底触底 distanceToBottom=', distanceToBottom)
if (recommendBottomLocked.value) {
recommendPendingLoad.value = true
return
}
recommendBottomLocked.value = true
loadRecommendProducts(false)
}
} catch (e) {
console.error('[cart推荐] 处理推荐滚动失败:', e)
}
}
// 加载数据
const loadCartData = async () => {
loading.value = true
try {
// 获取会员折扣信息
let memberDiscount = 1.0
try {
const memberInfo = await supabaseService.getUserMemberInfo()
const discountRaw = memberInfo.get('discount')
if (discountRaw != null) {
memberDiscount = discountRaw as number
}
} catch (e) {
console.log('获取会员信息失败,使用默认折扣:', e)
}
// 从Supabase加载购物车数据
const supabaseCartItems = await supabaseService.getCartItems()
// 转换数据格式以匹配前端界面
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem): LocalCartItem => {
// 调试日志:打印每条商品数据的关键字段
console.log(`CartItem raw: id=${item.id}, shop_id=${item.shop_id}, shop_name=${item.shop_name}, name=${item.product_name}, price=${item.product_price}`);
// 关键修复确保shopId有值如果后端返回null/undefined使用'default_shop'作为分组键
const shopId = (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop'
const shopName = (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选'
// 计算会员价
const originalPrice = item.product_price != null ? item.product_price : 0
let memberPrice = 0
if (memberDiscount > 0 && memberDiscount < 1 && originalPrice > 0) {
memberPrice = Math.round(originalPrice * memberDiscount * 100) / 100
}
return {
id: item.id,
shopId: shopId,
shopName: shopName,
name: item.product_name ?? '未知商品',
price: originalPrice,
originalPrice: originalPrice,
memberPrice: memberPrice,
image: item.product_image ?? '/static/images/default.png',
spec: item.product_specification ?? '标准规格',
quantity: item.quantity ?? 1,
selected: item.selected ?? false,
productId: item.product_id ?? '',
skuId: item.sku_id ?? '',
merchantId: item.merchant_id ?? ''
} as LocalCartItem
})
console.log('Transformed items count:', transformedItems.length);
cartItems.value = transformedItems
if (!recommendInitialized.value) {
await loadRecommendProducts(true)
}
} catch (error) {
console.error('加载购物车数据失败:', error)
cartItems.value = []
} finally {
loading.value = false
}
}
onShow(() => {
loadCartData()
})
// 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => {
// 乐观更新
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
const newSelected = !cartItems.value[index].selected
cartItems.value[index].selected = newSelected
cartItems.value = [...cartItems.value] // 触发响应式更新
// 更新到Supabase
const success = await supabaseService.updateCartItemSelection(itemId, newSelected)
if (!success) {
console.error('更新选中状态失败')
// 恢复状态
cartItems.value[index].selected = !newSelected
cartItems.value = [...cartItems.value]
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
}
}
}
const toggleShopSelect = async (shopId: string) => {
console.log('[toggleShopSelect] shopId:', shopId)
console.log('[toggleShopSelect] shopId length:', shopId.length)
console.log('[toggleShopSelect] cartItems.value.length:', cartItems.value.length)
// 用 for 循环替代 filter避免安卓端 UTS filter 的问题
const shopItems: LocalCartItem[] = []
for (let i = 0; i < cartItems.value.length; i++) {
const item = cartItems.value[i]
const itemShopId = item.shopId
// 安卓端字符串比较问题:使用 localeCompare 或逐字符比较
const isMatch = compareStrings(itemShopId, shopId)
console.log('[toggleShopSelect] checking item:', item.id, 'item.shopId:', itemShopId, 'match:', isMatch)
if (isMatch) {
shopItems.push(item)
}
}
console.log('[toggleShopSelect] shopItems count:', shopItems.length)
if (shopItems.length === 0) return
// 用 for 循环替代 every
let allSelected = true
for (let i = 0; i < shopItems.length; i++) {
if (!shopItems[i].selected) {
allSelected = false
break
}
}
const newState = !allSelected
console.log('[toggleShopSelect] allSelected:', allSelected, 'newState:', newState)
const shopItemIds: string[] = []
for (let i = 0; i < shopItems.length; i++) {
shopItemIds.push(shopItems[i].id)
}
console.log('[toggleShopSelect] shopItemIds:', shopItemIds)
// 创建全新的数组来触发响应式更新
const newCartItems: LocalCartItem[] = []
for (let i = 0; i < cartItems.value.length; i++) {
const item = cartItems.value[i]
const isMatch = compareStrings(item.shopId, shopId)
if (isMatch) {
console.log('[toggleShopSelect] updating item:', item.id, 'to selected:', newState)
// 创建新的对象
const newItem: LocalCartItem = {
id: item.id,
shopId: item.shopId,
shopName: item.shopName,
name: item.name,
price: item.price,
originalPrice: item.originalPrice,
memberPrice: item.memberPrice,
image: item.image,
spec: item.spec,
quantity: item.quantity,
selected: newState,
productId: item.productId,
skuId: item.skuId,
merchantId: item.merchantId
}
newCartItems.push(newItem)
} else {
newCartItems.push(item)
}
}
// 替换整个数组
cartItems.value = newCartItems
// 批量更新到Supabase
const success = await supabaseService.batchUpdateCartItemSelection(shopItemIds, newState)
if (!success) {
console.error('批量更新店铺商品选中状态失败')
uni.showToast({
title: '操作失败',
icon: 'none'
})
// 重新加载数据以确保状态一致
loadCartData()
}
}
const toggleSelectAll = async () => {
// 目标状态:如果当前全选,则取消全选;否则全选
const newSelectedState = !allSelected.value
// 乐观更新
const oldItems = JSON.parse(JSON.stringify(cartItems.value)) as LocalCartItem[]
const selectedItems = cartItems.value.map((item): LocalCartItem => {
item.selected = newSelectedState
return item
})
cartItems.value = selectedItems
// 更新到Supabase
const itemIds = cartItems.value.map(item => item.id)
if (itemIds.length === 0) return
const success = await supabaseService.batchUpdateCartItemSelection(itemIds, newSelectedState)
if (!success) {
console.error('批量更新选中状态失败')
cartItems.value = oldItems
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
const increaseQuantity = async (itemId: string) => {
if (updatingItems.value.has(itemId)) return
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
updatingItems.value.add(itemId)
const newQuantity = cartItems.value[index].quantity + 1
cartItems.value[index].quantity = newQuantity
cartItems.value = [...cartItems.value]
// 更新到Supabase
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
updatingItems.value.delete(itemId)
if (!success) {
console.error('更新商品数量失败')
// 恢复状态
cartItems.value[index].quantity = newQuantity - 1
cartItems.value = [...cartItems.value]
uni.showToast({ title: '更新失败', icon: 'none' })
}
}
}
const decreaseQuantity = async (itemId: string) => {
if (updatingItems.value.has(itemId)) return
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
if (cartItems.value[index].quantity > 1) {
updatingItems.value.add(itemId)
const newQuantity = cartItems.value[index].quantity - 1
cartItems.value[index].quantity = newQuantity
cartItems.value = [...cartItems.value]
// 更新到Supabase
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
updatingItems.value.delete(itemId)
if (!success) {
console.error('更新商品数量失败')
// 恢复状态
cartItems.value[index].quantity = newQuantity + 1
cartItems.value = [...cartItems.value]
uni.showToast({ title: '更新失败', icon: 'none' })
}
} else {
// 数量为1时询问是否删除
uni.showModal({
title: '提示',
content: '确定要从购物车移除该商品吗?',
success: (res) => {
if (res.confirm) {
// 从Supabase删除
supabaseService.deleteCartItem(itemId).then((success) => {
if (success) {
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
uni.showToast({
title: '已移除',
icon: 'none'
})
} else {
console.error('删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
}
}
}
// 删除商品 - 增加保存逻辑
const deleteSelectedItems = async () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择要删除的商品',
icon: 'none'
})
return
}
uni.showModal({
title: '提示',
content: `确定要删除选中的 ${selectedCount.value} 件商品吗?`,
success: (res) => {
if (res.confirm) {
// 获取选中的商品ID
const selectedItemIds = cartItems.value
.filter(item => item.selected)
.map(item => item.id)
// 批量删除到Supabase
supabaseService.batchDeleteCartItems(selectedItemIds).then((success) => {
if (success) {
// 从本地列表移除
cartItems.value = cartItems.value.filter(item => !item.selected)
// 如果购物车删空了,退出管理模式
if (cartItems.value.length === 0) {
isManageMode.value = false
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} else {
console.error('批量删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
}
const addToCart = async (product: RecommendProduct) => {
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin('/pages/main/cart')
return
}
uni.showLoading({ title: '检查商品...' })
try {
const productId = product.id
const skuId = product.skuId
const merchantId = product.merchant_id
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(productId)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
uni.showToast({
title: '请选择规格',
icon: 'none'
})
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + productId
})
}, 500)
} else {
// 无规格,直接加入购物车
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(productId, 1, skuId, merchantId)
uni.hideLoading()
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
// 重新加载购物车数据
loadCartData()
} else {
console.error('添加商品到购物车失败')
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
} catch (error) {
console.error('添加商品到购物车异常:', error)
uni.hideLoading()
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
// 导航函数
const navigateToShop = (shopId: string, merchantId: any) => {
// Prevent navigation for invalid shops
if (shopId == '' || shopId === 'default_shop' || shopId === 'unknown') return
let url = `/pages/mall/consumer/shop-detail?id=${shopId}`
if (merchantId != null) {
const mId = `${merchantId}`
if (mId !== '' && mId !== 'null' && mId !== 'undefined' && mId !== 'false') {
url += `&merchantId=${mId}`
}
}
uni.navigateTo({ url })
}
const goShopping = () => {
uni.switchTab({ url: '/pages/main/index' })
}
const navigateToProduct = (product: any) => {
console.log('navigateToProduct', product)
// 使用 JSON 转换确保可以作为 JSONObject 处理,兼容 LocalCartItem 类型和普通对象
const productJson = JSON.parse(JSON.stringify(product)) as UTSJSONObject
// 使用productId如果存在作为跳转的商品ID否则使用id
let productId = productJson.getString('productId')
if (productId == null || productId == '') {
productId = productJson.getString('id')
}
if (productId == null || productId == '') {
console.error('无法获取商品ID', product)
return
}
// 传递完整的参数,确保商品详情页能正确加载
let paramsArr: string[] = []
paramsArr.push('id=' + encodeURIComponent(productId))
paramsArr.push('productId=' + encodeURIComponent(productId))
const price = productJson.getNumber('price') ?? 0
paramsArr.push('price=' + price)
let originalPrice = productJson.getNumber('original_price')
if (originalPrice == null) {
originalPrice = productJson.getNumber('originalPrice')
}
if (originalPrice == null) {
originalPrice = parseFloat((price * 1.2).toFixed(2))
}
paramsArr.push('originalPrice=' + originalPrice)
const name = productJson.getString('name') ?? ''
paramsArr.push('name=' + encodeURIComponent(name))
const image = productJson.getString('image') ?? '/static/images/default.png'
paramsArr.push('image=' + encodeURIComponent(image))
const url = `/pages/mall/consumer/product-detail?${paramsArr.join('&')}`
console.log('Navigate to:', url)
uni.navigateTo({
url: url
})
}
const goToCheckout = () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择商品',
icon: 'none'
})
return
}
// 获取选中的商品 (直接过滤cartItems不依赖cartGroups确保扁平化传递)
const selectedItems = cartItems.value
.filter(item => item.selected)
.map(item => ({
id: item.id,
product_id: item.productId ?? item.id,
sku_id: item.skuId ?? item.id,
product_name: item.name,
shop_id: item.shopId, // 关键保留shopId用于分组
shop_name: item.shopName, // 关键保留shopName
merchant_id: item.merchantId,
product_image: item.image,
sku_specifications: item.spec,
price: item.price, // 确保是数字
quantity: item.quantity // 确保是数字
}))
// 关键修复:将结算数据写入 Storage确保 checkout 页面能稳定获取
uni.setStorageSync('checkout_type', 'cart')
// 使用纯JSON序列化防止复杂对象引发的问题
try {
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
} catch (e) {
console.error('存储结算数据失败', e)
uni.showToast({ title: '系统异常,请重试', icon: 'none' })
return
}
// 跳转到结算页面并传递数据
uni.navigateTo({
url: '/pages/mall/consumer/checkout'
})
}
</script>
<style>
.cart-page {
width: 100%;
height: 100vh;
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
overflow: hidden; /* 防止整页滚动 */
}
/* 智能导航栏 */
.smart-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #ff5000;
z-index: 1000;
box-shadow: 0 2px 12px rgba(255, 80, 0, 0.15);
display: flex;
flex-direction: column;
justify-content: flex-start;
flex-shrink: 0;
}
.nav-container {
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1400px;
margin: 0 auto;
height: 44px; /* 统一高度 44px */
}
.nav-title {
font-size: 18px;
font-weight: bold;
color: white;
}
.nav-title-group {
display: flex;
flex-direction: row;
align-items: baseline;
flex-shrink: 0;
}
.nav-count {
font-size: 16px;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
margin-left: 4px;
}
.nav-search-entry {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
margin-left: 14px;
height: 34px;
padding: 0 12px;
border-radius: 17px;
background: rgba(255, 255, 255, 0.18);
min-width: 0;
}
.search-bar-row {
padding: 0 16px 12px 16px;
}
.nav-search-icon {
font-size: 15px;
color: rgba(255, 255, 255, 0.92);
margin-right: 6px;
}
.nav-search-text {
font-size: 13px;
color: rgba(255, 255, 255, 0.92);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-bar {
background-color: rgba(255, 255, 255, 0.96);
border-radius: 18px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
}
.search-bar-icon {
font-size: 16px;
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
height: 40px;
font-size: 14px;
color: #333;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
}
.search-clear {
font-size: 13px;
color: #666;
margin-left: 8px;
white-space: nowrap;
}
/* 导航栏占位符 */
.navbar-placeholder {
width: 100%;
flex-shrink: 0;
}
.cart-tabs-row {
width: 100%;
height: 52px;
padding: 0 16px;
background-color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.cart-tabs-left {
display: flex;
flex-direction: row;
align-items: center;
}
.cart-tab {
height: 52px;
margin-right: 32px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.cart-tab-text {
font-size: 18px;
color: #222222;
font-weight: 600;
}
.cart-tab-active .cart-tab-text {
color: #e60012;
font-weight: 700;
}
.cart-tab-active::after {
content: '';
position: absolute;
left: 50%;
bottom: 5px;
width: 28px;
height: 3px;
border-radius: 2px;
background-color: #e60012;
transform: translateX(-50%);
}
.cart-manage-btn {
height: 36px;
padding: 0 4px 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-left: 1px solid #eeeeee;
flex-shrink: 0;
}
.cart-manage-text {
font-size: 16px;
color: #222222;
}
.cart-filter-row {
width: 100%;
height: 46px;
padding: 0 16px;
background-color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
border-bottom: 1px solid #f1f1f1;
}
.cart-filter-left {
display: flex;
flex-direction: row;
align-items: center;
min-width: 0;
}
.filter-chip {
height: 32px;
padding: 0 14px;
margin-right: 12px;
border-radius: 8px;
background-color: #f6f6f6;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.filter-chip-active {
background-color: #fff2e8;
}
.filter-chip-icon {
font-size: 14px;
color: #ff5000;
margin-right: 4px;
}
.filter-chip-text {
font-size: 15px;
color: #222222;
}
.filter-chip-active .filter-chip-text {
color: #ff5000;
font-weight: 600;
}
.filter-more-btn {
height: 32px;
padding: 0 4px 0 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.filter-more-text {
font-size: 15px;
color: #222222;
margin-right: 4px;
}
.filter-more-icon {
font-size: 18px;
color: #222222;
}
/* 内容区 */
.cart-content {
flex: 1;
/* 必须设置 height: 0 或 overflow: hidden 可以在 flex 容器中正确收缩 */
height: 0px;
width: 100%;
background-color: #f5f5f5;
}
.select-all {
display: flex;
flex-direction: row;
align-items: center;
}
.select-all-text {
font-size: 14px;
color: #333;
margin-left: 8px;
}
.total-info {
display: flex;
flex-direction: row;
align-items: baseline;
margin-right: 15px;
}
.total-text {
font-size: 14px;
color: #666;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
margin-left: 4px;
}
.checkout-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 20px;
padding: 8px 15px;
font-size: 14px;
font-weight: bold;
min-width: 100px;
}
.delete-btn {
background-color: #fff;
color: #ff5000;
border: 1px solid #ff5000;
border-radius: 20px;
padding: 8px 15px;
font-size: 14px;
font-weight: bold;
min-width: 100px;
}
/* 空购物车 */
.empty-cart {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
/* 购物车商品列表 */
.cart-list {
background-color: transparent; /* 背景透明,因为每个店铺有自己的卡片 */
margin: 10px;
border-radius: 0;
overflow: visible;
}
.shop-group {
background-color: white;
border-radius: 12px;
margin-bottom: 12px;
overflow: hidden;
}
.service-cart-list {
margin-top: 12px;
}
.service-group {
padding: 14px;
}
.service-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.service-shop-name {
font-size: 15px;
font-weight: 700;
color: #222;
}
.service-status {
font-size: 12px;
color: #ff5000;
background-color: #fff2e8;
padding: 4px 8px;
border-radius: 12px;
}
.service-item {
display: flex;
flex-direction: row;
align-items: center;
}
.service-image {
width: 76px;
height: 76px;
border-radius: 10px;
margin-right: 12px;
flex-shrink: 0;
background-color: #f3f3f3;
}
.service-info {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.service-name {
font-size: 15px;
font-weight: 700;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.service-spec {
font-size: 12px;
color: #666;
margin-bottom: 6px;
}
.service-desc {
font-size: 12px;
color: #999;
line-height: 18px;
margin-bottom: 8px;
}
.service-footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.service-price {
font-size: 18px;
font-weight: 700;
color: #ff5000;
}
.service-action {
font-size: 13px;
color: #666;
}
.shop-header {
display: flex;
flex-direction: row; /* 强制横向排列 */
align-items: center;
justify-content: flex-start; /* 靠左对齐 */
padding: 12px;
border-bottom: 1px solid #f5f5f5;
}
.shop-select {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
flex-shrink: 0; /* 防止被压缩 */
}
.shop-icon {
font-size: 16px;
margin-right: 6px;
flex-shrink: 0;
}
.shop-name {
font-size: 14px;
font-weight: 700;
color: #333;
margin-right: 4px;
/* 自适应宽度,但不超过剩余空间 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.shop-arrow {
font-size: 12px;
color: #999;
flex-shrink: 0;
}
.cart-item {
display: flex;
flex-direction: row; /* 显式横向排列 */
padding: 12px; /* 减小内边距 */
border-bottom: 1px solid #f5f5f5;
align-items: center;
height: 100px; /* 固定高度节省空间 */
}
.cart-item:last-child {
border-bottom: none;
}
.item-select {
width: 30px; /* 减小选择框区域 */
height: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
}
.selected-icon {
width: 18px;
height: 18px;
background-color: #ff5000;
color: white;
border-radius: 9px;
text-align: center;
line-height: 18px;
font-size: 12px;
}
.unselected-icon {
width: 18px;
height: 18px;
border: 1px solid #ddd;
border-radius: 9px;
}
.item-image {
width: 70px; /* 减小图片尺寸 */
height: 70px;
border-radius: 6px;
margin-right: 10px;
flex-shrink: 0;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 70px; /* 与图片高度一致 */
overflow: hidden;
}
.info-top {
display: flex;
flex-direction: column;
}
.item-name {
font-size: 14px; /* 稍微减小字体 */
color: #333;
margin-bottom: 2px;
/* display: -webkit-box; REMOVED */
/* -webkit-line-clamp: 1; REMOVED */
/* -webkit-box-orient: vertical; REMOVED */
overflow: hidden;
font-weight: bold;
text-overflow: ellipsis;
}
.item-spec {
font-size: 12px;
color: #999;
margin-bottom: auto; /* 自动占据中间空间 */
}
.item-footer {
display: flex;
flex-direction: row; /* 显式设置横向排列 */
justify-content: space-between;
align-items: center;
width: 100%; /* 确保占满宽度 */
}
.item-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
}
.quantity-control {
display: flex;
flex-direction: row;
align-items: center;
background-color: #f5f5f5;
border-radius: 12px;
overflow: hidden;
height: 28px;
}
.quantity-btn {
width: 28px;
height: 28px;
text-align: center;
line-height: 28px;
font-size: 16px;
color: #333;
background-color: #eee;
}
.quantity-value {
min-width: 36px;
text-align: center;
font-size: 14px;
line-height: 28px;
color: #333;
}
/* 推荐商品 */
.recommend-section {
margin: 20px 10px;
background-color: white;
border-radius: 10px;
padding: 15px;
}
.section-header {
margin-bottom: 15px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
display: flex;
flex-direction: row;
align-items: center;
padding: 4px 8px;
}
.refresh-icon {
font-size: 14px;
margin-right: 4px;
}
.refresh-text {
font-size: 12px;
color: #999;
}
.recommend-tip {
font-size: 12px;
color: #999;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.recommend-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 12px; REMOVED */
}
.recommend-item {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
width: 48%; /* 替换 grid 1fr auto fit */
margin-bottom: 12px;
}
.recommend-image-wrapper {
width: 100%;
padding-bottom: 100%;
position: relative;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
}
.recommend-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 8px;
}
.recommend-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
/* display: -webkit-box; REMOVED */
/* -webkit-line-clamp: 2; REMOVED */
/* -webkit-box-orient: vertical; REMOVED */
text-overflow: ellipsis;
}
.recommend-bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding-right: 8px;
}
.recommend-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
}
.recommend-add-btn {
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.recommend-add-icon {
color: white;
font-size: 16px;
line-height: 1;
font-weight: bold;
}
.recommend-load-more {
width: 100%;
height: 48px;
padding-top: 4px;
padding-bottom: 6px;
display: flex;
align-items: center;
flex-direction: row;
justify-content: center;
}
.recommend-load-text {
font-size: 13px;
color: #999999;
}
/* 响应式布局优化 */
@media screen and (max-width: 414px) {
.recommend-item {
width: 48%;
}
}
@media screen and (min-width: 415px) and (max-width: 768px) {
.recommend-item {
width: 48%;
}
}
@media screen and (min-width: 769px) and (max-width: 1024px) {
.recommend-item {
width: 32%;
}
}
@media screen and (min-width: 1025px) and (max-width: 1399px) {
.recommend-item {
width: 23%;
}
}
@media screen and (min-width: 1400px) {
.recommend-item {
width: 18%;
}
}
/* 保留原有的媒体查询用于其他样式 */
@media screen and (min-width: 768px) {
.cart-list,
.recommend-section {
margin: 20px auto;
width: 95%; /* max-width -> width */
}
.recommend-list {
/* grid-template-columns: repeat(4, 1fr); REMOVED */
/* gap: 16px; REMOVED */
/* Flex 布局参数调整在下方 update */
}
}
@media screen and (min-width: 1024px) {
/* 桌面端整体布局调整 */
.cart-content {
padding: 20px 40px;
background-color: #f5f5f5;
}
.cart-list,
.recommend-section {
margin: 20px auto;
width: 96%; /* max-width -> width: percentage is safer */
max-width: 1200px;
}
/* 店铺分组在桌面端显示为网格布局 */
.shop-group {
display: flex;
flex-direction: column;
background: transparent;
box-shadow: none;
border-radius: 0;
overflow: visible;
}
.shop-header {
background: white;
border-radius: 12px;
margin-bottom: 12px;
padding: 16px 80px 16px 24px; /* 同步增加右侧内边距 */
}
/* 购物车商品列表转为列表布局 */
.cart-item {
background: white;
border-radius: 0;
padding: 15px 80px 15px 30px; /* 进一步增加右侧内边距 */
height: 80px; /* 固定高度 */
border-bottom: 1px solid #eee;
box-shadow: none;
display: flex;
flex-direction: row; /* 显式设置横向排列 */
align-items: center; /* 垂直居中 */
width: 100%;
}
.cart-item:hover {
background-color: #f9f9f9;
transform: none;
box-shadow: none;
}
.item-image {
width: 50px;
height: 50px;
margin-right: 20px;
flex-shrink: 0;
}
.item-info {
flex: 1;
height: 100%;
display: flex;
flex-direction: row; /* 信息区域横向排列 */
align-items: center;
justify-content: space-between;
overflow: visible;
}
.info-top {
flex: 1;
display: flex;
flex-direction: row; /* 名称和规格横向排列 */
align-items: center;
margin-right: 20px;
height: 100%;
}
.item-name {
font-size: 14px;
width: 250px; /* 固定名称宽度 */
margin-right: 20px;
/* 限制行数 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0;
}
.item-spec {
width: 150px;
margin-bottom: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-footer {
width: auto;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
/* gap: 40px; REMOVED */
height: 100%;
}
.item-price {
width: 100px;
text-align: right;
margin-bottom: 0;
margin-right: 40px; /* Replace gap */
}
.quantity-control {
margin-left: 0;
display: flex;
flex-direction: row;
}
/* 推荐商品优化 */
.recommend-list {
/* grid-template-columns: repeat(5, 1fr); REMOVED */
/* gap: 20px; REMOVED */
}
.recommend-image-wrapper {
padding-bottom: 100%;
}
/* 底部结算栏优化 */
.cart-footer {
padding: 0 40px;
width: 100%; /* max-width -> width */
margin: 0 auto;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
}
@media screen and (min-width: 1400px) {
.cart-list,
.recommend-section {
width: 1400px;
}
/* 大屏下购物车商品显示3列 - 移除,保持单列列表 */
/* .cart-list .shop-group > view:not(.shop-header) {
grid-template-columns: repeat(3, 1fr);
} */
.recommend-list {
/* grid-template-columns: repeat(6, 1fr); REMOVED */
}
.footer-content {
width: 1400px;
}
}
.fixed-cart-settlement-bar {
position: fixed;
left: 0;
right: 0;
bottom: var(--window-bottom);
z-index: 999;
background-color: #ffffff;
border-top: 1px solid #f1f1f1;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.04);
}
.settlement-inner {
height: 64px;
padding: 0 14px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.settlement-left {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
}
.settlement-right {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
flex: 1;
min-width: 0;
}
.total-info {
display: flex;
flex-direction: row;
align-items: baseline;
margin-right: 12px;
min-width: 0;
}
.total-text {
font-size: 14px;
color: #333333;
white-space: nowrap;
}
.total-price {
font-size: 20px;
color: #ff5000;
font-weight: 700;
margin-left: 3px;
white-space: nowrap;
}
.checkout-btn {
height: 44px;
min-width: 118px;
padding: 0 20px;
background-color: #ff5000;
color: #ffffff;
border-radius: 24px;
font-size: 16px;
font-weight: 700;
border: none;
line-height: 44px;
margin: 0;
white-space: nowrap;
flex-shrink: 0;
}
.delete-btn {
height: 44px;
min-width: 108px;
padding: 0 20px;
background-color: #ff3b30;
color: #ffffff;
border-radius: 24px;
font-size: 16px;
font-weight: 700;
border: none;
line-height: 44px;
margin: 0;
white-space: nowrap;
flex-shrink: 0;
}
@media screen and (max-width: 375px) {
.nav-search-entry {
margin-left: 10px;
padding: 0 10px;
}
.nav-search-text {
font-size: 12px;
}
.search-bar-row {
padding: 0 10px 10px 10px;
}
.cart-tabs-row,
.cart-filter-row {
padding: 0 10px;
}
.cart-tab {
margin-right: 20px;
}
.cart-tab-text,
.cart-manage-text,
.filter-chip-text,
.filter-more-text {
font-size: 14px;
}
.filter-chip {
padding: 0 10px;
margin-right: 8px;
}
.settlement-inner {
padding: 0 10px;
}
.total-price {
font-size: 18px;
}
.checkout-btn,
.delete-btn {
min-width: 96px;
padding: 0 14px;
font-size: 14px;
}
.select-all-text {
font-size: 13px;
}
}
@media screen and (min-width: 768px) {
.cart-tabs-row,
.cart-filter-row {
padding: 0 24px;
}
.fixed-cart-settlement-bar {
padding: 0 20px;
}
.settlement-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.total-price {
font-size: 22px;
}
}
@media screen and (min-width: 1400px) {
.settlement-inner {
max-width: 1400px;
}
}
.tabbar-safe-area {
height: 150px;
width: 100%;
background-color: transparent;
}
</style>