2941b14b38
- Moved contents of main_dc/yalarba/easySite/easySite/ up to easySite/ - Updated docker-compose.yml build context path - Deleted empty nested easySite/ directory
521 lines
14 KiB
Vue
521 lines
14 KiB
Vue
<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>
|