Files
sports-empower-hub/app/citizen-frontend/src/pages/appointment.vue
T
skytek_xinliang cc1a3f95e2 style: checkin
2026-06-23 11:48:29 +08:00

295 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Layout>
<template #main>
<v-card class="mb-6" variant="flat">
<v-card-title class="text-primary">預約</v-card-title>
<v-card-subtitle class="text-grey-darken-3">
報名免費體能普測取得個人運動處方
</v-card-subtitle>
</v-card>
<!-- 活動分類選擇 -->
<v-tabs v-model="filterTab" class="mb-4 bg-white rounded-xl elevation-1" color="primary" grow>
<v-tab value="all">全部活動</v-tab>
<v-tab value="registered">我的報名</v-tab>
</v-tabs>
<!-- 活動列表 -->
<div v-if="filteredAppointments.length === 0" class="text-center py-12">
<v-icon class="mb-3" color="grey-lighten-1" icon="mdi-calendar-blank" size="64" />
<div class="text-title-small text-grey font-weight-bold">目前沒有相關活動</div>
</div>
<v-row v-else>
<v-col v-for="apt in filteredAppointments" :key="apt.id" class="mb-4" cols="12">
<v-card class="rounded-xl elevation-3" color="surface">
<!-- 卡片頂部狀態條 -->
<v-card-item class="bg-teal-lighten-5 py-4">
<div class="d-flex justify-space-between align-center">
<span class="text-title-medium font-weight-bold text-primary">
<v-icon class="mr-1" icon="mdi-office-building" size="14" />
{{ apt.organizer }}
</span>
<v-chip
class="font-weight-black text-title-small"
:color="getStatusColor(apt.status)"
size="small"
variant="flat"
>
{{ apt.status }}
</v-chip>
</div>
</v-card-item>
<v-card-text>
<h2 class="text-title-large font-weight-bold text-grey-darken-4 mb-4">
{{ apt.title }}
</h2>
<div class="d-flex align-start mb-2">
<v-icon class="mr-2" color="primary" icon="mdi-calendar-clock" size="18" />
<div class="text-body-2 text-grey-darken-3">
<span class="font-weight-bold">{{ apt.date }}</span>
<span class="ml-4 text-grey-darken-1">{{ apt.time }}</span>
</div>
</div>
<!-- <div class="d-flex align-start mb-2">
<v-icon class="mr-2 mt-0.5" color="primary" icon="mdi-map-marker" size="18" />
<div class="text-body-2 text-grey-darken-3">{{ apt.location }}</div>
</div> -->
<div class="d-flex align-start mb-3">
<v-icon class="mr-2" color="primary" icon="mdi-human-queue" size="18" />
<div class="text-body-2 text-grey-darken-3">
剩餘名額<span
class="font-weight-bold"
:class="apt.slots < 10 ? 'text-error' : 'text-success'"
>{{ apt.slots }}</span>
</div>
</div>
<v-divider class="my-2" />
<!-- 量測項目標籤 -->
<!-- <div class="mb-2">
<div class="text-caption font-weight-bold text-grey mb-1">量測項目</div>
<div class="d-flex flex-wrap gap-1">
<v-chip
v-for="item in apt.items"
:key="item"
class="font-weight-medium"
color="primary"
size="x-small"
variant="outlined"
>
{{ item }}
</v-chip>
</div>
</div> -->
</v-card-text>
<!-- 卡片按鈕操作 -->
<v-card-actions class="px-4 pb-4 pt-0">
<!-- 未報名狀態 -->
<template v-if="apt.status === '未報名'">
<v-btn
block
class="rounded-lg text-white font-weight-bold"
color="secondary"
:disabled="apt.slots === 0"
variant="flat"
@click="handleRegister(apt.id)"
>
{{ apt.slots === 0 ? '名額已滿' : '線上報名活動' }}
</v-btn>
</template>
<!-- 已報名狀態 -->
<template v-else-if="apt.status === '已報名'">
<v-row class="w-100">
<v-col cols="6">
<v-btn
block
class="rounded-lg text-white font-weight-bold"
color="primary"
variant="flat"
@click="showCheckinQRCode(apt)"
>
出示 QR Code
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
block
class="rounded-lg font-weight-bold"
color="error"
variant="outlined"
@click="handleCancel(apt.id)"
>
取消報名
</v-btn>
</v-col>
</v-row>
</template>
<!-- 已報到狀態 -->
<template v-else-if="apt.status === '已報到'">
<v-btn
block
class="rounded-lg font-weight-bold"
color="primary"
to="/checkin"
variant="outlined"
>
進行現場量測 (已報到)
</v-btn>
</template>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<!-- 報到 QR Code 對話框 -->
<v-dialog v-model="qrcodeDialog" max-width="400">
<v-card class="rounded-xl overflow-hidden text-center">
<v-card-title class="bg-primary text-white font-weight-bold py-3">
現場報到 QR Code
</v-card-title>
<v-card-text class="pa-6">
<h3 class="text-subtitle-1 font-weight-bold mb-1 text-grey-darken-4">
{{ selectedApt?.title }}
</h3>
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
<div
class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center"
>
<QRCode :size="220" :value="selectedCheckinPayload" />
</div>
<div class="text-caption text-grey-darken-2 font-weight-bold mb-4">
姓名: {{ store.userProfile.name }} 先生 | 電話: {{ store.userProfile.phone }}
</div>
<!-- 模擬現場合對處高齡測試快捷鍵 -->
<v-btn
block
class="rounded-lg font-weight-bold"
color="success"
prepend-icon="mdi-qrcode-scan"
variant="tonal"
@click="simulateOnSiteCheckin"
>
已由現場人員掃描報到
</v-btn>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4 justify-center">
<v-btn
class="rounded-lg px-6"
color="primary"
variant="flat"
@click="qrcodeDialog = false"
>
關閉條碼
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
</Layout>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Layout from '@/components/layout/DefaultLayout.vue'
import QRCode from '@/components/QRCode.vue'
import { type Appointment, useAppStore } from '@/stores/app'
const store = useAppStore()
const filterTab = ref('all')
const qrcodeDialog = ref(false)
const selectedApt = ref<Appointment | null>(null)
const filteredAppointments = computed(() => {
if (filterTab.value === 'registered') {
return store.appointments.filter(a => a.status === '已報名' || a.status === '已報到')
}
return store.appointments
})
const selectedCheckinPayload = computed(() => {
if (!selectedApt.value) {
return 'https://hp.local/c'
}
return `https://hp.local/c/${selectedApt.value.id}`
})
function getStatusColor (status: string) {
switch (status) {
case '未報名': {
return 'grey-darken-1'
}
case '已報名': {
return 'secondary'
}
case '已報到': {
return 'success'
}
default: {
return 'grey'
}
}
}
function handleRegister (id: string) {
store.registerAppointment(id)
}
function handleCancel (id: string) {
store.cancelAppointment(id)
}
function showCheckinQRCode (apt: Appointment) {
selectedApt.value = apt
qrcodeDialog.value = true
}
function simulateOnSiteCheckin () {
if (selectedApt.value) {
store.checkinAppointment(selectedApt.value.id)
qrcodeDialog.value = false
}
}
</script>
<style scoped>
.gap-1 {
gap: 4px;
}
.gap-2 {
gap: 8px;
}
.qrcode-wrapper {
position: relative;
width: 244px;
height: 244px;
overflow: hidden;
}
</style>