modified: main_dc/yalarba/easySite/easySite/app/components/forms/LoginForm.vue

modified:   main_dc/yalarba/easySite/easySite/app/components/forms/ObjectForm.vue
	modified:   main_dc/yalarba/easySite/easySite/app/components/layout/Header.vue
	modified:   main_dc/yalarba/easySite/easySite/app/components/objects/ObjectCard.vue
	new file:   main_dc/yalarba/easySite/easySite/app/composables/objects/TourCard.vue
	modified:   main_dc/yalarba/easySite/easySite/app/composables/useAuth.ts
	modified:   main_dc/yalarba/easySite/easySite/app/composables/useObjects.ts
	modified:   main_dc/yalarba/easySite/easySite/app/layouts/auth.vue
	modified:   main_dc/yalarba/easySite/easySite/app/layouts/default.vue
	modified:   main_dc/yalarba/easySite/easySite/app/middleware/auth.ts
	modified:   main_dc/yalarba/easySite/easySite/app/pages/auth/login.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/index.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/objects/[id]/edit.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/objects/[id]/index.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/objects/create.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/objects/index.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/objects/my-objects.vue
add pages for easysite102.ru and a lot of ather thinks
This commit is contained in:
2025-10-29 01:51:31 +05:00
parent 5c609a3641
commit b3d7de5857
17 changed files with 1720 additions and 150 deletions
@@ -0,0 +1,72 @@
<template>
<form @submit.prevent="handleSubmit" class="space-y-6">
<div class="form-group">
<label class="form-label">Email</label>
<input
v-model="form.email"
type="email"
class="form-input"
placeholder="your@email.com"
required
>
</div>
<div class="form-group">
<label class="form-label">Пароль</label>
<input
v-model="form.password"
type="password"
class="form-input"
placeholder="Введите пароль"
required
>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<input type="checkbox" class="rounded border-gray-300">
<span class="ml-2 text-sm text-gray-600">Запомнить меня</span>
</label>
<a href="#" class="text-sm text-primary-600 hover:text-primary-700">
Забыли пароль?
</a>
</div>
<button
type="submit"
class="btn btn-primary w-full"
:disabled="loading"
>
<span v-if="loading">Вход...</span>
<span v-else>Войти</span>
</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const { login } = useAuth()
const form = ref({
email: '',
password: ''
})
const loading = ref(false)
const handleSubmit = async () => {
loading.value = true
try {
await login(form.value)
await navigateTo('/profile')
} catch (error) {
console.error('Login error:', error)
// Здесь можно добавить уведомление об ошибке
} finally {
loading.value = false
}
}
</script>
@@ -0,0 +1,234 @@
<template>
<form @submit.prevent="handleSubmit" class="space-y-8">
<!-- Основная информация -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Основная информация</h2>
</div>
<div class="card-body space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-group">
<label class="form-label">Название объекта *</label>
<input v-model="form.title" type="text" class="form-input"
placeholder="Например: Отель 'Морской бриз'" required>
</div>
<div class="form-group">
<label class="form-label">Тип объекта *</label>
<select v-model="form.type" class="form-select" required>
<option value="">Выберите тип</option>
<option value="hotel">Отель</option>
<option value="apartment">Апартаменты</option>
<option value="villa">Вилла</option>
<option value="camping">Кемпинг</option>
<option value="restaurant">Ресторан</option>
<option value="attraction">Достопримечательность</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Описание *</label>
<textarea v-model="form.description" rows="4" class="form-input"
placeholder="Подробное описание объекта, его преимуществ и особенностей..." required></textarea>
</div>
</div>
</div>
<!-- Местоположение -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Местоположение</h2>
</div>
<div class="card-body space-y-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="form-group">
<label class="form-label">Страна *</label>
<input v-model="form.country" type="text" class="form-input" placeholder="Россия" required>
</div>
<div class="form-group">
<label class="form-label">Город *</label>
<input v-model="form.city" type="text" class="form-input" placeholder="Москва" required>
</div>
<div class="form-group">
<label class="form-label">Категория *</label>
<select v-model="form.category" class="form-select" required>
<option value="">Выберите категорию</option>
<option value="accommodation">Проживание</option>
<option value="food">Питание</option>
<option value="entertainment">Развлечения</option>
<option value="culture">Культура</option>
<option value="nature">Природа</option>
<option value="shopping">Шоппинг</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Адрес *</label>
<input v-model="form.address" type="text" class="form-input" placeholder="ул. Примерная, 123"
required>
</div>
</div>
</div>
<!-- Цены и услуги -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Цены и услуги</h2>
</div>
<div class="card-body space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-group">
<label class="form-label">Цена *</label>
<input v-model="form.price" type="number" class="form-input" placeholder="5000" min="0"
required>
</div>
<div class="form-group">
<label class="form-label">Единица измерения цены *</label>
<select v-model="form.priceUnit" class="form-select" required>
<option value="">Выберите единицу</option>
<option value="per_night">За ночь</option>
<option value="per_person">За человека</option>
<option value="fixed">Фиксированная цена</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Услуги и удобства</label>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mt-2">
<label v-for="amenity in availableAmenities" :key="amenity.value"
class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" :value="amenity.value" v-model="form.amenities"
class="rounded border-gray-300">
<span class="text-sm">{{ amenity.label }}</span>
</label>
</div>
</div>
</div>
</div>
<!-- Контактная информация -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Контактная информация</h2>
</div>
<div class="card-body space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-group">
<label class="form-label">Email для связи *</label>
<input v-model="form.contactEmail" type="email" class="form-input"
placeholder="contact@example.com" required>
</div>
<div class="form-group">
<label class="form-label">Телефон для связи *</label>
<input v-model="form.contactPhone" type="tel" class="form-input" placeholder="+7 999 123-45-67"
required>
</div>
</div>
<div class="form-group">
<label class="form-label">Веб-сайт (необязательно)</label>
<input v-model="form.website" type="url" class="form-input" placeholder="https://example.com">
</div>
</div>
</div>
<!-- Кнопки действий -->
<div class="flex justify-end gap-4 pt-6">
<button type="button" @click="$emit('cancel')" class="btn btn-outline" :disabled="props.loading">
Отмена
</button>
<button type="submit" class="btn btn-primary" :disabled="props.loading">
<span v-if="props.loading">Сохранение...</span>
<span v-else>{{ props.object ? 'Обновить' : 'Создать' }} объект</span>
</button>
</div>
</form>
</template>
<script setup lang="ts">
import type { ObjectItem } from '~/composables/useObjects'
interface Props {
object?: ObjectItem | null
loading?: boolean
}
interface Emits {
(e: 'submit', data: any): void
(e: 'cancel'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// Доступные услуги
const availableAmenities = [
{ value: 'wifi', label: 'Wi-Fi' },
{ value: 'parking', label: 'Парковка' },
{ value: 'breakfast', label: 'Завтрак' },
{ value: 'pool', label: 'Бассейн' },
{ value: 'spa', label: 'СПА' },
{ value: 'gym', label: 'Тренажерный зал' },
{ value: 'air_conditioning', label: 'Кондиционер' },
{ value: 'heating', label: 'Отопление' },
{ value: 'kitchen', label: 'Кухня' },
{ value: 'washing_machine', label: 'Стиральная машина' },
{ value: 'tv', label: 'Телевизор' },
{ value: 'elevator', label: 'Лифт' }
]
// Форма данных
const form = ref({
title: '',
description: '',
type: '',
category: '',
address: '',
city: '',
country: '',
price: 0,
priceUnit: '',
amenities: [] as string[],
contactEmail: '',
contactPhone: '',
website: ''
})
// Заполняем форму данными при редактировании
watch(() => props.object, (object) => {
if (object) {
form.value = {
title: object.title,
description: object.description,
type: object.type,
category: object.category,
address: object.address,
city: object.city,
country: object.country,
price: object.price,
priceUnit: object.priceUnit,
amenities: object.amenities || [],
contactEmail: object.contactEmail,
contactPhone: object.contactPhone,
website: object.website || ''
}
}
}, { immediate: true })
const handleSubmit = () => {
// Валидация
if (!form.value.title || !form.value.type || !form.value.description) {
alert('Пожалуйста, заполните все обязательные поля')
return
}
emit('submit', form.value)
}
</script>
@@ -0,0 +1,64 @@
<template>
<header class="bg-white shadow-sm sticky top-0 z-50">
<div class="container">
<div class="flex justify-between items-center py-4">
<!-- Логотип -->
<NuxtLink to="/" class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center">
<span class="text-white font-bold text-lg">T</span>
</div>
<span class="font-bold text-2xl">TravelEasy</span>
</NuxtLink>
<!-- Навигация -->
<nav class="hidden md:flex gap-6">
<NuxtLink
to="/"
class="nav-link"
:class="{ active: $route.path === '/' }"
>
Главная
</NuxtLink>
<NuxtLink
to="/objects"
class="nav-link"
:class="{ active: $route.path.startsWith('/objects') }"
>
Объекты
</NuxtLink>
<NuxtLink
to="/objects/my-objects"
class="nav-link"
>
Мои объекты
</NuxtLink>
<NuxtLink
to="/objects/create"
class="nav-link"
>
Добавить объект
</NuxtLink>
</nav>
<!-- Действия пользователя -->
<div class="flex items-center gap-4">
<NuxtLink to="/objects/create" class="btn btn-primary hidden sm:block">
Добавить объект
</NuxtLink>
<div class="flex gap-2">
<NuxtLink to="/auth/login" class="btn btn-outline">
Войти
</NuxtLink>
<NuxtLink to="/auth/register" class="btn btn-primary">
Регистрация
</NuxtLink>
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
// Убрана проверка авторизации - все страницы доступны
</script>
@@ -0,0 +1,116 @@
<template>
<div class="card cursor-pointer hover:shadow-lg transition-shadow" @click="$emit('click')">
<div class="relative">
<img
:src="object.images[0] || '/images/placeholder.jpg'"
:alt="object.title"
class="w-full h-48 object-cover"
>
<div class="absolute top-3 right-3">
<span class="badge badge-primary">
{{ getTypeLabel(object.type) }}
</span>
</div>
</div>
<div class="card-body">
<h3 class="font-semibold text-lg mb-2 line-clamp-2">{{ object.title }}</h3>
<p class="text-gray-600 mb-3 flex items-center gap-1">
📍 {{ object.city }}, {{ object.country }}
</p>
<p class="text-sm text-gray-600 mb-4 line-clamp-2">
{{ object.description }}
</p>
<div class="flex flex-wrap gap-1 mb-4">
<span
v-for="amenity in object.amenities.slice(0, 3)"
:key="amenity"
class="badge badge-secondary text-xs"
>
{{ getAmenityLabel(amenity) }}
</span>
<span
v-if="object.amenities.length > 3"
class="badge badge-outline text-xs"
>
+{{ object.amenities.length - 3 }}
</span>
</div>
<div class="flex items-center justify-between">
<div class="text-lg font-semibold text-primary-600">
{{ formatPrice(object.price) }}
<span class="text-sm text-gray-500">{{ getPriceUnitLabel(object.priceUnit) }}</span>
</div>
<div v-if="object.rating" class="flex items-center gap-1">
<span class="text-yellow-500"></span>
<span class="font-medium">{{ object.rating }}</span>
<span class="text-gray-500 text-sm">({{ object.reviewCount }})</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { ObjectItem } from '~/composables/useObjects'
interface Props {
object: ObjectItem
}
defineProps<Props>()
defineEmits<{
click: []
}>()
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
hotel: 'Отель',
apartment: 'Апартаменты',
villa: 'Вилла',
camping: 'Кемпинг',
restaurant: 'Ресторан',
attraction: 'Достопримечательность'
}
return types[type] || type
}
const getAmenityLabel = (amenity: string) => {
const amenities: Record<string, string> = {
wifi: 'Wi-Fi',
parking: 'Парковка',
breakfast: 'Завтрак',
pool: 'Бассейн',
spa: 'СПА',
kitchen: 'Кухня',
washing_machine: 'Стиральная машина'
}
return amenities[amenity] || amenity
}
const getPriceUnitLabel = (unit: string) => {
const units: Record<string, string> = {
per_night: '/ночь',
per_person: '/чел',
fixed: ''
}
return units[unit] || unit
}
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU').format(price) + ' ₽'
}
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div class="tour-card card">
<div class="relative">
<img
:src="tour.image"
:alt="tour.title"
class="tour-card__image"
>
<div class="tour-card__badge">
<span class="badge badge-primary">{{ tour.badge }}</span>
</div>
</div>
<div class="card-body">
<h3 class="font-semibold text-lg mb-2">{{ tour.title }}</h3>
<p class="text-gray-600 mb-3">{{ tour.location }}</p>
<div class="flex items-center justify-between mb-4">
<div class="rating-stars">
<span
v-for="star in 5"
:key="star"
class="rating-star"
:class="{ empty: star > Math.round(tour.rating) }"
>
</span>
<span class="rating-value">{{ tour.rating }}</span>
</div>
</div>
<div class="flex items-center justify-between">
<div>
<span class="tour-card__price">{{ formatPrice(tour.discountPrice || tour.price) }}</span>
<span
v-if="tour.discountPrice"
class="tour-card__discount ml-2"
>
{{ formatPrice(tour.price) }}
</span>
</div>
<button class="btn btn-primary btn-sm">Подробнее</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Tour {
id: number
title: string
location: string
price: number
discountPrice: number | null
rating: number
image: string
badge: string
}
defineProps<{
tour: Tour
}>()
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(price)
}
</script>
@@ -0,0 +1,94 @@
import { ref } from 'vue'
interface User {
id: number
email: string
name: string
avatar?: string
}
interface LoginData {
email: string
password: string
}
interface RegisterData {
name: string
email: string
password: string
passwordConfirmation: string
}
export const useAuth = () => {
const user = ref<User | null>(null)
const isAuthenticated = ref(false)
// Мок-функция входа
const login = async (credentials: LoginData): Promise<User> => {
// В реальном приложении здесь будет запрос к API
return new Promise((resolve, reject) => {
setTimeout(() => {
if (credentials.email === 'user@example.com' && credentials.password === 'password') {
const mockUser: User = {
id: 1,
email: credentials.email,
name: 'Иван Иванов'
}
user.value = mockUser
isAuthenticated.value = true
localStorage.setItem('user', JSON.stringify(mockUser))
resolve(mockUser)
} else {
reject(new Error('Неверные учетные данные'))
}
}, 1000)
})
}
// Мок-функция регистрации
const register = async (data: RegisterData): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockUser: User = {
id: Date.now(),
email: data.email,
name: data.name
}
user.value = mockUser
isAuthenticated.value = true
localStorage.setItem('user', JSON.stringify(mockUser))
resolve(mockUser)
}, 1000)
})
}
// Выход
const logout = async (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
user.value = null
isAuthenticated.value = false
localStorage.removeItem('user')
resolve()
}, 500)
})
}
// Проверка авторизации при загрузке
const checkAuth = () => {
const storedUser = localStorage.getItem('user')
if (storedUser) {
user.value = JSON.parse(storedUser)
isAuthenticated.value = true
}
}
return {
user,
isAuthenticated,
login,
register,
logout,
checkAuth
}
}
@@ -0,0 +1,225 @@
import { ref } from 'vue'
export interface ObjectItem {
id: number
title: string
description: string
type: 'hotel' | 'apartment' | 'villa' | 'camping' | 'restaurant' | 'attraction'
category: string
address: string
city: string
country: string
price: number
priceUnit: 'per_night' | 'per_person' | 'fixed'
amenities: string[]
images: string[]
contactEmail: string
contactPhone: string
website?: string
coordinates?: {
lat: number
lng: number
}
isActive: boolean
createdAt: string
updatedAt: string
userId: number
rating?: number
reviewCount?: number
}
export const useObjects = () => {
const objects = ref<ObjectItem[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// Мок-данные объектов
const mockObjects: ObjectItem[] = [
{
id: 1,
title: 'Отель "Морской бриз"',
description: 'Комфортабельный отель с видом на море. Идеальное место для отдыха всей семьей.',
type: 'hotel',
category: 'accommodation',
address: 'ул. Приморская, 15',
city: 'Сочи',
country: 'Россия',
price: 5000,
priceUnit: 'per_night',
amenities: ['wifi', 'parking', 'breakfast', 'pool', 'spa'],
images: ['/images/hotel1.jpg', '/images/hotel2.jpg'],
contactEmail: 'hotel@example.com',
contactPhone: '+7 999 123-45-67',
website: 'https://hotel-example.com',
coordinates: { lat: 43.5855, lng: 39.7231 },
isActive: true,
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z',
userId: 1,
rating: 4.5,
reviewCount: 23
},
{
id: 2,
title: 'Апартаменты в центре',
description: 'Современные апартаменты в историческом центре города.',
type: 'apartment',
category: 'accommodation',
address: 'ул. Центральная, 25',
city: 'Москва',
country: 'Россия',
price: 3500,
priceUnit: 'per_night',
amenities: ['wifi', 'kitchen', 'washing_machine'],
images: ['/images/apartment1.jpg'],
contactEmail: 'apart@example.com',
contactPhone: '+7 999 765-43-21',
isActive: true,
createdAt: '2024-01-10T14:30:00Z',
updatedAt: '2024-01-12T09:15:00Z',
userId: 2,
rating: 4.2,
reviewCount: 15
}
]
// Получить все объекты
const fetchObjects = async (filters?: any): Promise<ObjectItem[]> => {
loading.value = true
error.value = null
return new Promise((resolve) => {
setTimeout(() => {
let filteredObjects = [...mockObjects]
// Применяем фильтры
if (filters) {
if (filters.type) {
filteredObjects = filteredObjects.filter(obj => obj.type === filters.type)
}
if (filters.city) {
filteredObjects = filteredObjects.filter(obj =>
obj.city.toLowerCase().includes(filters.city.toLowerCase())
)
}
if (filters.userId) {
filteredObjects = filteredObjects.filter(obj => obj.userId === filters.userId)
}
}
objects.value = filteredObjects
loading.value = false
resolve(filteredObjects)
}, 500)
})
}
// Получить объект по ID
const fetchObjectById = async (id: number): Promise<ObjectItem | null> => {
loading.value = true
return new Promise((resolve) => {
setTimeout(() => {
const object = mockObjects.find(obj => obj.id === id) || null
loading.value = false
resolve(object)
}, 300)
})
}
// Создать новый объект
const createObject = async (objectData: Omit<ObjectItem, 'id' | 'createdAt' | 'updatedAt'>): Promise<ObjectItem> => {
loading.value = true
error.value = null
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const newObject: ObjectItem = {
...objectData,
id: Date.now(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
mockObjects.push(newObject)
objects.value.push(newObject)
loading.value = false
resolve(newObject)
} catch (err) {
error.value = 'Ошибка при создании объекта'
loading.value = false
reject(err)
}
}, 1000)
})
}
// Обновить объект
const updateObject = async (id: number, objectData: Partial<ObjectItem>): Promise<ObjectItem> => {
loading.value = true
error.value = null
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const index = mockObjects.findIndex(obj => obj.id === id)
if (index === -1) {
throw new Error('Объект не найден')
}
const updatedObject: ObjectItem = {
...mockObjects[index],
...objectData,
updatedAt: new Date().toISOString()
}
mockObjects[index] = updatedObject
objects.value[index] = updatedObject
loading.value = false
resolve(updatedObject)
} catch (err) {
error.value = 'Ошибка при обновлении объекта'
loading.value = false
reject(err)
}
}, 800)
})
}
// Удалить объект
const deleteObject = async (id: number): Promise<void> => {
loading.value = true
return new Promise((resolve) => {
setTimeout(() => {
const index = mockObjects.findIndex(obj => obj.id === id)
if (index !== -1) {
mockObjects.splice(index, 1)
objects.value.splice(index, 1)
}
loading.value = false
resolve()
}, 500)
})
}
// Получить объекты текущего пользователя
const fetchMyObjects = async (): Promise<ObjectItem[]> => {
// В реальном приложении здесь будет userId из авторизации
const currentUserId = 1
return fetchObjects({ userId: currentUserId })
}
return {
objects,
loading,
error,
fetchObjects,
fetchObjectById,
createObject,
updateObject,
deleteObject,
fetchMyObjects
}
}
@@ -1,6 +1,27 @@
<template>
<div>
<h1>Страница</h1>
<p>Содержимое страницы</p>
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow-sm">
<div class="container py-4">
<div class="flex justify-between items-center">
<NuxtLink to="/" class="flex items-center gap-2">
<div class="w-8 h-8 bg-primary-500 rounded-lg"></div>
<span class="font-bold text-xl">TravelEasy</span>
</NuxtLink>
<nav class="flex gap-4">
<NuxtLink to="/" class="nav-link">Главная</NuxtLink>
<NuxtLink to="/objects" class="nav-link">Объекты</NuxtLink>
</nav>
</div>
</div>
</header>
<main>
<slot />
</main>
</div>
</template>
<script setup lang="ts">
// Базовый layout для страниц авторизации
</script>
@@ -1,54 +1,15 @@
<template>
<div>
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<NuxtLink to="/" class="text-xl font-bold text-gray-900">
EasySite
</NuxtLink>
<nav class="flex space-x-4">
<NuxtLink
to="/"
class="text-gray-600 hover:text-gray-900"
>
Главная
</NuxtLink>
<NuxtLink
to="/objects"
class="text-gray-600 hover:text-gray-900"
>
Объекты
</NuxtLink>
<NuxtLink
to="/auth/login"
class="text-gray-600 hover:text-gray-900"
>
Войти
</NuxtLink>
</nav>
</div>
</div>
</header>
<div class="min-h-screen bg-gray-50">
<Header />
<!-- Main Content -->
<main>
<NuxtPage />
<slot />
</main>
<!-- Footer -->
<footer class="bg-gray-800 text-white mt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<p>&copy; 2024 EasySite. Все права защищены.</p>
</div>
</footer>
<Footer />
</div>
</template>
<style scoped>
.router-link-active {
color: #2563eb;
font-weight: 500;
}
</style>
<script setup lang="ts">
// Основной layout с хедером и футером
</script>
@@ -0,0 +1,8 @@
export default defineNuxtRouteMiddleware((to) => {
console.log(to)
const { isAuthenticated } = useAuth()
if (!isAuthenticated) {
return navigateTo('/auth/login')
}
})
@@ -1,66 +1,31 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Вход в систему
</h2>
<div class="min-h-screen bg-gray-50 flex items-center justify-center py-12">
<div class="max-w-md w-full">
<div class="card">
<div class="card-header text-center">
<h1 class="text-2xl font-bold">Вход в систему</h1>
<p class="text-gray-600 mt-2">Введите свои учетные данные</p>
</div>
<form class="mt-8 space-y-6" @submit.prevent="handleLogin">
<div>
<label for="email" class="sr-only">Email</label>
<input
id="email"
v-model="form.email"
name="email"
type="email"
required
class="relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Email"
/>
<div class="card-body">
<LoginForm />
</div>
<div>
<label for="password" class="sr-only">Пароль</label>
<input
id="password"
v-model="form.password"
name="password"
type="password"
required
class="relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Пароль"
/>
</div>
<div>
<button
type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Войти
</button>
</div>
</form>
<div class="text-center">
<NuxtLink to="/auth/register" class="text-indigo-600 hover:text-indigo-500">
Нет аккаунта? Зарегистрируйтесь
<div class="card-footer text-center">
<p class="text-gray-600">
Нет аккаунта?
<NuxtLink to="/auth/register" class="text-primary-600 font-medium">
Зарегистрируйтесь
</NuxtLink>
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const form = ref({
email: '',
password: ''
definePageMeta({
layout: 'auth'
})
const handleLogin = async () => {
// Логика входа
console.log('Login attempt:', form.value)
}
</script>
<style scoped>
/* Стили если нужны */
</style>
@@ -1,34 +1,93 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-900 mb-8">
Добро пожаловать в EasySite
</h1>
<p class="text-xl text-gray-600 mb-8">
Платформа для создания страницы в интернете, продивижения туристических услуг
</p>
<div class="space-x-4">
<NuxtLink
to="/auth/login"
class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
<!-- Hero Section -->
<section class="hero-section">
<div class="container">
<div class="relative z-10 py-20">
<div class="max-w-2xl mx-auto text-center">
<h1 class="text-5xl font-bold mb-6">База объектов для путешествий</h1>
<p class="text-xl mb-8 opacity-90">Добавляйте и находите лучшие места для ваших маршрутов</p>
<!-- Простой поиск -->
<div class="max-w-xl mx-auto mb-12">
<div class="flex gap-2">
<input
type="text"
placeholder="Поиск объектов..."
class="form-input flex-1"
@keyup.enter="handleSearch"
v-model="searchQuery"
>
<button @click="handleSearch" class="btn btn-primary">
Найти
</button>
</div>
</div>
<!-- Кнопки авторизации -->
<div class="flex gap-4 justify-center">
<NuxtLink to="/auth/login" class="btn btn-outline text-lg px-8 py-3">
Войти
</NuxtLink>
<NuxtLink
to="/auth/register"
class="bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700"
>
<NuxtLink to="/auth/register" class="btn btn-primary text-lg px-8 py-3">
Регистрация
</NuxtLink>
</div>
</div>
</div>
</div>
</section>
<!-- Быстрая навигация -->
<section class="py-16 bg-white">
<div class="container">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
<NuxtLink
to="/objects"
class="bg-gray-600 text-white px-6 py-3 rounded-lg hover:bg-gray-700"
class="category-card text-center group"
>
Смотреть объекты
<div class="category-card__icon group-hover:bg-primary-200">
🔍
</div>
<h3 class="font-semibold text-lg mb-2">Все объекты</h3>
<p class="text-sm text-gray-600">Просмотр всей базы объектов</p>
</NuxtLink>
<NuxtLink
to="/objects/my-objects"
class="category-card text-center group"
>
<div class="category-card__icon group-hover:bg-primary-200">
📝
</div>
<h3 class="font-semibold text-lg mb-2">Мои объекты</h3>
<p class="text-sm text-gray-600">Управление вашими объектами</p>
</NuxtLink>
<NuxtLink
to="/objects/create"
class="category-card text-center group"
>
<div class="category-card__icon group-hover:bg-primary-200">
</div>
<h3 class="font-semibold text-lg mb-2">Добавить объект</h3>
<p class="text-sm text-gray-600">Создать новую запись</p>
</NuxtLink>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const searchQuery = ref('')
const handleSearch = () => {
if (searchQuery.value.trim()) {
navigateTo(`/objects?search=${encodeURIComponent(searchQuery.value)}`)
} else {
navigateTo('/objects')
}
}
</script>
@@ -1,6 +1,83 @@
<template>
<div>
<h1>Страница</h1>
<p>Содержимое страницы</p>
<div class="min-h-screen bg-gray-50 py-8">
<div class="container max-w-4xl">
<div class="mb-8">
<div class="flex items-center gap-4 mb-2">
<NuxtLink
:to="`/objects/${$route.params.id}`"
class="btn btn-outline btn-sm"
>
Назад
</NuxtLink>
<h1 class="text-3xl font-bold">Редактировать объект</h1>
</div>
<p class="text-gray-600">Обновите информацию о вашем объекте</p>
</div>
<div v-if="loading" class="text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto"></div>
<p class="mt-4 text-gray-600">Загрузка данных объекта...</p>
</div>
<ObjectForm
v-else-if="object"
:object="object"
:loading="updating"
@submit="handleSubmit"
@cancel="handleCancel"
/>
<div v-else class="text-center py-12">
<div class="text-gray-400 text-6xl mb-4"></div>
<h3 class="text-xl font-semibold mb-2">Объект не найден</h3>
<p class="text-gray-600 mb-6">Возможно, объект был удален или у вас нет к нему доступа</p>
<NuxtLink to="/objects/my-objects" class="btn btn-primary">
Вернуться к моим объектам
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
const route = useRoute()
const { fetchObjectById, updateObject } = useObjects()
const object = ref(null)
const loading = ref(true)
const updating = ref(false)
onMounted(async () => {
const objectId = parseInt(route.params.id as string)
if (objectId) {
const result = await fetchObjectById(objectId)
object.value = result
}
loading.value = false
})
const handleSubmit = async (formData: any) => {
updating.value = true
try {
const objectId = parseInt(route.params.id as string)
await updateObject(objectId, formData)
alert('Объект успешно обновлен!')
await navigateTo(`/objects/${objectId}`)
} catch (error) {
console.error('Error updating object:', error)
alert('Ошибка при обновлении объекта')
} finally {
updating.value = false
}
}
const handleCancel = () => {
const objectId = route.params.id
navigateTo(`/objects/${objectId}`)
}
</script>
@@ -1,6 +1,290 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- Хлебные крошки -->
<div class="bg-white border-b">
<div class="container py-4">
<nav class="flex items-center gap-2 text-sm">
<NuxtLink to="/objects" class="text-primary-600 hover:text-primary-700">
Объекты
</NuxtLink>
<span class="text-gray-400">/</span>
<span class="text-gray-600">{{ object?.title }}</span>
</nav>
</div>
</div>
<div class="container py-8">
<!-- Заголовок и действия -->
<div class="flex justify-between items-start mb-8">
<div>
<h1>Страница</h1>
<p>Содержимое страницы</p>
<h1 class="text-3xl font-bold mb-2">{{ object?.title }}</h1>
<div class="flex items-center gap-4 text-gray-600">
<span class="flex items-center gap-1">
📍 {{ object?.city }}, {{ object?.country }}
</span>
<span class="badge badge-primary">
{{ getTypeLabel(object?.type || '') }}
</span>
<span v-if="object?.rating" class="flex items-center gap-1">
{{ object.rating }} ({{ object.reviewCount }} отзывов)
</span>
</div>
</div>
<div class="flex gap-3" v-if="isOwner">
<NuxtLink
:to="`/objects/${object?.id}/edit`"
class="btn btn-outline"
>
Редактировать
</NuxtLink>
<button
@click="handleDelete"
class="btn btn-outline text-red-600 hover:bg-red-50"
>
Удалить
</button>
</div>
</div>
<!-- Основной контент -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Левая колонка - изображения и описание -->
<div class="lg:col-span-2 space-y-8">
<!-- Галерея изображений -->
<div class="card">
<div class="card-body p-0">
<div class="aspect-w-16 aspect-h-9 bg-gray-200 rounded-lg overflow-hidden">
<img
:src="object?.images[0] || '/images/placeholder.jpg'"
:alt="object?.title"
class="w-full h-96 object-cover"
>
</div>
<div class="grid grid-cols-4 gap-2 p-4" v-if="object?.images.length > 1">
<img
v-for="(image, index) in object.images.slice(1, 5)"
:key="index"
:src="image"
:alt="`${object.title} - изображение ${index + 2}`"
class="w-full h-20 object-cover rounded cursor-pointer"
@click="currentImage = image"
>
</div>
</div>
</div>
<!-- Описание -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Описание</h2>
</div>
<div class="card-body">
<p class="text-gray-700 leading-relaxed">{{ object?.description }}</p>
</div>
</div>
<!-- Услуги -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Услуги и удобства</h2>
</div>
<div class="card-body">
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<div
v-for="amenity in object?.amenities"
:key="amenity"
class="flex items-center gap-2"
>
<span class="w-2 h-2 bg-primary-500 rounded-full"></span>
<span>{{ getAmenityLabel(amenity) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Правая колонка - информация и контакты -->
<div class="space-y-6">
<!-- Карточка цены и бронирования -->
<div class="card sticky top-24">
<div class="card-body">
<div class="text-3xl font-bold text-primary-600 mb-2">
{{ formatPrice(object?.price || 0) }}
<span class="text-sm font-normal text-gray-500">
{{ getPriceUnitLabel(object?.priceUnit || '') }}
</span>
</div>
<div class="space-y-4 mt-6">
<button class="btn btn-primary w-full">
📞 Связаться
</button>
<button class="btn btn-outline w-full">
📍 Показать на карте
</button>
</div>
</div>
</div>
<!-- Контактная информация -->
<div class="card">
<div class="card-header">
<h3 class="font-semibold">Контактная информация</h3>
</div>
<div class="card-body space-y-3">
<div class="flex items-center gap-3">
<span class="text-gray-400">📧</span>
<a
:href="`mailto:${object?.contactEmail}`"
class="text-primary-600 hover:underline"
>
{{ object?.contactEmail }}
</a>
</div>
<div class="flex items-center gap-3">
<span class="text-gray-400">📞</span>
<a
:href="`tel:${object?.contactPhone}`"
class="text-primary-600 hover:underline"
>
{{ object?.contactPhone }}
</a>
</div>
<div
v-if="object?.website"
class="flex items-center gap-3"
>
<span class="text-gray-400">🌐</span>
<a
:href="object.website"
target="_blank"
class="text-primary-600 hover:underline"
>
{{ object.website }}
</a>
</div>
<div class="flex items-center gap-3">
<span class="text-gray-400">📍</span>
<span>{{ object?.address }}</span>
</div>
</div>
</div>
<!-- Информация о владельце -->
<div class="card">
<div class="card-header">
<h3 class="font-semibold">О владельце</h3>
</div>
<div class="card-body">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
<span class="text-primary-600 font-semibold">
{{ getInitials(object?.userId === 1 ? 'Иван Иванов' : 'Владелец') }}
</span>
</div>
<div>
<div class="font-medium">
{{ object?.userId === 1 ? 'Иван Иванов' : 'Владелец объекта' }}
</div>
<div class="text-sm text-gray-600">На сайте с 2024</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { ObjectItem } from '~/composables/useObjects'
const route = useRoute()
const { isAuthenticated } = useAuth()
const { fetchObjectById, deleteObject } = useObjects()
const object = ref<ObjectItem | null>(null)
const loading = ref(true)
const currentImage = ref('')
// Проверяем, является ли текущий пользователь владельцем
const isOwner = computed(() => {
return isAuthenticated && object.value?.userId === 1 // В реальном приложении сравниваем с ID текущего пользователя
})
onMounted(async () => {
const objectId = parseInt(route.params.id as string)
if (objectId) {
const result = await fetchObjectById(objectId)
object.value = result
if (result?.images?.[0]) {
currentImage.value = result.images[0]
}
}
loading.value = false
})
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
hotel: 'Отель',
apartment: 'Апартаменты',
villa: 'Вилла',
camping: 'Кемпинг',
restaurant: 'Ресторан',
attraction: 'Достопримечательность'
}
return types[type] || type
}
const getAmenityLabel = (amenity: string) => {
const amenities: Record<string, string> = {
wifi: 'Wi-Fi',
parking: 'Парковка',
breakfast: 'Завтрак',
pool: 'Бассейн',
spa: 'СПА',
kitchen: 'Кухня',
washing_machine: 'Стиральная машина',
air_conditioning: 'Кондиционер',
heating: 'Отопление',
gym: 'Тренажерный зал',
tv: 'Телевизор',
elevator: 'Лифт'
}
return amenities[amenity] || amenity
}
const getPriceUnitLabel = (unit: string) => {
const units: Record<string, string> = {
per_night: '/ночь',
per_person: '/человека',
fixed: ''
}
return units[unit] || unit
}
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU').format(price) + ' ₽'
}
const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase()
}
const handleDelete = async () => {
if (!object.value) return
if (confirm('Вы уверены, что хотите удалить этот объект? Это действие нельзя отменить.')) {
try {
await deleteObject(object.value.id)
alert('Объект успешно удален')
await navigateTo('/objects/my-objects')
} catch (error) {
console.error('Error deleting object:', error)
alert('Ошибка при удалении объекта')
}
}
}
</script>
@@ -1,6 +1,52 @@
<template>
<div>
<h1>Страница</h1>
<p>Содержимое страницы</p>
<div class="min-h-screen bg-gray-50 py-8">
<div class="container max-w-4xl">
<div class="mb-8">
<h1 class="text-3xl font-bold">Добавить новый объект</h1>
<p class="text-gray-600 mt-2">Заполните информацию о вашем объекте</p>
</div>
<ObjectForm
:loading="loading"
@submit="handleSubmit"
@cancel="handleCancel"
/>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
const { createObject } = useObjects()
const loading = ref(false)
const handleSubmit = async (formData: any) => {
loading.value = true
try {
await createObject({
...formData,
userId: 1, // В реальном приложении из авторизации
isActive: true,
images: formData.images || ['/images/placeholder.jpg'],
amenities: formData.amenities || []
})
// Показываем уведомление об успехе
alert('Объект успешно создан!')
await navigateTo('/objects/my-objects')
} catch (error) {
console.error('Error creating object:', error)
alert('Ошибка при создании объекта')
} finally {
loading.value = false
}
}
const handleCancel = () => {
navigateTo('/objects/my-objects')
}
</script>
@@ -1,6 +1,104 @@
<template>
<div class="min-h-screen bg-gray-50 py-8">
<div class="container">
<!-- Заголовок и кнопка добавления -->
<div class="flex justify-between items-center mb-8">
<div>
<h1>Страница</h1>
<p>Содержимое страницы</p>
<h1 class="text-3xl font-bold">Все объекты</h1>
<p class="text-gray-600 mt-2">База объектов для путешествий</p>
</div>
<NuxtLink to="/objects/create" class="btn btn-primary">
Добавить объект
</NuxtLink>
</div>
<!-- Простой поиск -->
<div class="search-filters mb-8">
<div class="flex gap-4">
<div class="form-group flex-1">
<input
v-model="searchQuery"
type="text"
class="form-input"
placeholder="Поиск по названию, городу или описанию..."
@keyup.enter="searchObjects"
>
</div>
<button @click="searchObjects" class="btn btn-primary">
Найти
</button>
<button @click="resetSearch" class="btn btn-outline">
Сброс
</button>
</div>
</div>
<!-- Быстрые ссылки -->
<div class="flex gap-4 mb-6">
<NuxtLink to="/objects/my-objects" class="btn btn-outline">
📝 Мои объекты
</NuxtLink>
<NuxtLink to="/objects/create" class="btn btn-outline">
Добавить объект
</NuxtLink>
</div>
<!-- Результаты поиска -->
<div v-if="loading" class="text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto"></div>
<p class="mt-4 text-gray-600">Загрузка объектов...</p>
</div>
<div v-else-if="objects.length === 0" class="text-center py-12">
<div class="text-gray-400 text-6xl mb-4">🏢</div>
<h3 class="text-xl font-semibold mb-2">Объекты не найдены</h3>
<p class="text-gray-600 mb-6">Попробуйте изменить поисковый запрос или добавьте первый объект</p>
<NuxtLink to="/objects/create" class="btn btn-primary">
Добавить первый объект
</NuxtLink>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ObjectCard
v-for="object in objects"
:key="object.id"
:object="object"
@click="navigateToObject(object.id)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ObjectItem } from '~/composables/useObjects'
const route = useRoute()
const { objects, loading, fetchObjects } = useObjects()
const searchQuery = ref(route.query.search as string || '')
// Загрузка объектов при монтировании
onMounted(() => {
if (searchQuery.value) {
searchObjects()
} else {
fetchObjects()
}
})
const searchObjects = async () => {
await fetchObjects({
search: searchQuery.value
})
}
const resetSearch = async () => {
searchQuery.value = ''
await fetchObjects()
}
const navigateToObject = (id: number) => {
navigateTo(`/objects/${id}`)
}
</script>
@@ -1,6 +1,182 @@
<template>
<div class="min-h-screen bg-gray-50 py-8">
<div class="container">
<!-- Заголовок и статистика -->
<div class="mb-8">
<div class="flex justify-between items-center mb-6">
<div>
<h1>Страница</h1>
<p>Содержимое страницы</p>
<h1 class="text-3xl font-bold">Мои объекты</h1>
<p class="text-gray-600 mt-2">Управление вашими добавленными местами</p>
</div>
<NuxtLink to="/objects/create" class="btn btn-primary">
Добавить объект
</NuxtLink>
</div>
<!-- Статистика -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="card text-center">
<div class="text-2xl font-bold text-primary-600">{{ stats.total }}</div>
<div class="text-gray-600">Всего объектов</div>
</div>
<div class="card text-center">
<div class="text-2xl font-bold text-green-600">{{ stats.active }}</div>
<div class="text-gray-600">Активных</div>
</div>
<div class="card text-center">
<div class="text-2xl font-bold text-blue-600">{{ stats.rating }}</div>
<div class="text-gray-600">Средний рейтинг</div>
</div>
<div class="card text-center">
<div class="text-2xl font-bold text-orange-600">{{ stats.reviews }}</div>
<div class="text-gray-600">Отзывов</div>
</div>
</div>
</div>
<!-- Таблица объектов -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold">Список объектов</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4">Название</th>
<th class="text-left py-3 px-4">Тип</th>
<th class="text-left py-3 px-4">Город</th>
<th class="text-left py-3 px-4">Цена</th>
<th class="text-left py-3 px-4">Статус</th>
<th class="text-left py-3 px-4">Действия</th>
</tr>
</thead>
<tbody>
<tr
v-for="object in myObjects"
:key="object.id"
class="border-b border-gray-100 hover:bg-gray-50"
>
<td class="py-3 px-4">
<div class="font-medium">{{ object.title }}</div>
<div class="text-sm text-gray-500">{{ formatDate(object.createdAt) }}</div>
</td>
<td class="py-3 px-4">
<span class="badge badge-primary">
{{ getTypeLabel(object.type) }}
</span>
</td>
<td class="py-3 px-4">{{ object.city }}</td>
<td class="py-3 px-4 font-medium">
{{ formatPrice(object.price) }}
</td>
<td class="py-3 px-4">
<span
class="badge"
:class="object.isActive ? 'badge-success' : 'badge-secondary'"
>
{{ object.isActive ? 'Активен' : 'Неактивен' }}
</span>
</td>
<td class="py-3 px-4">
<div class="flex gap-2">
<NuxtLink
:to="`/objects/${object.id}`"
class="btn btn-outline btn-sm"
>
Просмотр
</NuxtLink>
<NuxtLink
:to="`/objects/${object.id}/edit`"
class="btn btn-outline btn-sm"
>
Редактировать
</NuxtLink>
<button
@click="deleteObject(object.id)"
class="btn btn-outline btn-sm text-red-600 hover:bg-red-50"
>
Удалить
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Пустой state -->
<div
v-if="!loading && myObjects.length === 0"
class="text-center py-12"
>
<div class="text-gray-400 text-6xl mb-4">🏢</div>
<h3 class="text-xl font-semibold mb-2">У вас пока нет объектов</h3>
<p class="text-gray-600 mb-6">Добавьте первый объект, чтобы начать работу</p>
<NuxtLink to="/objects/create" class="btn btn-primary">
Добавить первый объект
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ObjectItem } from '~/composables/useObjects'
definePageMeta({
middleware: 'auth'
})
const { fetchMyObjects, deleteObject, objects: myObjects, loading } = useObjects()
// Статистика (мок-данные)
const stats = ref({
total: 0,
active: 0,
rating: 4.5,
reviews: 0
})
onMounted(async () => {
await fetchMyObjects()
updateStats()
})
const updateStats = () => {
stats.value.total = myObjects.value.length
stats.value.active = myObjects.value.filter(obj => obj.isActive).length
stats.value.reviews = myObjects.value.reduce((sum, obj) => sum + (obj.reviewCount || 0), 0)
}
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
hotel: 'Отель',
apartment: 'Апартаменты',
villa: 'Вилла',
camping: 'Кемпинг',
restaurant: 'Ресторан',
attraction: 'Достопримечательность'
}
return types[type] || type
}
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU').format(price) + ' ₽'
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('ru-RU')
}
// Обработка удаления объекта
const handleDeleteObject = async (id: number) => {
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
await deleteObject(id)
await fetchMyObjects()
updateStats()
}
}
</script>