实现V4 Trace事项卡前端展示
This commit is contained in:
1 parent
38661257d1
commit
f3b6adda17
10 files changed
+931
-7
No files matched your search
@@ -439,6 +439,13 @@ function isSelectControl(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
}
|
||||
|
||||
function fixedFieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
const backendOptions = (field.fixed_options ?? []).map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label || option.value,
|
||||
}))
|
||||
if (backendOptions.length) {
|
||||
return backendOptions
|
||||
}
|
||||
const optionsSource = normalizeControlType(field.options_source)
|
||||
if (optionsSource !== 'RESERVATION_V4_GROUP_BOOKING_STATUS_FIXED') {
|
||||
return []
|
||||
|
||||
@@ -58,6 +58,16 @@
|
||||
:card="card"
|
||||
:source-message-id="sourceMessageId"
|
||||
/>
|
||||
<ReservationV4TraceCard
|
||||
v-else-if="isTraceCard"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
v-else
|
||||
class="task-card-section__fields"
|
||||
@@ -72,7 +82,7 @@
|
||||
</template>
|
||||
|
||||
<details
|
||||
v-if="!isRoomInformationCard && !isRoomingListCard && safePayloadRows.length"
|
||||
v-if="!isRoomInformationCard && !isRoomingListCard && !isTraceCard && safePayloadRows.length"
|
||||
class="safe-payload"
|
||||
>
|
||||
<summary>
|
||||
@@ -124,6 +134,7 @@
|
||||
<label>
|
||||
<span>{{ t('taskV4.reviewReason') }}</span>
|
||||
<textarea
|
||||
name="v4_review_reason"
|
||||
:value="reviewForm.reason"
|
||||
:placeholder="t('taskV4.reviewReasonPlaceholder')"
|
||||
rows="3"
|
||||
@@ -182,6 +193,7 @@ import ReservationV4PaymentAttachmentPreview from '@/components/reservation/Rese
|
||||
import ReservationV4RoomInformationCard from '@/components/reservation/ReservationV4RoomInformationCard.vue'
|
||||
import ReservationV4RoomingListCard from '@/components/reservation/ReservationV4RoomingListCard.vue'
|
||||
import ReservationV4TaskCardFieldRenderer from '@/components/reservation/ReservationV4TaskCardFieldRenderer.vue'
|
||||
import ReservationV4TraceCard from '@/components/reservation/ReservationV4TraceCard.vue'
|
||||
import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation'
|
||||
import { formatReservationReadonlyReason } from '@/utils/reservationDisplay'
|
||||
import {
|
||||
@@ -242,6 +254,7 @@ 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 isTraceCard = computed(() => props.card.card_type === 'TRACE_RESERVATION_NOTES')
|
||||
const visibleFields = computed(() =>
|
||||
isPaymentCard.value
|
||||
? props.card.fields.filter((field) => !isPaymentAttachmentIdField(field))
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<section
|
||||
class="trace-card"
|
||||
data-testid="trace-card"
|
||||
>
|
||||
<article
|
||||
v-for="item in traceItems"
|
||||
:key="item.index"
|
||||
class="trace-item"
|
||||
>
|
||||
<header class="trace-item__header">
|
||||
<span>{{ t('taskV4.trace.itemLabel', { index: item.index + 1 }) }}</span>
|
||||
<strong>{{ item.type === 'EXTRA_BED' ? t('taskV4.trace.extraBedTitle') : t('taskV4.trace.generalTitle') }}</strong>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="item.type === 'EXTRA_BED'"
|
||||
class="trace-item__action"
|
||||
>
|
||||
<span>{{ t('taskV4.trace.action') }}</span>
|
||||
<strong>SET EXTRA BED</strong>
|
||||
</div>
|
||||
|
||||
<div class="trace-item__fields">
|
||||
<label
|
||||
v-for="field in fieldsForItem(item)"
|
||||
:key="fieldKey(field)"
|
||||
class="trace-field"
|
||||
:class="{ 'trace-field--readonly': !isEditable(field) }"
|
||||
>
|
||||
<span class="trace-field__label">
|
||||
{{ field.display_name }}
|
||||
<sup v-if="field.required">*</sup>
|
||||
<em v-if="!isEditable(field)">{{ t('taskV4.field.readonly') }}</em>
|
||||
</span>
|
||||
|
||||
<select
|
||||
v-if="isEditable(field) && isSelectField(field)"
|
||||
class="trace-field__control"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:disabled="selectDisabled(field)"
|
||||
:value="fieldValue(field)"
|
||||
@change="updateField(field, $event)"
|
||||
>
|
||||
<option value="">{{ t('taskV4.field.selectPlaceholder') }}</option>
|
||||
<option
|
||||
v-for="option in fieldOptions(field)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<textarea
|
||||
v-else-if="isEditable(field) && isTextField(field)"
|
||||
class="trace-field__control trace-field__control--textarea"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
rows="3"
|
||||
@input="updateField(field, $event)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field) && isPositiveIntegerField(field)"
|
||||
class="trace-field__control"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field)"
|
||||
class="trace-field__control"
|
||||
type="text"
|
||||
:name="fieldKey(field)"
|
||||
:aria-invalid="visibleFieldErrors(field).length ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="trace-field__static"
|
||||
>
|
||||
{{ staticFieldValue(field) }}
|
||||
</span>
|
||||
|
||||
<small
|
||||
v-if="field.control_hint"
|
||||
class="trace-field__hint"
|
||||
>{{ field.control_hint }}</small>
|
||||
<small
|
||||
v-for="hint in lookupHints(field)"
|
||||
:key="hint"
|
||||
class="trace-field__hint"
|
||||
>{{ hint }}</small>
|
||||
<small
|
||||
v-for="error in visibleFieldErrors(field)"
|
||||
:key="error"
|
||||
class="trace-field__error"
|
||||
>{{ error }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { fetchReservationV4RoomTypeLookups } from '@/services/reservationService'
|
||||
import type {
|
||||
ReservationRecord,
|
||||
ReservationV4CatalogLookupItem,
|
||||
ReservationV4CatalogLookupResult,
|
||||
ReservationV4TaskCardFieldResult,
|
||||
ReservationV4TaskCardResult,
|
||||
} from '@/types/reservation'
|
||||
import {
|
||||
isReservationV4EditableField,
|
||||
isReservationV4TraceSafeField,
|
||||
fieldPathToPointer,
|
||||
readV4FieldValue,
|
||||
reservationV4FieldKey,
|
||||
reservationV4LookupKindForOptionsSource,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
type TraceItemType = 'GENERAL' | 'EXTRA_BED'
|
||||
type TraceItem = {
|
||||
index: number
|
||||
type: TraceItemType
|
||||
}
|
||||
type FieldOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
card: ReservationV4TaskCardResult
|
||||
modelValue: ReservationRecord
|
||||
readOnly: boolean
|
||||
editableKeys: string[]
|
||||
validationErrors?: Record<string, string>
|
||||
hotelId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ReservationRecord]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const roomTypeLookup = reactive<{
|
||||
loading: boolean
|
||||
error: string
|
||||
result: ReservationV4CatalogLookupResult | null
|
||||
}>({
|
||||
loading: false,
|
||||
error: '',
|
||||
result: null,
|
||||
})
|
||||
let roomTypeLookupRequestVersion = 0
|
||||
|
||||
const traceFields = computed(() => props.card.fields.filter(isReservationV4TraceSafeField))
|
||||
const hasRoomTypeLookupField = computed(() =>
|
||||
traceFields.value.some((field) => reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE'),
|
||||
)
|
||||
const traceItems = computed<TraceItem[]>(() => {
|
||||
const itemIndexes = [...new Set(traceFields.value.map(fieldIndex).filter((index) => index >= 0))]
|
||||
return itemIndexes.sort((left, right) => left - right).map((index) => ({
|
||||
index,
|
||||
type: traceItemType(index),
|
||||
}))
|
||||
})
|
||||
|
||||
watch(
|
||||
[hasRoomTypeLookupField, () => props.hotelId],
|
||||
() => {
|
||||
if (hasRoomTypeLookupField.value) {
|
||||
void loadRoomTypeLookup()
|
||||
} else {
|
||||
roomTypeLookupRequestVersion += 1
|
||||
roomTypeLookup.loading = false
|
||||
roomTypeLookup.error = ''
|
||||
roomTypeLookup.result = null
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function fieldsForItem(item: TraceItem): ReservationV4TaskCardFieldResult[] {
|
||||
const preferredOrder = item.type === 'EXTRA_BED'
|
||||
? ['target_room_type_code', 'extra_bed_room_count', 'department_code']
|
||||
: ['text', 'department_code']
|
||||
return preferredOrder
|
||||
.map((suffix) => traceFields.value.find((field) => fieldPointer(field) === `/trace_items/${item.index}/${suffix}`))
|
||||
.filter((field): field is ReservationV4TaskCardFieldResult => Boolean(field))
|
||||
}
|
||||
|
||||
function isEditable(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const key = fieldKey(field)
|
||||
return !props.readOnly &&
|
||||
props.editableKeys.includes(key) &&
|
||||
isReservationV4EditableField(field)
|
||||
}
|
||||
|
||||
function isSelectField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fixedFieldOptions(field).length > 0 ||
|
||||
isDepartmentField(field) ||
|
||||
reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE'
|
||||
}
|
||||
|
||||
function isTextField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fieldPointer(field).endsWith('/text')
|
||||
}
|
||||
|
||||
function isPositiveIntegerField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fieldPointer(field).endsWith('/extra_bed_room_count')
|
||||
}
|
||||
|
||||
function isDepartmentField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return fieldPointer(field).endsWith('/department_code')
|
||||
}
|
||||
|
||||
function fieldKey(field: ReservationV4TaskCardFieldResult): string {
|
||||
return reservationV4FieldKey(field)
|
||||
}
|
||||
|
||||
function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
||||
const value = readV4FieldValue(field, props.modelValue)
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return stringifyValue(value)
|
||||
}
|
||||
|
||||
function staticFieldValue(field: ReservationV4TaskCardFieldResult): string {
|
||||
const value = String(fieldValue(field))
|
||||
return fieldOptions(field).find((option) => option.value === value)?.label ?? stringifyValue(value)
|
||||
}
|
||||
|
||||
function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
if (reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE') {
|
||||
return roomTypeOptions(field)
|
||||
}
|
||||
const options = fixedFieldOptions(field)
|
||||
if (!options.length && isDepartmentField(field)) {
|
||||
const currentValue = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
return currentValue
|
||||
? [{
|
||||
value: currentValue,
|
||||
label: currentValue,
|
||||
}]
|
||||
: []
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function fixedFieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
return (field.fixed_options ?? []).map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label || option.value,
|
||||
}))
|
||||
}
|
||||
|
||||
function roomTypeOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
||||
const options = (roomTypeLookup.result?.items ?? []).map((item) => ({
|
||||
value: item.code,
|
||||
label: lookupItemLabel(item),
|
||||
}))
|
||||
const currentValue = String(readV4FieldValue(field, props.modelValue) || '')
|
||||
if (currentValue && !options.some((option) => option.value === currentValue)) {
|
||||
options.push({
|
||||
value: currentValue,
|
||||
label: currentValue,
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function selectDisabled(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
if (isDepartmentField(field)) {
|
||||
return fixedFieldOptions(field).length === 0
|
||||
}
|
||||
return reservationV4LookupKindForOptionsSource(field.options_source) === 'ROOM_TYPE' &&
|
||||
(roomTypeLookup.loading || Boolean(roomTypeLookup.error))
|
||||
}
|
||||
|
||||
function lookupHints(field: ReservationV4TaskCardFieldResult): string[] {
|
||||
if (reservationV4LookupKindForOptionsSource(field.options_source) !== 'ROOM_TYPE') {
|
||||
return []
|
||||
}
|
||||
if (roomTypeLookup.loading) {
|
||||
return [t('taskV4.lookup.loading')]
|
||||
}
|
||||
if (roomTypeLookup.error) {
|
||||
return [roomTypeLookup.error]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function visibleFieldErrors(field: ReservationV4TaskCardFieldResult): string[] {
|
||||
return [
|
||||
...(field.validation_errors ?? []),
|
||||
props.validationErrors?.[fieldKey(field)] ?? '',
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
||||
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[fieldKey(field)]: inputValue(field, target.value),
|
||||
})
|
||||
}
|
||||
|
||||
function inputValue(field: ReservationV4TaskCardFieldResult, value: string): string | number {
|
||||
if (!isPositiveIntegerField(field)) {
|
||||
return value
|
||||
}
|
||||
if (value.trim() === '') {
|
||||
return ''
|
||||
}
|
||||
const numericValue = Number(value)
|
||||
return Number.isFinite(numericValue) ? numericValue : value
|
||||
}
|
||||
|
||||
async function loadRoomTypeLookup(): Promise<void> {
|
||||
const requestVersion = ++roomTypeLookupRequestVersion
|
||||
roomTypeLookup.loading = true
|
||||
roomTypeLookup.error = ''
|
||||
try {
|
||||
const result = await fetchReservationV4RoomTypeLookups({
|
||||
...(props.hotelId ? { hotel_id: props.hotelId } : {}),
|
||||
page_num: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
if (requestVersion === roomTypeLookupRequestVersion) {
|
||||
roomTypeLookup.result = {
|
||||
...result,
|
||||
items: result.items ?? [],
|
||||
warnings: result.warnings ?? [],
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (requestVersion === roomTypeLookupRequestVersion) {
|
||||
roomTypeLookup.error = t('taskV4.lookup.error')
|
||||
}
|
||||
} finally {
|
||||
if (requestVersion === roomTypeLookupRequestVersion) {
|
||||
roomTypeLookup.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function traceItemType(index: number): TraceItemType {
|
||||
const payloadItem = payloadTraceItem(index)
|
||||
if (readString(payloadItem?.item_type).toUpperCase() === 'EXTRA_BED') {
|
||||
return 'EXTRA_BED'
|
||||
}
|
||||
return traceFields.value.some((field) =>
|
||||
fieldPointer(field) === `/trace_items/${index}/target_room_type_code` ||
|
||||
fieldPointer(field) === `/trace_items/${index}/extra_bed_room_count`,
|
||||
)
|
||||
? 'EXTRA_BED'
|
||||
: 'GENERAL'
|
||||
}
|
||||
|
||||
function payloadTraceItem(index: number): ReservationRecord | null {
|
||||
const traceItemsValue = props.card.display_payload?.trace_items
|
||||
if (!Array.isArray(traceItemsValue)) {
|
||||
return null
|
||||
}
|
||||
const item = traceItemsValue[index]
|
||||
return isRecord(item) ? item : null
|
||||
}
|
||||
|
||||
function fieldIndex(field: ReservationV4TaskCardFieldResult): number {
|
||||
const match = fieldPointer(field).match(/^\/trace_items\/(\d+)\//)
|
||||
return match ? Number(match[1]) : -1
|
||||
}
|
||||
|
||||
function fieldPointer(field: ReservationV4TaskCardFieldResult): string {
|
||||
return field.field_pointer || fieldPathToPointer(field.field_path)
|
||||
}
|
||||
|
||||
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
||||
if (item.display_name && item.display_name !== item.code) {
|
||||
return `${item.code} - ${item.display_name}`
|
||||
}
|
||||
return item.code
|
||||
}
|
||||
|
||||
function stringifyValue(value: unknown): string {
|
||||
return stringifyReservationV4SafeDisplayValue(
|
||||
value,
|
||||
t('taskV4.field.empty'),
|
||||
t('taskV4.field.hidden'),
|
||||
)
|
||||
}
|
||||
|
||||
function readString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is ReservationRecord {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trace-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin: 0 20px 18px;
|
||||
}
|
||||
|
||||
.trace-item {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.trace-item__header,
|
||||
.trace-item__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trace-item__header span,
|
||||
.trace-item__action span {
|
||||
color: var(--th-color-blue-600);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.trace-item__header strong,
|
||||
.trace-item__action strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.trace-item__action {
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.trace-item__fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.trace-field {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.trace-field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.trace-field__label sup {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.trace-field__label em {
|
||||
border-radius: 999px;
|
||||
background: var(--th-color-slate-100);
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.trace-field__control,
|
||||
.trace-field__static {
|
||||
min-height: 38px;
|
||||
width: 100%;
|
||||
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: 9px 10px;
|
||||
}
|
||||
|
||||
.trace-field__control--textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.trace-field__control[aria-invalid='true'] {
|
||||
border-color: var(--th-color-danger);
|
||||
box-shadow: 0 0 0 3px var(--th-color-danger-bg);
|
||||
}
|
||||
|
||||
.trace-field__static {
|
||||
display: block;
|
||||
background: var(--th-color-white);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.trace-field__hint {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trace-field__error {
|
||||
color: var(--th-color-danger);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.trace-item__fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user