+
- 礼品卡
+
+
+ 使用礼品卡
+ 使用说明
+
+
-¥{{ unitPrice(giftCardDeductAmount) }}
- {{ giftCardList.length }} 张可用已抵扣 -¥{{ unitPrice(giftCardDeductAmount) }}
+ {{ giftCardList.length }} 张可用
-
+ 暂无可用礼品卡
+
- {{ item.giftCardName || '礼品卡' }}
-
- 余额 ¥{{ unitPrice(item.balance) }}
- {{ item.cardNo }}
+
+
+
+ ¥{{ unitPrice(item.balance) }}
+ 余额
+
+ {{ item.cardNo }}
+
+ 已选
@@ -428,7 +453,10 @@ import {
secrecyMobile,
isLogin,
} from '@/utils/filters.js'
-import { hasECouponInCheckout } from '@/utils/goodsType.js'
+import { hasECouponInCheckout, hasAppointmentInCheckout } from '@/utils/goodsType.js'
+import { SERVICE_MODE_LABEL, formatDate } from '@/utils/appointmentGoods.js'
+import { bindCheckout } from '@/api/appointment.js'
+import { decorateGiftCard, GIFT_CARD_PAY_NOTICE } from '@/utils/giftCard.js'
const store = useStore()
const { proxy } = getCurrentInstance()!
@@ -474,12 +502,32 @@ const originOrderData = ref
('')
// E_COUPON 结算:免地址/免配送(hidePhysicalCheckout);优惠券/活动/礼品卡可用(M-02)
const isECouponCheckout = computed(() => hasECouponInCheckout(orderMessage.value))
+const isAppointmentCheckout = computed(() => hasAppointmentInCheckout(orderMessage.value))
const isVirtualCheckout = computed(() => orderMessage.value?.cartTypeEnum === 'VIRTUAL')
const hidePhysicalCheckout = computed(
- () => isVirtualCheckout.value || isECouponCheckout.value
+ () => isVirtualCheckout.value || isECouponCheckout.value || isAppointmentCheckout.value
)
+const appointmentCheckoutContext = ref(null)
-const giftCardList = computed(() => orderMessage.value?.canUseGiftCards || [])
+const appointmentCheckoutSummary = computed(() => {
+ const ctx =
+ appointmentCheckoutContext.value ||
+ orderMessage.value?.appointmentCheckout ||
+ orderMessage.value?.appointmentContext
+ if (!ctx) return ''
+ const date = formatDate(ctx.slotDate)
+ const mode = SERVICE_MODE_LABEL[ctx.serviceMode] || ctx.serviceMode || ''
+ const slot = ctx.slotStartTime && ctx.slotEndTime ? `${ctx.slotStartTime}-${ctx.slotEndTime}` : ''
+ const contact = ctx.contactName ? `${ctx.contactName} ${ctx.contactPhone || ''}` : ''
+ return [date, slot, mode, contact].filter(Boolean).join(' · ')
+})
+
+const selectedGiftCardIds = computed(() => orderMessage.value?.selectedGiftCardIds || [])
+const giftCardList = computed(() =>
+ (orderMessage.value?.canUseGiftCards || []).map((item: any) =>
+ decorateGiftCard(item, selectedGiftCardIds.value)
+ )
+)
const giftCardDeductAmount = computed(() => {
const dto = orderMessage.value?.priceDetailDTO
if (!dto) return 0
@@ -488,8 +536,6 @@ const giftCardDeductAmount = computed(() => {
}
return Number(dto.giftCardPrice) || 0
})
-const selectedGiftCardIds = computed(() => orderMessage.value?.selectedGiftCardIds || [])
-const giftCardExpanded = ref(false)
watch(
remarkVal,
@@ -527,9 +573,10 @@ onShow(async () => {
mask: true,
})
try {
+ appointmentCheckoutContext.value = uni.getStorageSync('appointment_checkout_context') || null
await getOrderList()
- // E_COUPON 不需要配送方式
- if (!isECouponCheckout.value) {
+ // E_COUPON / 预约商品不需要配送方式
+ if (!isECouponCheckout.value && !isAppointmentCheckout.value) {
await getDistribution()
}
if (routerVal.value.way == 'PINTUAN') {
@@ -605,6 +652,15 @@ 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))
@@ -717,36 +773,62 @@ function createTradeFun() {
delete submit.parentOrderSn
}
- API_Trade.createTrade(submit).then((res) => {
- if (res.data.success) {
- uni.showToast({
- title: '创建订单成功!',
- duration: 2000,
- icon: 'none',
- })
- if (orderMessage.value.priceDetailDTO.billPrice == 0) {
- uni.navigateTo({
- url: '/pages/order/myOrder?status=0',
+ const runCreate = () =>
+ API_Trade.createTrade(submit).then((res) => {
+ if (res.data.success) {
+ uni.removeStorageSync('appointment_checkout_context')
+ uni.showToast({
+ title: '创建订单成功!',
+ duration: 2000,
+ icon: 'none',
})
- } else {
- // #ifdef MP-WEIXIN
- pay(res.data.result.sn)
- // #endif
+ if (orderMessage.value.priceDetailDTO.billPrice == 0) {
+ uni.navigateTo({
+ 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
+ // #ifndef MP-WEIXIN
+ navigateTo(
+ `/pages/cart/payment/payOrder?trade_sn=${res.data.result.sn}`
+ )
+ // #endif
+ }
+ } else {
+ uni.showToast({
+ title: res.data.message,
+ duration: 2000,
+ icon: 'none',
+ })
}
- } else {
+ })
+
+ if (isAppointmentCheckout.value) {
+ const ctx =
+ appointmentCheckoutContext.value ||
+ uni.getStorageSync('appointment_checkout_context')
+ if (!ctx) {
uni.showToast({
- title: res.data.message,
- duration: 2000,
+ title: '预约信息不完整,请返回重选时段',
icon: 'none',
})
+ return
}
- })
+ bindCheckout(ctx)
+ .then(runCreate)
+ .catch((e: any) => {
+ uni.showToast({
+ title: e?.data?.message || e?.message || '预约信息提交失败',
+ icon: 'none',
+ })
+ })
+ return
+ }
+
+ runCreate()
}, 3000)
}
@@ -861,8 +943,8 @@ function getOrderList() {
;(store.state as any).canUseCoupons = res.data.result.canUseCoupons
;(store.state as any).cantUseCoupons = res.data.result.cantUseCoupons
- // E_COUPON 免地址,不自动回填 memberAddress
- if (!hasECouponInCheckout(res.data.result)) {
+ // E_COUPON / 预约商品免地址,不自动回填 memberAddress
+ if (!hasECouponInCheckout(res.data.result) && !hasAppointmentInCheckout(res.data.result)) {
if (!res.data.result.memberAddress) {
getUserAddress()
} else {
@@ -894,6 +976,22 @@ page {
}
diff --git a/pages/order/myOrder.vue b/pages/order/myOrder.vue
index 2a29f92..3c8d7fc 100644
--- a/pages/order/myOrder.vue
+++ b/pages/order/myOrder.vue
@@ -62,7 +62,7 @@
v-if="order.allowOperationVO.pay" @click="waitPay(order)">立即付款
+ v-if="showOrderCancel(order)" @click="onCancel(order.sn)">
取消订单
@@ -71,7 +71,7 @@
确认收货
退款/售后
@@ -123,7 +123,8 @@ import {
orderStatusList,
} from '@/utils/filters.js'
// E_COUPON 订单不展示确认收货、退款/售后(FR-B-04)
-import { isECouponOrder } from '@/utils/goodsType.js'
+import { isECouponOrder, isAppointmentOrder } from '@/utils/goodsType.js'
+import { getAppointmentAllowOperation } from '@/utils/appointmentGoods.js'
const store = useStore()
const { proxy } = getCurrentInstance()!
@@ -371,6 +372,23 @@ function orderStateExp(state: number) {
return { stateTip, stateTipColor }
}
+function showOrderCancel(order: any) {
+ if (isAppointmentOrder(order?.orderType)) {
+ return !!getAppointmentAllowOperation(order)?.cancel
+ }
+ return !!order?.allowOperationVO?.cancel
+}
+
+function showOrderAfterSale(order: any) {
+ if (isECouponOrder(order?.orderType)) return false
+ if (isAppointmentOrder(order?.orderType)) {
+ const allow = getAppointmentAllowOperation(order)
+ if (!allow?.afterSale) return false
+ }
+ const status = order?.groupAfterSaleStatus
+ return status && (status.includes('NOT_APPLIED') || status.includes('PART_AFTER_SALE'))
+}
+
function navigateToOrderDetail(sn: string) {
uni.navigateTo({
url: './orderDetail?sn=' + sn,
diff --git a/pages/order/orderDetail.vue b/pages/order/orderDetail.vue
index b5020da..d3e9b19 100644
--- a/pages/order/orderDetail.vue
+++ b/pages/order/orderDetail.vue
@@ -11,7 +11,55 @@
-
+
+
+ 预约时间
+ {{ appointmentSlotText }}
+
+
+ 服务方式
+ {{ serviceModeLabel }}
+
+
+ 联系人
+ {{ appointmentDetail.contactName }} {{ appointmentDetail.contactPhone }}
+
+
+ 服务地址
+ {{ appointmentDetail.serviceAddressSnapshot }}
+
+
+ {{ appointmentFormGroups.length > 1 ? `补充信息${gi + 1}` : '补充信息' }}
+
+
+ {{ row.fieldLabel }}:{{ row.value }}
+
+
+
+
+ 核销码({{ appointmentCodes.length }})
+
+ {{ code.seqNo ? '#' + code.seqNo + ' ' : '' }}{{ code.code }}
+ ({{ verificationStatusText(code.status) }})
+
+
+
+ 申请改期
+
+
+
查看卡密
@@ -92,7 +140,7 @@
¥{{unitPrice(sku.goodsPrice) }}
-
+
{{refundPriceList(sku.isRefund)}} ({{unitPrice(sku.refundPrice, "¥") }})
@@ -112,7 +160,7 @@
商品总价:
¥{{unitPrice(order.goodsPrice) }}
-
+
运费:
¥{{unitPrice(order.freightPrice) }}
@@ -142,7 +190,7 @@
取消订单
查看物流
store.getters.lightColor)
@@ -319,7 +376,30 @@ const orderPackage = ref([])
const legacyTraces = ref(null)
const isECouponOrder = computed(() => checkECouponOrder(order.value?.orderType))
+const isAppointmentOrderFlag = computed(() => checkAppointmentOrder(order.value?.orderType))
const isNonPhysicalOrder = computed(() => checkNonPhysicalOrder(order.value?.orderType))
+const appointmentDetail = ref>({})
+const appointmentAllow = computed(() =>
+ getAppointmentAllowOperation(order.value) || { cancel: false, afterSale: false, reschedule: false }
+)
+const showCancelBtn = computed(() => {
+ if (isAppointmentOrderFlag.value) return appointmentAllow.value.cancel
+ return orderDetail.value.allowOperationVO?.cancel === true
+})
+const appointmentCodes = computed(() => appointmentDetail.value?.codes || [])
+const serviceModeLabel = computed(
+ () => SERVICE_MODE_LABEL[appointmentDetail.value?.serviceMode] || appointmentDetail.value?.serviceMode || ''
+)
+const appointmentSlotText = computed(() => {
+ const d = appointmentDetail.value
+ if (!d?.slotDate) return '—'
+ const date = formatDate(d.slotDate)
+ const slot = d.slotStartTime && d.slotEndTime ? `${d.slotStartTime}-${d.slotEndTime}` : ''
+ return [date, slot].filter(Boolean).join(' ')
+})
+const appointmentFormGroups = computed(() =>
+ parseFormAnswerGroups(appointmentDetail.value?.formAnswerJson)
+)
const ecouponCardKeyDelivered = computed(() =>
orderHasCardKeyContent(orderGoodsList.value)
)
@@ -335,6 +415,7 @@ const allowOperation = computed(() =>
)
const showDeliveryInfoBlock = computed(() => {
+ if (isAppointmentOrderFlag.value) return true
if (isECouponOrder.value) return true
if (order.value.orderType === 'VIRTUAL') return false
if (order.value.deliveryMethod === 'SELF_PICK_UP') return false
@@ -444,6 +525,29 @@ function ByUserMessage(orderItem: any) {
})
}
+function verificationStatusText(status: string) {
+ return VERIFICATION_CODE_STATUS_TEXT[status] || status || '—'
+}
+
+async function loadAppointmentDetail(orderSnParam: string) {
+ if (!checkAppointmentOrder(order.value?.orderType)) {
+ appointmentDetail.value = {}
+ return
+ }
+ try {
+ const res = await getAppointmentOrderDetail(orderSnParam)
+ appointmentDetail.value = res.data?.result || {}
+ } catch {
+ appointmentDetail.value = {}
+ }
+}
+
+function onRescheduleStub() {
+ uni.navigateTo({
+ url: `/pages/order/appointment/appointmentReschedule?orderSn=${order.value.sn}`,
+ })
+}
+
function loadData(orderSnParam: string) {
uni.showLoading({ title: '加载中' })
getOrderDetail(orderSnParam).then(async (res) => {
@@ -454,6 +558,7 @@ function loadData(orderSnParam: string) {
if (!order.value.allowOperationVO && result.allowOperationVO) {
order.value.allowOperationVO = result.allowOperationVO
}
+ await loadAppointmentDetail(orderSnParam)
await loadDelivery(order.value)
hideLoadingIfNeeded()
})
@@ -774,6 +879,39 @@ page,
}
}
+.appointment-entry__row {
+ display: flex;
+ font-size: 26rpx;
+ margin-bottom: 12rpx;
+ line-height: 1.5;
+}
+.appointment-entry__row .label {
+ color: #999;
+ width: 140rpx;
+ flex-shrink: 0;
+}
+.form-answers {
+ flex: 1;
+}
+.form-answer-row {
+ margin-bottom: 4rpx;
+}
+.appointment-codes__title {
+ font-size: 26rpx;
+ font-weight: 600;
+ margin: 16rpx 0 8rpx;
+}
+.appointment-code-item {
+ margin-bottom: 8rpx;
+}
+.code-status {
+ color: #999;
+ font-size: 22rpx;
+}
+.reschedule-stub {
+ margin-top: 16rpx;
+}
+
.verificationCode {
font-weight: bold;
letter-spacing: 2rpx;
diff --git a/pages/tabbar/user/utils/tool.vue b/pages/tabbar/user/utils/tool.vue
index 716b764..241a195 100644
--- a/pages/tabbar/user/utils/tool.vue
+++ b/pages/tabbar/user/utils/tool.vue
@@ -53,6 +53,10 @@
优惠券
+
+
+ 礼品卡
+
diff --git a/utils/appointmentGoods.js b/utils/appointmentGoods.js
new file mode 100644
index 0000000..e25bf39
--- /dev/null
+++ b/utils/appointmentGoods.js
@@ -0,0 +1,297 @@
+/**
+ * 预约商品(APPOINTMENT_GOODS)买家端工具
+ * 规则:禁加购、仅 BUY_NOW、免物流地址、支持买家售后(含部分数量退款)
+ */
+export const APPOINTMENT_GOODS_TYPE = 'APPOINTMENT_GOODS'
+export const APPOINTMENT_ORDER_TYPE = 'APPOINTMENT'
+
+export const SERVICE_MODE = {
+ IN_STORE: 'IN_STORE',
+ ON_SITE: 'ON_SITE',
+}
+
+export const SERVICE_MODE_LABEL = {
+ IN_STORE: '到店服务',
+ ON_SITE: '上门服务',
+}
+
+export const VERIFICATION_CODE_STATUS = {
+ UNUSED: 'UNUSED',
+ USED: 'USED',
+ INVALID: 'INVALID',
+}
+
+export const VERIFICATION_CODE_STATUS_TEXT = {
+ UNUSED: '待核销',
+ USED: '已核销',
+ INVALID: '已失效',
+}
+
+export function isAppointmentGoods(goodsType) {
+ return goodsType === APPOINTMENT_GOODS_TYPE
+}
+
+export function isAppointmentOrder(orderType) {
+ return orderType === APPOINTMENT_ORDER_TYPE
+}
+
+/**
+ * 结算页识别当前交易是否含预约 SKU(免地址/免配送)
+ */
+export function hasAppointmentInCheckout(orderMessage) {
+ if (!orderMessage) return false
+ const checked = orderMessage.checkedSkuList || []
+ if (checked.some((item) => isAppointmentGoods(item?.goodsSku?.goodsType))) {
+ return true
+ }
+ const cartList = orderMessage.cartList || []
+ return cartList.some((cart) =>
+ (cart.checkedSkuList || []).some((item) =>
+ isAppointmentGoods(item?.goodsSku?.goodsType)
+ )
+ )
+}
+
+export function parseServiceModes(ext) {
+ if (!ext) return [SERVICE_MODE.IN_STORE]
+ const raw = ext.serviceModes
+ if (Array.isArray(raw) && raw.length) return raw
+ if (typeof raw === 'string' && raw.trim()) {
+ return raw.split(',').map((s) => s.trim()).filter(Boolean)
+ }
+ return [SERVICE_MODE.IN_STORE]
+}
+
+export function formatSlotLabel(slot) {
+ if (!slot) return ''
+ const date = formatDate(slot.slotDate)
+ const start = slot.slotStartTime || ''
+ const end = slot.slotEndTime || ''
+ const qty =
+ slot.showRemainingQty && slot.availableQty != null
+ ? `(余${slot.availableQty})`
+ : ''
+ return `${date} ${start}-${end}${qty}`
+}
+
+export function formatDate(value) {
+ if (!value) return ''
+ if (typeof value === 'string') return value.slice(0, 10)
+ const d = new Date(value)
+ if (Number.isNaN(d.getTime())) return ''
+ const y = d.getFullYear()
+ const m = String(d.getMonth() + 1).padStart(2, '0')
+ const day = String(d.getDate()).padStart(2, '0')
+ return `${y}-${m}-${day}`
+}
+
+export function buildBookableDates(ext, days = 30) {
+ const rangeDays = Number(ext?.bookingDateRangeDays) || days
+ const list = []
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+ for (let i = 0; i < rangeDays; i++) {
+ const d = new Date(today)
+ d.setDate(today.getDate() + i)
+ list.push(formatDate(d))
+ }
+ return list
+}
+
+export function buildCancelNotice(ext) {
+ if (!ext) return '请按时到店/等候服务;具体取消规则以商家设置为准。'
+ const mode = ext.cancelMode
+ const hours = ext.cancelHours
+ if (mode === 'NOT_ALLOWED') return '预约成功后不可取消,请谨慎下单。'
+ if (mode === 'ALLOW_BEFORE_HOURS' && hours) {
+ return `服务开始前 ${hours} 小时可免费取消,逾期可能无法退款。`
+ }
+ return '请按时到店/等候服务;具体取消规则以商家设置为准。'
+}
+
+export function parseFormFields(schemaJson) {
+ if (!schemaJson) return []
+ let data = schemaJson
+ if (typeof data === 'string') {
+ try {
+ data = JSON.parse(data)
+ } catch {
+ return []
+ }
+ }
+ const list = Array.isArray(data) ? data : data?.fields || []
+ return list.map((field, index) => {
+ const fieldType = String(field.field_type || field.fieldType || field.type || 'TEXT')
+ .toUpperCase()
+ .replace(/-/g, '_')
+ const options = Array.isArray(field.options)
+ ? field.options.map((item, oi) =>
+ typeof item === 'string'
+ ? { label: item, value: item }
+ : {
+ label: item?.label || item?.name || `选项${oi + 1}`,
+ value: item?.value || item?.label || `opt_${oi}`,
+ }
+ )
+ : []
+ return {
+ fieldKey: field.field_key || field.fieldKey || field.id || field.key || `field_${index}`,
+ fieldLabel: field.field_label || field.fieldLabel || field.label || field.title || `字段${index + 1}`,
+ fieldType,
+ required: !!field.required,
+ hint: field.hint_text || field.hintText || field.hint || field.placeholder || '请输入',
+ richText: fieldType === 'RICH_TEXT',
+ richTextContent: field.rich_text_content || field.richTextContent || field.content || '',
+ options,
+ optionLabels: options.map((item) => item.label),
+ contentType: field.content_type || field.contentType || 'TEXT',
+ defaultValue: field.default_value || field.defaultValue || '',
+ cityLevel: field.city_level || field.cityLevel || '',
+ defaultVisibility: field.default_visibility || field.defaultVisibility || '',
+ defaultDateMode: field.default_date_mode || field.defaultDateMode || '',
+ defaultDate: field.default_date || field.defaultDate || '',
+ defaultDateStart: field.default_date_start || field.defaultDateStart || '',
+ defaultDateEnd: field.default_date_end || field.defaultDateEnd || '',
+ defaultTimeMode: field.default_time_mode || field.defaultTimeMode || '',
+ defaultTime: field.default_time || field.defaultTime || '',
+ defaultTimeStart: field.default_time_start || field.defaultTimeStart || '',
+ defaultTimeEnd: field.default_time_end || field.defaultTimeEnd || '',
+ maxUploadCount: Number(field.max_upload_count || field.maxUploadCount) || 1,
+ }
+ })
+}
+
+function pad2(n) {
+ return String(n).padStart(2, '0')
+}
+
+function currentTime() {
+ const d = new Date()
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`
+}
+
+export function defaultFormValue(field) {
+ const type = field?.fieldType
+ const opts = field?.options || []
+ if (type === 'RADIO' || type === 'SELECT') {
+ return opts[0]?.value || ''
+ }
+ if (type === 'CHECKBOX' || type === 'IMAGE') return []
+ if (type === 'TEXT') return field.defaultValue || ''
+ if (type === 'DATE' && field.defaultVisibility === 'SHOW') {
+ return field.defaultDateMode === 'SPECIFIED_DATE' ? field.defaultDate || '' : formatDate(new Date())
+ }
+ if (type === 'DATE_RANGE' && field.defaultVisibility === 'SHOW') {
+ if (field.defaultDateMode === 'SPECIFIED_DATE') {
+ return [field.defaultDateStart, field.defaultDateEnd].filter(Boolean).join('~')
+ }
+ const today = formatDate(new Date())
+ return `${today}~${today}`
+ }
+ if (type === 'TIME' && field.defaultVisibility === 'SHOW') {
+ return field.defaultTimeMode === 'SPECIFIED_TIME' ? field.defaultTime || '' : currentTime()
+ }
+ if (type === 'TIME_RANGE' && field.defaultVisibility === 'SHOW') {
+ if (field.defaultTimeMode === 'SPECIFIED_TIME') {
+ return [field.defaultTimeStart, field.defaultTimeEnd].filter(Boolean).join('~')
+ }
+ const now = currentTime()
+ return `${now}~${now}`
+ }
+ return ''
+}
+
+export function isBlankFormValue(value) {
+ if (value == null) return true
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text || text === '~') return true
+ if (text.startsWith('~') || text.endsWith('~')) return true
+ return false
+ }
+ if (Array.isArray(value)) return !value.length
+ return false
+}
+
+export function validateTextContent(field, value) {
+ if (isBlankFormValue(value)) return true
+ const type = field?.contentType || 'TEXT'
+ const text = String(value).trim()
+ if (type === 'MOBILE') return /^1\d{10}$/.test(text)
+ if (type === 'ID_CARD') return /(^\d{15}$)|(^\d{17}[\dXx]$)/.test(text)
+ if (type === 'EMAIL') return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)
+ if (type === 'NUMBER') return /^-?\d+(\.\d+)?$/.test(text)
+ return true
+}
+
+export function buildFormGroups(ext, quantity) {
+ const fields = parseFormFields(ext?.systemFormSnapshot)
+ const fillable = fields.filter((item) => !item.richText)
+ if (!ext?.systemFormId && !fillable.length) return []
+ const mode = ext?.formSubmitMode || 'PER_APPOINTMENT'
+ const count = mode === 'PER_ORDER' ? 1 : Math.max(Number(quantity) || 1, 1)
+ return Array.from({ length: count }, () => {
+ const group = {}
+ fillable.forEach((field) => {
+ group[field.fieldKey] = defaultFormValue(field)
+ })
+ return group
+ })
+}
+
+export function parseFormAnswerGroups(formAnswerJson) {
+ if (!formAnswerJson) return []
+ let data = formAnswerJson
+ if (typeof data === 'string') {
+ try {
+ data = JSON.parse(data)
+ } catch {
+ return []
+ }
+ }
+ const fields = Array.isArray(data?.fields) ? data.fields : []
+ const answers = Array.isArray(data?.answers) ? data.answers : []
+ return answers
+ .map((group) => {
+ const map = group && typeof group === 'object' && !Array.isArray(group) ? group : {}
+ const source = fields.length
+ ? fields
+ : Object.keys(map).map((key) => ({ fieldKey: key, fieldLabel: key }))
+ return source
+ .map((field) => ({
+ fieldKey: field.fieldKey,
+ fieldLabel: field.fieldLabel || field.fieldKey,
+ value: map[field.fieldKey] ?? '',
+ }))
+ .filter((row) => row.value !== '' && row.value != null)
+ })
+ .filter((rows) => rows.length)
+}
+
+/** 预约单:未付款仅可取消;已付款仅可售后 */
+export function getAppointmentAllowOperation(order) {
+ if (!order || !isAppointmentOrder(order.orderType)) {
+ return null
+ }
+ const unpaid =
+ order.orderStatus === 'UNPAID' && order.payStatus === 'UNPAID'
+ const paid = order.payStatus === 'PAID'
+ return {
+ cancel: unpaid,
+ pay: unpaid,
+ rog: false,
+ showLogistics: false,
+ afterSale: paid && order.orderStatus !== 'CANCELLED',
+ reschedule: paid && ['PAID', 'TAKE'].includes(order.orderStatus),
+ }
+}
+
+export function computePartialRefundPrice(applyInfo, num) {
+ const totalNum = Number(applyInfo?.totalNum || applyInfo?.orderItemNum) || 0
+ const totalPrice = Number(applyInfo?.applyRefundPrice) || 0
+ const qty = Number(num) || 1
+ if (!totalNum || totalNum <= 0) return totalPrice
+ if (qty >= totalNum) return totalPrice
+ const unit = totalPrice / totalNum
+ return Math.round(unit * qty * 100) / 100
+}
diff --git a/utils/giftCard.js b/utils/giftCard.js
new file mode 100644
index 0000000..a630a19
--- /dev/null
+++ b/utils/giftCard.js
@@ -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 '不可用'
+}
diff --git a/utils/goodsType.js b/utils/goodsType.js
index 649363d..9eebcc5 100644
--- a/utils/goodsType.js
+++ b/utils/goodsType.js
@@ -13,6 +13,22 @@ export const E_COUPON_ORDER_TYPE = 'E_COUPON'
export const VIRTUAL_GOODS_TYPE = 'VIRTUAL_GOODS'
export const VIRTUAL_ORDER_TYPE = 'VIRTUAL'
+import {
+ APPOINTMENT_GOODS_TYPE,
+ APPOINTMENT_ORDER_TYPE,
+ isAppointmentGoods,
+ isAppointmentOrder,
+ hasAppointmentInCheckout,
+} from './appointmentGoods.js'
+
+export {
+ APPOINTMENT_GOODS_TYPE,
+ APPOINTMENT_ORDER_TYPE,
+ isAppointmentGoods,
+ isAppointmentOrder,
+ hasAppointmentInCheckout,
+}
+
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
export const E_COUPON_MAX_BUY_NUM = 999
@@ -32,13 +48,13 @@ export function isVirtualOrder(orderType) {
return orderType === VIRTUAL_ORDER_TYPE
}
-/** 非实物:含 E_COUPON 与核销型 VIRTUAL_GOODS(详情页隐藏「送至」等) */
+/** 非实物:含 E_COUPON、预约商品与核销型 VIRTUAL_GOODS(详情页隐藏「送至」等) */
export function isNonPhysicalGoods(goodsType) {
- return isECoupon(goodsType) || isVirtualGoods(goodsType)
+ return isECoupon(goodsType) || isVirtualGoods(goodsType) || isAppointmentGoods(goodsType)
}
export function isNonPhysicalOrder(orderType) {
- return isECouponOrder(orderType) || isVirtualOrder(orderType)
+ return isECouponOrder(orderType) || isVirtualOrder(orderType) || isAppointmentOrder(orderType)
}
/** 可售库存:E_COUPON 读 SKU quantity(由卡池 syncSkuStock 同步) */