295 lines
9.1 KiB
Vue
295 lines
9.1 KiB
Vue
<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>
|