dramaling-app/apps/web/src/views/auth/LoginView.vue

354 lines
7.8 KiB
Vue

<template>
<div class="login-view">
<BaseCard class="login-card">
<template #header>
<h2 class="login-title">歡迎回來</h2>
<p class="login-subtitle">登入你的 Drama Ling 帳戶</p>
<!-- 開發模式提示 -->
<div v-if="isDevelopment" class="dev-notice">
<q-banner class="bg-amber-1 text-amber-9 q-mb-md" rounded>
<template v-slot:avatar>
<q-icon name="developer_mode" color="amber" />
</template>
<div class="text-body2">
<strong>🚧 開發模式</strong><br>
使用測試帳戶登入:<br>
📧 <code>test@dramaling.com</code><br>
🔑 <code>test123</code>
</div>
<template v-slot:action>
<q-btn
flat
color="amber"
label="快速填入"
size="sm"
@click="fillTestCredentials"
/>
</template>
</q-banner>
</div>
</template>
<form @submit.prevent="handleLogin" class="login-form">
<BaseInput
v-model="form.email"
type="email"
label="電子郵件"
placeholder="請輸入電子郵件地址"
prefix-icon="email"
:error="errors.email"
:disabled="isLoading"
required
/>
<BaseInput
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
label="密碼"
placeholder="請輸入密碼"
prefix-icon="lock"
:suffix-icon="showPassword ? 'visibility_off' : 'visibility'"
:error="errors.password"
:disabled="isLoading"
required
@suffix-click="togglePassword"
/>
<div class="login-options">
<QCheckbox
v-model="form.rememberMe"
label="記住我"
:disabled="isLoading"
/>
<router-link
to="/auth/forgot-password"
class="forgot-password-link"
>
忘記密碼?
</router-link>
</div>
<BaseButton
type="submit"
variant="primary"
size="lg"
block
:loading="isLoading"
:disabled="!canSubmit"
>
登入
</BaseButton>
<div class="divider">
<span>或</span>
</div>
<BaseButton
variant="outline"
size="lg"
block
icon="login"
:disabled="isLoading"
@click="handleGoogleLogin"
>
使用 Google 登入
</BaseButton>
</form>
<template #footer>
<div class="register-prompt">
還沒有帳戶?
<router-link to="/auth/register" class="register-link">
立即註冊
</router-link>
</div>
</template>
</BaseCard>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useUIStore } from '@/stores/ui'
import BaseCard from '@/components/base/BaseCard.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import { isValidEmail } from '@/utils'
const router = useRouter()
const authStore = useAuthStore()
const uiStore = useUIStore()
// 表單狀態
const form = reactive({
email: '',
password: '',
rememberMe: false
})
const errors = reactive({
email: '',
password: ''
})
const isLoading = ref(false)
const showPassword = ref(false)
// 開發模式檢查
const isDevelopment = import.meta.env.DEV
// 計算屬性
const canSubmit = computed(() => {
return form.email &&
form.password &&
isValidEmail(form.email) &&
!isLoading.value
})
// 方法
const validateForm = () => {
errors.email = ''
errors.password = ''
if (!form.email) {
errors.email = '請輸入電子郵件地址'
return false
}
if (!isValidEmail(form.email)) {
errors.email = '請輸入有效的電子郵件地址'
return false
}
if (!form.password) {
errors.password = '請輸入密碼'
return false
}
if (form.password.length < 6) {
errors.password = '密碼長度至少 6 個字元'
return false
}
return true
}
const handleLogin = async () => {
if (!validateForm()) return
isLoading.value = true
authStore.clearError()
try {
const result = await authStore.login({
email: form.email,
password: form.password,
rememberMe: form.rememberMe
})
if (result.success) {
uiStore.showSuccessToast('登入成功', '歡迎回來!')
// 跳轉到原本要去的頁面或首頁
const redirectPath = authStore.redirectPath || '/learning'
router.push(redirectPath)
} else {
uiStore.showErrorToast('登入失敗', result.error)
// 根據錯誤類型設定特定錯誤訊息
if (result.error?.includes('email')) {
errors.email = result.error
} else if (result.error?.includes('password')) {
errors.password = result.error
}
}
} catch (error) {
uiStore.showErrorToast('登入失敗', '發生未知錯誤,請稍後再試')
} finally {
isLoading.value = false
}
}
const handleGoogleLogin = async () => {
uiStore.showInfoToast('功能開發中', '第三方登入功能即將推出')
// TODO: 實現 Google 登入
}
const togglePassword = () => {
showPassword.value = !showPassword.value
}
// 填入測試帳戶資訊 (僅開發模式)
const fillTestCredentials = () => {
if (import.meta.env.DEV) {
form.email = 'test@dramaling.com'
form.password = 'test123'
form.rememberMe = true
clearErrors()
}
}
// 清理錯誤訊息
const clearErrors = () => {
authStore.clearError()
errors.email = ''
errors.password = ''
}
</script>
<style lang="scss" scoped>
.login-view {
width: 100%;
max-width: 400px;
margin: 0 auto;
}
.login-card {
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba($divider, 0.2);
backdrop-filter: blur(10px);
}
.login-title {
font-size: $text-2xl;
font-weight: 700;
color: $text-primary;
margin: 0 0 $space-2 0;
text-align: center;
}
.login-subtitle {
font-size: $text-sm;
color: $text-secondary;
margin: 0;
text-align: center;
}
.login-form {
display: flex;
flex-direction: column;
gap: $space-6;
}
.login-options {
display: flex;
justify-content: space-between;
align-items: center;
margin: $space-2 0;
.q-checkbox {
font-size: $text-sm;
}
}
.forgot-password-link {
font-size: $text-sm;
color: $primary-teal;
text-decoration: none;
transition: color 0.3s ease;
&:hover {
color: $primary-teal-light;
text-decoration: underline;
}
}
.divider {
position: relative;
text-align: center;
margin: $space-4 0;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: $divider;
}
span {
background: $card-background;
padding: 0 $space-4;
font-size: $text-sm;
color: $text-secondary;
}
}
.register-prompt {
text-align: center;
font-size: $text-sm;
color: $text-secondary;
.register-link {
color: $primary-teal;
text-decoration: none;
font-weight: 600;
margin-left: $space-1;
transition: color 0.3s ease;
&:hover {
color: $primary-teal-light;
text-decoration: underline;
}
}
}
// 響應式設計
@include respond-to(xs) {
.login-view {
padding: $space-4;
}
.login-options {
flex-direction: column;
gap: $space-3;
align-items: flex-start;
}
}
</style>