From 0ca12de2f4d8156022d28b3274ddb9e0279e472c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 10 Oct 2026 12:40:52 +0800 Subject: [PATCH] fix: show overnight appointments on each covered calendar day --- .../AppointmentCalendarPanel.test.ts | 13 +++++++++++++ .../AppointmentCalendarPanel.vue | 12 ++++++++---- .../AppointmentCalendarView.vue | 4 +++- src/modules/appointment-calendar/calendar.ts | 8 ++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts index 41d43a17..aeec06bc 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.test.ts @@ -17,6 +17,19 @@ describe('appointment calendar interactions', () => { expect(wrapper.get('[aria-label="当天安排"]').text()).toContain('当天暂无到港或送仓预约') }) + it('shows an overnight slot on both days without fetching again and excludes midnight ends', async () => { + const wrapper = render() + await wrapper.setProps({ events: [ + { key: 'overnight', type: 'delivery', date: '2026-09-23', endAt: '2026-09-24T01:00:00', time: '23:00–01:00', title: '跨日送仓', fields: [] }, + { key: 'midnight', type: 'delivery', date: '2026-09-23', endAt: '2026-09-24T00:00:00', time: '23:00–00:00', title: '午夜结束', fields: [] }, + ] }) + expect(wrapper.find('[aria-label="2026-09-23,2 项安排"]').exists()).toBe(true) + expect(wrapper.find('[aria-label="2026-09-24,1 项安排"]').exists()).toBe(true) + expect(wrapper.get('[aria-label="当天安排"]').text()).toContain('跨日送仓') + expect(wrapper.get('[aria-label="当天安排"]').text()).not.toContain('午夜结束') + expect(wrapper.emitted('range')).toBeUndefined() + }) + it('requests exactly one range for a view switch and navigates seven days', async () => { const wrapper = render() await wrapper.get('[aria-label="预约视图"]').findAll('button')[1]!.trigger('click') diff --git a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue index fbba8105..4b44c3a9 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarPanel.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarPanel.vue @@ -2,11 +2,12 @@ import '@/styles/adminDensity.css' import { computed, ref } from 'vue' import AppointmentInboundCodes from './AppointmentInboundCodes.vue' -import { addDays, calendarDates, calendarRange, dateKey, moveCalendar, type CalendarMode } from './calendar' +import { addDays, calendarDates, calendarRange, dateKey, moveCalendar, occursOnDate, type CalendarMode } from './calendar' export interface CalendarEvent { key: string date: string + endAt?: string time: string type: 'arrival' | 'delivery' | 'pickup' | 'trip' title: string @@ -38,9 +39,12 @@ const dates = computed(() => calendarDates(selectedDate.value, mode.value)) const grouped = computed(() => { const result = new Map() for (const event of props.events) { - const group = result.get(event.date) || [] - group.push(event) - result.set(event.date, group) + for (const date of dates.value) { + if (!occursOnDate(event.date, event.endAt, date)) continue + const group = result.get(date) || [] + group.push(event) + result.set(date, group) + } } for (const group of result.values()) group.sort((a, b) => a.time.localeCompare(b.time) || a.key.localeCompare(b.key)) return result diff --git a/src/modules/appointment-calendar/AppointmentCalendarView.vue b/src/modules/appointment-calendar/AppointmentCalendarView.vue index d8440ac3..a2f948dd 100644 --- a/src/modules/appointment-calendar/AppointmentCalendarView.vue +++ b/src/modules/appointment-calendar/AppointmentCalendarView.vue @@ -132,12 +132,14 @@ const events = computed(() => [...arrivals.value.map(arrival => ], })), ...appointments.value.map(appointment => ({ key: appointment.appointmentNo, type: 'delivery' as const, date: (appointment.appointmentAt || appointment.plannedAt || appointment.plannedDate || '').slice(0, 10), - time: (appointment.appointmentAt || appointment.plannedAt || '').slice(11, 16) + (appointment.appointmentEndAt ? `–${appointment.appointmentEndAt.slice(11, 16)}` : ''), title: `${appointment.fbaWarehouseCode} ${appointment.appointmentStatus === 'SUCCESS' ? '已确认送仓' : '计划送仓'}`, + endAt: (appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt) || undefined, + time: (appointment.appointmentAt || appointment.plannedAt || '').slice(11, 16) + ((appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt) ? `–${(appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt)!.slice(11, 16)}` : ''), title: `${appointment.fbaWarehouseCode} ${appointment.appointmentStatus === 'SUCCESS' ? '已确认送仓' : '计划送仓'}`, fields: [ { label: '预约仓库', value: appointment.fbaWarehouseCode }, { label: '系统单号', value: appointment.appointmentNo }, { label: '外部约号', value: appointment.confirmationNo }, { label: '地点时区', value: appointment.timezone || '待核实' }, + { label: '完整时段', value: `${appointment.appointmentAt || appointment.plannedAt || appointment.plannedDate || '待定'}${(appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt) ? ` — ${appointment.appointmentAt ? appointment.appointmentEndAt : appointment.plannedEndAt}` : ''}` }, { label: '预约状态', value: statusLabels[appointment.appointmentStatus] || '—' }, { label: '货物信息', value: `${appointment.selectedCount} 票 / ${new Set(appointment.items.map(item => item.customerId)).size} 个客户` }, { label: '预约备注', value: appointment.appointmentRemark }, diff --git a/src/modules/appointment-calendar/calendar.ts b/src/modules/appointment-calendar/calendar.ts index c457c48e..0c4257fc 100644 --- a/src/modules/appointment-calendar/calendar.ts +++ b/src/modules/appointment-calendar/calendar.ts @@ -1,5 +1,13 @@ export type CalendarMode = 'month' | 'week' +/** End times are exclusive; a slot ending at midnight does not occupy the next day. */ +export function occursOnDate(startDate: string, endAt: string | undefined, date: string): boolean { + if (date === startDate) return true + if (date < startDate || !endAt) return false + const endDate = endAt.slice(0, 10) + return date < endDate || (date === endDate && /[1-9]/.test(endAt.slice(11))) +} + /** Calendar dates are warehouse-local dates, never UTC conversions. */ export function dateKey(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`