218 lines
5.4 KiB
Vue
218 lines
5.4 KiB
Vue
<template>
|
||
<div class="login-page">
|
||
<div class="container login-shell">
|
||
<div class="login-visual" aria-hidden="true">
|
||
<p class="visual-brand">گام نو</p>
|
||
<p class="visual-text">کلاسها و گواهینامههایتان در یک نگاه.</p>
|
||
<div class="visual-steps">
|
||
<span></span><span></span><span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<form class="login-form" @submit.prevent="handleLogin">
|
||
<h1>ورود به حساب</h1>
|
||
<p class="form-lead">نام کاربری و رمز عبور خود را وارد کنید.</p>
|
||
|
||
<div v-if="formError" class="alert" role="alert">{{ formError }}</div>
|
||
|
||
<div class="field">
|
||
<label for="username">نام کاربری</label>
|
||
<input
|
||
id="username"
|
||
v-model.trim="form.username"
|
||
type="text"
|
||
autocomplete="username"
|
||
placeholder="نام کاربری"
|
||
/>
|
||
<span v-if="errors.username" class="field-error">{{ errors.username }}</span>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="password">رمز عبور</label>
|
||
<input
|
||
id="password"
|
||
v-model="form.password"
|
||
type="password"
|
||
autocomplete="current-password"
|
||
placeholder="رمز عبور"
|
||
/>
|
||
<span v-if="errors.password" class="field-error">{{ errors.password }}</span>
|
||
</div>
|
||
|
||
<button class="btn btn-ink btn-block" type="submit" :disabled="loading">
|
||
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
||
{{ loading ? 'در حال ورود…' : 'ورود' }}
|
||
</button>
|
||
|
||
<RouterLink to="/" class="back-link">بازگشت به صفحه اصلی</RouterLink>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref } from 'vue'
|
||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||
import { useAuthStore } from '@/stores/authStore'
|
||
import { getApiErrorMessage } from '@/utils/format'
|
||
|
||
const auth = useAuthStore()
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
const form = reactive({ username: '', password: '' })
|
||
const errors = reactive({ username: '', password: '' })
|
||
const formError = ref('')
|
||
const loading = ref(false)
|
||
|
||
function validate() {
|
||
errors.username = form.username ? '' : 'نام کاربری الزامی است'
|
||
errors.password = form.password ? '' : 'رمز عبور الزامی است'
|
||
return !errors.username && !errors.password
|
||
}
|
||
|
||
async function handleLogin() {
|
||
formError.value = ''
|
||
if (!validate()) return
|
||
|
||
loading.value = true
|
||
try {
|
||
await auth.login({
|
||
username: form.username,
|
||
password: form.password
|
||
})
|
||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/account'
|
||
router.push(redirect)
|
||
} catch (error) {
|
||
formError.value = getApiErrorMessage(error, 'نام کاربری یا رمز عبور نادرست است.')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.login-page {
|
||
min-height: calc(100vh - var(--header-h));
|
||
padding: 6.5rem 0 3rem;
|
||
background:
|
||
radial-gradient(circle at 85% 15%, rgba(232, 163, 23, 0.18), transparent 35%),
|
||
linear-gradient(180deg, var(--canvas), var(--canvas-deep));
|
||
}
|
||
|
||
.login-shell {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0;
|
||
max-width: 920px;
|
||
border-radius: 1.75rem;
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid rgba(20, 53, 43, 0.08);
|
||
background: var(--chalk);
|
||
}
|
||
|
||
.login-visual {
|
||
background:
|
||
linear-gradient(160deg, rgba(12, 34, 27, 0.88), rgba(45, 106, 79, 0.85)),
|
||
url('https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1200&q=80') center/cover;
|
||
color: var(--chalk);
|
||
padding: 2.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
min-height: 28rem;
|
||
}
|
||
|
||
.visual-brand {
|
||
font-family: var(--font-display);
|
||
font-size: 3rem;
|
||
font-weight: 700;
|
||
color: var(--saffron);
|
||
line-height: 1;
|
||
}
|
||
|
||
.visual-text {
|
||
margin-top: 0.75rem;
|
||
color: rgba(247, 251, 248, 0.85);
|
||
max-width: 16rem;
|
||
}
|
||
|
||
.visual-steps {
|
||
display: flex;
|
||
gap: 0.55rem;
|
||
margin-top: 1.75rem;
|
||
}
|
||
|
||
.visual-steps span {
|
||
width: 0.55rem;
|
||
height: 0.55rem;
|
||
border-radius: 50%;
|
||
background: var(--saffron);
|
||
animation: pulse 1.6s ease-in-out infinite;
|
||
}
|
||
|
||
.visual-steps span:nth-child(2) { animation-delay: 0.2s; }
|
||
.visual-steps span:nth-child(3) { animation-delay: 0.4s; }
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); opacity: 0.55; }
|
||
50% { transform: scale(1.25); opacity: 1; }
|
||
}
|
||
|
||
.login-form {
|
||
padding: 2.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
}
|
||
|
||
.login-form h1 {
|
||
font-family: var(--font-display);
|
||
font-size: 2.2rem;
|
||
}
|
||
|
||
.form-lead {
|
||
color: var(--ink-soft);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.alert {
|
||
background: rgba(180, 35, 24, 0.08);
|
||
color: var(--danger);
|
||
border: 1px solid rgba(180, 35, 24, 0.2);
|
||
border-radius: 0.85rem;
|
||
padding: 0.75rem 0.9rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.btn .spinner {
|
||
width: 1.1rem;
|
||
height: 1.1rem;
|
||
border-width: 2px;
|
||
}
|
||
|
||
.back-link {
|
||
text-align: center;
|
||
color: var(--ink-soft);
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.back-link:hover {
|
||
color: var(--leaf);
|
||
}
|
||
|
||
@media (max-width: 800px) {
|
||
.login-shell {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.login-visual {
|
||
min-height: 12rem;
|
||
}
|
||
}
|
||
</style>
|