feat: 更新预约功能与礼品卡支持

- 新增礼品卡页面,支持礼品卡的选择与使用。
- 调整预约下单流程,增强预约信息展示与用户交互。
- 更新支付成功页面的导航逻辑,确保用户体验流畅。
- 优化搜索页面的历史关键词加载逻辑,提升性能。
This commit is contained in:
pikachu1995@126.com
2026-09-28 14:37:26 +08:00
parent 2107e3f03a
commit 0bffe81c6b
25 changed files with 2272 additions and 1038 deletions

View File

@@ -148,6 +148,7 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
- ✅ 卡密商品
- ✅ 二级分销
- ✅ 定时任务管理
- ✅ 服务商品
- 🔜 主题色管理
- 🔜 店铺结算汇总
- 🔜 支付方式汇总

View File

@@ -6,7 +6,7 @@ import { http, Method } from '@/utils/request.js'
/** 查询可预约时段 */
export function listSlots(skuId, slotDate) {
return http.request({
url: '/buyer/appointment/slots',
url: '/appointment/slots',
method: Method.GET,
needToken: true,
params: { skuId, slotDate },
@@ -16,7 +16,7 @@ export function listSlots(skuId, slotDate) {
/** 绑定预约结算上下文(addToCart 后、createTrade 前) */
export function bindCheckout(data) {
return http.request({
url: '/buyer/appointment/checkout/bind',
url: '/appointment/checkout/bind',
method: Method.POST,
needToken: true,
data,
@@ -26,7 +26,7 @@ export function bindCheckout(data) {
/** 预约商品扩展配置 */
export function getGoodsExt(goodsId) {
return http.request({
url: `/buyer/appointment/goods/${goodsId}/ext`,
url: `/appointment/goods/${goodsId}/ext`,
method: Method.GET,
needToken: false,
})
@@ -35,18 +35,9 @@ export function getGoodsExt(goodsId) {
/** 买家预约订单详情(时段、核销码) */
export function getAppointmentOrderDetail(orderSn) {
return http.request({
url: `/buyer/appointment/order/${orderSn}`,
url: `/appointment/order/${orderSn}`,
method: Method.GET,
needToken: true,
})
}
/** 改期申请 */
export function requestReschedule(orderSn, payload) {
return http.request({
url: `/buyer/appointment/orders/${orderSn}/reschedule`,
method: Method.POST,
needToken: true,
data: payload || {},
})
}

View File

@@ -53,7 +53,11 @@
<span>,{{ num }}个</span>
</span>
</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>{{ availableStock }}</span>
@@ -105,7 +109,9 @@
<!-- 按钮 -->
<view class="btns" v-if="availableStock !== 0">
<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>
</u-popup>
@@ -124,6 +130,8 @@ import {
isAppointmentGoods,
getECouponStock,
getECouponMaxBuyNum,
getAppointmentStock,
getAppointmentMaxBuyNum,
} from '@/utils/goodsType.js'
const props = withDefaults(defineProps<{
@@ -159,11 +167,14 @@ const formatList = ref<any[]>([])
const currentSelected = ref<string[]>([])
const skuList = ref('')
// E_COUPON 库存读 SKU quantity,上限 min(quantity, 999)
// E_COUPON 读 SKU quantity;预约商品 quantity 固定 0,库存走时段
const availableStock = computed(() => {
if (isECoupon(props.goodsDetail?.goodsType)) {
return getECouponStock(props.goodsDetail)
}
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
return getAppointmentStock(props.goodsDetail)
}
return Number(props.goodsDetail?.quantity) || 0
})
@@ -171,6 +182,9 @@ const maxBuyNum = computed(() => {
if (isECoupon(props.goodsDetail?.goodsType)) {
return getECouponMaxBuyNum(props.goodsDetail)
}
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
return getAppointmentMaxBuyNum(props.goodsDetail)
}
return 999
})
@@ -340,17 +354,17 @@ const addToCartOrBuy = (val: string) => {
}
})
} 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()
API_trade.addToCart(data).then((res) => {
if (res.data.success) {
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
const goodsId = props.goodsDetail.goodsId || ''
uni.navigateTo({
url: `/pages/order/appointment/appointmentCheckout?skuId=${props.goodsDetail.id}&goodsId=${goodsId}&num=${num.value}`,
})
closeMask()
return
}
uni.navigateTo({
url: `/pages/order/fillorder?way=${data.cartType}&addr=${props.addr?.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value || ''))}`,
})

View File

@@ -210,7 +210,7 @@
},
"plugins" : {},
"libVersion" : "3.5.8",
"__usePrivacyCheck__" : true,
"__usePrivacyCheck__" : false,
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
},
"h5" : {

View File

@@ -777,6 +777,12 @@
"navigationBarTitleText": "我的礼品卡"
}
},
{
"path": "gift-card/index",
"style": {
"navigationBarTitleText": "礼品卡"
}
},
{
"path": "payment/payOrder",
"style": {
@@ -876,13 +882,7 @@
{
"path": "appointment/appointmentCheckout",
"style": {
"navigationBarTitleText": "预约信息"
}
},
{
"path": "appointment/appointmentReschedule",
"style": {
"navigationBarTitleText": "申请改期"
"navigationBarTitleText": "立即预约"
}
},
{

View 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>

View File

@@ -124,7 +124,7 @@ function hideLoadingIfNeeded() {
}
function callback(paymentMethod: string) {
uni.navigateTo({
uni.redirectTo({
url:
'/pages/cart/payment/success?paymentMethod=' +
paymentMethod +

View File

@@ -60,9 +60,11 @@ function checkOrder() {
uni.reLaunch({
url: '/pages/mine/deposit/operation',
})
} else {
navigateTo('/pages/order/myOrder?status=0')
return
}
uni.reLaunch({
url: '/pages/order/myOrder?status=0',
})
}
function navigateTo(url: string, type?: string) {

View File

@@ -37,7 +37,7 @@
</block>
</scroll-view>
<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="u-tips">热门搜索</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() {
hotKeywordList.value = []
if (
!storage.getHotWords().time ||
storage.getHotWords().time <= new Date().getTime() / 1000
) {
const res = await getHotKeywords(10)
const keywords = res.data.result
const cached = storage.getHotWords() || {}
const now = Date.now() / 1000
let keywords: string[] = []
keywords.forEach((item: string) => {
hotKeywordList.value.push(item)
})
const hotData = {
time: new Date().getTime() / 1000 + 30 * 5,
keywords: keywords,
}
storage.setHotWords(hotData)
if (cached.time && cached.time > now && Array.isArray(cached.keywords) && cached.keywords.length) {
keywords = cached.keywords
} else {
const keywords = storage.getHotWords().keywords
keywords.forEach((item: string) => {
hotKeywordList.value.push(item)
try {
const res = await getHotKeywords(10)
if (res?.data?.success && Array.isArray(res.data.result)) {
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()
}

View File

@@ -17,7 +17,7 @@
</view>
<view>
<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)">
<view class="goods-img">
<u-image
@@ -32,7 +32,7 @@
<view class="goods-title">{{ getGoodsName(item) }}</view>
<view class="goods-price">
<text>¥{{ unitPrice(displayRefundPrice) }}</text>
<text class="num">购买数量: {{ item.num }}</text>
<text class="num">购买数量: {{ item.num || 0 }}</text>
</view>
</view>
</view>
@@ -41,10 +41,10 @@
<view class="after-num">
<text class="after-num-label">申请数量</text>
<u-number-box
:value="parseInt(form.num)"
:value="applyNum"
disabled-input
:min="1"
:max="parseInt(sku.num)"
:max="maxApplyNum"
bg-color="#fff"
@change="valChange"
></u-number-box>
@@ -164,7 +164,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, getCurrentInstance, watch } from 'vue'
import { ref, reactive, computed, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { unitPrice, parseGoodsImageUrl } from '@/utils/filters.js'
@@ -189,22 +189,6 @@ const applyInfo = ref<any>({})
const uToast = ref<any>(null)
const isAppointmentAfterSale = ref(false)
const displayRefundPrice = computed(() => {
if (!isAppointmentAfterSale.value) return applyInfo.value?.applyRefundPrice
return computePartialRefundPrice(applyInfo.value, form.num)
})
watch(
() => form.num,
() => {
if (!isAppointmentAfterSale.value) return
applyInfo.value = {
...applyInfo.value,
displayRefundPrice: displayRefundPrice.value,
}
}
)
const form = reactive({
orderItemSn: '',
skuId: '',
@@ -222,6 +206,21 @@ const form = reactive({
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) => {
let navTitle = '申请售后'
form.serviceType = 'RETURN_GOODS'
@@ -239,12 +238,14 @@ onLoad((options) => {
}
uni.setNavigationBarTitle({ title: navTitle })
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.skuId = sku.value.skuId
form.num = sku.value.num
form.goodsId = sku.value.goodsId
form.skuId = sku.value.skuId || ''
form.num = Number(sku.value.num) > 0 ? Number(sku.value.num) : 1
form.goodsId = sku.value.goodsId || ''
fetchReasonActions(form.serviceType)
init(options?.sn || '')
})

View File

@@ -10,7 +10,7 @@
</view>
<view>
<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">
<u-image
border-radius="6"
@@ -23,8 +23,8 @@
<view class="goods-info">
<view class="goods-title">{{ getGoodsName(item) }}</view>
<view class="goods-price">
<text v-if="sku.orderItems.length <= 1">¥{{ unitPrice(sku.flowPrice) }}</text>
<text class="num">购买数量: {{ item.num }}</text>
<text v-if="!sku.orderItems || sku.orderItems.length <= 1">¥{{ unitPrice(sku.flowPrice) }}</text>
<text class="num">购买数量: {{ item.num || 0 }}</text>
</view>
</view>
</view>
@@ -72,7 +72,9 @@ const applyInfo = ref<any>({})
onLoad((options) => {
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)) {
uni.showToast({
title: '电子卡券订单不支持售后',

File diff suppressed because it is too large Load Diff

View File

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

View 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>

View File

@@ -1,5 +1,5 @@
<template>
<div class="wrapper">
<div class="wrapper" :style="themeStyle">
<!-- 选择地址 -->
<div
class="address-box"
@@ -68,9 +68,24 @@
</div>
</div>
<view class="address-box appointment-summary" v-if="isAppointmentCheckout && appointmentCheckoutSummary">
<view class="appointment-summary__title">预约信息</view>
<view class="appointment-summary__text">{{ appointmentCheckoutSummary }}</view>
<view 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>
<!-- 开团信息 -->
@@ -341,54 +356,24 @@
</u-col>
</u-row>
</div>
<div class="gift-card-module">
<u-row>
<u-col :span="6">
<view class="gift-card-title-row">
<text>使用礼品卡</text>
<text class="gift-card-help" @click.stop="showGiftCardNotice">使用说明</text>
</view>
</u-col>
<u-col :span="6" class="tr tipsColor" textAlign="right">
<span v-if="giftCardDeductAmount > 0" class="main-color"
>已抵扣 -¥{{ unitPrice(giftCardDeductAmount) }}</span
>
<span v-else>{{ giftCardList.length }} 张可用</span>
</u-col>
</u-row>
<view v-if="!giftCardList.length" class="gift-card-empty">暂无可用礼品卡</view>
<view v-else 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__header">
<view class="gift-card-item__pattern" />
<view class="gift-card-item__inner">
<view>
<view class="gift-card-item__name">{{ item.giftCardName || '礼品卡' }}</view>
<view class="gift-card-item__face">面值{{ item.faceText }}元</view>
</view>
<view class="gift-card-item__right">
<view class="gift-card-item__type">现金卡</view>
<view class="gift-card-item__expire">{{ item.expireText }}</view>
</view>
</view>
</view>
<view class="gift-card-item__body">
<view class="gift-card-item__balance">
<text class="gift-card-item__amt">¥{{ unitPrice(item.balance) }}</text>
<text>余额</text>
</view>
<view class="gift-card-item__no">{{ item.cardNo }}</view>
</view>
<view v-if="isGiftCardSelected(item)" class="gift-card-item__check">已选</view>
<u-row>
<u-col :span="8" @click="openGiftCards()">使用礼品卡</u-col>
<u-col
:span="4"
textAlign="right"
class="coupon-col"
@click="openGiftCards()"
>
<view class="coupon-col__content" v-if="giftCardDeductAmount > 0">
<text class="main-color">-¥{{ unitPrice(giftCardDeductAmount) }}</text>
<u-icon name="arrow-right" size="14" color="#909399"></u-icon>
</view>
</view>
</div>
<view class="coupon-col__content" v-else>
<text>{{ giftCardList.length }} 张可用</text>
<u-icon name="arrow-right" size="14" color="#909399"></u-icon>
</view>
</u-col>
</u-row>
</div>
<!-- 配送地区没有提示 -->
@@ -441,11 +426,11 @@
import { ref, computed, watch, getCurrentInstance } from 'vue'
import { onLoad, onShow, onBackPress } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import * as API_Address from '@/api/address'
import * as API_Order from '@/api/order'
import * as API_Trade from '@/api/trade'
import configs from '@/config/config'
import LiLiWXPay from '@/js_sdk/lili-pay/wx-pay.js'
import invoices from '@/pages/order/invoice/setInvoice'
import {
unitPrice,
@@ -456,9 +441,9 @@ import {
import { hasECouponInCheckout, hasAppointmentInCheckout } from '@/utils/goodsType.js'
import { SERVICE_MODE_LABEL, formatDate } from '@/utils/appointmentGoods.js'
import { bindCheckout } from '@/api/appointment.js'
import { decorateGiftCard, GIFT_CARD_PAY_NOTICE } from '@/utils/giftCard.js'
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const { proxy } = getCurrentInstance()!
const lightColor = computed(() => store.getters.lightColor)
const mainColor = computed(() => store.getters.mainColor)
@@ -509,25 +494,66 @@ const hidePhysicalCheckout = computed(
)
const appointmentCheckoutContext = ref<any>(null)
const appointmentCheckoutSummary = computed(() => {
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 ''
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 slot = ctx.slotStartTime && ctx.slotEndTime ? `${ctx.slotStartTime}-${ctx.slotEndTime}` : ''
const contact = ctx.contactName ? `${ctx.contactName} ${ctx.contactPhone || ''}` : ''
return [date, slot, mode, contact].filter(Boolean).join(' · ')
const 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 selectedGiftCardIds = computed(() => orderMessage.value?.selectedGiftCardIds || [])
const giftCardList = computed(() =>
(orderMessage.value?.canUseGiftCards || []).map((item: any) =>
decorateGiftCard(item, selectedGiftCardIds.value)
)
)
const giftCardList = computed(() => orderMessage.value?.canUseGiftCards || [])
const giftCardDeductAmount = computed(() => {
const dto = orderMessage.value?.priceDetailDTO
if (!dto) return 0
@@ -652,36 +678,10 @@ function invoice() {
invoiceFlag.value = true
}
function showGiftCardNotice() {
uni.showModal({
title: '礼品卡使用说明',
content: GIFT_CARD_PAY_NOTICE,
showCancel: false,
confirmText: '我知道了',
})
}
function isGiftCardSelected(item: any) {
if (!item?.id) return false
return (selectedGiftCardIds.value || []).map(String).includes(String(item.id))
}
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 openGiftCards() {
;(store.state as any).canUseGiftCards = giftCardList.value
;(store.state as any).selectedGiftCardIds = orderMessage.value?.selectedGiftCardIds || []
navigateTo(`/pages/cart/gift-card/index?way=${routerVal.value.way}`)
}
function GET_Discount() {
@@ -787,15 +787,9 @@ function createTradeFun() {
url: '/pages/order/myOrder?status=0',
})
} else {
// #ifdef MP-WEIXIN
pay(res.data.result.sn)
// #endif
// #ifndef MP-WEIXIN
navigateTo(
`/pages/cart/payment/payOrder?trade_sn=${res.data.result.sn}`
)
// #endif
}
} else {
uni.showToast({
@@ -832,13 +826,6 @@ function createTradeFun() {
}, 3000)
}
async function pay(sn: string) {
new LiLiWXPay({
sn,
price: orderMessage.value.priceDetailDTO.billPrice,
}).pay()
}
function getUserAddress() {
API_Address.getAddressDefault().then((res) => {
if (res.data.result) {
@@ -942,6 +929,8 @@ function getOrderList() {
orderMessage.value = res.data.result
;(store.state as any).canUseCoupons = res.data.result.canUseCoupons
;(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
if (!hasECouponInCheckout(res.data.result) && !hasAppointmentInCheckout(res.data.result)) {
@@ -976,20 +965,87 @@ page {
}
</style>
<style scoped lang="scss">
.appointment-summary {
padding: 24rpx 32rpx;
background: #fff;
.appointment-card {
margin-bottom: 20rpx;
padding: 28rpx 32rpx 12rpx;
background: #fff;
border-radius: 40rpx;
}
.appointment-summary__title {
font-size: 28rpx;
.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;
margin-bottom: 12rpx;
color: #222;
line-height: 1.2;
letter-spacing: 1rpx;
}
.appointment-summary__text {
font-size: 24rpx;
.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 {
@@ -1159,6 +1215,7 @@ page {
.box6 {
position: fixed;
z-index: 20;
bottom: 0;
left: 0;
width: 100%;
@@ -1170,6 +1227,7 @@ page {
color: #333;
display: flex;
justify-content: space-between;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.tabbar-right {
@@ -1260,7 +1318,8 @@ page {
.wrapper {
height: auto;
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;
}
@@ -1348,129 +1407,4 @@ page {
margin: 20rpx 0;
}
.gift-card-module {
margin-top: 8rpx;
}
.gift-card-title-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.gift-card-help {
font-size: 22rpx;
color: #909399;
}
.gift-card-empty {
margin-top: 16rpx;
padding: 24rpx;
font-size: 24rpx;
color: #999;
text-align: center;
background: #fafafa;
border-radius: 12rpx;
}
.gift-card-list {
margin-top: 16rpx;
}
.gift-card-item {
position: relative;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
background: #fff;
&--selected {
border-color: $main-color;
}
}
.gift-card-item__header {
position: relative;
background: linear-gradient(125deg, #ff9a4a 0%, #ff7729 42%, #ff8f3d 100%);
}
.gift-card-item__pattern {
position: absolute;
inset: 0;
opacity: 0.22;
pointer-events: none;
background-image: repeating-linear-gradient(
-36deg,
transparent,
transparent 10rpx,
rgba(255, 255, 255, 0.45) 10rpx,
rgba(255, 255, 255, 0.45) 12rpx
);
}
.gift-card-item__inner {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
padding: 20rpx 24rpx 16rpx;
color: #fff;
}
.gift-card-item__name {
font-size: 28rpx;
font-weight: 700;
}
.gift-card-item__face,
.gift-card-item__expire {
margin-top: 8rpx;
font-size: 22rpx;
opacity: 0.92;
}
.gift-card-item__right {
text-align: right;
}
.gift-card-item__type {
font-size: 28rpx;
font-weight: 700;
}
.gift-card-item__body {
padding: 16rpx 24rpx 20rpx;
}
.gift-card-item__balance {
display: flex;
align-items: baseline;
gap: 8rpx;
font-size: 22rpx;
color: #999;
}
.gift-card-item__amt {
font-size: 32rpx;
font-weight: 700;
color: #ff6b22;
}
.gift-card-item__no {
margin-top: 8rpx;
font-size: 22rpx;
color: #999;
}
.gift-card-item__check {
position: absolute;
right: 0;
bottom: 0;
padding: 4rpx 14rpx;
font-size: 20rpx;
color: #fff;
background: $main-color;
border-top-left-radius: 12rpx;
}
</style>

View File

@@ -1,17 +1,48 @@
<template>
<view class="content">
<view class="navbar">
<view v-for="(item, index) in navList" :key="index" class="nav-item"
:class="{ current: tabCurrentIndex === index }" @click="tabClick(index)">{{ item.text }}</view>
</view>
<scroll-view class="navbar" scroll-x :show-scrollbar="false">
<view class="navbar-inner">
<view v-for="(item, index) in navList" :key="index" class="nav-item"
:class="{ current: tabCurrentIndex === index }" @click="tabClick(index)">{{ item.text }}</view>
</view>
</scroll-view>
<swiper :current="tabCurrentIndex" class="swiper-box" duration="300" @change="changeTab">
<swiper-item class="tab-content" v-for="(tabItem, tabIndex) in navList" :key="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>
<!-- 订单列表 -->
<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-name wes" @click="navigateToStore(order)">
@@ -26,12 +57,11 @@
</view>
</view>
<view class="order-sn">{{orderStatusList(order.orderStatus)
}}</view>
<view class="order-sn">{{ orderCardStatus(order) }}</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"
:key="goodsIndex">
<u-image border-radius="6" width="131rpx" height="131rpx" mode="aspectFill"
@@ -70,16 +100,12 @@
v-if="order.allowOperationVO.rog && !isECouponOrder(order.orderType)" @click="onRog(order.sn)">
确认收货
</view>
<view ripple shape="circle" class="cancel-btn" size="mini"
v-if="showOrderAfterSale(order)"
@click="applyService(order)">
退款/售后
</view>
</view>
</view>
</view>
</template>
</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>
</swiper-item>
</swiper>
@@ -114,7 +140,7 @@ import { onLoad, onShow, onPullDownRefresh, onBackPress } from '@dcloudio/uni-ap
import { useStore } from '@/store'
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue'
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 {
unitPrice,
@@ -124,7 +150,7 @@ import {
} from '@/utils/filters.js'
// E_COUPON 订单不展示确认收货、退款/售后(FR-B-04)
import { isECouponOrder, isAppointmentOrder } from '@/utils/goodsType.js'
import { getAppointmentAllowOperation } from '@/utils/appointmentGoods.js'
import { appointmentOrderStatusText, getAppointmentAllowOperation } from '@/utils/appointmentGoods.js'
const store = useStore()
const { proxy } = getCurrentInstance()!
@@ -134,6 +160,9 @@ const mainColor = computed(() => store.getters.mainColor)
interface NavTabItem {
state: number
text: string
tag: string
commentStatus?: string
kind?: 'order' | 'afterSale'
loadStatus: string
orderList: any[]
pageNumber: number
@@ -142,30 +171,27 @@ interface NavTabItem {
const tabCurrentIndex = ref(0)
const navList = reactive<NavTabItem[]>([
{ state: 0, text: '全部', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 1, text: '待付款', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 2, text: '待发货', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 3, text: '待收货', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 4, text: '已完成', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 5, text: '已取消', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 0, text: '全部', tag: 'ALL', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 1, text: '待支付', tag: 'WAIT_PAY', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 2, text: '待发货', tag: 'WAIT_SHIP', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 3, text: '待收货/使用', tag: 'WAIT_USE', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 4, text: '评价', tag: 'ALL', commentStatus: 'UNFINISHED', loadStatus: 'more', orderList: [], pageNumber: 1 },
{ state: 5, text: '售后', tag: 'ALL', kind: 'afterSale', loadStatus: 'more', orderList: [], pageNumber: 1 },
])
const status = ref<number>(0)
const params = reactive({
pageNumber: 1,
pageSize: 10,
tag: 'ALL',
})
const orderStatus = [
{ orderStatus: 'ALL' },
{ orderStatus: 'WAIT_PAY' },
{ orderStatus: 'WAIT_SHIP' },
{ orderStatus: 'WAIT_ROG' },
{ orderStatus: 'COMPLETE' },
{ orderStatus: 'CANCELLED' },
{ orderStatus: 'STAY_PICKED_UP' },
]
const afterSaleStatusText: Record<string, string> = {
APPLY: '申请中',
PASS: '已通过',
REFUSE: '已拒绝',
BUYER_RETURN: '待卖家收货',
SELLER_CONFIRM: '卖家确认收货',
SELLER_TERMINATION: '卖家终止售后',
BUYER_CANCEL: '买家取消售后',
WAIT_REFUND: '等待平台退款',
COMPLETE: '完成',
}
const cancelShow = ref(false)
const orderSn = ref('')
@@ -208,21 +234,15 @@ onLoad((options) => {
})
watch(tabCurrentIndex, (val) => {
params.tag = orderStatus[val].orderStatus
navList.forEach((res) => {
res.pageNumber = 1
res.loadStatus = 'more'
res.orderList = []
res.loaded = false
})
loadData(val)
})
function applyService(order: any) {
uni.navigateTo({
url: `/pages/order/afterSales/afterSales?orderSn=${order.sn}`,
})
}
function navigateToStore(val: any) {
uni.navigateTo({
url: '/pages/product/shopPage?id=' + val.storeId,
@@ -286,6 +306,7 @@ function initData(index: number) {
navList[index].pageNumber = 1
navList[index].loadStatus = 'more'
navList[index].orderList = []
navList[index].loaded = false
loadData(index)
}
@@ -302,26 +323,99 @@ function pay(val: any) {
}
function loadData(index: number) {
params.pageNumber = navList[index].pageNumber
getOrderList(params).then((res) => {
const tab = navList[index]
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()
tab.loaded = true
if (!res.data.success) {
navList[index].loadStatus = 'noMore'
tab.loadStatus = 'noMore'
return false
}
const records = res.data.result.records
if (records.length == 0) {
navList[index].loadStatus = 'noMore'
} else if (records.length < 10) {
navList[index].loadStatus = 'noMore'
let records = res.data.result.records || []
if (tab.kind === 'afterSale') {
records = records.map(mapAfterSaleOrder)
}
if (records.length < 10) {
tab.loadStatus = 'noMore'
}
if (records.length > 0) {
navList[index].orderList = navList[index].orderList.concat(records)
navList[index].pageNumber += 1
tab.orderList = tab.orderList.concat(records)
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) {
tabCurrentIndex.value = e.target.current
}
@@ -379,16 +473,6 @@ function showOrderCancel(order: any) {
return !!order?.allowOperationVO?.cancel
}
function showOrderAfterSale(order: any) {
if (isECouponOrder(order?.orderType)) return false
if (isAppointmentOrder(order?.orderType)) {
const allow = getAppointmentAllowOperation(order)
if (!allow?.afterSale) return false
}
const status = order?.groupAfterSaleStatus
return status && (status.includes('NOT_APPLIED') || status.includes('PART_AFTER_SALE'))
}
function navigateToOrderDetail(sn: string) {
uni.navigateTo({
url: './orderDetail?sn=' + sn,
@@ -474,23 +558,31 @@ function reBuy(order: any) {
}
.navbar {
display: flex;
height: 40px;
padding: 0 5px;
background: #fff;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.06);
position: relative;
z-index: 10;
white-space: nowrap;
.navbar-inner {
display: inline-flex;
align-items: stretch;
height: 40px;
min-width: 100%;
}
.nav-item {
flex: 1;
display: flex;
flex: none;
display: inline-flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 0 24rpx;
font-size: 26rpx;
color: $font-color-light;
position: relative;
white-space: nowrap;
&.current {
color: $main-color;
@@ -543,6 +635,12 @@ function reBuy(order: any) {
}
}
.after-sale-sn {
padding: 0 32rpx 8rpx;
font-size: 24rpx;
color: #999;
}
.goods-item-view {
display: flex;
flex-wrap: wrap;

View File

@@ -1,10 +1,10 @@
<template>
<view>
<view :style="themeStyle">
<!-- 订单状态 -->
<div class="info-view order-view">
<div class="order-status" v-if="orderStatusMap[order.orderStatus]">
<view class="order-status-title">{{ orderStatusMap[order.orderStatus].title }}</view>
<view class="order-status-desc">{{ orderStatusMap[order.orderStatus].value }}</view>
<div class="order-status" v-if="displayOrderStatus">
<view class="order-status-title">{{ displayOrderStatus.title }}</view>
<view class="order-status-desc" v-if="displayOrderStatus.value">{{ displayOrderStatus.value }}</view>
</div>
</div>
@@ -14,50 +14,24 @@
<view v-if="isAppointmentOrderFlag" class="appointment-entry">
<view class="appointment-entry__row">
<text class="label">预约时间</text>
<text>{{ appointmentSlotText }}</text>
<text class="value">{{ appointmentSlotText }}</text>
</view>
<view class="appointment-entry__row" v-if="appointmentDetail.serviceMode">
<text class="label">服务方式</text>
<text>{{ serviceModeLabel }}</text>
</view>
<view class="appointment-entry__row" v-if="appointmentDetail.contactName">
<text class="label">联系人</text>
<text>{{ appointmentDetail.contactName }} {{ appointmentDetail.contactPhone }}</text>
<text class="value">{{ serviceModeLabel }}</text>
</view>
<view class="appointment-entry__row" v-if="appointmentDetail.serviceAddressSnapshot">
<text class="label">服务地址</text>
<text>{{ appointmentDetail.serviceAddressSnapshot }}</text>
<text class="value">{{ appointmentDetail.serviceAddressSnapshot }}</text>
</view>
<view
v-for="(rows, gi) in appointmentFormGroups"
:key="gi"
class="appointment-entry__row appointment-form-block"
>
<text class="label">{{ appointmentFormGroups.length > 1 ? `补充信息${gi + 1}` : '补充信息' }}</text>
<view class="form-answers">
<view v-for="row in rows" :key="row.fieldKey" class="form-answer-row">
{{ row.fieldLabel }}:{{ row.value }}
</view>
<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>
</view>
<view v-if="appointmentCodes.length" class="appointment-codes">
<view class="appointment-codes__title">核销码({{ appointmentCodes.length }})</view>
<view
v-for="code in appointmentCodes"
:key="code.id || code.code"
class="verificationCode appointment-code-item"
>
{{ code.seqNo ? '#' + code.seqNo + ' ' : '' }}{{ code.code }}
<text class="code-status">({{ verificationStatusText(code.status) }})</text>
</view>
</view>
<view
v-if="appointmentAllow.reschedule"
class="card-key-entry__btn reschedule-stub"
@click="onRescheduleStub"
>
申请改期
</view>
</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">
@@ -70,9 +44,7 @@
{{ ecouponPendingMessage || '卡密尚未发放,请稍后刷新' }}
</view>
</view>
<view class="verificationCode" v-else-if="order.verificationCode">
券码: {{ order.orderStatus == 'CANCELLED' ? '已失效' : order.verificationCode }}
</view>
<verification-codes v-else-if="verificationCodeItems.length" :items="verificationCodeItems" />
<view
v-else-if="allowOperation.showLogistics"
@click="handleClickDeliver()"
@@ -124,8 +96,7 @@
<view class="seller-info u-flex u-row-between">
<view class="seller-name" @click="goToShopPage(order)">
<view class="name">{{ order.storeName }}</view>
<view class="status" v-if="orderStatusMap[order.orderStatus]"> {{ orderStatusMap[order.orderStatus].title
}}</view>
<view class="status" v-if="displayOrderStatus"> {{ displayOrderStatus.title }}</view>
</view>
<view class="order-sn"></view>
</view>
@@ -135,18 +106,23 @@
<view class="goods-img" @click="gotoGoodsDetail(sku)">
<u-image border-radius="6" width="131rpx" height="131rpx" :src="sku.image"></u-image>
</view>
<view class="goods-info" @click="gotoGoodsDetail(sku)">
<view class="goods-title u-line-2">{{ sku.goodsName }}</view>
<view class="goods-price">
¥{{unitPrice(sku.goodsPrice) }}
<!-- <span v-if="sku.point">+{{ sku.point }}积分</span> -->
<span style="font-size: 24rpx;margin-left: 14rpx;color: #ff9900;" v-if="!isECouponOrder && !isAppointmentOrderFlag && sku.isRefund && sku.isRefund !== 'NO_REFUND'">
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
</span>
<view class="goods-body">
<view class="goods-info" @click="gotoGoodsDetail(sku)">
<view class="goods-title u-line-2">{{ sku.goodsName }}</view>
<view class="goods-price">
¥{{unitPrice(sku.goodsPrice) }}
<!-- <span v-if="sku.point">+{{ sku.point }}积分</span> -->
<span style="font-size: 24rpx;margin-left: 14rpx;color: #ff9900;" v-if="!isECouponOrder && !isAppointmentOrderFlag && sku.isRefund && sku.isRefund !== 'NO_REFUND'">
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
</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 class="goods-num">
<view>x{{ sku.num }}</view>
</view>
</view>
@@ -206,8 +182,8 @@
<view class="title">订单编号:</view>
<view class="value order-sn-value">
<text class="order-sn-text">{{ order.sn }}</text>
<view class="copy" @click.stop="onCopy(order.sn)">
<u-tag text="复制" type="info" plain size="mini" />
<view class="copy" @tap.stop="onCopy(order.sn)">
<view class="copy-icon"></view>
</view>
</view>
</view>
@@ -312,10 +288,12 @@
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import { getExpress, getPackage } from '@/api/trade.js'
import { cancelOrder, confirmReceipt, getOrderDetail } from '@/api/order.js'
import shares from '@/components/m-share/index'
import { getClearReason } from '@/api/after-sale.js'
import storage from '@/utils/storage'
import {
unitPrice,
secrecyMobile,
@@ -338,15 +316,18 @@ import {
CARD_KEY_FULFILL_STATUS,
} from '@/utils/goodsType.js'
import { getAppointmentOrderDetail } from '@/api/appointment.js'
import VerificationCodes from './components/verificationCodes.vue'
import {
SERVICE_MODE_LABEL,
VERIFICATION_CODE_STATUS_TEXT,
appointmentOrderStatusText,
getAppointmentAllowOperation,
formatDate,
parseFormAnswerGroups,
formatFormAnswerValue,
} from '@/utils/appointmentGoods.js'
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const lightColor = computed(() => store.getters.lightColor)
const mainColor = computed(() => store.getters.mainColor)
@@ -375,12 +356,27 @@ const reason = ref('')
const orderPackage = ref<any[]>([])
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 isAppointmentOrderFlag = computed(() => checkAppointmentOrder(order.value?.orderType))
const isNonPhysicalOrder = computed(() => checkNonPhysicalOrder(order.value?.orderType))
const appointmentDetail = ref<Record<string, any>>({})
const appointmentAllow = computed(() =>
getAppointmentAllowOperation(order.value) || { cancel: false, afterSale: false, reschedule: false }
getAppointmentAllowOperation(order.value) || { cancel: false, afterSale: false }
)
const showCancelBtn = computed(() => {
if (isAppointmentOrderFlag.value) return appointmentAllow.value.cancel
@@ -400,6 +396,30 @@ const appointmentSlotText = computed(() => {
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(() =>
orderHasCardKeyContent(orderGoodsList.value)
)
@@ -417,13 +437,43 @@ const allowOperation = computed(() =>
const showDeliveryInfoBlock = computed(() => {
if (isAppointmentOrderFlag.value) return true
if (isECouponOrder.value) return true
if (order.value.verificationCode) return true
if (order.value.orderType === 'VIRTUAL') return false
if (order.value.deliveryMethod === 'SELF_PICK_UP') return false
if (order.value.verificationCode) return true
if (order.value.deliveryMethod === 'LOGISTICS') return true
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(() =>
getDeliveryEntryText(
orderPackage.value,
@@ -525,10 +575,6 @@ function ByUserMessage(orderItem: any) {
})
}
function verificationStatusText(status: string) {
return VERIFICATION_CODE_STATUS_TEXT[status] || status || '—'
}
async function loadAppointmentDetail(orderSnParam: string) {
if (!checkAppointmentOrder(order.value?.orderType)) {
appointmentDetail.value = {}
@@ -542,12 +588,6 @@ async function loadAppointmentDetail(orderSnParam: string) {
}
}
function onRescheduleStub() {
uni.navigateTo({
url: `/pages/order/appointment/appointmentReschedule?orderSn=${order.value.sn}`,
})
}
function loadData(orderSnParam: string) {
uni.showLoading({ title: '加载中' })
getOrderDetail(orderSnParam).then(async (res) => {
@@ -584,6 +624,35 @@ function onCopy(orderSnText: string) {
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) {
uni.navigateTo({
url: `./afterSales/afterSalesSelect?sn=${orderSnText}&sku=${encodeURIComponent(
@@ -676,13 +745,56 @@ function reBuy(orderItem: any) {
<style lang="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 {
flex: 1;
min-width: 0;
.goods-title {
font-size: 24rpx;
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 {
@@ -857,6 +969,38 @@ page,
.copy {
margin-left: 16rpx;
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;
}
}
@@ -881,8 +1025,9 @@ page,
.appointment-entry__row {
display: flex;
align-items: flex-start;
font-size: 26rpx;
margin-bottom: 12rpx;
margin-bottom: 16rpx;
line-height: 1.5;
}
.appointment-entry__row .label {
@@ -890,26 +1035,17 @@ page,
width: 140rpx;
flex-shrink: 0;
}
.form-answers {
.appointment-entry__row .value {
flex: 1;
min-width: 0;
color: #333;
word-break: break-all;
}
.form-answer-row {
margin-bottom: 4rpx;
}
.appointment-codes__title {
font-size: 26rpx;
font-weight: 600;
margin: 16rpx 0 8rpx;
}
.appointment-code-item {
margin-bottom: 8rpx;
}
.code-status {
color: #999;
font-size: 22rpx;
}
.reschedule-stub {
margin-top: 16rpx;
.appointment-group {
margin: 8rpx 0 12rpx;
font-size: 24rpx;
color: var(--theme-primary, #ff3c2a);
line-height: 1.4;
}
.verificationCode {

View File

@@ -1,5 +1,5 @@
<template>
<div class="main-page">
<div class="main-page" :style="themeStyle">
<!-- #ifdef APP-PLUS -->
<view class="status_bar"></view>
<!-- #endif -->
@@ -13,8 +13,6 @@
routerVal.goodsId
" :thumbnail="goodsDetail.thumbnail" :goodsName="goodsDetail.goodsName" type="goods"
@close="enableShare = false" />
<popups v-model="popupsSwitch" @tapPopup="handleNavbarList" :popData="navbarListData" :x="navbarListX"
:y="navbarListY" placement="top-start" />
<view class="index">
<!-- topBar -->
<u-navbar
@@ -25,17 +23,8 @@
:bg-color="navbar.background"
:auto-back="false"
: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>
<view class="headerList" :class="headerFlag ? 'tab-bar' : 'tab-bar scroll-hide'">
<view class="headerRow headerRow--tabs">
@@ -62,17 +51,8 @@
class="header-only-back"
:bg-color="navbarOnlyBack.background"
: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>
</view>
@@ -262,7 +242,13 @@
<view class="detail-btn" v-if="!isGroup && !takeDownFromSale">
<view class="to-store-car to-store-btn" v-if="showAddToCart" @click="shutMask(4)">
加入购物车</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>
<!-- 拼团结算 -->
@@ -325,13 +311,13 @@ import * as API_trade from '@/api/trade.js'
import * as API_Members from '@/api/members.js'
import * as API_store from '@/api/store.js'
import { getIMDetail } from '@/api/common'
import { modelNavigateTo } from '@/pages/tabbar/home/template/tpl.js'
/************请求存储***************/
import storage from '@/utils/storage.js'
/************工具函数***************/
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
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'
@@ -347,36 +333,17 @@ import GoodsSwiper from './product/goods/-goods-swiper'
import popupGoods from '@/components/m-buy/goods'
import popupAddress from './product/popup/address'
import shares from '@/components/m-share/index'
import popups from '@/components/popups/popups'
import takeDownFormSaleGoods from '@/components/m-take-down-sale-goods/index'
import setup from './product/popup/popup'
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const instance = getCurrentInstance()
const popupGoodsRef = ref()
const takeDownSaleRef = ref()
// 响应式数据
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 selectedGoods = ref<any>("")
const isGroup = ref(false)
@@ -437,6 +404,7 @@ const isNonPhysicalGoodsType = computed(() => isNonPhysicalGoods(goodsDetail.val
const showAddToCart = computed(() => {
return !isNonPhysicalGoodsType.value
})
const isAppointmentGoodsType = computed(() => isAppointmentGoods(goodsDetail.value?.goodsType))
// watch
watch(isGroup, (val) => {
@@ -494,10 +462,6 @@ function share() {
return `/pages/product/goods?id=${routerVal.value.id}&goodsId=${routerVal.value.goodsId}`
}
function handleNavbarList(val: any) {
modelNavigateTo({ url: val })
}
function emptyPromotion(): boolean {
if (PromotionList.value == "" || PromotionList.value == null || PromotionList.value == []) {
return true
@@ -637,13 +601,11 @@ function cartCount() {
}
function back() {
if (getCurrentPages().length == 1) {
uni.switchTab({
url: "/pages/tabbar/home/index",
})
} else {
uni.navigateBack()
}
uni.navigateBack({
fail() {
uni.switchTab({ url: '/pages/tabbar/home/index' })
},
})
}
function getStoreBaseInfoFun(id: any) {
@@ -724,6 +686,26 @@ function getCoupon(item: any) {
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) {
promotionShow.value = false
buyMask.value = false

View File

@@ -46,6 +46,10 @@ page {
> .to-buy {
background-image: linear-gradient(135deg, #ffba0d, #ffc30d 69%, #ffcf0d);
}
> .to-buy-theme {
background-image: none;
background: var(--theme-primary, #ff3c2a);
}
> .to-store-car {
background-image: linear-gradient(135deg, #f2140c, #f2270c 70%, #f24d0c);
}
@@ -201,7 +205,7 @@ page {
.header-only-back {
background: transparent;
z-index: 9;
z-index: 12 !important;
}
.header,
.header-only-back {
@@ -332,26 +336,10 @@ page {
padding: 0;
}
.nav-action + .nav-action {
border-left: 2rpx solid rgb(194, 194, 194);
}
.backs,
.bg-back {
width: 150rpx;
border: 2rpx solid #e8e8e8;
border-radius: 100px;
display: flex;
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 {

View File

@@ -234,7 +234,7 @@ import {
getStoreIsCollect,
} from '@/api/members.js'
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 { getFloorStoreData } from '@/api/home'
import {
@@ -349,6 +349,33 @@ function fallbackToBasePage() {
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() {
uni.navigateTo({
url: `/pages/product/licencePhoto?id=${storeId.value}`,
@@ -505,11 +532,15 @@ function getCoupon(item: any) {
}
onLoad((options: any) => {
storeId.value = options.id
console.log(storeId.value, 'this.storeId')
goodsParams.storeId = options.id
couponParams.storeId = options.id
applyStoreId(options?.id)
if (!storeId.value && options?.scene) {
resolveStoreIdFromScene(options.scene).then((id) => {
if (!storeId.value && id) {
applyStoreId(id)
init()
}
})
}
})
onPageScroll((e) => {
@@ -531,7 +562,7 @@ onMounted(() => {
withShareTicket: true,
})
// #endif
init()
if (storeId.value) init()
})
</script>

View File

@@ -32,37 +32,46 @@
<view class="point-value">{{ footNum || 0 }}</view>
</view>
</view>
<!-- 我的订单,代付款 -->
<!-- 我的订单 -->
<view class="order">
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=1')">
<view class="bag bag2">
<u-icon name="bag-fill" size="22" color="#fff"></u-icon>
<view class="order-head">
<text class="order-head-title">我的订单</text>
<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 class="order-item-label">待付款</view>
</view>
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=3')">
<view class="bag bag3">
<u-icon name="car-fill" size="22" color="#fff"></u-icon>
<view class="order-row">
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=1')">
<view class="bag bag2">
<u-icon name="bag-fill" size="22" color="#fff"></u-icon>
</view>
<view class="order-item-label">待支付</view>
</view>
<view class="order-item-label">待收货</view>
</view>
<view class="order-item" @click="navigateTo('/pages/order/evaluate/myEvaluate')">
<view class="bag bag4">
<u-icon name="star-fill" size="22" color="#fff"></u-icon>
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=2')">
<view class="bag bag1">
<u-icon name="gift-fill" size="22" color="#fff"></u-icon>
</view>
<view class="order-item-label">待发货</view>
</view>
<view class="order-item-label">待评价</view>
</view>
<view class="order-item" @click="navigateTo('/pages/order/afterSales/afterSales')">
<view class="bag bag5">
<u-icon name="server-fill" size="22" color="#fff"></u-icon>
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=3')">
<view class="bag bag3">
<u-icon name="car-fill" size="22" color="#fff"></u-icon>
</view>
<view class="order-item-label">待收货</view>
</view>
<view class="order-item-label">售后</view>
</view>
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=0')">
<view class="bag bag1">
<u-icon name="order" size="22" color="#fff"></u-icon>
<view class="order-item" @click="navigateTo('/pages/order/myOrder?status=4')">
<view class="bag bag4">
<u-icon name="star-fill" size="22" color="#fff"></u-icon>
</view>
<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 class="order-item-label">我的订单</view>
</view>
</view>
</div>
@@ -257,12 +266,34 @@ body {
}
.order {
height: 140rpx;
font-size: $font-sm;
display: flex;
align-items: center;
padding: 0 3%;
color: #999;
padding: 8rpx 16rpx 24rpx;
color: #333;
.order-head {
display: flex;
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 {
flex: 1;
@@ -274,9 +305,10 @@ body {
}
.order-item-label {
margin-top: 10rpx;
margin-top: 12rpx;
line-height: 1.4;
font-size: $font-sm;
font-size: 24rpx;
color: #333;
}
}
}

View File

@@ -31,6 +31,66 @@ 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
}
@@ -55,9 +115,22 @@ export function hasAppointmentInCheckout(orderMessage) {
export function parseServiceModes(ext) {
if (!ext) return [SERVICE_MODE.IN_STORE]
const raw = ext.serviceModes
if (Array.isArray(raw) && raw.length) return raw
if (Array.isArray(raw) && raw.length) {
return raw.map(String).filter(Boolean)
}
if (typeof raw === 'string' && raw.trim()) {
return raw.split(',').map((s) => s.trim()).filter(Boolean)
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]
}
@@ -85,28 +158,296 @@ export function formatDate(value) {
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 = Number(ext?.bookingDateRangeDays) || days
const list = []
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 d = new Date(today)
d.setDate(today.getDate() + i)
list.push(formatDate(d))
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 function buildCancelNotice(ext) {
if (!ext) return '请按时到店/等候服务;具体取消规则以商家设置为准。'
const mode = ext.cancelMode
const hours = ext.cancelHours
if (mode === 'NOT_ALLOWED') return '预约成功后不可取消,请谨慎下单。'
if (mode === 'ALLOW_BEFORE_HOURS' && hours) {
return `服务开始前 ${hours} 小时可免费取消,逾期可能无法退款。`
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,
}
return '请按时到店/等候服务;具体取消规则以商家设置为准。'
}
/** 以 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) {
@@ -224,6 +565,64 @@ export function validateTextContent(field, value) {
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)
@@ -268,6 +667,14 @@ export function parseFormAnswerGroups(formAnswerJson) {
.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)) {
@@ -282,7 +689,6 @@ export function getAppointmentAllowOperation(order) {
rog: false,
showLogistics: false,
afterSale: paid && order.orderStatus !== 'CANCELLED',
reschedule: paid && ['PAID', 'TAKE'].includes(order.orderStatus),
}
}

View File

@@ -73,15 +73,23 @@ export function setClipboard (val) {
// #endif
// #ifndef H5
uni.setClipboardData({
data: val,
const text = val == null ? "" : String(val);
const clipboard = typeof wx !== "undefined" && wx.setClipboardData ? wx : uni;
clipboard.setClipboardData({
data: text,
success: function () {
uni.showToast({
title: "复制成功!",
title: "复制成功",
duration: 2000,
icon: "none",
});
},
fail: function () {
uni.showToast({
title: "复制失败,请重试",
icon: "none",
});
},
});
// #endif
}

View File

@@ -19,6 +19,8 @@ import {
isAppointmentGoods,
isAppointmentOrder,
hasAppointmentInCheckout,
getAppointmentStock,
getAppointmentMaxBuyNum,
} from './appointmentGoods.js'
export {
@@ -27,6 +29,8 @@ export {
isAppointmentGoods,
isAppointmentOrder,
hasAppointmentInCheckout,
getAppointmentStock,
getAppointmentMaxBuyNum,
}
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */

View File

@@ -67,15 +67,20 @@ export function getThemeStyle(theme = DEFAULT_THEME) {
const safeTheme = theme || DEFAULT_THEME;
const primary = safeTheme.mainColor || DEFAULT_THEME.mainColor;
const light = safeTheme.lightColor || DEFAULT_THEME.lightColor;
const primarySoft = hexToRgba(primary, 0.1);
return {
"--theme-primary": primary,
"--theme-light": light,
"--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-30": hexToRgba(primary, 0.3),
"--theme-primary-70": hexToRgba(primary, 0.7),
"--theme-light-10": hexToRgba(light, 0.1),
"--theme-light-80": hexToRgba(light, 0.8),
"--u-primary": primary,
"--up-primary": primary,
"--u-primary-light": primarySoft,
"--up-primary-light": primarySoft,
};
}