Backend (FastAPI): - REST API: auth, plants, beds, plantings - CRUD layer with CRUDBase - Pydantic v2 schemas for all entities - Alembic migration: complete schema + all enums - Seed data: 28 global plants + 15 compatibilities Frontend (Vue 3 + PrimeVue): - Axios client with JWT interceptor + auto-refresh - Pinia stores: auth, beds, plants - Views: Login, Beds, BedDetail, PlantLibrary - Components: AppLayout, BedForm, PlantingForm, PlantForm Docker: - docker-compose.yml (production) - docker-compose.dev.yml (development with hot-reload) - Nginx config with SPA fallback + API proxy - Multi-stage frontend Dockerfile - .env.example, .gitignore Version: 1.0.0-alpha
105 lines
2.9 KiB
Vue
105 lines
2.9 KiB
Vue
<template>
|
|
<div class="login-wrapper">
|
|
<div class="login-card">
|
|
<div class="login-header">
|
|
<i class="pi pi-leaf" style="font-size: 2.5rem; color: var(--green-500)" />
|
|
<h1>Gartenmanager</h1>
|
|
<p>Bitte melden Sie sich an</p>
|
|
</div>
|
|
|
|
<form @submit.prevent="handleLogin">
|
|
<div class="field">
|
|
<label for="email">E-Mail</label>
|
|
<InputText
|
|
id="email"
|
|
v-model="form.email"
|
|
type="email"
|
|
autocomplete="email"
|
|
:disabled="loading"
|
|
required
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="password">Passwort</label>
|
|
<Password
|
|
id="password"
|
|
v-model="form.password"
|
|
:feedback="false"
|
|
toggle-mask
|
|
:disabled="loading"
|
|
required
|
|
class="w-full"
|
|
input-class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<Message v-if="errorMsg" severity="error" :closable="false">{{ errorMsg }}</Message>
|
|
|
|
<Button
|
|
type="submit"
|
|
label="Anmelden"
|
|
icon="pi pi-sign-in"
|
|
:loading="loading"
|
|
class="w-full mt-2"
|
|
/>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import InputText from 'primevue/inputtext'
|
|
import Password from 'primevue/password'
|
|
import Button from 'primevue/button'
|
|
import Message from 'primevue/message'
|
|
|
|
const auth = useAuthStore()
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
const form = ref({ email: '', password: '' })
|
|
const loading = ref(false)
|
|
const errorMsg = ref('')
|
|
|
|
async function handleLogin() {
|
|
loading.value = true
|
|
errorMsg.value = ''
|
|
try {
|
|
await auth.login(form.value.email, form.value.password)
|
|
const redirect = route.query.redirect || '/beete'
|
|
router.push(redirect)
|
|
} catch (err) {
|
|
errorMsg.value = err.response?.data?.detail || 'Anmeldung fehlgeschlagen.'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.login-wrapper {
|
|
min-height: 100vh; display: flex;
|
|
align-items: center; justify-content: center;
|
|
background: var(--surface-ground);
|
|
}
|
|
.login-card {
|
|
background: var(--surface-card);
|
|
border-radius: 12px;
|
|
padding: 2.5rem;
|
|
width: 100%; max-width: 420px;
|
|
box-shadow: 0 4px 24px rgba(0,0,0,.1);
|
|
}
|
|
.login-header { text-align: center; margin-bottom: 2rem; }
|
|
.login-header h1 { font-size: 1.6rem; font-weight: 700; color: var(--green-700); margin: 0.5rem 0 0.25rem; }
|
|
.login-header p { color: var(--text-color-secondary); font-size: 0.9rem; }
|
|
.field { margin-bottom: 1.25rem; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
label { font-size: 0.875rem; font-weight: 600; color: var(--text-color); }
|
|
.w-full { width: 100%; }
|
|
.mt-2 { margin-top: 0.5rem; }
|
|
</style>
|