fix: 自評去識別化

This commit is contained in:
skytek_xinliang
2026-09-29 09:42:47 +08:00
parent 7eb37a37fb
commit 86ffaf54be
5 changed files with 39 additions and 343 deletions
+2 -2
View File
@@ -209,10 +209,10 @@ const existingDraft = readGuestSelfAssessmentDraft();
const routePhone = normalizePhone(String(route.query.phone ?? ""));
const form = reactive({
phone: routePhone || existingDraft?.profile.phone || "",
phone: routePhone,
password: "",
passwordConfirmation: "",
consent: existingDraft?.profile.consent ?? false,
consent: false,
});
const loading = ref(false);
@@ -9,7 +9,7 @@
v-for="(stepName, idx) in stepNames"
:key="idx"
class="text-center"
cols="3"
cols="4"
>
<v-avatar
class="text-white font-weight-bold mb-1 elevation-1"
@@ -54,149 +54,8 @@
{{ errorMessage }}
</v-alert>
<!-- 步驟 1:基本資料 -->
<!-- 步驟 1:活動量與自評 -->
<div v-if="currentStep === 0">
<h2
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
>
<v-icon
class="mr-2"
color="secondary"
icon="mdi-account-circle"
/>
基本資料確認
</h2>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model="profileForm.name"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="姓名"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="profileForm.phone"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="手機號碼"
type="tel"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="profileForm.gender"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
:items="genderOptions"
label="性別"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-date-input
v-model="birthdayDate"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="生日"
prepend-icon=""
prepend-inner-icon="mdi-calendar"
variant="outlined"
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model.number="profileForm.height"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="身高 (公分)"
type="number"
variant="outlined"
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model.number="profileForm.weight"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="體重 (公斤)"
type="number"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="profileForm.email"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="Email(選填)"
type="email"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="profileForm.emergencyContact"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="緊急聯絡人(選填)"
variant="outlined"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="profileForm.emergencyPhone"
class="mb-3"
color="primary"
density="comfortable"
hide-details="auto"
label="緊急聯絡電話(選填)"
type="tel"
variant="outlined"
/>
</v-col>
</v-row>
<v-checkbox
v-model="profileForm.consent"
class="mt-2 text-caption"
color="primary"
density="compact"
hide-details
label="我已閱讀並同意個資授權條款"
/>
</div>
<!-- 步驟 2:活動量與自評 -->
<div v-if="currentStep === 1">
<h2
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
>
@@ -305,8 +164,8 @@
</v-row>
</div>
<!-- 步驟 3:身體狀況自評 -->
<div v-if="currentStep === 2">
<!-- 步驟 2:身體狀況自評 -->
<div v-if="currentStep === 1">
<h2
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
>
@@ -373,8 +232,8 @@
</v-row>
</div>
<!-- 步驟 4:自評結果 -->
<div v-if="currentStep === 3" class="text-center py-4">
<!-- 步驟 3:自評結果 -->
<div v-if="currentStep === 2" class="text-center py-4">
<v-avatar
class="elevation-2 mb-4"
:color="getResultColor(resultLevel)"
@@ -444,7 +303,7 @@
<v-card-actions class="px-5 py-4 d-flex justify-between">
<v-btn
v-if="currentStep > 0 && currentStep < 3"
v-if="currentStep > 0 && currentStep < 2"
class="rounded-lg px-4"
color="primary"
variant="outlined"
@@ -456,10 +315,9 @@
<v-spacer />
<v-btn
v-if="currentStep < 2"
v-if="currentStep < 1"
class="rounded-lg px-6"
color="primary"
:disabled="currentStep === 0 && !profileForm.consent"
variant="flat"
@click="goToNextStep"
>
@@ -467,7 +325,7 @@
</v-btn>
<v-btn
v-else-if="currentStep === 2"
v-else-if="currentStep === 1"
class="rounded-lg px-6 text-white"
color="secondary"
variant="flat"
@@ -493,7 +351,7 @@
<!-- 返回按鈕 (結果頁專用) -->
<v-btn
v-if="currentStep === 3"
v-if="currentStep === 2"
block
class="rounded-lg py-2"
color="primary"
@@ -507,18 +365,16 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import { onMounted, reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import type {
ChronicDisease,
ExerciseFrequency,
Gender,
PainLevel,
RiskLevel,
SelfAssessmentRequest,
WantedImprovement,
} from "@/api/types";
import { CONSENT_VERSION } from "@/api/types";
import Layout from "@/components/layout/DefaultLayout.vue";
import PageHeading from "@/components/layout/PageHeading.vue";
import { APPOINTMENT_ENABLED } from "@/config/features";
@@ -535,36 +391,7 @@ const router = useRouter();
const currentStep = ref(0);
const appointmentLoading = ref(false);
const errorMessage = ref("");
const stepNames = ["基本資料", "功能狀態", "健康限制", "自評結果"];
const profileForm = reactive<{
name: string;
phone: string;
birthday: string;
gender: Gender | "";
height: number;
weight: number;
email: string;
emergencyContact: string;
emergencyPhone: string;
consent: boolean;
}>({
name: store.userProfile.name,
phone: store.userProfile.phone,
birthday: store.userProfile.birthday,
gender:
store.userProfile.gender === "男"
? "male"
: store.userProfile.gender === "女"
? "female"
: "",
height: store.userProfile.height,
weight: store.userProfile.weight,
email: store.userProfile.email,
emergencyContact: store.userProfile.emergencyContact,
emergencyPhone: store.userProfile.emergencyPhone,
consent: store.userProfile.consent,
});
const stepNames = ["功能狀態", "健康限制", "自評結果"];
const assessmentForm = reactive<{
exerciseFrequency: ExerciseFrequency | "";
@@ -582,10 +409,6 @@ const assessmentForm = reactive<{
const resultLevel = ref<RiskLevel | "">(store.selfAssessment.scoreLevel);
const genderOptions = [
{ title: "男", value: "male" as const },
{ title: "女", value: "female" as const },
];
const painOptions = [
{ title: "無", value: "none" as const },
{ title: "輕微", value: "mild" as const },
@@ -617,59 +440,21 @@ const diseaseOptions = [
{ title: "骨質疏鬆", value: "osteoporosis" as const },
];
const birthdayDate = computed<Date | null>({
get: () => parseDateString(profileForm.birthday),
set: (value) => {
profileForm.birthday = formatDateString(value);
},
});
onMounted(() => {
const draft = readGuestSelfAssessmentDraft();
if (!draft) {
return;
}
profileForm.name = draft.profile.name;
profileForm.phone = draft.profile.phone;
profileForm.birthday = draft.profile.birthday;
profileForm.gender = draft.profile.gender;
profileForm.height = draft.profile.heightCm;
profileForm.weight = draft.profile.weightKg;
profileForm.email = draft.profile.email ?? "";
profileForm.emergencyContact = draft.profile.emergencyContact ?? "";
profileForm.emergencyPhone = draft.profile.emergencyPhone ?? "";
profileForm.consent = draft.profile.consent;
assessmentForm.exerciseFrequency = draft.assessment.exerciseFrequency;
assessmentForm.wantedImprovements = [...draft.assessment.wantedImprovements];
assessmentForm.hasFalls = draft.assessment.hasFalls;
assessmentForm.painLevel = draft.assessment.painLevel;
assessmentForm.chronicDiseases = [...draft.assessment.chronicDiseases];
resultLevel.value = draft.provisionalRiskLevel;
currentStep.value = 3;
currentStep.value = 2;
});
function parseDateString(value: string) {
const [year, month, day] = value.split("-").map(Number);
if (!year || !month || !day) {
return null;
}
return new Date(year, month - 1, day);
}
function formatDateString(value: Date | null) {
if (!value) {
return "";
}
const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, "0");
const day = String(value.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function toggleImprovement(improvement: WantedImprovement) {
const index = assessmentForm.wantedImprovements.indexOf(improvement);
if (index === -1) {
@@ -699,26 +484,9 @@ function goToNextStep() {
scrollToPageTop();
}
watch(profileForm, clearErrorMessage);
watch(assessmentForm, clearErrorMessage);
function buildAssessmentRequest(): SelfAssessmentRequest | null {
if (
!profileForm.name.trim() ||
!/^09\d{8}$/.test(profileForm.phone.replace(/\D/g, "")) ||
!profileForm.birthday ||
!profileForm.gender ||
profileForm.height < 50 ||
profileForm.height > 250 ||
profileForm.weight < 20 ||
profileForm.weight > 300 ||
!profileForm.consent
) {
errorMessage.value = "請完整填寫基本資料,並同意個資授權條款。";
currentStep.value = 0;
return null;
}
if (!assessmentForm.exerciseFrequency || !assessmentForm.painLevel) {
errorMessage.value = "請完成所有自評題目後再送出。";
return null;
@@ -736,26 +504,12 @@ function buildAssessmentRequest(): SelfAssessmentRequest | null {
function handleSubmit() {
errorMessage.value = "";
const assessment = buildAssessmentRequest();
if (!assessment || !profileForm.gender) {
if (!assessment) {
return;
}
const normalizedPhone = profileForm.phone.replace(/\D/g, "");
const provisionalRiskLevel = calculateProvisionalRiskLevel(assessment);
const draft: GuestSelfAssessmentDraft = {
profile: {
name: profileForm.name.trim(),
phone: normalizedPhone,
birthday: profileForm.birthday,
gender: profileForm.gender,
heightCm: profileForm.height,
weightKg: profileForm.weight,
email: profileForm.email.trim() || null,
emergencyContact: profileForm.emergencyContact.trim() || null,
emergencyPhone: profileForm.emergencyPhone.replace(/\D/g, "") || null,
consent: profileForm.consent,
consentVersion: CONSENT_VERSION,
},
assessment,
provisionalRiskLevel,
};
@@ -763,7 +517,7 @@ function handleSubmit() {
saveGuestSelfAssessmentDraft(draft);
store.setProvisionalSelfAssessment(assessment);
resultLevel.value = provisionalRiskLevel;
currentStep.value = 3;
currentStep.value = 2;
scrollToPageTop();
}