new file: main_dc/yalarba/easySite/easySite/app/components/BookingModal.vue
new file: main_dc/yalarba/easySite/easySite/app/components/ImageGallery.vue new file: main_dc/yalarba/easySite/easySite/app/components/layout/ObjectsNavigation.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 upgrade UI/UX
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="modal-overlay" @click="$emit('close')">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Бронирование</h2>
|
||||
<button class="modal-close" @click="$emit('close')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Контент модального окна бронирования -->
|
||||
<div class="booking-summary">
|
||||
<h3>{{ object?.title }}</h3>
|
||||
<p>{{ object?.city }}, {{ object?.address }}</p>
|
||||
</div>
|
||||
<!-- Форма бронирования -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
object: any
|
||||
dates: any
|
||||
guests: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
confirm: [bookingData: any]
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="gallery-overlay" @click="$emit('close')">
|
||||
<div class="gallery-content" @click.stop>
|
||||
<button class="gallery-close" @click="$emit('close')">✕</button>
|
||||
<img :src="currentImage" :alt="`Image ${currentIndex + 1}`" class="gallery-image" />
|
||||
<button class="gallery-nav prev" @click="prevImage">‹</button>
|
||||
<button class="gallery-nav next" @click="nextImage">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
images: string[]
|
||||
initialIndex: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const currentIndex = ref(props.initialIndex)
|
||||
const currentImage = computed(() => props.images[currentIndex.value])
|
||||
|
||||
const nextImage = () => {
|
||||
currentIndex.value = (currentIndex.value + 1) % props.images.length
|
||||
}
|
||||
|
||||
const prevImage = () => {
|
||||
currentIndex.value = (currentIndex.value - 1 + props.images.length) % props.images.length
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- components/layout/ObjectsNavigation.vue -->
|
||||
<template>
|
||||
<nav class="objects-navigation">
|
||||
<div class="nav-container">
|
||||
<!-- Основные ссылки -->
|
||||
<div class="nav-links">
|
||||
<NuxtLink to="/objects" class="nav-link" :class="{ active: $route.path === '/objects' }">
|
||||
🏢 Все объекты
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/objects/my-objects" class="nav-link"
|
||||
:class="{ active: $route.path === '/objects/my-objects' }">
|
||||
📝 Мои объекты
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/objects/create" class="nav-link" :class="{ active: $route.path === '/objects/create' }">
|
||||
➕ Добавить объект
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные действия -->
|
||||
<div class="nav-actions">
|
||||
<NuxtLink to="/" class="nav-action">
|
||||
🏠 На главную
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.objects-navigation {
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0.75rem 1.25rem;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--primary-600);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--primary-500);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--primary-500);
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
padding: 0.5rem 1rem;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-action:hover {
|
||||
color: var(--primary-500);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,51 @@
|
||||
<!-- pages/objects/[id]/edit.vue -->
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="page-wrapper">
|
||||
<Header />
|
||||
|
||||
<main class="edit-object-page">
|
||||
<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"
|
||||
>
|
||||
← Назад
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-main">
|
||||
<NuxtLink :to="`/objects/${$route.params.id}`" class="btn btn-outline btn-sm btn-with-icon">
|
||||
← Назад к объекту
|
||||
</NuxtLink>
|
||||
<h1 class="text-3xl font-bold">Редактировать объект</h1>
|
||||
<h1 class="page-title">Редактировать объект</h1>
|
||||
</div>
|
||||
<p class="page-subtitle">Обновите информацию о вашем объекте</p>
|
||||
</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 v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">Загрузка данных объекта...</p>
|
||||
</div>
|
||||
|
||||
<ObjectForm
|
||||
v-else-if="object"
|
||||
:object="object"
|
||||
:loading="updating"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
<!-- Форма -->
|
||||
<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>
|
||||
<!-- Объект не найден -->
|
||||
<div v-else class="error-state">
|
||||
<div class="error-icon">❌</div>
|
||||
<h3 class="error-title">Объект не найден</h3>
|
||||
<p class="error-description">Возможно, объект был удален или у вас нет к нему доступа</p>
|
||||
<div class="error-actions">
|
||||
<NuxtLink to="/objects/my-objects" class="btn btn-primary">
|
||||
Вернуться к моим объектам
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/objects" class="btn btn-outline">
|
||||
Все объекты
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -67,7 +75,7 @@ const mockObject: ObjectData = {
|
||||
id: 1,
|
||||
title: 'Гостевой дом "У озера"',
|
||||
type: 'guest_house',
|
||||
description: 'Уютный гостевой дом на берегу живописного озера в Карелии',
|
||||
description: 'Уютный гостевой дом на берегу живописного озера в Карелии. Идеальное место для отдыха от городской суеты.',
|
||||
city: 'Карелия',
|
||||
address: 'ул. Озерная, 15',
|
||||
price: 2800,
|
||||
@@ -117,3 +125,148 @@ const handleCancel = () => {
|
||||
navigateTo(`/objects/${objectId}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.edit-object-page {
|
||||
padding: var(--space-xl) 0;
|
||||
min-height: calc(100vh - 160px);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 3px solid var(--border-light);
|
||||
border-top: 3px solid var(--primary-500);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto var(--space-md);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: var(--space-lg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.error-description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-lg);
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.header-main {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-actions .btn {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.error-state {
|
||||
padding: var(--space-xl);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,28 @@
|
||||
<!-- pages/objects/create.vue -->
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="page-wrapper">
|
||||
<Header />
|
||||
|
||||
<main class="create-object-page">
|
||||
<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 class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Добавить новый объект</h1>
|
||||
<p class="page-subtitle">Заполните информацию о вашем объекте</p>
|
||||
</div>
|
||||
<NuxtLink to="/objects/my-objects" class="btn btn-outline btn-with-icon">
|
||||
← Мои объекты
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма -->
|
||||
<ObjectForm :loading="loading" @submit="handleSubmit" @cancel="handleCancel" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,3 +60,67 @@ const handleCancel = () => {
|
||||
navigateTo('/objects/my-objects')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-object-page {
|
||||
padding: var(--space-xl) 0;
|
||||
min-height: calc(100vh - 160px);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.header-content .btn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,103 +1,144 @@
|
||||
<!-- pages/objects/index.vue -->
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="page-wrapper">
|
||||
<Header />
|
||||
|
||||
<main class="objects-page">
|
||||
<div class="container">
|
||||
<!-- Заголовок и кнопка добавления -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Все объекты</h1>
|
||||
<p class="text-gray-600 mt-2">База объектов для путешествий</p>
|
||||
<!-- Заголовок и действия -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Все объекты</h1>
|
||||
<p class="page-subtitle">Найдено {{ filteredObjects.length }} объектов</p>
|
||||
</div>
|
||||
<NuxtLink to="/objects/create" class="btn btn-primary">
|
||||
<div class="header-actions">
|
||||
<button @click="showFilters = !showFilters" class="btn btn-outline btn-with-icon">
|
||||
<span>🔍</span>
|
||||
Фильтры
|
||||
<span v-if="activeFiltersCount > 0" class="badge badge-primary">
|
||||
{{ activeFiltersCount }}
|
||||
</span>
|
||||
</button>
|
||||
<NuxtLink to="/objects/create" class="btn btn-primary btn-with-icon">
|
||||
<span>➕</span>
|
||||
Добавить объект
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры поиска -->
|
||||
<div class="search-filters mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<!-- Быстрые фильтры -->
|
||||
<div class="quick-filters">
|
||||
<button v-for="type in quickTypes" :key="type.value" @click="toggleQuickFilter(type.value)"
|
||||
class="quick-filter" :class="{ active: filters.type === type.value }">
|
||||
{{ type.icon }} {{ type.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Расширенные фильтры -->
|
||||
<div v-if="showFilters" class="search-filters card">
|
||||
<div class="filter-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Тип объекта</label>
|
||||
<select v-model="filters.type" class="form-select">
|
||||
<option value="">Все типы</option>
|
||||
<option value="hotel">Гостиница</option>
|
||||
<option value="sanatorium">Санаторий</option>
|
||||
<option value="tour">Тур</option>
|
||||
<option value="restaurant">Ресторан</option>
|
||||
<option value="hotel">🏨 Гостиница</option>
|
||||
<option value="sanatorium">🏥 Санаторий</option>
|
||||
<option value="tour">🧳 Тур</option>
|
||||
<option value="restaurant">🍴 Ресторан</option>
|
||||
<option value="guest_house">🏡 Гостевой дом</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Город</label>
|
||||
<input
|
||||
v-model="filters.city"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="Введите город"
|
||||
>
|
||||
<input v-model="filters.city" type="text" class="form-input" placeholder="Введите город">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Цена до</label>
|
||||
<input
|
||||
v-model="filters.maxPrice"
|
||||
type="number"
|
||||
class="form-input"
|
||||
placeholder="Макс. цена"
|
||||
>
|
||||
<input v-model="filters.maxPrice" type="number" class="form-input" placeholder="Макс. цена">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Рейтинг от</label>
|
||||
<select v-model="filters.minRating" class="form-select">
|
||||
<option value="0">Любой рейтинг</option>
|
||||
<option value="4">⭐ 4.0+</option>
|
||||
<option value="4.5">⭐ 4.5+</option>
|
||||
<option value="4.8">⭐ 4.8+</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="form-group flex-1">
|
||||
<input
|
||||
v-model="filters.search"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="Поиск по названию, городу или описанию..."
|
||||
@keyup.enter="searchObjects"
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="searchObjects">
|
||||
Найти
|
||||
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-primary" @click="applyFilters">
|
||||
Применить фильтры
|
||||
</button>
|
||||
<button class="btn btn-outline" @click="resetSearch">
|
||||
Сброс
|
||||
<button class="btn btn-outline" @click="resetFilters">
|
||||
Сбросить
|
||||
</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 class="view-controls">
|
||||
<div class="sort-controls">
|
||||
<select v-model="sortBy" class="form-select">
|
||||
<option value="title">По названию</option>
|
||||
<option value="price">По цене</option>
|
||||
<option value="rating">По рейтингу</option>
|
||||
<option value="city">По городу</option>
|
||||
</select>
|
||||
<button @click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'" class="btn btn-outline btn-sm">
|
||||
{{ sortOrder === 'asc' ? '↑' : '↓' }}
|
||||
</button>
|
||||
</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 class="view-toggle">
|
||||
<button @click="viewMode = 'grid'" class="btn btn-outline btn-sm"
|
||||
:class="{ 'btn-primary': viewMode === 'grid' }">
|
||||
▦
|
||||
</button>
|
||||
<button @click="viewMode = 'list'" class="btn btn-outline btn-sm"
|
||||
:class="{ 'btn-primary': viewMode === 'list' }">
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
</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 v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">Загрузка объектов...</p>
|
||||
</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 v-else-if="filteredObjects.length === 0" class="empty-state">
|
||||
<div class="empty-icon">🏢</div>
|
||||
<h3 class="empty-title">Объекты не найдены</h3>
|
||||
<p class="empty-description">Попробуйте изменить параметры поиска</p>
|
||||
<button @click="resetFilters" class="btn btn-primary">
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Сетка объектов -->
|
||||
<div v-else class="objects-grid" :class="viewMode === 'grid' ? 'grid-view' : 'list-view'">
|
||||
<ObjectCard v-for="object in paginatedObjects" :key="object.id" :object="object" :view-mode="viewMode"
|
||||
@click="navigateToObject(object.id)" />
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
<div v-if="!loading && filteredObjects.length > 0" class="pagination">
|
||||
<button v-for="page in totalPages" :key="page" @click="currentPage = page" class="pagination-btn"
|
||||
:class="{ active: currentPage === page }">
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -112,8 +153,40 @@ interface ObjectItem {
|
||||
image: string
|
||||
description: string
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// Навигация
|
||||
useHead({
|
||||
title: 'Все объекты - EasySite'
|
||||
})
|
||||
|
||||
// Состояние
|
||||
const objects = ref<ObjectItem[]>([])
|
||||
const loading = ref(true)
|
||||
const showFilters = ref(false)
|
||||
const viewMode = ref<'grid' | 'list'>('grid')
|
||||
const sortBy = ref<'title' | 'price' | 'rating' | 'city'>('title')
|
||||
const sortOrder = ref<'asc' | 'desc'>('asc')
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 9
|
||||
|
||||
const filters = ref({
|
||||
search: '',
|
||||
type: '',
|
||||
city: '',
|
||||
maxPrice: null as number | null,
|
||||
minRating: 0
|
||||
})
|
||||
|
||||
// Быстрые фильтры
|
||||
const quickTypes = [
|
||||
{ value: 'hotel', label: 'Гостиницы', icon: '🏨' },
|
||||
{ value: 'sanatorium', label: 'Санатории', icon: '🏥' },
|
||||
{ value: 'tour', label: 'Туры', icon: '🧳' },
|
||||
{ value: 'restaurant', label: 'Рестораны', icon: '🍴' }
|
||||
]
|
||||
|
||||
// Мок-данные
|
||||
const mockObjects: ObjectItem[] = [
|
||||
{
|
||||
@@ -124,49 +197,45 @@ const mockObjects: ObjectItem[] = [
|
||||
price: 4500,
|
||||
rating: 4.8,
|
||||
image: '/images/hotels/edelweiss.jpg',
|
||||
description: 'Комфортабельный отель в горах с видом на море',
|
||||
isActive: true
|
||||
description: 'Комфортабельный отель в горах с видом на море. Идеальное место для отдыха от городской суеты.',
|
||||
isActive: true,
|
||||
createdAt: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Тур по Алтаю',
|
||||
type: 'tour',
|
||||
city: 'Горно-Алтайск',
|
||||
price: 12500,
|
||||
rating: 4.9,
|
||||
image: '/images/tours/altai.jpg',
|
||||
description: '7-дневный тур по самым живописным местам Алтая',
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Санаторий "Здоровье"',
|
||||
type: 'sanatorium',
|
||||
city: 'Кисловодск',
|
||||
price: 3200,
|
||||
rating: 4.6,
|
||||
image: '/images/sanatoriums/health.jpg',
|
||||
description: 'Лечебно-оздоровительный комплекс с минеральными водами',
|
||||
isActive: true
|
||||
description: 'Лечебно-оздоровительный комплекс с минеральными водами и современным оборудованием.',
|
||||
isActive: true,
|
||||
createdAt: '2024-01-10'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Тур по Золотому кольцу',
|
||||
type: 'tour',
|
||||
city: 'Москва',
|
||||
price: 12500,
|
||||
rating: 4.9,
|
||||
image: '/images/tours/golden-ring.jpg',
|
||||
description: '7-дневный тур по древним городам России с опытным гидом и комфортабельным транспортом.',
|
||||
isActive: true,
|
||||
createdAt: '2024-01-08'
|
||||
}
|
||||
]
|
||||
|
||||
const objects = ref<ObjectItem[]>(mockObjects)
|
||||
const loading = ref(false)
|
||||
|
||||
const filters = ref({
|
||||
search: '',
|
||||
type: '',
|
||||
city: '',
|
||||
maxPrice: null as number | null
|
||||
// Вычисляемые свойства
|
||||
const activeFiltersCount = computed(() => {
|
||||
return Object.values(filters.value).filter(val =>
|
||||
val !== '' && val !== null && val !== 0
|
||||
).length
|
||||
})
|
||||
|
||||
const searchObjects = async () => {
|
||||
loading.value = true
|
||||
// Имитация загрузки
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
let filtered = [...mockObjects]
|
||||
const filteredObjects = computed(() => {
|
||||
let filtered = [...objects.value]
|
||||
|
||||
if (filters.value.search) {
|
||||
const search = filters.value.search.toLowerCase()
|
||||
@@ -191,21 +260,344 @@ const searchObjects = async () => {
|
||||
filtered = filtered.filter(obj => obj.price <= filters.value.maxPrice!)
|
||||
}
|
||||
|
||||
objects.value = filtered
|
||||
loading.value = false
|
||||
if (filters.value.minRating) {
|
||||
filtered = filtered.filter(obj => obj.rating >= filters.value.minRating!)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const sortedObjects = computed(() => {
|
||||
const sorted = [...filteredObjects.value].sort((a, b) => {
|
||||
let aVal = a[sortBy.value]
|
||||
let bVal = b[sortBy.value]
|
||||
|
||||
if (sortBy.value === 'price' || sortBy.value === 'rating') {
|
||||
return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
|
||||
}
|
||||
|
||||
aVal = String(aVal).toLowerCase()
|
||||
bVal = String(bVal).toLowerCase()
|
||||
|
||||
if (sortOrder.value === 'asc') {
|
||||
return aVal.localeCompare(bVal)
|
||||
} else {
|
||||
return bVal.localeCompare(aVal)
|
||||
}
|
||||
})
|
||||
|
||||
return sorted
|
||||
})
|
||||
|
||||
const paginatedObjects = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage
|
||||
return sortedObjects.value.slice(start, start + itemsPerPage)
|
||||
})
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(filteredObjects.value.length / itemsPerPage)
|
||||
})
|
||||
|
||||
// Методы
|
||||
const toggleQuickFilter = (type: string) => {
|
||||
filters.value.type = filters.value.type === type ? '' : type
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
const resetSearch = async () => {
|
||||
const applyFilters = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.value = {
|
||||
search: '',
|
||||
type: '',
|
||||
city: '',
|
||||
maxPrice: null
|
||||
maxPrice: null,
|
||||
minRating: 0
|
||||
}
|
||||
objects.value = mockObjects
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const navigateToObject = (id: number) => {
|
||||
navigateTo(`/objects/${id}`)
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
objects.value = mockObjects
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
// Следим за изменениями фильтров
|
||||
watch([filters, sortBy, sortOrder], () => {
|
||||
applyFilters()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.objects-page {
|
||||
padding: var(--space-xl) 0;
|
||||
min-height: calc(100vh - 160px);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.quick-filter:hover {
|
||||
border-color: var(--primary-300);
|
||||
background: var(--primary-50);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.quick-filter.active {
|
||||
background: var(--primary-500);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--primary-500);
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-lg);
|
||||
padding: var(--space-md);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.objects-grid.grid-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.objects-grid.list-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 3px solid var(--border-light);
|
||||
border-top: 3px solid var(--primary-500);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto var(--space-md);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: var(--space-lg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-xl);
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.pagination-btn:hover {
|
||||
border-color: var(--primary-300);
|
||||
background: var(--primary-50);
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: var(--primary-500);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--primary-500);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.objects-grid.grid-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.quick-filters {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,23 @@
|
||||
<!-- pages/objects/my-objects.vue -->
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="page-wrapper">
|
||||
<Header />
|
||||
|
||||
<main class="my-objects-page">
|
||||
<div class="container">
|
||||
<!-- Заголовок -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Мои объекты</h1>
|
||||
<p class="text-gray-600 mt-2">Управление вашими добавленными местами</p>
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Мои объекты</h1>
|
||||
<p class="page-subtitle">Управление вашими добавленными местами</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<NuxtLink to="/objects" class="btn btn-outline">
|
||||
🔍 Все объекты
|
||||
<div class="header-actions">
|
||||
<NuxtLink to="/objects" class="btn btn-outline btn-with-icon">
|
||||
<span>🔍</span>
|
||||
Все объекты
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/objects/create" class="btn btn-primary">
|
||||
<NuxtLink to="/objects/create" class="btn btn-primary btn-with-icon">
|
||||
<span>➕</span>
|
||||
Добавить объект
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -21,9 +25,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Фильтры -->
|
||||
<div class="card mb-6">
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="search-filters card">
|
||||
<div class="filter-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Статус</label>
|
||||
<select v-model="filters.status" class="form-select">
|
||||
@@ -36,22 +39,18 @@
|
||||
<label class="form-label">Тип</label>
|
||||
<select v-model="filters.type" class="form-select">
|
||||
<option value="">Все типы</option>
|
||||
<option value="hotel">Гостиница</option>
|
||||
<option value="sanatorium">Санаторий</option>
|
||||
<option value="tour">Тур</option>
|
||||
<option value="hotel">🏨 Гостиница</option>
|
||||
<option value="sanatorium">🏥 Санаторий</option>
|
||||
<option value="tour">🧳 Тур</option>
|
||||
<option value="guest_house">🏡 Гостевой дом</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Поиск</label>
|
||||
<input
|
||||
v-model="filters.search"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="Название или город"
|
||||
>
|
||||
<input v-model="filters.search" type="text" class="form-input" placeholder="Название или город">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-primary" @click="applyFilters">
|
||||
Применить
|
||||
</button>
|
||||
@@ -60,72 +59,61 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Таблица объектов -->
|
||||
<div class="card">
|
||||
<div class="objects-table card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-xl font-semibold">Список объектов</h2>
|
||||
<h2 class="card-title">Список объектов</h2>
|
||||
<p class="card-subtitle">Всего: {{ myObjects.length }} объектов</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<div class="table-container">
|
||||
<table class="objects-table">
|
||||
<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>
|
||||
<th class="table-header">Название</th>
|
||||
<th class="table-header">Тип</th>
|
||||
<th class="table-header">Город</th>
|
||||
<th class="table-header">Цена</th>
|
||||
<th class="table-header">Статус</th>
|
||||
<th class="table-header">Действия</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>
|
||||
<tr v-for="object in myObjects" :key="object.id" class="table-row">
|
||||
<td class="table-cell">
|
||||
<div class="object-info">
|
||||
<div class="object-title">{{ object.title }}</div>
|
||||
<div class="object-date">{{ formatDate(object.createdAt) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<td class="table-cell">
|
||||
<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 class="table-cell">{{ object.city }}</td>
|
||||
<td class="table-cell">
|
||||
<div class="object-price">{{ formatPrice(object.price) }}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<span
|
||||
class="badge"
|
||||
:class="object.isActive ? 'badge-success' : 'badge-secondary'"
|
||||
>
|
||||
<td class="table-cell">
|
||||
<span class="status-badge" :class="object.isActive ? 'status-active' : 'status-inactive'">
|
||||
{{ 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"
|
||||
>
|
||||
Просмотр
|
||||
<td class="table-cell">
|
||||
<div class="action-buttons">
|
||||
<NuxtLink :to="`/objects/${object.id}`" class="btn btn-outline btn-sm btn-with-icon"
|
||||
title="Просмотр">
|
||||
👁️
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
:to="`/objects/${object.id}/edit`"
|
||||
class="btn btn-outline btn-sm"
|
||||
>
|
||||
Редактировать
|
||||
<NuxtLink :to="`/objects/${object.id}/edit`" class="btn btn-outline btn-sm btn-with-icon"
|
||||
title="Редактировать">
|
||||
✏️
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="deleteObject(object.id)"
|
||||
class="btn btn-outline btn-sm text-red-600 hover:bg-red-50"
|
||||
>
|
||||
Удалить
|
||||
<button @click="deleteObject(object.id)" class="btn btn-outline btn-sm btn-with-icon delete-btn"
|
||||
title="Удалить">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -135,13 +123,10 @@
|
||||
</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>
|
||||
<div v-if="!loading && myObjects.length === 0" class="empty-state">
|
||||
<div class="empty-icon">🏢</div>
|
||||
<h3 class="empty-title">У вас пока нет объектов</h3>
|
||||
<p class="empty-description">Добавьте первый объект, чтобы начать работу</p>
|
||||
<NuxtLink to="/objects/create" class="btn btn-primary">
|
||||
Добавить первый объект
|
||||
</NuxtLink>
|
||||
@@ -149,15 +134,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Навигация -->
|
||||
<div class="mt-8 flex justify-between">
|
||||
<NuxtLink to="/objects" class="btn btn-outline">
|
||||
<div class="page-navigation">
|
||||
<NuxtLink to="/objects" class="btn btn-outline btn-with-icon">
|
||||
← Все объекты
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/" class="btn btn-outline">
|
||||
<NuxtLink to="/" class="btn btn-outline btn-with-icon">
|
||||
🏠 На главную
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -186,7 +174,7 @@ const mockMyObjects: MyObjectItem[] = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'Экскурсия по историческому центру',
|
||||
type: 'excursion',
|
||||
type: 'tour',
|
||||
city: 'Санкт-Петербург',
|
||||
price: 1500,
|
||||
isActive: true,
|
||||
@@ -247,7 +235,6 @@ const resetFilters = () => {
|
||||
|
||||
const deleteObject = async (id: number) => {
|
||||
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
|
||||
// Имитация удаления
|
||||
myObjects.value = myObjects.value.filter(obj => obj.id !== id)
|
||||
}
|
||||
}
|
||||
@@ -275,3 +262,265 @@ const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-objects-page {
|
||||
padding: var(--space-xl) 0;
|
||||
min-height: calc(100vh - 160px);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.objects-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-xl);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.objects-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: var(--space-md);
|
||||
text-align: left;
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-secondary);
|
||||
font-size: var(--text-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: var(--space-md);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.object-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.object-title {
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.object-date {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.object-price {
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--primary-600);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: var(--success-50);
|
||||
color: var(--success-600);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: var(--error-50);
|
||||
border-color: var(--error-300);
|
||||
color: var(--error-600);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: var(--space-lg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--space-xl);
|
||||
padding-top: var(--space-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-navigation {
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.page-navigation .btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.objects-table {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user