Files
medical-mall/pages/mall/consumer/checkout.uvue

2197 lines
57 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.
<!-- 结算页面 -->
<template>
<view class="checkout-page">
<scroll-view class="checkout-content" scroll-y>
<!-- 收货地址 -->
<view class="address-section" @click="selectAddress">
<view v-if="selectedAddress" class="address-info">
<view class="address-header">
<text class="recipient">{{ selectedAddress.recipient_name }}</text>
<text class="phone">{{ selectedAddress.phone }}</text>
<view v-if="selectedAddress.is_default" class="default-tag">
<text class="tag-text">默认</text>
</view>
</view>
<text class="address-detail">{{ getFullAddress(selectedAddress) }}</text>
</view>
<view v-else class="no-address">
<text class="no-address-text">请选择收货地址</text>
<text class="no-address-arrow"></text>
</view>
</view>
<!-- 商品列表 (按店铺分组) -->
<view class="products-section">
<!-- 调试信息 -->
<view v-if="checkoutItems.length > 0" class="debug-info">
<text class="debug-text">共 {{ checkoutItems.length }} 件商品</text>
</view>
<view v-if="shopGroups.length > 0">
<view v-for="group in shopGroups" :key="group.shopId" class="shop-group">
<view class="shop-header">
<text class="shop-icon">🏪</text>
<text class="shop-name">{{ group.shopName }}</text>
</view>
<view v-for="item in group.items" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
<!-- 店铺小计 -->
<view class="shop-subtotal">
<text class="subtotal-label">配送方式</text>
<text class="subtotal-value">快递 免邮</text>
</view>
<view class="shop-subtotal">
<text class="subtotal-text">小计: </text>
<text class="subtotal-price">¥{{ getGroupTotal(group) }}</text>
</view>
</view>
</view>
<view v-else class="no-products">
<text class="no-products-text">暂无商品信息</text>
</view>
</view>
<!-- 配送方式 -->
<view class="delivery-section">
<text class="section-title">配送方式</text>
<view class="delivery-options">
<view v-for="option in deliveryOptions"
:key="option.id"
:class="['delivery-option', { selected: selectedDelivery === option.id }]"
@click="selectDelivery(option)">
<text class="option-name">{{ option.name }}</text>
<text class="option-price">¥{{ option.price }}</text>
<text v-if="selectedDelivery === option.id" class="option-selected">✓</text>
</view>
</view>
</view>
<!-- 优惠券 -->
<view class="coupon-section" @click="selectCoupon">
<text class="section-title">优惠券</text>
<view class="coupon-info">
<text v-if="selectedCoupon" class="coupon-selected">{{ selectedCoupon.template?.name || '优惠券' }}</text>
<text v-else class="coupon-placeholder">选择优惠券</text>
<text class="coupon-arrow"></text>
</view>
</view>
<!-- 买家留言 -->
<view class="remark-section">
<text class="section-title">买家留言</text>
<textarea class="remark-input"
v-model="remark"
placeholder="选填,请先和商家协商一致"
maxlength="100" />
</view>
<!-- 价格明细 -->
<view class="price-section">
<text class="section-title">价格明细</text>
<view class="price-detail">
<view class="price-row">
<text class="price-label">商品总价</text>
<text class="price-value">¥{{ totalAmount.toFixed(2) }}</text>
</view>
<view class="price-row">
<text class="price-label">运费</text>
<text class="price-value">+¥{{ deliveryFee.toFixed(2) }}</text>
</view>
<view v-if="discountAmount > 0" class="price-row">
<text class="price-label">优惠减免</text>
<text class="price-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
</view>
<view class="price-row total">
<text class="price-label">应付金额</text>
<text class="price-value total-price">¥{{ actualAmount.toFixed(2) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部结算栏 -->
<view class="bottom-bar">
<view class="price-summary">
<text class="summary-label">合计:</text>
<text class="summary-price">¥{{ actualAmount.toFixed(2) }}</text>
</view>
<button class="submit-btn" @click="submitOrder">提交订单</button>
</view>
<!-- 地址选择弹窗 -->
<view v-if="showAddressPopup" class="address-popup-mask" @click="showAddressPopup = false">
<view class="address-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">选择收货地址</text>
<text class="popup-close" @click="showAddressPopup = false">×</text>
</view>
<scroll-view class="address-list-container" scroll-y>
<!-- 登录提示 -->
<view v-if="!isLoggedIn" class="login-prompt" @click="goToLogin">
<text class="login-prompt-icon">🔒</text>
<text class="login-prompt-text">您尚未登录,点击登录以同步服务器地址</text>
<text class="login-prompt-arrow"></text>
</view>
<!-- 地址列表 -->
<view v-if="isLoggedIn">
<view v-if="addressList.length > 0">
<view v-for="address in addressList" :key="address.id"
class="popup-address-item" @click="handleSelectAddress(address)">
<view class="popup-address-header">
<text class="popup-address-name">{{ address.recipient_name }}</text>
<text class="popup-address-phone">{{ address.phone }}</text>
<view v-if="address.is_default" class="popup-default-tag">
<text class="popup-tag-text">默认</text>
</view>
</view>
<text class="popup-address-detail">{{ getFullAddress(address) }}</text>
<view v-if="selectedAddress && selectedAddress.id === address.id" class="popup-selected-indicator">
<text>✓</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="popup-empty-address">
<text class="popup-empty-icon">📍</text>
<text class="popup-empty-text">暂无收货地址</text>
</view>
</view>
<!-- 未登录时的本地地址展示 -->
<view v-if="!isLoggedIn && addressList.length > 0">
<text class="local-address-title">本地地址(未同步)</text>
<view v-for="address in addressList" :key="address.id"
class="popup-address-item" @click="handleSelectAddress(address)">
<view class="popup-address-header">
<text class="popup-address-name">{{ address.recipient_name }}</text>
<text class="popup-address-phone">{{ address.phone }}</text>
<view v-if="address.is_default" class="popup-default-tag">
<text class="popup-tag-text">默认</text>
</view>
</view>
<text class="popup-address-detail">{{ getFullAddress(address) }}</text>
<view v-if="selectedAddress && selectedAddress.id === address.id" class="popup-selected-indicator">
<text>✓</text>
</view>
</view>
</view>
<!-- 完全无地址状态 -->
<view v-if="isLoggedIn && addressList.length === 0" class="popup-empty-address">
<text class="popup-empty-icon">📍</text>
<text class="popup-empty-text">暂无收货地址</text>
</view>
</scroll-view>
<!-- 新建地址按钮 -->
<view class="popup-add-address-btn" @click="handleAddNewAddress">
<text class="popup-btn-icon">+</text>
<text class="popup-btn-text">新建收货地址</text>
</view>
</view>
</view>
<!-- 新建地址表单弹窗 -->
<view v-if="showNewAddressForm" class="address-form-mask" @click="cancelNewAddress">
<view class="address-form-popup" @click.stop>
<view class="form-header">
<text class="form-title">新建收货地址</text>
<text class="form-close" @click="cancelNewAddress">×</text>
</view>
<scroll-view class="form-content" scroll-y>
<view class="form-item">
<text class="form-label">收货人</text>
<input class="form-input" v-model="newAddress.recipient_name"
placeholder="请输入收货人姓名" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="newAddress.phone"
placeholder="请输入手机号码" type="number" />
</view>
<view class="form-item">
<text class="form-label">智能填写地址</text>
<textarea class="form-textarea smart-address-input"
v-model="smartAddressInput"
placeholder="粘贴如北京市朝阳区三里屯SOHO A座 张三 13800138000"
@input="parseSmartAddress"
maxlength="200" />
<text class="smart-tip">自动识别:地址+姓名+电话(支持粘贴文本)</text>
</view>
<view class="form-item">
<text class="form-label">所在地区</text>
<view class="region-inputs">
<input class="form-input region-input" v-model="newAddress.province"
placeholder="省" readonly />
<input class="form-input region-input" v-model="newAddress.city"
placeholder="市" readonly />
<input class="form-input region-input" v-model="newAddress.district"
placeholder="区/县" readonly />
</view>
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<textarea class="form-textarea" v-model="newAddress.detail"
placeholder="街道、小区、楼栋、门牌号等"
maxlength="100" />
</view>
<view class="form-item checkbox-item">
<view class="checkbox-wrapper" @click="newAddress.is_default = !newAddress.is_default">
<view :class="['checkbox', { checked: newAddress.is_default }]">
<text v-if="newAddress.is_default" class="checkbox-check">✓</text>
</view>
<text class="checkbox-label">设为默认地址</text>
</view>
</view>
</scroll-view>
<view class="form-buttons">
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
</view>
</view>
</view>
<!-- 确认保存弹窗 -->
<view v-if="showSaveConfirm" class="confirm-popup-mask">
<view class="confirm-popup">
<view class="confirm-header">
<text class="confirm-title">保存地址</text>
</view>
<view class="confirm-content">
<text class="confirm-message">是否保存该地址用于下次使用?</text>
</view>
<view class="confirm-buttons">
<button class="confirm-btn cancel" @click="handleSaveConfirm(false)">仅本次</button>
<button class="confirm-btn confirm" @click="handleSaveConfirm(true)">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, watch, onUnmounted, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts'
type CheckoutItemType = {
id: string
product_id: string
sku_id: string
product_name: string
product_image: string
sku_specifications: any
price: number
quantity: number
shop_id?: string
shop_name?: string
}
type DeliveryOptionType = {
id: string
name: string
price: number
description: string
}
type UserCouponType = {
id: string
template: {
name: string
discount_value: number
min_order_amount: number
} | null
}
const checkoutItems = ref<Array<CheckoutItemType>>([])
const selectedAddress = ref<any>(null)
const deliveryOptions = ref<Array<DeliveryOptionType>>([
{ id: 'standard', name: '快递配送', price: 8.00, description: '1-3天送达' },
{ id: 'express', name: '加急配送', price: 15.00, description: '当天送达' }
])
const selectedDelivery = ref<string>('standard')
const selectedCoupon = ref<UserCouponType | null>(null)
const remark = ref<string>('')
const showAddressPopup = ref<boolean>(false)
const addressList = ref<Array<any>>([])
const newAddress = ref<any>({
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
})
const showNewAddressForm = ref<boolean>(false)
const showSaveConfirm = ref<boolean>(false)
const smartAddressInput = ref<string>('')
// 计算属性 - 修复价格同步问题
// 按店铺分组商品
const shopGroups = computed(() => {
const groups = new Map<string, any>()
checkoutItems.value.forEach(item => {
// 使用类型断言访问可能的额外属性
const rawItem = item as any
const shopId = rawItem.shop_id || 'unknown'
if (!groups.has(shopId)) {
groups.set(shopId, {
shopId: shopId,
shopName: rawItem.shop_name || '商城优选',
merchant_id: rawItem.merchant_id || rawItem.shop_id,
items: [] as any[]
})
}
groups.get(shopId).items.push(item)
})
return Array.from(groups.values())
})
const getGroupTotal = (group: any) => {
return group.items.reduce((sum: number, item: any) => {
return sum + (Number(item.price) * Number(item.quantity))
}, 0).toFixed(2)
}
const totalAmount = computed(() => {
console.log('计算商品总价checkoutItems:', checkoutItems.value)
if (!checkoutItems.value || checkoutItems.value.length === 0) {
console.log('商品列表为空返回0')
return 0
}
// 确保每个商品的价格和数量都是数字类型,并计算总和
const total = checkoutItems.value.reduce((sum, item) => {
// 确保item存在且包含必要的属性
if (!item) return sum
// 将价格转换为数字(确保是数字类型)
let price = 0
if (item.price !== undefined && item.price !== null) {
price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
}
// 将数量转换为数字
let quantity = 0
if (item.quantity !== undefined && item.quantity !== null) {
quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
}
// 验证转换后的数字是否有效
if (isNaN(price) || isNaN(quantity) || price <= 0 || quantity <= 0) {
console.warn('商品价格或数量无效:', item, 'price:', price, 'quantity:', quantity)
return sum
}
const itemTotal = price * quantity
console.log(`商品 ${item.product_name},单价: ${price},数量: ${quantity},小计: ${itemTotal}`)
return sum + itemTotal
}, 0)
console.log('商品总价计算结果:', total)
return total
})
const deliveryFee = computed(() => {
const option = deliveryOptions.value.find(opt => opt.id === selectedDelivery.value)
return option?.price || 0
})
const discountAmount = computed(() => {
if (!selectedCoupon.value || !selectedCoupon.value.template) return 0
const coupon = selectedCoupon.value.template
// 确保使用计算后的商品总价进行比较
if (totalAmount.value < coupon.min_order_amount) return 0
// 简单处理:假设都是满减券
return coupon.discount_value
})
const actualAmount = computed(() => {
// 确保所有值都是数字类型
const total = typeof totalAmount.value === 'number' ? totalAmount.value : 0
const delivery = typeof deliveryFee.value === 'number' ? deliveryFee.value : 0
const discount = typeof discountAmount.value === 'number' ? discountAmount.value : 0
// 正确计算:商品总价 + 运费 - 优惠减免
let amount = total + delivery - discount
// 金额必须大于等于0
return amount > 0 ? amount : 0
})
// 监听checkoutItems变化 - 调试用
watch(checkoutItems, (newItems) => {
console.log('checkoutItems变化了:', newItems)
console.log('商品总价计算:', totalAmount.value)
}, { deep: true })
// 页面加载时监听eventChannel
onLoad(() => {
let dataLoaded = false
// 优先检查Storage中是否有结算数据 (支持 buy_now 和 cart 两种模式)
const checkoutType = uni.getStorageSync('checkout_type')
if (checkoutType === 'buy_now' || checkoutType === 'cart') {
console.log(`检测到结算模式(${checkoutType})从Storage加载数据`)
const itemsStr = uni.getStorageSync('checkout_items')
if (itemsStr) {
try {
const items = JSON.parse(itemsStr as string)
console.log('从Storage加载的商品数据:', items)
if (items && Array.isArray(items) && items.length > 0) {
processCheckoutItems(items)
dataLoaded = true
}
} catch (e) {
console.error('解析结算数据失败', e)
}
}
}
// 如果没有从checkout_items加载到数据则尝试从通用购物车Storage加载 (回退方案)
if (!dataLoaded) {
console.log('未找到预结算数据,尝试从购物车本地存储加载')
loadFromLocalStorage()
} else {
// 如果已经加载了数据还需要单独加载地址因为loadFromLocalStorage通常会附带加载地址
loadDefaultAddress()
}
// 从上一页获取数据
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
console.log('接收到商品数据:', data)
let items = data.selectedItems || []
if (items.length > 0) {
processCheckoutItems(items)
}
loadDefaultAddress()
})
}
})
// 处理商品数据清洗
const processCheckoutItems = (items: any[]) => {
// 数据清洗:确保价格和数量是数字类型
if (items && items.length > 0) {
items = items.map((item: any) => {
// 确保价格是数字
let price = 0
if (item.price !== undefined && item.price !== null) {
price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
if (isNaN(price)) price = 0
}
// 确保数量是数字
let quantity = 1
if (item.quantity !== undefined && item.quantity !== null) {
quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
}
return {
id: item.id,
product_id: item.product_id || item.id,
sku_id: item.sku_id || item.id,
product_name: item.product_name || item.name || '',
product_image: item.product_image || item.image || '',
sku_specifications: item.sku_specifications || item.spec || {},
price: Number(price.toFixed(2)),
quantity: quantity
}
})
}
checkoutItems.value = items
// 调试:打印每个商品的价格
if (checkoutItems.value && checkoutItems.value.length > 0) {
console.log('清洗后商品价格明细:')
checkoutItems.value.forEach((item: any, index: number) => {
console.log(`商品${index}:`, item.product_name, '价格:', item.price, '类型:', typeof item.price, '数量:', item.quantity)
})
}
}
// 生命周期
onMounted(() => {
// 监听地址更新事件
uni.$on('addressUpdated', (updatedAddressList: any) => {
addressList.value = updatedAddressList
// 如果当前没有选中地址,尝试选择默认地址
if (!selectedAddress.value && addressList.value.length > 0) {
const defaultAddress = addressList.value.find(addr => addr.is_default)
if (defaultAddress) {
selectedAddress.value = defaultAddress
}
}
})
})
// 页面显示时触发
const onShow = () => {
console.log('checkout页面显示检查登录状态并重新加载地址')
// 检查用户登录状态
const userId = getCurrentUserId()
if (userId) {
console.log('用户已登录,重新加载地址数据')
// 重新加载默认地址和地址列表
loadDefaultAddress()
loadAddressList()
}
}
// 注册页面显示事件
uni.$on('checkoutPageShow', onShow)
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('addressUpdated')
uni.$off('checkoutPageShow')
// 离开页面时清除结算数据,防止下次进入时显示旧数据
uni.removeStorageSync('checkout_type')
uni.removeStorageSync('checkout_items')
})
// 从本地存储加载结算数据(例如从购物车进入)
const loadFromLocalStorage = () => {
// 尝试从本地存储获取购物车选中的商品
const cartData = uni.getStorageSync('cart')
if (cartData) {
try {
const cartItems = JSON.parse(cartData as string) as any[]
// 筛选选中的商品
const selectedCartItems = cartItems.filter(item => item.selected === true)
if (selectedCartItems.length > 0) {
// 转换为CheckoutItemType格式
const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => {
let quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
return {
id: item.id,
product_id: item.product_id || item.productId || item.id,
sku_id: item.sku_id || item.id,
product_name: item.name || '',
product_image: item.image || '',
sku_specifications: item.spec ? { spec: item.spec } : {},
price: Number(item.price) || 0,
quantity: quantity
}
})
// 再次经过process处理确保类型正确
processCheckoutItems(convertedItems)
}
} catch (e) {
console.error('解析购物车数据失败:', e)
}
}
loadDefaultAddress()
}
// 加载结算数据兼容旧版本现在主要在onLoad中处理
const loadCheckoutData = () => {
// 为了兼容性保留但主要逻辑已在onLoad中实现
loadFromLocalStorage()
}
// 加载默认地址
const loadDefaultAddress = async () => {
try {
// 首先检查用户是否登录
const currentUserId = getCurrentUserId()
console.log('loadDefaultAddress: 当前用户ID:', currentUserId)
// 如果用户已登录尝试从Supabase加载地址数据
if (currentUserId) {
console.log('loadDefaultAddress: 用户已登录从Supabase加载地址')
const supabaseAddresses = await supabaseService.getAddresses()
console.log('loadDefaultAddress: Supabase返回地址:', supabaseAddresses)
if (supabaseAddresses && supabaseAddresses.length > 0) {
// 查找默认地址
const defaultAddress = supabaseAddresses.find((addr: SupabaseUserAddress) => addr.is_default === true)
if (defaultAddress) {
// 转换地址格式以匹配selectedAddress的结构
selectedAddress.value = {
id: defaultAddress.id,
recipient_name: defaultAddress.recipient_name,
phone: defaultAddress.phone,
province: defaultAddress.province,
city: defaultAddress.city,
district: defaultAddress.district,
detail: defaultAddress.detail_address,
is_default: defaultAddress.is_default
}
console.log('loadDefaultAddress: 找到默认地址:', selectedAddress.value)
} else {
// 如果没有默认地址,使用第一个地址
const firstAddress = supabaseAddresses[0]
selectedAddress.value = {
id: firstAddress.id,
recipient_name: firstAddress.recipient_name,
phone: firstAddress.phone,
province: firstAddress.province,
city: firstAddress.city,
district: firstAddress.district,
detail: firstAddress.detail_address,
is_default: firstAddress.is_default
}
console.log('loadDefaultAddress: 使用第一个地址:', selectedAddress.value)
}
// 同时更新本地存储缓存
const localAddresses = supabaseAddresses.map((addr: SupabaseUserAddress) => ({
id: addr.id,
name: addr.recipient_name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail_address,
isDefault: addr.is_default
}))
uni.setStorageSync('addresses', JSON.stringify(localAddresses))
console.log('loadDefaultAddress: 地址已保存到本地存储')
} else {
console.log('loadDefaultAddress: Supabase未返回地址数据')
}
}
// 如果Supabase没有地址数据或用户未登录尝试从本地存储加载
if (!selectedAddress.value) {
console.log('loadDefaultAddress: 尝试从本地存储加载地址')
const storedAddresses = uni.getStorageSync('addresses')
console.log('loadDefaultAddress: 本地存储地址数据:', storedAddresses)
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
// 查找默认地址
const defaultAddress = addresses.find((addr: any) => addr.isDefault === true)
if (defaultAddress) {
selectedAddress.value = {
id: defaultAddress.id,
recipient_name: defaultAddress.name,
phone: defaultAddress.phone,
province: defaultAddress.province,
city: defaultAddress.city,
district: defaultAddress.district,
detail: defaultAddress.detail,
is_default: defaultAddress.isDefault
}
console.log('loadDefaultAddress: 从本地存储找到默认地址:', selectedAddress.value)
} else {
// 如果没有默认地址,使用第一个地址
const firstAddress = addresses[0]
selectedAddress.value = {
id: firstAddress.id,
recipient_name: firstAddress.name,
phone: firstAddress.phone,
province: firstAddress.province,
city: firstAddress.city,
district: firstAddress.district,
detail: firstAddress.detail,
is_default: firstAddress.isDefault
}
console.log('loadDefaultAddress: 从本地存储使用第一个地址:', selectedAddress.value)
}
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
}
}
}
// 如果仍然没有地址,使用模拟地址数据
if (!selectedAddress.value) {
console.log('loadDefaultAddress: 使用模拟地址数据')
// 模拟地址数据
const mockAddresses = [
{
id: 'addr_001',
name: '张三',
phone: '13800138001',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '建国路88号SOHO现代城A座1001',
isDefault: true
},
{
id: 'addr_002',
name: '李四',
phone: '13900139001',
province: '上海市',
city: '上海市',
district: '浦东新区',
detail: '陆家嘴环路1000号汇亚大厦20层',
isDefault: false
}
]
// 保存模拟地址到本地存储
uni.setStorageSync('addresses', JSON.stringify(mockAddresses))
console.log('loadDefaultAddress: 模拟地址已保存到本地存储')
// 使用第一个地址作为默认地址
selectedAddress.value = {
id: mockAddresses[0].id,
recipient_name: mockAddresses[0].name,
phone: mockAddresses[0].phone,
province: mockAddresses[0].province,
city: mockAddresses[0].city,
district: mockAddresses[0].district,
detail: mockAddresses[0].detail,
is_default: mockAddresses[0].isDefault
}
console.log('loadDefaultAddress: 使用模拟地址:', selectedAddress.value)
}
// 如果仍然没有地址selectedAddress.value将保持为null
// 用户可以在结算页面点击地址区域添加新地址
} catch (error) {
console.error('从Supabase加载默认地址失败:', error)
// 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
const defaultAddress = addresses.find((addr: any) => addr.isDefault === true)
if (defaultAddress) {
selectedAddress.value = {
id: defaultAddress.id,
recipient_name: defaultAddress.name,
phone: defaultAddress.phone,
province: defaultAddress.province,
city: defaultAddress.city,
district: defaultAddress.district,
detail: defaultAddress.detail,
is_default: defaultAddress.isDefault
}
} else {
const firstAddress = addresses[0]
selectedAddress.value = {
id: firstAddress.id,
recipient_name: firstAddress.name,
phone: firstAddress.phone,
province: firstAddress.province,
city: firstAddress.city,
district: firstAddress.district,
detail: firstAddress.detail,
is_default: firstAddress.isDefault
}
}
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
}
}
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
// 使用 SupabaseService 获取当前用户ID
const userId = supabaseService.getCurrentUserId()
console.log('getCurrentUserId: 从SupabaseService获取到用户ID:', userId)
return userId ?? ''
}
// 用户登录状态
const isLoggedIn = computed(() => {
const userId = getCurrentUserId()
return !!userId
})
// 获取完整地址
const getFullAddress = (address: any): string => {
return `${address.province}${address.city}${address.district}${address.detail}`
}
// 加载地址列表
const loadAddressList = async () => {
try {
// 首先检查用户是否登录
const currentUserId = getCurrentUserId()
console.log('loadAddressList: 当前用户ID:', currentUserId)
// 如果用户已登录尝试从Supabase加载地址数据
if (currentUserId) {
console.log('loadAddressList: 用户已登录从Supabase加载地址')
const supabaseAddresses = await supabaseService.getAddresses()
console.log('loadAddressList: Supabase返回地址:', supabaseAddresses)
if (supabaseAddresses && supabaseAddresses.length > 0) {
// 转换地址格式以匹配addressList的结构
addressList.value = supabaseAddresses.map((addr: SupabaseUserAddress) => ({
id: addr.id,
recipient_name: addr.recipient_name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail_address,
is_default: addr.is_default
}))
console.log('loadAddressList: 从Supabase加载地址成功数量:', addressList.value.length)
// 同时更新本地存储缓存
const localAddresses = supabaseAddresses.map((addr: SupabaseUserAddress) => ({
id: addr.id,
name: addr.recipient_name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail_address,
isDefault: addr.is_default
}))
uni.setStorageSync('addresses', JSON.stringify(localAddresses))
console.log('loadAddressList: 地址已保存到本地存储')
} else {
console.log('loadAddressList: Supabase未返回地址数据')
}
}
// 如果Supabase没有地址数据或用户未登录尝试从本地存储加载
if (!addressList.value || addressList.value.length === 0) {
console.log('loadAddressList: 尝试从本地存储加载地址')
const storedAddresses = uni.getStorageSync('addresses')
console.log('loadAddressList: 本地存储地址数据:', storedAddresses)
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
// 转换地址格式以匹配addressList的结构
addressList.value = addresses.map((addr: any) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
}))
console.log('loadAddressList: 从本地存储加载地址成功,数量:', addressList.value.length)
} else {
addressList.value = []
console.log('loadAddressList: 本地存储地址为空数组')
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
addressList.value = []
}
} else {
addressList.value = []
console.log('loadAddressList: 本地存储没有地址数据')
}
}
// 如果仍然没有地址使用模拟地址数据与loadDefaultAddress保持一致
if (!addressList.value || addressList.value.length === 0) {
console.log('loadAddressList: 使用模拟地址数据')
// 模拟地址数据与loadDefaultAddress中保持一致
const mockAddresses = [
{
id: 'addr_001',
name: '张三',
phone: '13800138001',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '建国路88号SOHO现代城A座1001',
isDefault: true
},
{
id: 'addr_002',
name: '李四',
phone: '13900139001',
province: '上海市',
city: '上海市',
district: '浦东新区',
detail: '陆家嘴环路1000号汇亚大厦20层',
isDefault: false
}
]
// 保存模拟地址到本地存储
uni.setStorageSync('addresses', JSON.stringify(mockAddresses))
console.log('loadAddressList: 模拟地址已保存到本地存储')
// 转换为checkout页面格式
addressList.value = mockAddresses.map((addr: any) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
}))
console.log('loadAddressList: 模拟地址已加载到地址列表,数量:', addressList.value.length)
}
console.log('loadAddressList: 最终地址列表:', addressList.value)
} catch (error) {
console.error('从Supabase加载地址列表失败:', error)
// 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
// 转换地址格式以匹配addressList的结构
addressList.value = addresses.map((addr: any) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
}))
} else {
addressList.value = []
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
addressList.value = []
}
} else {
addressList.value = []
}
}
}
// 选择地址
const handleSelectAddress = (address: any) => {
selectedAddress.value = address
showAddressPopup.value = false
}
// 新建地址
const handleAddNewAddress = () => {
showNewAddressForm.value = true
}
// 保存新地址
const saveNewAddress = async () => {
// 验证表单
if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
const userId = getCurrentUserId()
// if (!userId) return // 允许未登录用户保存地址用于演示
// 触发保存确认弹窗
showSaveConfirm.value = true
}
// 处理保存确认
const handleSaveConfirm = async (save: boolean) => {
showSaveConfirm.value = false
const newAddressData = {
id: `addr_${Date.now()}`,
name: newAddress.value.recipient_name,
phone: newAddress.value.phone,
province: newAddress.value.province,
city: newAddress.value.city,
district: newAddress.value.district,
detail: newAddress.value.detail,
isDefault: newAddress.value.is_default,
label: ''
}
if (save) {
// 从本地存储加载现有地址
const storedAddresses = uni.getStorageSync('addresses')
let addresses: any[] = []
if (storedAddresses) {
try {
addresses = JSON.parse(storedAddresses as string) as any[]
} catch (e) {
console.error('解析地址数据失败:', e)
addresses = []
}
}
// 如果是默认地址,取消其他默认地址
if (newAddressData.isDefault) {
addresses.forEach(addr => {
addr.isDefault = false
})
}
// 如果是第一个地址且未设置默认,则自动设为默认
if (addresses.length === 0 && !newAddressData.isDefault) {
newAddressData.isDefault = true
}
// 添加到地址列表
addresses.unshift(newAddressData)
// 保存到本地存储
uni.setStorageSync('addresses', JSON.stringify(addresses))
// 发布地址更新事件让address-list页面也能获取到
uni.$emit('addressUpdated', addresses.map((addr) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
})))
}
// 转换为checkout页面格式并添加到当前地址列表
const checkoutFormatAddress = {
id: newAddressData.id,
recipient_name: newAddressData.name,
phone: newAddressData.phone,
province: newAddressData.province,
city: newAddressData.city,
district: newAddressData.district,
detail: newAddressData.detail,
is_default: newAddressData.isDefault
}
// 如果是默认地址,取消其他默认地址
if (checkoutFormatAddress.is_default) {
addressList.value.forEach(addr => {
addr.is_default = false
})
}
// 添加到当前地址列表
addressList.value.unshift(checkoutFormatAddress)
// 如果选择了保存address-list 已通过事件收到更新;未保存则不影响地址簿
// 如果保存的是默认地址,或者当前没有选中地址,则选中这个新地址
if (checkoutFormatAddress.is_default || !selectedAddress.value) {
selectedAddress.value = checkoutFormatAddress
}
// 重置表单
newAddress.value = {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
}
smartAddressInput.value = ''
showNewAddressForm.value = false
uni.showToast({
title: '地址保存成功',
icon: 'success'
})
}
// 解析智能地址
const parseSmartAddress = () => {
const input = smartAddressInput.value.trim()
if (!input) return
// 重置表单
newAddress.value.recipient_name = ''
newAddress.value.phone = ''
newAddress.value.province = ''
newAddress.value.city = ''
newAddress.value.district = ''
newAddress.value.detail = ''
// 尝试匹配手机号码11位数字
const phoneRegex = /(1[3-9]\d{9})/g
const phoneMatches = input.match(phoneRegex)
if (phoneMatches && phoneMatches.length > 0) {
newAddress.value.phone = phoneMatches[0]
}
// 尝试匹配收件人姓名中文姓名2-4个汉字
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
const nameMatches = input.match(nameRegex)
if (nameMatches && nameMatches.length > 0) {
// 取第一个匹配的中文姓名作为收件人
newAddress.value.recipient_name = nameMatches[0]
}
// 提取地址部分(移除姓名和手机号)
let addressText = input
if (newAddress.value.recipient_name) {
addressText = addressText.replace(newAddress.value.recipient_name, '')
}
if (newAddress.value.phone) {
addressText = addressText.replace(newAddress.value.phone, '')
}
// 清理地址文本(移除多余的空格和标点)
addressText = addressText.replace(/[,;\s]+/g, ' ').trim()
// 地址解析逻辑
const patterns = [
// 匹配格式:省市区详细地址
/^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/,
// 匹配格式:省市详细地址
/^(.*?省)?(.*?市)?(.*)$/
]
for (const pattern of patterns) {
const match = addressText.match(pattern)
if (match) {
const [, province, city, district, detail] = match
if (province) newAddress.value.province = province.replace('省', '').trim()
if (city) newAddress.value.city = city.replace('市', '').trim()
if (district) newAddress.value.district = district.trim()
if (detail) newAddress.value.detail = detail.trim()
// 如果详细地址为空,但还有剩余内容,则作为详细地址
if (!newAddress.value.detail && district && detail) {
newAddress.value.detail = detail.trim()
}
break
}
}
// 如果没有匹配到模式,尝试简单分割
if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) {
// 尝试按常见分隔符分割
const parts = addressText.split(/[省市县区]/)
if (parts.length >= 2) {
newAddress.value.province = parts[0] || ''
newAddress.value.city = parts[1] || ''
newAddress.value.detail = parts.slice(2).join('').trim() || addressText
} else {
newAddress.value.detail = addressText
}
}
// 如果地址部分为空,但还有剩余文本,则作为详细地址
if (!newAddress.value.detail && addressText.trim()) {
newAddress.value.detail = addressText.trim()
}
}
// 取消新建地址
const cancelNewAddress = () => {
showNewAddressForm.value = false
newAddress.value = {
recipient_name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
is_default: false
}
smartAddressInput.value = ''
}
// 获取规格文本
const getSpecText = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`)
.join('; ')
}
return String(specs)
}
// 选择地址
const selectAddress = () => {
showAddressPopup.value = true
loadAddressList()
}
// 选择配送方式
const selectDelivery = (option: DeliveryOptionType) => {
selectedDelivery.value = option.id
}
// 选择优惠券
const selectCoupon = () => {
uni.navigateTo({
url: '/pages/mall/consumer/coupons',
success: (res) => {
res.eventChannel.emit('setSelectMode', { selectMode: true })
}
})
// 监听优惠券选择
uni.$on('couponSelected', (coupon: any) => {
selectedCoupon.value = coupon
uni.$off('couponSelected')
})
}
// 提交订单
const submitOrder = async () => {
// 校验地址
if (!selectedAddress.value) {
uni.showToast({
title: '请选择收货地址',
icon: 'none'
})
return
}
// 校验商品
if (checkoutItems.value.length === 0) {
uni.showToast({
title: '订单中没有商品',
icon: 'none'
})
return
}
uni.showLoading({ title: '提交中...' })
try {
const userId = supabaseService.getCurrentUserId()
if (!userId) {
uni.hideLoading()
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
// 准备按店铺分组数据
const groups = shopGroups.value.map((group: any): any => {
return {
merchant_id: group.merchant_id || group.shopId,
shopName: group.shopName,
items: group.items.map((item: any): any => ({
id: item.id, // 用于清理购物车
product_id: item.product_id,
sku_id: item.sku_id,
quantity: item.quantity,
price: item.price,
product_name: item.product_name,
product_image: item.product_image,
specifications: item.sku_specifications // 保持原始对象createOrder 会处理序列化
}))
}
})
// 调用 Supabase 服务创建多店铺订单
const result = await supabaseService.createOrdersByShop({
shipping_address: selectedAddress.value,
shopGroups: groups,
deliveryFee: deliveryFee.value,
discountAmount: discountAmount.value
})
uni.hideLoading()
if (result.success) {
// 清除结算商品
try {
uni.removeStorageSync('checkout_items')
uni.removeStorageSync('checkout_type')
} catch(e) {
console.error('清除结算商品失败', e)
}
const orderIds = result.orderIds
if (orderIds.length === 1) {
// 单个订单跳转支付
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderIds[0]}&amount=${actualAmount.value}`
})
} else {
// 多个订单跳转到订单列表
uni.showToast({
title: `成功创建${orderIds.length}个订单`,
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/mall/consumer/orders'
})
}, 1500)
}
} else {
throw new Error(result.error || '创建订单失败')
}
} catch (err: any) {
uni.hideLoading()
console.error('提交订单错误:', err)
uni.showToast({
title: err.message || '提交订单失败',
icon: 'none'
})
}
}
// 生成订单号
const generateOrderNo = (): string => {
const date = new Date()
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const random = Math.random().toString().slice(2, 8)
return `ORD${year}${month}${day}${random}`
}
// 清空购物车
const clearShoppingCart = async () => {
const userId = getCurrentUserId()
if (!userId) return
const productIds = checkoutItems.value.map(item => item.product_id)
try {
/* const { error } = await supa
.from('shopping_cart')
.delete()
.eq('user_id', userId)
.in('product_id', productIds)
if (error !== null) {
console.error('清空购物车失败:', error)
} */
} catch (err) {
console.error('清空购物车异常:', err)
}
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.checkout-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 删除未使用的顶部栏样式 */
.checkout-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #e5e5e5;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.checkout-content {
flex: 1;
}
.address-section {
background-color: #ffffff;
margin-bottom: 10px;
padding: 20px 15px;
display: flex;
align-items: center;
}
.address-info {
flex: 1;
}
.address-header {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.recipient {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-right: 15px;
}
.phone {
font-size: 14px;
color: #666666;
margin-right: 10px;
}
.default-tag {
background-color: #ff4757;
padding: 2px 8px;
border-radius: 10px;
}
.tag-text {
color: #ffffff;
font-size: 12px;
}
.address-detail {
font-size: 14px;
color: #333333;
line-height: 1.4;
}
.no-address {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.no-address-text {
font-size: 16px;
color: #999999;
}
.no-address-arrow {
color: #999999;
font-size: 18px;
}
.products-section {
background-color: #ffffff;
margin-bottom: 10px;
padding: 0 15px;
}
.debug-info {
background-color: #f8f9fa;
padding: 10px 15px;
border-bottom: 1px solid #e5e5e5;
margin-bottom: 10px;
}
.debug-text {
font-size: 12px;
color: #666;
display: block;
text-align: center;
}
.shop-group {
background-color: #fff;
margin: 10px 0;
border-radius: 12px;
padding: 10px;
}
.shop-header {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.shop-icon {
font-size: 18px;
margin-right: 8px;
}
.shop-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.shop-subtotal {
display: flex;
justify-content: flex-end; /* 右对齐 */
align-items: center;
padding-top: 10px;
margin-top: 5px;
border-top: 1px dashed #f0f0f0;
font-size: 14px;
}
.subtotal-label {
color: #666;
margin-right: 10px;
}
.subtotal-value {
color: #333;
}
.subtotal-text {
color: #333;
margin-right: 5px;
}
.subtotal-price {
color: #ff4757;
font-weight: bold;
font-size: 16px;
}
.product-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.product-item:last-child {
border-bottom: none;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 5px;
margin-right: 15px;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 14px;
color: #333333;
line-height: 1.4;
margin-bottom: 5px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.product-spec {
font-size: 12px;
color: #999999;
margin-bottom: 10px;
}
.product-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 16px;
color: #ff4757;
font-weight: bold;
}
.product-quantity {
font-size: 14px;
color: #666666;
}
.delivery-section,
.coupon-section,
.remark-section,
.price-section {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.delivery-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.delivery-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.delivery-option.selected {
border-color: #007aff;
background-color: #f0f8ff;
}
.option-name {
font-size: 14px;
color: #333333;
}
.option-price {
font-size: 14px;
color: #ff4757;
font-weight: bold;
}
.option-selected {
color: #007aff;
font-size: 16px;
}
.coupon-info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.coupon-selected {
font-size: 14px;
color: #007aff;
}
.coupon-placeholder {
font-size: 14px;
color: #999999;
}
.coupon-arrow {
color: #999999;
font-size: 16px;
}
.remark-input {
width: 100%;
min-height: 40px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
color: #333333;
}
.price-detail {
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.price-row.total {
border-top: 1px solid #e5e5e5;
margin-top: 8px;
padding-top: 15px;
}
.price-label {
font-size: 14px;
color: #666666;
}
.price-value {
font-size: 14px;
color: #333333;
}
.price-value.discount {
color: #4caf50;
}
.price-value.total-price {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.bottom-bar {
background-color: #ffffff;
padding: 15px;
border-top: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.price-summary {
display: flex;
align-items: baseline;
}
.summary-label {
font-size: 14px;
color: #333333;
margin-right: 5px;
}
.summary-price {
font-size: 20px;
color: #ff4757;
font-weight: bold;
}
.submit-btn {
background-color: #007aff;
color: #ffffff;
padding: 0 40px;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
/* 地址选择弹窗样式 */
.address-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 9998;
}
.address-popup {
background-color: #ffffff;
width: 100%;
max-height: 70vh;
border-radius: 20px 20px 0 0;
display: flex;
flex-direction: column;
}
.popup-header {
padding: 20px 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.popup-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.address-list-container {
flex: 1;
padding: 15px;
max-height: 50vh;
}
.popup-address-item {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
position: relative;
}
.popup-address-item:last-child {
margin-bottom: 0;
}
.popup-address-header {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.popup-address-name {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-right: 15px;
}
.popup-address-phone {
font-size: 14px;
color: #666666;
margin-right: 10px;
}
.popup-default-tag {
background-color: #ff4757;
padding: 2px 8px;
border-radius: 10px;
}
.popup-tag-text {
color: #ffffff;
font-size: 12px;
}
.popup-address-detail {
font-size: 14px;
color: #333333;
line-height: 1.4;
display: block;
}
.popup-selected-indicator {
position: absolute;
top: 15px;
right: 15px;
color: #007aff;
font-size: 16px;
}
.popup-empty-address {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.popup-empty-icon {
font-size: 60px;
margin-bottom: 15px;
}
.popup-empty-text {
font-size: 16px;
color: #999999;
}
.popup-add-address-btn {
background-color: #007aff;
margin: 15px;
padding: 15px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.popup-btn-icon {
color: #ffffff;
font-size: 20px;
margin-right: 10px;
}
.popup-btn-text {
color: #ffffff;
font-size: 16px;
font-weight: bold;
}
/* 新建地址表单弹窗样式 */
.address-form-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.address-form-popup {
background-color: #ffffff;
width: 90%;
max-width: 500px;
max-height: 80vh;
border-radius: 12px;
display: flex;
flex-direction: column;
}
.form-header {
padding: 20px 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
}
.form-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.form-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.form-content {
flex: 1;
padding: 15px;
max-height: 50vh;
}
.form-item {
margin-bottom: 20px;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 14px;
color: #333333;
margin-bottom: 8px;
display: block;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
color: #333333;
box-sizing: border-box;
}
.form-input[readonly] {
background-color: #f9f9f9;
color: #666666;
}
.region-inputs {
display: flex;
gap: 10px;
}
.region-input {
flex: 1;
}
.form-textarea {
width: 100%;
min-height: 80px;
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
color: #333333;
box-sizing: border-box;
}
.smart-address-input {
min-height: 60px;
}
.smart-tip {
font-size: 12px;
color: #999999;
margin-top: 5px;
display: block;
}
.checkbox-item {
margin-top: 20px;
}
.checkbox-wrapper {
display: flex;
align-items: center;
}
.checkbox {
width: 20px;
height: 20px;
border: 1px solid #e5e5e5;
border-radius: 4px;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.checkbox.checked {
background-color: #007aff;
border-color: #007aff;
}
.checkbox-check {
color: #ffffff;
font-size: 14px;
}
.checkbox-label {
font-size: 14px;
color: #333333;
}
.form-buttons {
display: flex;
padding: 15px;
border-top: 1px solid #e5e5e5;
gap: 10px;
}
.form-cancel-btn {
flex: 1;
background-color: #f5f5f5;
color: #333333;
padding: 12px;
border-radius: 8px;
font-size: 16px;
border: none;
}
.form-submit-btn {
flex: 1;
background-color: #007aff;
color: #ffffff;
padding: 12px;
border-radius: 8px;
font-size: 16px;
border: none;
}
/* 确认弹窗样式 */
.confirm-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10002;
}
.confirm-popup {
background-color: #ffffff;
width: 80%;
max-width: 320px;
border-radius: 12px;
overflow: hidden;
}
.confirm-header {
padding: 20px 0 10px;
text-align: center;
}
.confirm-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.confirm-content {
padding: 0 20px 20px;
text-align: center;
}
.confirm-message {
font-size: 16px;
color: #666666;
}
.confirm-buttons {
display: flex;
border-top: 1px solid #e5e5e5;
}
.confirm-btn {
flex: 1;
height: 50px;
line-height: 50px;
text-align: center;
font-size: 16px;
background-color: #ffffff;
border: none;
border-radius: 0;
}
.confirm-btn::after {
border: none;
}
.confirm-btn.cancel {
color: #666666;
border-right: 1px solid #e5e5e5;
}
.confirm-btn.confirm {
color: #007aff;
font-weight: bold;
}
</style>