Files
tp/main_dc/yalarba/easySite/app/pages/objects/my-objects.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

521 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="my-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">Управление вашими добавленными местами</p>
</div>
<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 btn-with-icon">
<span></span>
Добавить объект
</NuxtLink>
</div>
</div>
</div>
<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">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="draft">Черновики</option>
<option value="moderation">На модерации</option>
<option value="inactive">Неактивные</option>
<option value="rejected">Отклонённые</option>
</select>
</div>
<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="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>
<div class="filter-actions">
<button class="btn btn-primary" @click="applyFilters">Применить</button>
<button class="btn btn-outline" @click="resetFilters">Сбросить</button>
</div>
</div>
<div v-if="loading" class="loading-state">
<div class="loading-spinner"/>
<p class="loading-text">Загрузка объектов...</p>
</div>
<template v-else>
<div class="objects-grid">
<div class="add-card" @click="navigateTo('/objects/create')">
<div class="add-card-content">
<div class="add-icon"></div>
<h3 class="add-title">Добавить объект</h3>
<p class="add-description">Создайте новое место для размещения</p>
</div>
</div>
<div v-for="item in filteredObjects" :key="item.id" class="object-card">
<div class="card-image">
<img :src="'/images/placeholder.jpg'" :alt="item.title || item.short_name">
<div class="card-badge" :class="statusBadgeClass(item.status)">
{{ statusLabel(item.status) }}
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ item.title || item.short_name }}</h3>
<div class="card-meta">
<span class="card-type">{{ getTypeLabel(item.type) }}</span>
</div>
<p class="card-description">{{ item.address || 'Адрес не указан' }}</p>
<div class="card-price">{{ formatPrice(item.price) }}</div>
</div>
<div class="card-actions">
<NuxtLink :to="`/objects/${item.id}`" class="btn btn-outline btn-sm btn-with-icon view-btn">
<span>👁</span>
Просмотр
</NuxtLink>
<div class="action-buttons">
<NuxtLink :to="`/objects/${item.id}/edit`" class="btn btn-outline btn-sm btn-with-icon">
<span></span>
</NuxtLink>
<button
class="btn btn-outline btn-sm btn-with-icon delete-btn"
@click="deleteObject(item.id)">
<span>🗑</span>
</button>
</div>
</div>
</div>
</div>
<div v-if="!loading && filteredObjects.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>
</div>
</template>
<div class="page-navigation">
<NuxtLink to="/objects" class="btn btn-outline btn-with-icon"> Все объекты</NuxtLink>
<NuxtLink to="/" class="btn btn-outline btn-with-icon">🏠 На главную</NuxtLink>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import type { ObjectShortResponse } from '~/types/objects'
const { getMy, remove } = useObjects()
const objects = ref<ObjectShortResponse[]>([])
const loading = ref(true)
const filters = ref({ status: '', type: '', q: '' })
const filteredObjects = computed(() => {
let result = [...objects.value]
if (filters.value.status) {
result = result.filter(o => o.status === filters.value.status)
}
if (filters.value.type) {
result = result.filter(o => o.type === filters.value.type)
}
if (filters.value.q) {
const q = filters.value.q.toLowerCase()
result = result.filter(o =>
(o.title || o.short_name || '').toLowerCase().includes(q) ||
(o.address || '').toLowerCase().includes(q)
)
}
return result
})
const applyFilters = () => {}
const resetFilters = () => {
filters.value = { status: '', type: '', q: '' }
}
const statusLabel = (status: string) => {
const labels: Record<string, string> = {
active: 'Активен',
draft: 'Черновик',
moderation: 'На модерации',
inactive: 'Неактивен',
rejected: 'Отклонён'
}
return labels[status] || status
}
const statusBadgeClass = (status: string) => {
const classes: Record<string, string> = {
active: 'badge-success',
draft: 'badge-secondary',
moderation: 'badge-warning',
inactive: 'badge-secondary',
rejected: 'badge-error'
}
return classes[status] || 'badge-secondary'
}
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
hotel: '🏨 Гостиница',
sanatorium: '🏥 Санаторий',
guest_house: '🏡 Гостевой дом',
tour: '🧳 Тур',
excursion: '🚶 Экскурсия'
}
return types[type] || type
}
const formatPrice = (price: number | undefined) => {
if (!price && price !== 0) return '—'
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(price)
}
const deleteObject = async (id: number) => {
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
try {
await remove(id)
objects.value = objects.value.filter(o => o.id !== id)
} catch (error) {
console.error('Error deleting object:', error)
alert('Ошибка при удалении объекта')
}
}
}
onMounted(async () => {
loading.value = true
try {
const response = await getMy()
objects.value = response.items
} catch (error) {
console.error('Error loading my objects:', error)
objects.value = []
} finally {
loading.value = false
}
})
</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-xl);
}
.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;
}
.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);
}
.objects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.add-card {
border: 2px dashed var(--border-light);
border-radius: var(--radius-lg);
padding: var(--space-xl);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
min-height: 200px;
}
.add-card:hover {
border-color: var(--primary-500);
background: var(--primary-50);
}
.add-card-content {
text-align: center;
}
.add-icon {
font-size: 3rem;
margin-bottom: var(--space-md);
}
.add-title {
font-family: var(--font-heading);
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-xs);
}
.add-description {
color: var(--text-tertiary);
font-size: var(--text-sm);
}
.object-card {
background: var(--bg-primary);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-light);
transition: box-shadow 0.3s ease;
}
.object-card:hover {
box-shadow: var(--shadow-md);
}
.card-image {
position: relative;
height: 180px;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-badge {
position: absolute;
top: var(--space-sm);
right: var(--space-sm);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-weight: var(--font-medium);
}
.badge-success {
background: var(--success-100);
color: var(--success-700);
}
.badge-secondary {
background: var(--gray-100);
color: var(--gray-600);
}
.badge-warning {
background: var(--warning-100);
color: var(--warning-700);
}
.badge-error {
background: var(--danger-100);
color: var(--danger-700);
}
.card-content {
padding: var(--space-lg);
}
.card-title {
font-family: var(--font-heading);
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-sm);
}
.card-meta {
display: flex;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
}
.card-type {
padding: 2px var(--space-xs);
background: var(--bg-secondary);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
color: var(--text-secondary);
}
.card-description {
font-size: var(--text-sm);
color: var(--text-tertiary);
margin-bottom: var(--space-md);
line-height: 1.4;
}
.card-price {
font-family: var(--font-heading);
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--primary-600);
}
.card-actions {
padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.action-buttons {
display: flex;
gap: var(--space-xs);
}
.delete-btn {
color: var(--danger-500);
border-color: var(--danger-200);
}
.delete-btn:hover {
background: var(--danger-50);
border-color: var(--danger-500);
}
.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);
}
.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);
margin-bottom: var(--space-sm);
}
.page-navigation {
display: flex;
justify-content: space-between;
margin-top: var(--space-xl);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
}
.filter-grid {
grid-template-columns: 1fr;
}
.objects-grid {
grid-template-columns: 1fr;
}
.page-navigation {
flex-direction: column;
gap: var(--space-sm);
}
}
</style>