mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-11-07 17:38:54 +08:00
Merge branch 'feat/appointment-goods'
This commit is contained in:
@@ -148,6 +148,7 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
|
|||||||
- ✅ 卡密商品
|
- ✅ 卡密商品
|
||||||
- ✅ 二级分销
|
- ✅ 二级分销
|
||||||
- ✅ 定时任务管理
|
- ✅ 定时任务管理
|
||||||
|
- ✅ 服务商品
|
||||||
- 🔜 主题色管理
|
- 🔜 主题色管理
|
||||||
- 🔜 店铺结算汇总
|
- 🔜 店铺结算汇总
|
||||||
- 🔜 支付方式汇总
|
- 🔜 支付方式汇总
|
||||||
|
|||||||
43
api/appointment.js
Normal file
43
api/appointment.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 预约服务买家端 API
|
||||||
|
*/
|
||||||
|
import { http, Method } from '@/utils/request.js'
|
||||||
|
|
||||||
|
/** 查询可预约时段 */
|
||||||
|
export function listSlots(skuId, slotDate) {
|
||||||
|
return http.request({
|
||||||
|
url: '/appointment/slots',
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
params: { skuId, slotDate },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定预约结算上下文(addToCart 后、createTrade 前) */
|
||||||
|
export function bindCheckout(data) {
|
||||||
|
return http.request({
|
||||||
|
url: '/appointment/checkout/bind',
|
||||||
|
method: Method.POST,
|
||||||
|
needToken: true,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约商品扩展配置 */
|
||||||
|
export function getGoodsExt(goodsId) {
|
||||||
|
return http.request({
|
||||||
|
url: `/appointment/goods/${goodsId}/ext`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 买家预约订单详情(时段、核销码) */
|
||||||
|
export function getAppointmentOrderDetail(orderSn) {
|
||||||
|
return http.request({
|
||||||
|
url: `/appointment/order/${orderSn}`,
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,11 @@
|
|||||||
<span>,{{ num }}个</span>
|
<span>,{{ num }}个</span>
|
||||||
</span>
|
</span>
|
||||||
</view>
|
</view>
|
||||||
<view class="goods-check-skus">
|
<view class="goods-check-skus" v-if="isAppointmentGoods(goodsDetail?.goodsType)">
|
||||||
|
可预约
|
||||||
|
<span class="goods-check-skus-name">按时段选择</span>
|
||||||
|
</view>
|
||||||
|
<view class="goods-check-skus" v-else>
|
||||||
库存
|
库存
|
||||||
<span class="goods-check-skus-name">
|
<span class="goods-check-skus-name">
|
||||||
<span>{{ availableStock }}</span>
|
<span>{{ availableStock }}</span>
|
||||||
@@ -105,7 +109,9 @@
|
|||||||
<!-- 按钮 -->
|
<!-- 按钮 -->
|
||||||
<view class="btns" v-if="availableStock !== 0">
|
<view class="btns" v-if="availableStock !== 0">
|
||||||
<view class="box-btn card" v-if="showAddToCart" @click="addToCartOrBuy('cart')">加入购物车</view>
|
<view class="box-btn card" v-if="showAddToCart" @click="addToCartOrBuy('cart')">加入购物车</view>
|
||||||
<view class="box-btn buy" @click="addToCartOrBuy('buy')">立即购买</view>
|
<view class="box-btn buy" @click="addToCartOrBuy('buy')">
|
||||||
|
{{ isAppointmentGoods(goodsDetail?.goodsType) ? '立即预约' : '立即购买' }}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</u-popup>
|
</u-popup>
|
||||||
@@ -121,8 +127,11 @@ import { goodsFormatPrice } from '@/utils/filters.js'
|
|||||||
import {
|
import {
|
||||||
isECoupon,
|
isECoupon,
|
||||||
isVirtualGoods,
|
isVirtualGoods,
|
||||||
|
isAppointmentGoods,
|
||||||
getECouponStock,
|
getECouponStock,
|
||||||
getECouponMaxBuyNum,
|
getECouponMaxBuyNum,
|
||||||
|
getAppointmentStock,
|
||||||
|
getAppointmentMaxBuyNum,
|
||||||
} from '@/utils/goodsType.js'
|
} from '@/utils/goodsType.js'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
@@ -158,11 +167,14 @@ const formatList = ref<any[]>([])
|
|||||||
const currentSelected = ref<string[]>([])
|
const currentSelected = ref<string[]>([])
|
||||||
const skuList = ref('')
|
const skuList = ref('')
|
||||||
|
|
||||||
// E_COUPON 库存读 SKU quantity,上限 min(quantity, 999)
|
// E_COUPON 读 SKU quantity;预约商品 quantity 固定 0,库存走时段
|
||||||
const availableStock = computed(() => {
|
const availableStock = computed(() => {
|
||||||
if (isECoupon(props.goodsDetail?.goodsType)) {
|
if (isECoupon(props.goodsDetail?.goodsType)) {
|
||||||
return getECouponStock(props.goodsDetail)
|
return getECouponStock(props.goodsDetail)
|
||||||
}
|
}
|
||||||
|
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
|
||||||
|
return getAppointmentStock(props.goodsDetail)
|
||||||
|
}
|
||||||
return Number(props.goodsDetail?.quantity) || 0
|
return Number(props.goodsDetail?.quantity) || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -170,15 +182,19 @@ const maxBuyNum = computed(() => {
|
|||||||
if (isECoupon(props.goodsDetail?.goodsType)) {
|
if (isECoupon(props.goodsDetail?.goodsType)) {
|
||||||
return getECouponMaxBuyNum(props.goodsDetail)
|
return getECouponMaxBuyNum(props.goodsDetail)
|
||||||
}
|
}
|
||||||
|
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
|
||||||
|
return getAppointmentMaxBuyNum(props.goodsDetail)
|
||||||
|
}
|
||||||
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)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -338,6 +354,14 @@ const addToCartOrBuy = (val: string) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
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) {
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
},
|
},
|
||||||
"plugins" : {},
|
"plugins" : {},
|
||||||
"libVersion" : "3.5.8",
|
"libVersion" : "3.5.8",
|
||||||
"__usePrivacyCheck__" : true,
|
"__usePrivacyCheck__" : false,
|
||||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
|
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
|
||||||
},
|
},
|
||||||
"h5" : {
|
"h5" : {
|
||||||
|
|||||||
18
pages.json
18
pages.json
@@ -771,6 +771,18 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "gift-card/myGiftCards",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的礼品卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "gift-card/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "礼品卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "payment/payOrder",
|
"path": "payment/payOrder",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -867,6 +879,12 @@
|
|||||||
"enablePullDownRefresh": true
|
"enablePullDownRefresh": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "appointment/appointmentCheckout",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "立即预约"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "deliverDetail",
|
"path": "deliverDetail",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
209
pages/cart/gift-card/index.vue
Normal file
209
pages/cart/gift-card/index.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<view class="b-content" :style="themeStyle">
|
||||||
|
<scroll-view class="list-scroll-content" scroll-y>
|
||||||
|
<u-empty
|
||||||
|
mode="coupon"
|
||||||
|
text="暂无可用礼品卡"
|
||||||
|
v-if="!cardList.length"
|
||||||
|
></u-empty>
|
||||||
|
|
||||||
|
<view class="coupon-card" v-for="item in cardList" :key="item.id">
|
||||||
|
<view class="coupon-card-left">
|
||||||
|
<view class="price-row">
|
||||||
|
<text class="coupon-price-symbol">¥</text>
|
||||||
|
<text class="coupon-price-value">{{ unitPrice(item.balance) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="coupon-divider"></view>
|
||||||
|
<view class="coupon-card-right">
|
||||||
|
<text class="coupon-card-name">{{ item.giftCardName || '礼品卡' }}</text>
|
||||||
|
<view class="coupon-card-foot">
|
||||||
|
<view
|
||||||
|
class="coupon-status-btn"
|
||||||
|
:class="{ cancel: item.selected }"
|
||||||
|
@click="clickWay(item)"
|
||||||
|
>
|
||||||
|
{{ item.selected ? '取消使用' : '立即使用' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="notice" v-if="cardList.length">{{ GIFT_CARD_PAY_NOTICE }}</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import { selectGiftCard } from '@/api/trade.js'
|
||||||
|
import { unitPrice } from '@/utils/filters.js'
|
||||||
|
import { decorateGiftCard, GIFT_CARD_PAY_NOTICE } from '@/utils/giftCard.js'
|
||||||
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
const way = ref('')
|
||||||
|
const cardList = ref<any[]>([])
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
way.value = options?.way || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
const selectedIds = (store.state as any).selectedGiftCardIds || []
|
||||||
|
const cards = (store.state as any).canUseGiftCards || []
|
||||||
|
cardList.value = cards.map((item: any) => decorateGiftCard(item, selectedIds))
|
||||||
|
})
|
||||||
|
|
||||||
|
function clickWay(item: any) {
|
||||||
|
if (!item?.id) return
|
||||||
|
selectGiftCard({
|
||||||
|
way: way.value,
|
||||||
|
credentialId: String(item.id),
|
||||||
|
used: !item.selected,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.data.success) {
|
||||||
|
uni.navigateBack()
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res.data.message || '礼品卡选择失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.b-content {
|
||||||
|
background: #f7f8fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-scroll-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
position: relative;
|
||||||
|
border: 1rpx solid #f0f1f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card-left {
|
||||||
|
width: 200rpx;
|
||||||
|
min-height: 148rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #ff3b30;
|
||||||
|
background: linear-gradient(135deg, #fff5f5 0%, #ffecec 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-price-symbol {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-price-value {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-price-label {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ff8a80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-divider {
|
||||||
|
position: absolute;
|
||||||
|
left: 200rpx;
|
||||||
|
top: 24rpx;
|
||||||
|
bottom: 24rpx;
|
||||||
|
width: 0;
|
||||||
|
border-left: 2rpx dashed #f0d6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card-right {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 24rpx 24rpx 20rpx 28rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card-desc,
|
||||||
|
.coupon-card-time {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8a8f99;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card-time {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-card-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-status-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, var(--theme-light, #ff6b35), var(--theme-primary, #ff4b2b));
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -124,7 +124,7 @@ function hideLoadingIfNeeded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callback(paymentMethod: string) {
|
function callback(paymentMethod: string) {
|
||||||
uni.navigateTo({
|
uni.redirectTo({
|
||||||
url:
|
url:
|
||||||
'/pages/cart/payment/success?paymentMethod=' +
|
'/pages/cart/payment/success?paymentMethod=' +
|
||||||
paymentMethod +
|
paymentMethod +
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ function checkOrder() {
|
|||||||
uni.reLaunch({
|
uni.reLaunch({
|
||||||
url: '/pages/mine/deposit/operation',
|
url: '/pages/mine/deposit/operation',
|
||||||
})
|
})
|
||||||
} else {
|
return
|
||||||
navigateTo('/pages/order/myOrder?status=0')
|
|
||||||
}
|
}
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/order/myOrder?status=0',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(url: string, type?: string) {
|
function navigateTo(url: string, type?: string) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</block>
|
</block>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<div class="keyword-box" v-show="!isShowKeywordList">
|
<div class="keyword-box" v-show="!isShowKeywordList">
|
||||||
<view class="keyword-block add1">
|
<view class="keyword-block add1" v-if="hotKeywordList.length">
|
||||||
<view class="keyword-list-header">
|
<view class="keyword-list-header">
|
||||||
<view class="u-tips">热门搜索</view>
|
<view class="u-tips">热门搜索</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -624,31 +624,43 @@ function loadOldKeyword(index: number) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHistoryKeywords(limit = 10) {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync('OldKeys')
|
||||||
|
const list = raw ? JSON.parse(raw) : []
|
||||||
|
return Array.isArray(list) ? list.filter(Boolean).slice(0, limit) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadHotKeyword() {
|
async function loadHotKeyword() {
|
||||||
hotKeywordList.value = []
|
const cached = storage.getHotWords() || {}
|
||||||
if (
|
const now = Date.now() / 1000
|
||||||
!storage.getHotWords().time ||
|
let keywords: string[] = []
|
||||||
storage.getHotWords().time <= new Date().getTime() / 1000
|
|
||||||
) {
|
|
||||||
const res = await getHotKeywords(10)
|
|
||||||
const keywords = res.data.result
|
|
||||||
|
|
||||||
keywords.forEach((item: string) => {
|
if (cached.time && cached.time > now && Array.isArray(cached.keywords) && cached.keywords.length) {
|
||||||
hotKeywordList.value.push(item)
|
keywords = cached.keywords
|
||||||
})
|
|
||||||
|
|
||||||
const hotData = {
|
|
||||||
time: new Date().getTime() / 1000 + 30 * 5,
|
|
||||||
keywords: keywords,
|
|
||||||
}
|
|
||||||
storage.setHotWords(hotData)
|
|
||||||
} else {
|
} else {
|
||||||
const keywords = storage.getHotWords().keywords
|
try {
|
||||||
|
const res = await getHotKeywords(10)
|
||||||
keywords.forEach((item: string) => {
|
if (res?.data?.success && Array.isArray(res.data.result)) {
|
||||||
hotKeywordList.value.push(item)
|
keywords = res.data.result.filter(Boolean)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
keywords = []
|
||||||
|
}
|
||||||
|
storage.setHotWords({
|
||||||
|
time: now + 30 * 5,
|
||||||
|
keywords,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!keywords.length) {
|
||||||
|
keywords = getHistoryKeywords(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
hotKeywordList.value = keywords
|
||||||
loadDefaultKeyword()
|
loadDefaultKeyword()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<template v-for="(item,index) in sku.orderItems" :key="index">
|
<template v-for="(item,index) in sku.orderItems" :key="index">
|
||||||
<view class="goods-item-view" v-if="item.sn == sn"
|
<view class="goods-item-view" v-if="item && item.sn == sn"
|
||||||
@click="gotoGoodsDetail(sku.goodsId || sku.goods_id)">
|
@click="gotoGoodsDetail(sku.goodsId || sku.goods_id)">
|
||||||
<view class="goods-img">
|
<view class="goods-img">
|
||||||
<u-image
|
<u-image
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
<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 || 0 }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -41,10 +41,10 @@
|
|||||||
<view class="after-num">
|
<view class="after-num">
|
||||||
<text class="after-num-label">申请数量</text>
|
<text class="after-num-label">申请数量</text>
|
||||||
<u-number-box
|
<u-number-box
|
||||||
:value="parseInt(form.num)"
|
:value="applyNum"
|
||||||
disabled-input
|
disabled-input
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="parseInt(sku.num)"
|
:max="maxApplyNum"
|
||||||
bg-color="#fff"
|
bg-color="#fff"
|
||||||
@change="valChange"
|
@change="valChange"
|
||||||
></u-number-box>
|
></u-number-box>
|
||||||
@@ -169,6 +169,8 @@ 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,7 @@ 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 form = reactive({
|
const form = reactive({
|
||||||
orderItemSn: '',
|
orderItemSn: '',
|
||||||
@@ -203,6 +206,21 @@ const form = reactive({
|
|||||||
bankAccountNumber: '',
|
bankAccountNumber: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const displayRefundPrice = computed(() => {
|
||||||
|
if (!isAppointmentAfterSale.value) return applyInfo.value?.applyRefundPrice
|
||||||
|
return computePartialRefundPrice(applyInfo.value, form.num)
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxApplyNum = computed(() => {
|
||||||
|
const value = Number(sku.value?.num)
|
||||||
|
return value > 0 ? value : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const applyNum = computed(() => {
|
||||||
|
const value = Number(form.num)
|
||||||
|
return value > 0 ? value : 1
|
||||||
|
})
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
let navTitle = '申请售后'
|
let navTitle = '申请售后'
|
||||||
form.serviceType = 'RETURN_GOODS'
|
form.serviceType = 'RETURN_GOODS'
|
||||||
@@ -220,11 +238,14 @@ onLoad((options) => {
|
|||||||
}
|
}
|
||||||
uni.setNavigationBarTitle({ title: navTitle })
|
uni.setNavigationBarTitle({ title: navTitle })
|
||||||
sn.value = options?.sn || ''
|
sn.value = options?.sn || ''
|
||||||
sku.value = storage.getAfterSaleData()
|
const cached = storage.getAfterSaleData()
|
||||||
|
sku.value = cached && typeof cached === 'object' ? cached : { orderItems: [] }
|
||||||
|
if (!Array.isArray(sku.value.orderItems)) sku.value.orderItems = []
|
||||||
|
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 = Number(sku.value.num) > 0 ? Number(sku.value.num) : 1
|
||||||
form.goodsId = sku.value.goodsId
|
form.goodsId = sku.value.goodsId || ''
|
||||||
fetchReasonActions(form.serviceType)
|
fetchReasonActions(form.serviceType)
|
||||||
init(options?.sn || '')
|
init(options?.sn || '')
|
||||||
})
|
})
|
||||||
@@ -271,8 +292,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 +319,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 +351,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()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<template v-for="(item,index) in sku.orderItems" :key="index">
|
<template v-for="(item,index) in sku.orderItems" :key="index">
|
||||||
<view class="goods-item-view" v-if="item.sn == sn" @click="navigateToGoodsDetail(sku.skuId)">
|
<view class="goods-item-view" v-if="item && item.sn == sn" @click="navigateToGoodsDetail(sku.skuId)">
|
||||||
<view class="goods-img">
|
<view class="goods-img">
|
||||||
<u-image
|
<u-image
|
||||||
border-radius="6"
|
border-radius="6"
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
<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 v-if="sku.orderItems.length <= 1">¥{{ unitPrice(sku.flowPrice) }}</text>
|
<text v-if="!sku.orderItems || sku.orderItems.length <= 1">¥{{ unitPrice(sku.flowPrice) }}</text>
|
||||||
<text class="num">购买数量: {{ item.num }}</text>
|
<text class="num">购买数量: {{ item.num || 0 }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -72,7 +72,9 @@ const applyInfo = ref<any>({})
|
|||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
sn.value = options?.sn || ''
|
sn.value = options?.sn || ''
|
||||||
sku.value = storage.getAfterSaleData()
|
const cached = storage.getAfterSaleData()
|
||||||
|
sku.value = cached && typeof cached === 'object' ? cached : { orderItems: [] }
|
||||||
|
if (!Array.isArray(sku.value.orderItems)) sku.value.orderItems = []
|
||||||
if (isECouponOrder(sku.value?.orderType)) {
|
if (isECouponOrder(sku.value?.orderType)) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '电子卡券订单不支持售后',
|
title: '电子卡券订单不支持售后',
|
||||||
|
|||||||
1131
pages/order/appointment/appointmentCheckout.vue
Normal file
1131
pages/order/appointment/appointmentCheckout.vue
Normal file
File diff suppressed because it is too large
Load Diff
44
pages/order/components/verificationCodes.vue
Normal file
44
pages/order/components/verificationCodes.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="items.length" class="verify-codes">
|
||||||
|
<view v-for="(item, index) in items" :key="item.key" class="verify-code">
|
||||||
|
<text class="verify-code__label">{{ index === 0 ? '核销码' : '' }}</text>
|
||||||
|
<text class="verify-code__text" :class="{ 'is-struck': item.struck }">{{ item.code }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.verify-code {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.verify-code__label {
|
||||||
|
color: #999;
|
||||||
|
width: 140rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.verify-code__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.verify-code__text.is-struck {
|
||||||
|
color: #999;
|
||||||
|
font-weight: normal;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper" :style="themeStyle">
|
||||||
<!-- 选择地址 -->
|
<!-- 选择地址 -->
|
||||||
<div
|
<div
|
||||||
class="address-box"
|
class="address-box"
|
||||||
@@ -68,6 +68,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<view class="appointment-card" v-if="isAppointmentCheckout && appointmentCheckoutView">
|
||||||
|
<view class="appointment-card__title">预约信息</view>
|
||||||
|
<view class="appointment-hero">
|
||||||
|
<view class="appointment-hero__time">{{ appointmentCheckoutView.time }}</view>
|
||||||
|
<view class="appointment-hero__meta">
|
||||||
|
<text>{{ appointmentCheckoutView.dateText }} {{ appointmentCheckoutView.week }}</text>
|
||||||
|
<text v-if="appointmentCheckoutView.mode" class="appointment-tag">{{ appointmentCheckoutView.mode }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="appointment-form" v-if="appointmentCheckoutView.formRows.length">
|
||||||
|
<template v-for="(row, index) in appointmentCheckoutView.formRows" :key="index">
|
||||||
|
<view v-if="row.type === 'group'" class="appointment-group">{{ row.label }}</view>
|
||||||
|
<view v-else class="appointment-row">
|
||||||
|
<text class="appointment-row__label">{{ row.label }}</text>
|
||||||
|
<text class="appointment-row__value">{{ row.value }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</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,34 +356,24 @@
|
|||||||
</u-col>
|
</u-col>
|
||||||
</u-row>
|
</u-row>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="giftCardList.length || giftCardDeductAmount > 0">
|
<u-row>
|
||||||
<u-row>
|
<u-col :span="8" @click="openGiftCards()">使用礼品卡</u-col>
|
||||||
<u-col :span="6">礼品卡</u-col>
|
<u-col
|
||||||
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
:span="4"
|
||||||
<span v-if="giftCardDeductAmount > 0" class="main-color"
|
textAlign="right"
|
||||||
>-¥{{ unitPrice(giftCardDeductAmount) }}</span
|
class="coupon-col"
|
||||||
>
|
@click="openGiftCards()"
|
||||||
<span v-else @click="giftCardExpanded = !giftCardExpanded"
|
>
|
||||||
>{{ giftCardList.length }} 张可用</span
|
<view class="coupon-col__content" v-if="giftCardDeductAmount > 0">
|
||||||
>
|
<text class="main-color">-¥{{ unitPrice(giftCardDeductAmount) }}</text>
|
||||||
</u-col>
|
<u-icon name="arrow-right" size="14" color="#909399"></u-icon>
|
||||||
</u-row>
|
|
||||||
<view v-if="giftCardExpanded && giftCardList.length" class="gift-card-list">
|
|
||||||
<view
|
|
||||||
class="gift-card-item"
|
|
||||||
:class="{ 'gift-card-item--selected': isGiftCardSelected(item) }"
|
|
||||||
v-for="item in giftCardList"
|
|
||||||
:key="item.id"
|
|
||||||
@click="toggleGiftCard(item)"
|
|
||||||
>
|
|
||||||
<view class="gift-card-item__name">{{ item.giftCardName || '礼品卡' }}</view>
|
|
||||||
<view class="gift-card-item__meta">
|
|
||||||
<text>余额 ¥{{ unitPrice(item.balance) }}</text>
|
|
||||||
<text class="gift-card-item__no">{{ item.cardNo }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="coupon-col__content" v-else>
|
||||||
</div>
|
<text>{{ giftCardList.length }} 张可用</text>
|
||||||
|
<u-icon name="arrow-right" size="14" color="#909399"></u-icon>
|
||||||
|
</view>
|
||||||
|
</u-col>
|
||||||
|
</u-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 配送地区没有提示 -->
|
<!-- 配送地区没有提示 -->
|
||||||
@@ -416,11 +426,11 @@
|
|||||||
import { ref, computed, watch, getCurrentInstance } from 'vue'
|
import { ref, computed, watch, getCurrentInstance } from 'vue'
|
||||||
import { onLoad, onShow, onBackPress } from '@dcloudio/uni-app'
|
import { onLoad, onShow, onBackPress } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
import * as API_Address from '@/api/address'
|
import * as API_Address from '@/api/address'
|
||||||
import * as API_Order from '@/api/order'
|
import * as API_Order from '@/api/order'
|
||||||
import * as API_Trade from '@/api/trade'
|
import * as API_Trade from '@/api/trade'
|
||||||
import configs from '@/config/config'
|
import configs from '@/config/config'
|
||||||
import LiLiWXPay from '@/js_sdk/lili-pay/wx-pay.js'
|
|
||||||
import invoices from '@/pages/order/invoice/setInvoice'
|
import invoices from '@/pages/order/invoice/setInvoice'
|
||||||
import {
|
import {
|
||||||
unitPrice,
|
unitPrice,
|
||||||
@@ -428,9 +438,12 @@ 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'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
const { proxy } = getCurrentInstance()!
|
const { proxy } = getCurrentInstance()!
|
||||||
const lightColor = computed(() => store.getters.lightColor)
|
const lightColor = computed(() => store.getters.lightColor)
|
||||||
const mainColor = computed(() => store.getters.mainColor)
|
const mainColor = computed(() => store.getters.mainColor)
|
||||||
@@ -474,10 +487,71 @@ 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 APPOINTMENT_WEEK_LABELS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
|
|
||||||
|
function presentAppointmentFormRows(rows: any[]) {
|
||||||
|
const result: any[] = []
|
||||||
|
let lastGroup = ''
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (row?.type === 'group') {
|
||||||
|
result.push(row)
|
||||||
|
lastGroup = row.label || ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matched = String(row?.label || '').match(/^(.*?)(?:\s*[((](\d+)[))])$/)
|
||||||
|
if (!matched) {
|
||||||
|
result.push(row)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const group = `第${matched[2]}位`
|
||||||
|
if (group !== lastGroup) {
|
||||||
|
result.push({ type: 'group', label: group })
|
||||||
|
lastGroup = group
|
||||||
|
}
|
||||||
|
result.push({ label: matched[1], value: row.value })
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const appointmentCheckoutView = computed(() => {
|
||||||
|
const ctx =
|
||||||
|
appointmentCheckoutContext.value ||
|
||||||
|
orderMessage.value?.appointmentCheckout ||
|
||||||
|
orderMessage.value?.appointmentContext
|
||||||
|
if (!ctx) return null
|
||||||
|
const date = formatDate(ctx.slotDate)
|
||||||
|
let dateText = ''
|
||||||
|
let week = ''
|
||||||
|
if (date) {
|
||||||
|
const [year, month, day] = date.split('-').map(Number)
|
||||||
|
const current = new Date(year, month - 1, day)
|
||||||
|
dateText = `${month}月${day}日`
|
||||||
|
week = APPOINTMENT_WEEK_LABELS[current.getDay()] || ''
|
||||||
|
}
|
||||||
|
const time =
|
||||||
|
ctx.slotStartTime && ctx.slotEndTime ? `${ctx.slotStartTime} - ${ctx.slotEndTime}` : dateText
|
||||||
|
const mode = SERVICE_MODE_LABEL[ctx.serviceMode] || ctx.serviceMode || ''
|
||||||
|
const formRows = []
|
||||||
|
if (ctx.serviceAddressSnapshot) {
|
||||||
|
formRows.push({ label: '服务地址', value: ctx.serviceAddressSnapshot })
|
||||||
|
}
|
||||||
|
const savedRows = Array.isArray(ctx.formRows) ? ctx.formRows : []
|
||||||
|
if (savedRows.length) {
|
||||||
|
formRows.push(...presentAppointmentFormRows(savedRows))
|
||||||
|
} else {
|
||||||
|
if (ctx.contactName) formRows.push({ label: '联系人', value: ctx.contactName })
|
||||||
|
if (ctx.contactPhone) formRows.push({ label: '联系方式', value: ctx.contactPhone })
|
||||||
|
}
|
||||||
|
if (!time && !formRows.length) return null
|
||||||
|
return { dateText, week, time, mode, formRows }
|
||||||
|
})
|
||||||
|
|
||||||
const giftCardList = computed(() => orderMessage.value?.canUseGiftCards || [])
|
const giftCardList = computed(() => orderMessage.value?.canUseGiftCards || [])
|
||||||
const giftCardDeductAmount = computed(() => {
|
const giftCardDeductAmount = computed(() => {
|
||||||
@@ -488,8 +562,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 +599,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,27 +678,10 @@ function invoice() {
|
|||||||
invoiceFlag.value = true
|
invoiceFlag.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGiftCardSelected(item: any) {
|
function openGiftCards() {
|
||||||
if (!item?.id) return false
|
;(store.state as any).canUseGiftCards = giftCardList.value
|
||||||
return (selectedGiftCardIds.value || []).map(String).includes(String(item.id))
|
;(store.state as any).selectedGiftCardIds = orderMessage.value?.selectedGiftCardIds || []
|
||||||
}
|
navigateTo(`/pages/cart/gift-card/index?way=${routerVal.value.way}`)
|
||||||
|
|
||||||
function toggleGiftCard(item: any) {
|
|
||||||
if (!item?.id) return
|
|
||||||
API_Trade.selectGiftCard({
|
|
||||||
way: routerVal.value.way,
|
|
||||||
credentialId: String(item.id),
|
|
||||||
used: !isGiftCardSelected(item),
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.data.success) {
|
|
||||||
getOrderList()
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: res.data.message || '礼品卡选择失败',
|
|
||||||
icon: 'none',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GET_Discount() {
|
function GET_Discount() {
|
||||||
@@ -717,44 +773,57 @@ function createTradeFun() {
|
|||||||
delete submit.parentOrderSn
|
delete submit.parentOrderSn
|
||||||
}
|
}
|
||||||
|
|
||||||
API_Trade.createTrade(submit).then((res) => {
|
const runCreate = () =>
|
||||||
if (res.data.success) {
|
API_Trade.createTrade(submit).then((res) => {
|
||||||
uni.showToast({
|
if (res.data.success) {
|
||||||
title: '创建订单成功!',
|
uni.removeStorageSync('appointment_checkout_context')
|
||||||
duration: 2000,
|
uni.showToast({
|
||||||
icon: 'none',
|
title: '创建订单成功!',
|
||||||
})
|
duration: 2000,
|
||||||
if (orderMessage.value.priceDetailDTO.billPrice == 0) {
|
icon: 'none',
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/order/myOrder?status=0',
|
|
||||||
})
|
})
|
||||||
|
if (orderMessage.value.priceDetailDTO.billPrice == 0) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/order/myOrder?status=0',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
navigateTo(
|
||||||
|
`/pages/cart/payment/payOrder?trade_sn=${res.data.result.sn}`
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// #ifdef MP-WEIXIN
|
uni.showToast({
|
||||||
pay(res.data.result.sn)
|
title: res.data.message,
|
||||||
// #endif
|
duration: 2000,
|
||||||
|
icon: 'none',
|
||||||
// #ifndef MP-WEIXIN
|
})
|
||||||
navigateTo(
|
|
||||||
`/pages/cart/payment/payOrder?trade_sn=${res.data.result.sn}`
|
|
||||||
)
|
|
||||||
// #endif
|
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
|
|
||||||
|
if (isAppointmentCheckout.value) {
|
||||||
|
const ctx =
|
||||||
|
appointmentCheckoutContext.value ||
|
||||||
|
uni.getStorageSync('appointment_checkout_context')
|
||||||
|
if (!ctx) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: res.data.message,
|
title: '预约信息不完整,请返回重选时段',
|
||||||
duration: 2000,
|
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
bindCheckout(ctx)
|
||||||
}, 3000)
|
.then(runCreate)
|
||||||
}
|
.catch((e: any) => {
|
||||||
|
uni.showToast({
|
||||||
|
title: e?.data?.message || e?.message || '预约信息提交失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
async function pay(sn: string) {
|
runCreate()
|
||||||
new LiLiWXPay({
|
}, 3000)
|
||||||
sn,
|
|
||||||
price: orderMessage.value.priceDetailDTO.billPrice,
|
|
||||||
}).pay()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserAddress() {
|
function getUserAddress() {
|
||||||
@@ -860,9 +929,11 @@ function getOrderList() {
|
|||||||
orderMessage.value = res.data.result
|
orderMessage.value = res.data.result
|
||||||
;(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
|
||||||
|
;(store.state as any).canUseGiftCards = res.data.result.canUseGiftCards || []
|
||||||
|
;(store.state as any).selectedGiftCardIds = res.data.result.selectedGiftCardIds || []
|
||||||
|
|
||||||
// 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 +965,89 @@ page {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.appointment-card {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 28rpx 32rpx 12rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
}
|
||||||
|
.appointment-card__title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.appointment-hero {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
.appointment-hero__time {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
.appointment-hero__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.appointment-tag {
|
||||||
|
padding: 2rpx 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: var(--theme-primary-10, rgba(255, 60, 42, 0.1));
|
||||||
|
color: var(--theme-primary, #ff3c2a);
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.appointment-form {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
border-top: 1rpx solid #f2f2f2;
|
||||||
|
}
|
||||||
|
.appointment-group {
|
||||||
|
padding-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--theme-primary, #ff3c2a);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.appointment-row + .appointment-group {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
border-top: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
.appointment-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 22rpx 0;
|
||||||
|
}
|
||||||
|
.appointment-row + .appointment-row {
|
||||||
|
border-top: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
.appointment-group + .appointment-row {
|
||||||
|
padding-top: 12rpx;
|
||||||
|
}
|
||||||
|
.appointment-row__label {
|
||||||
|
flex: none;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.appointment-row__value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-8 {
|
.flex-8 {
|
||||||
flex: 8;
|
flex: 8;
|
||||||
}
|
}
|
||||||
@@ -1061,6 +1215,7 @@ page {
|
|||||||
|
|
||||||
.box6 {
|
.box6 {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1072,6 +1227,7 @@ page {
|
|||||||
color: #333;
|
color: #333;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabbar-right {
|
.tabbar-right {
|
||||||
@@ -1162,7 +1318,8 @@ page {
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
height: auto;
|
height: auto;
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
padding-bottom: 200rpx;
|
padding-bottom: calc(240rpx + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(240rpx + env(safe-area-inset-bottom));
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,39 +1407,4 @@ page {
|
|||||||
margin: 20rpx 0;
|
margin: 20rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-card-list {
|
|
||||||
margin-top: 16rpx;
|
|
||||||
padding: 0 8rpx 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-card-item {
|
|
||||||
border: 1rpx solid #eceef2;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
border-color: $main-color;
|
|
||||||
background: rgba($main-color, 0.04);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-card-item__name {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-card-item__meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 8rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-card-item__no {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="content">
|
<view class="content">
|
||||||
<view class="navbar">
|
<scroll-view class="navbar" scroll-x :show-scrollbar="false">
|
||||||
<view v-for="(item, index) in navList" :key="index" class="nav-item"
|
<view class="navbar-inner">
|
||||||
:class="{ current: tabCurrentIndex === index }" @click="tabClick(index)">{{ item.text }}</view>
|
<view v-for="(item, index) in navList" :key="index" class="nav-item"
|
||||||
</view>
|
:class="{ current: tabCurrentIndex === index }" @click="tabClick(index)">{{ item.text }}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
<swiper :current="tabCurrentIndex" class="swiper-box" duration="300" @change="changeTab">
|
<swiper :current="tabCurrentIndex" class="swiper-box" duration="300" @change="changeTab">
|
||||||
<swiper-item class="tab-content" v-for="(tabItem, tabIndex) in navList" :key="tabIndex">
|
<swiper-item class="tab-content" v-for="(tabItem, tabIndex) in navList" :key="tabIndex">
|
||||||
<scroll-view class="list-scroll-content" scroll-y @scrolltolower="loadData(tabIndex)">
|
<scroll-view class="list-scroll-content" scroll-y @scrolltolower="loadData(tabIndex)">
|
||||||
<!-- 空白页 -->
|
<!-- 空白页 -->
|
||||||
<u-empty text="暂无订单" mode="list"
|
<u-empty :text="tabItem.kind === 'afterSale' ? '暂无售后记录' : '暂无订单'" mode="list"
|
||||||
v-if="tabItem.loaded === true && tabItem.orderList.length === 0"></u-empty>
|
v-if="tabItem.loaded === true && tabItem.orderList.length === 0"></u-empty>
|
||||||
<!-- 订单列表 -->
|
<!-- 订单列表 -->
|
||||||
<view class="seller-view" :key="orderIndex" v-for="(order, orderIndex) in tabItem.orderList">
|
<view class="seller-view" :key="orderIndex" v-for="(order, orderIndex) in tabItem.orderList">
|
||||||
|
<template v-if="order.isAfterSale">
|
||||||
|
<view class="seller-info u-flex u-row-between">
|
||||||
|
<view class="seller-name wes">
|
||||||
|
<view class="name">{{ order.storeName }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="order-sn">{{ orderCardStatus(order) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="after-sale-sn">售后单号:{{ order.serviceSn }}</view>
|
||||||
|
<view class="goods-item-view" @click="openOrder(order)">
|
||||||
|
<view class="goods-img">
|
||||||
|
<u-image border-radius="6" width="131rpx" height="131rpx" mode="aspectFill"
|
||||||
|
:src="getOrderItemImage(order.orderItems[0], order, 0)"></u-image>
|
||||||
|
</view>
|
||||||
|
<view class="goods-info">
|
||||||
|
<view class="goods-title u-line-2">{{ order.groupName }}</view>
|
||||||
|
<view class="goods-price">申请退款 ¥{{ unitPrice(order.applyRefundPrice) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="goods-num">
|
||||||
|
<view>x{{ order.groupNum }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="btn-view u-flex u-row-between">
|
||||||
|
<view class="description">{{ afterSaleTypeText(order.serviceType) }}</view>
|
||||||
|
<view class="goods-btn flex flex-a-c">
|
||||||
|
<view class="pay-btn" @click="openOrder(order)">售后详情</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<!-- 店铺名称 -->
|
<!-- 店铺名称 -->
|
||||||
<view class="seller-info u-flex u-row-between">
|
<view class="seller-info u-flex u-row-between">
|
||||||
<view class="seller-name wes" @click="navigateToStore(order)">
|
<view class="seller-name wes" @click="navigateToStore(order)">
|
||||||
@@ -26,12 +57,11 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
<view class="order-sn">{{orderStatusList(order.orderStatus)
|
<view class="order-sn">{{ orderCardStatus(order) }}</view>
|
||||||
}}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view>
|
<view>
|
||||||
<view class="goods-item-view" @click="navigateToOrderDetail(order.sn)">
|
<view class="goods-item-view" @click="openOrder(order)">
|
||||||
<view class="goods-img" v-for="(goods, goodsIndex) in order.orderItems"
|
<view class="goods-img" v-for="(goods, goodsIndex) in order.orderItems"
|
||||||
:key="goodsIndex">
|
:key="goodsIndex">
|
||||||
<u-image border-radius="6" width="131rpx" height="131rpx" mode="aspectFill"
|
<u-image border-radius="6" width="131rpx" height="131rpx" mode="aspectFill"
|
||||||
@@ -62,7 +92,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>
|
||||||
<!-- 等待收货 -->
|
<!-- 等待收货 -->
|
||||||
@@ -70,16 +100,12 @@
|
|||||||
v-if="order.allowOperationVO.rog && !isECouponOrder(order.orderType)" @click="onRog(order.sn)">
|
v-if="order.allowOperationVO.rog && !isECouponOrder(order.orderType)" @click="onRog(order.sn)">
|
||||||
确认收货
|
确认收货
|
||||||
</view>
|
</view>
|
||||||
<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'))"
|
|
||||||
@click="applyService(order)">
|
|
||||||
退款/售后
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
<uni-load-more :status="tabItem.loadStatus"></uni-load-more>
|
<uni-load-more v-if="tabItem.orderList.length > 0" :status="tabItem.loadStatus"></uni-load-more>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
@@ -114,7 +140,7 @@ import { onLoad, onShow, onPullDownRefresh, onBackPress } from '@dcloudio/uni-ap
|
|||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue'
|
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue'
|
||||||
import { getOrderList, cancelOrder, confirmReceipt } from '@/api/order.js'
|
import { getOrderList, cancelOrder, confirmReceipt } from '@/api/order.js'
|
||||||
import { getClearReason } from '@/api/after-sale.js'
|
import { getClearReason, getAfterSaleList } from '@/api/after-sale.js'
|
||||||
import LiLiWXPay from '@/js_sdk/lili-pay/wx-pay.js'
|
import LiLiWXPay from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
import {
|
import {
|
||||||
unitPrice,
|
unitPrice,
|
||||||
@@ -123,7 +149,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 { appointmentOrderStatusText, getAppointmentAllowOperation } from '@/utils/appointmentGoods.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const { proxy } = getCurrentInstance()!
|
const { proxy } = getCurrentInstance()!
|
||||||
@@ -133,6 +160,9 @@ const mainColor = computed(() => store.getters.mainColor)
|
|||||||
interface NavTabItem {
|
interface NavTabItem {
|
||||||
state: number
|
state: number
|
||||||
text: string
|
text: string
|
||||||
|
tag: string
|
||||||
|
commentStatus?: string
|
||||||
|
kind?: 'order' | 'afterSale'
|
||||||
loadStatus: string
|
loadStatus: string
|
||||||
orderList: any[]
|
orderList: any[]
|
||||||
pageNumber: number
|
pageNumber: number
|
||||||
@@ -141,30 +171,27 @@ interface NavTabItem {
|
|||||||
|
|
||||||
const tabCurrentIndex = ref(0)
|
const tabCurrentIndex = ref(0)
|
||||||
const navList = reactive<NavTabItem[]>([
|
const navList = reactive<NavTabItem[]>([
|
||||||
{ state: 0, text: '全部', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
{ state: 0, text: '全部', tag: 'ALL', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
||||||
{ state: 1, text: '待付款', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
{ state: 1, text: '待支付', tag: 'WAIT_PAY', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
||||||
{ state: 2, text: '待发货', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
{ state: 2, text: '待发货', tag: 'WAIT_SHIP', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
||||||
{ state: 3, text: '待收货', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
{ state: 3, text: '待收货/使用', tag: 'WAIT_USE', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
||||||
{ state: 4, text: '已完成', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
{ state: 4, text: '评价', tag: 'ALL', commentStatus: 'UNFINISHED', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
||||||
{ state: 5, text: '已取消', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
{ state: 5, text: '售后', tag: 'ALL', kind: 'afterSale', loadStatus: 'more', orderList: [], pageNumber: 1 },
|
||||||
])
|
])
|
||||||
|
|
||||||
const status = ref<number>(0)
|
const status = ref<number>(0)
|
||||||
const params = reactive({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
tag: 'ALL',
|
|
||||||
})
|
|
||||||
|
|
||||||
const orderStatus = [
|
const afterSaleStatusText: Record<string, string> = {
|
||||||
{ orderStatus: 'ALL' },
|
APPLY: '申请中',
|
||||||
{ orderStatus: 'WAIT_PAY' },
|
PASS: '已通过',
|
||||||
{ orderStatus: 'WAIT_SHIP' },
|
REFUSE: '已拒绝',
|
||||||
{ orderStatus: 'WAIT_ROG' },
|
BUYER_RETURN: '待卖家收货',
|
||||||
{ orderStatus: 'COMPLETE' },
|
SELLER_CONFIRM: '卖家确认收货',
|
||||||
{ orderStatus: 'CANCELLED' },
|
SELLER_TERMINATION: '卖家终止售后',
|
||||||
{ orderStatus: 'STAY_PICKED_UP' },
|
BUYER_CANCEL: '买家取消售后',
|
||||||
]
|
WAIT_REFUND: '等待平台退款',
|
||||||
|
COMPLETE: '完成',
|
||||||
|
}
|
||||||
|
|
||||||
const cancelShow = ref(false)
|
const cancelShow = ref(false)
|
||||||
const orderSn = ref('')
|
const orderSn = ref('')
|
||||||
@@ -207,21 +234,15 @@ onLoad((options) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(tabCurrentIndex, (val) => {
|
watch(tabCurrentIndex, (val) => {
|
||||||
params.tag = orderStatus[val].orderStatus
|
|
||||||
navList.forEach((res) => {
|
navList.forEach((res) => {
|
||||||
res.pageNumber = 1
|
res.pageNumber = 1
|
||||||
res.loadStatus = 'more'
|
res.loadStatus = 'more'
|
||||||
res.orderList = []
|
res.orderList = []
|
||||||
|
res.loaded = false
|
||||||
})
|
})
|
||||||
loadData(val)
|
loadData(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
function applyService(order: any) {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/order/afterSales/afterSales?orderSn=${order.sn}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateToStore(val: any) {
|
function navigateToStore(val: any) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/product/shopPage?id=' + val.storeId,
|
url: '/pages/product/shopPage?id=' + val.storeId,
|
||||||
@@ -285,6 +306,7 @@ function initData(index: number) {
|
|||||||
navList[index].pageNumber = 1
|
navList[index].pageNumber = 1
|
||||||
navList[index].loadStatus = 'more'
|
navList[index].loadStatus = 'more'
|
||||||
navList[index].orderList = []
|
navList[index].orderList = []
|
||||||
|
navList[index].loaded = false
|
||||||
loadData(index)
|
loadData(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,26 +323,99 @@ function pay(val: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadData(index: number) {
|
function loadData(index: number) {
|
||||||
params.pageNumber = navList[index].pageNumber
|
const tab = navList[index]
|
||||||
getOrderList(params).then((res) => {
|
if (!tab || tab.loadStatus === 'noMore') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const pageNumber = tab.pageNumber
|
||||||
|
const request = tab.kind === 'afterSale'
|
||||||
|
? getAfterSaleList({
|
||||||
|
pageNumber,
|
||||||
|
pageSize: 10,
|
||||||
|
sort: 'createTime',
|
||||||
|
order: 'desc',
|
||||||
|
})
|
||||||
|
: getOrderList({
|
||||||
|
pageNumber,
|
||||||
|
pageSize: 10,
|
||||||
|
tag: tab.tag,
|
||||||
|
...(tab.commentStatus ? { commentStatus: tab.commentStatus } : {}),
|
||||||
|
})
|
||||||
|
request.then((res) => {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
|
tab.loaded = true
|
||||||
if (!res.data.success) {
|
if (!res.data.success) {
|
||||||
navList[index].loadStatus = 'noMore'
|
tab.loadStatus = 'noMore'
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const records = res.data.result.records
|
let records = res.data.result.records || []
|
||||||
if (records.length == 0) {
|
if (tab.kind === 'afterSale') {
|
||||||
navList[index].loadStatus = 'noMore'
|
records = records.map(mapAfterSaleOrder)
|
||||||
} else if (records.length < 10) {
|
}
|
||||||
navList[index].loadStatus = 'noMore'
|
if (records.length < 10) {
|
||||||
|
tab.loadStatus = 'noMore'
|
||||||
}
|
}
|
||||||
if (records.length > 0) {
|
if (records.length > 0) {
|
||||||
navList[index].orderList = navList[index].orderList.concat(records)
|
tab.orderList = tab.orderList.concat(records)
|
||||||
navList[index].pageNumber += 1
|
tab.pageNumber += 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const afterSaleTypeTextMap: Record<string, string> = {
|
||||||
|
RETURN_MONEY: '仅退款',
|
||||||
|
RETURN_GOODS: '退货退款',
|
||||||
|
EXCHANGE_GOODS: '换货',
|
||||||
|
SUPPLY_AGAIN_GOODS: '补发商品',
|
||||||
|
CANCEL: '取消订单',
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterSaleTypeText(type: string) {
|
||||||
|
return afterSaleTypeTextMap[type] || '售后'
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapAfterSaleOrder(item: any) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isAfterSale: true,
|
||||||
|
serviceSn: item.sn,
|
||||||
|
sn: item.orderSn,
|
||||||
|
orderStatus: item.serviceStatus,
|
||||||
|
groupName: item.goodsName,
|
||||||
|
groupNum: item.num,
|
||||||
|
groupImages: item.goodsImage,
|
||||||
|
applyRefundPrice: item.applyRefundPrice,
|
||||||
|
orderItems: [
|
||||||
|
{
|
||||||
|
image: item.goodsImage,
|
||||||
|
name: item.goodsName,
|
||||||
|
num: item.num,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allowOperationVO: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderCardStatus(order: any) {
|
||||||
|
if (order?.isAfterSale) {
|
||||||
|
return afterSaleStatusText[order.serviceStatus] || '售后'
|
||||||
|
}
|
||||||
|
if (isAppointmentOrder(order?.orderType)) {
|
||||||
|
return appointmentOrderStatusText(order.orderStatus)
|
||||||
|
}
|
||||||
|
return orderStatusList(order.orderStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrder(order: any) {
|
||||||
|
if (order?.isAfterSale) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: './afterSales/applyDetail?sn=' + order.serviceSn,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigateToOrderDetail(order.sn)
|
||||||
|
}
|
||||||
|
|
||||||
function changeTab(e: any) {
|
function changeTab(e: any) {
|
||||||
tabCurrentIndex.value = e.target.current
|
tabCurrentIndex.value = e.target.current
|
||||||
}
|
}
|
||||||
@@ -371,6 +466,13 @@ 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 navigateToOrderDetail(sn: string) {
|
function navigateToOrderDetail(sn: string) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: './orderDetail?sn=' + sn,
|
url: './orderDetail?sn=' + sn,
|
||||||
@@ -456,23 +558,31 @@ function reBuy(order: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
display: flex;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 5px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.06);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.navbar-inner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
flex: 1;
|
flex: none;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 0 24rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: $font-color-light;
|
color: $font-color-light;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.current {
|
&.current {
|
||||||
color: $main-color;
|
color: $main-color;
|
||||||
@@ -525,6 +635,12 @@ function reBuy(order: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.after-sale-sn {
|
||||||
|
padding: 0 32rpx 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.goods-item-view {
|
.goods-item-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<view>
|
<view :style="themeStyle">
|
||||||
<!-- 订单状态 -->
|
<!-- 订单状态 -->
|
||||||
<div class="info-view order-view">
|
<div class="info-view order-view">
|
||||||
<div class="order-status" v-if="orderStatusMap[order.orderStatus]">
|
<div class="order-status" v-if="displayOrderStatus">
|
||||||
<view class="order-status-title">{{ orderStatusMap[order.orderStatus].title }}</view>
|
<view class="order-status-title">{{ displayOrderStatus.title }}</view>
|
||||||
<view class="order-status-desc">{{ orderStatusMap[order.orderStatus].value }}</view>
|
<view class="order-status-desc" v-if="displayOrderStatus.value">{{ displayOrderStatus.value }}</view>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 物流/配送信息 / 卡密入口 -->
|
<!-- 物流/配送信息 / 卡密入口 -->
|
||||||
<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 class="value">{{ appointmentSlotText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="appointment-entry__row" v-if="appointmentDetail.serviceMode">
|
||||||
|
<text class="label">服务方式</text>
|
||||||
|
<text class="value">{{ serviceModeLabel }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="appointment-entry__row" v-if="appointmentDetail.serviceAddressSnapshot">
|
||||||
|
<text class="label">服务地址</text>
|
||||||
|
<text class="value">{{ appointmentDetail.serviceAddressSnapshot }}</text>
|
||||||
|
</view>
|
||||||
|
<template v-for="(row, index) in appointmentFormRows" :key="index">
|
||||||
|
<view v-if="row.type === 'group'" class="appointment-group">{{ row.label }}</view>
|
||||||
|
<view v-else class="appointment-entry__row">
|
||||||
|
<text class="label">{{ row.label }}</text>
|
||||||
|
<text class="value">{{ row.value }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<verification-codes :items="verificationCodeItems" />
|
||||||
|
</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>
|
||||||
@@ -22,9 +44,7 @@
|
|||||||
{{ ecouponPendingMessage || '卡密尚未发放,请稍后刷新' }}
|
{{ ecouponPendingMessage || '卡密尚未发放,请稍后刷新' }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="verificationCode" v-else-if="order.verificationCode">
|
<verification-codes v-else-if="verificationCodeItems.length" :items="verificationCodeItems" />
|
||||||
券码: {{ order.orderStatus == 'CANCELLED' ? '已失效' : order.verificationCode }}
|
|
||||||
</view>
|
|
||||||
<view
|
<view
|
||||||
v-else-if="allowOperation.showLogistics"
|
v-else-if="allowOperation.showLogistics"
|
||||||
@click="handleClickDeliver()"
|
@click="handleClickDeliver()"
|
||||||
@@ -76,8 +96,7 @@
|
|||||||
<view class="seller-info u-flex u-row-between">
|
<view class="seller-info u-flex u-row-between">
|
||||||
<view class="seller-name" @click="goToShopPage(order)">
|
<view class="seller-name" @click="goToShopPage(order)">
|
||||||
<view class="name">{{ order.storeName }}</view>
|
<view class="name">{{ order.storeName }}</view>
|
||||||
<view class="status" v-if="orderStatusMap[order.orderStatus]"> {{ orderStatusMap[order.orderStatus].title
|
<view class="status" v-if="displayOrderStatus"> {{ displayOrderStatus.title }}</view>
|
||||||
}}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="order-sn"></view>
|
<view class="order-sn"></view>
|
||||||
</view>
|
</view>
|
||||||
@@ -87,18 +106,23 @@
|
|||||||
<view class="goods-img" @click="gotoGoodsDetail(sku)">
|
<view class="goods-img" @click="gotoGoodsDetail(sku)">
|
||||||
<u-image border-radius="6" width="131rpx" height="131rpx" :src="sku.image"></u-image>
|
<u-image border-radius="6" width="131rpx" height="131rpx" :src="sku.image"></u-image>
|
||||||
</view>
|
</view>
|
||||||
<view class="goods-info" @click="gotoGoodsDetail(sku)">
|
<view class="goods-body">
|
||||||
<view class="goods-title u-line-2">{{ sku.goodsName }}</view>
|
<view class="goods-info" @click="gotoGoodsDetail(sku)">
|
||||||
<view class="goods-price">
|
<view class="goods-title u-line-2">{{ sku.goodsName }}</view>
|
||||||
¥{{unitPrice(sku.goodsPrice) }}
|
<view class="goods-price">
|
||||||
<!-- <span v-if="sku.point">+{{ sku.point }}积分</span> -->
|
¥{{unitPrice(sku.goodsPrice) }}
|
||||||
<span style="font-size: 24rpx;margin-left: 14rpx;color: #ff9900;" v-if="!isECouponOrder && sku.isRefund && sku.isRefund !== 'NO_REFUND'">
|
<!-- <span v-if="sku.point">+{{ sku.point }}积分</span> -->
|
||||||
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
|
<span style="font-size: 24rpx;margin-left: 14rpx;color: #ff9900;" v-if="!isECouponOrder && !isAppointmentOrderFlag && sku.isRefund && sku.isRefund !== 'NO_REFUND'">
|
||||||
</span>
|
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
|
||||||
|
</span>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="goods-side" :class="{ 'has-action': canShowSkuAfterSale(sku) }">
|
||||||
|
<view class="goods-num">
|
||||||
|
<view>x{{ sku.num }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="after-sale-btn" v-if="canShowSkuAfterSale(sku)" @click.stop="onSkuAfterSale(sku)">退款/售后</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
<view class="goods-num">
|
|
||||||
<view>x{{ sku.num }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -112,7 +136,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 +166,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'"
|
||||||
@@ -158,8 +182,8 @@
|
|||||||
<view class="title">订单编号:</view>
|
<view class="title">订单编号:</view>
|
||||||
<view class="value order-sn-value">
|
<view class="value order-sn-value">
|
||||||
<text class="order-sn-text">{{ order.sn }}</text>
|
<text class="order-sn-text">{{ order.sn }}</text>
|
||||||
<view class="copy" @click.stop="onCopy(order.sn)">
|
<view class="copy" @tap.stop="onCopy(order.sn)">
|
||||||
<u-tag text="复制" type="info" plain size="mini" />
|
<view class="copy-icon"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -264,10 +288,12 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
import { getExpress, getPackage } from '@/api/trade.js'
|
import { getExpress, getPackage } from '@/api/trade.js'
|
||||||
import { cancelOrder, confirmReceipt, getOrderDetail } from '@/api/order.js'
|
import { cancelOrder, confirmReceipt, getOrderDetail } from '@/api/order.js'
|
||||||
import shares from '@/components/m-share/index'
|
import shares from '@/components/m-share/index'
|
||||||
import { getClearReason } from '@/api/after-sale.js'
|
import { getClearReason } from '@/api/after-sale.js'
|
||||||
|
import storage from '@/utils/storage'
|
||||||
import {
|
import {
|
||||||
unitPrice,
|
unitPrice,
|
||||||
secrecyMobile,
|
secrecyMobile,
|
||||||
@@ -282,14 +308,26 @@ 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 VerificationCodes from './components/verificationCodes.vue'
|
||||||
|
import {
|
||||||
|
SERVICE_MODE_LABEL,
|
||||||
|
appointmentOrderStatusText,
|
||||||
|
getAppointmentAllowOperation,
|
||||||
|
formatDate,
|
||||||
|
parseFormAnswerGroups,
|
||||||
|
formatFormAnswerValue,
|
||||||
|
} from '@/utils/appointmentGoods.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
const lightColor = computed(() => store.getters.lightColor)
|
const lightColor = computed(() => store.getters.lightColor)
|
||||||
const mainColor = computed(() => store.getters.mainColor)
|
const mainColor = computed(() => store.getters.mainColor)
|
||||||
|
|
||||||
@@ -318,8 +356,70 @@ const reason = ref('')
|
|||||||
const orderPackage = ref<any[]>([])
|
const orderPackage = ref<any[]>([])
|
||||||
const legacyTraces = ref<any>(null)
|
const legacyTraces = ref<any>(null)
|
||||||
|
|
||||||
|
const displayOrderStatus = computed(() => {
|
||||||
|
if (isAppointmentOrderFlag.value) {
|
||||||
|
const status = order.value?.orderStatus
|
||||||
|
const title = appointmentOrderStatusText(status)
|
||||||
|
if (status === 'UNPAID') return { title, value: '请尽快完成支付' }
|
||||||
|
if (status === 'CANCELLED') return { title, value: '订单已取消' }
|
||||||
|
if (status === 'COMPLETED' || status === 'COMPLETE') return { title, value: '服务已完成' }
|
||||||
|
const onSite = appointmentDetail.value?.serviceMode === 'ON_SITE'
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
value: onSite ? '请等待上门服务' : '请按时到店使用',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orderStatusMap[order.value?.orderStatus]
|
||||||
|
})
|
||||||
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 }
|
||||||
|
)
|
||||||
|
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 appointmentFormRows = computed(() => {
|
||||||
|
const groups = appointmentFormGroups.value
|
||||||
|
const rows = []
|
||||||
|
if (groups.length) {
|
||||||
|
groups.forEach((group, index) => {
|
||||||
|
if (groups.length > 1) {
|
||||||
|
rows.push({ type: 'group', label: `第${index + 1}位` })
|
||||||
|
}
|
||||||
|
group.forEach((field) => {
|
||||||
|
const value = formatFormAnswerValue(field.value)
|
||||||
|
if (!value) return
|
||||||
|
rows.push({ label: field.fieldLabel || '补充信息', value })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
if (appointmentDetail.value?.contactName) {
|
||||||
|
rows.push({ label: '联系人', value: appointmentDetail.value.contactName })
|
||||||
|
}
|
||||||
|
if (appointmentDetail.value?.contactPhone) {
|
||||||
|
rows.push({ label: '联系方式', value: appointmentDetail.value.contactPhone })
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
})
|
||||||
const ecouponCardKeyDelivered = computed(() =>
|
const ecouponCardKeyDelivered = computed(() =>
|
||||||
orderHasCardKeyContent(orderGoodsList.value)
|
orderHasCardKeyContent(orderGoodsList.value)
|
||||||
)
|
)
|
||||||
@@ -335,14 +435,45 @@ 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.verificationCode) 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
|
||||||
if (order.value.verificationCode) return true
|
|
||||||
if (order.value.deliveryMethod === 'LOGISTICS') return true
|
if (order.value.deliveryMethod === 'LOGISTICS') return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const verificationCodeItems = computed(() => {
|
||||||
|
if (isAppointmentOrderFlag.value) {
|
||||||
|
const cancelled = order.value.orderStatus === 'CANCELLED'
|
||||||
|
let qrAssigned = false
|
||||||
|
return appointmentCodes.value.map((code, index) => {
|
||||||
|
const pending = !cancelled && code.status === 'UNUSED'
|
||||||
|
const showQr = pending && !qrAssigned
|
||||||
|
if (showQr) qrAssigned = true
|
||||||
|
return {
|
||||||
|
key: code.id || code.code || String(index),
|
||||||
|
canvasId: `verify-code-${index}`,
|
||||||
|
code: code.code,
|
||||||
|
struck: !pending,
|
||||||
|
showQr,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const code = order.value.verificationCode
|
||||||
|
if (!code || isECouponOrder.value) return []
|
||||||
|
const status = order.value.orderStatus
|
||||||
|
const pending = status !== 'CANCELLED' && status !== 'COMPLETED'
|
||||||
|
return [{
|
||||||
|
key: code,
|
||||||
|
canvasId: 'verify-code-0',
|
||||||
|
code,
|
||||||
|
struck: !pending,
|
||||||
|
showQr: pending,
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
const deliveryEntryText = computed(() =>
|
const deliveryEntryText = computed(() =>
|
||||||
getDeliveryEntryText(
|
getDeliveryEntryText(
|
||||||
orderPackage.value,
|
orderPackage.value,
|
||||||
@@ -444,6 +575,19 @@ function ByUserMessage(orderItem: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 +598,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()
|
||||||
})
|
})
|
||||||
@@ -479,6 +624,35 @@ function onCopy(orderSnText: string) {
|
|||||||
setClipboard(String(orderSnText))
|
setClipboard(String(orderSnText))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canApplySkuAfterSale(sku: any) {
|
||||||
|
const status = String(sku?.afterSaleStatus || '')
|
||||||
|
if (status.includes('NOT_APPLIED') || status.includes('PART_AFTER_SALE')) return true
|
||||||
|
if (isAppointmentOrderFlag.value && (!status || status === 'NEW')) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function canShowSkuAfterSale(sku: any) {
|
||||||
|
if (isECouponOrder.value) return false
|
||||||
|
if (!order.value?.sn || Number(order.value.flowPrice) === 0) return false
|
||||||
|
if (isAppointmentOrderFlag.value && !appointmentAllow.value.afterSale) return false
|
||||||
|
return canApplySkuAfterSale(sku)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSkuAfterSale(sku: any) {
|
||||||
|
if (!canShowSkuAfterSale(sku)) return
|
||||||
|
storage.setAfterSaleData({
|
||||||
|
...order.value,
|
||||||
|
...sku,
|
||||||
|
orderItems: orderGoodsList.value || [],
|
||||||
|
storeName: order.value.storeName,
|
||||||
|
flowPrice: order.value.flowPrice,
|
||||||
|
orderType: order.value.orderType,
|
||||||
|
})
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `./afterSales/afterSalesSelect?sn=${sku.sn}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function onAfterSales(orderSnText: string, sku: any) {
|
function onAfterSales(orderSnText: string, sku: any) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `./afterSales/afterSalesSelect?sn=${orderSnText}&sku=${encodeURIComponent(
|
url: `./afterSales/afterSalesSelect?sn=${orderSnText}&sku=${encodeURIComponent(
|
||||||
@@ -571,13 +745,56 @@ function reBuy(orderItem: any) {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "./goods.scss";
|
@import "./goods.scss";
|
||||||
|
|
||||||
.goods-item-view {
|
.seller-view .goods-item-view {
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.goods-body {
|
||||||
|
flex: 4;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.goods-info {
|
.goods-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.goods-title {
|
.goods-title {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.goods-side {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: stretch;
|
||||||
|
min-height: 131rpx;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-side.has-action {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-num {
|
||||||
|
flex: none;
|
||||||
|
width: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-sale-btn {
|
||||||
|
height: 44rpx;
|
||||||
|
line-height: 44rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
@@ -752,6 +969,38 @@ page,
|
|||||||
.copy {
|
.copy {
|
||||||
margin-left: 16rpx;
|
margin-left: 16rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
position: relative;
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon::before,
|
||||||
|
.copy-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border: 2rpx solid #666;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon::before {
|
||||||
|
left: 0;
|
||||||
|
top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon::after {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,6 +1023,31 @@ page,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appointment-entry__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.appointment-entry__row .label {
|
||||||
|
color: #999;
|
||||||
|
width: 140rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.appointment-entry__row .value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.appointment-group {
|
||||||
|
margin: 8rpx 0 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--theme-primary, #ff3c2a);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.verificationCode {
|
.verificationCode {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 2rpx;
|
letter-spacing: 2rpx;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-page">
|
<div class="main-page" :style="themeStyle">
|
||||||
<!-- #ifdef APP-PLUS -->
|
<!-- #ifdef APP-PLUS -->
|
||||||
<view class="status_bar"></view>
|
<view class="status_bar"></view>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
@@ -13,8 +13,6 @@
|
|||||||
routerVal.goodsId
|
routerVal.goodsId
|
||||||
" :thumbnail="goodsDetail.thumbnail" :goodsName="goodsDetail.goodsName" type="goods"
|
" :thumbnail="goodsDetail.thumbnail" :goodsName="goodsDetail.goodsName" type="goods"
|
||||||
@close="enableShare = false" />
|
@close="enableShare = false" />
|
||||||
<popups v-model="popupsSwitch" @tapPopup="handleNavbarList" :popData="navbarListData" :x="navbarListX"
|
|
||||||
:y="navbarListY" placement="top-start" />
|
|
||||||
<view class="index">
|
<view class="index">
|
||||||
<!-- topBar -->
|
<!-- topBar -->
|
||||||
<u-navbar
|
<u-navbar
|
||||||
@@ -25,17 +23,8 @@
|
|||||||
:bg-color="navbar.background"
|
:bg-color="navbar.background"
|
||||||
:auto-back="false"
|
:auto-back="false"
|
||||||
:class="headerFlag ? 'header' : 'header bg-none scroll-hide'"
|
:class="headerFlag ? 'header' : 'header bg-none scroll-hide'"
|
||||||
|
@leftClick="back"
|
||||||
>
|
>
|
||||||
<template #left>
|
|
||||||
<view class="backs" @tap.stop>
|
|
||||||
<view class="nav-action" @tap.stop="back()">
|
|
||||||
<u-icon name="arrow-left" class="icon-back"></u-icon>
|
|
||||||
</view>
|
|
||||||
<view class="nav-action" @tap.stop="popupsSwitch = !popupsSwitch">
|
|
||||||
<u-icon name="list" class="icon-list"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template #center>
|
<template #center>
|
||||||
<view class="headerList" :class="headerFlag ? 'tab-bar' : 'tab-bar scroll-hide'">
|
<view class="headerList" :class="headerFlag ? 'tab-bar' : 'tab-bar scroll-hide'">
|
||||||
<view class="headerRow headerRow--tabs">
|
<view class="headerRow headerRow--tabs">
|
||||||
@@ -62,17 +51,8 @@
|
|||||||
class="header-only-back"
|
class="header-only-back"
|
||||||
:bg-color="navbarOnlyBack.background"
|
:bg-color="navbarOnlyBack.background"
|
||||||
:auto-back="false"
|
:auto-back="false"
|
||||||
|
@leftClick="back"
|
||||||
>
|
>
|
||||||
<template #left>
|
|
||||||
<view class="bg-back" @tap.stop>
|
|
||||||
<view class="nav-action" @tap.stop="back()">
|
|
||||||
<u-icon size="20" name="arrow-left" class="icon-back"></u-icon>
|
|
||||||
</view>
|
|
||||||
<view class="nav-action" @tap.stop="popupsSwitch = !popupsSwitch">
|
|
||||||
<u-icon size="20" name="list" class="icon-list"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</u-navbar>
|
</u-navbar>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -262,7 +242,13 @@
|
|||||||
<view class="detail-btn" v-if="!isGroup && !takeDownFromSale">
|
<view class="detail-btn" v-if="!isGroup && !takeDownFromSale">
|
||||||
<view class="to-store-car to-store-btn" v-if="showAddToCart" @click="shutMask(4)">
|
<view class="to-store-car to-store-btn" v-if="showAddToCart" @click="shutMask(4)">
|
||||||
加入购物车</view>
|
加入购物车</view>
|
||||||
<view class="to-buy to-store-btn" @click="shutMask(4, 'buy')">立即购买</view>
|
<view
|
||||||
|
class="to-buy to-store-btn"
|
||||||
|
:class="{ 'to-buy-theme': isAppointmentGoodsType }"
|
||||||
|
@click="handleBuyClick"
|
||||||
|
>
|
||||||
|
{{ isAppointmentGoodsType ? '立即预约' : '立即购买' }}
|
||||||
|
</view>
|
||||||
<view class="to-store-car to-store-btn" v-if="startTimer">暂未开始</view>
|
<view class="to-store-car to-store-btn" v-if="startTimer">暂未开始</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 拼团结算 -->
|
<!-- 拼团结算 -->
|
||||||
@@ -325,13 +311,13 @@ import * as API_trade from '@/api/trade.js'
|
|||||||
import * as API_Members from '@/api/members.js'
|
import * as API_Members from '@/api/members.js'
|
||||||
import * as API_store from '@/api/store.js'
|
import * as API_store from '@/api/store.js'
|
||||||
import { getIMDetail } from '@/api/common'
|
import { getIMDetail } from '@/api/common'
|
||||||
import { modelNavigateTo } from '@/pages/tabbar/home/template/tpl.js'
|
|
||||||
/************请求存储***************/
|
/************请求存储***************/
|
||||||
import storage from '@/utils/storage.js'
|
import storage from '@/utils/storage.js'
|
||||||
/************工具函数***************/
|
/************工具函数***************/
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
import { unitPrice, goodsFormatPrice, isLogin, parseGoodsImageUrl, clearStrComma, talkIm } from '@/utils/filters.js'
|
import { unitPrice, goodsFormatPrice, isLogin, parseGoodsImageUrl, clearStrComma, talkIm } from '@/utils/filters.js'
|
||||||
import { isNonPhysicalGoods } from '@/utils/goodsType.js'
|
import { isNonPhysicalGoods, isAppointmentGoods } from '@/utils/goodsType.js'
|
||||||
|
|
||||||
/************组件***************/
|
/************组件***************/
|
||||||
import PromotionLayout from './product/promotion/-promotion'
|
import PromotionLayout from './product/promotion/-promotion'
|
||||||
@@ -347,36 +333,17 @@ import GoodsSwiper from './product/goods/-goods-swiper'
|
|||||||
import popupGoods from '@/components/m-buy/goods'
|
import popupGoods from '@/components/m-buy/goods'
|
||||||
import popupAddress from './product/popup/address'
|
import popupAddress from './product/popup/address'
|
||||||
import shares from '@/components/m-share/index'
|
import shares from '@/components/m-share/index'
|
||||||
import popups from '@/components/popups/popups'
|
|
||||||
import takeDownFormSaleGoods from '@/components/m-take-down-sale-goods/index'
|
import takeDownFormSaleGoods from '@/components/m-take-down-sale-goods/index'
|
||||||
import setup from './product/popup/popup'
|
import setup from './product/popup/popup'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
const instance = getCurrentInstance()
|
const instance = getCurrentInstance()
|
||||||
const popupGoodsRef = ref()
|
const popupGoodsRef = ref()
|
||||||
const takeDownSaleRef = ref()
|
const takeDownSaleRef = ref()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const promotionShow = ref(false)
|
const promotionShow = ref(false)
|
||||||
let navbarListX: any
|
|
||||||
let navbarListY: any
|
|
||||||
// #ifdef H5
|
|
||||||
navbarListX = ref(110)
|
|
||||||
navbarListY = ref(80)
|
|
||||||
// #endif
|
|
||||||
// #ifdef MP-WEIXIN || APP-PLUS
|
|
||||||
// #ifndef H5
|
|
||||||
navbarListX = ref(120)
|
|
||||||
navbarListY = ref(170)
|
|
||||||
// #endif
|
|
||||||
// #endif
|
|
||||||
const navbarListData = [
|
|
||||||
{ title: "首页", icon: "home-fill", ___type: "other" },
|
|
||||||
{ title: "购物车", icon: "bag-fill", ___type: "other" },
|
|
||||||
{ title: "搜索", icon: "search", ___type: "category" },
|
|
||||||
{ title: "个人中心", icon: "account-fill", ___type: "other" },
|
|
||||||
]
|
|
||||||
const popupsSwitch = ref(false)
|
|
||||||
const enableShare = ref(false)
|
const enableShare = ref(false)
|
||||||
const selectedGoods = ref<any>("")
|
const selectedGoods = ref<any>("")
|
||||||
const isGroup = ref(false)
|
const isGroup = ref(false)
|
||||||
@@ -437,6 +404,7 @@ const isNonPhysicalGoodsType = computed(() => isNonPhysicalGoods(goodsDetail.val
|
|||||||
const showAddToCart = computed(() => {
|
const showAddToCart = computed(() => {
|
||||||
return !isNonPhysicalGoodsType.value
|
return !isNonPhysicalGoodsType.value
|
||||||
})
|
})
|
||||||
|
const isAppointmentGoodsType = computed(() => isAppointmentGoods(goodsDetail.value?.goodsType))
|
||||||
|
|
||||||
// watch
|
// watch
|
||||||
watch(isGroup, (val) => {
|
watch(isGroup, (val) => {
|
||||||
@@ -494,10 +462,6 @@ function share() {
|
|||||||
return `/pages/product/goods?id=${routerVal.value.id}&goodsId=${routerVal.value.goodsId}`
|
return `/pages/product/goods?id=${routerVal.value.id}&goodsId=${routerVal.value.goodsId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNavbarList(val: any) {
|
|
||||||
modelNavigateTo({ url: val })
|
|
||||||
}
|
|
||||||
|
|
||||||
function emptyPromotion(): boolean {
|
function emptyPromotion(): boolean {
|
||||||
if (PromotionList.value == "" || PromotionList.value == null || PromotionList.value == []) {
|
if (PromotionList.value == "" || PromotionList.value == null || PromotionList.value == []) {
|
||||||
return true
|
return true
|
||||||
@@ -637,13 +601,11 @@ function cartCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
if (getCurrentPages().length == 1) {
|
uni.navigateBack({
|
||||||
uni.switchTab({
|
fail() {
|
||||||
url: "/pages/tabbar/home/index",
|
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||||
})
|
},
|
||||||
} else {
|
})
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoreBaseInfoFun(id: any) {
|
function getStoreBaseInfoFun(id: any) {
|
||||||
@@ -724,6 +686,26 @@ function getCoupon(item: any) {
|
|||||||
receiveCouponsFun(item.id)
|
receiveCouponsFun(item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goAppointmentCheckout(num = 1) {
|
||||||
|
const sku = goodsDetail.value?.id
|
||||||
|
const gid = goodsDetail.value?.goodsId || routerVal.value.goodsId
|
||||||
|
if (!sku || !gid) {
|
||||||
|
uni.showToast({ title: '商品信息异常', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/order/appointment/appointmentCheckout?skuId=${sku}&goodsId=${gid}&num=${num}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBuyClick() {
|
||||||
|
if (isAppointmentGoodsType.value) {
|
||||||
|
goAppointmentCheckout()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shutMask(4, 'buy')
|
||||||
|
}
|
||||||
|
|
||||||
function shutMask(flag: any, buyFlag?: any, type?: any) {
|
function shutMask(flag: any, buyFlag?: any, type?: any) {
|
||||||
promotionShow.value = false
|
promotionShow.value = false
|
||||||
buyMask.value = false
|
buyMask.value = false
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ page {
|
|||||||
> .to-buy {
|
> .to-buy {
|
||||||
background-image: linear-gradient(135deg, #ffba0d, #ffc30d 69%, #ffcf0d);
|
background-image: linear-gradient(135deg, #ffba0d, #ffc30d 69%, #ffcf0d);
|
||||||
}
|
}
|
||||||
|
> .to-buy-theme {
|
||||||
|
background-image: none;
|
||||||
|
background: var(--theme-primary, #ff3c2a);
|
||||||
|
}
|
||||||
> .to-store-car {
|
> .to-store-car {
|
||||||
background-image: linear-gradient(135deg, #f2140c, #f2270c 70%, #f24d0c);
|
background-image: linear-gradient(135deg, #f2140c, #f2270c 70%, #f24d0c);
|
||||||
}
|
}
|
||||||
@@ -201,7 +205,7 @@ page {
|
|||||||
|
|
||||||
.header-only-back {
|
.header-only-back {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
z-index: 9;
|
z-index: 12 !important;
|
||||||
}
|
}
|
||||||
.header,
|
.header,
|
||||||
.header-only-back {
|
.header-only-back {
|
||||||
@@ -332,26 +336,10 @@ page {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-action + .nav-action {
|
|
||||||
border-left: 2rpx solid rgb(194, 194, 194);
|
|
||||||
}
|
|
||||||
.backs,
|
.backs,
|
||||||
.bg-back {
|
.bg-back {
|
||||||
width: 150rpx;
|
|
||||||
border: 2rpx solid #e8e8e8;
|
|
||||||
border-radius: 100px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
|
||||||
padding: 8rpx 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.bg-back {
|
|
||||||
background: rgba($color: #fff, $alpha: 0.8);
|
|
||||||
}
|
|
||||||
.backs {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 42rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerImg {
|
.headerImg {
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ import {
|
|||||||
getStoreIsCollect,
|
getStoreIsCollect,
|
||||||
} from '@/api/members.js'
|
} from '@/api/members.js'
|
||||||
import config from '@/config/config'
|
import config from '@/config/config'
|
||||||
import { getGoodsList } from '@/api/goods.js'
|
import { getGoodsList, getMpScene } from '@/api/goods.js'
|
||||||
import { getAllCoupons } from '@/api/promotions.js'
|
import { getAllCoupons } from '@/api/promotions.js'
|
||||||
import { getFloorStoreData } from '@/api/home'
|
import { getFloorStoreData } from '@/api/home'
|
||||||
import {
|
import {
|
||||||
@@ -349,6 +349,33 @@ function fallbackToBasePage() {
|
|||||||
getCategoryData()
|
getCategoryData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyStoreId(id) {
|
||||||
|
storeId.value = id || ''
|
||||||
|
goodsParams.storeId = storeId.value
|
||||||
|
couponParams.storeId = storeId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStoreIdFromParams(params) {
|
||||||
|
const text = String(params || '').trim()
|
||||||
|
if (!text) return ''
|
||||||
|
if (text.indexOf('id=') === 0) return text.slice(3)
|
||||||
|
const match = /(?:^|&)id=([^&]+)/.exec(text)
|
||||||
|
return match ? decodeURIComponent(match[1]) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveStoreIdFromScene(scene) {
|
||||||
|
const raw = decodeURIComponent(String(scene || '')).trim()
|
||||||
|
if (!raw) return ''
|
||||||
|
const direct = parseStoreIdFromParams(raw)
|
||||||
|
if (direct) return direct
|
||||||
|
try {
|
||||||
|
const res = await getMpScene(raw)
|
||||||
|
return parseStoreIdFromParams(res?.data?.result)
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStoreLicencePhoto() {
|
function getStoreLicencePhoto() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/product/licencePhoto?id=${storeId.value}`,
|
url: `/pages/product/licencePhoto?id=${storeId.value}`,
|
||||||
@@ -505,11 +532,15 @@ function getCoupon(item: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onLoad((options: any) => {
|
onLoad((options: any) => {
|
||||||
storeId.value = options.id
|
applyStoreId(options?.id)
|
||||||
console.log(storeId.value, 'this.storeId')
|
if (!storeId.value && options?.scene) {
|
||||||
|
resolveStoreIdFromScene(options.scene).then((id) => {
|
||||||
goodsParams.storeId = options.id
|
if (!storeId.value && id) {
|
||||||
couponParams.storeId = options.id
|
applyStoreId(id)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onPageScroll((e) => {
|
onPageScroll((e) => {
|
||||||
@@ -531,7 +562,7 @@ onMounted(() => {
|
|||||||
withShareTicket: true,
|
withShareTicket: true,
|
||||||
})
|
})
|
||||||
// #endif
|
// #endif
|
||||||
init()
|
if (storeId.value) init()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -32,37 +32,46 @@
|
|||||||
<view class="point-value">{{ footNum || 0 }}</view>
|
<view class="point-value">{{ footNum || 0 }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 我的订单,代付款 -->
|
<!-- 我的订单 -->
|
||||||
<view class="order">
|
<view class="order">
|
||||||
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=1')">
|
<view class="order-head">
|
||||||
<view class="bag bag2">
|
<text class="order-head-title">我的订单</text>
|
||||||
<u-icon name="bag-fill" size="22" color="#fff"></u-icon>
|
<view class="order-head-all" @click="navigateTo('/pages/order/myOrder?status=0')">
|
||||||
|
<text>全部</text>
|
||||||
|
<u-icon name="arrow-right" size="12" color="#999"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-item-label">待付款</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=3')">
|
<view class="order-row">
|
||||||
<view class="bag bag3">
|
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=1')">
|
||||||
<u-icon name="car-fill" size="22" color="#fff"></u-icon>
|
<view class="bag bag2">
|
||||||
|
<u-icon name="bag-fill" size="22" color="#fff"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="order-item-label">待支付</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-item-label">待收货</view>
|
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=2')">
|
||||||
</view>
|
<view class="bag bag1">
|
||||||
<view class="order-item" @click="navigateTo('/pages/order/evaluate/myEvaluate')">
|
<u-icon name="gift-fill" size="22" color="#fff"></u-icon>
|
||||||
<view class="bag bag4">
|
</view>
|
||||||
<u-icon name="star-fill" size="22" color="#fff"></u-icon>
|
<view class="order-item-label">待发货</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-item-label">待评价</view>
|
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=3')">
|
||||||
</view>
|
<view class="bag bag3">
|
||||||
<view class="order-item" @click="navigateTo('/pages/order/afterSales/afterSales')">
|
<u-icon name="car-fill" size="22" color="#fff"></u-icon>
|
||||||
<view class="bag bag5">
|
</view>
|
||||||
<u-icon name="server-fill" size="22" color="#fff"></u-icon>
|
<view class="order-item-label">待收货</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-item-label">售后</view>
|
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=4')">
|
||||||
</view>
|
<view class="bag bag4">
|
||||||
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=0')">
|
<u-icon name="star-fill" size="22" color="#fff"></u-icon>
|
||||||
<view class="bag bag1">
|
</view>
|
||||||
<u-icon name="order" size="22" color="#fff"></u-icon>
|
<view class="order-item-label">待评价</view>
|
||||||
|
</view>
|
||||||
|
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=5')">
|
||||||
|
<view class="bag bag5">
|
||||||
|
<u-icon name="server-fill" size="22" color="#fff"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="order-item-label">售后</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-item-label">我的订单</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,12 +266,34 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order {
|
.order {
|
||||||
height: 140rpx;
|
padding: 8rpx 16rpx 24rpx;
|
||||||
font-size: $font-sm;
|
color: #333;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
.order-head {
|
||||||
padding: 0 3%;
|
display: flex;
|
||||||
color: #999;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8rpx 12rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-head-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-head-all {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.order-item {
|
.order-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -274,9 +305,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-item-label {
|
.order-item-label {
|
||||||
margin-top: 10rpx;
|
margin-top: 12rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
font-size: $font-sm;
|
font-size: 24rpx;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
703
utils/appointmentGoods.js
Normal file
703
utils/appointmentGoods.js
Normal file
@@ -0,0 +1,703 @@
|
|||||||
|
/**
|
||||||
|
* 预约商品(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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约商品 SKU quantity 固定为 0,可售性在结算页按时段校验 */
|
||||||
|
export const APPOINTMENT_BUY_MAX_NUM = 999
|
||||||
|
|
||||||
|
export function getAppointmentStock(_sku) {
|
||||||
|
return APPOINTMENT_BUY_MAX_NUM
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppointmentMaxBuyNum(_sku) {
|
||||||
|
return APPOINTMENT_BUY_MAX_NUM
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将买家端 specs 转为规格维度列表(与 m-buy 一致) */
|
||||||
|
export function buildSpecFormatList(specs) {
|
||||||
|
if (!Array.isArray(specs) || !specs.length) return []
|
||||||
|
const arr = []
|
||||||
|
specs.forEach((item) => {
|
||||||
|
;(item.specValues || []).forEach((spec) => {
|
||||||
|
if (spec.specName === 'images') return
|
||||||
|
const values = {
|
||||||
|
value: spec.specValue,
|
||||||
|
skuId: item.skuId,
|
||||||
|
}
|
||||||
|
let found = arr.find((row) => row.name === spec.specName)
|
||||||
|
if (!found) {
|
||||||
|
found = { name: spec.specName, values: [] }
|
||||||
|
arr.push(found)
|
||||||
|
}
|
||||||
|
if (!found.values.find((v) => v.value === values.value)) {
|
||||||
|
found.values.push(values)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按当前选中规格值匹配 skuId */
|
||||||
|
export function findSkuIdBySpecSelection(specs, selectedValues) {
|
||||||
|
if (!Array.isArray(specs) || !specs.length) return ''
|
||||||
|
const matched = specs.find((item) => {
|
||||||
|
const specValues = (item.specValues || []).filter((j) => j.specName !== 'images')
|
||||||
|
if (specValues.length !== selectedValues.length) return false
|
||||||
|
return specValues.every((sv, idx) => sv.specValue === selectedValues[idx])
|
||||||
|
})
|
||||||
|
return matched?.skuId || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 specs 解析某 sku 的规格选中值 */
|
||||||
|
export function getSpecSelectionForSku(specs, skuId) {
|
||||||
|
const selected = []
|
||||||
|
if (!Array.isArray(specs) || !skuId) return selected
|
||||||
|
const item = specs.find((row) => row.skuId === skuId)
|
||||||
|
if (!item) return selected
|
||||||
|
;(item.specValues || [])
|
||||||
|
.filter((v) => v.specName !== 'images')
|
||||||
|
.forEach((v, idx) => {
|
||||||
|
selected[idx] = v.specValue
|
||||||
|
})
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
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.map(String).filter(Boolean)
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string' && raw.trim()) {
|
||||||
|
const trimmed = raw.trim()
|
||||||
|
if (trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed)
|
||||||
|
if (Array.isArray(parsed) && parsed.length) {
|
||||||
|
return parsed.map(String).filter(Boolean)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* fall through */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed.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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWeekdays(raw) {
|
||||||
|
if (raw == null || raw === '') return null
|
||||||
|
const list = Array.isArray(raw) ? raw : String(raw).split(',')
|
||||||
|
const set = new Set(
|
||||||
|
list
|
||||||
|
.map((item) => Number(String(item).trim()))
|
||||||
|
.filter((num) => num >= 1 && num <= 7)
|
||||||
|
)
|
||||||
|
return set.size ? set : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDay(value) {
|
||||||
|
if (value == null || value === '') return null
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return null
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
const text = String(value).trim()
|
||||||
|
const dateOnly = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:$|[ T]\d{2}:\d{2})/)
|
||||||
|
const hasZone = /[zZ]|[+-]\d{2}:?\d{2}$/.test(text)
|
||||||
|
if (dateOnly && !hasZone) {
|
||||||
|
return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))
|
||||||
|
}
|
||||||
|
const date = new Date(text)
|
||||||
|
if (Number.isNaN(date.getTime())) return null
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 商家星期:周一=1 … 周日=7 */
|
||||||
|
function sellerWeekday(date) {
|
||||||
|
const day = date.getDay()
|
||||||
|
return day === 0 ? 7 : day
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBookableDates(ext, days = 30) {
|
||||||
|
const rangeDays = Math.max(Number(ext?.bookingDateRangeDays) || days, 1)
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const mode = String(ext?.bookableDateMode || '').toUpperCase()
|
||||||
|
const custom = mode === 'CUSTOM'
|
||||||
|
const weekdays = custom ? parseWeekdays(ext?.customWeekdays) : null
|
||||||
|
const startLimit = custom ? parseDay(ext?.customDateStart) : null
|
||||||
|
const endLimit = custom ? parseDay(ext?.customDateEnd) : null
|
||||||
|
const list = []
|
||||||
|
for (let i = 0; i < rangeDays; i++) {
|
||||||
|
const date = new Date(today)
|
||||||
|
date.setDate(today.getDate() + i)
|
||||||
|
if (startLimit && date < startLimit) continue
|
||||||
|
if (endLimit && date > endLimit) continue
|
||||||
|
if (weekdays && !weekdays.has(sellerWeekday(date))) continue
|
||||||
|
list.push(formatDate(date))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WEEKDAY_LABELS = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
|
||||||
|
export function parseDateParts(dateStr) {
|
||||||
|
if (!dateStr) return null
|
||||||
|
const parts = String(dateStr).slice(0, 10).split('-').map(Number)
|
||||||
|
if (parts.length < 3) return null
|
||||||
|
const [y, m, d] = parts
|
||||||
|
return { y, m, d, date: new Date(y, m - 1, d) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMonthTitle(dateStr) {
|
||||||
|
const parts = parseDateParts(dateStr)
|
||||||
|
if (!parts) return ''
|
||||||
|
return `${parts.y}年${String(parts.m).padStart(2, '0')}月`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCalendarDayCell(date, todayStr, bookableSet, inMonth) {
|
||||||
|
const value = formatDate(date)
|
||||||
|
const bookable = bookableSet instanceof Set ? bookableSet.has(value) : true
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
day: date.getDate(),
|
||||||
|
isToday: value === todayStr,
|
||||||
|
inMonth,
|
||||||
|
bookable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 以 anchor 所在自然周(周日始)生成 7 天条 */
|
||||||
|
export function buildWeekStrip(anchorDateStr, bookableSet) {
|
||||||
|
const parts = parseDateParts(anchorDateStr)
|
||||||
|
if (!parts) return []
|
||||||
|
const anchor = parts.date
|
||||||
|
anchor.setHours(0, 0, 0, 0)
|
||||||
|
const todayStr = formatDate(new Date())
|
||||||
|
const start = new Date(anchor)
|
||||||
|
start.setDate(anchor.getDate() - anchor.getDay())
|
||||||
|
const days = []
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const d = new Date(start)
|
||||||
|
d.setDate(start.getDate() + i)
|
||||||
|
days.push(buildCalendarDayCell(d, todayStr, bookableSet, true))
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 月初 YYYY-MM-01 */
|
||||||
|
export function monthStartDate(dateStr) {
|
||||||
|
const parts = parseDateParts(dateStr)
|
||||||
|
if (!parts) return formatDate(new Date())
|
||||||
|
return `${parts.y}-${String(parts.m).padStart(2, '0')}-01`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整月日历(周日始,含前后月补位日期) */
|
||||||
|
export function buildMonthCalendar(anchorDateStr, bookableSet) {
|
||||||
|
const parts = parseDateParts(monthStartDate(anchorDateStr))
|
||||||
|
if (!parts) return []
|
||||||
|
const year = parts.y
|
||||||
|
const month = parts.m - 1
|
||||||
|
const todayStr = formatDate(new Date())
|
||||||
|
const firstDay = new Date(year, month, 1)
|
||||||
|
const lastDay = new Date(year, month + 1, 0)
|
||||||
|
const startPad = firstDay.getDay()
|
||||||
|
const totalDays = lastDay.getDate()
|
||||||
|
const weeks = []
|
||||||
|
let currentWeek = []
|
||||||
|
|
||||||
|
const prevMonthLast = new Date(year, month, 0).getDate()
|
||||||
|
for (let i = startPad - 1; i >= 0; i--) {
|
||||||
|
const d = new Date(year, month - 1, prevMonthLast - i)
|
||||||
|
currentWeek.push(buildCalendarDayCell(d, todayStr, bookableSet, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let day = 1; day <= totalDays; day++) {
|
||||||
|
const d = new Date(year, month, day)
|
||||||
|
currentWeek.push(buildCalendarDayCell(d, todayStr, bookableSet, true))
|
||||||
|
if (currentWeek.length === 7) {
|
||||||
|
weeks.push(currentWeek)
|
||||||
|
currentWeek = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWeek.length) {
|
||||||
|
let nextDay = 1
|
||||||
|
while (currentWeek.length < 7) {
|
||||||
|
const d = new Date(year, month + 1, nextDay++)
|
||||||
|
currentWeek.push(buildCalendarDayCell(d, todayStr, bookableSet, false))
|
||||||
|
}
|
||||||
|
weeks.push(currentWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
return weeks
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftWeekDate(dateStr, deltaWeeks) {
|
||||||
|
const parts = parseDateParts(dateStr)
|
||||||
|
if (!parts) return formatDate(new Date())
|
||||||
|
const d = parts.date
|
||||||
|
d.setDate(d.getDate() + deltaWeeks * 7)
|
||||||
|
return formatDate(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftMonthDate(dateStr, deltaMonths) {
|
||||||
|
const parts = parseDateParts(monthStartDate(dateStr))
|
||||||
|
if (!parts) return monthStartDate(formatDate(new Date()))
|
||||||
|
const d = new Date(parts.y, parts.m - 1 + deltaMonths, 1)
|
||||||
|
return formatDate(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampBookableDate(dateStr, bookableList) {
|
||||||
|
if (!dateStr) return dateStr
|
||||||
|
if (!Array.isArray(bookableList) || !bookableList.length) return dateStr
|
||||||
|
if (bookableList.includes(dateStr)) return dateStr
|
||||||
|
const target = new Date(dateStr).getTime()
|
||||||
|
let nearest = bookableList[0]
|
||||||
|
let minDiff = Math.abs(new Date(nearest).getTime() - target)
|
||||||
|
bookableList.forEach((item) => {
|
||||||
|
const diff = Math.abs(new Date(item).getTime() - target)
|
||||||
|
if (diff < minDiff) {
|
||||||
|
minDiff = diff
|
||||||
|
nearest = item
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nearest
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSlotFull(slot) {
|
||||||
|
if (!slot) return true
|
||||||
|
return slot.availableQty != null && slot.availableQty <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时段剩余可约数量。capacity 为 0 表示不限,返回 null */
|
||||||
|
export function getSlotAvailableQty(slot) {
|
||||||
|
if (!slot) return null
|
||||||
|
const capacity = Number(slot.capacity)
|
||||||
|
if (!capacity) return null
|
||||||
|
const qty = Number(slot.availableQty)
|
||||||
|
if (!Number.isFinite(qty) || qty >= 1000000) return null
|
||||||
|
return Math.max(Math.floor(qty), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时段开始时间晚于当前时间才可预约;未来日期全部可约 */
|
||||||
|
export function isSlotStartAfterNow(slot, dateStr) {
|
||||||
|
if (!slot?.slotStartTime) return false
|
||||||
|
const date = formatDate(dateStr || slot.slotDate)
|
||||||
|
if (!date) return false
|
||||||
|
const today = formatDate(new Date())
|
||||||
|
if (date > today) return true
|
||||||
|
if (date < today) return false
|
||||||
|
const parts = String(slot.slotStartTime).split(':').map(Number)
|
||||||
|
const start = new Date()
|
||||||
|
start.setHours(parts[0] || 0, parts[1] || 0, 0, 0)
|
||||||
|
return start.getTime() > Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slotStatusText(slot) {
|
||||||
|
if (isSlotFull(slot)) return '约满'
|
||||||
|
if (slot?.showRemainingQty && slot?.availableQty != null) {
|
||||||
|
return `剩余 ${slot.availableQty}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const SELLER_WEEKDAY_LABELS = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||||
|
|
||||||
|
function formatWeekdayRule(raw) {
|
||||||
|
const set = parseWeekdays(raw)
|
||||||
|
if (!set) return ''
|
||||||
|
const days = [...set].sort((a, b) => a - b)
|
||||||
|
if (days.length >= 7) return '每天'
|
||||||
|
const consecutive = days.every((day, index) => index === 0 || day === days[index - 1] + 1)
|
||||||
|
if (consecutive && days.length > 1) {
|
||||||
|
return `${SELLER_WEEKDAY_LABELS[days[0]]}至${SELLER_WEEKDAY_LABELS[days[days.length - 1]]}`
|
||||||
|
}
|
||||||
|
return days.map((day) => SELLER_WEEKDAY_LABELS[day]).join('、')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约须知中的可预约日期说明 */
|
||||||
|
export function buildBookableDateNotice(ext) {
|
||||||
|
if (!ext) return ''
|
||||||
|
const mode = String(ext.bookableDateMode || '').toUpperCase()
|
||||||
|
if (mode !== 'CUSTOM') return '可预约日期:每天。'
|
||||||
|
const start = formatDate(parseDay(ext.customDateStart))
|
||||||
|
const end = formatDate(parseDay(ext.customDateEnd))
|
||||||
|
const weekdays = formatWeekdayRule(ext.customWeekdays)
|
||||||
|
let rangeText = ''
|
||||||
|
if (start && end) rangeText = `${start} 至 ${end}`
|
||||||
|
else if (start) rangeText = `${start} 起`
|
||||||
|
else if (end) rangeText = `至 ${end}`
|
||||||
|
let rule = rangeText
|
||||||
|
if (weekdays && weekdays !== '每天') {
|
||||||
|
rule = rangeText ? `${rangeText} 的${weekdays}` : weekdays
|
||||||
|
}
|
||||||
|
if (!rule) return '可预约日期:每天。'
|
||||||
|
return `可预约日期:${rule}。`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约须知中的预约日期范围(今天起连续 N 天,含今天) */
|
||||||
|
export function buildBookingRangeNotice(ext) {
|
||||||
|
if (!ext) return ''
|
||||||
|
const rangeDays = Math.max(Number(ext.bookingDateRangeDays) || 30, 1)
|
||||||
|
if (rangeDays === 1) return '预约日期范围:只能预约今天。'
|
||||||
|
if (rangeDays === 2) return '预约日期范围:可以预约今天和明天。'
|
||||||
|
return `预约日期范围:从今天起,连续 ${rangeDays} 天都可以预约。`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLeadTime(hours) {
|
||||||
|
const n = Math.max(Number(hours) || 1, 1)
|
||||||
|
if (n % 24 === 0) return `${n / 24} 天`
|
||||||
|
return `${n} 小时`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约须知中的提前预约说明 */
|
||||||
|
export function buildAdvanceNotice(ext) {
|
||||||
|
if (!ext) return ''
|
||||||
|
const mode = String(ext.advanceBookingMode || '').toUpperCase()
|
||||||
|
const hours = Number(ext.advanceBookingHours)
|
||||||
|
if (mode === 'REQUIRE_HOURS' && hours > 0) {
|
||||||
|
return `提前预约:请提前 ${formatLeadTime(hours)}预约。`
|
||||||
|
}
|
||||||
|
return '提前预约:随时可以约,选现在之后的时段就行。'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCancelNotice(ext) {
|
||||||
|
if (!ext) return '请按时到店或等候服务,取消规则以商家设置为准。'
|
||||||
|
const mode = String(ext.cancelMode || '').toUpperCase()
|
||||||
|
const hours = Number(ext.cancelHours)
|
||||||
|
if (mode === 'NOT_ALLOWED') return '取消订单:约好后就不能取消了,请确认时间再下单。'
|
||||||
|
if (mode === 'ALLOW_BEFORE_HOURS' && hours > 0) {
|
||||||
|
return `取消订单:服务开始前 ${formatLeadTime(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 formatFormAnswerValue(value) {
|
||||||
|
if (value == null || value === '') return ''
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const list = value.map((item) => (item == null ? '' : String(item).trim())).filter(Boolean)
|
||||||
|
if (!list.length) return ''
|
||||||
|
const looksLikeFile = list.every((item) => /^(https?:|wxfile:|data:|blob:|\/)/.test(item))
|
||||||
|
return looksLikeFile ? `${list.length}张图片` : list.join('、')
|
||||||
|
}
|
||||||
|
const text = String(value).trim()
|
||||||
|
return text === '~' ? '' : text
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结算页展示用:字段名 + 填写内容。多份时用分组标题,不把序号拼进字段名 */
|
||||||
|
export function buildFormDisplayRows(fields, groups) {
|
||||||
|
const rows = []
|
||||||
|
const list = groups || []
|
||||||
|
list.forEach((group, index) => {
|
||||||
|
const fieldRows = []
|
||||||
|
;(fields || []).forEach((field) => {
|
||||||
|
if (!field || field.richText) return
|
||||||
|
const text = formatFormAnswerValue(group?.[field.fieldKey])
|
||||||
|
if (!text) return
|
||||||
|
fieldRows.push({
|
||||||
|
label: field.fieldLabel || '补充信息',
|
||||||
|
value: text,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!fieldRows.length) return
|
||||||
|
if (list.length > 1) {
|
||||||
|
rows.push({ type: 'group', label: `第${index + 1}位` })
|
||||||
|
}
|
||||||
|
rows.push(...fieldRows)
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从表单答案提取联系人快照:姓名、手机号字段 */
|
||||||
|
export function pickContactFromForm(fields, groups) {
|
||||||
|
const group = (groups && groups[0]) || {}
|
||||||
|
let contactName = ''
|
||||||
|
let contactPhone = ''
|
||||||
|
;(fields || []).forEach((field) => {
|
||||||
|
if (!field || field.richText || field.fieldType !== 'TEXT') return
|
||||||
|
const text = String(group[field.fieldKey] ?? '').trim()
|
||||||
|
if (!text) return
|
||||||
|
const label = field.fieldLabel || ''
|
||||||
|
const contentType = field.contentType || ''
|
||||||
|
if (!contactPhone && (contentType === 'MOBILE' || /电话|手机/.test(label))) {
|
||||||
|
contactPhone = text
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!contactName && /姓名|联系人|名字/.test(label)) {
|
||||||
|
contactName = text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { contactName, contactPhone }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 appointmentOrderStatusText(orderStatus) {
|
||||||
|
if (orderStatus === 'UNPAID') return '待支付'
|
||||||
|
if (orderStatus === 'CANCELLED') return '已取消'
|
||||||
|
if (orderStatus === 'COMPLETED' || orderStatus === 'COMPLETE') return '已完成'
|
||||||
|
return '待使用'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预约单:未付款仅可取消;已付款仅可售后 */
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -73,15 +73,23 @@ export function setClipboard (val) {
|
|||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// #ifndef H5
|
// #ifndef H5
|
||||||
uni.setClipboardData({
|
const text = val == null ? "" : String(val);
|
||||||
data: val,
|
const clipboard = typeof wx !== "undefined" && wx.setClipboardData ? wx : uni;
|
||||||
|
clipboard.setClipboardData({
|
||||||
|
data: text,
|
||||||
success: function () {
|
success: function () {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "复制成功!",
|
title: "复制成功",
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
fail: function () {
|
||||||
|
uni.showToast({
|
||||||
|
title: "复制失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
}
|
}
|
||||||
|
|||||||
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,26 @@ 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,
|
||||||
|
getAppointmentStock,
|
||||||
|
getAppointmentMaxBuyNum,
|
||||||
|
} from './appointmentGoods.js'
|
||||||
|
|
||||||
|
export {
|
||||||
|
APPOINTMENT_GOODS_TYPE,
|
||||||
|
APPOINTMENT_ORDER_TYPE,
|
||||||
|
isAppointmentGoods,
|
||||||
|
isAppointmentOrder,
|
||||||
|
hasAppointmentInCheckout,
|
||||||
|
getAppointmentStock,
|
||||||
|
getAppointmentMaxBuyNum,
|
||||||
|
}
|
||||||
|
|
||||||
/** 单次购买数量上限(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 +52,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 同步) */
|
||||||
|
|||||||
@@ -67,15 +67,20 @@ export function getThemeStyle(theme = DEFAULT_THEME) {
|
|||||||
const safeTheme = theme || DEFAULT_THEME;
|
const safeTheme = theme || DEFAULT_THEME;
|
||||||
const primary = safeTheme.mainColor || DEFAULT_THEME.mainColor;
|
const primary = safeTheme.mainColor || DEFAULT_THEME.mainColor;
|
||||||
const light = safeTheme.lightColor || DEFAULT_THEME.lightColor;
|
const light = safeTheme.lightColor || DEFAULT_THEME.lightColor;
|
||||||
|
const primarySoft = hexToRgba(primary, 0.1);
|
||||||
return {
|
return {
|
||||||
"--theme-primary": primary,
|
"--theme-primary": primary,
|
||||||
"--theme-light": light,
|
"--theme-light": light,
|
||||||
"--theme-aider": safeTheme.aiderLightColor || DEFAULT_THEME.aiderLightColor,
|
"--theme-aider": safeTheme.aiderLightColor || DEFAULT_THEME.aiderLightColor,
|
||||||
"--theme-primary-10": hexToRgba(primary, 0.1),
|
"--theme-primary-10": primarySoft,
|
||||||
"--theme-primary-20": hexToRgba(primary, 0.2),
|
"--theme-primary-20": hexToRgba(primary, 0.2),
|
||||||
"--theme-primary-30": hexToRgba(primary, 0.3),
|
"--theme-primary-30": hexToRgba(primary, 0.3),
|
||||||
"--theme-primary-70": hexToRgba(primary, 0.7),
|
"--theme-primary-70": hexToRgba(primary, 0.7),
|
||||||
"--theme-light-10": hexToRgba(light, 0.1),
|
"--theme-light-10": hexToRgba(light, 0.1),
|
||||||
"--theme-light-80": hexToRgba(light, 0.8),
|
"--theme-light-80": hexToRgba(light, 0.8),
|
||||||
|
"--u-primary": primary,
|
||||||
|
"--up-primary": primary,
|
||||||
|
"--u-primary-light": primarySoft,
|
||||||
|
"--up-primary-light": primarySoft,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user