refactor: checkindetail
This commit is contained in:
@@ -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,29 +49,69 @@
|
||||
{{ station.description }}
|
||||
</div>
|
||||
|
||||
<!-- 數據輸入表單 -->
|
||||
<v-form @submit.prevent="handleSave">
|
||||
<h3 class="text-subtitle-2 font-weight-bold text-grey-darken-3 mb-3">數據錄入:</h3>
|
||||
<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-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-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>
|
||||
<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>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -72,12 +119,12 @@
|
||||
block
|
||||
class="rounded-lg font-weight-bold text-white elevation-2 mt-2"
|
||||
color="secondary"
|
||||
:disabled="machineStatus !== 'updated'"
|
||||
size="large"
|
||||
type="submit"
|
||||
@click="goToNextStation"
|
||||
>
|
||||
儲存並進入下一站
|
||||
進入下一站
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</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>
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface MeasurementStation {
|
||||
|
||||
function createDefaultUserProfile (): UserProfile {
|
||||
return {
|
||||
name: '林建國',
|
||||
name: '林大明',
|
||||
birthday: '1961-08-15',
|
||||
gender: '男',
|
||||
height: 168,
|
||||
|
||||
Reference in New Issue
Block a user