refactor: checkindetail

This commit is contained in:
skytek_xinliang
2026-06-23 15:01:49 +08:00
parent cc1a3f95e2
commit 2a2535ef10
2 changed files with 214 additions and 67 deletions
+213 -66
View File
@@ -6,35 +6,42 @@
class="mb-4 font-weight-bold px-0"
color="primary"
prepend-icon="mdi-arrow-left"
size="large"
to="/checkin"
variant="text"
>
返回量測清單
返回量測清單
</v-btn>
<!-- 關卡詳細卡片 -->
<v-card
v-if="station"
class="rounded-xl elevation-3 border-accent-light mb-6"
color="surface"
>
<v-card v-if="station" class="rounded-xl elevation-3 mb-6" color="surface">
<v-card-item class="bg-primary text-white py-4">
<div class="d-flex align-center">
<v-avatar class="mr-3 text-white elevation-1" color="accent" size="48">
<div class="d-flex align-center mb-4">
<v-avatar class="mr-3 text-white elevation-1 flex-shrink-0" color="accent" size="48">
<v-icon :icon="station.icon" size="24" />
</v-avatar>
<div>
<h1 class="text-h6 font-weight-black">{{ station.name }}</h1>
<h1 class="text-title-large font-weight-black">{{ station.name }}</h1>
<span class="text-caption text-white-70"> {{ currentStationIndex }} / 11 </span>
<span class="text-title-medium text-white"> {{ currentStationIndex }} / 11 </span>
</div>
</div>
<div class="checkin-qr-panel rounded-lg pa-4 text-center">
<div class="text-title-small font-weight-bold mb-2">請出示 QR Code 給量測機掃描</div>
<div class="d-flex justify-center mb-2">
<QRCode :size="160" :value="pidQrPayload" />
</div>
</div>
</v-card-item>
<v-card-text class="pa-5">
<v-card-text class="py-5">
<!-- 關卡簡介 -->
<div class="bg-teal-lighten-5 rounded-lg pa-4 mb-6 text-body-2 text-primary-darken-1">
<div
class="bg-teal-lighten-5 rounded-lg pa-3 mb-6 text-body-medium text-primary-darken-1"
>
<div class="font-weight-bold mb-1 d-flex align-center">
<v-icon class="mr-1" icon="mdi-information-outline" size="18" />
檢測說明
@@ -42,42 +49,82 @@
{{ station.description }}
</div>
<!-- 數據輸入表單 -->
<v-form @submit.prevent="handleSave">
<h3 class="text-subtitle-2 font-weight-bold text-grey-darken-3 mb-3">數據錄入</h3>
<v-row>
<v-col v-for="field in station.fields" :key="field.key" class="mb-4" cols="12">
<div class="text-body-2 font-weight-bold text-grey-darken-3 mb-1">
{{ field.label }} <span class="text-caption text-grey">({{ field.unit }})</span>
</div>
<v-text-field
v-model.number="formValues[field.key]"
color="primary"
density="comfortable"
hide-details="auto"
:placeholder="field.placeholder"
required
rounded="lg"
:rules="[(v) => (v !== null && v !== undefined) || '此欄位不可為空']"
step="any"
type="number"
variant="outlined"
/>
</v-col>
</v-row>
<v-btn
block
class="rounded-lg font-weight-bold text-white elevation-2 mt-2"
color="secondary"
size="large"
type="submit"
>
儲存並進入下一站
<v-card class="rounded-lg mb-4" color="grey-lighten-4" variant="flat">
<v-btn v-if="machineStatus === 'waiting'" block @click="handleMachineStatusClick">
狀態模擬
</v-btn>
</v-form>
<v-card-text class="pa-4">
<div class="d-flex align-center">
<v-progress-circular
v-if="machineStatus !== 'updated'"
class="mr-3"
color="secondary"
indeterminate
size="32"
/>
<v-icon
v-else
class="mr-3"
color="success"
icon="mdi-cloud-check"
size="32"
/>
<div>
<v-btn
class="rounded-lg font-weight-bold mb-1"
color="primary"
:disabled="machineStatus !== 'waiting'"
size="small"
variant="flat"
>
{{ machineStatusText }}
</v-btn>
<div class="text-title-small text-grey-darken-1">{{ machineStatusHint }}</div>
</div>
</div>
</v-card-text>
</v-card>
<h3 class="text-title-medium font-weight-bold text-grey-darken-3 mb-3">量測數據</h3>
<v-row>
<v-col v-for="field in station.fields" :key="field.key" class="mb-3" cols="12">
<v-card class="rounded-lg border-accent-light" color="surface" variant="flat">
<v-card-text class="pa-4 d-flex justify-space-between align-center">
<div>
<div class="text-body-large font-weight-bold text-grey-darken-3">
{{ field.label }}
</div>
<div class="text-title-small text-grey">由量測機上傳後自動更新</div>
</div>
<div class="text-right">
<div class="text-title-large font-weight-black text-primary">
{{ displayMeasurementValue(field.key) }}
</div>
<div class="text-title-small text-grey">{{ field.unit }}</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-btn
block
class="rounded-lg font-weight-bold text-white elevation-2 mt-2"
color="secondary"
:disabled="machineStatus !== 'updated'"
size="large"
@click="goToNextStation"
>
進入下一站
</v-btn>
</v-card-text>
</v-card>
@@ -99,16 +146,21 @@
</template>
<script setup lang="ts">
import { computed, reactive, watch } from 'vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Layout from '@/components/layout/DefaultLayout.vue'
import QRCode from '@/components/QRCode.vue'
import { useAppStore } from '@/stores/app'
const route = useRoute()
const router = useRouter()
const store = useAppStore()
type MachineStatus = 'waiting' | 'measuring' | 'updated'
const code = computed(() => route.params.code as string)
const machineStatus = ref<MachineStatus>('waiting')
const mockTimers: number[] = []
// 各個測量站點的詳細規格定義
const stationsSpec: Record<
@@ -346,6 +398,33 @@ const stationsSpec: Record<
}
const station = computed(() => stationsSpec[code.value])
const currentMeasurement = computed(() => store.measurements[code.value])
const userPid = computed(() => `PID-${store.userProfile.phone.replace(/\D/g, '')}`)
const pidQrPayload = computed(() => `hp:pid:${userPid.value}`)
const machineStatusText = computed(() => {
if (machineStatus.value === 'waiting') {
return '等待掃描QR Code'
}
if (machineStatus.value === 'measuring') {
return '量測完成,資料上傳中'
}
return '伺服器已同步數據'
})
const machineStatusHint = computed(() => {
if (machineStatus.value === 'waiting') {
return '按下開始模擬後,量測機會掃描 QR Code 取得使用者 PID。'
}
if (machineStatus.value === 'measuring') {
return '設備正在將本站量測值送往後端伺服器,請稍候。'
}
return `更新時間:${currentMeasurement.value?.time ?? '--'}`
})
// 計算當前關卡在 11 關中的順序
const currentStationIndex = computed(() => {
@@ -353,44 +432,107 @@ const currentStationIndex = computed(() => {
return keys.indexOf(code.value) + 1
})
// 表單綁定的動態狀態
const formValues = reactive<Record<string, any>>({})
// 監聽 code 改變,初始化表單預設值
watch(
code,
newCode => {
if (stationsSpec[newCode]) {
// 優先從 store 中讀取已有的測量數據
const existing = store.measurements[newCode]
for (const field of stationsSpec[newCode].fields) {
formValues[field.key]
= existing && existing.completed && existing.values[field.key] !== undefined
? existing.values[field.key]
: field.defaultValue
}
resetMockMeasurementFlow()
}
},
{ immediate: true },
)
function handleSave () {
// 將數據儲存到 store
store.saveMeasurement(code.value, { ...formValues })
onBeforeUnmount(() => {
clearMockTimers()
})
// 尋找下一站
function displayMeasurementValue (fieldKey: string) {
if (machineStatus.value !== 'updated') {
return '--'
}
return currentMeasurement.value?.values[fieldKey] ?? '--'
}
function goToNextStation () {
const keys = Object.keys(stationsSpec)
const currentIndex = keys.indexOf(code.value)
if (currentIndex !== -1 && currentIndex < keys.length - 1) {
// 導向下一站
const nextCode = keys[currentIndex + 1]
router.push(`/checkin/${nextCode}`)
} else {
// 已完成最後一站,返回量測清單
router.push('/checkin')
}
}
function handleMachineStatusClick () {
startMockMeasurementFlow(code.value)
}
function startMockMeasurementFlow (stationCode: string) {
clearMockTimers()
machineStatus.value = 'measuring'
mockTimers.push(
window.setTimeout(() => {
store.saveMeasurement(stationCode, createMockMeasurementValues(stationCode))
machineStatus.value = 'updated'
}, 1500),
)
}
function resetMockMeasurementFlow () {
clearMockTimers()
machineStatus.value = 'waiting'
}
function clearMockTimers () {
while (mockTimers.length > 0) {
window.clearTimeout(mockTimers.pop())
}
}
function createMockMeasurementValues (stationCode: string) {
switch (stationCode) {
case 'BloodPressure': {
return { systolic: 128, diastolic: 82, heartRate: 74 }
}
case 'BodyFat': {
return { bmi: 23.4, bodyFatPct: 24.8 }
}
case 'GripStrength': {
return { leftHand: 31, rightHand: 34 }
}
case 'Sit5x': {
return { durationSeconds: 8.7 }
}
case 'Sit30s': {
return { count: 17 }
}
case 'StepTest': {
return { count: 94 }
}
case 'ArmTest': {
return { count: 19 }
}
case 'BackTest': {
return { leftDistanceCm: 2, rightDistanceCm: 3 }
}
case 'SitReachTest': {
return { distanceCm: 13 }
}
case 'UpGo8ft': {
return { durationSeconds: 5.9 }
}
case 'OneLegTest': {
return { leftSeconds: 23, rightSeconds: 26 }
}
default: {
return {}
}
}
}
</script>
<style scoped>
@@ -402,4 +544,9 @@ function handleSave () {
white-space: pre-line;
line-height: 1.6;
}
.checkin-qr-panel {
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.22);
}
</style>
+1 -1
View File
@@ -45,7 +45,7 @@ export interface MeasurementStation {
function createDefaultUserProfile (): UserProfile {
return {
name: '林建國',
name: '林大明',
birthday: '1961-08-15',
gender: '男',
height: 168,