Files
sports-empower-hub/app/citizen-frontend/src/pages/register.vue
T
2026-10-05 10:11:04 +08:00

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>