feat: draft

This commit is contained in:
skytek_xinliang
2026-06-22 10:59:41 +08:00
commit c2d4c06e7e
38 changed files with 7505 additions and 0 deletions
@@ -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>