This commit is contained in:
dzc committed 2026-04-13 14:46:06 +08:00
commit f04663c2c1
8 files changed
+500 -21

No files matched your search

+13 -2
View File
@@ -12,8 +12,14 @@
mode="aspectFill" />
<view class="ml-18rpx min-w-0">
<view class="sub-text-32 font-700 truncate">{{ state.row.merchantName || "—" }}</view>
<view v-if="state.row.createTime" class="second-text-22 color-#999 mt-6rpx">
{{ state.row.createTime }}
<view class="second-text-22 color-#666 mt-8rpx">
下单时间:{{ formatOrderTimeLabel(state.row.createTime) }}
</view>
<view class="second-text-24 color-#333 mt-8rpx font-600">
订单类型:{{ expectDeliveryTypeLabel(state.row.expectDeliveryType) }}
</view>
<view class="second-text-22 color-#1879ff mt-6rpx leading-relaxed">
{{ deliveryExpectDetailLine(state.row) }}
</view>
</view>
</view>
@@ -124,6 +130,11 @@ import { onLoad } from "@dcloudio/uni-app"
import { hooks } from "@@/index"
import { canteenDeliveryApi } from "@@/api"
import { filePath } from "@@/utils/file"
import {
deliveryExpectDetailLine,
expectDeliveryTypeLabel,
formatOrderTimeLabel,
} from "@@/utils/canteen-delivery-display"
import { getFileUploadActionUrl, getUploadAuthHeader, parseSysFileUploadResponse } from "@@/utils/upload"
const UPLOAD_OK = 2
+128 -15
View File
@@ -1,5 +1,5 @@
<template>
<crud-layout title="骑手配送" mainIsScroll bg="/static/page/blue-bg.png">
<crud-layout title="骑手配送" mainIsScroll bg="/static/page/blue-bg.png" @scrolltolower="onReachBottom">
<view class="mx-30rpx pt-20rpx">
<scroll-view scroll-x class="tabs-scroll white-space-nowrap" :show-scrollbar="false">
<view class="flex gap-16rpx pb-20rpx">
@@ -14,8 +14,10 @@
</view>
</scroll-view>
<view v-if="state.loading" class="py-80rpx text-center second-text-26 color-#999">加载中…</view>
<view v-else-if="state.list.length === 0" class="py-80rpx text-center second-text-26 color-#999">
<view v-if="state.loading && state.list.length === 0" class="py-80rpx text-center second-text-26 color-#999">
加载中…
</view>
<view v-else-if="!state.loading && state.list.length === 0" class="py-80rpx text-center second-text-26 color-#999">
暂无配送单
</view>
<view v-else>
@@ -32,7 +34,15 @@
mode="aspectFill" />
<view class="ml-16rpx min-w-0">
<view class="sub-text-30 font-700 truncate">{{ item.merchantName || "—" }}</view>
<view v-if="item.createTime" class="second-text-22 color-#999 mt-6rpx">{{ item.createTime }}</view>
<view class="second-text-22 color-#666 mt-8rpx">
下单时间:{{ formatOrderTimeLabel(item.createTime) }}
</view>
<view class="second-text-24 color-#333 mt-6rpx font-600">
订单类型:{{ expectDeliveryTypeLabel(item.expectDeliveryType) }}
</view>
<view class="second-text-22 color-#1879ff mt-4rpx leading-relaxed">
{{ deliveryExpectDetailLine(item) }}
</view>
</view>
</view>
<view class="status-pill sub-text-24 px-14rpx py-8rpx rounded-full shrink-0" :style="statusStyle(item.status)">
@@ -83,6 +93,11 @@
</view>
</view>
</view>
<view v-if="state.list.length > 0" class="py-24rpx text-center second-text-24 color-#999">
<text v-if="state.loadingMore">加载更多…</text>
<text v-else-if="!state.hasMore">没有更多了</text>
<text v-else>上拉加载更多</text>
</view>
<view class="h-40rpx" />
</view>
</crud-layout>
@@ -96,6 +111,11 @@ import { hooks, router } from "@@/index"
import { canteenDeliveryApi } from "@@/api"
import type { CanteenDeliveryTab } from "@@/api/canteen-delivery"
import { filePath } from "@@/utils/file"
import {
deliveryExpectDetailLine,
expectDeliveryTypeLabel,
formatOrderTimeLabel,
} from "@@/utils/canteen-delivery-display"
const tabs: { key: CanteenDeliveryTab; label: string }[] = [
{ key: "pending", label: "待配送" },
@@ -108,15 +128,58 @@ const state = reactive({
tab: "pending" as CanteenDeliveryTab,
list: [] as any[],
loading: false,
loadingMore: false,
/** 每页条数(与服务端约定,可被响应里的 size 覆盖) */
size: 5,
current: 0,
pages: 0,
total: 0,
hasMore: true,
})
const isApiOk = (res: any) => res?.code === 0 || res?.ok === true
const extractList = (res: any): any[] => {
/** 分页 data:records、total、current、size、pages;兼容旧版 data 为数组 */
const extractPage = (res: any): { records: any[]; total: number; current: number; size: number; pages: number } => {
const d = res?.data
if (Array.isArray(d)) return d
if (Array.isArray(d?.records)) return d.records
return []
if (Array.isArray(d)) {
return { records: d, total: d.length, current: 1, size: d.length, pages: 1 }
}
if (!d || typeof d !== "object") {
return { records: [], total: 0, current: 1, size: state.size, pages: 0 }
}
const records = Array.isArray(d.records) ? d.records : []
return {
records,
total: Number(d.total ?? 0),
current: Number(d.current ?? 1),
size: Number(d.size ?? state.size),
pages: Number(d.pages ?? 0),
}
}
const applyPage = (page: ReturnType<typeof extractPage>, append: boolean) => {
if (append) {
state.list = [...state.list, ...page.records]
} else {
state.list = page.records
}
state.total = page.total
if (page.size > 0) state.size = page.size
state.current = page.current
state.pages = page.pages
if (append && page.records.length === 0) {
state.hasMore = false
return
}
if (state.pages > 0) {
state.hasMore = state.current < state.pages
} else if (state.total > 0) {
state.hasMore = state.list.length < state.total
} else {
state.hasMore = page.records.length >= state.size && page.records.length > 0
}
}
const statusText = (s: number) => {
@@ -142,28 +205,75 @@ const statusStyle = (s: number) => {
return { color: c, backgroundColor: `${c}18` }
}
const loadList = async () => {
const loadFirstPage = async () => {
if (state.loading) return
state.loading = true
try {
const res: any = await canteenDeliveryApi.list({ tab: state.tab })
const res: any = await canteenDeliveryApi.list({
tab: state.tab,
current: 1,
size: state.size,
})
if (!isApiOk(res)) {
state.list = []
state.hasMore = false
uni.showToast({ title: res?.msg || "加载失败", icon: "none" })
return
}
state.list = extractList(res)
applyPage(extractPage(res), false)
} catch (e: any) {
state.list = []
state.hasMore = false
uni.showToast({ title: e?.message || "加载失败", icon: "none" })
} finally {
state.loading = false
}
}
const loadNextPage = async () => {
if (state.loading || state.loadingMore || !state.hasMore) return
const next = state.current + 1
if (state.pages > 0 && next > state.pages) {
state.hasMore = false
return
}
state.loadingMore = true
try {
const res: any = await canteenDeliveryApi.list({
tab: state.tab,
current: next,
size: state.size,
})
if (!isApiOk(res)) {
uni.showToast({ title: res?.msg || "加载失败", icon: "none" })
return
}
const p = extractPage(res)
if (p.records.length === 0) {
state.hasMore = false
return
}
applyPage(p, true)
} catch (e: any) {
uni.showToast({ title: e?.message || "加载失败", icon: "none" })
} finally {
state.loadingMore = false
}
}
const onReachBottom = () => {
loadNextPage()
}
const switchTab = async (key: CanteenDeliveryTab) => {
if (state.tab === key) return
state.tab = key
await loadList()
state.list = []
state.hasMore = true
state.current = 0
state.pages = 0
state.total = 0
await loadFirstPage()
}
const goDetail = (item: any) => {
@@ -185,12 +295,15 @@ const calcTotalPrice = (item: any): number => {
}
onShow(async () => {
await loadList()
await loadFirstPage()
})
onPullDownRefresh(async () => {
await loadList()
uni.stopPullDownRefresh()
try {
await loadFirstPage()
} finally {
uni.stopPullDownRefresh()
}
})
</script>
+252 -1
View File
@@ -49,6 +49,69 @@
<view class="h-20rpx" />
<!-- 送达时间:立即送 / 预约时段(对接 HardAppTakeOutApptDto.appt) -->
<view class="mx-30rpx rounded-20rpx shadow-main p-30rpx bg-white">
<view class="sub-text-30 mb-16rpx">送达时间</view>
<view class="flex rounded-16rpx bg-#f5f6f8 p-6rpx mb-24rpx">
<view
class="flex-1 py-16rpx text-center second-text-26 rounded-12rpx"
:class="state.deliveryExpectType === 'IMMEDIATE' ? 'bg-white shadow-main sub-text-28' : 'color-#666'"
@tap="selectDeliveryMode('IMMEDIATE')">
立即送出
</view>
<view
class="flex-1 py-16rpx text-center second-text-26 rounded-12rpx"
:class="state.deliveryExpectType === 'SCHEDULED' ? 'bg-white shadow-main sub-text-28' : 'color-#666'"
@tap="selectDeliveryMode('SCHEDULED')">
预约时段
</view>
</view>
<view v-if="state.deliveryExpectType === 'SCHEDULED'">
<view class="second-text-26 color-#666 mb-12rpx">选择日期</view>
<scroll-view scroll-x class="whitespace-nowrap mb-20rpx" :show-scrollbar="false">
<view class="inline-flex gap-12rpx pb-8rpx">
<view
v-for="d in dateOptions"
:key="d.value"
class="inline-block px-22rpx py-14rpx rounded-24rpx second-text-26 border border-solid sched-date-chip"
:class="
d.value === state.schedDate
? 'border-blue bg-blue sched-date-chip--active'
: 'border-gray-200 color-#333'
"
@tap="onPickSchedDate(d.value)">
{{ d.label }}
</view>
</view>
</scroll-view>
<view class="second-text-26 color-#666 mb-12rpx">选择时段</view>
<view v-if="slotsLoading" class="second-text-26 color-#999 py-20rpx">可预约时段加载中…</view>
<template v-else>
<view v-if="slotList.length === 0" class="second-text-26 color-#999 py-20rpx">
{{ scheduledSlotsEmptyText }}
</view>
<view v-else class="flex flex-wrap gap-12rpx">
<view
v-for="s in slotList"
:key="s.start + s.end"
class="px-18rpx py-14rpx rounded-16rpx second-text-24 border border-solid min-w-160rpx text-center sched-slot-chip"
:class="
s.start === state.schedStart && s.end === state.schedEnd
? 'border-blue bg-blue sched-slot-chip--active'
: 'border-gray-200 color-#333'
"
@tap="onPickSlot(s)">
{{ s.label }}
</view>
</view>
</template>
</view>
</view>
<view class="h-20rpx" />
<!-- 订单详情(购物车明细) -->
<view class="mx-30rpx rounded-20rpx shadow-main p-30rpx bg-white">
<view class="sub-text-30 mb-20rpx">订单详情</view>
@@ -92,13 +155,70 @@
</template>
<script setup lang="ts">
import { reactive, ref } from "vue"
import { computed, reactive, ref } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app"
import { hooks, router } from "@@/index"
import { orderApi, payApi, userAddressApi } from "@@/api"
import { filePath } from "@@/utils/file"
/** 与文档 §6.3 一致:GET /hard-order/app/take-out/delivery-time-slots,与下单校验同源 */
/** 连续可选配送日:从今天起共 3 天(与接口 days 一致) */
const TAKE_OUT_SLOT_DAYS = 3
const pad2 = (n: number) => (n < 10 ? `0${n}` : `${String(n)}`)
const formatYmd = (d: Date) =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
const startOfDay = (d: Date) => {
const x = new Date(d)
x.setHours(0, 0, 0, 0)
return x
}
const dateChipLabel = (ymd: string) => {
const [y, mo, day] = ymd.split("-").map(Number)
const d = new Date(y, mo - 1, day)
const today = startOfDay(new Date())
const diff = Math.round((startOfDay(d).getTime() - today.getTime()) / 86400000)
if (diff === 0) return "今天"
if (diff === 1) return "明天"
const weeks = ["日", "一", "二", "三", "四", "五", "六"]
return `${mo}月${day}日 周${weeks[d.getDay()]}`
}
/** 下单 appt 使用 HH:mm:ss */
const normalizeTime = (t: string | undefined) => {
if (t == null || t === "") return ""
const s = String(t).trim()
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s
if (/^\d{2}:\d{2}$/.test(s)) return `${s}:00`
return s
}
const slotLabel = (start: string, end: string) => {
const hm = (t: string) => (t.length >= 5 ? t.slice(0, 5) : t)
return `${hm(start)}–${hm(end)}`
}
/** 解析 HardTakeOutDeliveryTimeOptionVO:优先 daySlots;days=1 时可能仅有顶层 slots */
const parseDeliveryTimePayload = (data: any): { expectDate: string; slots: any[] }[] => {
if (!data) return []
if (Array.isArray(data.daySlots) && data.daySlots.length) {
return data.daySlots.map((d: any) => ({
expectDate: String(d.expectDate ?? "").slice(0, 10),
slots: Array.isArray(d.slots) ? d.slots : [],
}))
}
if (Array.isArray(data.slots) && data.slots.length) {
const ed =
String(data.expectDate ?? "").slice(0, 10) || formatYmd(new Date())
return [{ expectDate: ed, slots: data.slots }]
}
return []
}
const state = reactive<any>({
merchantId: "",
carParentId: "",
@@ -110,8 +230,122 @@ const state = reactive<any>({
cartList: [],
totPrice: 0,
totCount: 0,
/** IMMEDIATE | SCHEDULED → 提交 appt.expectDeliveryType */
deliveryExpectType: "IMMEDIATE" as "IMMEDIATE" | "SCHEDULED",
schedDate: "",
schedStart: "",
schedEnd: "",
/** GET delivery-time-slots → parseDeliveryTimePayload */
deliveryDaySlots: [] as { expectDate: string; slots: any[] }[],
})
const slotsLoading = ref(false)
const scheduledSlotsEmptyText = computed(() => {
if (!state.merchantId) return "缺少商户信息,请从食堂返回重新进入"
if (!state.deliveryDaySlots?.length) return "暂无可预约时段(请确认门店已配置营业时间)"
if (!state.schedDate) return "请选择日期"
return "当天暂无可选时段,请换一天试试"
})
const dateOptions = computed(() =>
(state.deliveryDaySlots || []).map((d: { expectDate: string }) => ({
value: d.expectDate,
label: dateChipLabel(d.expectDate),
})),
)
const slotList = computed(() => {
const day = (state.deliveryDaySlots || []).find((d: any) => d.expectDate === state.schedDate)
if (!day?.slots?.length) return []
return day.slots.map((s: any) => {
const start = normalizeTime(s.expectDeliveryStart ?? s.start)
const end = normalizeTime(s.expectDeliveryEnd ?? s.end)
return { start, end, label: slotLabel(start, end) }
})
})
const ensureScheduledSelection = () => {
const days = state.deliveryDaySlots || []
const withSlots = days.find((d: any) => Array.isArray(d.slots) && d.slots.length > 0)
if (withSlots && withSlots.slots[0]) {
state.schedDate = withSlots.expectDate
const s0 = withSlots.slots[0]
state.schedStart = normalizeTime(s0.expectDeliveryStart ?? s0.start)
state.schedEnd = normalizeTime(s0.expectDeliveryEnd ?? s0.end)
return
}
state.schedDate = ""
state.schedStart = ""
state.schedEnd = ""
}
const loadDeliveryTimeSlots = async () => {
if (!state.merchantId) {
state.deliveryDaySlots = []
return
}
slotsLoading.value = true
try {
const res: any = await orderApi.takeOutDeliveryTimeSlots({
merchantId: state.merchantId,
days: TAKE_OUT_SLOT_DAYS,
})
if (res?.code === 0 && res?.data != null) {
state.deliveryDaySlots = parseDeliveryTimePayload(res.data)
} else {
state.deliveryDaySlots = []
const msg = res?.msg || "加载可预约时段失败"
uni.showToast({ title: msg, icon: "none" })
}
} catch (e: any) {
state.deliveryDaySlots = []
uni.showToast({ title: e?.message || "加载可预约时段失败", icon: "none" })
} finally {
slotsLoading.value = false
}
}
const selectDeliveryMode = async (mode: "IMMEDIATE" | "SCHEDULED") => {
state.deliveryExpectType = mode
if (mode === "SCHEDULED") {
await loadDeliveryTimeSlots()
ensureScheduledSelection()
}
}
const onPickSchedDate = (ymd: string) => {
state.schedDate = ymd
const day = (state.deliveryDaySlots || []).find((d: any) => d.expectDate === ymd)
if (day?.slots?.length) {
const s0 = day.slots[0]
state.schedStart = normalizeTime(s0.expectDeliveryStart ?? s0.start)
state.schedEnd = normalizeTime(s0.expectDeliveryEnd ?? s0.end)
} else {
state.schedStart = ""
state.schedEnd = ""
}
}
const onPickSlot = (s: { start: string; end: string }) => {
state.schedStart = s.start
state.schedEnd = s.end
}
const buildTakeOutAppt = () => {
if (state.deliveryExpectType === "IMMEDIATE") {
return { expectDeliveryType: "IMMEDIATE" }
}
return {
expectDeliveryType: "SCHEDULED",
expectDeliveryDate: state.schedDate,
expectDeliveryStart: state.schedStart,
expectDeliveryEnd: state.schedEnd,
}
}
const loadAddressList = async () => {
const res: any = await userAddressApi.list()
if (res?.code === 0) {
@@ -160,6 +394,11 @@ const doSubmitOrder = async () => {
if (!state.carParentId) {
return uni.showToast({ title: "订单信息缺失(carParentId)", icon: "none" })
}
if (state.deliveryExpectType === "SCHEDULED") {
if (!state.schedDate || !state.schedStart || !state.schedEnd) {
return uni.showToast({ title: "请选择预约送达时段", icon: "none" })
}
}
submitting.value = true
uni.showLoading({ title: "提交中...", mask: true })
try {
@@ -177,6 +416,9 @@ const doSubmitOrder = async () => {
const _pa: any = {
// Long id 可能超过 JS 安全整数范围,禁止 Number(),否则末尾会变(精度丢失)
carParentId: String(state.carParentId),
deliveryPrice: 0,
/** 硬件侧 @NotNull:HardAppTakeOutApptDto */
appt: buildTakeOutAppt(),
address: {
id: String(selected.id),
addressId: String(selected.id),
@@ -189,6 +431,9 @@ const doSubmitOrder = async () => {
fullAddress: fullAddr,
},
}
if (String(state.apptId || "").trim()) {
_pa.apptId = String(state.apptId).trim()
}
const res: any = await payApi.createTakeOutOrder(_pa)
if (res?.code === 0 || res === 0) {
uni.showToast({ title: "下单成功", icon: "none" })
@@ -215,5 +460,11 @@ const doSubmitOrder = async () => {
.addr-set-btn {
color: #ffffff;
}
/* 选中日期/时段:压过 second-text-* 的字色,保证蓝底白字清晰 */
.sched-date-chip--active,
.sched-slot-chip--active {
color: #ffffff !important;
}
</style>
+1
View File
@@ -104,6 +104,7 @@ const doRefund = (v: any) => {
<tm-tag v-if="v.orderStatus == 10" color="error" size="s">售后</tm-tag>
<tm-tag v-if="v.orderStatus == 20" size="s">已核销</tm-tag>
<tm-tag v-if="v.orderStatus == 30" color="info" size="s">已取消(智能柜)</tm-tag>
<tm-tag v-if="v.orderStatus == 110" color="success" size="s">配送中</tm-tag>
</view>
<view class="flex-row items-center justify-between mt-5rpx second-text-24">
<view>
+5 -1
View File
@@ -9,7 +9,11 @@ export type CanteenDeliveryTab = "pending" | "all" | "completed" | "cancelled"
* @see canteen-delivery-rider 对接文档
*/
export default {
list: (params: { tab?: CanteenDeliveryTab }) =>
/**
* 分页与 MyBatis-Plus 一致:current、size;未传由服务端默认。
* 响应 data 为分页对象:records、total、current、size、pages
*/
list: (params: { tab?: CanteenDeliveryTab; current?: number; size?: number }) =>
request({
url: `${base}/list`,
method: "GET",
+18
View File
@@ -53,6 +53,24 @@ export default {
url: "/hard/hard-appt/app/list",
method: "GET",
}),
/**
* 外卖预约配送可选时段(与下单校验同源,见 take-out-scheduled-delivery 业务说明 §6.3)
* GET /app/hard-order/app/take-out/delivery-time-slots
*/
takeOutDeliveryTimeSlots: (params: {
merchantId: string | number
/** yyyy-MM-dd,起始配送日;不传默认今天 */
expectDate?: string
/** 连续天数,默认 1,最大 14 */
days?: number
slotMinutes?: number
leadMinutes?: number
}) =>
request({
url: "/hard/hard-order/app/take-out/delivery-time-slots",
method: "GET",
params,
}),
/**
* 用户确认设备二维码扫一扫订单
* @param orderId 订单id
@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
mainIsScroll: false,
bg: "",
})
const emit = defineEmits(["scroll", "navRightClick"])
const emit = defineEmits(["scroll", "scrolltolower", "navRightClick"])
const riderPushStore = useRiderPushBannerStore()
const { visible: riderPushVisible, title: riderPushTitle, body: riderPushBody, merchantName: riderPushMerchant, fullAddress: riderPushAddr, sourceOrderId: riderPushOrderId, remainSec: riderPushRemain, topPx: riderPushTopPx } =
@@ -133,7 +133,11 @@ function initBackStyle() {
<view class="layout_main_top">
<slot name="main-top"></slot>
</view>
<scroll-view :scroll-y="mainIsScroll" @scroll="scroll">
<scroll-view
:scroll-y="mainIsScroll"
@scroll="scroll"
@scrolltolower="emit('scrolltolower')"
:lower-threshold="100">
<slot></slot>
</scroll-view>
<view class="layout_main_bottom">
@@ -0,0 +1,77 @@
/**
* 骑手端外送单:配送方式 / 预计时间展示(与 CanteenDeliveryOrderAppVO 字段一致)
*/
export type CanteenDeliveryExpectFields = {
expectDeliveryType?: string | null
expectDeliveryDate?: string | null
expectDeliveryStart?: string | null
expectDeliveryEnd?: string | null
expectArriveTime?: string | null
createTime?: string | null
}
const pad2 = (n: number) => (n < 10 ? `0${n}` : `${n}`)
/** 展示用 yyyy-MM-dd HH:mm */
export const formatDateTime = (d: Date): string =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`
/** 解析后端常见 createTime:2026-04-09 17:26:31 */
export const parseDateTimeLoose = (s?: string | null): Date | null => {
if (s == null || String(s).trim() === "") return null
const t = String(s).trim()
const d = new Date(t.includes("T") ? t : t.replace(/-/g, "/"))
return Number.isNaN(d.getTime()) ? null : d
}
const hm = (t?: string | null) => {
if (t == null || t === "") return ""
const u = String(t).trim()
return u.length >= 5 ? u.slice(0, 5) : u
}
/** IMMEDIATE / SCHEDULED — 订单类型:立即送 / 预约送 */
export const expectDeliveryTypeLabel = (t?: string | null) =>
String(t || "").toUpperCase() === "SCHEDULED" ? "预约送" : "立即送"
/**
* 预约单:预计送达(日期 + 时段)
*/
export const formatScheduledExpectLine = (row: CanteenDeliveryExpectFields): string => {
const d = row.expectDeliveryDate ? String(row.expectDeliveryDate).slice(0, 10) : ""
const s = hm(row.expectDeliveryStart)
const e = hm(row.expectDeliveryEnd)
if (d && s && e) return `预计送达 ${d} ${s}–${e}`
if (d && s) return `预计送达 ${d} ${s} 起`
if (d) return `预计送达 ${d}`
return "预约送(时间未返回)"
}
/**
* 立即送:优先展示后端 expectArriveTime;否则 下单时间 + 1 小时
*/
export const formatImmediateExpectLine = (row: CanteenDeliveryExpectFields): string => {
const arrive = row.expectArriveTime
if (arrive != null && String(arrive).trim() !== "") {
const d = parseDateTimeLoose(String(arrive).replace("T", " "))
if (d) return `预计送达约 ${formatDateTime(d)}`
return `预计送达约 ${String(arrive).trim()}`
}
const base = parseDateTimeLoose(row.createTime)
if (!base) return "立即送(无下单时间,无法预估)"
const plus1h = new Date(base.getTime() + 60 * 60 * 1000)
return `预计送达约 ${formatDateTime(plus1h)}(下单后约 1 小时)`
}
/** 列表 / 详情:配送时间说明(预约=时段;立即=预计约或下单+1h) */
export const deliveryExpectDetailLine = (row: CanteenDeliveryExpectFields): string => {
if (String(row.expectDeliveryType || "").toUpperCase() === "SCHEDULED") {
return formatScheduledExpectLine(row)
}
return formatImmediateExpectLine(row)
}
/** 下单时间展示(原样或略格式化) */
export const formatOrderTimeLabel = (createTime?: string | null): string =>
createTime != null && String(createTime).trim() !== "" ? String(createTime).trim() : "—"