2595 lines
65 KiB
Plaintext
2595 lines
65 KiB
Plaintext
<!-- 支付页面 -->
|
||
<template>
|
||
<view class="payment-page">
|
||
<view class="jd-pay-header" :style="'padding-top:' + statusBarHeight + 'px;'">
|
||
<view class="jd-nav-row" :style="'height:' + navHeight + 'px;padding-right:' + capsuleRightReserve + 'px;'">
|
||
<view class="jd-left-capsule">
|
||
<text class="jd-back-icon" @click="goBack">‹</text>
|
||
<view class="jd-nav-divider"></view>
|
||
<text class="jd-menu-icon">☰</text>
|
||
</view>
|
||
|
||
<view class="jd-title-center">
|
||
<text class="jd-title-clock">◷</text>
|
||
<text class="jd-title-text">{{ orderMissing ? '订单不存在' : (isPaymentExpired ? '已取消' : '等待付款') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="jd-countdown-row" v-if="paymentReady && !orderMissing && !isPaymentExpired">
|
||
<text class="jd-countdown-prefix">请在 </text>
|
||
<text class="jd-countdown-number">{{ countdownText }}</text>
|
||
<text class="jd-countdown-suffix"> 内支付</text>
|
||
</view>
|
||
|
||
<view class="jd-countdown-row" v-else-if="orderMissing">
|
||
<text class="jd-expired-text">订单不存在或已删除</text>
|
||
</view>
|
||
|
||
<view class="jd-countdown-row" v-else>
|
||
<text class="jd-expired-text">订单超时未支付,已自动取消</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="jd-pay-notice" v-if="paymentReady && !orderMissing && !isPaymentExpired">
|
||
<text class="jd-pay-notice-text">请在下单后0.16小时内完成付款,超时未付款订单将被自动取消。</text>
|
||
</view>
|
||
|
||
<view class="payment-content">
|
||
<view v-if="orderMissing" class="card missing-card">
|
||
<view class="card-header-row">
|
||
<text class="card-title">订单状态异常</text>
|
||
</view>
|
||
<text class="missing-title">订单不存在或已删除</text>
|
||
<text class="missing-desc">请返回订单列表重新查看有效订单。</text>
|
||
<button class="missing-btn" @click="goToOrderList">返回订单列表</button>
|
||
</view>
|
||
|
||
<template v-else>
|
||
<view class="card address-card">
|
||
<view class="card-header-row">
|
||
<text class="card-title">{{ source === 'service' ? '服务地址' : '收货地址' }}</text>
|
||
<text class="card-action" @click="goEditAddress">修改</text>
|
||
</view>
|
||
<view class="address-main-row">
|
||
<view class="address-marker">
|
||
<text class="address-marker-text">收</text>
|
||
</view>
|
||
<view class="address-content">
|
||
<view class="address-user-row">
|
||
<text class="address-name">{{ addressName }}</text>
|
||
<text class="address-phone">{{ maskedAddressPhone }}</text>
|
||
</view>
|
||
<text class="address-detail">{{ fullAddressText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card goods-card">
|
||
<view class="shop-row">
|
||
<text class="shop-name">{{ shopName }}</text>
|
||
</view>
|
||
<view v-for="item in orderItems" :key="item.id" class="goods-item" @click="goToProductDetail(item.productId)">
|
||
<image class="goods-image" :src="item.image" mode="aspectFill" />
|
||
<view class="goods-content">
|
||
<text class="goods-name">{{ item.name }}</text>
|
||
<text class="goods-spec">{{ item.spec }}</text>
|
||
<view class="goods-meta-row">
|
||
<text class="goods-price">¥{{ item.price.toFixed(2) }}</text>
|
||
<text class="goods-count">x{{ item.quantity }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card fee-card">
|
||
<view class="card-header-row">
|
||
<text class="card-title">费用明细</text>
|
||
</view>
|
||
<view class="fee-row">
|
||
<text class="fee-label">商品数量</text>
|
||
<text class="fee-value">共 {{ totalItemCount }} 件</text>
|
||
</view>
|
||
<view class="fee-row">
|
||
<text class="fee-label">商品总价</text>
|
||
<text class="fee-value">¥{{ productAmount.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="fee-row">
|
||
<text class="fee-label">运费</text>
|
||
<text class="fee-value">¥{{ deliveryFee.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="fee-row">
|
||
<text class="fee-label">红包减免</text>
|
||
<text class="fee-value fee-discount">-¥{{ redPacketDiscount.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="fee-row">
|
||
<text class="fee-label">会员减免</text>
|
||
<text class="fee-value fee-discount">-¥{{ memberDiscount.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="fee-row">
|
||
<text class="fee-label">总减免</text>
|
||
<text class="fee-value fee-discount">-¥{{ totalDiscount.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="fee-row fee-row-total">
|
||
<text class="fee-label total-label">需付款金额</text>
|
||
<text class="fee-value total-value">¥{{ payAmount.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card order-info-card">
|
||
<view class="card-header-row">
|
||
<text class="card-title">订单信息</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">订单编号</text>
|
||
<view class="info-value-action" @click="copyOrderNo">
|
||
<text class="info-value">{{ orderNo }}</text>
|
||
<text class="info-copy">复制</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">支付方式</text>
|
||
<text class="info-value">在线支付</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">发票类型</text>
|
||
<text class="info-value">电子普通发票</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">下单时间</text>
|
||
<text class="info-value">{{ orderTimeText }}</text>
|
||
</view>
|
||
<view v-if="paymentReady && !isPaymentExpired" class="method-section">
|
||
<text class="method-section-title">选择支付方式</text>
|
||
<view class="method-list">
|
||
<view v-for="method in paymentMethods"
|
||
:key="method.id"
|
||
class="method-item-modern"
|
||
@click="selectMethod(method)">
|
||
<view class="method-left">
|
||
<image class="method-img" :src="getMethodBrandIcon(method.id)" mode="aspectFit" />
|
||
<view class="method-info">
|
||
<text class="method-name">{{ method.name }}</text>
|
||
<text class="method-desc" v-if="method.id === 'balance'">当前余额: ¥{{ userBalance.toFixed(2) }}</text>
|
||
<text class="method-desc" v-else>{{ method.description }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="method-right">
|
||
<view :class="['radio-circle', { checked: selectedMethod === method.id }]">
|
||
<view class="radio-inner" v-if="selectedMethod === method.id"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<GuessYouLike
|
||
title="你可能还喜欢"
|
||
:pageSize="8"
|
||
:excludeProductIds="excludeRecommendProductIds"
|
||
:loadMoreKey="guessLoadMoreKey"
|
||
@productClick="goToProductDetail"
|
||
/>
|
||
|
||
<view class="bottom-safe-space"></view>
|
||
</template>
|
||
</view>
|
||
|
||
<view class="payment-bottom" v-if="paymentReady && !orderMissing && !showPassword && !isPaymentExpired">
|
||
<button class="cancel-btn" :disabled="isCancelling" @click="openCancelSheet">取消订单</button>
|
||
<button class="pay-btn"
|
||
:class="{ disabled: !canPay || (selectedMethod === 'balance' && userBalance < payAmount) }"
|
||
@click="confirmPayment">
|
||
<text v-if="!isPaying" class="pay-text">立即支付 ¥{{ payAmount.toFixed(2) }}</text>
|
||
<text v-else class="pay-text">支付中...</text>
|
||
</button>
|
||
</view>
|
||
|
||
<view v-if="!orderMissing && isPaymentExpired" class="payment-expired-bottom">
|
||
<button class="payment-expired-btn secondary" @click="goToOrderList">返回订单列表</button>
|
||
<button class="payment-expired-btn primary" @click="goToShoppingHome">重新选购</button>
|
||
</view>
|
||
|
||
<view v-if="orderMissing" class="payment-expired-bottom">
|
||
<button class="payment-expired-btn primary" @click="goToOrderList">返回订单列表</button>
|
||
</view>
|
||
|
||
<view v-if="showCancelSheet" class="sheet-mask" @click="closeCancelSheet">
|
||
<view class="cancel-sheet" @click.stop="">
|
||
<view class="sheet-header">
|
||
<text class="sheet-title">取消订单</text>
|
||
<text class="sheet-subtitle">请选择取消原因</text>
|
||
</view>
|
||
<scroll-view class="sheet-scroll" direction="vertical" :show-scrollbar="false">
|
||
<view v-for="reason in cancelReasonOptions" :key="reason.id" class="reason-item" @click="selectCancelReason(reason.id)">
|
||
<text class="reason-text">{{ reason.label }}</text>
|
||
<view :class="['radio-circle', 'sheet-radio', { checked: selectedCancelReason === reason.id }]">
|
||
<view class="radio-inner" v-if="selectedCancelReason === reason.id"></view>
|
||
</view>
|
||
</view>
|
||
<view v-if="selectedCancelReason === 'other'" class="other-reason-box">
|
||
<input class="other-reason-input" v-model="otherCancelReason" placeholder="请输入其他原因,选填" />
|
||
</view>
|
||
</scroll-view>
|
||
<view class="sheet-actions">
|
||
<button class="sheet-btn sheet-btn-secondary" @click="closeCancelSheet">暂不取消</button>
|
||
<button class="sheet-btn sheet-btn-primary" :disabled="isCancelling" @click="confirmCancelOrder">确认取消</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showPassword" class="password-popup-mask" @click="closePasswordPopup">
|
||
<view class="password-popup-content" @click.stop="">
|
||
<view class="popup-header">
|
||
<text class="popup-close" @click="closePasswordPopup">✕</text>
|
||
<text class="popup-title">请输入支付密码</text>
|
||
<view class="popup-placeholder"></view>
|
||
</view>
|
||
|
||
<view class="popup-amount-info">
|
||
<text class="popup-amount-label">支付金额</text>
|
||
<view class="popup-amount-row">
|
||
<text class="popup-currency">¥</text>
|
||
<text class="popup-value">{{ payAmount.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="password-input-row">
|
||
<view v-for="(_, index) in 6"
|
||
:key="index"
|
||
class="password-box">
|
||
<view v-if="password.length > index" class="password-dot"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<text class="forgot-password-link" @click="forgotPassword">忘记密码?</text>
|
||
|
||
<view class="password-keyboard-popup">
|
||
<view class="keyboard-grid keyboard-grid-popup">
|
||
<view v-for="num in 9"
|
||
:key="num"
|
||
class="keyboard-key keyboard-key-popup"
|
||
@click="inputPassword(num.toString())">
|
||
<text class="key-text key-text-popup">{{ num }}</text>
|
||
</view>
|
||
<view class="keyboard-key keyboard-key-popup keyboard-key-empty"></view>
|
||
<view class="keyboard-key keyboard-key-popup" @click="inputPassword('0')">
|
||
<text class="key-text key-text-popup">0</text>
|
||
</view>
|
||
<view class="keyboard-key keyboard-key-popup" @click="deletePassword">
|
||
<text class="key-text key-text-popup">⌫</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, watch, computed, onUnmounted } from 'vue'
|
||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||
import { goToLogin } from '@/utils/utils.uts'
|
||
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
|
||
import { formatCountdownHMS, getUnifiedDisplayState, ORDER_PAY_TIMEOUT_SECONDS, ORDER_STATUS_CANCELLED, ORDER_STATUS_PAID_OR_SHIPPING, ORDER_STATUS_PENDING, ORDER_STATUS_TIMEOUT_LEGACY, ORDER_TIMEOUT_CANCEL_REASON, PAYMENT_STATUS_PAID, PAYMENT_STATUS_TIMEOUT, PAYMENT_STATUS_UNPAID, type OrderStatusSource } from '@/utils/orderStatus.uts'
|
||
import { dispatchPaidHomecareOrder, showHomecareDispatchFailureModal } from '@/services/serviceOrderService.uts'
|
||
|
||
type PaymentMethodType = {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
icon: string
|
||
enabled: boolean
|
||
}
|
||
|
||
type AddressInfo = {
|
||
name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
address: string
|
||
}
|
||
|
||
type OrderProductInfo = {
|
||
id: string
|
||
productId: string
|
||
name: string
|
||
spec: string
|
||
image: string
|
||
price: number
|
||
quantity: number
|
||
}
|
||
|
||
type CancelReasonOption = {
|
||
id: string
|
||
label: string
|
||
}
|
||
|
||
const DEFAULT_IMAGE = '/static/logo.png'
|
||
|
||
const getOptionString = (options: UTSJSONObject, key: string): string => {
|
||
try {
|
||
const value = options.getString(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
} catch (e) {
|
||
}
|
||
|
||
try {
|
||
const normalized = JSON.parse(JSON.stringify(options)) as UTSJSONObject
|
||
const value = normalized.getString(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
} catch (e) {
|
||
}
|
||
|
||
return ''
|
||
}
|
||
|
||
const parseNumberValue = (value: string): number => {
|
||
if (value == null || value === '') {
|
||
return 0
|
||
}
|
||
const parsed = parseFloat(value)
|
||
if (isNaN(parsed)) {
|
||
return 0
|
||
}
|
||
return parsed
|
||
}
|
||
|
||
const getStringField = (obj: UTSJSONObject, key: string): string => {
|
||
try {
|
||
const value = obj.getString(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return ''
|
||
}
|
||
|
||
const getNumberField = (obj: UTSJSONObject, key: string): number => {
|
||
try {
|
||
const value = obj.getNumber(key)
|
||
if (value != null) {
|
||
return value
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return 0
|
||
}
|
||
|
||
const createMockAddress = (): AddressInfo => {
|
||
return {
|
||
name: '张女士',
|
||
phone: '13800138000',
|
||
province: '广东省',
|
||
city: '深圳市',
|
||
district: '南山区',
|
||
address: '科技园科苑路 15 号 2 栋 1608 室'
|
||
}
|
||
}
|
||
|
||
const createEmptyAddress = (): AddressInfo => {
|
||
return {
|
||
name: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
address: ''
|
||
}
|
||
}
|
||
|
||
const createMockOrderItems = (): Array<OrderProductInfo> => {
|
||
return [
|
||
{
|
||
id: 'mock-product-1',
|
||
productId: 'mock-product-1',
|
||
name: '医用雾化器 家用便携静音款',
|
||
spec: '标准版 / 一机三配件',
|
||
image: DEFAULT_IMAGE,
|
||
price: 199.00,
|
||
quantity: 1
|
||
},
|
||
{
|
||
id: 'mock-product-2',
|
||
productId: 'mock-product-2',
|
||
name: '护理级一次性口罩 50 只装',
|
||
spec: '成人款 / 独立包装',
|
||
image: DEFAULT_IMAGE,
|
||
price: 39.90,
|
||
quantity: 2
|
||
}
|
||
]
|
||
}
|
||
|
||
const createCancelReasonOptions = (): Array<CancelReasonOption> => {
|
||
return [
|
||
{ id: 'no-need', label: '不想买了' },
|
||
{ id: 'info-error', label: '信息填写错误,重新下单' },
|
||
{ id: 'address-error', label: '地址或服务时间填写错误' },
|
||
{ id: 'price', label: '商品价格原因' },
|
||
{ id: 'payment', label: '支付方式遇到问题' },
|
||
{ id: 'other', label: '其他' }
|
||
]
|
||
}
|
||
|
||
const orderId = ref<string>('')
|
||
const source = ref<string>('goods')
|
||
const bizType = ref<string>('goods')
|
||
const orderNo = ref<string>('')
|
||
const amount = ref<number>(0)
|
||
const productAmount = ref<number>(0)
|
||
const deliveryFee = ref<number>(0)
|
||
const redPacketDiscount = ref<number>(0)
|
||
const memberDiscount = ref<number>(0)
|
||
const paymentMethods = ref<Array<PaymentMethodType>>([])
|
||
const selectedMethod = ref<string>('wechat')
|
||
const userBalance = ref<number>(0)
|
||
const isPaying = ref<boolean>(false)
|
||
const isDispatchSubmitting = ref<boolean>(false)
|
||
const showPassword = ref<boolean>(false)
|
||
const password = ref<string>('')
|
||
const addressInfo = ref<AddressInfo>(createEmptyAddress())
|
||
const shopName = ref<string>('')
|
||
const orderItems = ref<Array<OrderProductInfo>>([])
|
||
const cancelReasonOptions = ref<Array<CancelReasonOption>>(createCancelReasonOptions())
|
||
const showCancelSheet = ref<boolean>(false)
|
||
const selectedCancelReason = ref<string>('')
|
||
const otherCancelReason = ref<string>('')
|
||
const isCancelling = ref<boolean>(false)
|
||
const orderCreatedAt = ref<string>('')
|
||
const remainingSeconds = ref<number>(ORDER_PAY_TIMEOUT_SECONDS)
|
||
const timeoutHandled = ref<boolean>(false)
|
||
const paymentDeadlineMs = ref<number>(0)
|
||
const guessLoadMoreKey = ref<number>(0)
|
||
const isPaymentExpired = ref<boolean>(false)
|
||
const orderMissing = ref<boolean>(false)
|
||
const paymentReady = ref<boolean>(false)
|
||
const isPreviewMode = ref<boolean>(false)
|
||
const orderStatus = ref<number>(ORDER_STATUS_PENDING)
|
||
const paymentStatus = ref<number>(PAYMENT_STATUS_UNPAID)
|
||
const cancelReason = ref<string>('')
|
||
const payExpireAtText = ref<string>('')
|
||
const statusBarHeight = ref<number>(0)
|
||
const navHeight = ref<number>(44)
|
||
const capsuleRightReserve = ref<number>(12)
|
||
|
||
let countdownTimer = 0
|
||
|
||
const ensureLoggedIn = (): boolean => {
|
||
const userId = supabaseService.getCurrentUserId()
|
||
if (userId == null || userId === '') {
|
||
const redirectUrl = orderId.value !== ''
|
||
? '/pages/mall/consumer/payment?orderId=' + orderId.value + '&bizType=' + bizType.value + '&source=' + source.value + '&amount=' + amount.value
|
||
: '/pages/mall/consumer/payment'
|
||
goToLogin(redirectUrl)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
const totalDiscount = computed((): number => {
|
||
return redPacketDiscount.value + memberDiscount.value
|
||
})
|
||
|
||
const calculatedPayAmount = computed((): number => {
|
||
const value = productAmount.value + deliveryFee.value - totalDiscount.value
|
||
if (value > 0) {
|
||
return value
|
||
}
|
||
return 0
|
||
})
|
||
|
||
const payAmount = computed((): number => {
|
||
if (amount.value > 0) {
|
||
return amount.value
|
||
}
|
||
return calculatedPayAmount.value
|
||
})
|
||
|
||
const canPay = computed((): boolean => {
|
||
return paymentReady.value
|
||
&& !orderMissing.value
|
||
&& !isPaymentExpired.value
|
||
&& orderStatus.value == ORDER_STATUS_PENDING
|
||
&& paymentStatus.value == PAYMENT_STATUS_UNPAID
|
||
&& !isPaying.value
|
||
})
|
||
|
||
const totalItemCount = computed((): number => {
|
||
let count = 0
|
||
for (let i = 0; i < orderItems.value.length; i++) {
|
||
count += orderItems.value[i].quantity
|
||
}
|
||
return count
|
||
})
|
||
|
||
const maskedAddressPhone = computed((): string => {
|
||
const phone = addressInfo.value.phone
|
||
if (phone.length >= 11) {
|
||
return phone.slice(0, 3) + '****' + phone.slice(phone.length - 4)
|
||
}
|
||
return phone
|
||
})
|
||
|
||
const fullAddressText = computed((): string => {
|
||
return addressInfo.value.province + addressInfo.value.city + addressInfo.value.district + addressInfo.value.address
|
||
})
|
||
|
||
const addressName = computed((): string => {
|
||
return addressInfo.value.name
|
||
})
|
||
|
||
const excludeRecommendProductIds = computed((): Array<string> => {
|
||
const result: Array<string> = []
|
||
for (let i = 0; i < orderItems.value.length; i++) {
|
||
const productId = orderItems.value[i].productId
|
||
if (productId !== '' && result.indexOf(productId) < 0) {
|
||
result.push(productId)
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const formatClockCountdown = (seconds: number): string => {
|
||
let safeSeconds = seconds
|
||
if (safeSeconds < 0) {
|
||
safeSeconds = 0
|
||
}
|
||
const hours = Math.floor(safeSeconds / 3600)
|
||
const minutes = Math.floor((safeSeconds % 3600) / 60)
|
||
const remainSeconds = safeSeconds % 60
|
||
const hourText = hours < 10 ? '0' + hours : '' + hours
|
||
const minuteText = minutes < 10 ? '0' + minutes : '' + minutes
|
||
const secondText = remainSeconds < 10 ? '0' + remainSeconds : '' + remainSeconds
|
||
return hourText + ':' + minuteText + ':' + secondText
|
||
}
|
||
|
||
const countdownText = computed((): string => {
|
||
return formatClockCountdown(remainingSeconds.value)
|
||
})
|
||
|
||
const payStatusTitle = computed((): string => {
|
||
return isPaymentExpired.value ? '已取消' : '等待付款'
|
||
})
|
||
|
||
const payStatusDesc = computed((): string => {
|
||
return isPaymentExpired.value ? '订单超时未支付,已自动取消' : '请在 ' + countdownText.value + ' 内支付'
|
||
})
|
||
|
||
const getDeadlineByCreatedAt = (createdAt: string): number => {
|
||
const createdAtMs = parseDateTime(createdAt)
|
||
if (createdAtMs <= 0) {
|
||
return 0
|
||
}
|
||
return createdAtMs + ORDER_PAY_TIMEOUT_SECONDS * 1000
|
||
}
|
||
|
||
const orderTimeText = computed((): string => {
|
||
if (orderCreatedAt.value === '') {
|
||
return '--'
|
||
}
|
||
const date = new Date(orderCreatedAt.value)
|
||
if (isNaN(date.getTime())) {
|
||
return orderCreatedAt.value
|
||
}
|
||
const year = date.getFullYear()
|
||
const month = date.getMonth() + 1
|
||
const day = date.getDate()
|
||
const hour = date.getHours()
|
||
const minute = date.getMinutes()
|
||
const second = date.getSeconds()
|
||
const monthText = month < 10 ? '0' + month : '' + month
|
||
const dayText = day < 10 ? '0' + day : '' + day
|
||
const hourText = hour < 10 ? '0' + hour : '' + hour
|
||
const minuteText = minute < 10 ? '0' + minute : '' + minute
|
||
const secondText = second < 10 ? '0' + second : '' + second
|
||
return year + '-' + monthText + '-' + dayText + ' ' + hourText + ':' + minuteText + ':' + secondText
|
||
})
|
||
|
||
const getLocalDeadline = (targetOrderId: string): number => {
|
||
if (targetOrderId === '') {
|
||
return 0
|
||
}
|
||
try {
|
||
const cache = uni.getStorageSync('payment_deadline_' + targetOrderId)
|
||
if (cache == null || cache === '') {
|
||
return 0
|
||
}
|
||
if (typeof cache === 'number') {
|
||
return cache as number
|
||
}
|
||
const parsed = parseInt('' + cache)
|
||
if (!isNaN(parsed) && parsed > 0) {
|
||
return parsed
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return 0
|
||
}
|
||
|
||
const saveLocalDeadline = (targetOrderId: string, deadlineMs: number): void => {
|
||
if (targetOrderId === '' || deadlineMs <= 0) {
|
||
return
|
||
}
|
||
try {
|
||
uni.setStorageSync('payment_deadline_' + targetOrderId, '' + deadlineMs)
|
||
} catch (e) {
|
||
}
|
||
}
|
||
|
||
const removeLocalDeadline = (targetOrderId: string): void => {
|
||
if (targetOrderId === '') {
|
||
return
|
||
}
|
||
try {
|
||
uni.removeStorageSync('payment_deadline_' + targetOrderId)
|
||
} catch (e) {
|
||
}
|
||
}
|
||
|
||
const parseDateTime = (value: string): number => {
|
||
if (value === '') {
|
||
return 0
|
||
}
|
||
const date = new Date(value)
|
||
if (isNaN(date.getTime())) {
|
||
return 0
|
||
}
|
||
return date.getTime()
|
||
}
|
||
|
||
const createCurrentOrderSource = (status: number, payment: number, reason: string, payExpireAt: string): OrderStatusSource => {
|
||
return {
|
||
source: source.value,
|
||
order_status: status,
|
||
payment_status: payment,
|
||
pay_expire_at: payExpireAt,
|
||
created_at: orderCreatedAt.value,
|
||
cancel_reason: reason
|
||
}
|
||
}
|
||
|
||
const isExpiredState = (status: number, payment: number, reason: string, payExpireAt: string): boolean => {
|
||
const displayState = getUnifiedDisplayState(createCurrentOrderSource(status, payment, reason, payExpireAt))
|
||
return displayState == 'cancelled' || displayState == 'expired'
|
||
}
|
||
|
||
const stopCountdown = (): void => {
|
||
if (countdownTimer > 0) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = 0
|
||
}
|
||
}
|
||
|
||
const getCancelledStatusBySource = (): number => {
|
||
return source.value == 'service' ? ORDER_STATUS_TIMEOUT_LEGACY : ORDER_STATUS_CANCELLED
|
||
}
|
||
|
||
const resetRealOrderView = (): void => {
|
||
addressInfo.value = createEmptyAddress()
|
||
shopName.value = ''
|
||
orderItems.value = []
|
||
productAmount.value = 0
|
||
deliveryFee.value = 0
|
||
redPacketDiscount.value = 0
|
||
memberDiscount.value = 0
|
||
}
|
||
|
||
const enterPreviewMode = (): void => {
|
||
isPreviewMode.value = true
|
||
orderMissing.value = false
|
||
paymentReady.value = true
|
||
addressInfo.value = createMockAddress()
|
||
shopName.value = '京东健康自营店'
|
||
orderItems.value = createMockOrderItems()
|
||
productAmount.value = 0
|
||
deliveryFee.value = 0
|
||
redPacketDiscount.value = 0
|
||
memberDiscount.value = 0
|
||
calculateProductAmountFromItems()
|
||
calculatePriceDetails(amount.value)
|
||
if (orderNo.value === '') {
|
||
orderNo.value = 'ORD_PENDING_' + Date.now()
|
||
}
|
||
if (orderCreatedAt.value === '') {
|
||
orderCreatedAt.value = new Date().toISOString()
|
||
}
|
||
paymentDeadlineMs.value = Date.now() + ORDER_PAY_TIMEOUT_SECONDS * 1000
|
||
startCountdown(paymentDeadlineMs.value)
|
||
}
|
||
|
||
const markPaymentExpiredInPage = (): void => {
|
||
isPaymentExpired.value = true
|
||
remainingSeconds.value = 0
|
||
orderStatus.value = getCancelledStatusBySource()
|
||
paymentStatus.value = PAYMENT_STATUS_TIMEOUT
|
||
cancelReason.value = ORDER_TIMEOUT_CANCEL_REASON
|
||
paymentDeadlineMs.value = 0
|
||
paymentReady.value = !orderMissing.value
|
||
stopCountdown()
|
||
showCancelSheet.value = false
|
||
showPassword.value = false
|
||
password.value = ''
|
||
}
|
||
|
||
const applyOrderState = (status: number, payment: number, reason: string, payExpireAt: string): void => {
|
||
orderStatus.value = status
|
||
paymentStatus.value = payment
|
||
cancelReason.value = reason
|
||
payExpireAtText.value = payExpireAt
|
||
if (isExpiredState(status, payment, reason, payExpireAt)) {
|
||
markPaymentExpiredInPage()
|
||
orderStatus.value = getCancelledStatusBySource()
|
||
paymentStatus.value = PAYMENT_STATUS_TIMEOUT
|
||
cancelReason.value = reason !== '' ? reason : ORDER_TIMEOUT_CANCEL_REASON
|
||
} else {
|
||
isPaymentExpired.value = false
|
||
}
|
||
}
|
||
|
||
const updateCountdownByDeadline = (): void => {
|
||
if (paymentDeadlineMs.value <= 0) {
|
||
return
|
||
}
|
||
const diff = Math.floor((paymentDeadlineMs.value - Date.now()) / 1000)
|
||
remainingSeconds.value = diff > 0 ? diff : 0
|
||
if (remainingSeconds.value <= 0) {
|
||
stopCountdown()
|
||
handlePaymentTimeout()
|
||
}
|
||
}
|
||
|
||
const startCountdown = (deadlineMs: number): void => {
|
||
if (deadlineMs <= 0) {
|
||
paymentDeadlineMs.value = 0
|
||
remainingSeconds.value = 0
|
||
return
|
||
}
|
||
if (deadlineMs <= Date.now()) {
|
||
paymentDeadlineMs.value = deadlineMs
|
||
remainingSeconds.value = 0
|
||
handlePaymentTimeout()
|
||
return
|
||
}
|
||
paymentDeadlineMs.value = deadlineMs
|
||
stopCountdown()
|
||
updateCountdownByDeadline()
|
||
if (remainingSeconds.value <= 0) {
|
||
return
|
||
}
|
||
countdownTimer = setInterval(() => {
|
||
updateCountdownByDeadline()
|
||
}, 1000)
|
||
}
|
||
|
||
const resolvePaymentDeadline = (orderObj: UTSJSONObject | null): number => {
|
||
if (orderObj == null) {
|
||
return 0
|
||
}
|
||
const payExpireAt = getStringField(orderObj, 'pay_expire_at')
|
||
if (payExpireAt !== '') {
|
||
const expireDate = new Date(payExpireAt)
|
||
if (!isNaN(expireDate.getTime())) {
|
||
return expireDate.getTime()
|
||
}
|
||
}
|
||
const createdAt = getStringField(orderObj, 'created_at')
|
||
if (createdAt !== '') {
|
||
const createdDate = new Date(createdAt)
|
||
if (!isNaN(createdDate.getTime())) {
|
||
return createdDate.getTime() + ORDER_PAY_TIMEOUT_SECONDS * 1000
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
const loadPaymentMethods = () => {
|
||
const methods: PaymentMethodType[] = [
|
||
{
|
||
id: 'wechat',
|
||
name: '微信支付',
|
||
description: '使用微信支付完成订单',
|
||
icon: '💳',
|
||
enabled: true
|
||
}
|
||
]
|
||
paymentMethods.value = methods
|
||
}
|
||
|
||
const loadUserBalance = async () => {
|
||
try {
|
||
const balance = await supabaseService.getUserBalanceNumber()
|
||
userBalance.value = balance
|
||
} catch (err) {
|
||
console.error('加载用户余额异常:', err)
|
||
userBalance.value = 0
|
||
}
|
||
}
|
||
|
||
const calculatePriceDetails = (totalAmount: number) => {
|
||
if (productAmount.value <= 0) {
|
||
productAmount.value = totalAmount > 0 ? totalAmount : 0
|
||
}
|
||
if (deliveryFee.value < 0) {
|
||
deliveryFee.value = 0
|
||
}
|
||
if (redPacketDiscount.value < 0) {
|
||
redPacketDiscount.value = 0
|
||
}
|
||
if (memberDiscount.value < 0) {
|
||
memberDiscount.value = 0
|
||
}
|
||
const fallbackAmount = productAmount.value + deliveryFee.value - totalDiscount.value
|
||
if (totalAmount > 0) {
|
||
amount.value = totalAmount
|
||
} else if (fallbackAmount > 0) {
|
||
amount.value = fallbackAmount
|
||
}
|
||
}
|
||
|
||
const updateOrderInStorage = (targetOrderId: string, status: number, cancelReason: string = '') => {
|
||
try {
|
||
const ordersStr = uni.getStorageSync('orders')
|
||
let orders: UTSJSONObject[] = []
|
||
if (ordersStr != null && ordersStr !== '') {
|
||
const parsed = JSON.parse(ordersStr as string)
|
||
if (Array.isArray(parsed)) {
|
||
for (let i = 0; i < parsed.length; i++) {
|
||
const itemStr = JSON.stringify(parsed[i])
|
||
const itemParsed = JSON.parse(itemStr)
|
||
if (itemParsed != null) {
|
||
orders.push(itemParsed as UTSJSONObject)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let foundIndex = -1
|
||
for (let i = 0; i < orders.length; i++) {
|
||
const order = orders[i]
|
||
if (order['id'] === targetOrderId) {
|
||
foundIndex = i
|
||
break
|
||
}
|
||
}
|
||
|
||
if (foundIndex !== -1) {
|
||
orders[foundIndex]['status'] = status
|
||
orders[foundIndex]['order_status'] = status
|
||
orders[foundIndex]['payment_status'] = status == ORDER_STATUS_PAID_OR_SHIPPING ? PAYMENT_STATUS_PAID : (status == ORDER_STATUS_CANCELLED ? PAYMENT_STATUS_TIMEOUT : PAYMENT_STATUS_UNPAID)
|
||
orders[foundIndex]['updated_at'] = new Date().toISOString()
|
||
if (cancelReason !== '') {
|
||
orders[foundIndex]['cancel_reason'] = cancelReason
|
||
}
|
||
uni.setStorageSync('orders', JSON.stringify(orders))
|
||
console.log('订单状态已更新到Storage (orders):', targetOrderId, status)
|
||
} else {
|
||
console.log('本地缓存中无订单数据,已忽略:', targetOrderId)
|
||
}
|
||
} catch (e) {
|
||
console.error('更新订单状态失败', e)
|
||
}
|
||
}
|
||
|
||
const navigateAfterCancel = () => {
|
||
setTimeout(() => {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/orders'
|
||
})
|
||
}
|
||
})
|
||
}, 1200)
|
||
}
|
||
|
||
const requestWechatPaymentResult = async (): Promise<string> => {
|
||
return await new Promise<string>((resolve: (value: string) => void) => {
|
||
uni.showActionSheet({
|
||
itemList: ['模拟支付成功', '模拟用户取消支付', '模拟支付失败'],
|
||
success: (res) => {
|
||
if (res.tapIndex == 0) {
|
||
resolve('success')
|
||
} else if (res.tapIndex == 1) {
|
||
resolve('cancel')
|
||
} else {
|
||
resolve('fail')
|
||
}
|
||
},
|
||
fail: () => {
|
||
resolve('cancel')
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
const handlePaymentTimeout = async () => {
|
||
if (timeoutHandled.value) {
|
||
return
|
||
}
|
||
timeoutHandled.value = true
|
||
markPaymentExpiredInPage()
|
||
let success = false
|
||
try {
|
||
if (orderId.value !== '') {
|
||
success = await supabaseService.expireUnifiedOrder(orderId.value, source.value)
|
||
} else {
|
||
success = true
|
||
}
|
||
} catch (e) {
|
||
console.error('订单超时更新失败:', e)
|
||
}
|
||
if (!success && orderId.value !== '') {
|
||
try {
|
||
const latestOrder = await supabaseService.getUnifiedOrderDetail(orderId.value, source.value)
|
||
if (latestOrder != null) {
|
||
const latestObj = JSON.parse(JSON.stringify(latestOrder)) as UTSJSONObject
|
||
const latestStatus = getNumberField(latestObj, 'order_status')
|
||
const latestPayment = getNumberField(latestObj, 'payment_status')
|
||
const latestReason = getStringField(latestObj, 'cancel_reason')
|
||
const latestExpire = getStringField(latestObj, 'pay_expire_at')
|
||
applyOrderState(latestStatus, latestPayment, latestReason, latestExpire)
|
||
success = isExpiredState(latestStatus, latestPayment, latestReason, latestExpire)
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
const cancelledStatus = getCancelledStatusBySource()
|
||
updateOrderInStorage(orderId.value, cancelledStatus, ORDER_TIMEOUT_CANCEL_REASON)
|
||
uni.$emit('orderUpdated', { orderId: orderId.value, status: cancelledStatus, paymentStatus: PAYMENT_STATUS_TIMEOUT, cancelReason: ORDER_TIMEOUT_CANCEL_REASON, payExpireAt: payExpireAtText.value })
|
||
if (success) {
|
||
uni.showToast({ title: '订单已取消', icon: 'none' })
|
||
return
|
||
}
|
||
uni.showToast({ title: '订单已取消', icon: 'none' })
|
||
}
|
||
|
||
const parseAddressFromOrder = (orderObj: UTSJSONObject) => {
|
||
let parsedAddress = createEmptyAddress()
|
||
try {
|
||
const shippingAddressRaw = orderObj.get('shipping_address')
|
||
if (shippingAddressRaw != null) {
|
||
const shippingAddress = JSON.parse(JSON.stringify(shippingAddressRaw)) as UTSJSONObject
|
||
const nameValue = getStringField(shippingAddress, 'name')
|
||
const mobileValue = getStringField(shippingAddress, 'mobile')
|
||
const phoneValue = getStringField(shippingAddress, 'phone')
|
||
const provinceValue = getStringField(shippingAddress, 'province')
|
||
const cityValue = getStringField(shippingAddress, 'city')
|
||
const districtValue = getStringField(shippingAddress, 'district')
|
||
const detailValue = getStringField(shippingAddress, 'detail')
|
||
const addressValue = getStringField(shippingAddress, 'address')
|
||
if (nameValue !== '') parsedAddress.name = nameValue
|
||
if (mobileValue !== '') parsedAddress.phone = mobileValue
|
||
if (phoneValue !== '') parsedAddress.phone = phoneValue
|
||
if (provinceValue !== '') parsedAddress.province = provinceValue
|
||
if (cityValue !== '') parsedAddress.city = cityValue
|
||
if (districtValue !== '') parsedAddress.district = districtValue
|
||
if (detailValue !== '') parsedAddress.address = detailValue
|
||
if (addressValue !== '') parsedAddress.address = addressValue
|
||
}
|
||
} catch (e) {
|
||
}
|
||
|
||
const receiverName = getStringField(orderObj, 'receiver_name')
|
||
const receiverPhone = getStringField(orderObj, 'receiver_phone')
|
||
const addressDetail = getStringField(orderObj, 'address_detail')
|
||
if (receiverName !== '') parsedAddress.name = receiverName
|
||
if (receiverPhone !== '') parsedAddress.phone = receiverPhone
|
||
if (addressDetail !== '') parsedAddress.address = addressDetail
|
||
|
||
addressInfo.value = parsedAddress
|
||
}
|
||
|
||
const parseItemsFromOrder = (orderObj: UTSJSONObject) => {
|
||
const nextItems: Array<OrderProductInfo> = []
|
||
try {
|
||
const itemsRaw = orderObj.get('items')
|
||
if (itemsRaw != null) {
|
||
const itemsParsed = JSON.parse(JSON.stringify(itemsRaw))
|
||
if (Array.isArray(itemsParsed)) {
|
||
for (let i = 0; i < itemsParsed.length; i++) {
|
||
const itemObj = JSON.parse(JSON.stringify(itemsParsed[i])) as UTSJSONObject
|
||
const itemId = getStringField(itemObj, 'id') !== '' ? getStringField(itemObj, 'id') : 'item-' + i
|
||
const productIdValue = getStringField(itemObj, 'product_id') !== '' ? getStringField(itemObj, 'product_id') : itemId
|
||
const nameValue = getStringField(itemObj, 'product_name') !== '' ? getStringField(itemObj, 'product_name') : (getStringField(itemObj, 'name') !== '' ? getStringField(itemObj, 'name') : '商品信息加载中')
|
||
const specValue = getStringField(itemObj, 'sku_name') !== '' ? getStringField(itemObj, 'sku_name') : (getStringField(itemObj, 'spec') !== '' ? getStringField(itemObj, 'spec') : (getStringField(itemObj, 'specification') !== '' ? getStringField(itemObj, 'specification') : '默认规格'))
|
||
const imageValue = getStringField(itemObj, 'product_image') !== '' ? getStringField(itemObj, 'product_image') : (getStringField(itemObj, 'image') !== '' ? getStringField(itemObj, 'image') : (getStringField(itemObj, 'cover') !== '' ? getStringField(itemObj, 'cover') : DEFAULT_IMAGE))
|
||
const priceValue = getNumberField(itemObj, 'price') > 0 ? getNumberField(itemObj, 'price') : (getNumberField(itemObj, 'unit_price') > 0 ? getNumberField(itemObj, 'unit_price') : getNumberField(itemObj, 'sale_price'))
|
||
const quantityValue = getNumberField(itemObj, 'quantity') > 0 ? getNumberField(itemObj, 'quantity') : (getNumberField(itemObj, 'count') > 0 ? getNumberField(itemObj, 'count') : 1)
|
||
nextItems.push({
|
||
id: itemId,
|
||
productId: productIdValue,
|
||
name: nameValue,
|
||
spec: specValue,
|
||
image: imageValue,
|
||
price: priceValue > 0 ? priceValue : 0,
|
||
quantity: quantityValue > 0 ? quantityValue : 1
|
||
})
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('解析订单商品失败:', e)
|
||
}
|
||
|
||
if (nextItems.length > 0) {
|
||
orderItems.value = nextItems
|
||
} else {
|
||
orderItems.value = []
|
||
}
|
||
}
|
||
|
||
const calculateProductAmountFromItems = () => {
|
||
if (productAmount.value > 0) {
|
||
return
|
||
}
|
||
let total = 0
|
||
for (let i = 0; i < orderItems.value.length; i++) {
|
||
total += orderItems.value[i].price * orderItems.value[i].quantity
|
||
}
|
||
productAmount.value = total
|
||
}
|
||
|
||
const loadOrderInfo = async () => {
|
||
try {
|
||
if (orderId.value == '') return
|
||
timeoutHandled.value = false
|
||
orderMissing.value = false
|
||
paymentReady.value = false
|
||
isPreviewMode.value = false
|
||
resetRealOrderView()
|
||
|
||
const order = await supabaseService.getUnifiedOrderDetail(orderId.value, source.value)
|
||
if (order != null) {
|
||
const orderStr = JSON.stringify(order)
|
||
const orderParsed = JSON.parse(orderStr)
|
||
if (orderParsed == null) {
|
||
console.error('订单数据解析失败')
|
||
return
|
||
}
|
||
const orderObj = orderParsed as UTSJSONObject
|
||
const latestStatus = getNumberField(orderObj, 'order_status')
|
||
const latestPayment = getNumberField(orderObj, 'payment_status')
|
||
const latestReason = getStringField(orderObj, 'cancel_reason')
|
||
const latestExpire = getStringField(orderObj, 'pay_expire_at')
|
||
source.value = getStringField(orderObj, 'source') !== '' ? getStringField(orderObj, 'source') : source.value
|
||
bizType.value = getStringField(orderObj, 'biz_type') !== '' ? getStringField(orderObj, 'biz_type') : bizType.value
|
||
|
||
const orderNoVal = getStringField(orderObj, 'order_no')
|
||
if (orderNoVal != '') {
|
||
orderNo.value = orderNoVal
|
||
}
|
||
|
||
const createdAtVal = getStringField(orderObj, 'created_at')
|
||
if (createdAtVal != '') {
|
||
orderCreatedAt.value = createdAtVal
|
||
}
|
||
|
||
let effectivePayExpireAt = latestExpire
|
||
if (effectivePayExpireAt == '') {
|
||
const ensuredExpireAt = await supabaseService.ensureUnifiedPayExpireAt(orderId.value, source.value)
|
||
if (ensuredExpireAt != '') {
|
||
effectivePayExpireAt = ensuredExpireAt
|
||
}
|
||
}
|
||
|
||
applyOrderState(latestStatus, latestPayment, latestReason, effectivePayExpireAt)
|
||
|
||
const shopNameValue = getStringField(orderObj, 'merchant_name') !== ''
|
||
? getStringField(orderObj, 'merchant_name')
|
||
: (getStringField(orderObj, 'store_name') !== ''
|
||
? getStringField(orderObj, 'store_name')
|
||
: (getStringField(orderObj, 'shop_name') !== '' ? getStringField(orderObj, 'shop_name') : shopName.value))
|
||
if (shopNameValue !== '') {
|
||
shopName.value = shopNameValue
|
||
}
|
||
|
||
productAmount.value = getNumberField(orderObj, 'product_amount')
|
||
if (productAmount.value <= 0) {
|
||
productAmount.value = getNumberField(orderObj, 'subtotal_amount')
|
||
}
|
||
deliveryFee.value = getNumberField(orderObj, 'delivery_fee')
|
||
if (deliveryFee.value <= 0) {
|
||
deliveryFee.value = getNumberField(orderObj, 'shipping_fee')
|
||
}
|
||
redPacketDiscount.value = getNumberField(orderObj, 'red_packet_discount')
|
||
if (redPacketDiscount.value <= 0) {
|
||
redPacketDiscount.value = getNumberField(orderObj, 'coupon_discount')
|
||
}
|
||
memberDiscount.value = getNumberField(orderObj, 'member_discount')
|
||
const discountAmountValue = getNumberField(orderObj, 'discount_amount')
|
||
if (memberDiscount.value <= 0 && discountAmountValue > 0 && redPacketDiscount.value <= 0) {
|
||
redPacketDiscount.value = discountAmountValue
|
||
}
|
||
|
||
const totalAmountValue = getNumberField(orderObj, 'total_amount')
|
||
if (totalAmountValue > 0) {
|
||
amount.value = totalAmountValue
|
||
}
|
||
|
||
parseAddressFromOrder(orderObj)
|
||
parseItemsFromOrder(orderObj)
|
||
calculateProductAmountFromItems()
|
||
calculatePriceDetails(totalAmountValue)
|
||
|
||
const deadlineMs = resolvePaymentDeadline(orderObj)
|
||
|
||
if (isExpiredState(latestStatus, latestPayment, latestReason, effectivePayExpireAt) || (deadlineMs > 0 && deadlineMs <= Date.now())) {
|
||
await supabaseService.expireUnifiedOrder(orderId.value, source.value)
|
||
markPaymentExpiredInPage()
|
||
payExpireAtText.value = effectivePayExpireAt
|
||
paymentReady.value = true
|
||
return
|
||
}
|
||
|
||
if (deadlineMs <= 0) {
|
||
orderMissing.value = true
|
||
stopCountdown()
|
||
remainingSeconds.value = 0
|
||
uni.showToast({ title: '订单数据异常', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
paymentReady.value = true
|
||
startCountdown(deadlineMs)
|
||
} else {
|
||
orderMissing.value = true
|
||
paymentReady.value = false
|
||
stopCountdown()
|
||
remainingSeconds.value = 0
|
||
resetRealOrderView()
|
||
uni.showToast({ title: '订单不存在或已删除', icon: 'none' })
|
||
}
|
||
} catch (err) {
|
||
console.error('加载订单信息异常:', err)
|
||
orderMissing.value = true
|
||
paymentReady.value = false
|
||
stopCountdown()
|
||
remainingSeconds.value = 0
|
||
resetRealOrderView()
|
||
}
|
||
}
|
||
|
||
const getCurrentUserId = (): string => {
|
||
const userStore = uni.getStorageSync('userInfo')
|
||
if (userStore != null) {
|
||
const userStr = JSON.stringify(userStore)
|
||
const userParsed = JSON.parse(userStr)
|
||
if (userParsed != null) {
|
||
const userObj = userParsed as UTSJSONObject
|
||
const id = userObj.getString('id')
|
||
if (id != null) {
|
||
return id
|
||
}
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
const getMethodBrandIcon = (methodId: string): string => {
|
||
if (methodId === 'wechat') {
|
||
return '/static/logo.png'
|
||
} else if (methodId === 'alipay') {
|
||
return '/static/logo.png'
|
||
} else if (methodId === 'balance') {
|
||
return '/static/logo.png'
|
||
} else if (methodId === 'bankcard') {
|
||
return '/static/logo.png'
|
||
}
|
||
return '/static/logo.png'
|
||
}
|
||
|
||
const selectMethod = (method: PaymentMethodType) => {
|
||
if (!method.enabled) {
|
||
uni.showToast({
|
||
title: '该支付方式暂不可用',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
selectedMethod.value = method.id
|
||
showPassword.value = false
|
||
password.value = ''
|
||
}
|
||
|
||
const closePasswordPopup = () => {
|
||
showPassword.value = false
|
||
password.value = ''
|
||
}
|
||
|
||
const openCancelSheet = () => {
|
||
if (!paymentReady.value || orderMissing.value || isPaymentExpired.value) {
|
||
return
|
||
}
|
||
showCancelSheet.value = true
|
||
}
|
||
|
||
const closeCancelSheet = () => {
|
||
showCancelSheet.value = false
|
||
selectedCancelReason.value = ''
|
||
otherCancelReason.value = ''
|
||
}
|
||
|
||
const selectCancelReason = (reasonId: string) => {
|
||
selectedCancelReason.value = reasonId
|
||
if (reasonId !== 'other') {
|
||
otherCancelReason.value = ''
|
||
}
|
||
}
|
||
|
||
const getSelectedCancelReasonText = (): string => {
|
||
for (let i = 0; i < cancelReasonOptions.value.length; i++) {
|
||
const item = cancelReasonOptions.value[i]
|
||
if (item.id === selectedCancelReason.value) {
|
||
if (item.id === 'other' && otherCancelReason.value !== '') {
|
||
return item.label + ':' + otherCancelReason.value
|
||
}
|
||
return item.label
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
const confirmCancelOrder = async () => {
|
||
if (orderMissing.value) {
|
||
uni.showToast({ title: '订单不存在或已删除', icon: 'none' })
|
||
return
|
||
}
|
||
if (selectedCancelReason.value === '') {
|
||
uni.showToast({ title: '请选择取消原因', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
isCancelling.value = true
|
||
uni.showLoading({ title: '取消中...' })
|
||
const cancelReasonText = getSelectedCancelReasonText()
|
||
const cancelledStatus = getCancelledStatusBySource()
|
||
let success = false
|
||
try {
|
||
if (orderId.value !== '') {
|
||
success = await supabaseService.cancelUnifiedOrder(orderId.value, source.value, cancelReasonText)
|
||
} else {
|
||
success = true
|
||
}
|
||
if (!success && orderId.value === '') {
|
||
success = true
|
||
}
|
||
if (success) {
|
||
applyOrderState(cancelledStatus, PAYMENT_STATUS_TIMEOUT, cancelReasonText, payExpireAtText.value)
|
||
updateOrderInStorage(orderId.value, cancelledStatus, cancelReasonText)
|
||
uni.$emit('orderUpdated', { orderId: orderId.value, status: cancelledStatus, paymentStatus: PAYMENT_STATUS_TIMEOUT, cancelReason: cancelReasonText, payExpireAt: payExpireAtText.value })
|
||
closeCancelSheet()
|
||
stopCountdown()
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '订单已取消', icon: 'success' })
|
||
navigateAfterCancel()
|
||
} else {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '取消失败', icon: 'none' })
|
||
}
|
||
} catch (err) {
|
||
console.error('取消订单异常:', err)
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '取消失败', icon: 'none' })
|
||
} finally {
|
||
isCancelling.value = false
|
||
}
|
||
}
|
||
|
||
const showLeaveConfirm = () => {
|
||
uni.showModal({
|
||
title: '离开支付页',
|
||
content: '确定离开支付页面?订单将保留在待付款中',
|
||
confirmText: '确定离开',
|
||
cancelText: '继续支付',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/orders'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const navigateBackDirect = () => {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/orders'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const shouldShowLeaveConfirm = (): boolean => {
|
||
return paymentReady.value
|
||
&& !orderMissing.value
|
||
&& !isPaymentExpired.value
|
||
&& orderStatus.value == ORDER_STATUS_PENDING
|
||
&& paymentStatus.value == PAYMENT_STATUS_UNPAID
|
||
&& remainingSeconds.value > 0
|
||
}
|
||
|
||
const goBack = () => {
|
||
if (shouldShowLeaveConfirm()) {
|
||
showLeaveConfirm()
|
||
return
|
||
}
|
||
navigateBackDirect()
|
||
}
|
||
|
||
const goEditAddress = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/address'
|
||
})
|
||
}
|
||
|
||
const copyOrderNo = () => {
|
||
if (orderNo.value === '') {
|
||
uni.showToast({ title: '暂无订单编号', icon: 'none' })
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: orderNo.value,
|
||
success: () => {
|
||
uni.showToast({ title: '订单编号已复制', icon: 'success' })
|
||
}
|
||
})
|
||
}
|
||
|
||
const goToProductDetail = (productId: string) => {
|
||
const targetId = productId !== '' ? productId : 'mock-product'
|
||
uni.navigateTo({ url: '/pages/mall/consumer/product-detail?id=' + targetId })
|
||
// TODO: 如果商品详情页参数要求 productId,可在该路由稳定后统一切换
|
||
}
|
||
|
||
const confirmPayment = async () => {
|
||
if (isPaying.value) return
|
||
if (orderMissing.value || !paymentReady.value) {
|
||
uni.showToast({ title: '订单不存在或已删除', icon: 'none' })
|
||
return
|
||
}
|
||
if (isPaymentExpired.value || remainingSeconds.value <= 0) {
|
||
markPaymentExpiredInPage()
|
||
uni.showToast({ title: '订单已取消,不能继续支付', icon: 'none' })
|
||
return
|
||
}
|
||
if (!ensureLoggedIn()) return
|
||
|
||
isPaying.value = true
|
||
|
||
try {
|
||
if (orderId.value !== '') {
|
||
const latestOrder = await supabaseService.getUnifiedOrderDetail(orderId.value, source.value)
|
||
if (latestOrder == null) {
|
||
orderMissing.value = true
|
||
paymentReady.value = false
|
||
stopCountdown()
|
||
uni.showToast({ title: '订单不存在或已删除', icon: 'none' })
|
||
isPaying.value = false
|
||
return
|
||
}
|
||
const latestStatus = getNumberField(latestOrder, 'order_status')
|
||
const latestPayment = getNumberField(latestOrder, 'payment_status')
|
||
const latestReason = getStringField(latestOrder, 'cancel_reason')
|
||
const latestExpire = getStringField(latestOrder, 'pay_expire_at')
|
||
applyOrderState(latestStatus, latestPayment, latestReason, latestExpire)
|
||
const latestState = getUnifiedDisplayState(createCurrentOrderSource(latestStatus, latestPayment, latestReason, latestExpire))
|
||
if (latestState == 'expired' || latestState == 'cancelled') {
|
||
await supabaseService.expireUnifiedOrder(orderId.value, source.value)
|
||
markPaymentExpiredInPage()
|
||
uni.showToast({ title: '订单已取消,不能继续支付', icon: 'none' })
|
||
isPaying.value = false
|
||
return
|
||
}
|
||
if (latestState != 'pending_pay') {
|
||
uni.showToast({ title: '订单状态已变更', icon: 'none' })
|
||
isPaying.value = false
|
||
return
|
||
}
|
||
}
|
||
|
||
const paymentResult = await requestWechatPaymentResult()
|
||
if (paymentResult == 'cancel') {
|
||
if (orderId.value !== '') {
|
||
await supabaseService.markUnifiedOrderPaymentCancelled(orderId.value, source.value)
|
||
}
|
||
applyOrderState(ORDER_STATUS_PENDING, PAYMENT_STATUS_UNPAID, '', payExpireAtText.value)
|
||
showPassword.value = false
|
||
password.value = ''
|
||
isPaying.value = false
|
||
uni.$emit('orderUpdated', { orderId: orderId.value, status: ORDER_STATUS_PENDING, paymentStatus: PAYMENT_STATUS_UNPAID, cancelReason: '', payExpireAt: payExpireAtText.value })
|
||
uni.showToast({ title: '已保留待付款订单', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
if (paymentResult == 'fail') {
|
||
isPaying.value = false
|
||
uni.showToast({ title: '支付未完成,请重试', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.showLoading({ title: '支付中...' })
|
||
console.log('[confirmPayment] 开始支付, orderId:', orderId.value, 'method:', selectedMethod.value)
|
||
|
||
const success = await supabaseService.payUnifiedOrder(orderId.value, source.value, selectedMethod.value, payAmount.value)
|
||
console.log('[confirmPayment] 支付结果:', success)
|
||
|
||
if (!success) {
|
||
console.error('[confirmPayment] payOrder 返回 false')
|
||
if (orderId.value !== '') {
|
||
const latestOrder = await supabaseService.getUnifiedOrderDetail(orderId.value, source.value)
|
||
if (latestOrder != null) {
|
||
const latestStatus = getNumberField(latestOrder, 'order_status')
|
||
const latestPayment = getNumberField(latestOrder, 'payment_status')
|
||
const latestReason = getStringField(latestOrder, 'cancel_reason')
|
||
const latestExpire = getStringField(latestOrder, 'pay_expire_at')
|
||
applyOrderState(latestStatus, latestPayment, latestReason, latestExpire)
|
||
if (isExpiredState(latestStatus, latestPayment, latestReason, latestExpire)) {
|
||
markPaymentExpiredInPage()
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '订单已取消,不能继续支付', icon: 'none' })
|
||
isPaying.value = false
|
||
return
|
||
}
|
||
}
|
||
}
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '支付处理失败',
|
||
icon: 'none'
|
||
})
|
||
isPaying.value = false
|
||
return
|
||
}
|
||
|
||
uni.hideLoading()
|
||
stopCountdown()
|
||
orderStatus.value = ORDER_STATUS_PAID_OR_SHIPPING
|
||
paymentStatus.value = PAYMENT_STATUS_PAID
|
||
isPaymentExpired.value = false
|
||
paymentReady.value = true
|
||
updateOrderInStorage(orderId.value, ORDER_STATUS_PAID_OR_SHIPPING)
|
||
|
||
uni.showToast({
|
||
title: '支付成功',
|
||
icon: 'success',
|
||
duration: 2000
|
||
})
|
||
|
||
uni.$emit('orderUpdated', { orderId: orderId.value, status: ORDER_STATUS_PAID_OR_SHIPPING, paymentStatus: PAYMENT_STATUS_PAID })
|
||
|
||
if (source.value == 'service' || bizType.value == 'service') {
|
||
isDispatchSubmitting.value = true
|
||
uni.showLoading({ title: '正在安排服务', mask: true })
|
||
try {
|
||
const dispatchResult = await dispatchPaidHomecareOrder(orderId.value)
|
||
uni.hideLoading()
|
||
if (dispatchResult.success) {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/home-service/order-detail?id=' + orderId.value
|
||
})
|
||
return
|
||
}
|
||
showHomecareDispatchFailureModal(orderId.value, dispatchResult, (id: string) => {
|
||
retryHomecareDispatch(id)
|
||
})
|
||
} catch (e) {
|
||
uni.hideLoading()
|
||
console.error('[confirmPayment] 派单异常:', e)
|
||
uni.showModal({
|
||
title: '派单服务异常',
|
||
content: '派单服务暂时异常,请稍后重试',
|
||
showCancel: true,
|
||
cancelText: '稍后再试',
|
||
confirmText: '重新派单',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
retryHomecareDispatch(orderId.value)
|
||
}
|
||
}
|
||
})
|
||
} finally {
|
||
isDispatchSubmitting.value = false
|
||
}
|
||
return
|
||
}
|
||
|
||
setTimeout(() => {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/payment-success?orderId=' + orderId.value
|
||
})
|
||
}, 1500)
|
||
|
||
} catch (err) {
|
||
console.error('[confirmPayment] 支付异常:', err)
|
||
try {
|
||
uni.hideLoading()
|
||
} catch (e) {
|
||
}
|
||
uni.showToast({
|
||
title: '支付失败',
|
||
icon: 'none'
|
||
})
|
||
isPaying.value = false
|
||
}
|
||
}
|
||
|
||
const verifyPassword = async () => {
|
||
const userId = getCurrentUserId()
|
||
if (userId === '') {
|
||
password.value = ''
|
||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||
showPassword.value = false
|
||
return
|
||
}
|
||
|
||
try {
|
||
await new Promise<void>((resolve: (value: void) => void) => {
|
||
setTimeout(() => {
|
||
resolve()
|
||
}, 500)
|
||
})
|
||
|
||
const isCorrect = true
|
||
|
||
if (isCorrect) {
|
||
confirmPayment()
|
||
} else {
|
||
password.value = ''
|
||
uni.showToast({
|
||
title: '密码错误',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('验证密码异常:', err)
|
||
}
|
||
}
|
||
|
||
const inputPassword = (num: string) => {
|
||
if (password.value.length >= 6) return
|
||
password.value += num
|
||
}
|
||
|
||
const deletePassword = () => {
|
||
if (password.value.length > 0) {
|
||
password.value = password.value.slice(0, -1)
|
||
}
|
||
}
|
||
|
||
watch(password, (newPassword: string) => {
|
||
if (newPassword.length === 6) {
|
||
verifyPassword()
|
||
}
|
||
})
|
||
|
||
const forgotPassword = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/user/forgot-password'
|
||
})
|
||
}
|
||
|
||
const retryHomecareDispatch = async (targetOrderId: string) => {
|
||
if (isDispatchSubmitting.value) {
|
||
return
|
||
}
|
||
isDispatchSubmitting.value = true
|
||
uni.showLoading({ title: '正在重新派单', mask: true })
|
||
try {
|
||
const result = await dispatchPaidHomecareOrder(targetOrderId)
|
||
uni.hideLoading()
|
||
if (result.success) {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/home-service/order-detail?id=' + targetOrderId
|
||
})
|
||
return
|
||
}
|
||
showHomecareDispatchFailureModal(targetOrderId, result, (id: string) => {
|
||
retryHomecareDispatch(id)
|
||
})
|
||
} catch (e) {
|
||
uni.hideLoading()
|
||
console.error('[retryHomecareDispatch] 重新派单异常:', e)
|
||
uni.showModal({
|
||
title: '派单服务异常',
|
||
content: '派单服务暂时异常,请稍后重试',
|
||
showCancel: true,
|
||
cancelText: '稍后再试',
|
||
confirmText: '重新派单',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
retryHomecareDispatch(targetOrderId)
|
||
}
|
||
}
|
||
})
|
||
} finally {
|
||
isDispatchSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
const goToOrderList = () => {
|
||
uni.redirectTo({
|
||
url: '/pages/mall/consumer/orders'
|
||
})
|
||
}
|
||
|
||
const goToShoppingHome = () => {
|
||
uni.switchTab({
|
||
url: '/pages/main/index'
|
||
})
|
||
}
|
||
|
||
onLoad((options) => {
|
||
if (!ensureLoggedIn()) {
|
||
return
|
||
}
|
||
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
|
||
navHeight.value = 44
|
||
capsuleRightReserve.value = 12
|
||
|
||
// #ifdef MP-WEIXIN
|
||
try {
|
||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||
navHeight.value = menuButton.height + 8
|
||
capsuleRightReserve.value = (systemInfo.windowWidth ?? 375) - menuButton.left + 8
|
||
} catch (e) {
|
||
navHeight.value = 44
|
||
capsuleRightReserve.value = 96
|
||
}
|
||
// #endif
|
||
|
||
loadPaymentMethods()
|
||
loadUserBalance()
|
||
|
||
if (options == null) {
|
||
enterPreviewMode()
|
||
return
|
||
}
|
||
|
||
const optionsObj = options as UTSJSONObject
|
||
const orderIdValue = getOptionString(optionsObj, 'orderId')
|
||
if (orderIdValue != '') {
|
||
orderId.value = orderIdValue
|
||
}
|
||
const sourceValue = getOptionString(optionsObj, 'source')
|
||
if (sourceValue != '') {
|
||
source.value = sourceValue
|
||
}
|
||
const bizTypeValue = getOptionString(optionsObj, 'bizType')
|
||
if (bizTypeValue != '') {
|
||
bizType.value = bizTypeValue
|
||
}
|
||
if (source.value == '') {
|
||
source.value = orderId.value.startsWith('so-') ? 'service' : 'goods'
|
||
}
|
||
if (bizType.value == '') {
|
||
bizType.value = source.value
|
||
}
|
||
|
||
const orderNoValue = getOptionString(optionsObj, 'orderNo')
|
||
if (orderNoValue != '') {
|
||
orderNo.value = orderNoValue
|
||
}
|
||
|
||
const amountValue = getOptionString(optionsObj, 'amount')
|
||
if (amountValue != '') {
|
||
amount.value = parseNumberValue(amountValue)
|
||
}
|
||
|
||
const productAmountValue = getOptionString(optionsObj, 'productAmount')
|
||
if (productAmountValue != '') {
|
||
productAmount.value = parseNumberValue(productAmountValue)
|
||
}
|
||
|
||
const deliveryFeeValue = getOptionString(optionsObj, 'deliveryFee')
|
||
if (deliveryFeeValue != '') {
|
||
deliveryFee.value = parseNumberValue(deliveryFeeValue)
|
||
}
|
||
|
||
const redPacketValue = getOptionString(optionsObj, 'redPacketDiscount')
|
||
if (redPacketValue != '') {
|
||
redPacketDiscount.value = parseNumberValue(redPacketValue)
|
||
}
|
||
|
||
const memberDiscountValue = getOptionString(optionsObj, 'memberDiscount')
|
||
if (memberDiscountValue != '') {
|
||
memberDiscount.value = parseNumberValue(memberDiscountValue)
|
||
}
|
||
|
||
const discountAmountValue = getOptionString(optionsObj, 'discountAmount')
|
||
if (discountAmountValue != '' && redPacketDiscount.value <= 0 && memberDiscount.value <= 0) {
|
||
redPacketDiscount.value = parseNumberValue(discountAmountValue)
|
||
}
|
||
|
||
const createdAtValue = getOptionString(optionsObj, 'createdAt')
|
||
if (createdAtValue != '') {
|
||
orderCreatedAt.value = createdAtValue
|
||
}
|
||
|
||
if (orderId.value != '') {
|
||
loadOrderInfo()
|
||
} else {
|
||
enterPreviewMode()
|
||
}
|
||
})
|
||
|
||
onShow(() => {
|
||
if (orderId.value !== '') {
|
||
loadOrderInfo()
|
||
return
|
||
}
|
||
if (!isPreviewMode.value) {
|
||
return
|
||
}
|
||
updateCountdownByDeadline()
|
||
if (!isPaymentExpired.value && paymentDeadlineMs.value > 0 && countdownTimer <= 0 && remainingSeconds.value > 0) {
|
||
startCountdown(paymentDeadlineMs.value)
|
||
}
|
||
})
|
||
|
||
onHide(() => {
|
||
stopCountdown()
|
||
})
|
||
|
||
onReachBottom(() => {
|
||
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
|
||
})
|
||
|
||
onBackPress((options) => {
|
||
if (options.from === 'navigateBack') {
|
||
return false
|
||
}
|
||
|
||
if (showCancelSheet.value) {
|
||
closeCancelSheet()
|
||
return true
|
||
}
|
||
|
||
if (showPassword.value) {
|
||
closePasswordPopup()
|
||
return true
|
||
}
|
||
|
||
if (shouldShowLeaveConfirm()) {
|
||
showLeaveConfirm()
|
||
return true
|
||
}
|
||
return false
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopCountdown()
|
||
})
|
||
|
||
onUnload(() => {
|
||
stopCountdown()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.payment-page {
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
padding-bottom: calc(96px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.jd-pay-header {
|
||
background: linear-gradient(180deg, #ffffff 0%, #f7f7f7 100%);
|
||
padding-left: 12px;
|
||
padding-right: 12px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.jd-nav-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.jd-left-capsule {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
height: 32px;
|
||
padding: 0 10px;
|
||
border-radius: 16px;
|
||
background-color: #ffffff;
|
||
border: 1px solid #eaeaea;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.jd-back-icon,
|
||
.jd-menu-icon {
|
||
font-size: 18px;
|
||
line-height: 18px;
|
||
color: #222222;
|
||
font-weight: 600;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.jd-nav-divider {
|
||
width: 1px;
|
||
height: 16px;
|
||
background-color: #ebebeb;
|
||
margin: 0 6px;
|
||
}
|
||
|
||
.jd-title-center {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.jd-title-clock {
|
||
font-size: 18px;
|
||
color: #222222;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.jd-title-text {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #222222;
|
||
}
|
||
|
||
.jd-countdown-row {
|
||
padding-top: 12px;
|
||
padding-bottom: 6px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.jd-countdown-prefix,
|
||
.jd-countdown-suffix {
|
||
font-size: 14px;
|
||
color: #666666;
|
||
}
|
||
|
||
.jd-countdown-number {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.jd-expired-text {
|
||
font-size: 14px;
|
||
color: #999999;
|
||
}
|
||
|
||
.jd-pay-notice {
|
||
margin: 10px 12px 0;
|
||
padding: 10px 12px;
|
||
background-color: #fff6d8;
|
||
border-radius: 10px;
|
||
border: 1px solid #f4d98a;
|
||
}
|
||
|
||
.jd-pay-notice-text {
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: #8a6700;
|
||
}
|
||
|
||
.payment-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 12px;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.card {
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
padding: 14px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.missing-card {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.missing-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.missing-desc {
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: #666666;
|
||
margin-top: 8px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.missing-btn {
|
||
height: 40px;
|
||
line-height: 40px;
|
||
border-radius: 20px;
|
||
padding: 0 20px;
|
||
background-color: #ff4d4f;
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
border-width: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.card-header-row,
|
||
.shop-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.card-title,
|
||
.shop-name {
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
}
|
||
|
||
.card-action {
|
||
font-size: 13px;
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.address-main-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.address-marker {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 12px;
|
||
background-color: #ff4d4f;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 10px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.address-marker-text {
|
||
font-size: 12px;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.address-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.address-user-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.address-name {
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.address-phone {
|
||
font-size: 13px;
|
||
color: #666666;
|
||
}
|
||
|
||
.address-detail {
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: #333333;
|
||
}
|
||
|
||
.goods-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
padding-top: 12px;
|
||
padding-bottom: 12px;
|
||
border-top: 1px solid #f3f3f3;
|
||
}
|
||
|
||
.goods-item:first-of-type {
|
||
border-top-width: 0;
|
||
padding-top: 0;
|
||
}
|
||
|
||
.goods-image {
|
||
width: 84px;
|
||
height: 84px;
|
||
border-radius: 10px;
|
||
background-color: #f2f2f2;
|
||
margin-right: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.goods-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.goods-name {
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
color: #222222;
|
||
max-height: 40px;
|
||
}
|
||
|
||
.goods-spec {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.goods-meta-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.goods-price,
|
||
.total-value {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.goods-count {
|
||
font-size: 13px;
|
||
color: #999999;
|
||
}
|
||
|
||
.fee-row,
|
||
.info-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.fee-row-total {
|
||
margin-top: 4px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f3f3f3;
|
||
}
|
||
|
||
.fee-label,
|
||
.info-label {
|
||
font-size: 13px;
|
||
color: #666666;
|
||
}
|
||
|
||
.fee-value,
|
||
.info-value {
|
||
font-size: 13px;
|
||
color: #222222;
|
||
}
|
||
|
||
.fee-discount {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.total-label {
|
||
font-weight: bold;
|
||
color: #222222;
|
||
}
|
||
|
||
.info-value-action {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-copy {
|
||
font-size: 12px;
|
||
color: #ff4d4f;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.method-section {
|
||
margin-top: 10px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f3f3f3;
|
||
}
|
||
|
||
.method-section-title {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.method-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.method-item-modern {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.method-item-modern:last-child {
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.method-left {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.method-img {
|
||
width: 28px;
|
||
height: 28px;
|
||
margin-right: 12px;
|
||
border-radius: 14px;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.method-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
}
|
||
|
||
.method-name {
|
||
font-size: 14px;
|
||
color: #222222;
|
||
}
|
||
|
||
.method-desc {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.method-right {
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.radio-circle {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 10px;
|
||
border: 1px solid #dddddd;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.radio-circle.checked {
|
||
border-color: #ff4d4f;
|
||
background-color: #ff4d4f;
|
||
}
|
||
|
||
.radio-inner {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 5px;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.bottom-safe-space {
|
||
height: 12px;
|
||
}
|
||
|
||
.payment-bottom {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 99;
|
||
background-color: #ffffff;
|
||
border-top: 1px solid #f0f0f0;
|
||
padding: 12px;
|
||
padding-bottom: constant(safe-area-inset-bottom);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.payment-expired-bottom {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 99;
|
||
background-color: #ffffff;
|
||
border-top: 1px solid #f0f0f0;
|
||
padding: 12px;
|
||
padding-bottom: constant(safe-area-inset-bottom);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.payment-expired-btn {
|
||
flex: 1;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
border-radius: 22px;
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
border-width: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.payment-expired-btn.secondary {
|
||
margin-right: 10px;
|
||
background-color: #f0f0f0;
|
||
color: #666666;
|
||
}
|
||
|
||
.payment-expired-btn.primary {
|
||
background-color: #ff4d4f;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.cancel-btn,
|
||
.pay-btn,
|
||
.sheet-btn {
|
||
height: 44px;
|
||
line-height: 44px;
|
||
border-radius: 22px;
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
border-width: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.cancel-btn {
|
||
width: 112px;
|
||
background-color: #f0f0f0;
|
||
color: #666666;
|
||
flex-shrink: 0;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.pay-btn {
|
||
flex: 1;
|
||
background-color: #ff4d4f;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.pay-btn.disabled {
|
||
background-color: #ffb3b4;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.pay-text {
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.sheet-mask,
|
||
.password-popup-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.cancel-sheet,
|
||
.password-popup-content {
|
||
background-color: #ffffff;
|
||
border-radius: 16px 16px 0 0;
|
||
animation: slideUp 0.3s ease-out;
|
||
width: 100%;
|
||
}
|
||
|
||
.cancel-sheet {
|
||
height: 75%;
|
||
padding: 18px 16px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sheet-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.sheet-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.sheet-subtitle {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
}
|
||
|
||
.sheet-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.reason-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 4px;
|
||
border-bottom: 1px solid #f4f4f4;
|
||
}
|
||
|
||
.reason-text {
|
||
font-size: 15px;
|
||
color: #222222;
|
||
}
|
||
|
||
.sheet-radio {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.other-reason-box {
|
||
padding-top: 12px;
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
.other-reason-input {
|
||
height: 42px;
|
||
background-color: #f7f7f7;
|
||
border-radius: 10px;
|
||
padding-left: 12px;
|
||
padding-right: 12px;
|
||
font-size: 14px;
|
||
color: #333333;
|
||
}
|
||
|
||
.sheet-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
padding-top: 12px;
|
||
padding-bottom: constant(safe-area-inset-bottom);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.sheet-btn {
|
||
flex: 1;
|
||
}
|
||
|
||
.sheet-btn-secondary {
|
||
background-color: #f0f0f0;
|
||
color: #666666;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.sheet-btn-primary {
|
||
background-color: #ff4d4f;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.password-popup-content {
|
||
padding: 20px 0 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.popup-header {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.popup-close {
|
||
font-size: 20px;
|
||
color: #999999;
|
||
padding: 4px;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.popup-placeholder {
|
||
width: 28px;
|
||
}
|
||
|
||
.popup-amount-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.popup-amount-label {
|
||
font-size: 14px;
|
||
color: #666666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.popup-amount-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.popup-currency {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-right: 2px;
|
||
}
|
||
|
||
.popup-value {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.password-input-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.password-box {
|
||
width: 45px;
|
||
height: 45px;
|
||
border: 1px solid #dddddd;
|
||
border-right-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
.password-box:first-child {
|
||
border-radius: 4px 0 0 4px;
|
||
}
|
||
|
||
.password-box:last-child {
|
||
border-right-width: 1px;
|
||
border-radius: 0 4px 4px 0;
|
||
}
|
||
|
||
.password-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 5px;
|
||
background-color: #000000;
|
||
}
|
||
|
||
.forgot-password-link {
|
||
font-size: 13px;
|
||
color: #576b95;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.password-keyboard-popup {
|
||
width: 100%;
|
||
background-color: #f5f5f5;
|
||
padding: 6px;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.keyboard-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.keyboard-key {
|
||
width: 33.33%;
|
||
background-color: #ffffff;
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid #f5f5f5;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.keyboard-grid-popup {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.keyboard-key-popup {
|
||
height: 54px;
|
||
border: 3px solid #f5f5f5;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.keyboard-key-empty {
|
||
background-color: transparent;
|
||
}
|
||
|
||
.keyboard-key:active {
|
||
background-color: #e0e0e0;
|
||
}
|
||
|
||
.key-text {
|
||
font-size: 24px;
|
||
color: #333333;
|
||
}
|
||
|
||
.key-text-popup {
|
||
font-size: 22px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
transform: translateY(100%);
|
||
}
|
||
to {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
</style>
|