f78a23ff38
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 add header footer components into new pages
529 lines
13 KiB
Vue
529 lines
13 KiB
Vue
<template>
|
||
<div class="page-wrapper">
|
||
<Header />
|
||
|
||
<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="inactive">Неактивные</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.search" 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 class="objects-table card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Список объектов</h2>
|
||
<p class="card-subtitle">Всего: {{ myObjects.length }} объектов</p>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table class="objects-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="table-header">Название</th>
|
||
<th class="table-header">Тип</th>
|
||
<th class="table-header">Город</th>
|
||
<th class="table-header">Цена</th>
|
||
<th class="table-header">Статус</th>
|
||
<th class="table-header">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="object in myObjects" :key="object.id" class="table-row">
|
||
<td class="table-cell">
|
||
<div class="object-info">
|
||
<div class="object-title">{{ object.title }}</div>
|
||
<div class="object-date">{{ formatDate(object.createdAt) }}</div>
|
||
</div>
|
||
</td>
|
||
<td class="table-cell">
|
||
<span class="badge badge-primary">
|
||
{{ getTypeLabel(object.type) }}
|
||
</span>
|
||
</td>
|
||
<td class="table-cell">{{ object.city }}</td>
|
||
<td class="table-cell">
|
||
<div class="object-price">{{ formatPrice(object.price) }}</div>
|
||
</td>
|
||
<td class="table-cell">
|
||
<span class="status-badge" :class="object.isActive ? 'status-active' : 'status-inactive'">
|
||
{{ object.isActive ? 'Активен' : 'Неактивен' }}
|
||
</span>
|
||
</td>
|
||
<td class="table-cell">
|
||
<div class="action-buttons">
|
||
<NuxtLink :to="`/objects/${object.id}`" class="btn btn-outline btn-sm btn-with-icon"
|
||
title="Просмотр">
|
||
👁️
|
||
</NuxtLink>
|
||
<NuxtLink :to="`/objects/${object.id}/edit`" class="btn btn-outline btn-sm btn-with-icon"
|
||
title="Редактировать">
|
||
✏️
|
||
</NuxtLink>
|
||
<button @click="deleteObject(object.id)" class="btn btn-outline btn-sm btn-with-icon delete-btn"
|
||
title="Удалить">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Пустой state -->
|
||
<div v-if="!loading && myObjects.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>
|
||
</div>
|
||
|
||
<!-- Навигация -->
|
||
<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>
|
||
|
||
<Footer />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import Header from '~/components/layout/Header.vue'
|
||
import Footer from '~/components/layout/Footer.vue'
|
||
|
||
interface MyObjectItem {
|
||
id: number
|
||
title: string
|
||
type: string
|
||
city: string
|
||
price: number
|
||
isActive: boolean
|
||
createdAt: string
|
||
}
|
||
|
||
// Мок-данные
|
||
const mockMyObjects: MyObjectItem[] = [
|
||
{
|
||
id: 1,
|
||
title: 'Гостевой дом "У озера"',
|
||
type: 'guest_house',
|
||
city: 'Карелия',
|
||
price: 2800,
|
||
isActive: true,
|
||
createdAt: '2024-01-15'
|
||
},
|
||
{
|
||
id: 2,
|
||
title: 'Экскурсия по историческому центру',
|
||
type: 'tour',
|
||
city: 'Санкт-Петербург',
|
||
price: 1500,
|
||
isActive: true,
|
||
createdAt: '2024-01-10'
|
||
},
|
||
{
|
||
id: 3,
|
||
title: 'Горнолыжный курорт "Снежный"',
|
||
type: 'hotel',
|
||
city: 'Красная Поляна',
|
||
price: 5200,
|
||
isActive: false,
|
||
createdAt: '2024-01-05'
|
||
}
|
||
]
|
||
|
||
const myObjects = ref<MyObjectItem[]>(mockMyObjects)
|
||
const loading = ref(false)
|
||
|
||
const filters = ref({
|
||
status: '',
|
||
type: '',
|
||
search: ''
|
||
})
|
||
|
||
const applyFilters = () => {
|
||
let filtered = [...mockMyObjects]
|
||
|
||
if (filters.value.status) {
|
||
filtered = filtered.filter(obj =>
|
||
filters.value.status === 'active' ? obj.isActive : !obj.isActive
|
||
)
|
||
}
|
||
|
||
if (filters.value.type) {
|
||
filtered = filtered.filter(obj => obj.type === filters.value.type)
|
||
}
|
||
|
||
if (filters.value.search) {
|
||
const search = filters.value.search.toLowerCase()
|
||
filtered = filtered.filter(obj =>
|
||
obj.title.toLowerCase().includes(search) ||
|
||
obj.city.toLowerCase().includes(search)
|
||
)
|
||
}
|
||
|
||
myObjects.value = filtered
|
||
}
|
||
|
||
const resetFilters = () => {
|
||
filters.value = {
|
||
status: '',
|
||
type: '',
|
||
search: ''
|
||
}
|
||
myObjects.value = mockMyObjects
|
||
}
|
||
|
||
const deleteObject = async (id: number) => {
|
||
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
|
||
myObjects.value = myObjects.value.filter(obj => obj.id !== id)
|
||
}
|
||
}
|
||
|
||
const getTypeLabel = (type: string) => {
|
||
const types: Record<string, string> = {
|
||
hotel: 'Гостиница',
|
||
sanatorium: 'Санаторий',
|
||
guest_house: 'Гостевой дом',
|
||
tour: 'Тур',
|
||
excursion: 'Экскурсия'
|
||
}
|
||
return types[type] || type
|
||
}
|
||
|
||
const formatPrice = (price: number) => {
|
||
return new Intl.NumberFormat('ru-RU', {
|
||
style: 'currency',
|
||
currency: 'RUB',
|
||
minimumFractionDigits: 0
|
||
}).format(price)
|
||
}
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('ru-RU')
|
||
}
|
||
</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> |