diff --git a/app/citizen-frontend/src/components/HealthRadarChart.vue b/app/citizen-frontend/src/components/HealthRadarChart.vue index 57e9197..9f341cb 100644 --- a/app/citizen-frontend/src/components/HealthRadarChart.vue +++ b/app/citizen-frontend/src/components/HealthRadarChart.vue @@ -47,23 +47,58 @@ stroke-width="0.5" /> - + 心肺 ({{ radarData.cardio }}) - + 肌力 ({{ radarData.strength }}) - + 平衡 ({{ radarData.balance }}) - + 柔軟 ({{ radarData.flexibility }}) - + 敏捷 ({{ radarData.agility }}) @@ -101,7 +136,7 @@ const centerX = 60 const centerY = 60 const radius = 45 -function getAxisEndPoint(index: number, pointRadius = radius) { +function getAxisEndPoint (index: number, pointRadius = radius) { const angle = -Math.PI / 2 + (index * 2 * Math.PI) / 5 return { @@ -110,7 +145,7 @@ function getAxisEndPoint(index: number, pointRadius = radius) { } } -function getPolygonPoints(percent: number) { +function getPolygonPoints (percent: number) { const pointRadius = (percent / 100) * radius const points = [] @@ -138,7 +173,7 @@ const userDataCoordArray = computed(() => { }) const userDataPoints = computed(() => { - return userDataCoordArray.value.map((point) => `${point.x},${point.y}`).join(' ') + return userDataCoordArray.value.map(point => `${point.x},${point.y}`).join(' ') }) diff --git a/app/citizen-frontend/src/components/layout/DefaultLayout.vue b/app/citizen-frontend/src/components/layout/DefaultLayout.vue index 375b7ba..1eec672 100644 --- a/app/citizen-frontend/src/components/layout/DefaultLayout.vue +++ b/app/citizen-frontend/src/components/layout/DefaultLayout.vue @@ -295,27 +295,27 @@ const showEmergency = ref(false) // 無障礙狀態 (可持久化儲存於 localStorage) const isLargeText = ref(localStorage.getItem('mode-large-text') === 'true') -watch(isLargeText, (newVal) => { +watch(isLargeText, newVal => { localStorage.setItem('mode-large-text', String(newVal)) }) // 同步底部導航狀態與當前路由 watch( () => route.path, - (path) => { + path => { if (path === '/') activeTab.value = 'home' else if (path.startsWith('/self-assessment')) activeTab.value = 'self-assessment' else if (path.startsWith('/appointment')) activeTab.value = 'appointment' else if (path.startsWith('/checkin')) activeTab.value = 'checkin' }, - { immediate: true } + { immediate: true }, ) -function getBottomNavIconColor(value: string) { +function getBottomNavIconColor (value: string) { return activeTab.value === value ? 'secondary' : 'secondary-darken-1' } -function handleLogout() { +function handleLogout () { store.logout() router.push('/login') drawer.value = false diff --git a/app/citizen-frontend/src/pages/appointment.vue b/app/citizen-frontend/src/pages/appointment.vue index d0c9cb6..4a3c889 100644 --- a/app/citizen-frontend/src/pages/appointment.vue +++ b/app/citizen-frontend/src/pages/appointment.vue @@ -66,8 +66,7 @@ 剩餘名額:{{ apt.slots }} + >{{ apt.slots }} 位 @@ -181,40 +180,142 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
@@ -268,12 +369,12 @@ const selectedApt = ref(null) const filteredAppointments = computed(() => { if (filterTab.value === 'registered') { - return store.appointments.filter((a) => a.status === '已報名' || a.status === '已報到') + return store.appointments.filter(a => a.status === '已報名' || a.status === '已報到') } return store.appointments }) -function getStatusColor(status: string) { +function getStatusColor (status: string) { switch (status) { case '未報名': { return 'grey-darken-1' @@ -290,20 +391,20 @@ function getStatusColor(status: string) { } } -function handleRegister(id: string) { +function handleRegister (id: string) { store.registerAppointment(id) } -function handleCancel(id: string) { +function handleCancel (id: string) { store.cancelAppointment(id) } -function showCheckinQRCode(apt: Appointment) { +function showCheckinQRCode (apt: Appointment) { selectedApt.value = apt qrcodeDialog.value = true } -function simulateOnSiteCheckin() { +function simulateOnSiteCheckin () { if (selectedApt.value) { store.checkinAppointment(selectedApt.value.id) qrcodeDialog.value = false diff --git a/app/citizen-frontend/src/pages/checkin.vue b/app/citizen-frontend/src/pages/checkin.vue index 1d1079a..b5c387b 100644 --- a/app/citizen-frontend/src/pages/checkin.vue +++ b/app/citizen-frontend/src/pages/checkin.vue @@ -36,9 +36,8 @@
測量站點完成度 - {{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%) + + {{ completedCount }} / {{ totalCount }} ({{ progressPercent }}%)

尚未在活動現場合對報到

+

請先前往「活動預約」頁面,出示您的報到條碼給現場志工進行核對。

+ 前往查看預約活動 @@ -127,9 +128,7 @@
{{ getMeasurementSummary(key, station.values) }} - | {{ station.time?.split(' ')[1] }} + | {{ station.time?.split(' ')[1] }}
等待現場檢測員登錄
@@ -199,14 +198,14 @@ const reportSuccessDialog = ref(false) // 尋找已報到的活動 const activeAppointment = computed(() => { - return store.appointments.find((a) => a.status === '已報到') + return store.appointments.find(a => a.status === '已報到') }) const completedCount = computed(() => store.completedStationsCount) const totalCount = computed(() => store.totalStationsCount) const progressPercent = computed(() => store.progressPercent) -function getStationIcon(code: string) { +function getStationIcon (code: string) { switch (code) { case 'BloodPressure': { return 'mdi-heart-flash' @@ -247,7 +246,7 @@ function getStationIcon(code: string) { } } -function getMeasurementSummary(code: string, vals: Record) { +function getMeasurementSummary (code: string, vals: Record) { switch (code) { case 'BloodPressure': { return `收縮壓: ${vals.systolic} / 舒張壓: ${vals.diastolic} mmHg | 心率: ${vals.heartRate} bpm` @@ -289,7 +288,7 @@ function getMeasurementSummary(code: string, vals: Record) { } // 模擬一鍵完成所有測量 (對 Demo 极為有用) -function simulateAllMeasurements() { +function simulateAllMeasurements () { // 自動將剩餘的 9 站填入合理健康的隨機值 const m = store.measurements @@ -322,12 +321,12 @@ function simulateAllMeasurements() { } } -function generateReport() { +function generateReport () { store.generateFinalReport() reportSuccessDialog.value = true } -function goToDashboard() { +function goToDashboard () { reportSuccessDialog.value = false router.push('/') } diff --git a/app/citizen-frontend/src/pages/checkinDetail.vue b/app/citizen-frontend/src/pages/checkinDetail.vue index 775a239..8c0e9fe 100644 --- a/app/citizen-frontend/src/pages/checkinDetail.vue +++ b/app/citizen-frontend/src/pages/checkinDetail.vue @@ -27,9 +27,7 @@

{{ station.name }}

- 第 {{ currentStationIndex }} 站 / 共 11 站 + 第 {{ currentStationIndex }} 站 / 共 11 站
@@ -361,22 +359,22 @@ const formValues = reactive>({}) // 監聽 code 改變,初始化表單預設值 watch( code, - (newCode) => { + 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 + formValues[field.key] + = existing && existing.completed && existing.values[field.key] !== undefined ? existing.values[field.key] : field.defaultValue } } }, - { immediate: true } + { immediate: true }, ) -function handleSave() { +function handleSave () { // 將數據儲存到 store store.saveMeasurement(code.value, { ...formValues }) diff --git a/app/citizen-frontend/src/pages/login.vue b/app/citizen-frontend/src/pages/login.vue index 5495634..6df589e 100644 --- a/app/citizen-frontend/src/pages/login.vue +++ b/app/citizen-frontend/src/pages/login.vue @@ -120,7 +120,7 @@ const password = ref('') const loading = ref(false) const lineLoading = ref(false) -function handleLogin() { +function handleLogin () { if (!account.value) return loading.value = true setTimeout(() => { @@ -130,7 +130,7 @@ function handleLogin() { }, 1000) } -function _handleLineLogin() { +function _handleLineLogin () { lineLoading.value = true setTimeout(() => { store.login('0912-345-678') // LINE 模擬登入預設手機 diff --git a/app/citizen-frontend/src/pages/self-assessment.vue b/app/citizen-frontend/src/pages/self-assessment.vue index 982d28e..3b97233 100644 --- a/app/citizen-frontend/src/pages/self-assessment.vue +++ b/app/citizen-frontend/src/pages/self-assessment.vue @@ -181,6 +181,7 @@ :color="assessmentForm.wantedImprovements.includes(imp) ? 'primary' : 'grey'" :icon="getImprovementIcon(imp)" /> +
{{ imp }}
@@ -342,7 +343,13 @@
- + 立即預約普測
@@ -397,7 +404,7 @@ const resultLevel = ref(store.selfAssessment.scoreLevel) const improvementsOptions = ['肌力', '平衡', '心肺', '柔軟度', '敏捷', '體脂/BMI'] const diseaseOptions = ['高血壓', '糖尿病', '高血脂', '心臟病', '關節退化', '骨質疏鬆'] -function getImprovementIcon(imp: string) { +function getImprovementIcon (imp: string) { switch (imp) { case '肌力': { return 'mdi-arm-flex' @@ -423,7 +430,7 @@ function getImprovementIcon(imp: string) { } } -function toggleImprovement(imp: string) { +function toggleImprovement (imp: string) { const index = assessmentForm.wantedImprovements.indexOf(imp) if (index === -1) { assessmentForm.wantedImprovements.push(imp) @@ -432,18 +439,18 @@ function toggleImprovement(imp: string) { } } -function addMedication() { +function addMedication () { if (medicationInput.value.trim()) { assessmentForm.medicationReminders.push(medicationInput.value.trim()) medicationInput.value = '' } } -function removeMedication(index: number) { +function removeMedication (index: number) { assessmentForm.medicationReminders.splice(index, 1) } -function handleSubmit() { +function handleSubmit () { // 儲存至 store store.updateProfile({ name: profileForm.name, @@ -468,13 +475,13 @@ function handleSubmit() { currentStep.value = 3 } -function getResultColor(lvl: string) { +function getResultColor (lvl: string) { if (lvl === '低風險') return 'success' if (lvl === '需追蹤') return 'warning' return 'error' } -function getResultIcon(lvl: string) { +function getResultIcon (lvl: string) { if (lvl === '低風險') return 'mdi-check-decagram' if (lvl === '需追蹤') return 'mdi-alert-decagram' return 'mdi-alert-octagon' diff --git a/doc/PRD.md b/doc/PRD.md new file mode 100644 index 0000000..500b907 --- /dev/null +++ b/doc/PRD.md @@ -0,0 +1,147 @@ +# 運動玩轉健康力 + +## 頁面路由結構 + +```text +/ +├── /login +├── /self-assessment +├── /appointment +├── /checkin +└── /checkin/:code +``` + +## 路由對應頁面 + +```text +/ +├── / 首頁 / 市民端主控台 +├── /login 登入頁 +├── /self-assessment 線上自評頁 +├── /appointment 預約場次頁 +├── /checkin 現場報到與量測清單頁 +└── /checkin/:code 量測子頁面 + ├── /checkin/BloodPressure + ├── /checkin/BodyFat + ├── /checkin/GripStrength + ├── /checkin/Sit5x + ├── /checkin/Sit30s + ├── /checkin/StepTest + ├── /checkin/ArmTest + ├── /checkin/BackTest + ├── /checkin/SitReachTest + ├── /checkin/UpGo8ft + ├── /checkin/OneLegTest + └── /checkin/ResultReview +``` + +## 備註 + +- `/checkin/:code` 使用同一個 Vue 頁面,透過 route param 顯示不同量測站內容。 +- 目前路由來源以 [src/router/index.ts](../health-platform/hp-frontend/src/router/index.ts:1) 為準。 + +## 預計功能 + +1. 帳號與身分 + +- 手機號碼登入 +- 基本資料維護:姓名、生日、性別、身高、體重、聯絡方式 +- 緊急聯絡人 +- 個資同意與資料授權狀態 + +2. 健康自評 + +- 3 分鐘線上自評問卷 +- 運動頻率、疼痛狀況、跌倒史、慢性病、用藥提醒 +- 生活功能自評:走路、上下樓、起身、平衡、柔軟度 +- 自評結果分級:低風險 / 需追蹤 / 建議諮詢 + +3. 活動與預約 + +- 公益普測活動列表 +- 活動詳情:地點、時間、名額、主辦單位、量測項目 +- 線上報名 / 取消報名 +- QR Code 報到 +- 報名紀錄與到場狀態 + +4. 現場量測結果 + +- 血壓 BloodPressure +- 體脂 BodyFat +- 握力 GripStrength +- 5 次坐站 Sit5x +- 30 秒椅子坐立 Sit30s +- 原地抬膝踏步 StepTest +- 肱二頭肌手臂屈舉 ArmTest +- 抓背測驗 BackTest +- 坐姿體前彎 SitReachTest +- 2.44 公尺椅子坐立繞物 UpGo8ft +- 開眼單足立 OneLegTest + +5. 個人健康力儀表板 + +- 健康力總分 +- 肌力、平衡、柔軟、敏捷、心肺等能力分項 +- 與同年齡區間參考值比較 +- 風險提示:跌倒風險、血壓異常、肌力不足等 +- 最近一次量測摘要 +- 歷史趨勢圖 + +6. 運動處方 + +- 依量測結果產生建議訓練 +- 每週訓練任務 +- 動作說明:次數、組數、頻率、注意事項 +- 禁忌提醒:血壓過高、疼痛、暈眩時暫停 +- 訓練完成回填 + +7. 追蹤與提醒 + +- 下次建議量測時間 +- 回訪提醒 +- 運動任務提醒 +- 異常數值提醒 +- 報名活動前提醒 + +8. 健康紀錄 + +- 每次自評紀錄 +- 每次量測紀錄 +- 運動任務完成紀錄 +- 健康報告下載 / 分享 +- 可出示給教練、社區據點或照護人員的摘要頁 + +9. 家屬 / 照護者協作 + +- 授權家屬查看摘要 +- 家屬提醒通知 +- 協助預約活動 +- 緊急聯絡資訊更新 + +10. 無障礙與高齡友善 + +- 大字模式 +- 高對比模式 +- 簡化操作流程 +- 語音提示 / 朗讀友善 +- 明確的下一步按鈕 +- 表單錯誤提示清楚可讀 + +## 使用者流程 + +登入 / 建檔 → 線上自評 → 預約普測 → QR Code 報到 → 查看量測結果 → 取得運動處方 → 追蹤回訪 + +訪客進站 +→ 點擊「開始自評」 +→ 建立帳號 / LINE 登入 / 手機驗證 +→ 基本資料與同意書 +→ 自評問卷 +→ 顯示初步分級 +→ 推薦普測或課程 +→ 預約場館 / 社區據點 +→ 報到 QR Code +→ 完成普測與運動處方 +→ 4 週 / 12 週提醒回測 +→ 個人報告與獎勵點數 + +[cloudflare](https://health-platform-8u1.pages.dev/)