Files
tp/main_dc/yalarba/easySite/app/pages/objects/index.vue
T
valitovgaziz 2941b14b38 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
2026-06-12 11:16:15 +05:00

452 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>