flatten easySite directory: remove extra easySite/easySite nesting
- Moved contents of main_dc/yalarba/easySite/easySite/ up to easySite/ - Updated docker-compose.yml build context path - Deleted empty nested easySite/ directory
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
|
||||
<main class="objects-page">
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Все объекты</h1>
|
||||
<p class="page-subtitle">Найдено {{ totalObjects }} объектов</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-outline btn-with-icon" @click="showFilters = !showFilters">
|
||||
<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="quick-filters">
|
||||
<button
|
||||
v-for="type in quickTypes"
|
||||
:key="type.value"
|
||||
class="quick-filter"
|
||||
:class="{ active: filters.type === type.value }"
|
||||
@click="toggleQuickFilter(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="guest_house">🏡 Гостевой дом</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Поиск</label>
|
||||
<input v-model="filters.q" 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="Макс. цена">
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<button class="btn btn-primary" @click="applyFilters">Применить фильтры</button>
|
||||
<button class="btn btn-outline" @click="resetFilters">Сбросить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</select>
|
||||
<button class="btn btn-outline btn-sm" @click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'">
|
||||
{{ sortOrder === 'asc' ? '↑' : '↓' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="btn btn-outline btn-sm" :class="{ 'btn-primary': viewMode === 'grid' }"
|
||||
@click="viewMode = 'grid'">
|
||||
▦
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline btn-sm" :class="{ 'btn-primary': viewMode === 'list' }"
|
||||
@click="viewMode = 'list'">
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"/>
|
||||
<p class="loading-text">Загрузка объектов...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="objects.length === 0" class="empty-state">
|
||||
<div class="empty-icon">🏢</div>
|
||||
<h3 class="empty-title">Объекты не найдены</h3>
|
||||
<p class="empty-description">Попробуйте изменить параметры поиска</p>
|
||||
<button class="btn btn-primary" @click="resetFilters">Сбросить фильтры</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 && objects.length > 0" class="pagination">
|
||||
<button
|
||||
v-for="page in totalPages"
|
||||
:key="page"
|
||||
class="pagination-btn"
|
||||
:class="{ active: currentPage === page }"
|
||||
@click="currentPage = page">
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ObjectShortResponse } from '~/types/objects'
|
||||
|
||||
useHead({ title: 'Все объекты - EasySite' })
|
||||
|
||||
const { getList } = useObjects()
|
||||
|
||||
const objects = ref<ObjectShortResponse[]>([])
|
||||
const totalObjects = ref(0)
|
||||
const loading = ref(true)
|
||||
const showFilters = ref(false)
|
||||
const viewMode = ref<'grid' | 'list'>('grid')
|
||||
const sortBy = ref<'title' | 'price' | 'rating'>('title')
|
||||
const sortOrder = ref<'asc' | 'desc'>('asc')
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 9
|
||||
|
||||
const filters = ref({
|
||||
q: '',
|
||||
type: '',
|
||||
maxPrice: null as number | null
|
||||
})
|
||||
|
||||
const quickTypes = [
|
||||
{ value: 'hotel', label: 'Гостиницы', icon: '🏨' },
|
||||
{ value: 'sanatorium', label: 'Санатории', icon: '🏥' },
|
||||
{ value: 'tour', label: 'Туры', icon: '🧳' },
|
||||
{ value: 'restaurant', label: 'Рестораны', icon: '🍴' }
|
||||
]
|
||||
|
||||
const activeFiltersCount = computed(() => {
|
||||
return Object.values(filters.value).filter(val =>
|
||||
val !== '' && val !== null
|
||||
).length
|
||||
})
|
||||
|
||||
const sortedObjects = computed(() => {
|
||||
const sorted = [...objects.value].sort((a, b) => {
|
||||
if (sortBy.value === 'price') {
|
||||
const aVal = a.price || 0
|
||||
const bVal = b.price || 0
|
||||
return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
|
||||
}
|
||||
if (sortBy.value === 'rating') {
|
||||
const aVal = a.tourist_average_score || a.entrepreneur_average_score || 0
|
||||
const bVal = b.tourist_average_score || b.entrepreneur_average_score || 0
|
||||
return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
|
||||
}
|
||||
const aVal = (a.title || a.short_name || '').toLowerCase()
|
||||
const bVal = (b.title || b.short_name || '').toLowerCase()
|
||||
return sortOrder.value === 'asc' ? aVal.localeCompare(bVal) : 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(sortedObjects.value.length / itemsPerPage)
|
||||
})
|
||||
|
||||
const toggleQuickFilter = (type: string) => {
|
||||
filters.value.type = filters.value.type === type ? '' : type
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
const applyFilters = () => {
|
||||
currentPage.value = 1
|
||||
loadObjects()
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.value = { q: '', type: '', maxPrice: null }
|
||||
currentPage.value = 1
|
||||
loadObjects()
|
||||
}
|
||||
|
||||
const navigateToObject = (id: number) => {
|
||||
navigateTo(`/objects/${id}`)
|
||||
}
|
||||
|
||||
const loadObjects = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getList({
|
||||
page: currentPage.value,
|
||||
page_size: 50,
|
||||
type: filters.value.type || undefined,
|
||||
q: filters.value.q || undefined
|
||||
})
|
||||
let items = response.items
|
||||
if (filters.value.maxPrice) {
|
||||
items = items.filter(o => (o.price || 0) <= filters.value.maxPrice!)
|
||||
}
|
||||
objects.value = items
|
||||
totalObjects.value = response.total
|
||||
} catch (error) {
|
||||
console.error('Error loading objects:', error)
|
||||
objects.value = []
|
||||
totalObjects.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadObjects)
|
||||
</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-xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-filters {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--bg-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.quick-filter:hover,
|
||||
.quick-filter.active {
|
||||
background: var(--primary-500);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--primary-500);
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
padding: var(--space-lg);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.view-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.objects-grid {
|
||||
display: grid;
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.objects-grid.grid-view {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.objects-grid.list-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-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);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: var(--space-lg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-btn.active,
|
||||
.pagination-btn:hover {
|
||||
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;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.objects-grid.grid-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user