Files
lilishop-uniapp/pages/order/appointment/appointmentReschedule.vue
pikachu1995@126.com 2107e3f03a feat: 补齐预约下单售后链路,并接入礼品卡结算
立即购买先选时段再结算,免物流地址,售后按数量退款;同步接入礼品卡入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 17:07:27 +08:00

235 lines
6.2 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<view class="section current">
<view class="section-title">当前预约</view>
<text class="current-text">{{ currentSlotText }}</text>
</view>
<view class="section">
<view class="section-title">新预约日期</view>
<picker mode="date" :value="slotDate" :start="minDate" :end="maxDate" @change="onDateChange">
<view class="picker-row">
<text>{{ slotDate || '请选择日期' }}</text>
<u-icon name="arrow-right" color="#ccc" size="16" />
</view>
</picker>
</view>
<view class="section">
<view class="section-title">新预约时段</view>
<view v-if="loadingSlots" class="tip">加载时段中...</view>
<view v-else-if="!slots.length" class="tip">该日期暂无可预约时段</view>
<view v-else class="slot-list">
<view
v-for="slot in slots"
:key="slot.id"
class="slot-item"
:class="{ active: selectedSlot && selectedSlot.id === slot.id }"
@click="selectSlot(slot)"
>
{{ slot.slotStartTime }}-{{ slot.slotEndTime }}
<text v-if="slot.showRemainingQty && slot.availableQty != null" class="slot-qty">
{{ slot.availableQty }}
</text>
</view>
</view>
</view>
<view class="section">
<view class="section-title">改期说明选填</view>
<u-input v-model="remark" type="textarea" placeholder="如有需要请填写改期原因" border="surround" height="100" />
</view>
<view class="footer">
<u-button type="primary" shape="circle" :loading="submitting" @click="submit">提交改期</u-button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { listSlots, getAppointmentOrderDetail, requestReschedule, getGoodsExt } from '@/api/appointment.js'
import { buildBookableDates, formatDate } from '@/utils/appointmentGoods.js'
const orderSn = ref('')
const skuId = ref('')
const goodsId = ref('')
const quantity = ref(1)
const currentDetail = ref<any>({})
const goodsExt = ref<any>(null)
const slotDate = ref('')
const slots = ref<any[]>([])
const selectedSlot = ref<any>(null)
const remark = ref('')
const loadingSlots = ref(false)
const submitting = ref(false)
const bookableDates = ref<string[]>([])
const minDate = computed(() => bookableDates.value[0] || formatDate(new Date()))
const maxDate = computed(() => {
const list = bookableDates.value
return list.length ? list[list.length - 1] : minDate.value
})
const currentSlotText = computed(() => {
const d = currentDetail.value
if (!d?.slotDate) return '加载中...'
return `${d.slotDate} ${d.slotStartTime || ''}-${d.slotEndTime || ''}`
})
onLoad((options) => {
orderSn.value = options?.orderSn || ''
})
onMounted(async () => {
if (!orderSn.value) return
try {
const res = await getAppointmentOrderDetail(orderSn.value)
currentDetail.value = res.data?.result || {}
skuId.value = currentDetail.value.skuId || ''
goodsId.value = currentDetail.value.goodsId || ''
quantity.value = currentDetail.value.quantity || 1
if (goodsId.value) {
const extRes = await getGoodsExt(goodsId.value)
goodsExt.value = extRes.data?.result || null
bookableDates.value = buildBookableDates(goodsExt.value)
} else {
bookableDates.value = buildBookableDates(null)
}
slotDate.value = bookableDates.value[0] || formatDate(new Date())
await loadSlots()
} catch {
uni.showToast({ title: '加载预约信息失败', icon: 'none' })
}
})
watch(slotDate, () => {
selectedSlot.value = null
loadSlots()
})
async function loadSlots() {
if (!skuId.value || !slotDate.value) return
loadingSlots.value = true
try {
const res = await listSlots(skuId.value, slotDate.value)
slots.value = res.data?.result || []
} catch {
slots.value = []
} finally {
loadingSlots.value = false
}
}
function onDateChange(e: any) {
slotDate.value = e.detail?.value || ''
}
function selectSlot(slot: any) {
const need = quantity.value || 1
if (slot?.availableQty != null && slot.availableQty < need) {
uni.showToast({ title: '该时段可约数量不足', icon: 'none' })
return
}
selectedSlot.value = slot
}
async function submit() {
if (!selectedSlot.value) {
uni.showToast({ title: '请选择新时段', icon: 'none' })
return
}
submitting.value = true
try {
const slot = selectedSlot.value
const res = await requestReschedule(orderSn.value, {
slotStockId: slot.id,
slotDate: slotDate.value,
slotStartTime: slot.slotStartTime,
slotEndTime: slot.slotEndTime,
remark: remark.value.trim(),
})
const action = res.data?.result?.action
uni.showToast({
title: action === 'APPLY' ? '改期申请已提交,等待审核' : '改期成功',
icon: 'none',
})
setTimeout(() => {
uni.navigateBack()
}, 1200)
} catch (e: any) {
uni.showToast({
title: e?.data?.message || e?.message || '改期失败',
icon: 'none',
})
} finally {
submitting.value = false
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f5f5f5;
padding: 24rpx 24rpx 160rpx;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 20rpx;
}
.current-text {
font-size: 28rpx;
color: #333;
}
.picker-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
color: #333;
}
.tip {
color: #999;
font-size: 26rpx;
}
.slot-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.slot-item {
padding: 16rpx 28rpx;
background: #f2f2f2;
border-radius: 30rpx;
font-size: 24rpx;
color: #333;
border: 2rpx solid transparent;
}
.slot-item.active {
background: #fff5f5;
border-color: #f2270c;
color: #f2270c;
}
.slot-qty {
margin-left: 8rpx;
color: #999;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
}
</style>