适配 M002 V3 字段矩阵前端交互

This commit is contained in:
andy
2026-07-12 09:23:03 +08:00
parent db95aece20
commit 9bd6bc1cd0
11 changed files with 515 additions and 66 deletions

View File

@@ -2,6 +2,20 @@ import type { ReservationRecord, ReservationTaskFieldResult } from '@/types/rese
const truthyRuleValues = new Set(['Y', 'YES', 'TRUE', '1', '是', '可编辑', 'ENABLED'])
const falsyRuleValues = new Set(['N', 'NO', 'FALSE', '0', '否', '不可编辑', 'DISABLED', '隐藏'])
const roomItemFieldPathAliases: Record<string, string[]> = {
'extracted_fields.room_quantity': ['extracted_fields.room_items.0.room_quantity'],
'extracted_fields.room_type': [
'extracted_fields.room_items.0.room_type_raw',
'extracted_fields.room_items.0.room_type_normalized',
],
'extracted_fields.pms_room_type_code': ['extracted_fields.room_items.0.pms_room_type_code'],
'extracted_fields.room_items.0.room_quantity': ['extracted_fields.room_quantity'],
'extracted_fields.room_items.0.room_type_raw': [
'extracted_fields.room_type',
'extracted_fields.room_items.0.room_type_normalized',
],
'extracted_fields.room_items.0.pms_room_type_code': ['extracted_fields.pms_room_type_code'],
}
export interface FieldAreaGroup {
area: string
@@ -166,9 +180,13 @@ export function parseEnumOptions(enumOptions: string | null): EnumOption[] {
})
}
export function buildInitialFieldValues(fields: ReservationTaskFieldResult[], payload: ReservationRecord | null): ReservationRecord {
export function buildInitialFieldValues(
fields: ReservationTaskFieldResult[],
payload: ReservationRecord | null,
fieldContractVersion?: string | null,
): ReservationRecord {
return fields.reduce<ReservationRecord>((values, field) => {
values[field.field_path] = payload?.[field.field_path] ?? field.value ?? ''
values[field.field_path] = readReservationFieldValue(field, payload ?? {}, fieldContractVersion)
return values
}, {})
}
@@ -185,7 +203,7 @@ export function buildEditableFieldValues(
.filter((field) => isReservationFieldActive(field, values))
.filter((field) => isReservationFieldEditable(field, readOnly))
.reduce<ReservationRecord>((payload, field) => {
payload[field.field_path] = field.field_path in values ? values[field.field_path] : field.value ?? ''
payload[field.field_path] = readReservationFieldValue(field, values)
return payload
}, {})
}
@@ -195,13 +213,14 @@ export function validateReservationFieldValues(
values: ReservationRecord,
): ReservationFieldValidationErrors {
return fields.filter((field) => isReservationFieldActive(field, values)).reduce<ReservationFieldValidationErrors>((errors, field) => {
const value = values[field.field_path] ?? field.value ?? ''
if (isReservationFieldRequired(field) && isEmptyReservationValue(value)) {
const value = readReservationFieldValue(field, values)
const shouldValidateInput = isReservationFieldEditable(field, false)
if (shouldValidateInput && isReservationFieldRequired(field) && isEmptyReservationValue(value)) {
errors[field.field_path] = createValidationError(field, 'required')
return errors
}
if (isEmptyReservationValue(value)) {
if (!shouldValidateInput || isEmptyReservationValue(value)) {
return errors
}
@@ -227,8 +246,9 @@ function valueForConditionPath(
currentField: ReservationTaskFieldResult,
fieldPath: string,
): unknown {
if (fieldPath in values) {
return values[fieldPath]
const value = readValueByFieldPath(values, fieldPath)
if (value !== undefined) {
return value
}
if (fieldPath === currentField.field_path) {
return currentField.value
@@ -306,6 +326,121 @@ function isIsoLocalDate(value: string): boolean {
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
}
export function readReservationFieldValue(
field: ReservationTaskFieldResult,
values: ReservationRecord,
fieldContractVersion?: string | null,
): unknown {
const fieldValues = recordProperty(values, 'field_values') ?? values
const legacyValues = recordProperty(values, 'legacy_field_values')
const effectivePayload = recordProperty(values, 'effective_payload')
const legacyFieldPath = field.legacy_field_path ?? undefined
const codeV1 = normalizeContractVersion(fieldContractVersion) === 'CODE_V1'
const mainCandidates = fieldPathCandidates(field.field_path)
const legacyCandidates = legacyFieldPath ? fieldPathCandidates(legacyFieldPath) : []
if (codeV1) {
return firstDefined([
() => firstValueByPaths(legacyValues, legacyCandidates),
() => firstValueByPaths(fieldValues, legacyCandidates),
() => firstNestedValueByPaths(fieldValues, legacyCandidates),
() => firstValueByPaths(values, legacyCandidates),
() => firstNestedValueByPaths(effectivePayload, legacyCandidates),
() => firstValueByPaths(fieldValues, mainCandidates),
() => firstNestedValueByPaths(fieldValues, mainCandidates),
() => firstValueByPaths(values, mainCandidates),
() => firstNestedValueByPaths(effectivePayload, mainCandidates),
() => field.value,
() => '',
])
}
return firstDefined([
() => firstValueByPaths(fieldValues, mainCandidates),
() => firstNestedValueByPaths(fieldValues, mainCandidates),
() => firstValueByPaths(values, mainCandidates),
() => firstNestedValueByPaths(effectivePayload, mainCandidates),
() => firstValueByPaths(legacyValues, legacyCandidates),
() => firstValueByPaths(fieldValues, legacyCandidates),
() => firstNestedValueByPaths(fieldValues, legacyCandidates),
() => firstValueByPaths(values, legacyCandidates),
() => field.value,
() => '',
])
}
function readValueByFieldPath(values: ReservationRecord, fieldPath: string): unknown {
const flatValue = firstValueByPaths(values, fieldPathCandidates(fieldPath))
if (flatValue !== undefined) {
return flatValue
}
return readNestedPath(values, fieldPath)
}
function fieldPathCandidates(fieldPath: string): string[] {
return [fieldPath, ...(roomItemFieldPathAliases[fieldPath] ?? [])]
}
function firstValueByPaths(values: ReservationRecord | undefined, fieldPaths: string[]): unknown {
if (!values) {
return undefined
}
for (const fieldPath of fieldPaths) {
if (Object.prototype.hasOwnProperty.call(values, fieldPath)) {
return values[fieldPath]
}
}
return undefined
}
function firstNestedValueByPaths(values: ReservationRecord | undefined, fieldPaths: string[]): unknown {
if (!values) {
return undefined
}
for (const fieldPath of fieldPaths) {
const value = readNestedPath(values, fieldPath)
if (value !== undefined) {
return value
}
}
return undefined
}
function readNestedPath(values: ReservationRecord, fieldPath: string): unknown {
return fieldPath.split('.').reduce<unknown>((current, segment) => {
if (current === null || current === undefined) {
return undefined
}
if (Array.isArray(current)) {
const index = Number(segment)
return Number.isInteger(index) ? current[index] : undefined
}
if (typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, segment)) {
return (current as ReservationRecord)[segment]
}
return undefined
}, values)
}
function firstDefined(candidates: Array<() => unknown>): unknown {
for (const candidate of candidates) {
const value = candidate()
if (value !== undefined && value !== null) {
return value
}
}
return ''
}
function recordProperty(values: ReservationRecord, key: string): ReservationRecord | undefined {
const value = values[key]
return value && typeof value === 'object' && !Array.isArray(value) ? value as ReservationRecord : undefined
}
function normalizeContractVersion(version: string | null | undefined): string {
return version?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
}
function isRuleTruthy(value: string | null): boolean {
if (!value) {
return false