354 lines
7.8 KiB
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> |