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

1717 lines
40 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 }}件商品,总价计算:{{ 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>