实现V4 Rooming List轻量事项卡

This commit is contained in:
andy committed 2026-07-21 17:21:01 +07:00
1 parent 61380a9ed8
commit 0c4f5e298d
7 files changed
+380 -6

No files matched your search

@@ -0,0 +1,124 @@
<template>
<section
class="rooming-list-card"
data-testid="rooming-list-card"
>
<div class="rooming-list-card__body">
<div>
<span>{{ t('taskV4.roomingList.targetOrder') }}</span>
<strong>{{ orderLabel }}</strong>
</div>
<RouterLink
v-if="boundOrderId"
class="rooming-list-card__link"
:to="`/reservation/orders/${boundOrderId}`"
>
{{ t('task.viewOrder') }}
</RouterLink>
</div>
<p>{{ t('taskV4.roomingList.description') }}</p>
<small>{{ t('taskV4.roomingList.confirmHint') }}</small>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
const props = withDefaults(defineProps<{
orderReference?: string | null
targetLocator?: string | null
boundOrderId?: string | null
boundOrderDisplayName?: string | null
}>(), {
orderReference: null,
targetLocator: null,
boundOrderId: null,
boundOrderDisplayName: null,
})
const { t } = useI18n()
const orderLabel = computed(() =>
readableValue(props.boundOrderDisplayName) ||
readableValue(props.orderReference) ||
readableValue(props.targetLocator) ||
t('taskV4.roomingList.unboundOrder'),
)
function readableValue(value?: string | null): string {
const trimmedValue = value?.trim() ?? ''
return trimmedValue === '-' ? '' : trimmedValue
}
</script>
<style scoped>
.rooming-list-card {
display: grid;
gap: 12px;
margin: 0 20px 18px;
border: 1px solid var(--th-color-slate-200);
border-radius: var(--th-radius-sm);
background: var(--th-color-slate-50);
padding: 14px;
}
.rooming-list-card__body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.rooming-list-card__body div {
display: grid;
gap: 4px;
min-width: 0;
}
.rooming-list-card__body span {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 800;
}
.rooming-list-card__body strong {
color: var(--th-color-slate-900);
font-size: 14px;
overflow-wrap: anywhere;
}
.rooming-list-card p,
.rooming-list-card small {
margin: 0;
color: var(--th-color-slate-600);
font-size: 13px;
line-height: 1.55;
}
.rooming-list-card small {
color: var(--th-color-slate-500);
font-weight: 700;
}
.rooming-list-card__link {
color: var(--th-color-blue-600);
flex: 0 0 auto;
font-size: 13px;
font-weight: 900;
text-decoration: none;
}
.rooming-list-card__link:hover {
text-decoration: underline;
}
@media (max-width: 760px) {
.rooming-list-card__body {
align-items: flex-start;
flex-direction: column;
}
}
</style>
@@ -46,8 +46,15 @@
@update:model-value="emit('update:modelValue', $event)"
/>
<template v-else>
<ReservationV4RoomingListCard
v-if="isRoomingListCard"
:order-reference="orderReference"
:target-locator="targetLocator"
:bound-order-id="boundOrderId"
:bound-order-display-name="boundOrderDisplayName"
/>
<ReservationV4PaymentAttachmentPreview
v-if="isPaymentCard"
v-else-if="isPaymentCard"
:card="card"
:source-message-id="sourceMessageId"
/>
@@ -65,7 +72,7 @@
</template>
<details
v-if="!isRoomInformationCard && safePayloadRows.length"
v-if="!isRoomInformationCard && !isRoomingListCard && safePayloadRows.length"
class="safe-payload"
>
<summary>
@@ -133,7 +140,7 @@
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
</button>
<RouterLink
v-if="boundOrderId"
v-if="boundOrderId && !isRoomingListCard"
class="card-order-link"
:to="`/reservation/orders/${boundOrderId}`"
>
@@ -155,7 +162,7 @@
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
</button>
<RouterLink
v-if="boundOrderId"
v-if="boundOrderId && !isRoomingListCard"
class="card-order-link"
:to="`/reservation/orders/${boundOrderId}`"
>
@@ -173,6 +180,7 @@ import { useI18n } from 'vue-i18n'
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
import ReservationV4PaymentAttachmentPreview from '@/components/reservation/ReservationV4PaymentAttachmentPreview.vue'
import ReservationV4RoomInformationCard from '@/components/reservation/ReservationV4RoomInformationCard.vue'
import ReservationV4RoomingListCard from '@/components/reservation/ReservationV4RoomingListCard.vue'
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
@@ -196,6 +204,8 @@ const props = withDefaults(defineProps<{
hotelId?: string
orderTaskId?: string | null
sourceMessageId?: string | null
orderReference?: string | null
targetLocator?: string | null
actionErrors?: string[]
successMessage?: string
reviewForm?: ReviewForm
@@ -203,11 +213,14 @@ const props = withDefaults(defineProps<{
canConfirm: boolean
canReview: boolean
boundOrderId?: string | null
boundOrderDisplayName?: string | null
}>(), {
validationErrors: () => ({}),
hotelId: undefined,
orderTaskId: null,
sourceMessageId: null,
orderReference: null,
targetLocator: null,
actionErrors: () => [],
successMessage: '',
reviewForm: () => ({
@@ -215,6 +228,7 @@ const props = withDefaults(defineProps<{
reason: '',
}),
boundOrderId: null,
boundOrderDisplayName: null,
})
const emit = defineEmits<{
@@ -227,6 +241,7 @@ const emit = defineEmits<{
const { t } = useI18n()
const isRoomInformationCard = computed(() => props.card.card_type === 'ROOM_INFORMATION')
const isPaymentCard = computed(() => props.card.card_type === 'PAYMENT')
const isRoomingListCard = computed(() => props.card.card_type === 'ROOMING_LIST')
const visibleFields = computed(() =>
isPaymentCard.value
? props.card.fields.filter((field) => !isPaymentAttachmentIdField(field))