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>
|
<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="container max-w-4xl">
|
||||||
<div class="mb-8">
|
<!-- Заголовок -->
|
||||||
<div class="flex items-center gap-4 mb-2">
|
<div class="page-header">
|
||||||
<NuxtLink
|
<div class="header-content">
|
||||||
:to="`/objects/${$route.params.id}`"
|
<div class="header-main">
|
||||||
class="btn btn-outline btn-sm"
|
<NuxtLink :to="`/objects/${$route.params.id}`" class="btn btn-outline btn-sm btn-with-icon">
|
||||||
>
|
← Назад к объекту
|
||||||
← Назад
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<h1 class="text-3xl font-bold">Редактировать объект</h1>
|
<h1 class="page-title">Редактировать объект</h1>
|
||||||
|
</div>
|
||||||
|
<p class="page-subtitle">Обновите информацию о вашем объекте</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-600">Обновите информацию о вашем объекте</p>
|
|
||||||
</div>
|
</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>
|
<div v-if="loading" class="loading-state">
|
||||||
<p class="mt-4 text-gray-600">Загрузка данных объекта...</p>
|
<div class="loading-spinner"></div>
|
||||||
|
<p class="loading-text">Загрузка данных объекта...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ObjectForm
|
<!-- Форма -->
|
||||||
v-else-if="object"
|
<ObjectForm v-else-if="object" :object="object" :loading="updating" @submit="handleSubmit"
|
||||||
:object="object"
|
@cancel="handleCancel" />
|
||||||
:loading="updating"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@cancel="handleCancel"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-else class="text-center py-12">
|
<!-- Объект не найден -->
|
||||||
<div class="text-gray-400 text-6xl mb-4">❌</div>
|
<div v-else class="error-state">
|
||||||
<h3 class="text-xl font-semibold mb-2">Объект не найден</h3>
|
<div class="error-icon">❌</div>
|
||||||
<p class="text-gray-600 mb-6">Возможно, объект был удален или у вас нет к нему доступа</p>
|
<h3 class="error-title">Объект не найден</h3>
|
||||||
|
<p class="error-description">Возможно, объект был удален или у вас нет к нему доступа</p>
|
||||||
|
<div class="error-actions">
|
||||||
<NuxtLink to="/objects/my-objects" class="btn btn-primary">
|
<NuxtLink to="/objects/my-objects" class="btn btn-primary">
|
||||||
Вернуться к моим объектам
|
Вернуться к моим объектам
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/objects" class="btn btn-outline">
|
||||||
|
Все объекты
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -67,7 +75,7 @@ const mockObject: ObjectData = {
|
|||||||
id: 1,
|
id: 1,
|
||||||
title: 'Гостевой дом "У озера"',
|
title: 'Гостевой дом "У озера"',
|
||||||
type: 'guest_house',
|
type: 'guest_house',
|
||||||
description: 'Уютный гостевой дом на берегу живописного озера в Карелии',
|
description: 'Уютный гостевой дом на берегу живописного озера в Карелии. Идеальное место для отдыха от городской суеты.',
|
||||||
city: 'Карелия',
|
city: 'Карелия',
|
||||||
address: 'ул. Озерная, 15',
|
address: 'ул. Озерная, 15',
|
||||||
price: 2800,
|
price: 2800,
|
||||||
@@ -117,3 +125,148 @@ const handleCancel = () => {
|
|||||||
navigateTo(`/objects/${objectId}`)
|
navigateTo(`/objects/${objectId}`)
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
<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="container max-w-4xl">
|
||||||
<div class="mb-8">
|
<!-- Заголовок -->
|
||||||
<h1 class="text-3xl font-bold">Добавить новый объект</h1>
|
<div class="page-header">
|
||||||
<p class="text-gray-600 mt-2">Заполните информацию о вашем объекте</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма -->
|
||||||
<ObjectForm :loading="loading" @submit="handleSubmit" @cancel="handleCancel" />
|
<ObjectForm :loading="loading" @submit="handleSubmit" @cancel="handleCancel" />
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -46,3 +60,67 @@ const handleCancel = () => {
|
|||||||
navigateTo('/objects/my-objects')
|
navigateTo('/objects/my-objects')
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
<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="container">
|
||||||
<!-- Заголовок и кнопка добавления -->
|
<!-- Заголовок и действия -->
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="page-header">
|
||||||
<div>
|
<div class="header-content">
|
||||||
<h1 class="text-3xl font-bold">Все объекты</h1>
|
<div class="header-text">
|
||||||
<p class="text-gray-600 mt-2">База объектов для путешествий</p>
|
<h1 class="page-title">Все объекты</h1>
|
||||||
|
<p class="page-subtitle">Найдено {{ filteredObjects.length }} объектов</p>
|
||||||
</div>
|
</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>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Фильтры поиска -->
|
<!-- Быстрые фильтры -->
|
||||||
<div class="search-filters mb-8">
|
<div class="quick-filters">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<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">
|
<div class="form-group">
|
||||||
<label class="form-label">Тип объекта</label>
|
<label class="form-label">Тип объекта</label>
|
||||||
<select v-model="filters.type" class="form-select">
|
<select v-model="filters.type" class="form-select">
|
||||||
<option value="">Все типы</option>
|
<option value="">Все типы</option>
|
||||||
<option value="hotel">Гостиница</option>
|
<option value="hotel">🏨 Гостиница</option>
|
||||||
<option value="sanatorium">Санаторий</option>
|
<option value="sanatorium">🏥 Санаторий</option>
|
||||||
<option value="tour">Тур</option>
|
<option value="tour">🧳 Тур</option>
|
||||||
<option value="restaurant">Ресторан</option>
|
<option value="restaurant">🍴 Ресторан</option>
|
||||||
|
<option value="guest_house">🏡 Гостевой дом</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Город</label>
|
<label class="form-label">Город</label>
|
||||||
<input
|
<input v-model="filters.city" type="text" class="form-input" placeholder="Введите город">
|
||||||
v-model="filters.city"
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
placeholder="Введите город"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Цена до</label>
|
<label class="form-label">Цена до</label>
|
||||||
<input
|
<input v-model="filters.maxPrice" type="number" class="form-input" placeholder="Макс. цена">
|
||||||
v-model="filters.maxPrice"
|
</div>
|
||||||
type="number"
|
|
||||||
class="form-input"
|
<div class="form-group">
|
||||||
placeholder="Макс. цена"
|
<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>
|
</div>
|
||||||
<div class="flex gap-4">
|
|
||||||
<div class="form-group flex-1">
|
<div class="filter-actions">
|
||||||
<input
|
<button class="btn btn-primary" @click="applyFilters">
|
||||||
v-model="filters.search"
|
Применить фильтры
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
placeholder="Поиск по названию, городу или описанию..."
|
|
||||||
@keyup.enter="searchObjects"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" @click="searchObjects">
|
|
||||||
Найти
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline" @click="resetSearch">
|
<button class="btn btn-outline" @click="resetFilters">
|
||||||
Сброс
|
Сбросить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Быстрые ссылки -->
|
<!-- Управление видом -->
|
||||||
<div class="flex gap-4 mb-6">
|
<div class="view-controls">
|
||||||
<NuxtLink to="/objects/my-objects" class="btn btn-outline">
|
<div class="sort-controls">
|
||||||
📝 Мои объекты
|
<select v-model="sortBy" class="form-select">
|
||||||
</NuxtLink>
|
<option value="title">По названию</option>
|
||||||
<NuxtLink to="/objects/create" class="btn btn-outline">
|
<option value="price">По цене</option>
|
||||||
➕ Добавить объект
|
<option value="rating">По рейтингу</option>
|
||||||
</NuxtLink>
|
<option value="city">По городу</option>
|
||||||
|
</select>
|
||||||
|
<button @click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'" class="btn btn-outline btn-sm">
|
||||||
|
{{ sortOrder === 'asc' ? '↑' : '↓' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Результаты поиска -->
|
<div class="view-toggle">
|
||||||
<div v-if="loading" class="text-center py-12">
|
<button @click="viewMode = 'grid'" class="btn btn-outline btn-sm"
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto"></div>
|
:class="{ 'btn-primary': viewMode === 'grid' }">
|
||||||
<p class="mt-4 text-gray-600">Загрузка объектов...</p>
|
▦
|
||||||
|
</button>
|
||||||
|
<button @click="viewMode = 'list'" class="btn btn-outline btn-sm"
|
||||||
|
:class="{ 'btn-primary': viewMode === 'list' }">
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="objects.length === 0" class="text-center py-12">
|
<!-- Результаты -->
|
||||||
<div class="text-gray-400 text-6xl mb-4">🏢</div>
|
<div v-if="loading" class="loading-state">
|
||||||
<h3 class="text-xl font-semibold mb-2">Объекты не найдены</h3>
|
<div class="loading-spinner"></div>
|
||||||
<p class="text-gray-600 mb-6">Попробуйте изменить поисковый запрос или добавьте первый объект</p>
|
<p class="loading-text">Загрузка объектов...</p>
|
||||||
<NuxtLink to="/objects/create" class="btn btn-primary">
|
|
||||||
Добавить первый объект
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div v-else-if="filteredObjects.length === 0" class="empty-state">
|
||||||
<ObjectCard
|
<div class="empty-icon">🏢</div>
|
||||||
v-for="object in objects"
|
<h3 class="empty-title">Объекты не найдены</h3>
|
||||||
:key="object.id"
|
<p class="empty-description">Попробуйте изменить параметры поиска</p>
|
||||||
:object="object"
|
<button @click="resetFilters" class="btn btn-primary">
|
||||||
@click="navigateToObject(object.id)"
|
Сбросить фильтры
|
||||||
/>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -112,8 +153,40 @@ interface ObjectItem {
|
|||||||
image: string
|
image: string
|
||||||
description: string
|
description: string
|
||||||
isActive: boolean
|
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[] = [
|
const mockObjects: ObjectItem[] = [
|
||||||
{
|
{
|
||||||
@@ -124,49 +197,45 @@ const mockObjects: ObjectItem[] = [
|
|||||||
price: 4500,
|
price: 4500,
|
||||||
rating: 4.8,
|
rating: 4.8,
|
||||||
image: '/images/hotels/edelweiss.jpg',
|
image: '/images/hotels/edelweiss.jpg',
|
||||||
description: 'Комфортабельный отель в горах с видом на море',
|
description: 'Комфортабельный отель в горах с видом на море. Идеальное место для отдыха от городской суеты.',
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
createdAt: '2024-01-15'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Тур по Алтаю',
|
|
||||||
type: 'tour',
|
|
||||||
city: 'Горно-Алтайск',
|
|
||||||
price: 12500,
|
|
||||||
rating: 4.9,
|
|
||||||
image: '/images/tours/altai.jpg',
|
|
||||||
description: '7-дневный тур по самым живописным местам Алтая',
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Санаторий "Здоровье"',
|
title: 'Санаторий "Здоровье"',
|
||||||
type: 'sanatorium',
|
type: 'sanatorium',
|
||||||
city: 'Кисловодск',
|
city: 'Кисловодск',
|
||||||
price: 3200,
|
price: 3200,
|
||||||
rating: 4.6,
|
rating: 4.6,
|
||||||
image: '/images/sanatoriums/health.jpg',
|
image: '/images/sanatoriums/health.jpg',
|
||||||
description: 'Лечебно-оздоровительный комплекс с минеральными водами',
|
description: 'Лечебно-оздоровительный комплекс с минеральными водами и современным оборудованием.',
|
||||||
isActive: true
|
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 activeFiltersCount = computed(() => {
|
||||||
|
return Object.values(filters.value).filter(val =>
|
||||||
const filters = ref({
|
val !== '' && val !== null && val !== 0
|
||||||
search: '',
|
).length
|
||||||
type: '',
|
|
||||||
city: '',
|
|
||||||
maxPrice: null as number | null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const searchObjects = async () => {
|
const filteredObjects = computed(() => {
|
||||||
loading.value = true
|
let filtered = [...objects.value]
|
||||||
// Имитация загрузки
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
let filtered = [...mockObjects]
|
|
||||||
|
|
||||||
if (filters.value.search) {
|
if (filters.value.search) {
|
||||||
const search = filters.value.search.toLowerCase()
|
const search = filters.value.search.toLowerCase()
|
||||||
@@ -191,21 +260,344 @@ const searchObjects = async () => {
|
|||||||
filtered = filtered.filter(obj => obj.price <= filters.value.maxPrice!)
|
filtered = filtered.filter(obj => obj.price <= filters.value.maxPrice!)
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.value = filtered
|
if (filters.value.minRating) {
|
||||||
loading.value = false
|
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 = {
|
filters.value = {
|
||||||
search: '',
|
search: '',
|
||||||
type: '',
|
type: '',
|
||||||
city: '',
|
city: '',
|
||||||
maxPrice: null
|
maxPrice: null,
|
||||||
|
minRating: 0
|
||||||
}
|
}
|
||||||
objects.value = mockObjects
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateToObject = (id: number) => {
|
const navigateToObject = (id: number) => {
|
||||||
navigateTo(`/objects/${id}`)
|
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>
|
</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>
|
<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="container">
|
||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<div class="mb-8">
|
<div class="page-header">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="header-content">
|
||||||
<div>
|
<div class="header-text">
|
||||||
<h1 class="text-3xl font-bold">Мои объекты</h1>
|
<h1 class="page-title">Мои объекты</h1>
|
||||||
<p class="text-gray-600 mt-2">Управление вашими добавленными местами</p>
|
<p class="page-subtitle">Управление вашими добавленными местами</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<div class="header-actions">
|
||||||
<NuxtLink to="/objects" class="btn btn-outline">
|
<NuxtLink to="/objects" class="btn btn-outline btn-with-icon">
|
||||||
🔍 Все объекты
|
<span>🔍</span>
|
||||||
|
Все объекты
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/objects/create" class="btn btn-primary">
|
<NuxtLink to="/objects/create" class="btn btn-primary btn-with-icon">
|
||||||
|
<span>➕</span>
|
||||||
Добавить объект
|
Добавить объект
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,9 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Фильтры -->
|
<!-- Фильтры -->
|
||||||
<div class="card mb-6">
|
<div class="search-filters card">
|
||||||
<div class="card-body">
|
<div class="filter-grid">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Статус</label>
|
<label class="form-label">Статус</label>
|
||||||
<select v-model="filters.status" class="form-select">
|
<select v-model="filters.status" class="form-select">
|
||||||
@@ -36,22 +39,18 @@
|
|||||||
<label class="form-label">Тип</label>
|
<label class="form-label">Тип</label>
|
||||||
<select v-model="filters.type" class="form-select">
|
<select v-model="filters.type" class="form-select">
|
||||||
<option value="">Все типы</option>
|
<option value="">Все типы</option>
|
||||||
<option value="hotel">Гостиница</option>
|
<option value="hotel">🏨 Гостиница</option>
|
||||||
<option value="sanatorium">Санаторий</option>
|
<option value="sanatorium">🏥 Санаторий</option>
|
||||||
<option value="tour">Тур</option>
|
<option value="tour">🧳 Тур</option>
|
||||||
|
<option value="guest_house">🏡 Гостевой дом</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Поиск</label>
|
<label class="form-label">Поиск</label>
|
||||||
<input
|
<input v-model="filters.search" type="text" class="form-input" placeholder="Название или город">
|
||||||
v-model="filters.search"
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
placeholder="Название или город"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="filter-actions">
|
||||||
<button class="btn btn-primary" @click="applyFilters">
|
<button class="btn btn-primary" @click="applyFilters">
|
||||||
Применить
|
Применить
|
||||||
</button>
|
</button>
|
||||||
@@ -60,72 +59,61 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Таблица объектов -->
|
<!-- Таблица объектов -->
|
||||||
<div class="card">
|
<div class="objects-table card">
|
||||||
<div class="card-header">
|
<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>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="table-container">
|
||||||
<table class="w-full">
|
<table class="objects-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200">
|
<tr>
|
||||||
<th class="text-left py-3 px-4">Название</th>
|
<th class="table-header">Название</th>
|
||||||
<th class="text-left py-3 px-4">Тип</th>
|
<th class="table-header">Тип</th>
|
||||||
<th class="text-left py-3 px-4">Город</th>
|
<th class="table-header">Город</th>
|
||||||
<th class="text-left py-3 px-4">Цена</th>
|
<th class="table-header">Цена</th>
|
||||||
<th class="text-left py-3 px-4">Статус</th>
|
<th class="table-header">Статус</th>
|
||||||
<th class="text-left py-3 px-4">Действия</th>
|
<th class="table-header">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr v-for="object in myObjects" :key="object.id" class="table-row">
|
||||||
v-for="object in myObjects"
|
<td class="table-cell">
|
||||||
:key="object.id"
|
<div class="object-info">
|
||||||
class="border-b border-gray-100 hover:bg-gray-50"
|
<div class="object-title">{{ object.title }}</div>
|
||||||
>
|
<div class="object-date">{{ formatDate(object.createdAt) }}</div>
|
||||||
<td class="py-3 px-4">
|
</div>
|
||||||
<div class="font-medium">{{ object.title }}</div>
|
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(object.createdAt) }}</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4">
|
<td class="table-cell">
|
||||||
<span class="badge badge-primary">
|
<span class="badge badge-primary">
|
||||||
{{ getTypeLabel(object.type) }}
|
{{ getTypeLabel(object.type) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4">{{ object.city }}</td>
|
<td class="table-cell">{{ object.city }}</td>
|
||||||
<td class="py-3 px-4 font-medium">
|
<td class="table-cell">
|
||||||
{{ formatPrice(object.price) }}
|
<div class="object-price">{{ formatPrice(object.price) }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4">
|
<td class="table-cell">
|
||||||
<span
|
<span class="status-badge" :class="object.isActive ? 'status-active' : 'status-inactive'">
|
||||||
class="badge"
|
|
||||||
:class="object.isActive ? 'badge-success' : 'badge-secondary'"
|
|
||||||
>
|
|
||||||
{{ object.isActive ? 'Активен' : 'Неактивен' }}
|
{{ object.isActive ? 'Активен' : 'Неактивен' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4">
|
<td class="table-cell">
|
||||||
<div class="flex gap-2">
|
<div class="action-buttons">
|
||||||
<NuxtLink
|
<NuxtLink :to="`/objects/${object.id}`" class="btn btn-outline btn-sm btn-with-icon"
|
||||||
:to="`/objects/${object.id}`"
|
title="Просмотр">
|
||||||
class="btn btn-outline btn-sm"
|
👁️
|
||||||
>
|
|
||||||
Просмотр
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink :to="`/objects/${object.id}/edit`" class="btn btn-outline btn-sm btn-with-icon"
|
||||||
:to="`/objects/${object.id}/edit`"
|
title="Редактировать">
|
||||||
class="btn btn-outline btn-sm"
|
✏️
|
||||||
>
|
|
||||||
Редактировать
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
<button @click="deleteObject(object.id)" class="btn btn-outline btn-sm btn-with-icon delete-btn"
|
||||||
@click="deleteObject(object.id)"
|
title="Удалить">
|
||||||
class="btn btn-outline btn-sm text-red-600 hover:bg-red-50"
|
🗑️
|
||||||
>
|
|
||||||
Удалить
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -135,13 +123,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Пустой state -->
|
<!-- Пустой state -->
|
||||||
<div
|
<div v-if="!loading && myObjects.length === 0" class="empty-state">
|
||||||
v-if="!loading && myObjects.length === 0"
|
<div class="empty-icon">🏢</div>
|
||||||
class="text-center py-12"
|
<h3 class="empty-title">У вас пока нет объектов</h3>
|
||||||
>
|
<p class="empty-description">Добавьте первый объект, чтобы начать работу</p>
|
||||||
<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 to="/objects/create" class="btn btn-primary">
|
||||||
Добавить первый объект
|
Добавить первый объект
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -149,15 +134,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Навигация -->
|
<!-- Навигация -->
|
||||||
<div class="mt-8 flex justify-between">
|
<div class="page-navigation">
|
||||||
<NuxtLink to="/objects" class="btn btn-outline">
|
<NuxtLink to="/objects" class="btn btn-outline btn-with-icon">
|
||||||
← Все объекты
|
← Все объекты
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/" class="btn btn-outline">
|
<NuxtLink to="/" class="btn btn-outline btn-with-icon">
|
||||||
🏠 На главную
|
🏠 На главную
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -186,7 +174,7 @@ const mockMyObjects: MyObjectItem[] = [
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Экскурсия по историческому центру',
|
title: 'Экскурсия по историческому центру',
|
||||||
type: 'excursion',
|
type: 'tour',
|
||||||
city: 'Санкт-Петербург',
|
city: 'Санкт-Петербург',
|
||||||
price: 1500,
|
price: 1500,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -247,7 +235,6 @@ const resetFilters = () => {
|
|||||||
|
|
||||||
const deleteObject = async (id: number) => {
|
const deleteObject = async (id: number) => {
|
||||||
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
|
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
|
||||||
// Имитация удаления
|
|
||||||
myObjects.value = myObjects.value.filter(obj => obj.id !== id)
|
myObjects.value = myObjects.value.filter(obj => obj.id !== id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,3 +262,265 @@ const formatDate = (dateString: string) => {
|
|||||||
return new Date(dateString).toLocaleDateString('ru-RU')
|
return new Date(dateString).toLocaleDateString('ru-RU')
|
||||||
}
|
}
|
||||||
</script>
|
</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