302 lines
8.9 KiB
Vue
302 lines
8.9 KiB
Vue
<template>
|
|
<v-app class="bg-background">
|
|
<v-container>
|
|
<v-row justify="center" no-gutters>
|
|
<v-col cols="12" sm="8" md="6">
|
|
<v-card class="bg-transparent mb-4" variant="flat">
|
|
<div class="text-center">
|
|
<BrandAvatar />
|
|
<v-card-title class="text-primary font-weight-bold">
|
|
建立帳號
|
|
</v-card-title>
|
|
</div>
|
|
</v-card>
|
|
|
|
<v-card class="rounded-lg" color="surface">
|
|
<v-card-title>註冊資料</v-card-title>
|
|
<v-card-text>
|
|
<v-alert
|
|
v-if="errorMessage"
|
|
class="mb-4"
|
|
density="compact"
|
|
type="error"
|
|
variant="tonal"
|
|
>
|
|
{{ errorMessage }}
|
|
</v-alert>
|
|
|
|
<v-form @submit.prevent="handleRegister">
|
|
<v-text-field
|
|
v-model="form.phone"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="手機號碼"
|
|
persistent-hint
|
|
type="tel"
|
|
variant="outlined"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="form.password"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="登入密碼"
|
|
minlength="8"
|
|
type="password"
|
|
variant="outlined"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="form.passwordConfirmation"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="確認密碼"
|
|
minlength="8"
|
|
type="password"
|
|
variant="outlined"
|
|
/>
|
|
|
|
<v-divider class="my-5" />
|
|
|
|
<!-- <v-text-field
|
|
v-model="form.name"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="姓名"
|
|
variant="outlined"
|
|
/> -->
|
|
|
|
<!-- <v-row>
|
|
<v-col cols="12" sm="6">
|
|
<v-select
|
|
v-model="form.gender"
|
|
class="mb-3"
|
|
color="primary"
|
|
:items="genderOptions"
|
|
label="性別"
|
|
variant="outlined"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6">
|
|
<v-date-input
|
|
v-model="birthdayDate"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="生日"
|
|
prepend-icon=""
|
|
prepend-inner-icon="mdi-calendar"
|
|
variant="outlined"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="6">
|
|
<v-text-field
|
|
v-model.number="form.heightCm"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="身高(公分)"
|
|
type="number"
|
|
variant="outlined"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="6">
|
|
<v-text-field
|
|
v-model.number="form.weightKg"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="體重(公斤)"
|
|
type="number"
|
|
variant="outlined"
|
|
/>
|
|
</v-col>
|
|
</v-row> -->
|
|
|
|
<!-- <v-text-field
|
|
v-model="form.email"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="Email(選填)"
|
|
type="email"
|
|
variant="outlined"
|
|
/> -->
|
|
|
|
<!-- <v-row>
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field
|
|
v-model="form.emergencyContact"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="緊急聯絡人(選填)"
|
|
variant="outlined"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field
|
|
v-model="form.emergencyPhone"
|
|
class="mb-3"
|
|
color="primary"
|
|
label="緊急聯絡電話(選填)"
|
|
type="tel"
|
|
variant="outlined"
|
|
/>
|
|
</v-col>
|
|
</v-row> -->
|
|
|
|
<v-checkbox v-model="form.consent" class="mb-3" color="primary">
|
|
<template #label>
|
|
<span>
|
|
我已閱讀並同意
|
|
<button
|
|
class="text-primary text-decoration-underline"
|
|
type="button"
|
|
@click.stop="showPrivacyConsent = true"
|
|
>
|
|
個資授權條款
|
|
</button>
|
|
</span>
|
|
</template>
|
|
</v-checkbox>
|
|
|
|
<v-btn
|
|
block
|
|
class="rounded-lg font-weight-bold"
|
|
color="secondary"
|
|
:loading="loading"
|
|
type="submit"
|
|
>
|
|
完成註冊並繼續
|
|
</v-btn>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<v-card-actions class="justify-center pb-5">
|
|
<v-btn color="primary" to="/login" variant="text">
|
|
返回登入
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
|
|
<PrivacyConsentDialog v-model="showPrivacyConsent" />
|
|
</v-app>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, ref } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import type { RegisterRequest } from "@/api/types";
|
|
import { CONSENT_VERSION } from "@/api/types";
|
|
import BrandAvatar from "@/components/BrandAvatar.vue";
|
|
import PrivacyConsentDialog from "@/components/PrivacyConsentDialog.vue";
|
|
import { useAppStore } from "@/stores/app";
|
|
import {
|
|
getApiErrorStatus,
|
|
getApiErrorMessage,
|
|
isUnregisteredPhoneError,
|
|
} from "@/utils/api-error";
|
|
import { readGuestSelfAssessmentDraft } from "@/utils/guest-self-assessment";
|
|
|
|
const store = useAppStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const showPrivacyConsent = ref(false);
|
|
const existingDraft = readGuestSelfAssessmentDraft();
|
|
const routePhone = normalizePhone(String(route.query.phone ?? ""));
|
|
|
|
const form = reactive({
|
|
phone: routePhone || existingDraft?.profile.phone || "",
|
|
password: "",
|
|
passwordConfirmation: "",
|
|
consent: existingDraft?.profile.consent ?? false,
|
|
});
|
|
|
|
const loading = ref(false);
|
|
const errorMessage = ref("");
|
|
|
|
function normalizePhone(value: string) {
|
|
return value.replace(/\D/g, "");
|
|
}
|
|
|
|
function redirectTarget() {
|
|
const target = String(route.query.redirect ?? "");
|
|
return target.startsWith("/") && !target.startsWith("//")
|
|
? target
|
|
: existingDraft
|
|
? "/appointment"
|
|
: "/";
|
|
}
|
|
|
|
function validateForm(phone: string) {
|
|
if (!/^09\d{8}$/.test(phone)) {
|
|
return "請輸入正確的手機門號格式。";
|
|
}
|
|
if (form.password.length < 8) {
|
|
return "密碼至少需要 8 碼。";
|
|
}
|
|
if (form.password !== form.passwordConfirmation) {
|
|
return "兩次輸入的密碼不一致。";
|
|
}
|
|
if (!form.consent) {
|
|
return "請同意個資授權條款後再註冊。";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
async function handleRegister() {
|
|
errorMessage.value = "";
|
|
const phone = normalizePhone(form.phone);
|
|
const validationError = validateForm(phone);
|
|
if (validationError) {
|
|
errorMessage.value = validationError;
|
|
return;
|
|
}
|
|
|
|
const request: RegisterRequest = {
|
|
phone,
|
|
password: form.password,
|
|
consent: form.consent,
|
|
consentVersion: CONSENT_VERSION,
|
|
};
|
|
|
|
const currentDraft = readGuestSelfAssessmentDraft();
|
|
|
|
loading.value = true;
|
|
try {
|
|
if (!store.isLoggedIn) {
|
|
await store.register(request);
|
|
}
|
|
|
|
if (currentDraft) {
|
|
await store.persistGuestSelfAssessment();
|
|
}
|
|
|
|
await router.replace(redirectTarget());
|
|
} catch (error) {
|
|
if (isUnregisteredPhoneError(error)) {
|
|
errorMessage.value = "此手機尚未註冊,請完成註冊資料。";
|
|
} else if (getApiErrorStatus(error) === 409) {
|
|
await router.replace({
|
|
name: "login",
|
|
query: {
|
|
phone: form.phone,
|
|
redirect: redirectTarget(),
|
|
error: "phone-already-registered",
|
|
},
|
|
});
|
|
} else {
|
|
errorMessage.value = getApiErrorMessage(
|
|
error,
|
|
"註冊或保存資料失敗,草稿仍會保留,請稍後重試。",
|
|
);
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
</script>
|