feat: draft
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<div class="mb-6 text-center">
|
||||
<h1 class="text-h5 font-weight-black text-primary">公益普測與活動預約</h1>
|
||||
<p class="text-caption text-grey">報名免費體能普測,取得個人運動處方</p>
|
||||
</div>
|
||||
|
||||
<!-- 活動分類選擇 -->
|
||||
<v-tabs v-model="filterTab" class="mb-4 bg-white rounded-lg 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-subtitle-1 text-grey font-weight-bold">目前沒有相關活動</div>
|
||||
</div>
|
||||
|
||||
<v-row v-else dense>
|
||||
<v-col v-for="apt in filteredAppointments" :key="apt.id" class="mb-4" cols="12">
|
||||
<v-card class="rounded-xl elevation-3 border-accent-light" color="surface">
|
||||
<!-- 卡片頂部狀態條 -->
|
||||
<v-card-item class="bg-teal-lighten-5 py-2">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-caption 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-caption"
|
||||
:color="getStatusColor(apt.status)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ apt.status }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text class="pt-4 px-4 pb-2">
|
||||
<h2 class="text-subtitle-1 font-weight-bold text-grey-darken-4 mb-3">
|
||||
{{ apt.title }}
|
||||
</h2>
|
||||
|
||||
<div class="d-flex align-start mb-2">
|
||||
<v-icon class="mr-2 mt-0.5" 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-2 text-grey">{{ 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 mt-0.5" color="primary" icon="mdi-human-capacity" 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" dense>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
block
|
||||
class="rounded-lg text-white font-weight-bold"
|
||||
color="primary"
|
||||
prepend-icon="mdi-qrcode"
|
||||
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>
|
||||
|
||||
<!-- 模擬 QR Code SVG,更高級精緻 -->
|
||||
<div class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center">
|
||||
<svg class="qrcode-svg" height="180" viewBox="0 0 100 100" width="180">
|
||||
<!-- 四個角定位框 -->
|
||||
<path d="M 0,0 H 25 V 8 H 8 V 25 H 0 Z" fill="#1D7063" />
|
||||
<path d="M 75,0 H 100 V 25 H 92 V 8 H 75 Z" fill="#1D7063" />
|
||||
<path d="M 0,75 V 100 H 25 V 92 H 8 V 75 Z" fill="#1D7063" />
|
||||
<path d="M 92,75 V 92 H 75 V 100 H 100 V 75 Z" fill="#1D7063" />
|
||||
<!-- 內嵌二維碼方塊 -->
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="12"
|
||||
y="12"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="16"
|
||||
y="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="6"
|
||||
width="6"
|
||||
x="19"
|
||||
y="19"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="68"
|
||||
y="12"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="72"
|
||||
y="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="6"
|
||||
width="6"
|
||||
x="75"
|
||||
y="75"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="20"
|
||||
width="20"
|
||||
x="12"
|
||||
y="68"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="white"
|
||||
height="12"
|
||||
width="12"
|
||||
x="16"
|
||||
y="72"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="6"
|
||||
width="6"
|
||||
x="19"
|
||||
y="75"
|
||||
/>
|
||||
|
||||
<!-- 中間隨機小方塊 -->
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="8"
|
||||
width="8"
|
||||
x="40"
|
||||
y="20"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="14"
|
||||
width="6"
|
||||
x="52"
|
||||
y="12"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="6"
|
||||
width="16"
|
||||
x="42"
|
||||
y="36"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#47C2A6"
|
||||
height="10"
|
||||
width="28"
|
||||
x="36"
|
||||
y="48"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="12"
|
||||
width="14"
|
||||
x="68"
|
||||
y="42"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#E66926"
|
||||
height="16"
|
||||
width="16"
|
||||
x="72"
|
||||
y="60"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#1D7063"
|
||||
height="12"
|
||||
width="18"
|
||||
x="44"
|
||||
y="68"
|
||||
/>
|
||||
|
||||
<rect
|
||||
fill="#47C2A6"
|
||||
height="6"
|
||||
width="24"
|
||||
x="40"
|
||||
y="82"
|
||||
/>
|
||||
</svg>
|
||||
<!-- 紅色掃描雷射線 -->
|
||||
<div class="laser-line" />
|
||||
</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 { 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
|
||||
})
|
||||
|
||||
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: 204px;
|
||||
height: 204px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qrcode-svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 掃描雷射線效果 */
|
||||
.laser-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: #ff3b30;
|
||||
box-shadow: 0 0 8px #ff3b30;
|
||||
animation: scan 2.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { top: 0%; }
|
||||
50% { top: 100%; }
|
||||
100% { top: 0%; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user