1560 lines
54 KiB
Plaintext
1560 lines
54 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 }} 件商品</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">{{ formatSpecs(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 != null" class="coupon-selected">{{ selectedCoupon.template?.name ?? '已选择优惠券 (¥' + (selectedCoupon.template?.discount_value ?? 0) + ')' }}</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 == false" 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 !== null && 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 == false && 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 != null && 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 form-input-readonly" v-model="newAddress.province"
|
||
placeholder="省" readonly />
|
||
<input class="form-input region-input form-input-readonly" v-model="newAddress.city"
|
||
placeholder="市" readonly />
|
||
<input class="form-input region-input form-input-readonly" 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 ? false : true">
|
||
<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
|
||
merchant_id?: string
|
||
}
|
||
|
||
type DeliveryOptionType = {
|
||
id: string
|
||
name: string
|
||
price: number
|
||
description: string
|
||
}
|
||
|
||
type ShopGroupType = {
|
||
shopId: string
|
||
shopName: string
|
||
merchant_id: string
|
||
items: Array<CheckoutItemType>
|
||
}
|
||
|
||
type CouponTemplateType = {
|
||
name: string
|
||
discount_value: number
|
||
min_order_amount: number
|
||
}
|
||
|
||
type UserCouponType = {
|
||
id: string
|
||
template: CouponTemplateType | null
|
||
}
|
||
|
||
type AddressItem = {
|
||
id: string
|
||
recipient_name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
detail: string
|
||
is_default: boolean
|
||
}
|
||
|
||
type NewAddressData = {
|
||
id: string
|
||
name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
detail: string
|
||
isDefault: boolean
|
||
}
|
||
|
||
// 添加新地址表单类型定义
|
||
type NewAddressForm = {
|
||
recipient_name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
detail: string
|
||
is_default: boolean
|
||
}
|
||
|
||
type MockAddress = {
|
||
id: string
|
||
name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
detail: string
|
||
isDefault: boolean
|
||
}
|
||
|
||
// 添加对象 keys 获取函数
|
||
function getObjectKeys(obj: object): string[] {
|
||
const keys: string[] = []
|
||
// UTS 兼容的对象属性获取方式
|
||
const tempObj = obj as Record<string, any>
|
||
|
||
// 使用 try-catch 安全获取对象属性
|
||
try {
|
||
// 假设我们知道一些常见的属性名
|
||
const commonKeys = ['id', 'name', 'value', 'label', 'key', 'recipient_name', 'phone', 'province', 'city', 'district', 'detail', 'is_default']
|
||
for (let i = 0; i < commonKeys.length; i++) {
|
||
const key = commonKeys[i]
|
||
// 替换 hasOwnProperty 检查
|
||
if (tempObj[key] !== null) { // 移除对 undefined 的检查
|
||
keys.push(key)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 捕获异常,避免编译错误
|
||
}
|
||
|
||
return keys
|
||
}
|
||
|
||
const checkoutItems = ref<Array<CheckoutItemType>>([])
|
||
const selectedAddress = ref<AddressItem | null>(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<AddressItem>>([])
|
||
const newAddress = ref<NewAddressForm>({
|
||
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 toUTSJSONObject = (value: any): UTSJSONObject => {
|
||
if (value instanceof UTSJSONObject) return value as UTSJSONObject
|
||
return JSON.parse(JSON.stringify(value ?? {})) as UTSJSONObject
|
||
}
|
||
|
||
// 计算属性 - 修复价格同步问题
|
||
// 按店铺分组商品
|
||
const shopGroups = computed((): Array<ShopGroupType> => {
|
||
const groups: Array<ShopGroupType> = []
|
||
checkoutItems.value.forEach((item) => {
|
||
const shopId = item.shop_id ?? 'unknown'
|
||
let target: ShopGroupType | null = null
|
||
for (let i = 0; i < groups.length; i++) {
|
||
if (groups[i].shopId == shopId) {
|
||
target = groups[i]
|
||
break
|
||
}
|
||
}
|
||
if (target == null) {
|
||
target = {
|
||
shopId: shopId,
|
||
shopName: item.shop_name ?? '商城优选',
|
||
merchant_id: item.merchant_id ?? item.shop_id ?? '',
|
||
items: []
|
||
}
|
||
groups.push(target)
|
||
}
|
||
target.items.push(item)
|
||
})
|
||
return groups
|
||
})
|
||
|
||
const getGroupTotal = (group: ShopGroupType): string => {
|
||
let sum = 0
|
||
group.items.forEach((item) => {
|
||
const price = item.price
|
||
const quantity = item.quantity
|
||
if (isNaN(price) == false && isNaN(quantity) == false) {
|
||
sum += (price * quantity)
|
||
}
|
||
})
|
||
return sum.toFixed(2)
|
||
}
|
||
|
||
const totalAmount = computed(() => {
|
||
console.log('计算商品总价,checkoutItems:', checkoutItems.value)
|
||
if (checkoutItems.value.length == 0) {
|
||
console.log('商品列表为空,返回0')
|
||
return 0
|
||
}
|
||
|
||
// 确保每个商品的价格和数量都是数字类型,并计算总和
|
||
const total = checkoutItems.value.reduce((sum, item) => {
|
||
// 确保item存在且包含必要的属性
|
||
if (item == null) return sum
|
||
|
||
const price = item.price
|
||
const quantity = item.quantity
|
||
|
||
// 验证转换后的数字是否有效
|
||
if (isNaN(price) || isNaN(quantity) || price <= 0 || quantity <= 0) {
|
||
console.warn('商品价格或数量无效:', item, 'price:', price, 'quantity:', quantity)
|
||
return sum
|
||
}
|
||
|
||
const itemTotal = price * quantity
|
||
return sum + itemTotal
|
||
}, 0)
|
||
|
||
return total
|
||
})
|
||
|
||
const deliveryFee = computed(() => {
|
||
const option = deliveryOptions.value.find(opt => opt.id === selectedDelivery.value)
|
||
return option?.price ?? 0
|
||
})
|
||
|
||
const discountAmount = computed(() => {
|
||
const coupon = selectedCoupon.value?.template
|
||
if (coupon == null) return 0
|
||
// 确保使用计算后的商品总价进行比较 (should be min_order_amount)
|
||
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: Array<CheckoutItemType>) => {
|
||
console.log('checkoutItems变化了:', newItems)
|
||
console.log('商品总价计算:', totalAmount.value)
|
||
}, { deep: true })
|
||
|
||
// 处理商品数据清洗
|
||
const processCheckoutItems = (items: any[]) => {
|
||
// 数据清洗:确保价格和数量是数字类型
|
||
const converted: Array<CheckoutItemType> = []
|
||
if (items != null && items.length > 0) {
|
||
for (let i = 0; i < items.length; i++) {
|
||
const obj = toUTSJSONObject(items[i])
|
||
const id = obj.getString('id') ?? ''
|
||
const productId = obj.getString('product_id') ?? obj.getString('productId') ?? id
|
||
const skuId = obj.getString('sku_id') ?? obj.getString('skuId') ?? id
|
||
const productName = obj.getString('product_name') ?? obj.getString('name') ?? ''
|
||
const productImage = obj.getString('product_image') ?? obj.getString('image') ?? ''
|
||
|
||
let specs: any = {}
|
||
const skuSpecsAny = obj.get('sku_specifications')
|
||
if (skuSpecsAny != null) {
|
||
specs = skuSpecsAny
|
||
} else {
|
||
const specAny = obj.get('spec')
|
||
if (specAny != null) specs = ({ spec: specAny } as any)
|
||
}
|
||
|
||
let price = 0
|
||
const priceAny = obj.get('price')
|
||
if (priceAny != null) {
|
||
const parsed = parseFloat(priceAny.toString())
|
||
if (isNaN(parsed) == false) price = parsed
|
||
}
|
||
|
||
let quantity = 1
|
||
const quantityAny = obj.get('quantity')
|
||
if (quantityAny != null) {
|
||
const parsedQ = parseInt(quantityAny.toString())
|
||
if (isNaN(parsedQ) == false && parsedQ >= 1) quantity = parsedQ
|
||
}
|
||
|
||
const shopId = obj.getString('shop_id') ?? obj.getString('shopId') ?? 'unknown'
|
||
const shopName = obj.getString('shop_name') ?? obj.getString('shopName') ?? ''
|
||
const merchantId = obj.getString('merchant_id') ?? obj.getString('merchantId') ?? ''
|
||
|
||
converted.push({
|
||
id: id,
|
||
product_id: productId,
|
||
sku_id: skuId,
|
||
product_name: productName,
|
||
product_image: productImage,
|
||
sku_specifications: specs,
|
||
price: parseFloat(price.toFixed(2)),
|
||
quantity: quantity,
|
||
shop_id: shopId,
|
||
shop_name: shopName,
|
||
merchant_id: merchantId
|
||
} as CheckoutItemType)
|
||
}
|
||
}
|
||
checkoutItems.value = converted
|
||
// 调试:打印每个商品的价格
|
||
if (checkoutItems.value.length > 0) {
|
||
console.log('清洗后商品价格明细:')
|
||
checkoutItems.value.forEach((item: CheckoutItemType, index: number) => {
|
||
console.log(`商品${index}:`, item.product_name, '价格:', item.price, 'shop:', item.shop_id)
|
||
})
|
||
}
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
// 监听地址更新事件
|
||
uni.$on('addressUpdated', (updatedAddressList: Array<AddressItem>) => {
|
||
addressList.value = updatedAddressList
|
||
|
||
// 如果当前没有选中地址,尝试选择默认地址
|
||
if (selectedAddress.value == null && addressList.value.length > 0) {
|
||
let defaultAddress: AddressItem | null = null
|
||
for (let i = 0; i < addressList.value.length; i++) {
|
||
const addr = addressList.value[i]
|
||
if (addr.is_default) {
|
||
defaultAddress = addr
|
||
break
|
||
}
|
||
}
|
||
if (defaultAddress != null) selectedAddress.value = defaultAddress
|
||
}
|
||
})
|
||
})
|
||
|
||
// 组件卸载时移除事件监听
|
||
onUnmounted(() => {
|
||
uni.$off('addressUpdated')
|
||
uni.$off('checkoutPageShow')
|
||
// 离开页面时清除结算数据,防止下次进入时显示旧数据
|
||
uni.removeStorageSync('checkout_type')
|
||
uni.removeStorageSync('checkout_items')
|
||
})
|
||
|
||
// 加载默认地址
|
||
async function loadDefaultAddress(): Promise<void> {
|
||
try {
|
||
// 首先检查用户是否登录
|
||
const currentUserId = getCurrentUserId()
|
||
|
||
// 如果用户已登录,尝试从Supabase加载地址数据
|
||
if (currentUserId != '') {
|
||
const supabaseAddresses = await supabaseService.getAddresses()
|
||
|
||
if (supabaseAddresses != null && supabaseAddresses.length > 0) {
|
||
// 查找默认地址
|
||
const defaultAddress = supabaseAddresses.find((addr: SupabaseUserAddress) => addr.is_default === true)
|
||
if (defaultAddress != null) {
|
||
// 转换地址格式以匹配selectedAddress的结构
|
||
const addr: AddressItem = {
|
||
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
|
||
}
|
||
selectedAddress.value = addr
|
||
} else {
|
||
// 如果没有默认地址,使用第一个地址
|
||
const firstAddress = supabaseAddresses[0]
|
||
const addr: AddressItem = {
|
||
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
|
||
}
|
||
selectedAddress.value = addr
|
||
}
|
||
|
||
// 同时更新本地存储缓存
|
||
const localAddresses: any[] = []
|
||
for (let i = 0; i < supabaseAddresses.length; i++) {
|
||
const addr = supabaseAddresses[i]
|
||
localAddresses.push({
|
||
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))
|
||
}
|
||
}
|
||
|
||
// 如果Supabase没有地址数据或用户未登录,尝试从本地存储加载
|
||
if (selectedAddress.value == null) {
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
const storedAddressesStr = storedAddresses != null ? storedAddresses.toString() : ''
|
||
if (storedAddressesStr != '') {
|
||
try {
|
||
const addresses = JSON.parse(storedAddressesStr) as any[]
|
||
if (addresses != null && addresses.length > 0) {
|
||
let picked: UTSJSONObject | null = null
|
||
for (let i = 0; i < addresses.length; i++) {
|
||
const obj = toUTSJSONObject(addresses[i])
|
||
const isDef = obj.getBoolean('isDefault') ?? obj.getBoolean('is_default') ?? false
|
||
if (isDef) {
|
||
picked = obj
|
||
break
|
||
}
|
||
}
|
||
if (picked == null) picked = toUTSJSONObject(addresses[0])
|
||
|
||
const addr: AddressItem = {
|
||
id: picked.getString('id') ?? '',
|
||
recipient_name: picked.getString('recipient_name') ?? picked.getString('name') ?? '',
|
||
phone: picked.getString('phone') ?? '',
|
||
province: picked.getString('province') ?? '',
|
||
city: picked.getString('city') ?? '',
|
||
district: picked.getString('district') ?? '',
|
||
detail: picked.getString('detail') ?? picked.getString('detail_address') ?? '',
|
||
is_default: picked.getBoolean('isDefault') ?? picked.getBoolean('is_default') ?? false
|
||
}
|
||
selectedAddress.value = addr
|
||
}
|
||
} catch (err) {
|
||
console.error('解析本地地址数据失败:', err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果仍然没有地址,使用模拟地址数据
|
||
if (selectedAddress.value == null) {
|
||
// 模拟地址数据
|
||
const mockAddresses: MockAddress[] = [
|
||
{
|
||
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))
|
||
|
||
// 使用第一个地址作为默认地址
|
||
const first = mockAddresses[0]
|
||
const addr: AddressItem = {
|
||
id: first.id,
|
||
recipient_name: first.name,
|
||
phone: first.phone,
|
||
province: first.province,
|
||
city: first.city,
|
||
district: first.district,
|
||
detail: first.detail,
|
||
is_default: first.isDefault
|
||
}
|
||
selectedAddress.value = addr
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('加载地址失败:', error)
|
||
}
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
function getCurrentUserId(): string {
|
||
const userId = supabaseService.getCurrentUserId()
|
||
return userId ?? ''
|
||
}
|
||
|
||
// 用户登录状态
|
||
const isLoggedIn = computed(() => {
|
||
const userId = getCurrentUserId()
|
||
return userId != ''
|
||
})
|
||
|
||
// 获取完整地址
|
||
const getFullAddress = (address: AddressItem): string => {
|
||
return `${address.province}${address.city}${address.district}${address.detail}`
|
||
}
|
||
|
||
// 加载地址列表
|
||
async function loadAddressList(): Promise<void> {
|
||
console.log('[loadAddressList] 开始加载地址列表')
|
||
try {
|
||
const currentUserId = getCurrentUserId()
|
||
console.log('[loadAddressList] currentUserId:', currentUserId)
|
||
|
||
if (currentUserId != '') {
|
||
const supabaseAddresses = await supabaseService.getAddresses()
|
||
console.log('[loadAddressList] supabaseAddresses 数量:', supabaseAddresses != null ? supabaseAddresses.length : 0)
|
||
|
||
if (supabaseAddresses != null && supabaseAddresses.length > 0) {
|
||
const list: AddressItem[] = []
|
||
const localAddresses: any[] = []
|
||
for (let i = 0; i < supabaseAddresses.length; i++) {
|
||
const addr = supabaseAddresses[i]
|
||
console.log('[loadAddressList] 地址', i, ':', addr.recipient_name, addr.phone, addr.detail_address)
|
||
list.push({
|
||
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
|
||
})
|
||
localAddresses.push({
|
||
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
|
||
})
|
||
}
|
||
addressList.value = list
|
||
console.log('[loadAddressList] addressList.value 设置完成, 数量:', addressList.value.length)
|
||
uni.setStorageSync('addresses', JSON.stringify(localAddresses))
|
||
}
|
||
}
|
||
|
||
if (addressList.value.length == 0) {
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
const storedAddressesStr = storedAddresses != null ? storedAddresses.toString() : ''
|
||
if (storedAddressesStr != '') {
|
||
try {
|
||
const addresses = JSON.parse(storedAddressesStr) as any[]
|
||
if (addresses != null && addresses.length > 0) {
|
||
const list: AddressItem[] = []
|
||
for (let i = 0; i < addresses.length; i++) {
|
||
const obj = toUTSJSONObject(addresses[i])
|
||
list.push({
|
||
id: obj.getString('id') ?? '',
|
||
recipient_name: obj.getString('recipient_name') ?? obj.getString('name') ?? '',
|
||
phone: obj.getString('phone') ?? '',
|
||
province: obj.getString('province') ?? '',
|
||
city: obj.getString('city') ?? '',
|
||
district: obj.getString('district') ?? '',
|
||
detail: obj.getString('detail') ?? obj.getString('detail_address') ?? '',
|
||
is_default: obj.getBoolean('isDefault') ?? obj.getBoolean('is_default') ?? false
|
||
})
|
||
}
|
||
addressList.value = list
|
||
} else {
|
||
addressList.value = []
|
||
}
|
||
} catch (err) {
|
||
addressList.value = []
|
||
}
|
||
} else {
|
||
addressList.value = []
|
||
}
|
||
}
|
||
|
||
if (addressList.value.length == 0) {
|
||
const mockAddresses: MockAddress[] = [
|
||
{
|
||
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))
|
||
|
||
const list: AddressItem[] = []
|
||
for (let i = 0; i < mockAddresses.length; i++) {
|
||
const addr = mockAddresses[i]
|
||
list.push({
|
||
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
|
||
})
|
||
}
|
||
addressList.value = list
|
||
}
|
||
} catch (error) {
|
||
console.error('加载地址列表失败:', error)
|
||
}
|
||
}
|
||
|
||
// 从本地存储加载结算数据(例如从购物车进入)
|
||
function loadFromLocalStorage(): void {
|
||
const cartData = uni.getStorageSync('cart')
|
||
const cartDataStr = cartData != null ? cartData.toString() : ''
|
||
if (cartDataStr != '') {
|
||
try {
|
||
const cartItems = JSON.parse(cartDataStr) as any[]
|
||
const selectedCartItems: any[] = []
|
||
for (let i = 0; i < cartItems.length; i++) {
|
||
const obj = toUTSJSONObject(cartItems[i])
|
||
const selected = obj.getBoolean('selected') ?? false
|
||
if (selected) selectedCartItems.push(obj)
|
||
}
|
||
if (selectedCartItems.length > 0) {
|
||
processCheckoutItems(selectedCartItems)
|
||
}
|
||
} catch (e) {
|
||
console.error('解析购物车数据失败:', e)
|
||
}
|
||
}
|
||
loadDefaultAddress()
|
||
}
|
||
|
||
// 加载结算数据(兼容旧版本,现在主要在onLoad中处理)
|
||
function loadCheckoutData(): void {
|
||
loadFromLocalStorage()
|
||
}
|
||
|
||
onLoad((options: any) => {
|
||
let dataLoaded = false
|
||
const checkoutTypeAny = uni.getStorageSync('checkout_type')
|
||
const checkoutType = checkoutTypeAny != null ? checkoutTypeAny.toString() : ''
|
||
if (checkoutType == 'buy_now' || checkoutType == 'cart') {
|
||
console.log(`检测到结算模式(${checkoutType}),从Storage加载数据`)
|
||
const itemsStrAny = uni.getStorageSync('checkout_items')
|
||
const itemsStr = itemsStrAny != null ? itemsStrAny.toString() : ''
|
||
if (itemsStr != '') {
|
||
try {
|
||
const items = JSON.parse(itemsStr as string)
|
||
console.log('从Storage加载的商品数据:', items)
|
||
if (items != null && Array.isArray(items) && items.length > 0) {
|
||
processCheckoutItems(items)
|
||
dataLoaded = true
|
||
}
|
||
} catch (e) {
|
||
console.error('解析结算数据失败', e)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (dataLoaded == false) {
|
||
console.log('未找到预结算数据,尝试从购物车本地存储加载')
|
||
loadFromLocalStorage()
|
||
}
|
||
|
||
loadDefaultAddress()
|
||
loadAddressList()
|
||
})
|
||
|
||
// 页面显示时触发
|
||
function onShow(): void {
|
||
const userId = getCurrentUserId()
|
||
if (userId != '') {
|
||
loadDefaultAddress()
|
||
loadAddressList()
|
||
}
|
||
}
|
||
|
||
uni.$on('checkoutPageShow', onShow)
|
||
|
||
// 选择地址
|
||
const handleSelectAddress = (address: AddressItem) => {
|
||
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
|
||
}
|
||
// 触发保存确认弹窗
|
||
showSaveConfirm.value = true
|
||
}
|
||
|
||
// 处理保存确认
|
||
const handleSaveConfirm = async (save: boolean) => {
|
||
showSaveConfirm.value = false
|
||
|
||
const newAddressData: 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
|
||
}
|
||
|
||
if (save) {
|
||
const storedAddresses = uni.getStorageSync('addresses')
|
||
let addresses: any[] = []
|
||
const storedAddressesStr = storedAddresses != null ? storedAddresses.toString() : ''
|
||
if (storedAddressesStr != '') {
|
||
try {
|
||
addresses = JSON.parse(storedAddressesStr) as any[]
|
||
} catch (e) {
|
||
addresses = []
|
||
}
|
||
}
|
||
|
||
const normalized: any[] = []
|
||
for (let i = 0; i < addresses.length; i++) {
|
||
const obj = toUTSJSONObject(addresses[i])
|
||
const isDef = obj.getBoolean('isDefault') ?? obj.getBoolean('is_default') ?? false
|
||
normalized.push({
|
||
id: obj.getString('id') ?? '',
|
||
name: obj.getString('name') ?? obj.getString('recipient_name') ?? '',
|
||
phone: obj.getString('phone') ?? '',
|
||
province: obj.getString('province') ?? '',
|
||
city: obj.getString('city') ?? '',
|
||
district: obj.getString('district') ?? '',
|
||
detail: obj.getString('detail') ?? obj.getString('detail_address') ?? '',
|
||
isDefault: newAddressData.isDefault ? false : isDef as boolean,
|
||
label: obj.getString('label') ?? ''
|
||
})
|
||
}
|
||
|
||
if (normalized.length === 0 && newAddressData.isDefault == false) {
|
||
newAddressData.isDefault = true
|
||
}
|
||
|
||
normalized.unshift(newAddressData)
|
||
uni.setStorageSync('addresses', JSON.stringify(normalized))
|
||
|
||
const updatedList: AddressItem[] = []
|
||
for (let i = 0; i < normalized.length; i++) {
|
||
const obj = toUTSJSONObject(normalized[i])
|
||
updatedList.push({
|
||
id: obj.getString('id') ?? '',
|
||
recipient_name: obj.getString('recipient_name') ?? obj.getString('name') ?? '',
|
||
phone: obj.getString('phone') ?? '',
|
||
province: obj.getString('province') ?? '',
|
||
city: obj.getString('city') ?? '',
|
||
district: obj.getString('district') ?? '',
|
||
detail: obj.getString('detail') ?? obj.getString('detail_address') ?? '',
|
||
is_default: obj.getBoolean('isDefault') ?? obj.getBoolean('is_default') ?? false
|
||
})
|
||
}
|
||
uni.$emit('addressUpdated', updatedList)
|
||
}
|
||
|
||
const checkoutFormatAddress: AddressItem = {
|
||
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) {
|
||
for (let i = 0; i < addressList.value.length; i++) {
|
||
addressList.value[i].is_default = false
|
||
}
|
||
}
|
||
|
||
addressList.value.unshift(checkoutFormatAddress)
|
||
|
||
if (checkoutFormatAddress.is_default || selectedAddress.value == null) {
|
||
selectedAddress.value = checkoutFormatAddress
|
||
}
|
||
|
||
newAddress.value = {
|
||
recipient_name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
is_default: false
|
||
} as NewAddressForm;
|
||
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 = ''
|
||
|
||
const phoneRegex = /(1[3-9]\d{9})/g
|
||
const phoneMatches = input.match(phoneRegex)
|
||
if (phoneMatches != null && phoneMatches.length > 0) {
|
||
newAddress.value.phone = phoneMatches[0] ?? ''
|
||
}
|
||
|
||
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
|
||
const nameMatches = input.match(nameRegex)
|
||
if (nameMatches != null && 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 != null) {
|
||
const [, province, city, district, detail] = match
|
||
|
||
if (province != null) newAddress.value.province = province.replace('省', '').trim()
|
||
if (city != null) newAddress.value.city = city.replace('市', '').trim()
|
||
if (district != null) newAddress.value.district = district.trim()
|
||
if (detail != null) newAddress.value.detail = detail.trim()
|
||
|
||
if (newAddress.value.detail == '' && district != null && detail != null) {
|
||
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()
|
||
if (newAddress.value.detail == '') {
|
||
newAddress.value.detail = 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
|
||
} as NewAddressForm;
|
||
smartAddressInput.value = ''
|
||
}
|
||
|
||
// 获取规格文本
|
||
function formatSpecs(specs: any): string {
|
||
if (specs == null) return ''
|
||
|
||
try {
|
||
const specsStr = JSON.stringify(specs)
|
||
if (specsStr == '{}' || specsStr == '[]' || specsStr == '""' || specsStr == '') return ''
|
||
|
||
// 使用 Record 类型替代 UTSJSONObject 的迭代器方法
|
||
const specsObj = JSON.parse(specsStr) as Record<string, any>
|
||
|
||
const parts: string[] = []
|
||
// 遍历已知可能的规格键名
|
||
const possibleKeys = ['颜色', '尺寸', '规格', '型号', '版本', '材质', '款式', 'color', 'size', 'spec', 'version', 'style']
|
||
|
||
// 先尝试已知键名
|
||
for (let i = 0; i < possibleKeys.length; i++) {
|
||
const key = possibleKeys[i]
|
||
const value = specsObj[key]
|
||
if (value != null && value.toString() != '') {
|
||
parts.push(`${key}: ${value.toString()}`)
|
||
}
|
||
}
|
||
|
||
// 如果已知键名没找到,尝试遍历对象的所有属性
|
||
if (parts.length === 0) {
|
||
// 使用 JSON.stringify 后正则匹配键值对
|
||
const keyValueRegex = /"([^"]+)":\s*"([^"]+)"/g
|
||
let match: RegExpExecArray | null = null
|
||
while (true) {
|
||
match = keyValueRegex.exec(specsStr)
|
||
if (match == null) break
|
||
const key = match[1]
|
||
const value = match[2]
|
||
if (key != null && value != null && value != '') {
|
||
parts.push(`${key}: ${value}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (parts.length === 0) return ''
|
||
return parts.join('; ')
|
||
} catch (e) {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
// 选择配送方式
|
||
const selectDelivery = (option: DeliveryOptionType) => {
|
||
selectedDelivery.value = option.id
|
||
}
|
||
|
||
// 选择优惠券
|
||
const selectCoupon = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/coupons',
|
||
success: (res: any) => {
|
||
// 移除事件通道相关代码,避免使用不支持的 API
|
||
// 注释掉事件通道逻辑,因为当前环境不支持 createEventChannel
|
||
// const eventChannel = res.eventChannel || uni.createEventChannel()
|
||
// if (eventChannel && eventChannel.emit) {
|
||
// eventChannel.emit('setSelectMode', { selectMode: true })
|
||
// }
|
||
}
|
||
})
|
||
|
||
uni.$on('couponSelected', (coupon: any) => {
|
||
selectedCoupon.value = coupon as UserCouponType
|
||
uni.$off('couponSelected')
|
||
})
|
||
}
|
||
|
||
// 提交订单
|
||
const submitOrder = async () => {
|
||
if (selectedAddress.value == null) {
|
||
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 == null || userId == '') {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
console.log('[submitOrder] 开始创建订单, userId:', userId)
|
||
console.log('[submitOrder] shopGroups数量:', shopGroups.value.length)
|
||
|
||
const groups: any[] = []
|
||
for (let i = 0; i < shopGroups.value.length; i++) {
|
||
const group = shopGroups.value[i]
|
||
const items: any[] = []
|
||
for (let j = 0; j < group.items.length; j++) {
|
||
const item = group.items[j]
|
||
items.push({
|
||
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
|
||
})
|
||
}
|
||
groups.push({
|
||
merchant_id: (group.merchant_id != null && group.merchant_id != '') ? group.merchant_id : group.shopId,
|
||
shopId: group.shopId,
|
||
shopName: group.shopName,
|
||
items: items
|
||
})
|
||
}
|
||
|
||
console.log('[submitOrder] 准备传递的 groups 数量:', groups.length)
|
||
|
||
const result = await supabaseService.createOrdersByShop({
|
||
shipping_address: selectedAddress.value !== null ? toUTSJSONObject(selectedAddress.value!) : new UTSJSONObject(),
|
||
shopGroups: groups,
|
||
deliveryFee: deliveryFee.value,
|
||
discountAmount: discountAmount.value
|
||
})
|
||
|
||
uni.hideLoading()
|
||
|
||
console.log('[submitOrder] 创建结果 success:', result.success)
|
||
|
||
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 {
|
||
const errMsg = (result.error != null && result.error !== '') ? result.error : '创建订单失败'
|
||
console.error('[submitOrder] 订单创建失败:', errMsg)
|
||
uni.showToast({ title: errMsg, icon: 'none' })
|
||
}
|
||
|
||
} catch (err: any) {
|
||
uni.hideLoading()
|
||
console.error('[submitOrder] 提交订单错误:', err)
|
||
const errMsg = (err.message != null && err.message !== '') ? (err.message as string) : '提交订单失败'
|
||
uni.showToast({ title: errMsg, icon: 'none' })
|
||
}
|
||
}
|
||
|
||
// 生成订单号
|
||
const generateOrderNo = (): string => {
|
||
const date = new Date()
|
||
// ...
|
||
const random = Math.random().toString().slice(2, 8)
|
||
return `ORD${Date.now()}${random}`
|
||
}
|
||
|
||
// 返回
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
// 选择地址
|
||
const selectAddress = () => {
|
||
showAddressPopup.value = true;
|
||
}
|
||
|
||
// 添加登录跳转方法
|
||
const goToLogin = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/login/login' // 根据实际登录页面路径调整
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.checkout-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1; /* replace height: 100vh */
|
||
background-color: #f5f5f5;
|
||
}
|
||
/* 顶部栏 */
|
||
.checkout-header {
|
||
background-color: #ffffff;
|
||
padding: 15px;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
text-align: center;
|
||
}
|
||
|
||
.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; 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; lines: 2; text-overflow: ellipsis; 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; }
|
||
.delivery-option { display: flex; align-items: center; justify-content: space-between; padding: 15px; border: 1px solid #e5e5e5; border-radius: 8px; margin-bottom: 10px; }
|
||
.delivery-option:last-child { margin-bottom: 0; }
|
||
.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: flex-end; }
|
||
.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, .address-form-mask, .confirm-popup-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); z-index: 9998; }
|
||
.address-popup-mask { display: flex; align-items: flex-end; justify-content: center; }
|
||
.address-form-mask, .confirm-popup-mask { display: flex; align-items: center; justify-content: center; }
|
||
.address-popup { background-color: #ffffff; width: 100%; height: 500px; border-radius: 20px 20px 0 0; display: flex; flex-direction: column; }
|
||
.address-form-popup { background-color: #ffffff; width: 90%; max-width: 500px; height: 600px; border-radius: 12px; display: flex; flex-direction: column; }
|
||
.confirm-popup { background-color: #ffffff; width: 80%; max-width: 320px; border-radius: 12px; overflow: hidden; }
|
||
|
||
.popup-header, .form-header { padding: 20px 15px; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; }
|
||
.popup-title, .form-title { font-size: 18px; font-weight: bold; color: #333333; }
|
||
.popup-close, .form-close { font-size: 24px; color: #999999; padding: 5px; }
|
||
|
||
.address-list-container, .form-content { flex: 1; padding: 15px; }
|
||
.popup-address-item { padding: 15px; margin-bottom: 10px; border: 1px solid #e5e5e5; border-radius: 8px; position: relative; }
|
||
.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; }
|
||
|
||
.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; }
|
||
|
||
.form-item { margin-bottom: 20px; }
|
||
.form-label { font-size: 14px; color: #333333; margin-bottom: 8px; }
|
||
.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; justify-content: space-between; }
|
||
.region-input { flex: 1; margin-right: 10px; }
|
||
.region-input:last-child { margin-right: 0; }
|
||
.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; }
|
||
.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; }
|
||
.form-cancel-btn { flex: 1; background-color: #f5f5f5; color: #333333; padding: 12px; border-radius: 8px; font-size: 16px; border: none; margin-right: 10px; }
|
||
.form-submit-btn { flex: 1; background-color: #007aff; color: #ffffff; padding: 12px; border-radius: 8px; font-size: 16px; border: none; }
|
||
|
||
.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.cancel { color: #666666; border-right: 1px solid #e5e5e5; }
|
||
.confirm-btn.confirm { color: #007aff; font-weight: bold; }
|
||
</style> |