统一预订事项日期选择控件

This commit is contained in:
andy
2026-07-22 09:39:31 +07:00
parent fb980c6814
commit 434afeb7c5
8 changed files with 315 additions and 271 deletions

View File

@@ -201,20 +201,14 @@
>Booking Date</label>
</div>
<div class="date-control">
<DatePicker
v-model="documentBookingDateModel"
<LocalizedDatePicker
v-model="form.document.booking_date"
input-id="invoice-booking-date"
class="localized-date-picker"
data-testid="document-booking-date-picker"
date-format="yy-mm-dd"
test-id="document-booking-date"
:placeholder="t('manualInvoice.datePlaceholder')"
:input-class="validationClass('booking-date')"
:pt="datePickerPt('document-booking-date', 'booking-date')"
:invalid="Boolean(fieldError('booking-date'))"
show-icon
icon-display="input"
append-to="body"
panel-class="manual-invoice-datepicker-panel"
:described-by="fieldError('booking-date') ? fieldErrorId('booking-date') : undefined"
required
@update:model-value="markDocumentDatesEdited"
/>
@@ -274,20 +268,14 @@
>Arrival Date</label>
</div>
<div class="date-control">
<DatePicker
v-model="bookingArrivalDateModel"
<LocalizedDatePicker
v-model="form.booking.arrival_date"
input-id="invoice-arrival-date"
class="localized-date-picker"
data-testid="booking-arrival-date-picker"
date-format="yy-mm-dd"
test-id="booking-arrival-date"
:placeholder="t('manualInvoice.datePlaceholder')"
:input-class="validationClass('arrival-date')"
:pt="datePickerPt('booking-arrival-date', 'arrival-date')"
:invalid="Boolean(fieldError('arrival-date'))"
show-icon
icon-display="input"
append-to="body"
panel-class="manual-invoice-datepicker-panel"
:described-by="fieldError('arrival-date') ? fieldErrorId('arrival-date') : undefined"
required
/>
</div>
@@ -309,20 +297,14 @@
>Departure Date</label>
</div>
<div class="date-control">
<DatePicker
v-model="bookingDepartureDateModel"
<LocalizedDatePicker
v-model="form.booking.departure_date"
input-id="invoice-departure-date"
class="localized-date-picker"
data-testid="booking-departure-date-picker"
date-format="yy-mm-dd"
test-id="booking-departure-date"
:placeholder="t('manualInvoice.datePlaceholder')"
:input-class="validationClass('departure-date')"
:pt="datePickerPt('booking-departure-date', 'departure-date')"
:invalid="Boolean(fieldError('departure-date'))"
show-icon
icon-display="input"
append-to="body"
panel-class="manual-invoice-datepicker-panel"
:described-by="fieldError('departure-date') ? fieldErrorId('departure-date') : undefined"
required
/>
</div>
@@ -344,20 +326,14 @@
>Due Date</label>
</div>
<div class="date-control">
<DatePicker
v-model="documentDueDateModel"
<LocalizedDatePicker
v-model="form.document.due_date"
input-id="invoice-due-date"
class="localized-date-picker"
data-testid="document-due-date-picker"
date-format="yy-mm-dd"
test-id="document-due-date"
:placeholder="t('manualInvoice.datePlaceholder')"
:input-class="validationClass('due-date')"
:pt="datePickerPt('document-due-date', 'due-date')"
:invalid="Boolean(fieldError('due-date'))"
show-icon
icon-display="input"
append-to="body"
panel-class="manual-invoice-datepicker-panel"
:described-by="fieldError('due-date') ? fieldErrorId('due-date') : undefined"
required
@update:model-value="markDocumentDatesEdited"
/>
@@ -812,11 +788,10 @@
</template>
<script setup lang="ts">
import DatePicker from 'primevue/datepicker'
import { usePrimeVue, type PrimeVueLocaleOptions } from 'primevue/config'
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import LocalizedDatePicker from '@/components/common/LocalizedDatePicker.vue'
import { reservationTimeZone } from '@/config/reservationConfig'
import { ApiError } from '@/services/httpClient'
import { generateManualReservationInvoice } from '@/services/reservationService'
@@ -833,13 +808,6 @@ const MANUAL_SELECTION = 'MANUAL'
const VAT_RATE = 0.07
const previewCurrency = 'THB'
type ManualInvoiceDateLocale = 'zh-CN' | 'en-US' | 'th-TH'
type ManualInvoiceDatePickerLocale = Pick<
PrimeVueLocaleOptions,
'fileSizeTypes' | 'dayNames' | 'dayNamesShort' | 'dayNamesMin' | 'monthNames' | 'monthNamesShort'
> &
Partial<PrimeVueLocaleOptions>
interface RecipientContactSeed {
contact_id: string
attention: string
@@ -956,7 +924,6 @@ const recipientCompanies: RecipientCompanySeed[] = [
]
const { t, te, locale } = useI18n()
const primeVue = usePrimeVue()
const authStore = useAuthStore()
let nextChargeId = 1
const defaultDocumentDates = createDefaultDocumentDates(resolveHotelTimeZone())
@@ -1005,122 +972,6 @@ const primaryCharge = computed<ChargeForm>(() => form.charges[0] as ChargeForm)
const sourceGroupName = computed(() => form.booking.group_name.trim() || '未填写')
const downloadMessage = computed(() => (downloadMessageKey.value ? t(downloadMessageKey.value) : ''))
const fieldErrors = computed<FieldErrorMap>(() => (validationAttempted.value ? collectFieldErrors() : {}))
const documentBookingDateModel = createDatePickerModel(
() => form.document.booking_date,
(value) => {
form.document.booking_date = value
markDocumentDatesEdited()
},
)
const documentDueDateModel = createDatePickerModel(
() => form.document.due_date,
(value) => {
form.document.due_date = value
markDocumentDatesEdited()
},
)
const bookingArrivalDateModel = createDatePickerModel(
() => form.booking.arrival_date,
(value) => {
form.booking.arrival_date = value
},
)
const bookingDepartureDateModel = createDatePickerModel(
() => form.booking.departure_date,
(value) => {
form.booking.departure_date = value
},
)
const primeVueDatePickerLocales: Record<ManualInvoiceDateLocale, ManualInvoiceDatePickerLocale> = {
'zh-CN': {
fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
dayNamesShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
dayNamesMin: ['日', '一', '二', '三', '四', '五', '六'],
monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthNamesShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
chooseDate: '选择日期',
chooseMonth: '选择月份',
chooseYear: '选择年份',
prevMonth: '上个月',
nextMonth: '下个月',
prevYear: '上一年',
nextYear: '下一年',
today: '今天',
clear: '清除',
firstDayOfWeek: 1,
showMonthAfterYear: true,
dateFormat: 'yy-mm-dd',
},
'en-US': {
fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
monthNames: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
chooseDate: 'Choose date',
chooseMonth: 'Choose month',
chooseYear: 'Choose year',
prevMonth: 'Previous month',
nextMonth: 'Next month',
prevYear: 'Previous year',
nextYear: 'Next year',
today: 'Today',
clear: 'Clear',
firstDayOfWeek: 0,
showMonthAfterYear: false,
dateFormat: 'yy-mm-dd',
},
'th-TH': {
fileSizeTypes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
dayNames: ['วันอาทิตย์', 'วันจันทร์', 'วันอังคาร', 'วันพุธ', 'วันพฤหัสบดี', 'วันศุกร์', 'วันเสาร์'],
dayNamesShort: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'],
dayNamesMin: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'],
monthNames: [
'มกราคม',
'กุมภาพันธ์',
'มีนาคม',
'เมษายน',
'พฤษภาคม',
'มิถุนายน',
'กรกฎาคม',
'สิงหาคม',
'กันยายน',
'ตุลาคม',
'พฤศจิกายน',
'ธันวาคม',
],
monthNamesShort: ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'],
chooseDate: 'เลือกวันที่',
chooseMonth: 'เลือกเดือน',
chooseYear: 'เลือกปี',
prevMonth: 'เดือนก่อนหน้า',
nextMonth: 'เดือนถัดไป',
prevYear: 'ปีก่อนหน้า',
nextYear: 'ปีถัดไป',
today: 'วันนี้',
clear: 'ล้าง',
firstDayOfWeek: 1,
showMonthAfterYear: false,
dateFormat: 'yy-mm-dd',
},
}
const previewTotals = computed(() => {
const total = form.charges.reduce((sum, charge) => sum + lineAmount(charge), 0)
const subtotal = total / (1 + VAT_RATE)
@@ -1139,14 +990,6 @@ const alertTitle = computed(() => {
return t('manualInvoice.validationTitle')
})
watch(
locale,
(nextLocale) => {
syncPrimeVueDatePickerLocale(nextLocale)
},
{ immediate: true },
)
watch(
() => form.recipient.company_code,
(companyCode) => {
@@ -1466,37 +1309,6 @@ function validationAria(fieldKey: string): Record<string, string | undefined> {
}
}
function datePickerPt(testId: string, fieldKey: string): Record<string, unknown> {
return {
pcInputText: {
root: {
'data-testid': testId,
lang: locale.value,
...validationAria(fieldKey),
},
},
panel: {
lang: locale.value,
'data-testid': `${testId}-panel`,
},
}
}
function syncPrimeVueDatePickerLocale(nextLocale: string): void {
const supportedLocale = normalizeDateLocale(nextLocale)
primeVue.config.locale = {
...(primeVue.config.locale ?? primeVueDatePickerLocales['en-US']),
...primeVueDatePickerLocales[supportedLocale],
}
}
function normalizeDateLocale(nextLocale: string): ManualInvoiceDateLocale {
if (nextLocale === 'en-US' || nextLocale === 'th-TH') {
return nextLocale
}
return 'zh-CN'
}
function chargeFieldKey(index: number, fieldName: ChargeFieldName): string {
return `charge-${index}-${fieldName}`
}
@@ -1688,40 +1500,6 @@ function datePartsToInputValue(parts: Intl.DateTimeFormatPart[]): string {
return `${year}-${month}-${day}`
}
function createDatePickerModel(readValue: () => string, writeValue: (value: string) => void) {
return computed<Date | null>({
get: () => parseInputDateValue(readValue()),
set: (value) => {
writeValue(formatDatePickerValue(value))
},
})
}
function parseInputDateValue(readValue: string): Date | null {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(readValue)
if (!match) {
return null
}
const year = Number(match[1])
const month = Number(match[2])
const day = Number(match[3])
const date = new Date(year, month - 1, day)
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
return null
}
return date
}
function formatDatePickerValue(value: Date | null | undefined): string {
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
return ''
}
const year = value.getFullYear()
const month = String(value.getMonth() + 1).padStart(2, '0')
const day = String(value.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
@@ -1962,21 +1740,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
min-width: 0;
}
.localized-date-picker {
display: block;
width: 100%;
}
.localized-date-picker :deep(.p-inputtext) {
width: 100%;
padding-right: 40px;
}
.localized-date-picker :deep(.p-datepicker-input-icon-container) {
right: 12px;
color: var(--th-color-navy-950);
}
.number-control {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -2414,11 +2177,3 @@ function isRecord(value: unknown): value is Record<string, unknown> {
}
}
</style>
<style>
.manual-invoice-datepicker-panel {
z-index: 1200;
color: var(--th-color-navy-950);
font-family: inherit;
}
</style>