refactor: checkindetail
This commit is contained in:
@@ -6,35 +6,42 @@
|
|||||||
class="mb-4 font-weight-bold px-0"
|
class="mb-4 font-weight-bold px-0"
|
||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-arrow-left"
|
prepend-icon="mdi-arrow-left"
|
||||||
|
size="large"
|
||||||
to="/checkin"
|
to="/checkin"
|
||||||
variant="text"
|
variant="text"
|
||||||
>
|
>
|
||||||
返回量測站清單
|
返回量測清單
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<!-- 關卡詳細卡片 -->
|
<!-- 關卡詳細卡片 -->
|
||||||
<v-card
|
<v-card v-if="station" class="rounded-xl elevation-3 mb-6" color="surface">
|
||||||
v-if="station"
|
|
||||||
class="rounded-xl elevation-3 border-accent-light mb-6"
|
|
||||||
color="surface"
|
|
||||||
>
|
|
||||||
<v-card-item class="bg-primary text-white py-4">
|
<v-card-item class="bg-primary text-white py-4">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center mb-4">
|
||||||
<v-avatar class="mr-3 text-white elevation-1" color="accent" size="48">
|
<v-avatar class="mr-3 text-white elevation-1 flex-shrink-0" color="accent" size="48">
|
||||||
<v-icon :icon="station.icon" size="24" />
|
<v-icon :icon="station.icon" size="24" />
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
|
|
||||||
<div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</v-card-item>
|
</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">
|
<div class="font-weight-bold mb-1 d-flex align-center">
|
||||||
<v-icon class="mr-1" icon="mdi-information-outline" size="18" />
|
<v-icon class="mr-1" icon="mdi-information-outline" size="18" />
|
||||||
檢測說明:
|
檢測說明:
|
||||||
@@ -42,42 +49,82 @@
|
|||||||
{{ station.description }}
|
{{ station.description }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 數據輸入表單 -->
|
<v-card class="rounded-lg mb-4" color="grey-lighten-4" variant="flat">
|
||||||
<v-form @submit.prevent="handleSave">
|
<v-btn v-if="machineStatus === 'waiting'" block @click="handleMachineStatusClick">
|
||||||
<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-btn>
|
</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-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
@@ -99,16 +146,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||||
|
import QRCode from '@/components/QRCode.vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
|
|
||||||
|
type MachineStatus = 'waiting' | 'measuring' | 'updated'
|
||||||
|
|
||||||
const code = computed(() => route.params.code as string)
|
const code = computed(() => route.params.code as string)
|
||||||
|
const machineStatus = ref<MachineStatus>('waiting')
|
||||||
|
const mockTimers: number[] = []
|
||||||
|
|
||||||
// 各個測量站點的詳細規格定義
|
// 各個測量站點的詳細規格定義
|
||||||
const stationsSpec: Record<
|
const stationsSpec: Record<
|
||||||
@@ -346,6 +398,33 @@ const stationsSpec: Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const station = computed(() => stationsSpec[code.value])
|
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 關中的順序
|
// 計算當前關卡在 11 關中的順序
|
||||||
const currentStationIndex = computed(() => {
|
const currentStationIndex = computed(() => {
|
||||||
@@ -353,44 +432,107 @@ const currentStationIndex = computed(() => {
|
|||||||
return keys.indexOf(code.value) + 1
|
return keys.indexOf(code.value) + 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表單綁定的動態狀態
|
|
||||||
const formValues = reactive<Record<string, any>>({})
|
|
||||||
|
|
||||||
// 監聽 code 改變,初始化表單預設值
|
|
||||||
watch(
|
watch(
|
||||||
code,
|
code,
|
||||||
newCode => {
|
newCode => {
|
||||||
if (stationsSpec[newCode]) {
|
if (stationsSpec[newCode]) {
|
||||||
// 優先從 store 中讀取已有的測量數據
|
resetMockMeasurementFlow()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleSave () {
|
onBeforeUnmount(() => {
|
||||||
// 將數據儲存到 store
|
clearMockTimers()
|
||||||
store.saveMeasurement(code.value, { ...formValues })
|
})
|
||||||
|
|
||||||
// 尋找下一站
|
function displayMeasurementValue (fieldKey: string) {
|
||||||
|
if (machineStatus.value !== 'updated') {
|
||||||
|
return '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentMeasurement.value?.values[fieldKey] ?? '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNextStation () {
|
||||||
const keys = Object.keys(stationsSpec)
|
const keys = Object.keys(stationsSpec)
|
||||||
const currentIndex = keys.indexOf(code.value)
|
const currentIndex = keys.indexOf(code.value)
|
||||||
|
|
||||||
if (currentIndex !== -1 && currentIndex < keys.length - 1) {
|
if (currentIndex !== -1 && currentIndex < keys.length - 1) {
|
||||||
// 導向下一站
|
|
||||||
const nextCode = keys[currentIndex + 1]
|
const nextCode = keys[currentIndex + 1]
|
||||||
router.push(`/checkin/${nextCode}`)
|
router.push(`/checkin/${nextCode}`)
|
||||||
} else {
|
} else {
|
||||||
// 已完成最後一站,返回量測清單
|
|
||||||
router.push('/checkin')
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -402,4 +544,9 @@ function handleSave () {
|
|||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkin-qr-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface MeasurementStation {
|
|||||||
|
|
||||||
function createDefaultUserProfile (): UserProfile {
|
function createDefaultUserProfile (): UserProfile {
|
||||||
return {
|
return {
|
||||||
name: '林建國',
|
name: '林大明',
|
||||||
birthday: '1961-08-15',
|
birthday: '1961-08-15',
|
||||||
gender: '男',
|
gender: '男',
|
||||||
height: 168,
|
height: 168,
|
||||||
|
|||||||
Reference in New Issue
Block a user