mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-09-22 12:52:01 +08:00
feat: 补齐预约下单售后链路,并接入礼品卡结算
立即购买先选时段再结算,免物流地址,售后按数量退款;同步接入礼品卡入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
52
api/appointment.js
Normal file
52
api/appointment.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 预约服务买家端 API
|
||||||
|
*/
|
||||||
|
import { http, Method } from '@/utils/request.js'
|
||||||
|
|
||||||
|
/** 查询可预约时段 */
|
||||||
|
export function listSlots(skuId, slotDate) {
|
||||||
|
return http.request({
|
||||||
|
url: '/buyer/appointment/slots',
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
params: { skuId, slotDate },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定预约结算上下文(addToCart 后、createTrade 前) */
|
||||||
|
export function bindCheckout(data) {
|
||||||
|
return http.request({
|
||||||
|
url: '/buyer/appointment/checkout/bind',
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约商品扩展配置 */
|
||||||
|
export function getGoodsExt(goodsId) {
|
||||||
|
return http.request({
|
||||||
|
url: `/buyer/appointment/goods/${goodsId}/ext`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 买家预约订单详情(时段、核销码) */
|
||||||
|
export function getAppointmentOrderDetail(orderSn) {
|
||||||
|
return http.request({
|
||||||
|
url: `/buyer/appointment/order/${orderSn}`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改期申请 */
|
||||||
|
export function requestReschedule(orderSn, payload) {
|
||||||
|
return http.request({
|
||||||
|
url: `/buyer/appointment/orders/${orderSn}/reschedule`,
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
data: payload || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -202,4 +202,40 @@ export function getMineBargainLog(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询当前客户礼品卡
|
||||||
|
*/
|
||||||
|
export function getGiftCardCashMemberCardPage(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/promotion/giftCardCash/memberCard',
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定礼品卡(卡号 + 兑换码/卡密)
|
||||||
|
*/
|
||||||
|
export function bindGiftCardCashMemberCard(data) {
|
||||||
|
return http.request({
|
||||||
|
url: '/promotion/giftCardCash/memberCard/bind',
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
header: { 'content-type': 'application/json' },
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活礼品卡
|
||||||
|
*/
|
||||||
|
export function activateGiftCardCashMemberCard(memberCardId) {
|
||||||
|
return http.request({
|
||||||
|
url: `/promotion/giftCardCash/memberCard/${memberCardId}/activate`,
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ import { goodsFormatPrice } from '@/utils/filters.js'
|
|||||||
import {
|
import {
|
||||||
isECoupon,
|
isECoupon,
|
||||||
isVirtualGoods,
|
isVirtualGoods,
|
||||||
|
isAppointmentGoods,
|
||||||
getECouponStock,
|
getECouponStock,
|
||||||
getECouponMaxBuyNum,
|
getECouponMaxBuyNum,
|
||||||
} from '@/utils/goodsType.js'
|
} from '@/utils/goodsType.js'
|
||||||
@@ -173,12 +174,13 @@ const maxBuyNum = computed(() => {
|
|||||||
return 999
|
return 999
|
||||||
})
|
})
|
||||||
|
|
||||||
// 电子卡券禁止加购(后端 CARD_KEY_E_COUPON_CART_FORBIDDEN)
|
// 电子卡券/预约商品禁止加购
|
||||||
const showAddToCart = computed(() => {
|
const showAddToCart = computed(() => {
|
||||||
return (
|
return (
|
||||||
buyType.value != 'PINTUAN' &&
|
buyType.value != 'PINTUAN' &&
|
||||||
!isVirtualGoods(props.goodsDetail?.goodsType) &&
|
!isVirtualGoods(props.goodsDetail?.goodsType) &&
|
||||||
!isECoupon(props.goodsDetail?.goodsType)
|
!isECoupon(props.goodsDetail?.goodsType) &&
|
||||||
|
!isAppointmentGoods(props.goodsDetail?.goodsType)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -341,6 +343,14 @@ const addToCartOrBuy = (val: string) => {
|
|||||||
data.cartType = resolveCartType()
|
data.cartType = resolveCartType()
|
||||||
API_trade.addToCart(data).then((res) => {
|
API_trade.addToCart(data).then((res) => {
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
|
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
|
||||||
|
const goodsId = props.goodsDetail.goodsId || ''
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/order/appointment/appointmentCheckout?skuId=${props.goodsDetail.id}&goodsId=${goodsId}&num=${num.value}`,
|
||||||
|
})
|
||||||
|
closeMask()
|
||||||
|
return
|
||||||
|
}
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/order/fillorder?way=${data.cartType}&addr=${props.addr?.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value || ''))}`,
|
url: `/pages/order/fillorder?way=${data.cartType}&addr=${props.addr?.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value || ''))}`,
|
||||||
})
|
})
|
||||||
|
|||||||
18
pages.json
18
pages.json
@@ -771,6 +771,12 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "gift-card/myGiftCards",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的礼品卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "payment/payOrder",
|
"path": "payment/payOrder",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -867,6 +873,18 @@
|
|||||||
"enablePullDownRefresh": true
|
"enablePullDownRefresh": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "appointment/appointmentCheckout",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "预约信息"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "appointment/appointmentReschedule",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "申请改期"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "deliverDetail",
|
"path": "deliverDetail",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
432
pages/cart/gift-card/myGiftCards.vue
Normal file
432
pages/cart/gift-card/myGiftCards.vue
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="bind-section">
|
||||||
|
<view class="section-title">兑换礼品卡</view>
|
||||||
|
<view class="bind-row">
|
||||||
|
<text class="bind-label">卡号</text>
|
||||||
|
<u-input
|
||||||
|
v-model="bindForm.cardNo"
|
||||||
|
border="none"
|
||||||
|
maxlength="64"
|
||||||
|
placeholder="请输入卡号"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="bind-row">
|
||||||
|
<text class="bind-label">兑换码</text>
|
||||||
|
<u-input
|
||||||
|
v-model="bindForm.cardSecret"
|
||||||
|
border="none"
|
||||||
|
maxlength="128"
|
||||||
|
password
|
||||||
|
placeholder="请输入兑换码(卡密)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="bind-btn" :class="{ disabled: bindSubmitting }" @click="submitBind">
|
||||||
|
{{ bindSubmitting ? '兑换中...' : '兑换' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tabs-wrap">
|
||||||
|
<u-tabs
|
||||||
|
:list="tabList"
|
||||||
|
keyName="text"
|
||||||
|
:scrollable="false"
|
||||||
|
:inactiveStyle="{ color: '#333' }"
|
||||||
|
v-model:current="tabCurrentIndex"
|
||||||
|
:lineColor="lightColor"
|
||||||
|
:activeStyle="{ color: lightColor }"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="list-scroll" scroll-y @scrolltolower="loadMore">
|
||||||
|
<u-empty v-if="!loading && !list.length" mode="coupon" text="暂无礼品卡" />
|
||||||
|
<view
|
||||||
|
class="gcc-card"
|
||||||
|
:class="{ 'is-disabled': statusTab === 'UNAVAILABLE' }"
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.id || item.cardNo"
|
||||||
|
>
|
||||||
|
<view class="gcc-header">
|
||||||
|
<view class="gcc-header-pattern" />
|
||||||
|
<view class="gcc-header-inner">
|
||||||
|
<view class="gcc-header-left">
|
||||||
|
<view class="gcc-title">{{ item.giftCardName || '礼品卡' }}</view>
|
||||||
|
<view class="gcc-face">面值{{ item.faceText }}元</view>
|
||||||
|
<view class="gcc-cardno">{{ item.cardNo }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="gcc-header-right">
|
||||||
|
<view class="gcc-type">现金卡</view>
|
||||||
|
<view class="gcc-expire">{{ item.expireText }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="gcc-body">
|
||||||
|
<view class="gcc-row">
|
||||||
|
当前余额:
|
||||||
|
<text class="gcc-strong" v-if="item.balance != null">¥{{ unitPrice(item.balance) }}</text>
|
||||||
|
<text class="gcc-strong" v-else>—</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="gcc-action"
|
||||||
|
:class="{ 'is-disabled': actionDisabled || isActivating(item) }"
|
||||||
|
@click="onCardPrimaryAction(item)"
|
||||||
|
>
|
||||||
|
<text v-if="isActivating(item)">提交中...</text>
|
||||||
|
<text v-else>{{ actionLabel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="load-more" v-if="list.length">{{ loadStatusText }}</view>
|
||||||
|
<view class="notice">{{ useNotice }}</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
activateGiftCardCashMemberCard,
|
||||||
|
bindGiftCardCashMemberCard,
|
||||||
|
getGiftCardCashMemberCardPage,
|
||||||
|
} from '@/api/promotions.js'
|
||||||
|
import { unitPrice } from '@/utils/filters.js'
|
||||||
|
import {
|
||||||
|
decorateGiftCard,
|
||||||
|
GIFT_CARD_STATUS,
|
||||||
|
GIFT_CARD_USE_NOTICE,
|
||||||
|
giftCardActionLabel,
|
||||||
|
resolveMemberCardId,
|
||||||
|
} from '@/utils/giftCard.js'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const lightColor = computed(() => store.getters.lightColor)
|
||||||
|
const useNotice = GIFT_CARD_USE_NOTICE
|
||||||
|
|
||||||
|
const tabList = [
|
||||||
|
{ text: '可用', status: GIFT_CARD_STATUS.AVAILABLE },
|
||||||
|
{ text: '不可用', status: GIFT_CARD_STATUS.UNAVAILABLE },
|
||||||
|
{ text: '待激活', status: GIFT_CARD_STATUS.PENDING_ACTIVATION },
|
||||||
|
]
|
||||||
|
const tabCurrentIndex = ref(0)
|
||||||
|
const statusTab = computed(() => tabList[tabCurrentIndex.value].status)
|
||||||
|
const actionLabel = computed(() => giftCardActionLabel(statusTab.value))
|
||||||
|
const actionDisabled = computed(() => statusTab.value === GIFT_CARD_STATUS.UNAVAILABLE)
|
||||||
|
|
||||||
|
const bindForm = ref({ cardNo: '', cardSecret: '' })
|
||||||
|
const bindSubmitting = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref<any[]>([])
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
const loadStatus = ref('more')
|
||||||
|
const activatingId = ref('')
|
||||||
|
|
||||||
|
const loadStatusText = computed(() => (loadStatus.value === 'noMore' ? '没有更多了' : ''))
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
resetAndLoad()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(tabCurrentIndex, () => {
|
||||||
|
resetAndLoad()
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetAndLoad() {
|
||||||
|
pageNumber.value = 1
|
||||||
|
list.value = []
|
||||||
|
loadStatus.value = 'more'
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getList() {
|
||||||
|
if (loadStatus.value === 'noMore' && pageNumber.value > 1) return
|
||||||
|
loading.value = true
|
||||||
|
getGiftCardCashMemberCardPage({
|
||||||
|
pageNumber: pageNumber.value,
|
||||||
|
pageSize: 10,
|
||||||
|
sort: 'createTime',
|
||||||
|
order: 'desc',
|
||||||
|
memberCardStatus: statusTab.value,
|
||||||
|
}).then((res) => {
|
||||||
|
loading.value = false
|
||||||
|
if (!res.data.success) return
|
||||||
|
const records = ((res.data.result && res.data.result.records) || []).map((item: any) =>
|
||||||
|
decorateGiftCard(item)
|
||||||
|
)
|
||||||
|
if (pageNumber.value === 1) {
|
||||||
|
list.value = records
|
||||||
|
} else {
|
||||||
|
list.value = list.value.concat(records)
|
||||||
|
}
|
||||||
|
loadStatus.value = records.length < 10 ? 'noMore' : 'more'
|
||||||
|
}).catch(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
if (loadStatus.value === 'noMore' || loading.value) return
|
||||||
|
pageNumber.value += 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitBind() {
|
||||||
|
const cardNo = String(bindForm.value.cardNo || '').trim()
|
||||||
|
const cardSecret = String(bindForm.value.cardSecret || '').trim()
|
||||||
|
if (!cardNo) {
|
||||||
|
uni.showToast({ title: '请输入卡号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!cardSecret) {
|
||||||
|
uni.showToast({ title: '请输入兑换码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bindSubmitting.value) return
|
||||||
|
bindSubmitting.value = true
|
||||||
|
bindGiftCardCashMemberCard({ cardNo, cardSecret })
|
||||||
|
.then((res) => {
|
||||||
|
bindSubmitting.value = false
|
||||||
|
if (!res.data.success) return
|
||||||
|
uni.showToast({ title: res.data.message || '兑换成功', icon: 'none' })
|
||||||
|
bindForm.value = { cardNo: '', cardSecret: '' }
|
||||||
|
const pendingIndex = tabList.findIndex((item) => item.status === GIFT_CARD_STATUS.PENDING_ACTIVATION)
|
||||||
|
if (pendingIndex >= 0 && tabCurrentIndex.value !== pendingIndex) {
|
||||||
|
tabCurrentIndex.value = pendingIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resetAndLoad()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
bindSubmitting.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActivating(item: any) {
|
||||||
|
const id = resolveMemberCardId(item)
|
||||||
|
return !!activatingId.value && activatingId.value === id
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCardPrimaryAction(item: any) {
|
||||||
|
if (actionDisabled.value) return
|
||||||
|
if (statusTab.value === GIFT_CARD_STATUS.PENDING_ACTIVATION) {
|
||||||
|
const memberCardId = resolveMemberCardId(item)
|
||||||
|
if (!memberCardId) {
|
||||||
|
uni.showToast({ title: '卡片信息异常,无法激活', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activatingId.value) return
|
||||||
|
activatingId.value = memberCardId
|
||||||
|
activateGiftCardCashMemberCard(memberCardId)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.success) {
|
||||||
|
uni.showToast({ title: res.data.message || '激活成功', icon: 'none' })
|
||||||
|
const availableIndex = tabList.findIndex((item) => item.status === GIFT_CARD_STATUS.AVAILABLE)
|
||||||
|
if (availableIndex >= 0 && tabCurrentIndex.value !== availableIndex) {
|
||||||
|
tabCurrentIndex.value = availableIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resetAndLoad()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
activatingId.value = ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (statusTab.value === GIFT_CARD_STATUS.AVAILABLE) {
|
||||||
|
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-section {
|
||||||
|
margin: 24rpx;
|
||||||
|
padding: 28rpx 24rpx 24rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 88rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-label {
|
||||||
|
width: 110rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-btn {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #ff6b35, #ff4b2b);
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-wrap {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-card {
|
||||||
|
border-radius: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-header {
|
||||||
|
position: relative;
|
||||||
|
min-height: 180rpx;
|
||||||
|
background: linear-gradient(125deg, #ff9a4a 0%, #ff7729 42%, #ff8f3d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-header-pattern {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.22;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-36deg,
|
||||||
|
transparent,
|
||||||
|
transparent 10rpx,
|
||||||
|
rgba(255, 255, 255, 0.45) 10rpx,
|
||||||
|
rgba(255, 255, 255, 0.45) 12rpx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-header-inner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 28rpx 24rpx 24rpx;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-header-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-face {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-cardno {
|
||||||
|
margin-top: 28rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
opacity: 0.75;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-header-right {
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-type {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-expire {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-body {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-row {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-strong {
|
||||||
|
color: #ff6b22;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gcc-action {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e95b6c;
|
||||||
|
background: #faf4f5;
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
color: #c5c8ce;
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more,
|
||||||
|
.notice {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 8rpx 8rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<view class="goods-info">
|
<view class="goods-info">
|
||||||
<view class="goods-title">{{ getGoodsName(item) }}</view>
|
<view class="goods-title">{{ getGoodsName(item) }}</view>
|
||||||
<view class="goods-price">
|
<view class="goods-price">
|
||||||
<text>¥{{ unitPrice(applyInfo.applyRefundPrice) }}</text>
|
<text>¥{{ unitPrice(displayRefundPrice) }}</text>
|
||||||
<text class="num">购买数量: {{ item.num }}</text>
|
<text class="num">购买数量: {{ item.num }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -164,11 +164,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, getCurrentInstance } from 'vue'
|
import { ref, reactive, computed, getCurrentInstance, watch } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { unitPrice, parseGoodsImageUrl } from '@/utils/filters.js'
|
import { unitPrice, parseGoodsImageUrl } from '@/utils/filters.js'
|
||||||
import { getAfterSaleReason, applyReturn, getAfterSaleInfo } from '@/api/after-sale'
|
import { getAfterSaleReason, applyReturn, getAfterSaleInfo } from '@/api/after-sale'
|
||||||
|
import { isAppointmentOrder } from '@/utils/goodsType.js'
|
||||||
|
import { computePartialRefundPrice } from '@/utils/appointmentGoods.js'
|
||||||
import { handleUploadAfterRead } from '@/utils/uploadHelper.js'
|
import { handleUploadAfterRead } from '@/utils/uploadHelper.js'
|
||||||
import storage from '@/utils/storage.js'
|
import storage from '@/utils/storage.js'
|
||||||
|
|
||||||
@@ -185,6 +187,23 @@ const reasonSelectShow = ref(false)
|
|||||||
const reasonList = ref<any[]>([])
|
const reasonList = ref<any[]>([])
|
||||||
const applyInfo = ref<any>({})
|
const applyInfo = ref<any>({})
|
||||||
const uToast = ref<any>(null)
|
const uToast = ref<any>(null)
|
||||||
|
const isAppointmentAfterSale = ref(false)
|
||||||
|
|
||||||
|
const displayRefundPrice = computed(() => {
|
||||||
|
if (!isAppointmentAfterSale.value) return applyInfo.value?.applyRefundPrice
|
||||||
|
return computePartialRefundPrice(applyInfo.value, form.num)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.num,
|
||||||
|
() => {
|
||||||
|
if (!isAppointmentAfterSale.value) return
|
||||||
|
applyInfo.value = {
|
||||||
|
...applyInfo.value,
|
||||||
|
displayRefundPrice: displayRefundPrice.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
orderItemSn: '',
|
orderItemSn: '',
|
||||||
@@ -221,6 +240,7 @@ onLoad((options) => {
|
|||||||
uni.setNavigationBarTitle({ title: navTitle })
|
uni.setNavigationBarTitle({ title: navTitle })
|
||||||
sn.value = options?.sn || ''
|
sn.value = options?.sn || ''
|
||||||
sku.value = storage.getAfterSaleData()
|
sku.value = storage.getAfterSaleData()
|
||||||
|
isAppointmentAfterSale.value = isAppointmentOrder(sku.value?.orderType)
|
||||||
form.orderItemSn = options?.sn || ''
|
form.orderItemSn = options?.sn || ''
|
||||||
form.skuId = sku.value.skuId
|
form.skuId = sku.value.skuId
|
||||||
form.num = sku.value.num
|
form.num = sku.value.num
|
||||||
@@ -271,8 +291,12 @@ function init(orderItemSn: string) {
|
|||||||
icon: 'none',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
applyInfo.value = response.data.result
|
const result = response.data.result
|
||||||
form.accountType = response.data.result.accountType
|
applyInfo.value = {
|
||||||
|
...result,
|
||||||
|
totalNum: result.orderItemNum || sku.value?.num,
|
||||||
|
}
|
||||||
|
form.accountType = result.accountType
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -294,6 +318,12 @@ function reasonSelectConfirm(val: any) {
|
|||||||
|
|
||||||
function valChange(e: { value: number }) {
|
function valChange(e: { value: number }) {
|
||||||
form.num = e.value
|
form.num = e.value
|
||||||
|
if (isAppointmentAfterSale.value) {
|
||||||
|
applyInfo.value = {
|
||||||
|
...applyInfo.value,
|
||||||
|
applyRefundPrice: computePartialRefundPrice(applyInfo.value, form.num),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUploadAfterRead(event: any) {
|
function onUploadAfterRead(event: any) {
|
||||||
@@ -320,7 +350,9 @@ function onSubmit() {
|
|||||||
uni.showLoading({ title: '加载中' })
|
uni.showLoading({ title: '加载中' })
|
||||||
form.accountType = applyInfo.value.accountType
|
form.accountType = applyInfo.value.accountType
|
||||||
form.refundWay = applyInfo.value.refundWay
|
form.refundWay = applyInfo.value.refundWay
|
||||||
form.applyRefundPrice = applyInfo.value.applyRefundPrice
|
form.applyRefundPrice = isAppointmentAfterSale.value
|
||||||
|
? computePartialRefundPrice(applyInfo.value, form.num)
|
||||||
|
: applyInfo.value.applyRefundPrice
|
||||||
|
|
||||||
applyReturn(sn.value, form).then((resp) => {
|
applyReturn(sn.value, form).then((resp) => {
|
||||||
hideLoadingIfNeeded()
|
hideLoadingIfNeeded()
|
||||||
|
|||||||
563
pages/order/appointment/appointmentCheckout.vue
Normal file
563
pages/order/appointment/appointmentCheckout.vue
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<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" v-if="serviceModes.length > 1">
|
||||||
|
<view class="section-title">服务方式</view>
|
||||||
|
<view class="mode-list">
|
||||||
|
<view
|
||||||
|
v-for="mode in serviceModes"
|
||||||
|
:key="mode"
|
||||||
|
class="mode-item"
|
||||||
|
:class="{ active: serviceMode === mode }"
|
||||||
|
@click="serviceMode = mode"
|
||||||
|
>
|
||||||
|
{{ SERVICE_MODE_LABEL[mode] || mode }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">联系人</view>
|
||||||
|
<u-input v-model="contactName" placeholder="请输入联系人姓名" border="surround" />
|
||||||
|
<view class="input-gap" />
|
||||||
|
<u-input v-model="contactPhone" placeholder="请输入联系电话" type="number" border="surround" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section" v-if="serviceMode === SERVICE_MODE.ON_SITE">
|
||||||
|
<view class="section-title">服务地址</view>
|
||||||
|
<u-input
|
||||||
|
v-model="serviceAddress"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入上门服务地址"
|
||||||
|
border="surround"
|
||||||
|
height="100"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section" v-for="(group, gi) in formAnswers" :key="gi" v-show="formFields.length">
|
||||||
|
<view class="section-title">
|
||||||
|
{{ formAnswers.length > 1 ? `补充信息(${gi + 1}/${formAnswers.length})` : '补充信息' }}
|
||||||
|
</view>
|
||||||
|
<view v-for="field in formFields" :key="field.fieldKey" class="form-field">
|
||||||
|
<view v-if="field.richText" class="rich-text" v-html="field.richTextContent" />
|
||||||
|
<template v-else>
|
||||||
|
<view class="field-label">
|
||||||
|
{{ field.fieldLabel }}
|
||||||
|
<text v-if="field.required" class="required">*</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="field.fieldType === 'CHECKBOX'" class="opt-list">
|
||||||
|
<view
|
||||||
|
v-for="opt in field.options"
|
||||||
|
:key="opt.value"
|
||||||
|
class="opt-item"
|
||||||
|
@click="toggleCheckbox(gi, field, opt.value)"
|
||||||
|
>
|
||||||
|
<text class="opt-mark">{{ isChecked(group[field.fieldKey], opt.value) ? '☑' : '☐' }}</text>
|
||||||
|
{{ opt.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="field.fieldType === 'RADIO'" class="opt-list">
|
||||||
|
<view
|
||||||
|
v-for="opt in field.options"
|
||||||
|
:key="opt.value"
|
||||||
|
class="opt-item"
|
||||||
|
@click="setFormAnswer(gi, field.fieldKey, opt.value)"
|
||||||
|
>
|
||||||
|
<text class="opt-mark">{{ group[field.fieldKey] === opt.value ? '●' : '○' }}</text>
|
||||||
|
{{ opt.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<picker
|
||||||
|
v-else-if="field.fieldType === 'SELECT'"
|
||||||
|
:range="field.optionLabels"
|
||||||
|
@change="(e) => onSelectChange(gi, field, e)"
|
||||||
|
>
|
||||||
|
<view class="picker-row">
|
||||||
|
<text>{{ optionLabel(field, group[field.fieldKey]) || field.hint }}</text>
|
||||||
|
<text class="arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker
|
||||||
|
v-else-if="field.fieldType === 'DATE'"
|
||||||
|
mode="date"
|
||||||
|
:value="group[field.fieldKey]"
|
||||||
|
@change="(e) => setFormAnswer(gi, field.fieldKey, e.detail.value)"
|
||||||
|
>
|
||||||
|
<view class="picker-row">
|
||||||
|
<text>{{ group[field.fieldKey] || field.hint }}</text>
|
||||||
|
<text class="arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view v-else-if="field.fieldType === 'DATE_RANGE'" class="range-row">
|
||||||
|
<picker mode="date" :value="rangePart(group[field.fieldKey], 0)" @change="(e) => setRangePart(gi, field.fieldKey, 0, e.detail.value)">
|
||||||
|
<view class="picker-row"><text>{{ rangePart(group[field.fieldKey], 0) || '开始日期' }}</text></view>
|
||||||
|
</picker>
|
||||||
|
<text class="range-sep">至</text>
|
||||||
|
<picker mode="date" :value="rangePart(group[field.fieldKey], 1)" @change="(e) => setRangePart(gi, field.fieldKey, 1, e.detail.value)">
|
||||||
|
<view class="picker-row"><text>{{ rangePart(group[field.fieldKey], 1) || '结束日期' }}</text></view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<picker
|
||||||
|
v-else-if="field.fieldType === 'TIME'"
|
||||||
|
mode="time"
|
||||||
|
:value="group[field.fieldKey]"
|
||||||
|
@change="(e) => setFormAnswer(gi, field.fieldKey, e.detail.value)"
|
||||||
|
>
|
||||||
|
<view class="picker-row">
|
||||||
|
<text>{{ group[field.fieldKey] || field.hint }}</text>
|
||||||
|
<text class="arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view v-else-if="field.fieldType === 'TIME_RANGE'" class="range-row">
|
||||||
|
<picker mode="time" :value="rangePart(group[field.fieldKey], 0)" @change="(e) => setRangePart(gi, field.fieldKey, 0, e.detail.value)">
|
||||||
|
<view class="picker-row"><text>{{ rangePart(group[field.fieldKey], 0) || '开始时间' }}</text></view>
|
||||||
|
</picker>
|
||||||
|
<text class="range-sep">至</text>
|
||||||
|
<picker mode="time" :value="rangePart(group[field.fieldKey], 1)" @change="(e) => setRangePart(gi, field.fieldKey, 1, e.detail.value)">
|
||||||
|
<view class="picker-row"><text>{{ rangePart(group[field.fieldKey], 1) || '结束时间' }}</text></view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<picker
|
||||||
|
v-else-if="field.fieldType === 'CITY'"
|
||||||
|
mode="region"
|
||||||
|
@change="(e) => onCityChange(gi, field, e)"
|
||||||
|
>
|
||||||
|
<view class="picker-row">
|
||||||
|
<text>{{ group[field.fieldKey] || field.hint }}</text>
|
||||||
|
<text class="arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view v-else-if="field.fieldType === 'IMAGE'" class="image-row">
|
||||||
|
<view
|
||||||
|
v-for="(url, ui) in group[field.fieldKey] || []"
|
||||||
|
:key="ui"
|
||||||
|
class="image-thumb"
|
||||||
|
@click="previewImage(group[field.fieldKey], ui)"
|
||||||
|
>
|
||||||
|
<image :src="url" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="(group[field.fieldKey] || []).length < field.maxUploadCount"
|
||||||
|
class="image-add"
|
||||||
|
@click="chooseImages(gi, field)"
|
||||||
|
>+</view>
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
v-else
|
||||||
|
:model-value="group[field.fieldKey]"
|
||||||
|
:placeholder="field.hint"
|
||||||
|
border="surround"
|
||||||
|
:type="field.contentType === 'NUMBER' ? 'number' : 'text'"
|
||||||
|
@update:model-value="(val) => setFormAnswer(gi, field.fieldKey, val)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section notice">
|
||||||
|
<view class="section-title">预约须知</view>
|
||||||
|
<text class="notice-text">{{ cancelNotice }}</text>
|
||||||
|
</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, bindCheckout, getGoodsExt } from '@/api/appointment.js'
|
||||||
|
import {
|
||||||
|
SERVICE_MODE,
|
||||||
|
SERVICE_MODE_LABEL,
|
||||||
|
parseServiceModes,
|
||||||
|
buildBookableDates,
|
||||||
|
buildCancelNotice,
|
||||||
|
formatDate,
|
||||||
|
parseFormFields,
|
||||||
|
buildFormGroups,
|
||||||
|
isBlankFormValue,
|
||||||
|
validateTextContent,
|
||||||
|
} from '@/utils/appointmentGoods.js'
|
||||||
|
|
||||||
|
const skuId = ref('')
|
||||||
|
const goodsId = ref('')
|
||||||
|
const num = ref(1)
|
||||||
|
const goodsExt = ref<any>(null)
|
||||||
|
const slotDate = ref('')
|
||||||
|
const slots = ref<any[]>([])
|
||||||
|
const selectedSlot = ref<any>(null)
|
||||||
|
const serviceModes = ref<string[]>([SERVICE_MODE.IN_STORE])
|
||||||
|
const serviceMode = ref(SERVICE_MODE.IN_STORE)
|
||||||
|
const contactName = ref('')
|
||||||
|
const contactPhone = ref('')
|
||||||
|
const serviceAddress = ref('')
|
||||||
|
const loadingSlots = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const bookableDates = ref<string[]>([])
|
||||||
|
const formFields = ref<any[]>([])
|
||||||
|
const formAnswers = ref<Record<string, any>[]>([])
|
||||||
|
|
||||||
|
const cancelNotice = computed(() => buildCancelNotice(goodsExt.value))
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
skuId.value = options?.skuId || ''
|
||||||
|
goodsId.value = options?.goodsId || ''
|
||||||
|
num.value = Number(options?.num) || 1
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!goodsId.value) return
|
||||||
|
try {
|
||||||
|
const res = await getGoodsExt(goodsId.value)
|
||||||
|
goodsExt.value = res.data?.result || null
|
||||||
|
serviceModes.value = parseServiceModes(goodsExt.value)
|
||||||
|
serviceMode.value = serviceModes.value[0] || SERVICE_MODE.IN_STORE
|
||||||
|
bookableDates.value = buildBookableDates(goodsExt.value)
|
||||||
|
slotDate.value = bookableDates.value[0] || formatDate(new Date())
|
||||||
|
formFields.value = parseFormFields(goodsExt.value?.systemFormSnapshot)
|
||||||
|
formAnswers.value = buildFormGroups(goodsExt.value, num.value)
|
||||||
|
await loadSlots()
|
||||||
|
} catch {
|
||||||
|
bookableDates.value = buildBookableDates(null)
|
||||||
|
slotDate.value = bookableDates.value[0] || formatDate(new Date())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (!slot?.availableQty || slot.availableQty <= 0) {
|
||||||
|
uni.showToast({ title: '该时段已满', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedSlot.value = slot
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormAnswer(groupIndex: number, fieldKey: string, value: any) {
|
||||||
|
const next = [...formAnswers.value]
|
||||||
|
next[groupIndex] = { ...next[groupIndex], [fieldKey]: value }
|
||||||
|
formAnswers.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChecked(value: any, opt: string) {
|
||||||
|
return Array.isArray(value) && value.includes(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCheckbox(groupIndex: number, field: any, opt: string) {
|
||||||
|
const current = formAnswers.value[groupIndex]?.[field.fieldKey]
|
||||||
|
const list = Array.isArray(current) ? [...current] : []
|
||||||
|
const idx = list.indexOf(opt)
|
||||||
|
if (idx >= 0) list.splice(idx, 1)
|
||||||
|
else list.push(opt)
|
||||||
|
setFormAnswer(groupIndex, field.fieldKey, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionLabel(field: any, value: string) {
|
||||||
|
return field.options?.find((item: any) => item.value === value)?.label || value
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectChange(groupIndex: number, field: any, e: any) {
|
||||||
|
const idx = Number(e.detail?.value)
|
||||||
|
const opt = field.options?.[idx]
|
||||||
|
setFormAnswer(groupIndex, field.fieldKey, opt?.value || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function rangePart(value: string, index: number) {
|
||||||
|
return String(value || '').split('~')[index] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRangePart(groupIndex: number, fieldKey: string, index: number, part: string) {
|
||||||
|
const current = String(formAnswers.value[groupIndex]?.[fieldKey] || '').split('~')
|
||||||
|
current[index] = part
|
||||||
|
if (!current[0]) current[0] = ''
|
||||||
|
if (!current[1]) current[1] = ''
|
||||||
|
setFormAnswer(groupIndex, fieldKey, `${current[0]}~${current[1]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCityChange(groupIndex: number, field: any, e: any) {
|
||||||
|
const parts = e.detail?.value || []
|
||||||
|
let depth = 3
|
||||||
|
if (field.cityLevel === 'PROVINCE_CITY') depth = 2
|
||||||
|
setFormAnswer(groupIndex, field.fieldKey, parts.slice(0, depth).join(' '))
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewImage(list: string[], index: number) {
|
||||||
|
uni.previewImage({ urls: list, current: list[index] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseImages(groupIndex: number, field: any) {
|
||||||
|
const current = Array.isArray(formAnswers.value[groupIndex]?.[field.fieldKey])
|
||||||
|
? [...formAnswers.value[groupIndex][field.fieldKey]]
|
||||||
|
: []
|
||||||
|
const remain = Math.max((field.maxUploadCount || 1) - current.length, 0)
|
||||||
|
if (!remain) return
|
||||||
|
uni.chooseImage({
|
||||||
|
count: remain,
|
||||||
|
success: (res) => {
|
||||||
|
setFormAnswer(groupIndex, field.fieldKey, current.concat(res.tempFilePaths || []))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
if (!selectedSlot.value) {
|
||||||
|
uni.showToast({ title: '请选择预约时段', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!contactName.value.trim()) {
|
||||||
|
uni.showToast({ title: '请填写联系人', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!contactPhone.value.trim()) {
|
||||||
|
uni.showToast({ title: '请填写联系电话', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (serviceMode.value === SERVICE_MODE.ON_SITE && !serviceAddress.value.trim()) {
|
||||||
|
uni.showToast({ title: '请填写服务地址', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const field of formFields.value) {
|
||||||
|
if (field.richText) continue
|
||||||
|
for (const group of formAnswers.value) {
|
||||||
|
const val = group?.[field.fieldKey]
|
||||||
|
if (field.required && isBlankFormValue(val)) {
|
||||||
|
uni.showToast({ title: `请填写${field.fieldLabel}`, icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (field.fieldType === 'TEXT' && !validateTextContent(field, val)) {
|
||||||
|
uni.showToast({ title: `请填写正确的${field.fieldLabel}`, icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!validate()) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const slot = selectedSlot.value
|
||||||
|
const checkoutPayload = {
|
||||||
|
skuId: skuId.value,
|
||||||
|
slotStockId: slot.id,
|
||||||
|
serviceMode: serviceMode.value,
|
||||||
|
slotDate: slotDate.value,
|
||||||
|
slotStartTime: slot.slotStartTime,
|
||||||
|
slotEndTime: slot.slotEndTime,
|
||||||
|
contactName: contactName.value.trim(),
|
||||||
|
contactPhone: contactPhone.value.trim(),
|
||||||
|
serviceAddressSnapshot:
|
||||||
|
serviceMode.value === SERVICE_MODE.ON_SITE ? serviceAddress.value.trim() : '',
|
||||||
|
formAnswerJson: formAnswers.value.length
|
||||||
|
? JSON.stringify({ answers: formAnswers.value })
|
||||||
|
: '',
|
||||||
|
}
|
||||||
|
await bindCheckout(checkoutPayload)
|
||||||
|
uni.setStorageSync('appointment_checkout_context', checkoutPayload)
|
||||||
|
uni.navigateTo({ url: '/pages/order/fillorder?way=BUY_NOW' })
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
.picker-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.tip {
|
||||||
|
color: #999;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
.slot-list,
|
||||||
|
.mode-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
.slot-item,
|
||||||
|
.mode-item {
|
||||||
|
padding: 16rpx 28rpx;
|
||||||
|
background: #f2f2f2;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
}
|
||||||
|
.slot-item.active,
|
||||||
|
.mode-item.active {
|
||||||
|
background: #fff5f5;
|
||||||
|
border-color: #f2270c;
|
||||||
|
color: #f2270c;
|
||||||
|
}
|
||||||
|
.slot-qty {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.input-gap {
|
||||||
|
height: 16rpx;
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
.opt-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
.opt-item {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.opt-mark {
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
.range-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
.range-sep {
|
||||||
|
color: #999;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
.image-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
.image-thumb,
|
||||||
|
.image-add {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.image-thumb image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.image-add {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 48rpx;
|
||||||
|
border: 1rpx dashed #ddd;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.required {
|
||||||
|
color: #f2270c;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
.rich-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.notice-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.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>
|
||||||
234
pages/order/appointment/appointmentReschedule.vue
Normal file
234
pages/order/appointment/appointmentReschedule.vue
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<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>
|
||||||
@@ -68,6 +68,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<view class="address-box appointment-summary" v-if="isAppointmentCheckout && appointmentCheckoutSummary">
|
||||||
|
<view class="appointment-summary__title">预约信息</view>
|
||||||
|
<view class="appointment-summary__text">{{ appointmentCheckoutSummary }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 开团信息 -->
|
<!-- 开团信息 -->
|
||||||
<view class="group-box" v-if="isAssemble">
|
<view class="group-box" v-if="isAssemble">
|
||||||
<view class="group-title">
|
<view class="group-title">
|
||||||
@@ -336,19 +341,23 @@
|
|||||||
</u-col>
|
</u-col>
|
||||||
</u-row>
|
</u-row>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="giftCardList.length || giftCardDeductAmount > 0">
|
<div class="gift-card-module">
|
||||||
<u-row>
|
<u-row>
|
||||||
<u-col :span="6">礼品卡</u-col>
|
<u-col :span="6">
|
||||||
|
<view class="gift-card-title-row">
|
||||||
|
<text>使用礼品卡</text>
|
||||||
|
<text class="gift-card-help" @click.stop="showGiftCardNotice">使用说明</text>
|
||||||
|
</view>
|
||||||
|
</u-col>
|
||||||
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
||||||
<span v-if="giftCardDeductAmount > 0" class="main-color"
|
<span v-if="giftCardDeductAmount > 0" class="main-color"
|
||||||
>-¥{{ unitPrice(giftCardDeductAmount) }}</span
|
>已抵扣 -¥{{ unitPrice(giftCardDeductAmount) }}</span
|
||||||
>
|
|
||||||
<span v-else @click="giftCardExpanded = !giftCardExpanded"
|
|
||||||
>{{ giftCardList.length }} 张可用</span
|
|
||||||
>
|
>
|
||||||
|
<span v-else>{{ giftCardList.length }} 张可用</span>
|
||||||
</u-col>
|
</u-col>
|
||||||
</u-row>
|
</u-row>
|
||||||
<view v-if="giftCardExpanded && giftCardList.length" class="gift-card-list">
|
<view v-if="!giftCardList.length" class="gift-card-empty">暂无可用礼品卡</view>
|
||||||
|
<view v-else class="gift-card-list">
|
||||||
<view
|
<view
|
||||||
class="gift-card-item"
|
class="gift-card-item"
|
||||||
:class="{ 'gift-card-item--selected': isGiftCardSelected(item) }"
|
:class="{ 'gift-card-item--selected': isGiftCardSelected(item) }"
|
||||||
@@ -356,11 +365,27 @@
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
@click="toggleGiftCard(item)"
|
@click="toggleGiftCard(item)"
|
||||||
>
|
>
|
||||||
|
<view class="gift-card-item__header">
|
||||||
|
<view class="gift-card-item__pattern" />
|
||||||
|
<view class="gift-card-item__inner">
|
||||||
|
<view>
|
||||||
<view class="gift-card-item__name">{{ item.giftCardName || '礼品卡' }}</view>
|
<view class="gift-card-item__name">{{ item.giftCardName || '礼品卡' }}</view>
|
||||||
<view class="gift-card-item__meta">
|
<view class="gift-card-item__face">面值{{ item.faceText }}元</view>
|
||||||
<text>余额 ¥{{ unitPrice(item.balance) }}</text>
|
|
||||||
<text class="gift-card-item__no">{{ item.cardNo }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view class="gift-card-item__right">
|
||||||
|
<view class="gift-card-item__type">现金卡</view>
|
||||||
|
<view class="gift-card-item__expire">{{ item.expireText }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="gift-card-item__body">
|
||||||
|
<view class="gift-card-item__balance">
|
||||||
|
<text class="gift-card-item__amt">¥{{ unitPrice(item.balance) }}</text>
|
||||||
|
<text>余额</text>
|
||||||
|
</view>
|
||||||
|
<view class="gift-card-item__no">{{ item.cardNo }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="isGiftCardSelected(item)" class="gift-card-item__check">已选</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,7 +453,10 @@ import {
|
|||||||
secrecyMobile,
|
secrecyMobile,
|
||||||
isLogin,
|
isLogin,
|
||||||
} from '@/utils/filters.js'
|
} from '@/utils/filters.js'
|
||||||
import { hasECouponInCheckout } from '@/utils/goodsType.js'
|
import { hasECouponInCheckout, hasAppointmentInCheckout } from '@/utils/goodsType.js'
|
||||||
|
import { SERVICE_MODE_LABEL, formatDate } from '@/utils/appointmentGoods.js'
|
||||||
|
import { bindCheckout } from '@/api/appointment.js'
|
||||||
|
import { decorateGiftCard, GIFT_CARD_PAY_NOTICE } from '@/utils/giftCard.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const { proxy } = getCurrentInstance()!
|
const { proxy } = getCurrentInstance()!
|
||||||
@@ -474,12 +502,32 @@ const originOrderData = ref<any>('')
|
|||||||
|
|
||||||
// E_COUPON 结算:免地址/免配送(hidePhysicalCheckout);优惠券/活动/礼品卡可用(M-02)
|
// E_COUPON 结算:免地址/免配送(hidePhysicalCheckout);优惠券/活动/礼品卡可用(M-02)
|
||||||
const isECouponCheckout = computed(() => hasECouponInCheckout(orderMessage.value))
|
const isECouponCheckout = computed(() => hasECouponInCheckout(orderMessage.value))
|
||||||
|
const isAppointmentCheckout = computed(() => hasAppointmentInCheckout(orderMessage.value))
|
||||||
const isVirtualCheckout = computed(() => orderMessage.value?.cartTypeEnum === 'VIRTUAL')
|
const isVirtualCheckout = computed(() => orderMessage.value?.cartTypeEnum === 'VIRTUAL')
|
||||||
const hidePhysicalCheckout = computed(
|
const hidePhysicalCheckout = computed(
|
||||||
() => isVirtualCheckout.value || isECouponCheckout.value
|
() => isVirtualCheckout.value || isECouponCheckout.value || isAppointmentCheckout.value
|
||||||
)
|
)
|
||||||
|
const appointmentCheckoutContext = ref<any>(null)
|
||||||
|
|
||||||
const giftCardList = computed(() => orderMessage.value?.canUseGiftCards || [])
|
const appointmentCheckoutSummary = computed(() => {
|
||||||
|
const ctx =
|
||||||
|
appointmentCheckoutContext.value ||
|
||||||
|
orderMessage.value?.appointmentCheckout ||
|
||||||
|
orderMessage.value?.appointmentContext
|
||||||
|
if (!ctx) return ''
|
||||||
|
const date = formatDate(ctx.slotDate)
|
||||||
|
const mode = SERVICE_MODE_LABEL[ctx.serviceMode] || ctx.serviceMode || ''
|
||||||
|
const slot = ctx.slotStartTime && ctx.slotEndTime ? `${ctx.slotStartTime}-${ctx.slotEndTime}` : ''
|
||||||
|
const contact = ctx.contactName ? `${ctx.contactName} ${ctx.contactPhone || ''}` : ''
|
||||||
|
return [date, slot, mode, contact].filter(Boolean).join(' · ')
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedGiftCardIds = computed(() => orderMessage.value?.selectedGiftCardIds || [])
|
||||||
|
const giftCardList = computed(() =>
|
||||||
|
(orderMessage.value?.canUseGiftCards || []).map((item: any) =>
|
||||||
|
decorateGiftCard(item, selectedGiftCardIds.value)
|
||||||
|
)
|
||||||
|
)
|
||||||
const giftCardDeductAmount = computed(() => {
|
const giftCardDeductAmount = computed(() => {
|
||||||
const dto = orderMessage.value?.priceDetailDTO
|
const dto = orderMessage.value?.priceDetailDTO
|
||||||
if (!dto) return 0
|
if (!dto) return 0
|
||||||
@@ -488,8 +536,6 @@ const giftCardDeductAmount = computed(() => {
|
|||||||
}
|
}
|
||||||
return Number(dto.giftCardPrice) || 0
|
return Number(dto.giftCardPrice) || 0
|
||||||
})
|
})
|
||||||
const selectedGiftCardIds = computed(() => orderMessage.value?.selectedGiftCardIds || [])
|
|
||||||
const giftCardExpanded = ref(false)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
remarkVal,
|
remarkVal,
|
||||||
@@ -527,9 +573,10 @@ onShow(async () => {
|
|||||||
mask: true,
|
mask: true,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
appointmentCheckoutContext.value = uni.getStorageSync('appointment_checkout_context') || null
|
||||||
await getOrderList()
|
await getOrderList()
|
||||||
// E_COUPON 不需要配送方式
|
// E_COUPON / 预约商品不需要配送方式
|
||||||
if (!isECouponCheckout.value) {
|
if (!isECouponCheckout.value && !isAppointmentCheckout.value) {
|
||||||
await getDistribution()
|
await getDistribution()
|
||||||
}
|
}
|
||||||
if (routerVal.value.way == 'PINTUAN') {
|
if (routerVal.value.way == 'PINTUAN') {
|
||||||
@@ -605,6 +652,15 @@ function invoice() {
|
|||||||
invoiceFlag.value = true
|
invoiceFlag.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showGiftCardNotice() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '礼品卡使用说明',
|
||||||
|
content: GIFT_CARD_PAY_NOTICE,
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '我知道了',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function isGiftCardSelected(item: any) {
|
function isGiftCardSelected(item: any) {
|
||||||
if (!item?.id) return false
|
if (!item?.id) return false
|
||||||
return (selectedGiftCardIds.value || []).map(String).includes(String(item.id))
|
return (selectedGiftCardIds.value || []).map(String).includes(String(item.id))
|
||||||
@@ -717,8 +773,10 @@ function createTradeFun() {
|
|||||||
delete submit.parentOrderSn
|
delete submit.parentOrderSn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runCreate = () =>
|
||||||
API_Trade.createTrade(submit).then((res) => {
|
API_Trade.createTrade(submit).then((res) => {
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
|
uni.removeStorageSync('appointment_checkout_context')
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '创建订单成功!',
|
title: '创建订单成功!',
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
@@ -747,6 +805,30 @@ function createTradeFun() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isAppointmentCheckout.value) {
|
||||||
|
const ctx =
|
||||||
|
appointmentCheckoutContext.value ||
|
||||||
|
uni.getStorageSync('appointment_checkout_context')
|
||||||
|
if (!ctx) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '预约信息不完整,请返回重选时段',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bindCheckout(ctx)
|
||||||
|
.then(runCreate)
|
||||||
|
.catch((e: any) => {
|
||||||
|
uni.showToast({
|
||||||
|
title: e?.data?.message || e?.message || '预约信息提交失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runCreate()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,8 +943,8 @@ function getOrderList() {
|
|||||||
;(store.state as any).canUseCoupons = res.data.result.canUseCoupons
|
;(store.state as any).canUseCoupons = res.data.result.canUseCoupons
|
||||||
;(store.state as any).cantUseCoupons = res.data.result.cantUseCoupons
|
;(store.state as any).cantUseCoupons = res.data.result.cantUseCoupons
|
||||||
|
|
||||||
// E_COUPON 免地址,不自动回填 memberAddress
|
// E_COUPON / 预约商品免地址,不自动回填 memberAddress
|
||||||
if (!hasECouponInCheckout(res.data.result)) {
|
if (!hasECouponInCheckout(res.data.result) && !hasAppointmentInCheckout(res.data.result)) {
|
||||||
if (!res.data.result.memberAddress) {
|
if (!res.data.result.memberAddress) {
|
||||||
getUserAddress()
|
getUserAddress()
|
||||||
} else {
|
} else {
|
||||||
@@ -894,6 +976,22 @@ page {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.appointment-summary {
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.appointment-summary__title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.appointment-summary__text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-8 {
|
.flex-8 {
|
||||||
flex: 8;
|
flex: 8;
|
||||||
}
|
}
|
||||||
@@ -1250,39 +1348,129 @@ page {
|
|||||||
margin: 20rpx 0;
|
margin: 20rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gift-card-module {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-help {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-empty {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.gift-card-list {
|
.gift-card-list {
|
||||||
margin-top: 16rpx;
|
margin-top: 16rpx;
|
||||||
padding: 0 8rpx 8rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-card-item {
|
.gift-card-item {
|
||||||
border: 1rpx solid #eceef2;
|
position: relative;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
padding: 20rpx 24rpx;
|
overflow: hidden;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
border-color: $main-color;
|
border-color: $main-color;
|
||||||
background: rgba($main-color, 0.04);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gift-card-item__header {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(125deg, #ff9a4a 0%, #ff7729 42%, #ff8f3d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__pattern {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.22;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-36deg,
|
||||||
|
transparent,
|
||||||
|
transparent 10rpx,
|
||||||
|
rgba(255, 255, 255, 0.45) 10rpx,
|
||||||
|
rgba(255, 255, 255, 0.45) 12rpx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__inner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20rpx 24rpx 16rpx;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.gift-card-item__name {
|
.gift-card-item__name {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-card-item__meta {
|
.gift-card-item__face,
|
||||||
display: flex;
|
.gift-card-item__expire {
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 24rpx;
|
font-size: 22rpx;
|
||||||
color: #666;
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__type {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__body {
|
||||||
|
padding: 16rpx 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__balance {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-card-item__amt {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ff6b22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-card-item__no {
|
.gift-card-item__no {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gift-card-item__check {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #fff;
|
||||||
|
background: $main-color;
|
||||||
|
border-top-left-radius: 12rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
v-if="order.allowOperationVO.pay" @click="waitPay(order)">立即付款</view>
|
v-if="order.allowOperationVO.pay" @click="waitPay(order)">立即付款</view>
|
||||||
<!-- 取消订单 -->
|
<!-- 取消订单 -->
|
||||||
<view ripple class="cancel-btn" shape="circle" size="mini"
|
<view ripple class="cancel-btn" shape="circle" size="mini"
|
||||||
v-if="order.allowOperationVO.cancel" @click="onCancel(order.sn)">
|
v-if="showOrderCancel(order)" @click="onCancel(order.sn)">
|
||||||
取消订单
|
取消订单
|
||||||
</view>
|
</view>
|
||||||
<!-- 等待收货 -->
|
<!-- 等待收货 -->
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
确认收货
|
确认收货
|
||||||
</view>
|
</view>
|
||||||
<view ripple shape="circle" class="cancel-btn" size="mini"
|
<view ripple shape="circle" class="cancel-btn" size="mini"
|
||||||
v-if="!isECouponOrder(order.orderType) && order.groupAfterSaleStatus && ( order.groupAfterSaleStatus.includes('NOT_APPLIED') || order.groupAfterSaleStatus.includes('PART_AFTER_SALE'))"
|
v-if="showOrderAfterSale(order)"
|
||||||
@click="applyService(order)">
|
@click="applyService(order)">
|
||||||
退款/售后
|
退款/售后
|
||||||
</view>
|
</view>
|
||||||
@@ -123,7 +123,8 @@ import {
|
|||||||
orderStatusList,
|
orderStatusList,
|
||||||
} from '@/utils/filters.js'
|
} from '@/utils/filters.js'
|
||||||
// E_COUPON 订单不展示确认收货、退款/售后(FR-B-04)
|
// E_COUPON 订单不展示确认收货、退款/售后(FR-B-04)
|
||||||
import { isECouponOrder } from '@/utils/goodsType.js'
|
import { isECouponOrder, isAppointmentOrder } from '@/utils/goodsType.js'
|
||||||
|
import { getAppointmentAllowOperation } from '@/utils/appointmentGoods.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const { proxy } = getCurrentInstance()!
|
const { proxy } = getCurrentInstance()!
|
||||||
@@ -371,6 +372,23 @@ function orderStateExp(state: number) {
|
|||||||
return { stateTip, stateTipColor }
|
return { stateTip, stateTipColor }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showOrderCancel(order: any) {
|
||||||
|
if (isAppointmentOrder(order?.orderType)) {
|
||||||
|
return !!getAppointmentAllowOperation(order)?.cancel
|
||||||
|
}
|
||||||
|
return !!order?.allowOperationVO?.cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOrderAfterSale(order: any) {
|
||||||
|
if (isECouponOrder(order?.orderType)) return false
|
||||||
|
if (isAppointmentOrder(order?.orderType)) {
|
||||||
|
const allow = getAppointmentAllowOperation(order)
|
||||||
|
if (!allow?.afterSale) return false
|
||||||
|
}
|
||||||
|
const status = order?.groupAfterSaleStatus
|
||||||
|
return status && (status.includes('NOT_APPLIED') || status.includes('PART_AFTER_SALE'))
|
||||||
|
}
|
||||||
|
|
||||||
function navigateToOrderDetail(sn: string) {
|
function navigateToOrderDetail(sn: string) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: './orderDetail?sn=' + sn,
|
url: './orderDetail?sn=' + sn,
|
||||||
|
|||||||
@@ -11,7 +11,55 @@
|
|||||||
<!-- 物流/配送信息 / 卡密入口 -->
|
<!-- 物流/配送信息 / 卡密入口 -->
|
||||||
<view class="info-view logistics-view" v-if="showDeliveryInfoBlock">
|
<view class="info-view logistics-view" v-if="showDeliveryInfoBlock">
|
||||||
<view class="logistics-List">
|
<view class="logistics-List">
|
||||||
<view v-if="isECouponOrder" class="card-key-entry">
|
<view v-if="isAppointmentOrderFlag" class="appointment-entry">
|
||||||
|
<view class="appointment-entry__row">
|
||||||
|
<text class="label">预约时间</text>
|
||||||
|
<text>{{ appointmentSlotText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="appointment-entry__row" v-if="appointmentDetail.serviceMode">
|
||||||
|
<text class="label">服务方式</text>
|
||||||
|
<text>{{ serviceModeLabel }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="appointment-entry__row" v-if="appointmentDetail.contactName">
|
||||||
|
<text class="label">联系人</text>
|
||||||
|
<text>{{ appointmentDetail.contactName }} {{ appointmentDetail.contactPhone }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="appointment-entry__row" v-if="appointmentDetail.serviceAddressSnapshot">
|
||||||
|
<text class="label">服务地址</text>
|
||||||
|
<text>{{ appointmentDetail.serviceAddressSnapshot }}</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-for="(rows, gi) in appointmentFormGroups"
|
||||||
|
:key="gi"
|
||||||
|
class="appointment-entry__row appointment-form-block"
|
||||||
|
>
|
||||||
|
<text class="label">{{ appointmentFormGroups.length > 1 ? `补充信息${gi + 1}` : '补充信息' }}</text>
|
||||||
|
<view class="form-answers">
|
||||||
|
<view v-for="row in rows" :key="row.fieldKey" class="form-answer-row">
|
||||||
|
{{ row.fieldLabel }}:{{ row.value }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="appointmentCodes.length" class="appointment-codes">
|
||||||
|
<view class="appointment-codes__title">核销码({{ appointmentCodes.length }})</view>
|
||||||
|
<view
|
||||||
|
v-for="code in appointmentCodes"
|
||||||
|
:key="code.id || code.code"
|
||||||
|
class="verificationCode appointment-code-item"
|
||||||
|
>
|
||||||
|
{{ code.seqNo ? '#' + code.seqNo + ' ' : '' }}{{ code.code }}
|
||||||
|
<text class="code-status">({{ verificationStatusText(code.status) }})</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="appointmentAllow.reschedule"
|
||||||
|
class="card-key-entry__btn reschedule-stub"
|
||||||
|
@click="onRescheduleStub"
|
||||||
|
>
|
||||||
|
申请改期
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="isECouponOrder" class="card-key-entry">
|
||||||
<view v-if="ecouponCardKeyDelivered" class="card-key-entry__btn" @click="goCardKeyDetail">
|
<view v-if="ecouponCardKeyDelivered" class="card-key-entry__btn" @click="goCardKeyDetail">
|
||||||
查看卡密
|
查看卡密
|
||||||
</view>
|
</view>
|
||||||
@@ -92,7 +140,7 @@
|
|||||||
<view class="goods-price">
|
<view class="goods-price">
|
||||||
¥{{unitPrice(sku.goodsPrice) }}
|
¥{{unitPrice(sku.goodsPrice) }}
|
||||||
<!-- <span v-if="sku.point">+{{ sku.point }}积分</span> -->
|
<!-- <span v-if="sku.point">+{{ sku.point }}积分</span> -->
|
||||||
<span style="font-size: 24rpx;margin-left: 14rpx;color: #ff9900;" v-if="!isECouponOrder && sku.isRefund && sku.isRefund !== 'NO_REFUND'">
|
<span style="font-size: 24rpx;margin-left: 14rpx;color: #ff9900;" v-if="!isECouponOrder && !isAppointmentOrderFlag && sku.isRefund && sku.isRefund !== 'NO_REFUND'">
|
||||||
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
|
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
|
||||||
</span>
|
</span>
|
||||||
</view>
|
</view>
|
||||||
@@ -112,7 +160,7 @@
|
|||||||
<view class="title">商品总价:</view>
|
<view class="title">商品总价:</view>
|
||||||
<view class="value">¥{{unitPrice(order.goodsPrice) }}</view>
|
<view class="value">¥{{unitPrice(order.goodsPrice) }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-info-view" v-if="order.freightPrice && !isECouponOrder">
|
<view class="order-info-view" v-if="order.freightPrice && !isECouponOrder && !isAppointmentOrderFlag">
|
||||||
<view class="title">运费:</view>
|
<view class="title">运费:</view>
|
||||||
<view class="value">¥{{unitPrice(order.freightPrice) }}</view>
|
<view class="value">¥{{unitPrice(order.freightPrice) }}</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -142,7 +190,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="customer-list">
|
<view class="customer-list">
|
||||||
<view class="customer-service"
|
<view class="customer-service"
|
||||||
v-if="orderDetail.allowOperationVO && orderDetail.allowOperationVO.cancel == true"
|
v-if="showCancelBtn"
|
||||||
@click="onCancel(order.sn)">取消订单</view>
|
@click="onCancel(order.sn)">取消订单</view>
|
||||||
<view class="customer-service" v-if="allowOperation.showLogistics" @click="handleClickDeliver()">查看物流</view>
|
<view class="customer-service" v-if="allowOperation.showLogistics" @click="handleClickDeliver()">查看物流</view>
|
||||||
<view class="customer-service" v-if="order.orderStatus != 'UNPAID' && order.orderPromotionType == 'PINTUAN'"
|
<view class="customer-service" v-if="order.orderStatus != 'UNPAID' && order.orderPromotionType == 'PINTUAN'"
|
||||||
@@ -282,12 +330,21 @@ import {
|
|||||||
} from '@/utils/orderDelivery.js'
|
} from '@/utils/orderDelivery.js'
|
||||||
import {
|
import {
|
||||||
isECouponOrder as checkECouponOrder,
|
isECouponOrder as checkECouponOrder,
|
||||||
|
isAppointmentOrder as checkAppointmentOrder,
|
||||||
isNonPhysicalOrder as checkNonPhysicalOrder,
|
isNonPhysicalOrder as checkNonPhysicalOrder,
|
||||||
orderHasCardKeyContent,
|
orderHasCardKeyContent,
|
||||||
collectCardKeyFulfillLines,
|
collectCardKeyFulfillLines,
|
||||||
resolveCardKeyFulfillMessage,
|
resolveCardKeyFulfillMessage,
|
||||||
CARD_KEY_FULFILL_STATUS,
|
CARD_KEY_FULFILL_STATUS,
|
||||||
} from '@/utils/goodsType.js'
|
} from '@/utils/goodsType.js'
|
||||||
|
import { getAppointmentOrderDetail } from '@/api/appointment.js'
|
||||||
|
import {
|
||||||
|
SERVICE_MODE_LABEL,
|
||||||
|
VERIFICATION_CODE_STATUS_TEXT,
|
||||||
|
getAppointmentAllowOperation,
|
||||||
|
formatDate,
|
||||||
|
parseFormAnswerGroups,
|
||||||
|
} from '@/utils/appointmentGoods.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const lightColor = computed(() => store.getters.lightColor)
|
const lightColor = computed(() => store.getters.lightColor)
|
||||||
@@ -319,7 +376,30 @@ const orderPackage = ref<any[]>([])
|
|||||||
const legacyTraces = ref<any>(null)
|
const legacyTraces = ref<any>(null)
|
||||||
|
|
||||||
const isECouponOrder = computed(() => checkECouponOrder(order.value?.orderType))
|
const isECouponOrder = computed(() => checkECouponOrder(order.value?.orderType))
|
||||||
|
const isAppointmentOrderFlag = computed(() => checkAppointmentOrder(order.value?.orderType))
|
||||||
const isNonPhysicalOrder = computed(() => checkNonPhysicalOrder(order.value?.orderType))
|
const isNonPhysicalOrder = computed(() => checkNonPhysicalOrder(order.value?.orderType))
|
||||||
|
const appointmentDetail = ref<Record<string, any>>({})
|
||||||
|
const appointmentAllow = computed(() =>
|
||||||
|
getAppointmentAllowOperation(order.value) || { cancel: false, afterSale: false, reschedule: false }
|
||||||
|
)
|
||||||
|
const showCancelBtn = computed(() => {
|
||||||
|
if (isAppointmentOrderFlag.value) return appointmentAllow.value.cancel
|
||||||
|
return orderDetail.value.allowOperationVO?.cancel === true
|
||||||
|
})
|
||||||
|
const appointmentCodes = computed(() => appointmentDetail.value?.codes || [])
|
||||||
|
const serviceModeLabel = computed(
|
||||||
|
() => SERVICE_MODE_LABEL[appointmentDetail.value?.serviceMode] || appointmentDetail.value?.serviceMode || ''
|
||||||
|
)
|
||||||
|
const appointmentSlotText = computed(() => {
|
||||||
|
const d = appointmentDetail.value
|
||||||
|
if (!d?.slotDate) return '—'
|
||||||
|
const date = formatDate(d.slotDate)
|
||||||
|
const slot = d.slotStartTime && d.slotEndTime ? `${d.slotStartTime}-${d.slotEndTime}` : ''
|
||||||
|
return [date, slot].filter(Boolean).join(' ')
|
||||||
|
})
|
||||||
|
const appointmentFormGroups = computed(() =>
|
||||||
|
parseFormAnswerGroups(appointmentDetail.value?.formAnswerJson)
|
||||||
|
)
|
||||||
const ecouponCardKeyDelivered = computed(() =>
|
const ecouponCardKeyDelivered = computed(() =>
|
||||||
orderHasCardKeyContent(orderGoodsList.value)
|
orderHasCardKeyContent(orderGoodsList.value)
|
||||||
)
|
)
|
||||||
@@ -335,6 +415,7 @@ const allowOperation = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const showDeliveryInfoBlock = computed(() => {
|
const showDeliveryInfoBlock = computed(() => {
|
||||||
|
if (isAppointmentOrderFlag.value) return true
|
||||||
if (isECouponOrder.value) return true
|
if (isECouponOrder.value) return true
|
||||||
if (order.value.orderType === 'VIRTUAL') return false
|
if (order.value.orderType === 'VIRTUAL') return false
|
||||||
if (order.value.deliveryMethod === 'SELF_PICK_UP') return false
|
if (order.value.deliveryMethod === 'SELF_PICK_UP') return false
|
||||||
@@ -444,6 +525,29 @@ function ByUserMessage(orderItem: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verificationStatusText(status: string) {
|
||||||
|
return VERIFICATION_CODE_STATUS_TEXT[status] || status || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAppointmentDetail(orderSnParam: string) {
|
||||||
|
if (!checkAppointmentOrder(order.value?.orderType)) {
|
||||||
|
appointmentDetail.value = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getAppointmentOrderDetail(orderSnParam)
|
||||||
|
appointmentDetail.value = res.data?.result || {}
|
||||||
|
} catch {
|
||||||
|
appointmentDetail.value = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRescheduleStub() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/order/appointment/appointmentReschedule?orderSn=${order.value.sn}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function loadData(orderSnParam: string) {
|
function loadData(orderSnParam: string) {
|
||||||
uni.showLoading({ title: '加载中' })
|
uni.showLoading({ title: '加载中' })
|
||||||
getOrderDetail(orderSnParam).then(async (res) => {
|
getOrderDetail(orderSnParam).then(async (res) => {
|
||||||
@@ -454,6 +558,7 @@ function loadData(orderSnParam: string) {
|
|||||||
if (!order.value.allowOperationVO && result.allowOperationVO) {
|
if (!order.value.allowOperationVO && result.allowOperationVO) {
|
||||||
order.value.allowOperationVO = result.allowOperationVO
|
order.value.allowOperationVO = result.allowOperationVO
|
||||||
}
|
}
|
||||||
|
await loadAppointmentDetail(orderSnParam)
|
||||||
await loadDelivery(order.value)
|
await loadDelivery(order.value)
|
||||||
hideLoadingIfNeeded()
|
hideLoadingIfNeeded()
|
||||||
})
|
})
|
||||||
@@ -774,6 +879,39 @@ page,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appointment-entry__row {
|
||||||
|
display: flex;
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.appointment-entry__row .label {
|
||||||
|
color: #999;
|
||||||
|
width: 140rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.form-answers {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.form-answer-row {
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
.appointment-codes__title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16rpx 0 8rpx;
|
||||||
|
}
|
||||||
|
.appointment-code-item {
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.code-status {
|
||||||
|
color: #999;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
.reschedule-stub {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.verificationCode {
|
.verificationCode {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 2rpx;
|
letter-spacing: 2rpx;
|
||||||
|
|||||||
@@ -53,6 +53,10 @@
|
|||||||
<image class="interact-item-icon" src="/static/mine/mycoupon.png" mode=""></image>
|
<image class="interact-item-icon" src="/static/mine/mycoupon.png" mode=""></image>
|
||||||
<view>优惠券</view>
|
<view>优惠券</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="interact-item" @click="navigateTo('/pages/cart/gift-card/myGiftCards')">
|
||||||
|
<image class="interact-item-icon" src="/static/mine/pointgift.png" mode=""></image>
|
||||||
|
<view>礼品卡</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
297
utils/appointmentGoods.js
Normal file
297
utils/appointmentGoods.js
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
/**
|
||||||
|
* 预约商品(APPOINTMENT_GOODS)买家端工具
|
||||||
|
* 规则:禁加购、仅 BUY_NOW、免物流地址、支持买家售后(含部分数量退款)
|
||||||
|
*/
|
||||||
|
export const APPOINTMENT_GOODS_TYPE = 'APPOINTMENT_GOODS'
|
||||||
|
export const APPOINTMENT_ORDER_TYPE = 'APPOINTMENT'
|
||||||
|
|
||||||
|
export const SERVICE_MODE = {
|
||||||
|
IN_STORE: 'IN_STORE',
|
||||||
|
ON_SITE: 'ON_SITE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SERVICE_MODE_LABEL = {
|
||||||
|
IN_STORE: '到店服务',
|
||||||
|
ON_SITE: '上门服务',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VERIFICATION_CODE_STATUS = {
|
||||||
|
UNUSED: 'UNUSED',
|
||||||
|
USED: 'USED',
|
||||||
|
INVALID: 'INVALID',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VERIFICATION_CODE_STATUS_TEXT = {
|
||||||
|
UNUSED: '待核销',
|
||||||
|
USED: '已核销',
|
||||||
|
INVALID: '已失效',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAppointmentGoods(goodsType) {
|
||||||
|
return goodsType === APPOINTMENT_GOODS_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAppointmentOrder(orderType) {
|
||||||
|
return orderType === APPOINTMENT_ORDER_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结算页识别当前交易是否含预约 SKU(免地址/免配送)
|
||||||
|
*/
|
||||||
|
export function hasAppointmentInCheckout(orderMessage) {
|
||||||
|
if (!orderMessage) return false
|
||||||
|
const checked = orderMessage.checkedSkuList || []
|
||||||
|
if (checked.some((item) => isAppointmentGoods(item?.goodsSku?.goodsType))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const cartList = orderMessage.cartList || []
|
||||||
|
return cartList.some((cart) =>
|
||||||
|
(cart.checkedSkuList || []).some((item) =>
|
||||||
|
isAppointmentGoods(item?.goodsSku?.goodsType)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseServiceModes(ext) {
|
||||||
|
if (!ext) return [SERVICE_MODE.IN_STORE]
|
||||||
|
const raw = ext.serviceModes
|
||||||
|
if (Array.isArray(raw) && raw.length) return raw
|
||||||
|
if (typeof raw === 'string' && raw.trim()) {
|
||||||
|
return raw.split(',').map((s) => s.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
return [SERVICE_MODE.IN_STORE]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSlotLabel(slot) {
|
||||||
|
if (!slot) return ''
|
||||||
|
const date = formatDate(slot.slotDate)
|
||||||
|
const start = slot.slotStartTime || ''
|
||||||
|
const end = slot.slotEndTime || ''
|
||||||
|
const qty =
|
||||||
|
slot.showRemainingQty && slot.availableQty != null
|
||||||
|
? `(余${slot.availableQty})`
|
||||||
|
: ''
|
||||||
|
return `${date} ${start}-${end}${qty}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(value) {
|
||||||
|
if (!value) return ''
|
||||||
|
if (typeof value === 'string') return value.slice(0, 10)
|
||||||
|
const d = new Date(value)
|
||||||
|
if (Number.isNaN(d.getTime())) return ''
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBookableDates(ext, days = 30) {
|
||||||
|
const rangeDays = Number(ext?.bookingDateRangeDays) || days
|
||||||
|
const list = []
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
for (let i = 0; i < rangeDays; i++) {
|
||||||
|
const d = new Date(today)
|
||||||
|
d.setDate(today.getDate() + i)
|
||||||
|
list.push(formatDate(d))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCancelNotice(ext) {
|
||||||
|
if (!ext) return '请按时到店/等候服务;具体取消规则以商家设置为准。'
|
||||||
|
const mode = ext.cancelMode
|
||||||
|
const hours = ext.cancelHours
|
||||||
|
if (mode === 'NOT_ALLOWED') return '预约成功后不可取消,请谨慎下单。'
|
||||||
|
if (mode === 'ALLOW_BEFORE_HOURS' && hours) {
|
||||||
|
return `服务开始前 ${hours} 小时可免费取消,逾期可能无法退款。`
|
||||||
|
}
|
||||||
|
return '请按时到店/等候服务;具体取消规则以商家设置为准。'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFormFields(schemaJson) {
|
||||||
|
if (!schemaJson) return []
|
||||||
|
let data = schemaJson
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const list = Array.isArray(data) ? data : data?.fields || []
|
||||||
|
return list.map((field, index) => {
|
||||||
|
const fieldType = String(field.field_type || field.fieldType || field.type || 'TEXT')
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/-/g, '_')
|
||||||
|
const options = Array.isArray(field.options)
|
||||||
|
? field.options.map((item, oi) =>
|
||||||
|
typeof item === 'string'
|
||||||
|
? { label: item, value: item }
|
||||||
|
: {
|
||||||
|
label: item?.label || item?.name || `选项${oi + 1}`,
|
||||||
|
value: item?.value || item?.label || `opt_${oi}`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
return {
|
||||||
|
fieldKey: field.field_key || field.fieldKey || field.id || field.key || `field_${index}`,
|
||||||
|
fieldLabel: field.field_label || field.fieldLabel || field.label || field.title || `字段${index + 1}`,
|
||||||
|
fieldType,
|
||||||
|
required: !!field.required,
|
||||||
|
hint: field.hint_text || field.hintText || field.hint || field.placeholder || '请输入',
|
||||||
|
richText: fieldType === 'RICH_TEXT',
|
||||||
|
richTextContent: field.rich_text_content || field.richTextContent || field.content || '',
|
||||||
|
options,
|
||||||
|
optionLabels: options.map((item) => item.label),
|
||||||
|
contentType: field.content_type || field.contentType || 'TEXT',
|
||||||
|
defaultValue: field.default_value || field.defaultValue || '',
|
||||||
|
cityLevel: field.city_level || field.cityLevel || '',
|
||||||
|
defaultVisibility: field.default_visibility || field.defaultVisibility || '',
|
||||||
|
defaultDateMode: field.default_date_mode || field.defaultDateMode || '',
|
||||||
|
defaultDate: field.default_date || field.defaultDate || '',
|
||||||
|
defaultDateStart: field.default_date_start || field.defaultDateStart || '',
|
||||||
|
defaultDateEnd: field.default_date_end || field.defaultDateEnd || '',
|
||||||
|
defaultTimeMode: field.default_time_mode || field.defaultTimeMode || '',
|
||||||
|
defaultTime: field.default_time || field.defaultTime || '',
|
||||||
|
defaultTimeStart: field.default_time_start || field.defaultTimeStart || '',
|
||||||
|
defaultTimeEnd: field.default_time_end || field.defaultTimeEnd || '',
|
||||||
|
maxUploadCount: Number(field.max_upload_count || field.maxUploadCount) || 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad2(n) {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTime() {
|
||||||
|
const d = new Date()
|
||||||
|
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultFormValue(field) {
|
||||||
|
const type = field?.fieldType
|
||||||
|
const opts = field?.options || []
|
||||||
|
if (type === 'RADIO' || type === 'SELECT') {
|
||||||
|
return opts[0]?.value || ''
|
||||||
|
}
|
||||||
|
if (type === 'CHECKBOX' || type === 'IMAGE') return []
|
||||||
|
if (type === 'TEXT') return field.defaultValue || ''
|
||||||
|
if (type === 'DATE' && field.defaultVisibility === 'SHOW') {
|
||||||
|
return field.defaultDateMode === 'SPECIFIED_DATE' ? field.defaultDate || '' : formatDate(new Date())
|
||||||
|
}
|
||||||
|
if (type === 'DATE_RANGE' && field.defaultVisibility === 'SHOW') {
|
||||||
|
if (field.defaultDateMode === 'SPECIFIED_DATE') {
|
||||||
|
return [field.defaultDateStart, field.defaultDateEnd].filter(Boolean).join('~')
|
||||||
|
}
|
||||||
|
const today = formatDate(new Date())
|
||||||
|
return `${today}~${today}`
|
||||||
|
}
|
||||||
|
if (type === 'TIME' && field.defaultVisibility === 'SHOW') {
|
||||||
|
return field.defaultTimeMode === 'SPECIFIED_TIME' ? field.defaultTime || '' : currentTime()
|
||||||
|
}
|
||||||
|
if (type === 'TIME_RANGE' && field.defaultVisibility === 'SHOW') {
|
||||||
|
if (field.defaultTimeMode === 'SPECIFIED_TIME') {
|
||||||
|
return [field.defaultTimeStart, field.defaultTimeEnd].filter(Boolean).join('~')
|
||||||
|
}
|
||||||
|
const now = currentTime()
|
||||||
|
return `${now}~${now}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlankFormValue(value) {
|
||||||
|
if (value == null) return true
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const text = value.trim()
|
||||||
|
if (!text || text === '~') return true
|
||||||
|
if (text.startsWith('~') || text.endsWith('~')) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) return !value.length
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTextContent(field, value) {
|
||||||
|
if (isBlankFormValue(value)) return true
|
||||||
|
const type = field?.contentType || 'TEXT'
|
||||||
|
const text = String(value).trim()
|
||||||
|
if (type === 'MOBILE') return /^1\d{10}$/.test(text)
|
||||||
|
if (type === 'ID_CARD') return /(^\d{15}$)|(^\d{17}[\dXx]$)/.test(text)
|
||||||
|
if (type === 'EMAIL') return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)
|
||||||
|
if (type === 'NUMBER') return /^-?\d+(\.\d+)?$/.test(text)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFormGroups(ext, quantity) {
|
||||||
|
const fields = parseFormFields(ext?.systemFormSnapshot)
|
||||||
|
const fillable = fields.filter((item) => !item.richText)
|
||||||
|
if (!ext?.systemFormId && !fillable.length) return []
|
||||||
|
const mode = ext?.formSubmitMode || 'PER_APPOINTMENT'
|
||||||
|
const count = mode === 'PER_ORDER' ? 1 : Math.max(Number(quantity) || 1, 1)
|
||||||
|
return Array.from({ length: count }, () => {
|
||||||
|
const group = {}
|
||||||
|
fillable.forEach((field) => {
|
||||||
|
group[field.fieldKey] = defaultFormValue(field)
|
||||||
|
})
|
||||||
|
return group
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFormAnswerGroups(formAnswerJson) {
|
||||||
|
if (!formAnswerJson) return []
|
||||||
|
let data = formAnswerJson
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fields = Array.isArray(data?.fields) ? data.fields : []
|
||||||
|
const answers = Array.isArray(data?.answers) ? data.answers : []
|
||||||
|
return answers
|
||||||
|
.map((group) => {
|
||||||
|
const map = group && typeof group === 'object' && !Array.isArray(group) ? group : {}
|
||||||
|
const source = fields.length
|
||||||
|
? fields
|
||||||
|
: Object.keys(map).map((key) => ({ fieldKey: key, fieldLabel: key }))
|
||||||
|
return source
|
||||||
|
.map((field) => ({
|
||||||
|
fieldKey: field.fieldKey,
|
||||||
|
fieldLabel: field.fieldLabel || field.fieldKey,
|
||||||
|
value: map[field.fieldKey] ?? '',
|
||||||
|
}))
|
||||||
|
.filter((row) => row.value !== '' && row.value != null)
|
||||||
|
})
|
||||||
|
.filter((rows) => rows.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约单:未付款仅可取消;已付款仅可售后 */
|
||||||
|
export function getAppointmentAllowOperation(order) {
|
||||||
|
if (!order || !isAppointmentOrder(order.orderType)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const unpaid =
|
||||||
|
order.orderStatus === 'UNPAID' && order.payStatus === 'UNPAID'
|
||||||
|
const paid = order.payStatus === 'PAID'
|
||||||
|
return {
|
||||||
|
cancel: unpaid,
|
||||||
|
pay: unpaid,
|
||||||
|
rog: false,
|
||||||
|
showLogistics: false,
|
||||||
|
afterSale: paid && order.orderStatus !== 'CANCELLED',
|
||||||
|
reschedule: paid && ['PAID', 'TAKE'].includes(order.orderStatus),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computePartialRefundPrice(applyInfo, num) {
|
||||||
|
const totalNum = Number(applyInfo?.totalNum || applyInfo?.orderItemNum) || 0
|
||||||
|
const totalPrice = Number(applyInfo?.applyRefundPrice) || 0
|
||||||
|
const qty = Number(num) || 1
|
||||||
|
if (!totalNum || totalNum <= 0) return totalPrice
|
||||||
|
if (qty >= totalNum) return totalPrice
|
||||||
|
const unit = totalPrice / totalNum
|
||||||
|
return Math.round(unit * qty * 100) / 100
|
||||||
|
}
|
||||||
77
utils/giftCard.js
Normal file
77
utils/giftCard.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* 礼品卡(现金卡)展示与状态工具,与 PC 买家端对齐。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const GIFT_CARD_STATUS = {
|
||||||
|
AVAILABLE: 'AVAILABLE',
|
||||||
|
UNAVAILABLE: 'UNAVAILABLE',
|
||||||
|
PENDING_ACTIVATION: 'PENDING_ACTIVATION',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GIFT_CARD_PAY_NOTICE =
|
||||||
|
'请在礼品卡有效期内使用;结算时勾选礼品卡即可按规则抵扣订单金额,可与优惠券等活动叠加规则以平台说明为准。放弃勾选或取消抵扣将恢复对应余额。'
|
||||||
|
|
||||||
|
export const GIFT_CARD_USE_NOTICE =
|
||||||
|
'请在有效期内使用本礼品卡;消费时将优先使用卡内余额。具体使用范围以活动规则为准。如有疑问请联系客服。'
|
||||||
|
|
||||||
|
export function formatGiftFaceValue(val) {
|
||||||
|
if (val == null || val === '') {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
const n = Number(val)
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
return String(val)
|
||||||
|
}
|
||||||
|
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatGiftExpire(row) {
|
||||||
|
const t = row && row.expireTime
|
||||||
|
if (!t) {
|
||||||
|
return '长期有效'
|
||||||
|
}
|
||||||
|
const d = new Date(t)
|
||||||
|
if (Number.isNaN(d.getTime())) {
|
||||||
|
return '长期有效'
|
||||||
|
}
|
||||||
|
if (d.getFullYear() >= 2099) {
|
||||||
|
return '长期有效'
|
||||||
|
}
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day} 到期`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMemberCardId(row) {
|
||||||
|
if (!row) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (row.memberCardId != null && row.memberCardId !== '') {
|
||||||
|
return String(row.memberCardId)
|
||||||
|
}
|
||||||
|
if (row.id != null && row.id !== '') {
|
||||||
|
return String(row.id)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decorateGiftCard(item, selectedIds = []) {
|
||||||
|
const ids = (selectedIds || []).map(String)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
selected: ids.indexOf(String(item && item.id)) !== -1,
|
||||||
|
faceText: formatGiftFaceValue(item && item.faceValue),
|
||||||
|
expireText: formatGiftExpire(item),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function giftCardActionLabel(status) {
|
||||||
|
if (status === GIFT_CARD_STATUS.PENDING_ACTIVATION) {
|
||||||
|
return '激活自用'
|
||||||
|
}
|
||||||
|
if (status === GIFT_CARD_STATUS.AVAILABLE) {
|
||||||
|
return '去使用'
|
||||||
|
}
|
||||||
|
return '不可用'
|
||||||
|
}
|
||||||
@@ -13,6 +13,22 @@ export const E_COUPON_ORDER_TYPE = 'E_COUPON'
|
|||||||
export const VIRTUAL_GOODS_TYPE = 'VIRTUAL_GOODS'
|
export const VIRTUAL_GOODS_TYPE = 'VIRTUAL_GOODS'
|
||||||
export const VIRTUAL_ORDER_TYPE = 'VIRTUAL'
|
export const VIRTUAL_ORDER_TYPE = 'VIRTUAL'
|
||||||
|
|
||||||
|
import {
|
||||||
|
APPOINTMENT_GOODS_TYPE,
|
||||||
|
APPOINTMENT_ORDER_TYPE,
|
||||||
|
isAppointmentGoods,
|
||||||
|
isAppointmentOrder,
|
||||||
|
hasAppointmentInCheckout,
|
||||||
|
} from './appointmentGoods.js'
|
||||||
|
|
||||||
|
export {
|
||||||
|
APPOINTMENT_GOODS_TYPE,
|
||||||
|
APPOINTMENT_ORDER_TYPE,
|
||||||
|
isAppointmentGoods,
|
||||||
|
isAppointmentOrder,
|
||||||
|
hasAppointmentInCheckout,
|
||||||
|
}
|
||||||
|
|
||||||
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
||||||
export const E_COUPON_MAX_BUY_NUM = 999
|
export const E_COUPON_MAX_BUY_NUM = 999
|
||||||
|
|
||||||
@@ -32,13 +48,13 @@ export function isVirtualOrder(orderType) {
|
|||||||
return orderType === VIRTUAL_ORDER_TYPE
|
return orderType === VIRTUAL_ORDER_TYPE
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 非实物:含 E_COUPON 与核销型 VIRTUAL_GOODS(详情页隐藏「送至」等) */
|
/** 非实物:含 E_COUPON、预约商品与核销型 VIRTUAL_GOODS(详情页隐藏「送至」等) */
|
||||||
export function isNonPhysicalGoods(goodsType) {
|
export function isNonPhysicalGoods(goodsType) {
|
||||||
return isECoupon(goodsType) || isVirtualGoods(goodsType)
|
return isECoupon(goodsType) || isVirtualGoods(goodsType) || isAppointmentGoods(goodsType)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isNonPhysicalOrder(orderType) {
|
export function isNonPhysicalOrder(orderType) {
|
||||||
return isECouponOrder(orderType) || isVirtualOrder(orderType)
|
return isECouponOrder(orderType) || isVirtualOrder(orderType) || isAppointmentOrder(orderType)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 可售库存:E_COUPON 读 SKU quantity(由卡池 syncSkuStock 同步) */
|
/** 可售库存:E_COUPON 读 SKU quantity(由卡池 syncSkuStock 同步) */
|
||||||
|
|||||||
Reference in New Issue
Block a user