优化V4订单任务卡片样式
This commit is contained in:
1 parent
cf3531a748
commit
b50e004b06
3 files changed
+571
-274
No files matched your search
@@ -0,0 +1,494 @@
|
||||
<template>
|
||||
<section class="th-section task-card-section">
|
||||
<div class="task-card-section__header">
|
||||
<div class="task-card-section__heading">
|
||||
<h2 class="th-section-title">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p class="task-card-section__id">
|
||||
#{{ card.card_id }}
|
||||
</p>
|
||||
</div>
|
||||
<ReservationStatusBadge :status="card.card_status" />
|
||||
</div>
|
||||
|
||||
<div class="card-meta-grid">
|
||||
<div class="card-meta-item">
|
||||
<span>{{ t('task.reviewStatus') }}</span>
|
||||
<strong>{{ card.review_status ?? '-' }}</strong>
|
||||
</div>
|
||||
<div class="card-meta-item">
|
||||
<span>{{ t('taskV4.readonlyReason') }}</span>
|
||||
<strong>{{ availabilityReason || '-' }}</strong>
|
||||
</div>
|
||||
<div class="card-meta-item">
|
||||
<span>{{ t('taskV4.blockedReason') }}</span>
|
||||
<strong>{{ card.availability.blocked_reason ?? '-' }}</strong>
|
||||
</div>
|
||||
<div class="card-meta-item">
|
||||
<span>{{ t('taskV4.orderTaskId') }}</span>
|
||||
<strong>{{ orderTaskId ?? '-' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
class="task-card-section__fields"
|
||||
:fields="card.fields"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
|
||||
<details
|
||||
v-if="safePayloadRows.length"
|
||||
class="safe-payload"
|
||||
>
|
||||
<summary>
|
||||
<span>{{ t('taskV4.displayPayload') }}</span>
|
||||
</summary>
|
||||
<dl>
|
||||
<template
|
||||
v-for="row in safePayloadRows"
|
||||
:key="row.key"
|
||||
>
|
||||
<dt>{{ row.key }}</dt>
|
||||
<dd>{{ row.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<div
|
||||
v-if="actionErrors.length"
|
||||
class="action-message action-message--error"
|
||||
>
|
||||
<p
|
||||
v-for="message in actionErrors"
|
||||
:key="message"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="successMessage"
|
||||
class="action-message action-message--success"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="card.card_status === 'REVIEW_REQUIRED'"
|
||||
class="review-box"
|
||||
>
|
||||
<label>
|
||||
<span>{{ t('taskV4.confirmedOrderId') }}</span>
|
||||
<input
|
||||
:value="reviewForm.confirmed_order_id"
|
||||
:placeholder="t('taskV4.confirmedOrderIdPlaceholder')"
|
||||
@input="updateReviewForm('confirmed_order_id', $event)"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('taskV4.reviewReason') }}</span>
|
||||
<textarea
|
||||
:value="reviewForm.reason"
|
||||
:placeholder="t('taskV4.reviewReasonPlaceholder')"
|
||||
rows="3"
|
||||
@input="updateReviewForm('reason', $event)"
|
||||
/>
|
||||
</label>
|
||||
<div class="review-box__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="primary-button"
|
||||
:disabled="!canReview"
|
||||
@click="emit('resolveReview')"
|
||||
>
|
||||
{{ submitting ? t('taskV4.resolvingReview') : t('taskV4.resolveReview') }}
|
||||
</button>
|
||||
<RouterLink
|
||||
v-if="boundOrderId"
|
||||
class="card-order-link"
|
||||
:to="`/reservation/orders/${boundOrderId}`"
|
||||
>
|
||||
{{ t('task.viewOrder') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="card-actions"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="primary-button"
|
||||
:disabled="!canConfirm"
|
||||
@click="emit('confirm')"
|
||||
>
|
||||
{{ submitting ? t('taskV4.confirmingCard') : t('taskV4.confirmCard') }}
|
||||
</button>
|
||||
<RouterLink
|
||||
v-if="boundOrderId"
|
||||
class="card-order-link"
|
||||
:to="`/reservation/orders/${boundOrderId}`"
|
||||
>
|
||||
{{ t('task.viewOrder') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
||||
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
|
||||
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
|
||||
import {
|
||||
isReservationV4UnsafeDisplayKey,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
type ReviewForm = {
|
||||
confirmed_order_id: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
card: ReservationV4TaskCardResult
|
||||
title: string
|
||||
modelValue: ReservationRecord
|
||||
readOnly: boolean
|
||||
editableKeys: string[]
|
||||
validationErrors?: Record<string, string>
|
||||
hotelId?: string
|
||||
orderTaskId?: string | null
|
||||
actionErrors?: string[]
|
||||
successMessage?: string
|
||||
reviewForm?: ReviewForm
|
||||
submitting: boolean
|
||||
canConfirm: boolean
|
||||
canReview: boolean
|
||||
boundOrderId?: string | null
|
||||
}>(), {
|
||||
validationErrors: () => ({}),
|
||||
hotelId: undefined,
|
||||
orderTaskId: null,
|
||||
actionErrors: () => [],
|
||||
successMessage: '',
|
||||
reviewForm: () => ({
|
||||
confirmed_order_id: '',
|
||||
reason: '',
|
||||
}),
|
||||
boundOrderId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ReservationRecord]
|
||||
'update:reviewForm': [value: ReviewForm]
|
||||
confirm: []
|
||||
resolveReview: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const availabilityReason = computed(() => {
|
||||
return props.card.availability.readonly_reason_message ??
|
||||
(props.card.availability.readonly_reason_code
|
||||
? formatReservationReadonlyReason(t, props.card.availability.readonly_reason_code)
|
||||
: '')
|
||||
})
|
||||
|
||||
const safePayloadRows = computed(() => {
|
||||
if (!props.card.display_payload) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(props.card.display_payload)
|
||||
.filter(([key]) => !isReservationV4UnsafeDisplayKey(key))
|
||||
.slice(0, 12)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: stringifyReservationV4SafeDisplayValue(
|
||||
value,
|
||||
t('taskV4.field.empty'),
|
||||
t('taskV4.field.hidden'),
|
||||
),
|
||||
}))
|
||||
})
|
||||
|
||||
function updateReviewForm(field: keyof ReviewForm, event: Event): void {
|
||||
const target = event.target as HTMLInputElement | HTMLTextAreaElement
|
||||
emit('update:reviewForm', {
|
||||
...props.reviewForm,
|
||||
[field]: target.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-card-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-card-section__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.task-card-section__heading {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card-section__id {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.card-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
padding: 16px 20px 18px;
|
||||
}
|
||||
|
||||
.card-meta-item {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.card-meta-item span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.card-meta-item strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-card-section__fields {
|
||||
padding: 0 20px 18px;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
min-height: 38px;
|
||||
border: 0;
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-blue-600);
|
||||
color: var(--th-color-white);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
padding: 8px 14px;
|
||||
transition: background-color 160ms ease, transform 160ms ease, opacity 160ms ease;
|
||||
}
|
||||
|
||||
.primary-button:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--th-color-blue-600) 86%, var(--th-color-navy-950));
|
||||
}
|
||||
|
||||
.primary-button:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.primary-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.review-box {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
border-top: 1px solid var(--th-color-slate-200);
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-slate-50));
|
||||
padding: 16px 20px 18px;
|
||||
}
|
||||
|
||||
.review-box label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.review-box span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.review-box input,
|
||||
.review-box textarea {
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
font: inherit;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.review-box input:focus,
|
||||
.review-box textarea:focus {
|
||||
border-color: var(--th-color-blue-600);
|
||||
box-shadow: 0 0 0 3px var(--th-color-info-bg);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.review-box__actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.action-message {
|
||||
margin: 0 20px 18px;
|
||||
border-radius: var(--th-radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.action-message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-message p + p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.action-message--error {
|
||||
background: var(--th-color-danger-bg);
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.action-message--success {
|
||||
background: var(--th-color-success-bg);
|
||||
color: var(--th-color-success);
|
||||
}
|
||||
|
||||
.safe-payload {
|
||||
margin: 0 20px 18px;
|
||||
}
|
||||
|
||||
.safe-payload summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-700);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
list-style: none;
|
||||
padding: 9px 12px;
|
||||
transition: background-color 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
.safe-payload summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.safe-payload summary::after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-right: 2px solid var(--th-color-slate-500);
|
||||
border-bottom: 2px solid var(--th-color-slate-500);
|
||||
transform: rotate(45deg) translateY(-2px);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.safe-payload[open] summary {
|
||||
border-color: var(--th-color-blue-600);
|
||||
background: var(--th-color-info-bg);
|
||||
}
|
||||
|
||||
.safe-payload[open] summary::after {
|
||||
transform: rotate(225deg) translateY(-2px);
|
||||
}
|
||||
|
||||
.safe-payload dl {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 220px) minmax(0, 1fr);
|
||||
gap: 8px 12px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 var(--th-radius-sm) var(--th-radius-sm);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.safe-payload dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.safe-payload dd {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.card-order-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--th-color-blue-600);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card-order-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.card-meta-grid,
|
||||
.review-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user