From 22767e194d13b9c23f9fb56b3b64413ca628ac33 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 19 Jul 2026 16:38:44 +0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DV4=E7=9B=AE=E5=BD=95=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=8F=AA=E8=AF=BB=E4=B8=8E=E7=B2=BE=E7=A1=AE=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationV4TaskCardFieldRenderer.vue | 115 ++++++++++++++-- client/src/tests/reservationV4Views.spec.ts | 124 +++++++++++++++++- 2 files changed, 230 insertions(+), 9 deletions(-) diff --git a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue index a26d785..78668f6 100644 --- a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue +++ b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue @@ -146,8 +146,14 @@ type LookupState = { error: string result: ReservationV4CatalogLookupResult | null } +type ExactLookupState = { + loading: boolean + searched: boolean + item: ReservationV4CatalogLookupItem | null +} const invalidLookupValues = reactive>({}) +const exactLookupStates = reactive>({}) const lookupStates = reactive>({ ACCOUNT: createLookupState(), ROOM_TYPE: createLookupState(), @@ -160,14 +166,15 @@ const lookupRequestVersions: Record = { } const lookupKinds = computed(() => { const kinds = props.fields - .filter((field) => isSelectControl(field) && isReservationV4EditableField(field)) + .filter((field) => isSelectControl(field) && isEditable(field)) .map((field) => reservationV4LookupKindForOptionsSource(field.options_source)) .filter((kind): kind is ReservationV4LookupKind => Boolean(kind)) return [...new Set(kinds)] }) +const lookupKindKey = computed(() => lookupKinds.value.join('|')) watch( - [lookupKinds, () => props.hotelId], + [lookupKindKey, () => props.hotelId], () => { resetLookupStates() lookupKinds.value.forEach((kind) => { @@ -226,6 +233,14 @@ function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] { item, })) : [] + const exactItem = exactLookupItemForField(field) + if (exactItem && !options.some((option) => option.value === exactItem.code)) { + options.push({ + value: exactItem.code, + label: lookupItemLabel(exactItem), + item: exactItem, + }) + } return options } @@ -287,6 +302,13 @@ function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] { } const hints: LookupHint[] = [] + if (isExactLookupLoading(field)) { + hints.push({ + key: `${kind}-exact-loading`, + message: t('taskV4.lookup.loading'), + tone: 'normal', + }) + } const selectedItem = selectedLookupItem(field) if (kind === 'ACCOUNT' && selectedItem && (selectedItem.market_code || selectedItem.source_code)) { hints.push({ @@ -376,11 +398,13 @@ function lookupItemLabel(item: ReservationV4CatalogLookupItem): string { function selectedLookupItem(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null { const kind = reservationV4LookupKindForOptionsSource(field.options_source) - const value = String(fieldValue(field) || '') + const value = String(readV4FieldValue(field, props.modelValue) || '') if (!kind || !value) { return null } - return lookupStates[kind].result?.items.find((item) => item.code === value) ?? null + return lookupStates[kind].result?.items.find((item) => item.code === value) + ?? exactLookupStates[exactLookupKey(kind, value)]?.item + ?? null } function createLookupState(): LookupState { @@ -402,6 +426,9 @@ function resetLookupStates(): void { Object.keys(invalidLookupValues).forEach((key) => { delete invalidLookupValues[key] }) + Object.keys(exactLookupStates).forEach((key) => { + delete exactLookupStates[key] + }) } async function loadLookup(kind: ReservationV4LookupKind): Promise { @@ -448,12 +475,12 @@ function clearInvalidLoadedCatalogValues(): void { const nextValues = { ...props.modelValue } let changed = false props.fields.forEach((field) => { - if (!isSelectField(field) || !isReservationV4EditableField(field)) { + if (!isSelectField(field) || !isEditable(field)) { return } const kind = reservationV4LookupKindForOptionsSource(field.options_source) const result = kind ? lookupStates[kind].result : null - if (!kind || !result || !isFullyLoadedCatalog(result)) { + if (!kind || !result) { return } @@ -466,8 +493,18 @@ function clearInvalidLoadedCatalogValues(): void { delete invalidLookupValues[key] return } - invalidLookupValues[key] = currentValue - nextValues[key] = '' + if (!isFullyLoadedCatalog(result)) { + const exactState = exactLookupStates[exactLookupKey(kind, currentValue)] + if (!exactState) { + void loadExactLookup(kind, currentValue) + return + } + if (exactState.loading || (exactState.searched && exactState.item)) { + delete invalidLookupValues[key] + return + } + } + clearInvalidCatalogValue(field, currentValue, nextValues) changed = true }) if (changed) { @@ -478,6 +515,68 @@ function clearInvalidLoadedCatalogValues(): void { function isFullyLoadedCatalog(result: ReservationV4CatalogLookupResult): boolean { return result.items.length >= result.page.total } + +async function loadExactLookup(kind: ReservationV4LookupKind, code: string): Promise { + const key = exactLookupKey(kind, code) + exactLookupStates[key] = { + loading: true, + searched: false, + item: null, + } + try { + const result = await lookupFetcher(kind)({ + ...(props.hotelId ? { hotel_id: props.hotelId } : {}), + keyword: code, + page_num: 1, + page_size: 20, + }) + exactLookupStates[key] = { + loading: false, + searched: true, + item: (result.items ?? []).find((item) => item.code === code) ?? null, + } + } catch { + exactLookupStates[key] = { + loading: false, + searched: true, + item: null, + } + } finally { + clearInvalidLoadedCatalogValues() + } +} + +function clearInvalidCatalogValue( + field: ReservationV4TaskCardFieldResult, + currentValue: string, + nextValues: ReservationRecord, +): void { + const key = reservationV4FieldKey(field) + invalidLookupValues[key] = currentValue + nextValues[key] = '' +} + +function exactLookupItemForField(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null { + const kind = reservationV4LookupKindForOptionsSource(field.options_source) + const value = String(readV4FieldValue(field, props.modelValue) || '') + if (!kind || !value) { + return null + } + return exactLookupStates[exactLookupKey(kind, value)]?.item ?? null +} + +function isExactLookupLoading(field: ReservationV4TaskCardFieldResult): boolean { + const kind = reservationV4LookupKindForOptionsSource(field.options_source) + const value = String(readV4FieldValue(field, props.modelValue) || '') + if (!kind || !value) { + return false + } + return exactLookupStates[exactLookupKey(kind, value)]?.loading ?? false +} + +function exactLookupKey(kind: ReservationV4LookupKind, code: string): string { + return `${props.hotelId ?? ''}:${kind}:${code}` +}