优化V4订单任务卡片样式

This commit is contained in:
andy
2026-07-20 13:22:57 +07:00
parent cf3531a748
commit b50e004b06
3 changed files with 571 additions and 274 deletions

View File

@@ -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>

View File

@@ -96,6 +96,28 @@ describe('reservation V4 pages', () => {
})
})
it('keeps V4 task card layout classes for styled card metadata and payload summary', async () => {
const detail = createOrderTaskDetail()
detail.business_cards[0]!.display_payload = {
booking_scenario: 'STANDARD',
relevant_message_excerpt: 'Please keep this visible.',
}
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.findAll('.task-card-section')).toHaveLength(2)
expect(wrapper.find('.task-card-section__header').exists()).toBe(true)
expect(wrapper.find('.card-meta-grid').exists()).toBe(true)
expect(wrapper.find('.card-meta-item').text()).toContain(zhCN.task.reviewStatus)
expect(wrapper.find('details.safe-payload > summary').text()).toContain(zhCN.taskV4.displayPayload)
expect(wrapper.find('.card-actions .primary-button').text()).toContain(zhCN.taskV4.confirmCard)
})
it('does not render direct attachment URLs from V4 source message payload', async () => {
const detail = createOrderTaskDetail({
sourceDisplayPayload: {

View File

@@ -88,6 +88,23 @@
v-if="detail.basic_information_card"
:card="detail.basic_information_card"
:title="t('taskV4.basicInformationCard')"
:model-value="cardValues[detail.basic_information_card.card_id] ?? {}"
:read-only="isCardReadOnly(detail.basic_information_card)"
:editable-keys="editableFieldKeys(detail.basic_information_card)"
:validation-errors="cardFieldErrors[detail.basic_information_card.card_id] ?? {}"
:hotel-id="detail.order_task.hotel_id"
:order-task-id="detail.order_task.order_task_id"
:action-errors="cardActionErrors[detail.basic_information_card.card_id] ?? []"
:success-message="cardSuccessMessages[detail.basic_information_card.card_id] ?? ''"
:review-form="reviewForms[detail.basic_information_card.card_id]"
:submitting="submittingCardId === detail.basic_information_card.card_id"
:can-confirm="canConfirmCard(detail.basic_information_card)"
:can-review="canReviewCard(detail.basic_information_card)"
:bound-order-id="detail.bound_order?.order_id"
@update:model-value="setCardValues(detail.basic_information_card, $event)"
@update:review-form="setReviewForm(detail.basic_information_card.card_id, $event)"
@confirm="submitConfirm(detail.basic_information_card)"
@resolve-review="submitReview(detail.basic_information_card)"
/>
<section class="card-stack">
@@ -113,6 +130,23 @@
:key="businessCard.card_id"
:card="businessCard"
:title="cardTitle(businessCard)"
:model-value="cardValues[businessCard.card_id] ?? {}"
:read-only="isCardReadOnly(businessCard)"
:editable-keys="editableFieldKeys(businessCard)"
:validation-errors="cardFieldErrors[businessCard.card_id] ?? {}"
:hotel-id="detail.order_task.hotel_id"
:order-task-id="detail.order_task.order_task_id"
:action-errors="cardActionErrors[businessCard.card_id] ?? []"
:success-message="cardSuccessMessages[businessCard.card_id] ?? ''"
:review-form="reviewForms[businessCard.card_id]"
:submitting="submittingCardId === businessCard.card_id"
:can-confirm="canConfirmCard(businessCard)"
:can-review="canReviewCard(businessCard)"
:bound-order-id="detail.bound_order?.order_id"
@update:model-value="setCardValues(businessCard, $event)"
@update:review-form="setReviewForm(businessCard.card_id, $event)"
@confirm="submitConfirm(businessCard)"
@resolve-review="submitReview(businessCard)"
/>
</template>
</section>
@@ -148,13 +182,13 @@
</template>
<script setup lang="ts">
import { computed, defineComponent, h, nextTick, ref, watch } from 'vue'
import { useRoute, RouterLink } from 'vue-router'
import { computed, nextTick, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
import ReservationV4SourceMessageCard from '@/components/reservation/ReservationV4SourceMessageCard.vue'
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
import TaskCardSection from '@/components/reservation/ReservationV4TaskCardSection.vue'
import { ApiError } from '@/services/httpClient'
import {
confirmReservationV4OrderTaskCard,
@@ -169,7 +203,6 @@ import type {
ReservationV4TaskCardResult,
} from '@/types/reservation'
import {
formatReservationReadonlyReason,
formatReservationResultType,
formatReservationRouteCode,
formatReservationTaskCard,
@@ -181,10 +214,8 @@ import {
buildReservationV4ReviewOverrides,
isReservationV4ConfirmWritableField,
isReservationV4ReviewWritableField,
isReservationV4UnsafeDisplayKey,
mapReservationV4BackendDetailsToFields,
reservationV4FieldKey,
stringifyReservationV4SafeDisplayValue,
validateReservationV4Fields,
} from '@/utils/reservationV4FieldRules'
@@ -282,6 +313,19 @@ function setCardValues(card: ReservationV4TaskCardResult, values: ReservationRec
}
}
function setReviewForm(cardId: string, form: { confirmed_order_id: string; reason: string }): void {
reviewForms.value = {
...reviewForms.value,
[cardId]: form,
}
}
function isCardReadOnly(card: ReservationV4TaskCardResult): boolean {
return !card.availability.editable ||
card.availability.read_only ||
submittingCardId.value === card.card_id
}
function canConfirmCard(card: ReservationV4TaskCardResult): boolean {
return hasConfirmPermission.value &&
card.card_status === 'PENDING_CONFIRM' &&
@@ -423,13 +467,6 @@ function cardTitle(card: ReservationV4TaskCardResult): string {
return formatReservationTaskCard(t, card.card_type)
}
function availabilityReason(card: ReservationV4TaskCardResult): string {
return card.availability.readonly_reason_message ??
(card.availability.readonly_reason_code
? formatReservationReadonlyReason(t, card.availability.readonly_reason_code)
: card.availability.blocked_reason ?? '')
}
function formatTransitionSummary(error: ReservationAiTransitionDisplayResult): string {
return [
error.route_code ? formatReservationRouteCode(t, error.route_code) : '',
@@ -438,31 +475,6 @@ function formatTransitionSummary(error: ReservationAiTransitionDisplayResult): s
].filter(Boolean).join(' · ')
}
function safePayloadRows(payload: ReservationRecord | null): Array<{ key: string; value: string }> {
if (!payload) {
return []
}
return Object.entries(payload)
.filter(([key]) => !isUnsafePayloadKey(key))
.slice(0, 12)
.map(([key, value]) => ({
key,
value: stringifySafeValue(value),
}))
}
function isUnsafePayloadKey(key: string): boolean {
return isReservationV4UnsafeDisplayKey(key)
}
function stringifySafeValue(value: unknown): string {
return stringifyReservationV4SafeDisplayValue(
value,
t('taskV4.field.empty'),
t('taskV4.field.hidden'),
)
}
function formatApiError(error: unknown, fallback: string): {
message: string
details: unknown
@@ -487,114 +499,6 @@ function formatApiError(error: unknown, fallback: string): {
}
}
const TaskCardSection = defineComponent({
name: 'TaskCardSection',
props: {
card: {
type: Object as () => ReservationV4TaskCardResult,
required: true,
},
title: {
type: String,
required: true,
},
},
setup(props) {
return () => h('section', { class: 'th-section task-card-section' }, [
h('div', { class: 'th-section-header' }, [
h('div', [
h('h2', { class: 'th-section-title' }, props.title),
h('p', { class: 'th-muted' }, `#${props.card.card_id}`),
]),
h(ReservationStatusBadge, { status: props.card.card_status }),
]),
h('div', { class: 'card-meta-grid' }, [
h('div', [h('span', t('task.reviewStatus')), h('strong', props.card.review_status ?? '-')]),
h('div', [h('span', t('taskV4.readonlyReason')), h('strong', availabilityReason(props.card) || '-')]),
h('div', [h('span', t('taskV4.blockedReason')), h('strong', props.card.availability.blocked_reason ?? '-')]),
h('div', [h('span', t('taskV4.orderTaskId')), h('strong', detail.value?.order_task.order_task_id ?? '-')]),
]),
h(ReservationV4TaskCardFieldRenderer, {
fields: props.card.fields,
modelValue: cardValues.value[props.card.card_id] ?? {},
readOnly: !props.card.availability.editable ||
props.card.availability.read_only ||
submittingCardId.value === props.card.card_id,
editableKeys: editableFieldKeys(props.card),
validationErrors: cardFieldErrors.value[props.card.card_id] ?? {},
hotelId: detail.value?.order_task.hotel_id,
'onUpdate:modelValue': (values: ReservationRecord) => setCardValues(props.card, values),
}),
safePayloadRows(props.card.display_payload).length
? h('details', { class: 'safe-payload' }, [
h('summary', t('taskV4.displayPayload')),
h('dl', safePayloadRows(props.card.display_payload).flatMap((row) => [
h('dt', row.key),
h('dd', row.value),
])),
])
: null,
(cardActionErrors.value[props.card.card_id]?.length ?? 0) > 0
? h('div', { class: 'action-message action-message--error' },
(cardActionErrors.value[props.card.card_id] ?? []).map((message) => h('p', message)),
)
: null,
cardSuccessMessages.value[props.card.card_id]
? h('p', { class: 'action-message action-message--success' }, cardSuccessMessages.value[props.card.card_id])
: null,
props.card.card_status === 'REVIEW_REQUIRED'
? h('div', { class: 'review-box' }, [
h('label', [
h('span', t('taskV4.confirmedOrderId')),
h('input', {
value: reviewForms.value[props.card.card_id]?.confirmed_order_id ?? '',
placeholder: t('taskV4.confirmedOrderIdPlaceholder'),
onInput: (event: Event) => {
const target = event.target as HTMLInputElement
reviewForms.value[props.card.card_id] = {
...(reviewForms.value[props.card.card_id] ?? { reason: '' }),
confirmed_order_id: target.value,
}
},
}),
]),
h('label', [
h('span', t('taskV4.reviewReason')),
h('textarea', {
value: reviewForms.value[props.card.card_id]?.reason ?? '',
placeholder: t('taskV4.reviewReasonPlaceholder'),
rows: 3,
onInput: (event: Event) => {
const target = event.target as HTMLTextAreaElement
reviewForms.value[props.card.card_id] = {
...(reviewForms.value[props.card.card_id] ?? { confirmed_order_id: '' }),
reason: target.value,
}
},
}),
]),
h('button', {
type: 'button',
class: 'primary-button',
disabled: !canReviewCard(props.card),
onClick: () => void submitReview(props.card),
}, submittingCardId.value === props.card.card_id ? t('taskV4.resolvingReview') : t('taskV4.resolveReview')),
])
: h('button', {
type: 'button',
class: 'primary-button',
disabled: !canConfirmCard(props.card),
onClick: () => void submitConfirm(props.card),
}, submittingCardId.value === props.card.card_id ? t('taskV4.confirmingCard') : t('taskV4.confirmCard')),
detail.value?.bound_order?.order_id
? h(RouterLink, {
class: 'card-order-link',
to: `/reservation/orders/${detail.value.bound_order.order_id}`,
}, () => t('task.viewOrder'))
: null,
])
},
})
</script>
<style scoped>
@@ -639,35 +543,30 @@ const TaskCardSection = defineComponent({
}
.summary-section,
.task-card-section,
.diagnostics-section,
.card-stack {
overflow: hidden;
}
.summary-grid,
.card-meta-grid {
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
padding: 0 20px 20px;
}
.summary-grid div,
.card-meta-grid div {
.summary-grid div {
display: grid;
gap: 5px;
}
.summary-grid span,
.card-meta-grid span {
.summary-grid span {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 800;
}
.summary-grid strong,
.card-meta-grid strong {
.summary-grid strong {
color: var(--th-color-slate-900);
font-size: 13px;
overflow-wrap: anywhere;
@@ -688,11 +587,6 @@ const TaskCardSection = defineComponent({
padding: 0;
}
.task-card-section :deep(.v4-field-renderer) {
padding: 0 20px 18px;
}
.primary-button,
.secondary-button {
min-height: 38px;
border-radius: var(--th-radius-sm);
@@ -702,128 +596,17 @@ const TaskCardSection = defineComponent({
padding: 8px 14px;
}
.primary-button {
justify-self: start;
margin: 0 20px 18px;
border: 0;
background: var(--th-color-blue-600);
color: var(--th-color-white);
}
.secondary-button {
border: 1px solid var(--th-color-slate-200);
background: var(--th-color-white);
color: var(--th-color-slate-700);
}
.primary-button:disabled,
.secondary-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.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);
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);
color: var(--th-color-slate-900);
font: inherit;
padding: 8px 10px;
}
.review-box .primary-button {
margin: 0;
}
.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;
border: 1px solid var(--th-color-slate-200);
border-radius: var(--th-radius-sm);
padding: 10px 12px;
}
.safe-payload summary {
color: var(--th-color-slate-700);
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.safe-payload dl {
display: grid;
grid-template-columns: minmax(120px, 220px) minmax(0, 1fr);
gap: 8px 12px;
margin: 12px 0 0;
}
.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;
margin: 0 20px 18px;
color: var(--th-color-blue-600);
font-size: 13px;
font-weight: 800;
}
.diagnostic-list {
display: grid;
gap: 10px;
@@ -854,9 +637,7 @@ const TaskCardSection = defineComponent({
}
@media (max-width: 980px) {
.summary-grid,
.card-meta-grid,
.review-box {
.summary-grid {
grid-template-columns: 1fr;
}
}