2025-05-21 22:13:09 +07:00
|
|
|
|
<template>
|
|
|
|
|
<div class="edit-profile-form mt-3">
|
|
|
|
|
<h4>Редактировать профиль</h4>
|
|
|
|
|
<form @submit.prevent="handleSubmit">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="editName" class="form-label">Имя:</label>
|
|
|
|
|
<input type="text" class="form-control" id="editName" v-model="formData.name" :disabled="profileLoading" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="editBio" class="form-label">О себе:</label>
|
|
|
|
|
<textarea class="form-control" id="editBio" rows="3" v-model="formData.bio" :disabled="profileLoading"></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="editDateOfBirth" class="form-label">Дата рождения:</label>
|
|
|
|
|
<input type="date" class="form-control" id="editDateOfBirth" v-model="formData.dateOfBirth" :disabled="profileLoading" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="editGender" class="form-label">Пол:</label>
|
|
|
|
|
<select class="form-select" id="editGender" v-model="formData.gender" :disabled="profileLoading">
|
|
|
|
|
<option value="">Не выбрано</option>
|
|
|
|
|
<option value="male">Мужской</option>
|
|
|
|
|
<option value="female">Женский</option>
|
|
|
|
|
<option value="other">Другой</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2025-05-22 01:03:48 +07:00
|
|
|
|
|
2025-05-22 01:09:12 +07:00
|
|
|
|
<!-- Улучшенное поле для выбора города -->
|
|
|
|
|
<div class="mb-3 city-select-container">
|
2025-05-22 01:03:48 +07:00
|
|
|
|
<label for="editCity" class="form-label">Город:</label>
|
2025-05-22 01:09:12 +07:00
|
|
|
|
<div class="input-group">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
id="editCity"
|
|
|
|
|
v-model="citySearchQuery"
|
|
|
|
|
placeholder="Начните вводить название города..."
|
|
|
|
|
@focus="showCityList = true"
|
|
|
|
|
@input="onCitySearch"
|
|
|
|
|
:disabled="profileLoading"
|
|
|
|
|
/>
|
|
|
|
|
<span class="input-group-text" @click="clearCitySelection" v-if="formData.location.city">
|
|
|
|
|
<i class="bi bi-x-circle"></i>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="showCityList && filteredCities.length" class="city-dropdown">
|
|
|
|
|
<div class="search-results-info" v-if="filteredCities.length">
|
|
|
|
|
Найдено городов: {{ filteredCities.length }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="city-list-container">
|
|
|
|
|
<div
|
|
|
|
|
v-for="city in filteredCities"
|
|
|
|
|
:key="city.name"
|
|
|
|
|
class="city-item"
|
|
|
|
|
@click="selectCity(city)"
|
|
|
|
|
>
|
|
|
|
|
<div class="city-name">{{ city.name }}</div>
|
|
|
|
|
<div class="city-region">{{ city.subject }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else-if="showCityList && citySearchQuery && !filteredCities.length" class="no-results">
|
|
|
|
|
Городов не найдено
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="formData.location.city" class="selected-city mt-2">
|
|
|
|
|
<small>Выбранный город: <strong>{{ formData.location.city }}</strong></small>
|
|
|
|
|
</div>
|
|
|
|
|
<input type="hidden" v-model="formData.location.city" />
|
2025-05-22 01:03:48 +07:00
|
|
|
|
</div>
|
2025-05-22 01:09:12 +07:00
|
|
|
|
<!-- Конец улучшенного поля для выбора города -->
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
|
|
|
|
<div v-if="profileSuccessMessage" class="alert alert-success">{{ profileSuccessMessage }}</div>
|
|
|
|
|
<div v-if="profileErrorMessage" class="alert alert-danger">{{ profileErrorMessage }}</div>
|
|
|
|
|
|
|
|
|
|
<button type="submit" class="btn btn-primary" :disabled="profileLoading">
|
|
|
|
|
<span v-if="profileLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
|
|
|
{{ profileLoading ? 'Сохранение...' : 'Сохранить изменения' }}
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<hr class="my-4">
|
|
|
|
|
|
|
|
|
|
<h4>Загрузить фото профиля</h4>
|
2025-05-21 23:43:24 +07:00
|
|
|
|
<form @submit.prevent="handleMultiplePhotoUpload" class="mt-3">
|
2025-05-21 22:13:09 +07:00
|
|
|
|
<div class="mb-3">
|
2025-05-21 23:43:24 +07:00
|
|
|
|
<label for="profilePhotos" class="form-label">Выберите фото:</label>
|
|
|
|
|
<input type="file" class="form-control" id="profilePhotos" @change="onFilesSelected" accept="image/*" multiple :disabled="photoLoading">
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="previewUrl" class="mb-3">
|
|
|
|
|
<p>Предпросмотр:</p>
|
|
|
|
|
<img :src="previewUrl" alt="Предпросмотр фото" class="img-thumbnail" style="max-width: 200px; max-height: 200px;" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="photoUploadSuccessMessage" class="alert alert-success">{{ photoUploadSuccessMessage }}</div>
|
|
|
|
|
<div v-if="photoUploadErrorMessage" class="alert alert-danger">{{ photoUploadErrorMessage }}</div>
|
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
<div v-for="(file, index) in selectedFiles" :key="index" class="upload-message" :class="file.status">
|
|
|
|
|
{{ file.message }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button type="submit" class="btn btn-secondary" :disabled="photoLoading || selectedFiles.length === 0">
|
2025-05-21 22:13:09 +07:00
|
|
|
|
<span v-if="photoLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
|
|
|
{{ photoLoading ? 'Загрузка...' : 'Загрузить фото' }}
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-05-22 01:09:12 +07:00
|
|
|
|
import { ref, onMounted, watch, computed, nextTick } from 'vue';
|
2025-05-21 22:13:09 +07:00
|
|
|
|
import { useAuth } from '@/auth';
|
|
|
|
|
import api from '@/services/api';
|
2025-05-22 01:03:48 +07:00
|
|
|
|
import imageCompression from 'browser-image-compression';
|
|
|
|
|
import russianCities from '@/assets/russian-cities.json'; // Импортируем список городов
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
|
|
|
|
const { user, fetchUser } = useAuth();
|
|
|
|
|
|
|
|
|
|
const formData = ref({
|
|
|
|
|
name: '',
|
|
|
|
|
bio: '',
|
|
|
|
|
dateOfBirth: '',
|
|
|
|
|
gender: '',
|
2025-05-22 01:09:12 +07:00
|
|
|
|
location: {
|
2025-05-22 01:03:48 +07:00
|
|
|
|
city: '',
|
2025-05-22 01:09:12 +07:00
|
|
|
|
country: 'Россия'
|
2025-05-22 01:03:48 +07:00
|
|
|
|
}
|
2025-05-21 22:13:09 +07:00
|
|
|
|
});
|
2025-05-22 01:03:48 +07:00
|
|
|
|
|
|
|
|
|
// Для выбора города
|
2025-05-22 01:09:12 +07:00
|
|
|
|
const allCities = ref([]);
|
2025-05-22 01:03:48 +07:00
|
|
|
|
const citySearchQuery = ref('');
|
|
|
|
|
const showCityList = ref(false);
|
2025-05-22 01:09:12 +07:00
|
|
|
|
const selectedCity = ref(null);
|
|
|
|
|
const searchTimeout = ref(null);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
|
|
|
|
const profileLoading = ref(false);
|
|
|
|
|
const profileErrorMessage = ref('');
|
|
|
|
|
const profileSuccessMessage = ref('');
|
|
|
|
|
|
|
|
|
|
const photoLoading = ref(false);
|
|
|
|
|
const photoUploadErrorMessage = ref('');
|
|
|
|
|
const photoUploadSuccessMessage = ref('');
|
2025-05-21 23:43:24 +07:00
|
|
|
|
const selectedFile = ref(null); // Это будет использоваться для одиночной загрузки, если мы решим ее оставить
|
|
|
|
|
const previewUrl = ref(null); // И это
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
// Новые refs для множественной загрузки
|
|
|
|
|
const selectedFiles = ref([]); // Массив выбранных файлов
|
|
|
|
|
const photoUploadMessages = ref([]); // Массив сообщений о статусе загрузки каждого файла
|
|
|
|
|
|
2025-05-22 01:09:12 +07:00
|
|
|
|
// Улучшенный поиск с дебаунсингом
|
|
|
|
|
const onCitySearch = () => {
|
|
|
|
|
if (searchTimeout.value) {
|
|
|
|
|
clearTimeout(searchTimeout.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Если поле поиска пустое и нет выбранного города, сбрасываем город
|
|
|
|
|
if (!citySearchQuery.value && formData.value.location.city) {
|
|
|
|
|
formData.value.location.city = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Показываем список только если есть текст для поиска
|
|
|
|
|
if (citySearchQuery.value) {
|
|
|
|
|
showCityList.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Добавляем небольшую задержку для улучшения производительности
|
|
|
|
|
searchTimeout.value = setTimeout(() => {
|
|
|
|
|
// Если значение в поле поиска не соответствует выбранному городу,
|
|
|
|
|
// считаем, что пользователь начал новый поиск
|
|
|
|
|
if (selectedCity.value && citySearchQuery.value !== selectedCity.value.name) {
|
|
|
|
|
selectedCity.value = null;
|
|
|
|
|
}
|
|
|
|
|
}, 300);
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-22 01:03:48 +07:00
|
|
|
|
const filteredCities = computed(() => {
|
|
|
|
|
if (!citySearchQuery.value) {
|
2025-05-22 01:09:12 +07:00
|
|
|
|
return allCities.value.slice(0, 10); // Возвращаем первые 10 городов, если поле пустое
|
2025-05-22 01:03:48 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
const query = citySearchQuery.value.toLowerCase().trim();
|
|
|
|
|
|
|
|
|
|
// Сначала находим города, начинающиеся с запроса
|
|
|
|
|
const startsWithMatch = allCities.value.filter(city =>
|
|
|
|
|
city.name.toLowerCase().startsWith(query)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Затем города, содержащие запрос где-то в середине
|
|
|
|
|
const containsMatch = allCities.value.filter(city =>
|
|
|
|
|
!city.name.toLowerCase().startsWith(query) &&
|
2025-05-22 01:03:48 +07:00
|
|
|
|
city.name.toLowerCase().includes(query)
|
|
|
|
|
);
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
// Объединяем результаты, сначала точные совпадения, затем частичные
|
|
|
|
|
return [...startsWithMatch, ...containsMatch].slice(0, 15);
|
2025-05-22 01:03:48 +07:00
|
|
|
|
});
|
|
|
|
|
|
2025-05-22 01:09:12 +07:00
|
|
|
|
const selectCity = (city) => {
|
|
|
|
|
selectedCity.value = city;
|
|
|
|
|
formData.value.location.city = city.name;
|
|
|
|
|
citySearchQuery.value = city.name;
|
2025-05-22 01:03:48 +07:00
|
|
|
|
showCityList.value = false;
|
|
|
|
|
};
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
const clearCitySelection = () => {
|
|
|
|
|
selectedCity.value = null;
|
|
|
|
|
formData.value.location.city = '';
|
|
|
|
|
citySearchQuery.value = '';
|
|
|
|
|
};
|
2025-05-22 01:03:48 +07:00
|
|
|
|
|
2025-05-22 01:09:12 +07:00
|
|
|
|
// Улучшенная логика обработки кликов вне списка
|
2025-05-22 01:03:48 +07:00
|
|
|
|
const handleClickOutsideCityList = (event) => {
|
|
|
|
|
const cityInput = document.getElementById('editCity');
|
|
|
|
|
const cityDropdown = document.querySelector('.city-dropdown');
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
if (!cityInput || !event.target) return;
|
|
|
|
|
|
|
|
|
|
const clickedOnInput = cityInput.contains(event.target);
|
|
|
|
|
const clickedOnDropdown = cityDropdown && cityDropdown.contains(event.target);
|
|
|
|
|
|
|
|
|
|
if (showCityList.value && !clickedOnInput && !clickedOnDropdown) {
|
|
|
|
|
// Если есть выбранный город, восстанавливаем его имя в поле поиска
|
|
|
|
|
if (selectedCity.value) {
|
|
|
|
|
citySearchQuery.value = selectedCity.value.name;
|
|
|
|
|
}
|
2025-05-22 01:03:48 +07:00
|
|
|
|
showCityList.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
const initializeFormData = (currentUser) => {
|
|
|
|
|
console.log('[EditProfileForm] Инициализация формы с данными:', currentUser);
|
|
|
|
|
if (currentUser) {
|
|
|
|
|
formData.value.name = currentUser.name || '';
|
|
|
|
|
formData.value.bio = currentUser.bio || '';
|
|
|
|
|
formData.value.dateOfBirth = currentUser.dateOfBirth ? new Date(currentUser.dateOfBirth).toISOString().split('T')[0] : '';
|
|
|
|
|
formData.value.gender = currentUser.gender || '';
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
2025-05-22 01:03:48 +07:00
|
|
|
|
if (currentUser.location) {
|
|
|
|
|
formData.value.location.city = currentUser.location.city || '';
|
2025-05-22 01:09:12 +07:00
|
|
|
|
citySearchQuery.value = currentUser.location.city || '';
|
2025-05-22 01:03:48 +07:00
|
|
|
|
formData.value.location.country = currentUser.location.country || 'Россия';
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
// Если есть выбранный город, находим его в списке
|
|
|
|
|
if (currentUser.location.city) {
|
|
|
|
|
selectedCity.value = allCities.value.find(city =>
|
|
|
|
|
city.name === currentUser.location.city
|
|
|
|
|
) || null;
|
|
|
|
|
}
|
2025-05-22 01:03:48 +07:00
|
|
|
|
}
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-22 01:09:12 +07:00
|
|
|
|
onMounted(async () => {
|
2025-05-21 22:13:09 +07:00
|
|
|
|
console.log('[EditProfileForm] Компонент смонтирован');
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
// Загружаем список городов и сортируем по имени
|
|
|
|
|
allCities.value = russianCities.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
|
|
|
|
|
// Инициализируем данные формы
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
initializeFormData(user.value);
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-22 01:03:48 +07:00
|
|
|
|
document.addEventListener('click', handleClickOutsideCityList);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Добавляем onUnmounted для удаления слушателя
|
|
|
|
|
import { onUnmounted } from 'vue';
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
document.removeEventListener('click', handleClickOutsideCityList);
|
2025-05-21 22:13:09 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(user, (newUser) => {
|
|
|
|
|
console.log('[EditProfileForm] Пользователь изменился:', newUser);
|
|
|
|
|
initializeFormData(newUser);
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
console.log('[EditProfileForm] Отправка формы профиля...');
|
|
|
|
|
profileLoading.value = true;
|
|
|
|
|
profileErrorMessage.value = '';
|
|
|
|
|
profileSuccessMessage.value = '';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const dataToUpdate = {
|
|
|
|
|
name: formData.value.name,
|
|
|
|
|
bio: formData.value.bio,
|
|
|
|
|
dateOfBirth: formData.value.dateOfBirth || null,
|
|
|
|
|
gender: formData.value.gender || null,
|
2025-05-22 01:09:12 +07:00
|
|
|
|
location: {
|
2025-05-22 01:03:48 +07:00
|
|
|
|
city: formData.value.location.city,
|
|
|
|
|
country: formData.value.location.country
|
|
|
|
|
}
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('[EditProfileForm] Данные для обновления профиля:', dataToUpdate);
|
|
|
|
|
|
|
|
|
|
const response = await api.updateUserProfile(dataToUpdate);
|
|
|
|
|
console.log('[EditProfileForm] Ответ от сервера (профиль):', response.data);
|
|
|
|
|
|
|
|
|
|
profileSuccessMessage.value = response.data.message || 'Профиль успешно обновлен!';
|
|
|
|
|
|
|
|
|
|
await fetchUser();
|
|
|
|
|
console.log('[EditProfileForm] Данные пользователя обновлены (профиль)');
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[EditProfileForm] Ошибка при обновлении профиля:', err);
|
|
|
|
|
profileErrorMessage.value = (err.response && err.response.data && err.response.data.message)
|
|
|
|
|
? err.response.data.message
|
|
|
|
|
: 'Ошибка при обновлении профиля.';
|
|
|
|
|
} finally {
|
|
|
|
|
profileLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
// Вместо onFileSelected для одного файла, создадим onFilesSelected для нескольких
|
|
|
|
|
const onFilesSelected = (event) => {
|
|
|
|
|
const files = event.target.files;
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
selectedFiles.value = Array.from(files); // Сохраняем массив файлов
|
|
|
|
|
photoUploadMessages.value = selectedFiles.value.map(file => ({ // Инициализируем сообщения для каждого файла
|
|
|
|
|
name: file.name,
|
|
|
|
|
status: 'pending', // pending, uploading, success, error
|
|
|
|
|
message: 'Ожидает загрузки...'
|
|
|
|
|
}));
|
|
|
|
|
// Очищаем старые одиночные состояния, если они были
|
2025-05-21 22:13:09 +07:00
|
|
|
|
selectedFile.value = null;
|
|
|
|
|
previewUrl.value = null;
|
2025-05-21 23:43:24 +07:00
|
|
|
|
photoUploadErrorMessage.value = '';
|
|
|
|
|
photoUploadSuccessMessage.value = '';
|
|
|
|
|
} else {
|
|
|
|
|
selectedFiles.value = [];
|
|
|
|
|
photoUploadMessages.value = [];
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
// Обновляем handlePhotoUpload для загрузки нескольких файлов последовательно
|
|
|
|
|
const handleMultiplePhotoUpload = async () => {
|
|
|
|
|
if (selectedFiles.value.length === 0) {
|
|
|
|
|
photoUploadErrorMessage.value = 'Пожалуйста, выберите файлы для загрузки.';
|
|
|
|
|
// Очистим индивидуальные сообщения, если они были
|
|
|
|
|
photoUploadMessages.value.forEach(msg => {
|
|
|
|
|
if (msg.status === 'pending') msg.status = 'cancelled';
|
|
|
|
|
});
|
2025-05-21 22:13:09 +07:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
photoLoading.value = true; // Общий индикатор загрузки
|
|
|
|
|
photoUploadErrorMessage.value = ''; // Сбрасываем общую ошибку
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
for (let i = 0; i < selectedFiles.value.length; i++) {
|
|
|
|
|
const originalFile = selectedFiles.value[i];
|
|
|
|
|
const fileMessageIndex = photoUploadMessages.value.findIndex(m => m.name === originalFile.name && m.status !== 'success');
|
|
|
|
|
|
|
|
|
|
if (fileMessageIndex === -1) continue;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
photoUploadMessages.value[fileMessageIndex].status = 'uploading';
|
|
|
|
|
photoUploadMessages.value[fileMessageIndex].message = 'Сжатие и загрузка...';
|
2025-05-21 22:13:09 +07:00
|
|
|
|
|
2025-05-21 23:43:24 +07:00
|
|
|
|
try {
|
|
|
|
|
// Опции сжатия
|
|
|
|
|
const options = {
|
|
|
|
|
maxSizeMB: 1, // Максимальный размер файла в MB
|
|
|
|
|
maxWidthOrHeight: 1920, // Максимальная ширина или высота
|
|
|
|
|
useWebWorker: true, // Использовать Web Worker для лучшей производительности
|
|
|
|
|
// Дополнительные опции можно найти в документации библиотеки
|
|
|
|
|
// Например, initialQuality для JPEG
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[EditProfileForm] Сжатие файла ${originalFile.name}...`);
|
|
|
|
|
const compressedFile = await imageCompression(originalFile, options);
|
|
|
|
|
console.log(`[EditProfileForm] Файл ${originalFile.name} сжат. Оригинальный размер: ${(originalFile.size / 1024 / 1024).toFixed(2)} MB, Новый размер: ${(compressedFile.size / 1024 / 1024).toFixed(2)} MB`);
|
|
|
|
|
|
|
|
|
|
const fd = new FormData();
|
|
|
|
|
// Важно: сервер ожидает имя файла, поэтому передаем его как третий аргумент
|
|
|
|
|
fd.append('profilePhoto', compressedFile, originalFile.name);
|
|
|
|
|
|
|
|
|
|
console.log(`[EditProfileForm] Отправка файла ${originalFile.name} (сжатого) на сервер...`);
|
|
|
|
|
const response = await api.uploadUserProfilePhoto(fd);
|
|
|
|
|
console.log(`[EditProfileForm] Ответ от сервера (фото ${originalFile.name}):`, response.data);
|
|
|
|
|
|
|
|
|
|
photoUploadMessages.value[fileMessageIndex].status = 'success';
|
|
|
|
|
photoUploadMessages.value[fileMessageIndex].message = response.data.message || 'Фото успешно загружено!';
|
|
|
|
|
|
|
|
|
|
await fetchUser();
|
|
|
|
|
console.log(`[EditProfileForm] Данные пользователя обновлены (фото ${originalFile.name})`);
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`[EditProfileForm] Ошибка при сжатии или загрузке фото ${originalFile.name}:`, err);
|
|
|
|
|
photoUploadMessages.value[fileMessageIndex].status = 'error';
|
|
|
|
|
// Проверяем, является ли ошибка ошибкой сжатия
|
|
|
|
|
if (err instanceof Error && err.message.includes('compression')) {
|
|
|
|
|
photoUploadMessages.value[fileMessageIndex].message = `Ошибка при сжатии фото ${originalFile.name}.`;
|
|
|
|
|
} else {
|
|
|
|
|
photoUploadMessages.value[fileMessageIndex].message = (err.response && err.response.data && err.response.data.message)
|
|
|
|
|
? err.response.data.message
|
|
|
|
|
: `Ошибка при загрузке фото ${originalFile.name}.`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// После завершения всех загрузок
|
|
|
|
|
selectedFiles.value = []; // Очищаем выбранные файлы
|
|
|
|
|
// Не очищаем photoInput.value здесь, т.к. input type="file" multiple сам управляет списком
|
|
|
|
|
// Можно сбросить значение инпута, чтобы пользователь мог выбрать те же файлы снова, если захочет
|
|
|
|
|
const photoInput = document.getElementById('profilePhotos'); // Убедимся, что ID правильный
|
|
|
|
|
if (photoInput) {
|
|
|
|
|
photoInput.value = '';
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
2025-05-21 23:43:24 +07:00
|
|
|
|
photoLoading.value = false; // Выключаем общий индикатор
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-05-22 01:09:12 +07:00
|
|
|
|
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
.edit-profile-form {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
/* Стили для выбора города */
|
|
|
|
|
.city-select-container {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.city-dropdown {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 100%;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
background-color: white;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
border: 1px solid #dee2e6;
|
2025-05-22 01:09:12 +07:00
|
|
|
|
border-radius: 0 0 5px 5px;
|
|
|
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
|
|
|
max-height: 350px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
.search-results-info {
|
|
|
|
|
padding: 8px 12px;
|
2025-05-21 23:43:24 +07:00
|
|
|
|
font-size: 0.9em;
|
2025-05-22 01:09:12 +07:00
|
|
|
|
color: #6c757d;
|
|
|
|
|
border-bottom: 1px solid #dee2e6;
|
|
|
|
|
background-color: #f8f9fa;
|
2025-05-21 23:43:24 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
.city-list-container {
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
padding: 5px 0;
|
2025-05-21 23:43:24 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
.city-item {
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
2025-05-21 23:43:24 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
.city-item:hover {
|
|
|
|
|
background-color: #e9ecef;
|
2025-05-21 23:43:24 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
.city-name {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.city-region {
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selected-city {
|
|
|
|
|
color: #495057;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-results {
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 1px solid #dee2e6;
|
|
|
|
|
border-top: none;
|
|
|
|
|
border-radius: 0 0 5px 5px;
|
|
|
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
|
|
|
background-color: white;
|
2025-05-22 01:03:48 +07:00
|
|
|
|
position: absolute;
|
2025-05-22 01:09:12 +07:00
|
|
|
|
top: 100%;
|
2025-05-22 01:03:48 +07:00
|
|
|
|
left: 0;
|
2025-05-22 01:09:12 +07:00
|
|
|
|
right: 0;
|
2025-05-22 01:03:48 +07:00
|
|
|
|
z-index: 1000;
|
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
/* Кнопка очистки */
|
|
|
|
|
.input-group-text {
|
2025-05-22 01:03:48 +07:00
|
|
|
|
cursor: pointer;
|
2025-05-22 01:09:12 +07:00
|
|
|
|
background-color: transparent;
|
2025-05-22 01:03:48 +07:00
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
.input-group-text:hover {
|
2025-05-22 01:03:48 +07:00
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
}
|
2025-05-22 01:09:12 +07:00
|
|
|
|
|
|
|
|
|
/* ...existing code... */
|
2025-05-21 22:13:09 +07:00
|
|
|
|
</style>
|