feat(ui): improve mobile responsiveness and touch targets

Add mobile-optimized card view for seat lists on smaller screens
Increase minimum height for buttons and form items for better touch interaction
Adjust grid layouts, padding, and spacing across pages for mobile devices
This commit is contained in:
2026-05-08 16:28:47 +08:00
parent 227c64d346
commit b6749bc5e7
4 changed files with 89 additions and 32 deletions

View File

@@ -235,8 +235,8 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
<template>
<UContainer class="page-shell-narrow">
<div class="grid gap-8 xl:grid-cols-[minmax(0,1fr)_34rem] xl:items-start">
<section class="space-y-6 xl:sticky xl:top-6">
<div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_34rem] xl:items-start xl:gap-8">
<section class="space-y-4 xl:sticky xl:top-6 xl:space-y-6">
<div class="page-header">
<UBadge :label="t('layout.brand')" color="primary" variant="soft" class="page-eyebrow" />
<h1 class="page-title">
@@ -247,11 +247,11 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
</p>
</div>
<div class="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
<div class="grid gap-2 sm:grid-cols-3 xl:grid-cols-1 xl:gap-3">
<div
v-for="detail in eventDetails"
:key="detail.label"
class="surface-card rounded-lg p-4"
class="surface-card rounded-lg p-3 sm:p-4"
>
<div class="flex items-start gap-3">
<div class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
@@ -273,7 +273,7 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
<UCard
id="booking-form"
class="surface-card overflow-hidden rounded-lg"
:ui="{ body: 'space-y-6 p-5 sm:p-6' }"
:ui="{ body: 'space-y-5 p-4 sm:space-y-6 sm:p-6' }"
>
<div class="space-y-1">
<p class="text-sm font-semibold text-primary">
@@ -284,8 +284,8 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
</h2>
</div>
<UForm :state="form" :validate="validateBooking" class="space-y-6" @submit="bookTicket">
<div class="grid gap-5 sm:grid-cols-2">
<UForm :state="form" :validate="validateBooking" class="space-y-5 sm:space-y-6" @submit="bookTicket">
<div class="grid gap-4 sm:grid-cols-2 sm:gap-5">
<UFormField name="name" :label="t('booking.name')" required>
<UInput v-model="form.name" size="xl" class="w-full" :placeholder="t('booking.namePlaceholder')" />
</UFormField>
@@ -303,13 +303,13 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
indicator="hidden"
:items="bookingModeOptions"
:ui="{
fieldset: 'grid grid-cols-2 gap-3',
item: 'rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
fieldset: 'grid grid-cols-1 gap-3 sm:grid-cols-2',
item: 'min-h-14 rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
}"
/>
</UFormField>
<div class="grid gap-5 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
<div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-5">
<UFormField :label="quantityLabel" name="quantity">
<UInputNumber v-model="form.quantity" size="xl" class="w-full" :min="1" :step="1" />
<template #help>
@@ -337,13 +337,13 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
:items="ticketCatalogOptions"
:ui="{
fieldset: 'grid grid-cols-1 gap-3 sm:grid-cols-2',
item: 'rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
item: 'min-h-14 rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
}"
/>
</UFormField>
<div class="surface-panel rounded-lg px-4 py-4">
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<span class="text-sm font-medium text-muted">{{ t('common.totalPrice') }}</span>
<span class="text-2xl font-bold tabular-nums text-highlighted">{{ totalFormatted }}</span>
</div>
@@ -354,7 +354,7 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
type="submit"
:label="t('booking.bookNow')"
size="xl"
class="w-full justify-center"
class="min-h-12 w-full justify-center"
:disabled="!selectedPersonInCharge || !selectedBookingMode || !selectedTicket"
:loading="submittingBooking"
/>

View File

@@ -366,7 +366,7 @@ async function openBatchShare() {
<template>
<UContainer class="page-shell-narrow">
<div class="space-y-6">
<div class="space-y-5 sm:space-y-6">
<div class="page-header text-center sm:items-center">
<UBadge :label="t('receipt.badge')" color="primary" variant="soft" class="page-eyebrow" />
<h1 class="page-title">
@@ -380,7 +380,7 @@ async function openBatchShare() {
v-for="tab in tabs"
:key="tab.id"
type="button"
class="flex min-h-10 flex-1 items-center justify-center gap-1.5 rounded-lg px-3 py-2 text-xs font-medium transition sm:text-sm"
class="flex min-h-12 flex-1 items-center justify-center gap-1.5 rounded-lg px-2 py-2 text-xs font-medium transition sm:px-3 sm:text-sm"
:class="activeTab === tab.id
? 'bg-primary text-inverted shadow-sm'
: 'bg-elevated text-default hover:bg-muted'"
@@ -395,11 +395,11 @@ async function openBatchShare() {
<UCard
v-if="activeTab === 'main'"
class="surface-card overflow-hidden rounded-lg"
:ui="{ body: 'p-5 sm:p-8' }"
:ui="{ body: 'p-4 sm:p-8' }"
>
<div class="mx-auto flex max-w-sm flex-col items-center gap-5 text-center">
<div class="surface-panel rounded-lg p-5 shadow-sm">
<QrCodeSvg :value="receipt.receiptUrl" :size="240" />
<div class="surface-panel w-full rounded-lg p-3 shadow-sm sm:p-5">
<QrCodeSvg :value="receipt.receiptUrl" :size="220" />
</div>
<div class="grid w-full gap-2 sm:grid-cols-3">
@@ -434,7 +434,7 @@ async function openBatchShare() {
<UButton
:label="t('receipt.shareSeats')"
icon="i-lucide-share-2"
class="w-full justify-center sm:w-auto"
class="min-h-12 w-full justify-center sm:w-auto"
:disabled="!availableSeats.length || shareSeatsLoading || Boolean(seatActionId)"
@click="batchShareModalOpen = true"
/>
@@ -489,7 +489,64 @@ async function openBatchShare() {
</div>
</template>
<div class="overflow-x-auto">
<div class="grid gap-3 p-3 sm:hidden">
<div
v-for="seat in receipt.seats"
:key="seat.id"
class="surface-panel rounded-lg p-3"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<p class="font-semibold leading-tight text-highlighted">
{{ getSeatLabel(seat.seatNumber, locale) }}
</p>
<UBadge
:label="seat.sharedAt ? t('receipt.sharedStatus') : t('receipt.availableStatus')"
:color="seat.sharedAt ? 'primary' : 'neutral'"
variant="soft"
size="sm"
/>
</div>
<p class="mt-1 text-xs text-muted">
{{ seat.recipientName || t('receipt.unassigned') }}
<span v-if="seat.recipientPhone"> · {{ seat.recipientPhone }}</span>
</p>
<p class="mt-1 text-xs text-muted">
{{ seat.sharedAt
? t('receipt.sharedAt', { date: formatDateTime(seat.sharedAt, t('date.notAvailable'), locale) })
: t('receipt.readyToShare') }}
</p>
</div>
</div>
<div class="mt-3 grid grid-cols-2 gap-2">
<UButton
:to="seat.seatUrl"
target="_blank"
:label="t('common.open')"
color="neutral"
variant="outline"
icon="i-lucide-external-link"
size="sm"
class="min-h-11 justify-center"
/>
<UButton
:label="seat.sharedAt ? t('receipt.unshare') : t('receipt.share')"
:icon="seat.sharedAt ? 'i-lucide-user-minus' : 'i-lucide-share-2'"
color="neutral"
:variant="seat.sharedAt ? 'outline' : 'solid'"
size="sm"
class="min-h-11 justify-center"
:loading="seatActionId === seat.id"
:disabled="shareSeatsLoading"
@click="seat.sharedAt ? unshareSeat(seat) : shareSeat(seat)"
/>
</div>
</div>
</div>
<div class="hidden overflow-x-auto sm:block">
<UTable
:data="receipt.seats"
:columns="seatColumns"
@@ -632,7 +689,7 @@ async function openBatchShare() {
:label="t('common.cancel')"
color="neutral"
variant="ghost"
class="justify-center"
class="min-h-11 justify-center"
:disabled="shareSeatsLoading"
@click="batchShareModalOpen = false"
/>
@@ -640,7 +697,7 @@ async function openBatchShare() {
<UButton
:label="t('receipt.shareSeats')"
icon="i-lucide-share-2"
class="justify-center"
class="min-h-11 justify-center"
:disabled="!availableSeats.length || Boolean(seatActionId)"
:loading="shareSeatsLoading"
@click="openBatchShare"

View File

@@ -33,7 +33,7 @@ const totalFormatted = computed(() => formatBookingCurrency(receipt.value.bookin
<template>
<UContainer class="page-shell-narrow">
<div class="space-y-6">
<div class="space-y-5 sm:space-y-6">
<div class="page-header text-center sm:items-center">
<UBadge label="Seat Ticket" color="primary" variant="soft" class="page-eyebrow" />
<h1 class="page-title">
@@ -44,7 +44,7 @@ const totalFormatted = computed(() => formatBookingCurrency(receipt.value.bookin
</p>
</div>
<div class="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<div class="grid gap-4 lg:grid-cols-[18rem_minmax(0,1fr)] lg:gap-6">
<UCard class="surface-card overflow-hidden rounded-lg" :ui="{ body: 'space-y-4 p-4 sm:p-5' }">
<div class="space-y-1 text-center">
<p class="text-sm font-semibold text-highlighted">
@@ -82,7 +82,7 @@ const totalFormatted = computed(() => formatBookingCurrency(receipt.value.bookin
</p>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div class="grid gap-2 sm:grid-cols-2 sm:gap-3">
<div class="surface-panel rounded-lg p-4">
<p class="text-xs uppercase tracking-wide text-muted">
Guest / Organizer
@@ -149,7 +149,7 @@ const totalFormatted = computed(() => formatBookingCurrency(receipt.value.bookin
:to="receipt.receiptUrl"
label="Open Main Receipt"
icon="i-lucide-receipt"
class="flex-1 justify-center"
class="min-h-12 flex-1 justify-center"
/>
<UButton
:to="receipt.seat.seatUrl"
@@ -158,7 +158,7 @@ const totalFormatted = computed(() => formatBookingCurrency(receipt.value.bookin
color="neutral"
variant="outline"
icon="i-lucide-external-link"
class="flex-1 justify-center"
class="min-h-12 flex-1 justify-center"
/>
</div>
</UCard>