2941b14b38
- Moved contents of main_dc/yalarba/easySite/easySite/ up to easySite/ - Updated docker-compose.yml build context path - Deleted empty nested easySite/ directory
583 lines
16 KiB
Vue
583 lines
16 KiB
Vue
<template>
|
||
<div class="profile-page">
|
||
<div class="container container-large">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Профиль пользователя</h1>
|
||
<p class="page-subtitle">Информация о вашем аккаунте</p>
|
||
</div>
|
||
|
||
<div v-if="auth.user.value" class="profile-layout">
|
||
<!-- Основная информация -->
|
||
<div class="profile-main">
|
||
<!-- Личная информация -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Личная информация</h2>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="user-info">
|
||
<div class="user-avatar-section">
|
||
<div class="user-avatar">
|
||
<span class="avatar-text">{{ userInitials }}</span>
|
||
</div>
|
||
<div class="user-details">
|
||
<div class="user-name">{{ auth.user.value.full_name || 'Не указано' }}</div>
|
||
<div class="user-email">{{ auth.user.value.email || 'Не указано' }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<label class="info-label">Полное имя</label>
|
||
<div class="info-value">{{ auth.user.value.full_name || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<label class="info-label">ИНН</label>
|
||
<div class="info-value">{{ auth.user.value.inn || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<label class="info-label">Телефон</label>
|
||
<div class="info-value">{{ auth.user.value.phone || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<label class="info-label">Город</label>
|
||
<div class="info-value">{{ auth.user.value.city || 'Не указано' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Информация об организации -->
|
||
<div v-if="hasOrganizationInfo" class="card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Информация об организации</h2>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="org-info">
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<label class="info-label">Форма организации</label>
|
||
<div class="info-value">{{ orgTypeText }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<label class="info-label">Полное название организации</label>
|
||
<div class="info-value">{{ auth.user.value.org_full_name || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<label class="info-label">Короткое название организации</label>
|
||
<div class="info-value">{{ auth.user.value.org_short_name || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<label class="info-label">ИНН организации</label>
|
||
<div class="info-value">{{ auth.user.value.org_inn || 'Не указано' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Статистика -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Статистика</h2>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="user-stats">
|
||
<div class="stat-item">
|
||
<div class="stat-number">{{ userStats.objects }}</div>
|
||
<div class="stat-label">Объектов</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-number">{{ userStats.reviews }}</div>
|
||
<div class="stat-label">Отзывов</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-number">{{ userStats.active }}</div>
|
||
<div class="stat-label">Активных</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-number">{{ userStats.moderation }}</div>
|
||
<div class="stat-label">На модерации</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Быстрые действия -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Быстрые действия</h2>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="actions-grid">
|
||
<NuxtLink to="/objects/my-objects" class="btn btn-outline action-btn">
|
||
📝 Мои объекты
|
||
</NuxtLink>
|
||
<NuxtLink to="/objects/create" class="btn btn-primary action-btn">
|
||
➕ Добавить объект
|
||
</NuxtLink>
|
||
<NuxtLink to="/objects" class="btn btn-outline action-btn">
|
||
🔍 Все объекты
|
||
</NuxtLink>
|
||
<NuxtLink to="/profile/edit" class="btn btn-outline action-btn">
|
||
✏️ Редактировать профиль
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Боковая панель -->
|
||
<div class="profile-sidebar">
|
||
<!-- Статус -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Статус аккаунта</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="status-item">
|
||
<span class="status-icon">✅</span>
|
||
<span class="status-text">Активен</span>
|
||
</div>
|
||
<div v-if="auth.user.value.org_type" class="status-item">
|
||
<span class="status-icon">🏢</span>
|
||
<span class="status-text">{{ orgTypeText }}</span>
|
||
</div>
|
||
<p class="status-note">Демо-режим</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Контактная информация -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Контактная информация</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="contact-info">
|
||
<div class="contact-item">
|
||
<label class="contact-label">Телефон</label>
|
||
<div class="contact-value">{{ auth.user.value.phone || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="contact-item">
|
||
<label class="contact-label">Email</label>
|
||
<div class="contact-value">{{ auth.user.value.email || 'Не указано' }}</div>
|
||
</div>
|
||
<div class="contact-item">
|
||
<label class="contact-label">Город</label>
|
||
<div class="contact-value">{{ auth.user.value.city || 'Не указано' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Навигация -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Навигация</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="nav-links">
|
||
<NuxtLink to="/objects" class="nav-link-block">
|
||
🔍 Все объекты
|
||
</NuxtLink>
|
||
<NuxtLink to="/objects/my-objects" class="nav-link-block">
|
||
📝 Мои объекты
|
||
</NuxtLink>
|
||
<NuxtLink to="/objects/create" class="nav-link-block">
|
||
➕ Добавить объект
|
||
</NuxtLink>
|
||
<NuxtLink to="/profile/edit" class="nav-link-block">
|
||
✏️ Редактировать профиль
|
||
</NuxtLink>
|
||
<button class="nav-link-block logout-btn" @click="auth.logout()">
|
||
🚪 Выйти
|
||
</button>
|
||
<NuxtLink to="/" class="nav-link-block">
|
||
🏠 Главная
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Загрузка -->
|
||
<div v-else-if="auth.loading.value" class="loading-container">
|
||
<div class="loading-spinner" />
|
||
<p>Загрузка профиля...</p>
|
||
</div>
|
||
|
||
<!-- Ошибка -->
|
||
<div v-else class="error-container">
|
||
<p>Не удалось загрузить данные профиля</p>
|
||
<button class="btn btn-primary" @click="reload">Попробовать снова</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
definePageMeta({
|
||
middleware: 'auth',
|
||
requiresAuth: true
|
||
})
|
||
|
||
const auth = useAuth()
|
||
|
||
// Инициализация данных пользователя
|
||
onMounted(async () => {
|
||
if (!auth.user.value) {
|
||
await auth.checkAuth()
|
||
}
|
||
})
|
||
|
||
// Вычисляем инициалы пользователя для аватара
|
||
const userInitials = computed(() => {
|
||
if (!auth.user.value?.full_name) return '??'
|
||
|
||
const names = auth.user.value.full_name.split(' ')
|
||
if (names.length >= 2 && names[0] && names[1]) {
|
||
return `${names[0][0]}${names[1][0]}`.toUpperCase()
|
||
}
|
||
return auth.user.value.full_name.substring(0, 2).toUpperCase()
|
||
})
|
||
|
||
// Текст для типа организации
|
||
const orgTypeText = computed(() => {
|
||
const types: Record<string, string> = {
|
||
individual: 'Индивидуальный предприниматель',
|
||
llc: 'Общество с ограниченной ответственностью',
|
||
jsc: 'Акционерное общество'
|
||
}
|
||
return types[auth.user.value?.org_type || ''] || 'Не указано'
|
||
})
|
||
|
||
// Проверяем, есть ли информация об организации
|
||
const hasOrganizationInfo = computed(() => {
|
||
return auth.user.value?.org_type ||
|
||
auth.user.value?.org_full_name ||
|
||
auth.user.value?.org_short_name ||
|
||
auth.user.value?.org_inn
|
||
})
|
||
|
||
// Статистика пользователя (заглушка - в реальном приложении нужно получать с API)
|
||
const userStats = computed(() => {
|
||
return {
|
||
objects: auth.user.value?.objects_count || 0,
|
||
reviews: auth.user.value?.reviews_count || 0,
|
||
active: auth.user.value?.active_objects_count || 0,
|
||
moderation: auth.user.value?.moderation_objects_count || 0
|
||
}
|
||
})
|
||
|
||
const reload = () => {
|
||
window.location.reload()
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.profile-page {
|
||
min-height: 100vh;
|
||
background: var(--bg-secondary);
|
||
padding: 2rem 0;
|
||
}
|
||
|
||
.container-large {
|
||
max-width: 1200px;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.page-subtitle {
|
||
color: var(--text-secondary);
|
||
font-size: 1.125rem;
|
||
}
|
||
|
||
.profile-layout {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.profile-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.profile-sidebar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.user-avatar-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 4rem;
|
||
height: 4rem;
|
||
background: var(--primary-100);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-text {
|
||
color: var(--primary-600);
|
||
font-weight: bold;
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.user-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.user-email {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.info-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 0.875rem;
|
||
color: var(--text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.info-value {
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.org-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.contact-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.contact-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.contact-label {
|
||
font-size: 0.75rem;
|
||
color: var(--text-tertiary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.contact-value {
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.user-stats {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
padding: 1rem;
|
||
background: var(--bg-secondary);
|
||
border-radius: var(--radius-md);
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
color: var(--primary-600);
|
||
}
|
||
|
||
.stat-label {
|
||
color: var(--text-secondary);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.actions-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.action-btn {
|
||
display: flex;
|
||
justify-content: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 1.125rem;
|
||
}
|
||
|
||
.status-text {
|
||
font-weight: 500;
|
||
color: var(--success-600);
|
||
}
|
||
|
||
.status-note {
|
||
color: var(--text-secondary);
|
||
font-size: 0.875rem;
|
||
margin-top: 0.5rem;
|
||
padding-top: 0.5rem;
|
||
border-top: 1px solid var(--border-light);
|
||
}
|
||
|
||
.nav-links {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.nav-link-block {
|
||
display: block;
|
||
padding: 0.75rem 1rem;
|
||
border-radius: var(--radius-md);
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
transition: all 0.2s;
|
||
font-weight: 500;
|
||
border: none;
|
||
background: none;
|
||
width: 100%;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.nav-link-block:hover {
|
||
color: var(--primary-600);
|
||
background: var(--primary-50);
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
color: var(--error-600);
|
||
background: var(--error-50);
|
||
}
|
||
|
||
.loading-container,
|
||
.error-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 4rem 2rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.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-bottom: 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 968px) {
|
||
.profile-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.actions-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.user-stats {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.user-avatar-section {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.user-stats {
|
||
grid-template-columns: 1fr;
|
||
gap: 1rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.profile-page {
|
||
padding: 1rem 0;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.profile-main,
|
||
.profile-sidebar {
|
||
gap: 1rem;
|
||
}
|
||
}
|
||
</style> |