1717 lines
40 KiB
Plaintext
1717 lines
40 KiB
Plaintext
<!-- 结算页面 -->
|
||
<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 }}件商品,总价计算:{{ totalAmount }}</text>
|
||
</view>
|
||
<view v-if="checkoutItems.length > 0">
|
||
<view v-for="item in checkoutItems" :key="item.id" class="product-item">
|
||
<image class="product-image" :src="item.product_image" />
|
||
<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>
|
||
<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="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>
|
||
</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'
|
||
|
||
type CheckoutItemType = {
|
||
id: string
|
||
product_id: string
|
||
sku_id: string
|
||
product_name: string
|
||
product_image: string
|
||
sku_specifications: any
|
||
price: number
|
||
quantity: number
|
||
}
|
||
|
||
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 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(() => {
|
||
// 优先检查Storage中是否有"立即购买"的数据
|
||
const checkoutType = uni.getStorageSync('checkout_type')
|
||
if (checkoutType === 'buy_now') {
|
||
console.log('检测到立即购买模式,从Storage加载数据')
|
||
const itemsStr = uni.getStorageSync('checkout_items')
|
||
if (itemsStr) {
|
||
try {
|
||
const items = JSON.parse(itemsStr as string)
|
||
console.log('从Storage加载的商品数据:', items)
|
||
processCheckoutItems(items)
|
||
|
||
// 清除Storage,避免污染下次进入(刷新页面时可能需要保留?暂时不清除,或者在离开页面时清除)
|
||
// uni.removeStorageSync('checkout_type')
|
||
// uni.removeStorageSync('checkout_items')
|
||
loadDefaultAddress()
|
||
return // 成功加载,直接返回
|
||
} catch (e) {
|
||
console.error('解析立即购买数据失败', e)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从上一页获取数据
|
||
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
|
||
if (eventChannel) {
|
||
eventChannel.on('acceptData', (data: any) => {
|
||
console.log('接收到商品数据:', data)
|
||
let items = data.selectedItems || []
|
||
processCheckoutItems(items)
|
||
loadDefaultAddress()
|
||
})
|
||
} else {
|
||
// 如果没有eventChannel,尝试从本地存储加载(例如从购物车进入)
|
||
loadFromLocalStorage()
|
||
}
|
||
})
|
||
|
||
// 处理商品数据清洗
|
||
const processCheckoutItems = (items: any[]) => {
|
||
// 数据清洗:确保价格和数量是数字类型
|
||
if (items && items.length > 0) {
|
||
items = items.map((item: any) => {
|
||
// 确保价格是数字
|
||
let price = item.price
|
||
if (price !== undefined && price !== null) {
|
||
price = typeof price === 'string' ? parseFloat(price) : Number(item.price)
|
||
if (isNaN(price)) price = 0
|
||
} else {
|
||
price = 0
|
||
}
|
||
|
||
// 确保数量是数字
|
||
let quantity = item.quantity
|
||
if (quantity !== undefined && quantity !== null) {
|
||
quantity = typeof quantity === 'string' ? parseInt(quantity) : Number(item.quantity)
|
||
if (isNaN(quantity) || quantity < 1) quantity = 1
|
||
} else {
|
||
quantity = 1
|
||
}
|
||
|
||
return {
|
||
...item,
|
||
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
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// 组件卸载时移除事件监听
|
||
onUnmounted(() => {
|
||
uni.$off('addressUpdated')
|
||
})
|
||
|
||
// 从本地存储加载结算数据(例如从购物车进入)
|
||
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 => ({
|
||
id: item.id,
|
||
product_id: item.productId,
|
||
sku_id: item.id, // 购物车中item.id就是SKU ID
|
||
product_name: item.name,
|
||
product_image: item.image,
|
||
sku_specifications: item.spec ? { spec: item.spec } : {},
|
||
price: item.price,
|
||
quantity: item.quantity
|
||
}))
|
||
checkoutItems.value = convertedItems
|
||
}
|
||
} catch (e) {
|
||
console.error('解析购物车数据失败:', e)
|
||
}
|
||
}
|
||
loadDefaultAddress()
|
||
}
|
||
|
||
// 加载结算数据(兼容旧版本,现在主要在onLoad中处理)
|
||
const loadCheckoutData = () => {
|
||
// 为了兼容性保留,但主要逻辑已在onLoad中实现
|
||
loadFromLocalStorage()
|
||
}
|
||
|
||
// 加载默认地址
|
||
const loadDefaultAddress = async () => {
|
||
// 从本地存储加载地址数据
|
||
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的结构
|
||
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)
|
||
}
|
||
} else {
|
||
// 如果没有地址数据,尝试使用Mock数据初始化(为了演示效果)
|
||
const mockAddress = {
|
||
id: 'addr_mock_default',
|
||
name: '测试用户',
|
||
phone: '13800138000',
|
||
province: '北京市',
|
||
city: '北京市',
|
||
district: '朝阳区',
|
||
detail: '三里屯SOHO A座',
|
||
isDefault: true
|
||
}
|
||
uni.setStorageSync('addresses', JSON.stringify([mockAddress]))
|
||
selectedAddress.value = {
|
||
id: mockAddress.id,
|
||
recipient_name: mockAddress.name,
|
||
phone: mockAddress.phone,
|
||
province: mockAddress.province,
|
||
city: mockAddress.city,
|
||
district: mockAddress.district,
|
||
detail: mockAddress.detail,
|
||
is_default: mockAddress.isDefault
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
const getCurrentUserId = (): string => {
|
||
const userStore = uni.getStorageSync('userInfo')
|
||
return userStore?.id || ''
|
||
}
|
||
|
||
// 获取完整地址
|
||
const getFullAddress = (address: any): string => {
|
||
return `${address.province}${address.city}${address.district}${address.detail}`
|
||
}
|
||
|
||
// 加载地址列表
|
||
const loadAddressList = async () => {
|
||
// 从本地存储加载地址数据
|
||
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
|
||
}
|
||
|
||
// MOCK ORDER SUBMISSION
|
||
// 模拟创建成功
|
||
try {
|
||
const mockOrderId = `order_${Date.now()}`
|
||
|
||
// 创建订单对象
|
||
const newOrder = {
|
||
id: mockOrderId,
|
||
order_no: generateOrderNo(),
|
||
user_id: getCurrentUserId() || 'user_001',
|
||
merchant_id: checkoutItems.value[0]?.product_id || 'merchant_001', // 简化处理,取第一个商品的merchant
|
||
status: 1, // 待支付
|
||
total_amount: totalAmount.value,
|
||
discount_amount: discountAmount.value,
|
||
delivery_fee: deliveryFee.value,
|
||
actual_amount: actualAmount.value,
|
||
payment_method: 0,
|
||
payment_status: 0,
|
||
delivery_address: selectedAddress.value,
|
||
items: checkoutItems.value,
|
||
created_at: new Date().toISOString()
|
||
}
|
||
|
||
// 保存到本地存储
|
||
const storedOrders = uni.getStorageSync('orders')
|
||
let orders: any[] = []
|
||
if (storedOrders) {
|
||
try {
|
||
orders = JSON.parse(storedOrders as string) as any[]
|
||
} catch (e) {
|
||
console.error('解析订单数据失败', e)
|
||
}
|
||
}
|
||
orders.unshift(newOrder)
|
||
uni.setStorageSync('orders', JSON.stringify(orders))
|
||
|
||
uni.showLoading({ title: '提交中...' })
|
||
await new Promise(resolve => setTimeout(resolve, 500))
|
||
uni.hideLoading()
|
||
|
||
// 携带价格详情跳转
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
|
||
})
|
||
} catch (err) {
|
||
console.error('创建订单失败:', err)
|
||
uni.showToast({
|
||
title: '订单创建失败',
|
||
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;
|
||
}
|
||
|
||
.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>
|